From 3b9298ed2b1b2567ccb36a494a21f42d5bcf7946 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Wed, 3 Apr 2024 11:48:26 -0600 Subject: [PATCH 001/125] Feature/dependency autoconfig (#2588) * dependency autoconfig * FE portion --------- Co-authored-by: Aiden McClelland --- core/models/src/procedure_name.rs | 6 +- .../startos/bindings/CurrentDependencyInfo.ts | 1 + .../bindings/DependencyConfigErrors.ts | 4 - core/startos/bindings/Status.ts | 7 +- core/startos/bindings/index.ts | 1 - core/startos/src/context/rpc.rs | 25 ++---- core/startos/src/db/model/package.rs | 1 + core/startos/src/dependencies.rs | 27 +++--- core/startos/src/service/action.rs | 37 ++++++++ core/startos/src/service/config.rs | 51 +++++++++-- core/startos/src/service/control.rs | 2 - core/startos/src/service/dependencies.rs | 61 +++++++++++++ core/startos/src/service/mod.rs | 43 +-------- core/startos/src/service/properties.rs | 21 +++++ .../src/service/service_effect_handler.rs | 89 +++++++++++-------- core/startos/src/service/service_map.rs | 1 - .../startos/src/service/transition/restart.rs | 4 +- core/startos/src/status/mod.rs | 18 ---- core/startos/src/util/actor.rs | 7 +- sdk/lib/types.ts | 2 +- .../ui/src/app/services/api/api.fixures.ts | 8 +- .../ui/src/app/services/api/mock-patch.ts | 6 +- .../ui/src/app/services/dep-error.service.ts | 10 +-- 23 files changed, 262 insertions(+), 170 deletions(-) delete mode 100644 core/startos/bindings/DependencyConfigErrors.ts create mode 100644 core/startos/src/service/action.rs create mode 100644 core/startos/src/service/dependencies.rs create mode 100644 core/startos/src/service/properties.rs diff --git a/core/models/src/procedure_name.rs b/core/models/src/procedure_name.rs index c42068be3..c8ae8c3a8 100644 --- a/core/models/src/procedure_name.rs +++ b/core/models/src/procedure_name.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::ActionId; +use crate::{ActionId, PackageId}; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ProcedureName { @@ -14,8 +14,8 @@ pub enum ProcedureName { ActionMetadata, RunAction(ActionId), GetAction(ActionId), - QueryDependency(ActionId), - UpdateDependency(ActionId), + QueryDependency(PackageId), + UpdateDependency(PackageId), Init, Uninit, } diff --git a/core/startos/bindings/CurrentDependencyInfo.ts b/core/startos/bindings/CurrentDependencyInfo.ts index 10112e312..8b2b0100d 100644 --- a/core/startos/bindings/CurrentDependencyInfo.ts +++ b/core/startos/bindings/CurrentDependencyInfo.ts @@ -6,4 +6,5 @@ export type CurrentDependencyInfo = { icon: DataUrl; registryUrl: string; versionSpec: string; + configSatisfied: boolean; } & ({ kind: "exists" } | { kind: "running"; healthChecks: string[] }); diff --git a/core/startos/bindings/DependencyConfigErrors.ts b/core/startos/bindings/DependencyConfigErrors.ts deleted file mode 100644 index 5f2246fa9..000000000 --- a/core/startos/bindings/DependencyConfigErrors.ts +++ /dev/null @@ -1,4 +0,0 @@ -// 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"; - -export type DependencyConfigErrors = { [key: PackageId]: string }; diff --git a/core/startos/bindings/Status.ts b/core/startos/bindings/Status.ts index e23c5b4be..6b240347d 100644 --- a/core/startos/bindings/Status.ts +++ b/core/startos/bindings/Status.ts @@ -1,9 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DependencyConfigErrors } from "./DependencyConfigErrors"; import type { MainStatus } from "./MainStatus"; -export type Status = { - configured: boolean; - main: MainStatus; - dependencyConfigErrors: DependencyConfigErrors; -}; +export type Status = { configured: boolean; main: MainStatus }; diff --git a/core/startos/bindings/index.ts b/core/startos/bindings/index.ts index 4ce0ab643..fd422a2f5 100644 --- a/core/startos/bindings/index.ts +++ b/core/startos/bindings/index.ts @@ -18,7 +18,6 @@ export { CurrentDependencies } from "./CurrentDependencies"; export { CurrentDependencyInfo } from "./CurrentDependencyInfo"; export { DataUrl } from "./DataUrl"; export { Dependencies } from "./Dependencies"; -export { DependencyConfigErrors } from "./DependencyConfigErrors"; export { DependencyKind } from "./DependencyKind"; export { DependencyRequirement } from "./DependencyRequirement"; export { DepInfo } from "./DepInfo"; diff --git a/core/startos/src/context/rpc.rs b/core/startos/src/context/rpc.rs index 6450eb561..a51e23b08 100644 --- a/core/startos/src/context/rpc.rs +++ b/core/startos/src/context/rpc.rs @@ -209,33 +209,24 @@ impl RpcContext { self.services.init(&self).await?; tracing::info!("Initialized Package Managers"); - let mut all_dependency_config_errs = BTreeMap::new(); + let mut updated_current_dependents = BTreeMap::new(); let peek = self.db.peek().await; for (package_id, package) in peek.as_public().as_package_data().as_entries()?.into_iter() { let package = package.clone(); - let current_dependencies = package.as_current_dependencies().de()?; - all_dependency_config_errs.insert( - package_id.clone(), - compute_dependency_config_errs( - self, - &peek, - &package_id, - ¤t_dependencies, - &Default::default(), - ) - .await?, - ); + let mut current_dependencies = package.as_current_dependencies().de()?; + compute_dependency_config_errs(self, &package_id, &mut current_dependencies).await?; + updated_current_dependents.insert(package_id.clone(), current_dependencies); } self.db .mutate(|v| { - for (package_id, errs) in all_dependency_config_errs { - if let Some(config_errors) = v + for (package_id, deps) in updated_current_dependents { + if let Some(model) = v .as_public_mut() .as_package_data_mut() .as_idx_mut(&package_id) - .map(|i| i.as_status_mut().as_dependency_config_errors_mut()) + .map(|i| i.as_current_dependencies_mut()) { - config_errors.ser(&errs)?; + model.ser(&deps)?; } } Ok(()) diff --git a/core/startos/src/db/model/package.rs b/core/startos/src/db/model/package.rs index 83a35d086..29b7bb90f 100644 --- a/core/startos/src/db/model/package.rs +++ b/core/startos/src/db/model/package.rs @@ -380,6 +380,7 @@ pub struct CurrentDependencyInfo { pub registry_url: Url, #[ts(type = "string")] pub version_spec: VersionRange, + pub config_satisfied: bool, } #[derive(Clone, Debug, Deserialize, Serialize, TS)] diff --git a/core/startos/src/dependencies.rs b/core/startos/src/dependencies.rs index 69e4cad59..61f5af2b4 100644 --- a/core/startos/src/dependencies.rs +++ b/core/startos/src/dependencies.rs @@ -12,7 +12,6 @@ use crate::config::{Config, ConfigSpec, ConfigureContext}; use crate::context::RpcContext; use crate::db::model::package::CurrentDependencies; use crate::prelude::*; -use crate::status::DependencyConfigErrors; use crate::Error; pub fn dependency() -> ParentHandler { @@ -180,17 +179,23 @@ pub async fn configure_logic( #[instrument(skip_all)] pub async fn compute_dependency_config_errs( ctx: &RpcContext, - db: &Peeked, id: &PackageId, - current_dependencies: &CurrentDependencies, - dependency_config: &BTreeMap, -) -> Result { - let mut dependency_config_errs = BTreeMap::new(); - for (dependency, _dep_info) in current_dependencies.0.iter() { + current_dependencies: &mut CurrentDependencies, +) -> Result<(), Error> { + let service_guard = ctx.services.get(id).await; + let service = service_guard.as_ref().or_not_found(id)?; + for (dep_id, dep_info) in current_dependencies.0.iter_mut() { // check if config passes dependency check - if let Some(error) = todo!() { - dependency_config_errs.insert(dependency.clone(), error); - } + let Some(dependency) = &*ctx.services.get(dep_id).await else { + continue; + }; + + let dep_config = dependency.get_config().await?.config; + + dep_info.config_satisfied = service + .dependency_config(dep_id.clone(), dep_config) + .await? + .is_none(); } - Ok(DependencyConfigErrors(dependency_config_errs)) + Ok(()) } diff --git a/core/startos/src/service/action.rs b/core/startos/src/service/action.rs new file mode 100644 index 000000000..a33869222 --- /dev/null +++ b/core/startos/src/service/action.rs @@ -0,0 +1,37 @@ +use std::time::Duration; + +use models::{ActionId, ProcedureName}; + +use crate::action::ActionResult; +use crate::prelude::*; +use crate::service::{Service, ServiceActor}; +use crate::util::actor::{BackgroundJobs, Handler}; + +struct Action { + id: ActionId, + input: Value, +} +impl Handler for ServiceActor { + type Response = Result; + async fn handle( + &mut self, + Action { id, input }: Action, + _: &mut BackgroundJobs, + ) -> Self::Response { + let container = &self.0.persistent_container; + container + .execute::( + ProcedureName::RunAction(id), + input, + Some(Duration::from_secs(30)), + ) + .await + .with_kind(ErrorKind::Action) + } +} + +impl Service { + pub async fn action(&self, id: ActionId, input: Value) -> Result { + self.actor.send(Action { id, input }).await? + } +} diff --git a/core/startos/src/service/config.rs b/core/startos/src/service/config.rs index e1294a465..df3e5b046 100644 --- a/core/startos/src/service/config.rs +++ b/core/startos/src/service/config.rs @@ -1,19 +1,52 @@ +use std::time::Duration; + use models::ProcedureName; +use crate::config::action::ConfigRes; use crate::config::ConfigureContext; use crate::prelude::*; -use crate::service::Service; +use crate::service::{Service, ServiceActor}; +use crate::util::actor::{BackgroundJobs, Handler}; +use crate::util::serde::NoOutput; -impl Service { - pub async fn configure( - &self, - ConfigureContext { timeout, config }: ConfigureContext, - ) -> Result<(), Error> { - let container = &self.seed.persistent_container; +struct Configure(ConfigureContext); +impl Handler for ServiceActor { + type Response = Result<(), Error>; + async fn handle( + &mut self, + Configure(ConfigureContext { timeout, config }): Configure, + _: &mut BackgroundJobs, + ) -> Self::Response { + let container = &self.0.persistent_container; container - .execute::(ProcedureName::SetConfig, to_value(&config)?, timeout) + .execute::(ProcedureName::SetConfig, to_value(&config)?, timeout) .await - .with_kind(ErrorKind::Action)?; + .with_kind(ErrorKind::ConfigRulesViolation)?; Ok(()) } } + +struct GetConfig; +impl Handler for ServiceActor { + type Response = Result; + async fn handle(&mut self, _: GetConfig, _: &mut BackgroundJobs) -> Self::Response { + let container = &self.0.persistent_container; + container + .execute::( + ProcedureName::GetConfig, + Value::Null, + Some(Duration::from_secs(30)), // TODO timeout + ) + .await + .with_kind(ErrorKind::ConfigGen) + } +} + +impl Service { + pub async fn configure(&self, ctx: ConfigureContext) -> Result<(), Error> { + self.actor.send(Configure(ctx)).await? + } + pub async fn get_config(&self) -> Result { + self.actor.send(GetConfig).await? + } +} diff --git a/core/startos/src/service/control.rs b/core/startos/src/service/control.rs index 88d66d97c..5ef0bbff2 100644 --- a/core/startos/src/service/control.rs +++ b/core/startos/src/service/control.rs @@ -5,7 +5,6 @@ use crate::service::{Service, ServiceActor}; use crate::util::actor::{BackgroundJobs, Handler}; struct Start; -#[async_trait::async_trait] impl Handler for ServiceActor { type Response = (); async fn handle(&mut self, _: Start, _: &mut BackgroundJobs) -> Self::Response { @@ -22,7 +21,6 @@ impl Service { } struct Stop; -#[async_trait::async_trait] impl Handler for ServiceActor { type Response = (); async fn handle(&mut self, _: Stop, _: &mut BackgroundJobs) -> Self::Response { diff --git a/core/startos/src/service/dependencies.rs b/core/startos/src/service/dependencies.rs new file mode 100644 index 000000000..67cc5d53f --- /dev/null +++ b/core/startos/src/service/dependencies.rs @@ -0,0 +1,61 @@ +use std::time::Duration; + +use imbl_value::json; +use models::{PackageId, ProcedureName}; + +use crate::prelude::*; +use crate::service::{Service, ServiceActor}; +use crate::util::actor::{BackgroundJobs, Handler}; +use crate::Config; + +struct DependencyConfig { + dependency_id: PackageId, + remote_config: Option, +} +impl Handler for ServiceActor { + type Response = Result, Error>; + async fn handle( + &mut self, + DependencyConfig { + dependency_id, + remote_config, + }: DependencyConfig, + _: &mut BackgroundJobs, + ) -> Self::Response { + let container = &self.0.persistent_container; + container + .sanboxed::>( + ProcedureName::UpdateDependency(dependency_id.clone()), + json!({ + "queryResults": container + .execute::( + ProcedureName::QueryDependency(dependency_id), + Value::Null, + Some(Duration::from_secs(30)), + ) + .await + .with_kind(ErrorKind::Dependency)?, + "remoteConfig": remote_config, + }), + Some(Duration::from_secs(30)), + ) + .await + .with_kind(ErrorKind::Dependency) + .map(|res| res.filter(|c| !c.is_empty())) + } +} + +impl Service { + pub async fn dependency_config( + &self, + dependency_id: PackageId, + remote_config: Option, + ) -> Result, Error> { + self.actor + .send(DependencyConfig { + dependency_id, + remote_config, + }) + .await? + } +} diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index 95d507bfa..2b430dc73 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -5,7 +5,7 @@ use chrono::{DateTime, Utc}; use clap::Parser; use futures::future::BoxFuture; use imbl::OrdMap; -use models::{ActionId, HealthCheckId, PackageId, ProcedureName}; +use models::{HealthCheckId, PackageId, ProcedureName}; use persistent_container::PersistentContainer; use rpc_toolkit::{from_fn_async, CallRemoteHandler, Empty, Handler, HandlerArgs}; use serde::{Deserialize, Serialize}; @@ -13,7 +13,6 @@ use start_stop::StartStop; use tokio::sync::Notify; use ts_rs::TS; -use crate::action::ActionResult; use crate::config::action::ConfigRes; use crate::context::{CliContext, RpcContext}; use crate::core::rpc_continuations::RequestGuid; @@ -33,10 +32,13 @@ use crate::util::actor::{Actor, BackgroundJobs, SimpleActor}; use crate::util::serde::Pem; use crate::volume::data_dir; +mod action; pub mod cli; mod config; mod control; +mod dependencies; pub mod persistent_container; +mod properties; mod rpc; pub mod service_effect_handler; pub mod service_map; @@ -302,43 +304,6 @@ impl Service { Err(Error::new(eyre!("not yet implemented"), ErrorKind::Unknown)) } - pub async fn get_config(&self) -> Result { - let container = &self.seed.persistent_container; - container - .execute::( - ProcedureName::GetConfig, - Value::Null, - Some(Duration::from_secs(30)), // TODO timeout - ) - .await - .with_kind(ErrorKind::ConfigGen) - } - - // TODO DO the Action Get - - pub async fn action(&self, id: ActionId, input: Value) -> Result { - let container = &self.seed.persistent_container; - container - .execute::( - ProcedureName::RunAction(id), - input, - Some(Duration::from_secs(30)), - ) - .await - .with_kind(ErrorKind::Action) - } - pub async fn properties(&self) -> Result { - let container = &self.seed.persistent_container; - container - .execute::( - ProcedureName::Properties, - Value::Null, - Some(Duration::from_secs(30)), - ) - .await - .with_kind(ErrorKind::Unknown) - } - pub async fn shutdown(self) -> Result<(), Error> { self.actor .shutdown(crate::util::actor::PendingMessageStrategy::FinishAll { timeout: None }) // TODO timeout diff --git a/core/startos/src/service/properties.rs b/core/startos/src/service/properties.rs new file mode 100644 index 000000000..3e795207a --- /dev/null +++ b/core/startos/src/service/properties.rs @@ -0,0 +1,21 @@ +use std::time::Duration; + +use models::ProcedureName; + +use crate::prelude::*; +use crate::service::Service; + +impl Service { + // TODO: leave here or switch to Actor Message? + pub async fn properties(&self) -> Result { + let container = &self.seed.persistent_container; + container + .execute::( + ProcedureName::Properties, + Value::Null, + Some(Duration::from_secs(30)), + ) + .await + .with_kind(ErrorKind::Unknown) + } +} diff --git a/core/startos/src/service/service_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs index 4041cbb98..210c3d07c 100644 --- a/core/startos/src/service/service_effect_handler.rs +++ b/core/startos/src/service/service_effect_handler.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use std::ffi::OsString; use std::net::Ipv4Addr; use std::os::unix::process::CommandExt; @@ -1120,51 +1120,64 @@ async fn set_dependencies( ) -> Result<(), Error> { let ctx = ctx.deref()?; let id = &ctx.id; + let service_guard = ctx.ctx.services.get(id).await; + let service = service_guard.as_ref().or_not_found(id)?; + let mut deps = BTreeMap::new(); + for dependency in dependencies { + let (dep_id, kind, registry_url, version_spec) = match dependency { + DependencyRequirement::Exists { + id, + registry_url, + version_spec, + } => ( + id, + CurrentDependencyKind::Exists, + registry_url, + version_spec, + ), + DependencyRequirement::Running { + id, + health_checks, + registry_url, + version_spec, + } => ( + id, + CurrentDependencyKind::Running { health_checks }, + registry_url, + version_spec, + ), + }; + let icon = todo!(); + let title = todo!(); + let config_satisfied = if let Some(dep_service) = &*ctx.ctx.services.get(&dep_id).await { + service + .dependency_config(dep_id, dep_service.get_config().await?.config) + .await? + .is_none() + } else { + true + }; + deps.insert( + dep_id, + CurrentDependencyInfo { + kind: CurrentDependencyKind::Exists, + registry_url, + version_spec, + icon, + title, + config_satisfied, + }, + ); + } ctx.ctx .db .mutate(|db| { - let dependencies = CurrentDependencies( - dependencies - .into_iter() - .map(|dependency| match dependency { - DependencyRequirement::Exists { - id, - registry_url, - version_spec, - } => ( - id, - CurrentDependencyInfo { - kind: CurrentDependencyKind::Exists, - registry_url, - version_spec, - icon: todo!(), - title: todo!(), - }, - ), - DependencyRequirement::Running { - id, - health_checks, - registry_url, - version_spec, - } => ( - id, - CurrentDependencyInfo { - kind: CurrentDependencyKind::Running { health_checks }, - registry_url, - version_spec, - icon: todo!(), - title: todo!(), - }, - ), - }) - .collect(), - ); db.as_public_mut() .as_package_data_mut() .as_idx_mut(id) .or_not_found(id)? .as_current_dependencies_mut() - .ser(&dependencies) + .ser(&CurrentDependencies(deps)) }) .await } diff --git a/core/startos/src/service/service_map.rs b/core/startos/src/service/service_map.rs index 934497eb9..68e1fc8a8 100644 --- a/core/startos/src/service/service_map.rs +++ b/core/startos/src/service/service_map.rs @@ -165,7 +165,6 @@ impl ServiceMap { status: Status { configured: false, main: MainStatus::Stopped, - dependency_config_errors: Default::default(), }, marketplace_url: None, developer_key: Pem::new(developer_key), diff --git a/core/startos/src/service/transition/restart.rs b/core/startos/src/service/transition/restart.rs index 9c82d0282..7047bd18f 100644 --- a/core/startos/src/service/transition/restart.rs +++ b/core/startos/src/service/transition/restart.rs @@ -2,16 +2,14 @@ use std::sync::Arc; use futures::FutureExt; +use super::TempDesiredState; use crate::prelude::*; use crate::service::transition::{TransitionKind, TransitionState}; use crate::service::{Service, ServiceActor}; use crate::util::actor::{BackgroundJobs, Handler}; use crate::util::future::RemoteCancellable; -use super::TempDesiredState; - struct Restart; -#[async_trait::async_trait] impl Handler for ServiceActor { type Response = (); async fn handle(&mut self, _: Restart, jobs: &mut BackgroundJobs) -> Self::Response { diff --git a/core/startos/src/status/mod.rs b/core/startos/src/status/mod.rs index 2faa90e79..645c082f4 100644 --- a/core/startos/src/status/mod.rs +++ b/core/startos/src/status/mod.rs @@ -2,7 +2,6 @@ use std::collections::BTreeMap; use chrono::{DateTime, Utc}; use imbl::OrdMap; -use models::PackageId; use serde::{Deserialize, Serialize}; use ts_rs::TS; @@ -18,23 +17,6 @@ pub mod health_check; pub struct Status { pub configured: bool, pub main: MainStatus, - #[serde(default)] - pub dependency_config_errors: DependencyConfigErrors, -} - -#[derive(Clone, Debug, Deserialize, Serialize, HasModel, Default, TS)] -#[model = "Model"] -#[ts(export)] -pub struct DependencyConfigErrors(pub BTreeMap); -impl Map for DependencyConfigErrors { - type Key = PackageId; - type Value = String; - fn key_str(key: &Self::Key) -> Result, Error> { - Ok(key) - } - fn key_string(key: &Self::Key) -> Result { - Ok(key.clone().into()) - } } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, TS)] diff --git a/core/startos/src/util/actor.rs b/core/startos/src/util/actor.rs index 8e08a1d53..caa83d8b1 100644 --- a/core/startos/src/util/actor.rs +++ b/core/startos/src/util/actor.rs @@ -16,10 +16,13 @@ pub trait Actor: Send + 'static { fn init(&mut self, jobs: &mut BackgroundJobs) {} } -#[async_trait::async_trait] pub trait Handler: Actor { type Response: Any + Send; - async fn handle(&mut self, msg: M, jobs: &mut BackgroundJobs) -> Self::Response; + fn handle( + &mut self, + msg: M, + jobs: &mut BackgroundJobs, + ) -> impl Future + Send; } #[async_trait::async_trait] diff --git a/sdk/lib/types.ts b/sdk/lib/types.ts index c801d56cd..b921a40af 100644 --- a/sdk/lib/types.ts +++ b/sdk/lib/types.ts @@ -110,7 +110,7 @@ export type VersionString = string */ export type DependencyConfig = { /** During autoconfigure, we have access to effects and local data. We are going to figure out all the data that we need and send it to update. For the sdk it is the desired delta */ - query(options: { effects: Effects; localConfig: unknown }): Promise + query(options: { effects: Effects }): Promise /** This is the second part. Given the query results off the previous function, we will determine what to change the remote config to. In our sdk normall we are going to use the previous as a deep merge. */ update(options: { queryResults: unknown diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index f06980f20..7933109b3 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -1410,7 +1410,6 @@ export module Mock { started: new Date().toISOString(), health: {}, }, - dependencyConfigErrors: {}, }, actions: {}, // @TODO need mocks serviceInterfaces: { @@ -1650,7 +1649,6 @@ export module Mock { main: { status: 'stopped', }, - dependencyConfigErrors: {}, }, actions: {}, serviceInterfaces: { @@ -1770,6 +1768,7 @@ export module Mock { registryUrl: '', versionSpec: '>=26.0.0', healthChecks: [], + configSatisfied: true, }, }, hosts: {}, @@ -1790,9 +1789,6 @@ export module Mock { main: { status: 'stopped', }, - dependencyConfigErrors: { - 'btc-rpc-proxy': 'Username not found', - }, }, actions: {}, serviceInterfaces: { @@ -2015,6 +2011,7 @@ export module Mock { registryUrl: 'https://registry.start9.com', versionSpec: '>=26.0.0', healthChecks: [], + configSatisfied: true, }, 'btc-rpc-proxy': { title: Mock.MockManifestBitcoinProxy.title, @@ -2022,6 +2019,7 @@ export module Mock { kind: 'exists', registryUrl: 'https://community-registry.start9.com', versionSpec: '>2.0.0', // @TODO + configSatisfied: false, }, }, hosts: {}, diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 00c200440..8a3f714d1 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -125,7 +125,6 @@ export const mockPatchData: DataModel = { }, }, }, - dependencyConfigErrors: {}, }, actions: {}, // @TODO serviceInterfaces: { @@ -367,9 +366,6 @@ export const mockPatchData: DataModel = { main: { status: 'stopped', }, - dependencyConfigErrors: { - 'btc-rpc-proxy': 'This is a config unsatisfied error', - }, }, actions: {}, serviceInterfaces: { @@ -590,6 +586,7 @@ export const mockPatchData: DataModel = { registryUrl: 'https://registry.start9.com', versionSpec: '>=26.0.0', healthChecks: [], + configSatisfied: true, }, 'btc-rpc-proxy': { title: 'Bitcoin Proxy', @@ -598,6 +595,7 @@ export const mockPatchData: DataModel = { registryUrl: 'https://community-registry.start9.com', versionSpec: '>2.0.0', healthChecks: [], + configSatisfied: false, }, }, hosts: {}, diff --git a/web/projects/ui/src/app/services/dep-error.service.ts b/web/projects/ui/src/app/services/dep-error.service.ts index 987499a5d..5abee0d6b 100644 --- a/web/projects/ui/src/app/services/dep-error.service.ts +++ b/web/projects/ui/src/app/services/dep-error.service.ts @@ -83,20 +83,20 @@ export class DepErrorService { } } - const versionSpec = pkg.currentDependencies[depId].versionSpec + const currentDep = pkg.currentDependencies[depId] const depManifest = dep.stateInfo.manifest // incorrect version - if (!this.emver.satisfies(depManifest.version, versionSpec)) { + if (!this.emver.satisfies(depManifest.version, currentDep.versionSpec)) { return { type: 'incorrectVersion', - expected: versionSpec, + expected: currentDep.versionSpec, received: depManifest.version, } } // invalid config - if (Object.values(pkg.status.dependencyConfigErrors).some(err => !!err)) { + if (!currentDep.configSatisfied) { return { type: 'configUnsatisfied', } @@ -111,8 +111,6 @@ export class DepErrorService { } } - const currentDep = pkg.currentDependencies[depId] - // health check failure if (depStatus === 'running' && currentDep.kind === 'running') { for (let id of currentDep.healthChecks) { From 6bc8027644dd5a0bd99952a9e2bd21c854333d0b Mon Sep 17 00:00:00 2001 From: Jade <2364004+Blu-J@users.noreply.github.com> Date: Thu, 4 Apr 2024 09:09:59 -0600 Subject: [PATCH 002/125] Feat/implement rest of poly effects (#2587) * feat: Add the implementation of the rest of the polyfillEffects * chore: Add in the rsync * chore: Add in the changes needed to indicate that the service does not need config * fix: Vaultwarden sets, starts, stops, uninstalls * chore: Update the polyFilleffect and add two more * Update MainLoop.ts * chore: Add in the set config of the deps on the config set --- .../Systems/SystemForEmbassy/MainLoop.ts | 14 +- .../Systems/SystemForEmbassy/index.ts | 129 ++++++++++--- .../SystemForEmbassy/oldEmbassyTypes.ts | 10 -- .../SystemForEmbassy/polyfillEffects.ts | 169 +++++++++++++++--- container-runtime/update-image.sh | 2 +- core/startos/src/service/config.rs | 19 +- core/startos/src/service/mod.rs | 3 + sdk/lib/types.ts | 7 +- 8 files changed, 272 insertions(+), 81 deletions(-) diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts index 17fd13468..f2bcc5eba 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts @@ -48,19 +48,7 @@ export class MainLoop { this.system.manifest.volumes, ) if (jsMain) { - const daemons = Daemons.of({ - effects, - started: async (_) => {}, - healthReceipts: [], - }) - throw new Error("todo") - // return { - // daemon, - // wait: daemon.wait().finally(() => { - // this.clean() - // effects.setMainStatus({ status: "stopped" }) - // }), - // } + throw new Error("Unreachable") } const daemon = await daemons.runDaemon()( this.effects, diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index ade166eff..a41047ace 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -25,6 +25,7 @@ import { anyOf, deferred, Parser, + array, } from "ts-matches" import { HostSystemStartOs } from "../../HostSystemStartOs" import { JsonPath, unNestPath } from "../../../Models/JsonPath" @@ -41,6 +42,48 @@ const MANIFEST_LOCATION = "/usr/lib/startos/package/embassyManifest.json" const EMBASSY_JS_LOCATION = "/usr/lib/startos/package/embassy.js" const EMBASSY_POINTER_PATH_PREFIX = "/embassyConfig" +const matchSetResult = object( + { + "depends-on": dictionary([string, array(string)]), + dependsOn: dictionary([string, array(string)]), + signal: literals( + "SIGTERM", + "SIGHUP", + "SIGINT", + "SIGQUIT", + "SIGILL", + "SIGTRAP", + "SIGABRT", + "SIGBUS", + "SIGFPE", + "SIGKILL", + "SIGUSR1", + "SIGSEGV", + "SIGUSR2", + "SIGPIPE", + "SIGALRM", + "SIGSTKFLT", + "SIGCHLD", + "SIGCONT", + "SIGSTOP", + "SIGTSTP", + "SIGTTIN", + "SIGTTOU", + "SIGURG", + "SIGXCPU", + "SIGXFSZ", + "SIGVTALRM", + "SIGPROF", + "SIGWINCH", + "SIGIO", + "SIGPWR", + "SIGSYS", + "SIGINFO", + ), + }, + ["depends-on", "dependsOn"], +) + export type PackagePropertiesV2 = { [name: string]: PackagePropertyObject | PackagePropertyString } @@ -120,6 +163,7 @@ const matchProperties = object({ data: matchPackageProperties, }) +const DEFAULT_REGISTRY = "https://registry.start9.com" export class SystemForEmbassy implements System { currentRunning: MainLoop | undefined static async of(manifestLocation: string = MANIFEST_LOCATION) { @@ -383,7 +427,7 @@ export class SystemForEmbassy implements System { private async setConfig( effects: HostSystemStartOs, newConfigWithoutPointers: unknown, - ): Promise { + ): Promise { const newConfig = structuredClone(newConfigWithoutPointers) await updateConfig( effects, @@ -391,45 +435,76 @@ export class SystemForEmbassy implements System { newConfig, ) const setConfigValue = this.manifest.config?.set - if (!setConfigValue) return { signal: "SIGTERM", "depends-on": {} } + if (!setConfigValue) return if (setConfigValue.type === "docker") { const container = await DockerProcedureContainer.of( effects, setConfigValue, this.manifest.volumes, ) - return JSON.parse( - ( - await container.exec([ - setConfigValue.entrypoint, - ...setConfigValue.args, - JSON.stringify(newConfig), - ]) - ).stdout.toString(), + const answer = matchSetResult.unsafeCast( + JSON.parse( + ( + await container.exec([ + setConfigValue.entrypoint, + ...setConfigValue.args, + JSON.stringify(newConfig), + ]) + ).stdout.toString(), + ), ) + const dependsOn = answer["depends-on"] ?? answer.dependsOn ?? {} + await this.setConfigSetConfig(effects, dependsOn) + return } else if (setConfigValue.type === "script") { const moduleCode = await this.moduleCode const method = moduleCode.setConfig if (!method) throw new Error("Expecting that the method setConfig exists") - return await method( - new PolyfillEffects(effects, this.manifest), - newConfig as U.Config, - ).then((x): T.SetResult => { - if ("result" in x) - return { - "depends-on": x.result["depends-on"], - signal: x.result.signal === "SIGEMT" ? "SIGTERM" : x.result.signal, - } - if ("error" in x) throw new Error("Error getting config: " + x.error) - throw new Error("Error getting config: " + x["error-code"][1]) - }) - } else { - return { - "depends-on": {}, - signal: "SIGTERM", - } + + const answer = matchSetResult.unsafeCast( + await method( + new PolyfillEffects(effects, this.manifest), + newConfig as U.Config, + ).then((x): T.SetResult => { + if ("result" in x) + return { + dependsOn: x.result["depends-on"], + signal: + x.result.signal === "SIGEMT" ? "SIGTERM" : x.result.signal, + } + if ("error" in x) throw new Error("Error getting config: " + x.error) + throw new Error("Error getting config: " + x["error-code"][1]) + }), + ) + const dependsOn = answer["depends-on"] ?? answer.dependsOn ?? {} + await this.setConfigSetConfig(effects, dependsOn) + return } } + private async setConfigSetConfig( + effects: HostSystemStartOs, + dependsOn: { [x: string]: readonly string[] }, + ) { + await effects.setDependencies({ + dependencies: Object.entries(dependsOn).flatMap(([key, value]) => { + const dependency = this.manifest.dependencies?.[key] + if (!dependency) return [] + const versionSpec = dependency.version + const registryUrl = DEFAULT_REGISTRY + const kind = "running" + return [ + { + id: key, + versionSpec, + registryUrl, + kind, + healthChecks: [...value], + }, + ] + }), + }) + } + private async migration( effects: HostSystemStartOs, fromVersion: string, diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/oldEmbassyTypes.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/oldEmbassyTypes.ts index 072a1171c..35b95e095 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/oldEmbassyTypes.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/oldEmbassyTypes.ts @@ -100,16 +100,6 @@ export type Effects = { is_sandboxed(): boolean exists(input: { volumeId: string; path: string }): Promise - bindLocal(options: { - internalPort: number - name: string - externalPort: number - }): Promise - bindTor(options: { - internalPort: number - name: string - externalPort: number - }): Promise fetch( url: string, diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts index 4c176b2f7..ab6cb1c64 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts @@ -3,12 +3,11 @@ import * as oet from "./oldEmbassyTypes" import { Volume } from "../../../Models/Volume" import * as child_process from "child_process" import { promisify } from "util" -import { Daemons, startSdk, T } from "@start9labs/start-sdk" +import { daemons, startSdk, T } from "@start9labs/start-sdk" import { HostSystemStartOs } from "../../HostSystemStartOs" import "isomorphic-fetch" import { Manifest } from "./matchManifest" - -const execFile = promisify(child_process.execFile) +import { DockerProcedureContainer } from "./DockerProcedureContainer" export class PolyfillEffects implements oet.Effects { constructor( @@ -111,17 +110,100 @@ export class PolyfillEffects implements oet.Effects { wait(): Promise> term(): Promise } { - throw new Error("Method not implemented.") + const dockerProcedureContainer = DockerProcedureContainer.of( + this.effects, + this.manifest.main, + this.manifest.volumes, + ) + const daemon = dockerProcedureContainer.then((dockerProcedureContainer) => + daemons.runDaemon()( + this.effects, + this.manifest.main.image, + [input.command, ...(input.args || [])], + { + overlay: dockerProcedureContainer.overlay, + }, + ), + ) + return { + wait: () => + daemon.then((daemon) => + daemon.wait().then(() => { + return { result: "" } + }), + ), + term: () => daemon.then((daemon) => daemon.term()), + } } - chown(input: { volumeId: string; path: string; uid: string }): Promise { - throw new Error("Method not implemented.") + async chown(input: { + volumeId: string + path: string + uid: string + }): Promise { + await startSdk + .runCommand( + this.effects, + this.manifest.main.image, + ["chown", "--recursive", input.uid, `/drive/${input.path}`], + { + mounts: [ + { + path: "/drive", + options: { + type: "volume", + id: input.volumeId, + subpath: null, + readonly: false, + }, + }, + ], + }, + ) + .then((x: any) => ({ + stderr: x.stderr.toString(), + stdout: x.stdout.toString(), + })) + .then((x) => { + if (!!x.stderr) { + throw new Error(x.stderr) + } + }) + return null } - chmod(input: { + async chmod(input: { volumeId: string path: string mode: string }): Promise { - throw new Error("Method not implemented.") + await startSdk + .runCommand( + this.effects, + this.manifest.main.image, + ["chmod", "--recursive", input.mode, `/drive/${input.path}`], + { + mounts: [ + { + path: "/drive", + options: { + type: "volume", + id: input.volumeId, + subpath: null, + readonly: false, + }, + }, + ], + }, + ) + .then((x: any) => ({ + stderr: x.stderr.toString(), + stdout: x.stdout.toString(), + })) + .then((x) => { + if (!!x.stderr) { + throw new Error(x.stderr) + } + }) + return null } sleep(timeMs: number): Promise { return new Promise((resolve) => setTimeout(resolve, timeMs)) @@ -149,20 +231,6 @@ export class PolyfillEffects implements oet.Effects { .then(() => true) .catch(() => false) } - bindLocal(options: { - internalPort: number - name: string - externalPort: number - }): Promise { - throw new Error("Method not implemented.") - } - bindTor(options: { - internalPort: number - name: string - externalPort: number - }): Promise { - throw new Error("Method not implemented.") - } async fetch( url: string, options?: @@ -199,7 +267,7 @@ export class PolyfillEffects implements oet.Effects { json: () => fetched.json(), } } - runRsync(options: { + runRsync(rsyncOptions: { srcVolume: string dstVolume: string srcPath: string @@ -210,6 +278,59 @@ export class PolyfillEffects implements oet.Effects { wait: () => Promise progress: () => Promise } { - throw new Error("Method not implemented.") + const { srcVolume, dstVolume, srcPath, dstPath, options } = rsyncOptions + const command = "rsync" + const args: string[] = [] + if (options.delete) { + args.push("--delete") + } + if (options.force) { + args.push("--force") + } + if (options.ignoreExisting) { + args.push("--ignore-existing") + } + for (const exclude of options.exclude) { + args.push(`--exclude=${exclude}`) + } + args.push("-actAXH") + args.push("--info=progress2") + args.push("--no-inc-recursive") + args.push(new Volume(srcVolume, srcPath).path) + args.push(new Volume(dstVolume, dstPath).path) + const spawned = child_process.spawn(command, args, { detached: true }) + let percentage = 0.0 + spawned.stdout.on("data", (data: unknown) => { + const lines = String(data).replace("\r", "\n").split("\n") + for (const line of lines) { + const parsed = /$([0-9.]+)%/.exec(line)?.[1] + if (!parsed) continue + percentage = Number.parseFloat(parsed) + } + }) + + spawned.stderr.on("data", (data: unknown) => { + console.error(String(data)) + }) + + const id = async () => { + const pid = spawned.pid + if (pid === undefined) { + throw new Error("rsync process has no pid") + } + return String(pid) + } + const waitPromise = new Promise((resolve, reject) => { + spawned.on("exit", (code) => { + if (code === 0) { + resolve(null) + } else { + reject(new Error(`rsync exited with code ${code}`)) + } + }) + }) + const wait = () => waitPromise + const progress = () => Promise.resolve(percentage) + return { id, wait, progress } } } diff --git a/container-runtime/update-image.sh b/container-runtime/update-image.sh index e0d5dc0c3..bef525b3b 100755 --- a/container-runtime/update-image.sh +++ b/container-runtime/update-image.sh @@ -19,7 +19,7 @@ if [ "$ARCH" != "$(uname -m)" ]; then fi echo "nameserver 8.8.8.8" | sudo tee tmp/combined/etc/resolv.conf # TODO - delegate to host resolver? -sudo chroot tmp/combined $QEMU /sbin/apk add nodejs +sudo chroot tmp/combined $QEMU /sbin/apk add nodejs rsync sudo mkdir -p tmp/combined/usr/lib/startos/ sudo rsync -a --copy-unsafe-links dist/ tmp/combined/usr/lib/startos/init/ sudo cp containerRuntime.rc tmp/combined/etc/init.d/containerRuntime diff --git a/core/startos/src/service/config.rs b/core/startos/src/service/config.rs index df3e5b046..754e3e0ba 100644 --- a/core/startos/src/service/config.rs +++ b/core/startos/src/service/config.rs @@ -3,7 +3,7 @@ use std::time::Duration; use models::ProcedureName; use crate::config::action::ConfigRes; -use crate::config::ConfigureContext; +use crate::config::{action::SetResult, ConfigureContext}; use crate::prelude::*; use crate::service::{Service, ServiceActor}; use crate::util::actor::{BackgroundJobs, Handler}; @@ -18,10 +18,25 @@ impl Handler for ServiceActor { _: &mut BackgroundJobs, ) -> Self::Response { let container = &self.0.persistent_container; + let package_id = &self.0.id; + container .execute::(ProcedureName::SetConfig, to_value(&config)?, timeout) .await .with_kind(ErrorKind::ConfigRulesViolation)?; + self.0 + .ctx + .db + .mutate(move |db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(package_id) + .or_not_found(package_id)? + .as_status_mut() + .as_configured_mut() + .ser(&true) + }) + .await?; Ok(()) } } @@ -38,7 +53,7 @@ impl Handler for ServiceActor { Some(Duration::from_secs(30)), // TODO timeout ) .await - .with_kind(ErrorKind::ConfigGen) + .with_kind(ErrorKind::ConfigRulesViolation) } } diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index 2b430dc73..c6e64be63 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -281,6 +281,9 @@ impl Service { .as_package_data_mut() .as_idx_mut(&manifest.id) .or_not_found(&manifest.id)?; + if !manifest.has_config { + entry.as_status_mut().as_configured_mut().ser(&true)?; + } entry .as_state_info_mut() .ser(&PackageState::Installed(InstalledState { manifest }))?; diff --git a/sdk/lib/types.ts b/sdk/lib/types.ts index b921a40af..f266d2870 100644 --- a/sdk/lib/types.ts +++ b/sdk/lib/types.ts @@ -560,9 +560,8 @@ export type ActionResult = { } } export type SetResult = { - /** These are the unix process signals */ + dependsOn: DependsOn signal: Signals - "depends-on": DependsOn } export type PackageId = string @@ -570,13 +569,13 @@ export type Message = string export type DependencyKind = "running" | "exists" export type DependsOn = { - [packageId: string]: string[] + [packageId: string]: string[] | readonly string[] } export type KnownError = | { error: string } | { - "error-code": [number, string] | readonly [number, string] + errorCode: [number, string] | readonly [number, string] } export type Dependency = { From 056cab23e05205cb53dcde40f71ed7fabd556063 Mon Sep 17 00:00:00 2001 From: Jade <2364004+Blu-J@users.noreply.github.com> Date: Thu, 4 Apr 2024 13:17:11 -0600 Subject: [PATCH 003/125] Fix: Configure was borken (#2589) --- container-runtime/src/Adapters/HostSystemStartOs.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/container-runtime/src/Adapters/HostSystemStartOs.ts b/container-runtime/src/Adapters/HostSystemStartOs.ts index 5e52224fa..e7db00e07 100644 --- a/container-runtime/src/Adapters/HostSystemStartOs.ts +++ b/container-runtime/src/Adapters/HostSystemStartOs.ts @@ -250,9 +250,9 @@ export class HostSystemStartOs implements Effects { > } setDependencies( - ...[dependencies]: Parameters + dependencies: Parameters[0], ): ReturnType { - return this.rpcRound("setDependencies", { dependencies }) as ReturnType< + return this.rpcRound("setDependencies", dependencies) as ReturnType< T.Effects["setDependencies"] > } From 75ff541aec0c5fb516f2ade5495d1906d4858d93 Mon Sep 17 00:00:00 2001 From: Dominion5254 <125493428+Dominion5254@users.noreply.github.com> Date: Fri, 5 Apr 2024 13:20:49 -0600 Subject: [PATCH 004/125] complete get_service_port_forward fn (#2579) * complete get_service_port_forward fn * refactor get_service_port_forward and get_container_ip * remove unused function * move host_id to GetServicePortForwardParams * replace match with deref Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> * refactor get_container_ip to use deref --------- Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> --- core/startos/src/net/net_controller.rs | 17 +++++++++++++- .../src/service/service_effect_handler.rs | 22 +++++++++---------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/core/startos/src/net/net_controller.rs b/core/startos/src/net/net_controller.rs index a4c9ea507..97f38567d 100644 --- a/core/startos/src/net/net_controller.rs +++ b/core/startos/src/net/net_controller.rs @@ -7,6 +7,7 @@ use imbl::OrdMap; use lazy_format::lazy_format; use models::{HostId, OptionExt, PackageId}; use patch_db::PatchDb; +use tokio::sync::Mutex; use torut::onion::{OnionAddressV3, TorSecretKeyV3}; use tracing::instrument; @@ -387,7 +388,21 @@ impl NetService { } pub fn get_ip(&self) -> Ipv4Addr { - self.ip.to_owned() + self.ip + } + + pub fn get_ext_port(&self, host_id: HostId, internal_port: u16) -> Result { + let host_id_binds = self.binds.get_key_value(&host_id); + match host_id_binds { + Some((id, binds)) => { + if let Some(ext_port_info) = binds.lan.get(&internal_port) { + Ok(ext_port_info.0) + } else { + Err(Error::new(eyre!("Internal Port {} not found in NetService binds", internal_port), crate::ErrorKind::NotFound)) + } + }, + None => Err(Error::new(eyre!("HostID {} not found in NetService binds", host_id), crate::ErrorKind::NotFound)) + } } } diff --git a/core/startos/src/service/service_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs index 210c3d07c..b0b11b34b 100644 --- a/core/startos/src/service/service_effect_handler.rs +++ b/core/startos/src/service/service_effect_handler.rs @@ -186,6 +186,7 @@ struct GetServicePortForwardParams { #[ts(type = "string | null")] package_id: Option, internal_port: u32, + host_id: HostId, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] @@ -314,22 +315,19 @@ async fn get_system_smtp( todo!() } async fn get_container_ip(context: EffectContext, _: Empty) -> Result { - match context.0.upgrade() { - Some(c) => { - let net_service = c.persistent_container.net_service.lock().await; - Ok(net_service.get_ip()) - } - None => Err(Error::new( - eyre!("Upgrade on Weak resulted in a None variant"), - crate::ErrorKind::NotFound, - )), - } + let context = context.deref()?; + let net_service = context.persistent_container.net_service.lock().await; + Ok(net_service.get_ip()) } async fn get_service_port_forward( context: EffectContext, data: GetServicePortForwardParams, -) -> Result { - todo!() +) -> Result { + let internal_port = data.internal_port as u16; + + let context = context.deref()?; + let net_service = context.persistent_container.net_service.lock().await; + net_service.get_ext_port(data.host_id, internal_port) } async fn clear_network_interfaces(context: EffectContext, _: Empty) -> Result { todo!() From e41f8f1d0f7f79d1d5c84c083e4c830704a44caa Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Mon, 8 Apr 2024 11:53:35 -0600 Subject: [PATCH 005/125] allow concurrency in service actor (#2592) --- core/Cargo.lock | 4 +- core/startos/Cargo.toml | 2 +- core/startos/src/s9pk/v2/compat.rs | 5 +- core/startos/src/s9pk/v2/manifest.rs | 8 +- core/startos/src/service/action.rs | 14 +- core/startos/src/service/config.rs | 20 +- core/startos/src/service/control.rs | 21 +- core/startos/src/service/dependencies.rs | 10 +- core/startos/src/service/mod.rs | 12 +- .../src/service/service_effect_handler.rs | 37 +++- core/startos/src/service/transition/mod.rs | 4 +- .../startos/src/service/transition/restart.rs | 16 +- core/startos/src/util/actor/background.rs | 60 +++++ core/startos/src/util/actor/concurrent.rs | 208 ++++++++++++++++++ core/startos/src/util/actor/mod.rs | 148 +++++++++++++ .../src/util/{actor.rs => actor/simple.rs} | 95 +------- 16 files changed, 535 insertions(+), 129 deletions(-) create mode 100644 core/startos/src/util/actor/background.rs create mode 100644 core/startos/src/util/actor/concurrent.rs create mode 100644 core/startos/src/util/actor/mod.rs rename core/startos/src/util/{actor.rs => actor/simple.rs} (60%) diff --git a/core/Cargo.lock b/core/Cargo.lock index 7eaac03bd..455272344 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -4968,9 +4968,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.34.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index d9d4a4e36..cded2ab41 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -160,7 +160,7 @@ ssh-key = { version = "0.6.2", features = ["ed25519"] } stderrlog = "0.5.4" tar = "0.4.40" thiserror = "1.0.49" -tokio = { version = "1", features = ["full"] } +tokio = { version = "1.37", features = ["full"] } tokio-rustls = "0.25.0" tokio-socks = "0.5.1" tokio-stream = { version = "0.1.14", features = ["io-util", "sync", "net"] } diff --git a/core/startos/src/s9pk/v2/compat.rs b/core/startos/src/s9pk/v2/compat.rs index 5a98538dc..b4c2e1c14 100644 --- a/core/startos/src/s9pk/v2/compat.rs +++ b/core/startos/src/s9pk/v2/compat.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeSet; use std::io::Cursor; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -154,7 +155,7 @@ impl S9pk> { i.strip_prefix(&format!("start9/{}/", manifest.id)) .map(|s| s.to_owned()) }) { - new_manifest.images.push(image.parse()?); + new_manifest.images.insert(image.parse()?); let sqfs_path = images_dir.join(&image).with_extension("squashfs"); let image_name = format!("start9/{}/{}:{}", manifest.id, image, manifest.version); let id = String::from_utf8( @@ -334,7 +335,7 @@ impl From for Manifest { marketing_site: value.marketing_site.unwrap_or_else(|| default_url.clone()), donation_url: value.donation_url, description: value.description, - images: Vec::new(), + images: BTreeSet::new(), assets: value .volumes .iter() diff --git a/core/startos/src/s9pk/v2/manifest.rs b/core/startos/src/s9pk/v2/manifest.rs index ea4524400..ee2358b13 100644 --- a/core/startos/src/s9pk/v2/manifest.rs +++ b/core/startos/src/s9pk/v2/manifest.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use color_eyre::eyre::eyre; use helpers::const_true; @@ -43,9 +43,9 @@ pub struct Manifest { #[ts(type = "string | null")] pub donation_url: Option, pub description: Description, - pub images: Vec, - pub assets: Vec, // TODO: AssetsId - pub volumes: Vec, + pub images: BTreeSet, + pub assets: BTreeSet, // TODO: AssetsId + pub volumes: BTreeSet, #[serde(default)] pub alerts: Alerts, #[serde(default)] diff --git a/core/startos/src/service/action.rs b/core/startos/src/service/action.rs index a33869222..5f97685ae 100644 --- a/core/startos/src/service/action.rs +++ b/core/startos/src/service/action.rs @@ -4,19 +4,27 @@ use models::{ActionId, ProcedureName}; use crate::action::ActionResult; use crate::prelude::*; +use crate::service::config::GetConfig; +use crate::service::dependencies::DependencyConfig; use crate::service::{Service, ServiceActor}; -use crate::util::actor::{BackgroundJobs, Handler}; +use crate::util::actor::background::BackgroundJobQueue; +use crate::util::actor::{ConflictBuilder, Handler}; -struct Action { +pub(super) struct Action { id: ActionId, input: Value, } impl Handler for ServiceActor { type Response = Result; + fn conflicts_with(_: &Action) -> ConflictBuilder { + ConflictBuilder::everything() + .except::() + .except::() + } async fn handle( &mut self, Action { id, input }: Action, - _: &mut BackgroundJobs, + _: &BackgroundJobQueue, ) -> Self::Response { let container = &self.0.persistent_container; container diff --git a/core/startos/src/service/config.rs b/core/startos/src/service/config.rs index 754e3e0ba..0f166eedb 100644 --- a/core/startos/src/service/config.rs +++ b/core/startos/src/service/config.rs @@ -3,19 +3,24 @@ use std::time::Duration; use models::ProcedureName; use crate::config::action::ConfigRes; -use crate::config::{action::SetResult, ConfigureContext}; +use crate::config::ConfigureContext; use crate::prelude::*; +use crate::service::dependencies::DependencyConfig; use crate::service::{Service, ServiceActor}; -use crate::util::actor::{BackgroundJobs, Handler}; +use crate::util::actor::background::BackgroundJobQueue; +use crate::util::actor::{ConflictBuilder, Handler}; use crate::util::serde::NoOutput; -struct Configure(ConfigureContext); +pub(super) struct Configure(ConfigureContext); impl Handler for ServiceActor { type Response = Result<(), Error>; + fn conflicts_with(_: &Configure) -> ConflictBuilder { + ConflictBuilder::everything().except::() + } async fn handle( &mut self, Configure(ConfigureContext { timeout, config }): Configure, - _: &mut BackgroundJobs, + _: &BackgroundJobQueue, ) -> Self::Response { let container = &self.0.persistent_container; let package_id = &self.0.id; @@ -41,10 +46,13 @@ impl Handler for ServiceActor { } } -struct GetConfig; +pub(super) struct GetConfig; impl Handler for ServiceActor { type Response = Result; - async fn handle(&mut self, _: GetConfig, _: &mut BackgroundJobs) -> Self::Response { + fn conflicts_with(_: &GetConfig) -> ConflictBuilder { + ConflictBuilder::nothing().except::() + } + async fn handle(&mut self, _: GetConfig, _: &BackgroundJobQueue) -> Self::Response { let container = &self.0.persistent_container; container .execute::( diff --git a/core/startos/src/service/control.rs b/core/startos/src/service/control.rs index 5ef0bbff2..0443b80b1 100644 --- a/core/startos/src/service/control.rs +++ b/core/startos/src/service/control.rs @@ -1,13 +1,21 @@ use crate::prelude::*; +use crate::service::config::GetConfig; +use crate::service::dependencies::DependencyConfig; use crate::service::start_stop::StartStop; use crate::service::transition::TransitionKind; use crate::service::{Service, ServiceActor}; -use crate::util::actor::{BackgroundJobs, Handler}; +use crate::util::actor::background::BackgroundJobQueue; +use crate::util::actor::{ConflictBuilder, Handler}; -struct Start; +pub(super) struct Start; impl Handler for ServiceActor { type Response = (); - async fn handle(&mut self, _: Start, _: &mut BackgroundJobs) -> Self::Response { + fn conflicts_with(_: &Start) -> ConflictBuilder { + ConflictBuilder::everything() + .except::() + .except::() + } + async fn handle(&mut self, _: Start, _: &BackgroundJobQueue) -> Self::Response { self.0.persistent_container.state.send_modify(|x| { x.desired_state = StartStop::Start; }); @@ -23,7 +31,12 @@ impl Service { struct Stop; impl Handler for ServiceActor { type Response = (); - async fn handle(&mut self, _: Stop, _: &mut BackgroundJobs) -> Self::Response { + fn conflicts_with(_: &Stop) -> ConflictBuilder { + ConflictBuilder::everything() + .except::() + .except::() + } + async fn handle(&mut self, _: Stop, _: &BackgroundJobQueue) -> Self::Response { let mut transition_state = None; self.0.persistent_container.state.send_modify(|x| { x.desired_state = StartStop::Stop; diff --git a/core/startos/src/service/dependencies.rs b/core/startos/src/service/dependencies.rs index 67cc5d53f..e71b62917 100644 --- a/core/startos/src/service/dependencies.rs +++ b/core/startos/src/service/dependencies.rs @@ -5,22 +5,26 @@ use models::{PackageId, ProcedureName}; use crate::prelude::*; use crate::service::{Service, ServiceActor}; -use crate::util::actor::{BackgroundJobs, Handler}; +use crate::util::actor::background::BackgroundJobQueue; +use crate::util::actor::{ConflictBuilder, Handler}; use crate::Config; -struct DependencyConfig { +pub(super) struct DependencyConfig { dependency_id: PackageId, remote_config: Option, } impl Handler for ServiceActor { type Response = Result, Error>; + fn conflicts_with(_: &DependencyConfig) -> ConflictBuilder { + ConflictBuilder::nothing() + } async fn handle( &mut self, DependencyConfig { dependency_id, remote_config, }: DependencyConfig, - _: &mut BackgroundJobs, + _: &BackgroundJobQueue, ) -> Self::Response { let container = &self.0.persistent_container; container diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index c6e64be63..38923d726 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -13,7 +13,6 @@ use start_stop::StartStop; use tokio::sync::Notify; use ts_rs::TS; -use crate::config::action::ConfigRes; use crate::context::{CliContext, RpcContext}; use crate::core::rpc_continuations::RequestGuid; use crate::db::model::package::{ @@ -28,7 +27,9 @@ use crate::service::service_map::InstallProgressHandles; use crate::service::transition::TransitionKind; use crate::status::health_check::HealthCheckResult; use crate::status::MainStatus; -use crate::util::actor::{Actor, BackgroundJobs, SimpleActor}; +use crate::util::actor::background::BackgroundJobQueue; +use crate::util::actor::concurrent::ConcurrentActor; +use crate::util::actor::Actor; use crate::util::serde::Pem; use crate::volume::data_dir; @@ -66,7 +67,7 @@ pub enum LoadDisposition { } pub struct Service { - actor: SimpleActor, + actor: ConcurrentActor, seed: Arc, } impl Service { @@ -90,7 +91,7 @@ impl Service { .init(Arc::downgrade(&seed)) .await?; Ok(Self { - actor: SimpleActor::new(ServiceActor(seed.clone())), + actor: ConcurrentActor::new(ServiceActor(seed.clone())), seed, }) } @@ -391,10 +392,11 @@ impl ServiceActorSeed { }); } } +#[derive(Clone)] struct ServiceActor(Arc); impl Actor for ServiceActor { - fn init(&mut self, jobs: &mut BackgroundJobs) { + fn init(&mut self, jobs: &BackgroundJobQueue) { let seed = self.0.clone(); jobs.add_job(async move { let id = seed.id.clone(); diff --git a/core/startos/src/service/service_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs index b0b11b34b..0effccd5e 100644 --- a/core/startos/src/service/service_effect_handler.rs +++ b/core/startos/src/service/service_effect_handler.rs @@ -11,7 +11,7 @@ use clap::Parser; use emver::VersionRange; use imbl::OrdMap; use imbl_value::{json, InternedString}; -use models::{ActionId, HealthCheckId, HostId, ImageId, PackageId, VolumeId}; +use models::{ActionId, DataUrl, HealthCheckId, HostId, ImageId, PackageId, VolumeId}; use patch_db::json_ptr::JsonPointer; use rpc_toolkit::{from_fn, from_fn_async, AnyContext, Context, Empty, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; @@ -28,7 +28,9 @@ use crate::disk::mount::filesystem::overlayfs::OverlayGuard; use crate::net::host::binding::BindOptions; use crate::net::host::HostKind; use crate::prelude::*; +use crate::s9pk::merkle_archive::source::http::{HttpReader, HttpSource}; use crate::s9pk::rpc::SKIP_ENV; +use crate::s9pk::S9pk; use crate::service::cli::ContainerCliContext; use crate::service::ServiceActorSeed; use crate::status::health_check::HealthCheckResult; @@ -1145,11 +1147,36 @@ async fn set_dependencies( version_spec, ), }; - let icon = todo!(); - let title = todo!(); + let (icon, title) = match async { + let remote_s9pk = S9pk::deserialize( + &HttpSource::new( + ctx.ctx.client.clone(), + registry_url + .join(&format!("package/v2/{}.s9pk?spec={}", dep_id, version_spec))?, + ) + .await?, + ) + .await?; + + let icon = remote_s9pk.icon_data_url().await?; + + Ok::<_, Error>((icon, remote_s9pk.as_manifest().title.clone())) + } + .await + { + Ok(a) => a, + Err(e) => { + tracing::error!("Error fetching remote s9pk: {e}"); + tracing::debug!("{e:?}"); + ( + DataUrl::from_slice("image/png", include_bytes!("../install/package-icon.png")), + dep_id.to_string(), + ) + } + }; let config_satisfied = if let Some(dep_service) = &*ctx.ctx.services.get(&dep_id).await { service - .dependency_config(dep_id, dep_service.get_config().await?.config) + .dependency_config(dep_id.clone(), dep_service.get_config().await?.config) .await? .is_none() } else { @@ -1158,7 +1185,7 @@ async fn set_dependencies( deps.insert( dep_id, CurrentDependencyInfo { - kind: CurrentDependencyKind::Exists, + kind, registry_url, version_spec, icon, diff --git a/core/startos/src/service/transition/mod.rs b/core/startos/src/service/transition/mod.rs index af62ccc1c..25d225492 100644 --- a/core/startos/src/service/transition/mod.rs +++ b/core/startos/src/service/transition/mod.rs @@ -5,7 +5,7 @@ use tokio::sync::watch; use super::persistent_container::ServiceState; use crate::service::start_stop::StartStop; -use crate::util::actor::BackgroundJobs; +use crate::util::actor::background::BackgroundJobQueue; use crate::util::future::{CancellationHandle, RemoteCancellable}; pub mod backup; @@ -41,7 +41,7 @@ impl TransitionState { fn new( task: impl Future + Send + 'static, kind: TransitionKind, - jobs: &mut BackgroundJobs, + jobs: &BackgroundJobQueue, ) -> Self { let task = RemoteCancellable::new(task); let cancel_handle = task.cancellation_handle(); diff --git a/core/startos/src/service/transition/restart.rs b/core/startos/src/service/transition/restart.rs index 7047bd18f..dbf066e6f 100644 --- a/core/startos/src/service/transition/restart.rs +++ b/core/startos/src/service/transition/restart.rs @@ -1,18 +1,24 @@ -use std::sync::Arc; - use futures::FutureExt; use super::TempDesiredState; use crate::prelude::*; +use crate::service::config::GetConfig; +use crate::service::dependencies::DependencyConfig; use crate::service::transition::{TransitionKind, TransitionState}; use crate::service::{Service, ServiceActor}; -use crate::util::actor::{BackgroundJobs, Handler}; +use crate::util::actor::background::BackgroundJobQueue; +use crate::util::actor::{ConflictBuilder, Handler}; use crate::util::future::RemoteCancellable; -struct Restart; +pub(super) struct Restart; impl Handler for ServiceActor { type Response = (); - async fn handle(&mut self, _: Restart, jobs: &mut BackgroundJobs) -> Self::Response { + fn conflicts_with(_: &Restart) -> ConflictBuilder { + ConflictBuilder::everything() + .except::() + .except::() + } + async fn handle(&mut self, _: Restart, jobs: &BackgroundJobQueue) -> Self::Response { // So Need a handle to just a single field in the state let temp = TempDesiredState::new(&self.0.persistent_container.state); let mut current = self.0.persistent_container.state.subscribe(); diff --git a/core/startos/src/util/actor/background.rs b/core/startos/src/util/actor/background.rs new file mode 100644 index 000000000..f37e10c14 --- /dev/null +++ b/core/startos/src/util/actor/background.rs @@ -0,0 +1,60 @@ +use futures::future::BoxFuture; +use futures::{Future, FutureExt}; +use tokio::sync::mpsc; + +#[derive(Clone)] +pub struct BackgroundJobQueue(mpsc::UnboundedSender>); +impl BackgroundJobQueue { + pub fn new() -> (Self, BackgroundJobRunner) { + let (send, recv) = mpsc::unbounded_channel(); + ( + Self(send), + BackgroundJobRunner { + recv, + jobs: Vec::new(), + }, + ) + } + pub fn add_job(&self, fut: impl Future + Send + 'static) { + let _ = self.0.send(fut.boxed()); + } +} + +pub struct BackgroundJobRunner { + recv: mpsc::UnboundedReceiver>, + jobs: Vec>, +} +impl BackgroundJobRunner { + pub fn is_empty(&self) -> bool { + self.recv.is_empty() && self.jobs.is_empty() + } +} +impl Future for BackgroundJobRunner { + type Output = (); + fn poll( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + while let std::task::Poll::Ready(Some(job)) = self.recv.poll_recv(cx) { + self.jobs.push(job); + } + let complete = self + .jobs + .iter_mut() + .enumerate() + .filter_map(|(i, f)| match f.poll_unpin(cx) { + std::task::Poll::Pending => None, + std::task::Poll::Ready(_) => Some(i), + }) + .collect::>(); + for idx in complete.into_iter().rev() { + #[allow(clippy::let_underscore_future)] + let _ = self.jobs.swap_remove(idx); + } + if self.jobs.is_empty() && self.recv.is_closed() { + std::task::Poll::Ready(()) + } else { + std::task::Poll::Pending + } + } +} diff --git a/core/startos/src/util/actor/concurrent.rs b/core/startos/src/util/actor/concurrent.rs new file mode 100644 index 000000000..d330102ca --- /dev/null +++ b/core/startos/src/util/actor/concurrent.rs @@ -0,0 +1,208 @@ +use std::any::Any; +use std::sync::Arc; +use std::time::Duration; + +use futures::future::{ready, BoxFuture}; +use futures::{Future, FutureExt, TryFutureExt}; +use helpers::NonDetachingJoinHandle; +use tokio::sync::{mpsc, oneshot}; + +use crate::prelude::*; +use crate::util::actor::background::{BackgroundJobQueue, BackgroundJobRunner}; +use crate::util::actor::{Actor, ConflictFn, Handler, PendingMessageStrategy, Request}; + +#[pin_project::pin_project] +struct ConcurrentRunner { + actor: A, + shutdown: Option>, + waiting: Vec>, + recv: mpsc::UnboundedReceiver>, + handlers: Vec<( + Arc>, + oneshot::Sender>, + BoxFuture<'static, Box>, + )>, + queue: BackgroundJobQueue, + #[pin] + bg_runner: BackgroundJobRunner, +} +impl Future for ConcurrentRunner { + type Output = (); + fn poll( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + let mut this = self.project(); + *this.shutdown = this.shutdown.take().and_then(|mut s| { + if s.poll_unpin(cx).is_pending() { + Some(s) + } else { + None + } + }); + if this.shutdown.is_some() { + while let std::task::Poll::Ready(Some((msg, reply))) = this.recv.poll_recv(cx) { + if this.handlers.iter().any(|(f, _, _)| f(&*msg)) { + this.waiting.push((msg, reply)); + } else { + let mut actor = this.actor.clone(); + let queue = this.queue.clone(); + this.handlers.push(( + msg.conflicts_with(), + reply, + async move { msg.handle_with(&mut actor, &queue).await }.boxed(), + )) + } + } + } + // handlers + while { + let mut cont = false; + let complete = this + .handlers + .iter_mut() + .enumerate() + .filter_map(|(i, (_, _, f))| match f.poll_unpin(cx) { + std::task::Poll::Pending => None, + std::task::Poll::Ready(res) => Some((i, res)), + }) + .collect::>(); + for (idx, res) in complete.into_iter().rev() { + #[allow(clippy::let_underscore_future)] + let (f, reply, _) = this.handlers.swap_remove(idx); + let _ = reply.send(res); + // TODO: replace with Vec::extract_if once stable + if this.shutdown.is_some() { + let mut i = 0; + while i < this.waiting.len() { + if f(&*this.waiting[i].0) + && !this.handlers.iter().any(|(f, _, _)| f(&*this.waiting[i].0)) + { + let (msg, reply) = this.waiting.remove(i); + let mut actor = this.actor.clone(); + let queue = this.queue.clone(); + this.handlers.push(( + msg.conflicts_with(), + reply, + async move { msg.handle_with(&mut actor, &queue).await }.boxed(), + )); + cont = true; + } else { + i += 1; + } + } + } + } + cont + } {} + let _ = this.bg_runner.as_mut().poll(cx); + if this.waiting.is_empty() && this.handlers.is_empty() && this.bg_runner.is_empty() { + std::task::Poll::Ready(()) + } else { + std::task::Poll::Pending + } + } +} + +pub struct ConcurrentActor { + shutdown: oneshot::Sender<()>, + runtime: NonDetachingJoinHandle<()>, + messenger: mpsc::UnboundedSender>, +} +impl ConcurrentActor { + pub fn new(mut actor: A) -> Self { + let (shutdown_send, shutdown_recv) = oneshot::channel(); + let (messenger_send, messenger_recv) = mpsc::unbounded_channel::>(); + let runtime = NonDetachingJoinHandle::from(tokio::spawn(async move { + let (queue, runner) = BackgroundJobQueue::new(); + actor.init(&queue); + ConcurrentRunner { + actor, + shutdown: Some(shutdown_recv), + waiting: Vec::new(), + recv: messenger_recv, + handlers: Vec::new(), + queue, + bg_runner: runner, + } + .await + })); + Self { + shutdown: shutdown_send, + runtime, + messenger: messenger_send, + } + } + + /// Message is guaranteed to be queued immediately + pub fn queue( + &self, + message: M, + ) -> impl Future> + where + A: Handler, + { + if self.runtime.is_finished() { + return futures::future::Either::Left(ready(Err(Error::new( + eyre!("actor runtime has exited"), + ErrorKind::Unknown, + )))); + } + let (reply_send, reply_recv) = oneshot::channel(); + self.messenger + .send((Box::new(message), reply_send)) + .unwrap(); + futures::future::Either::Right( + reply_recv + .map_err(|_| Error::new(eyre!("actor runtime has exited"), ErrorKind::Unknown)) + .and_then(|a| { + ready( + a.downcast() + .map_err(|_| { + Error::new( + eyre!("received incorrect type in response"), + ErrorKind::Incoherent, + ) + }) + .map(|a| *a), + ) + }), + ) + } + + pub async fn send(&self, message: M) -> Result + where + A: Handler, + { + self.queue(message).await + } + + pub async fn shutdown(self, strategy: PendingMessageStrategy) { + drop(self.messenger); + let timeout = match strategy { + PendingMessageStrategy::CancelAll => { + self.shutdown.send(()).unwrap(); + Some(Duration::from_secs(0)) + } + PendingMessageStrategy::FinishCurrentCancelPending { timeout } => { + self.shutdown.send(()).unwrap(); + timeout + } + PendingMessageStrategy::FinishAll { timeout } => timeout, + }; + let aborter = if let Some(timeout) = timeout { + let hdl = self.runtime.abort_handle(); + async move { + tokio::time::sleep(timeout).await; + hdl.abort(); + } + .boxed() + } else { + futures::future::pending().boxed() + }; + tokio::select! { + _ = aborter => (), + _ = self.runtime => (), + } + } +} diff --git a/core/startos/src/util/actor/mod.rs b/core/startos/src/util/actor/mod.rs new file mode 100644 index 000000000..5cef7c22e --- /dev/null +++ b/core/startos/src/util/actor/mod.rs @@ -0,0 +1,148 @@ +use std::any::{Any, TypeId}; +use std::collections::BTreeMap; +use std::sync::Arc; +use std::time::Duration; + +use futures::future::BoxFuture; +use futures::{Future, FutureExt}; +use tokio::sync::oneshot; + +#[allow(unused_imports)] +use crate::prelude::*; +use crate::util::actor::background::BackgroundJobQueue; + +pub mod background; +pub mod concurrent; +pub mod simple; + +pub trait Actor: Sized + Send + 'static { + #[allow(unused_variables)] + fn init(&mut self, jobs: &BackgroundJobQueue) {} +} + +pub trait Handler: Actor { + type Response: Any + Send; + /// DRAGONS: this must be correctly implemented bi-directionally in order to work as expected + fn conflicts_with(#[allow(unused_variables)] msg: &M) -> ConflictBuilder { + ConflictBuilder::everything() + } + fn handle( + &mut self, + msg: M, + jobs: &BackgroundJobQueue, + ) -> impl Future + Send; +} + +type ConflictFn = dyn Fn(&dyn Message) -> bool + Send + Sync; + +trait Message: Send + Any { + fn conflicts_with(&self) -> Arc>; + fn handle_with<'a>( + self: Box, + actor: &'a mut A, + jobs: &'a BackgroundJobQueue, + ) -> BoxFuture<'a, Box>; +} +impl Message for M +where + A: Handler, +{ + fn conflicts_with(&self) -> Arc> { + A::conflicts_with(self).build() + } + fn handle_with<'a>( + self: Box, + actor: &'a mut A, + jobs: &'a BackgroundJobQueue, + ) -> BoxFuture<'a, Box> { + async move { Box::new(actor.handle(*self, jobs).await) as Box }.boxed() + } +} +impl dyn Message { + #[inline] + pub fn is>(&self) -> bool { + let t = TypeId::of::(); + let concrete = self.type_id(); + t == concrete + } + #[inline] + pub unsafe fn downcast_ref_unchecked>(&self) -> &M { + debug_assert!(self.is::()); + unsafe { &*(self as *const dyn Message as *const M) } + } + #[inline] + fn downcast_ref>(&self) -> Option<&M> { + if self.is::() { + unsafe { Some(self.downcast_ref_unchecked()) } + } else { + None + } + } +} + +type Request = (Box>, oneshot::Sender>); + +pub enum PendingMessageStrategy { + CancelAll, + FinishCurrentCancelPending { timeout: Option }, + FinishAll { timeout: Option }, +} + +pub struct ConflictBuilder { + base: bool, + except: BTreeMap) -> bool + Send + Sync>>>, +} +impl ConflictBuilder { + pub const fn everything() -> Self { + Self { + base: true, + except: BTreeMap::new(), + } + } + pub const fn nothing() -> Self { + Self { + base: false, + except: BTreeMap::new(), + } + } + pub fn except(mut self) -> Self + where + A: Handler, + { + self.except.insert(TypeId::of::(), None); + self + } + pub fn except_if bool + Send + Sync + 'static>( + mut self, + f: F, + ) -> Self + where + A: Handler, + { + self.except.insert( + TypeId::of::(), + Some(Box::new(move |m| { + if let Some(m) = m.downcast_ref() { + f(m) + } else { + false + } + })), + ); + self + } + fn build(self) -> Arc> { + Arc::new(move |m| { + self.base + ^ if let Some(entry) = self.except.get(&m.type_id()) { + if let Some(f) = entry { + f(m) + } else { + true + } + } else { + false + } + }) + } +} diff --git a/core/startos/src/util/actor.rs b/core/startos/src/util/actor/simple.rs similarity index 60% rename from core/startos/src/util/actor.rs rename to core/startos/src/util/actor/simple.rs index caa83d8b1..6f880a57a 100644 --- a/core/startos/src/util/actor.rs +++ b/core/startos/src/util/actor/simple.rs @@ -1,85 +1,14 @@ -use std::any::Any; -use std::future::ready; use std::time::Duration; -use futures::future::BoxFuture; +use futures::future::ready; use futures::{Future, FutureExt, TryFutureExt}; use helpers::NonDetachingJoinHandle; use tokio::sync::oneshot::error::TryRecvError; use tokio::sync::{mpsc, oneshot}; use crate::prelude::*; -use crate::util::Never; - -pub trait Actor: Send + 'static { - #[allow(unused_variables)] - fn init(&mut self, jobs: &mut BackgroundJobs) {} -} - -pub trait Handler: Actor { - type Response: Any + Send; - fn handle( - &mut self, - msg: M, - jobs: &mut BackgroundJobs, - ) -> impl Future + Send; -} - -#[async_trait::async_trait] -trait Message: Send { - async fn handle_with( - self: Box, - actor: &mut A, - jobs: &mut BackgroundJobs, - ) -> Box; -} -#[async_trait::async_trait] -impl Message for M -where - A: Handler, -{ - async fn handle_with( - self: Box, - actor: &mut A, - jobs: &mut BackgroundJobs, - ) -> Box { - Box::new(actor.handle(*self, jobs).await) - } -} - -type Request = (Box>, oneshot::Sender>); - -#[derive(Default)] -pub struct BackgroundJobs { - jobs: Vec>, -} -impl BackgroundJobs { - pub fn add_job(&mut self, fut: impl Future + Send + 'static) { - self.jobs.push(fut.boxed()); - } -} -impl Future for BackgroundJobs { - type Output = Never; - fn poll( - mut self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll { - let complete = self - .jobs - .iter_mut() - .enumerate() - .filter_map(|(i, f)| match f.poll_unpin(cx) { - std::task::Poll::Pending => None, - std::task::Poll::Ready(_) => Some(i), - }) - .collect::>(); - for idx in complete.into_iter().rev() { - #[allow(clippy::let_underscore_future)] - let _ = self.jobs.swap_remove(idx); - } - std::task::Poll::Pending - } -} +use crate::util::actor::background::BackgroundJobQueue; +use crate::util::actor::{Actor, Handler, PendingMessageStrategy, Request}; pub struct SimpleActor { shutdown: oneshot::Sender<()>, @@ -91,19 +20,17 @@ impl SimpleActor { let (shutdown_send, mut shutdown_recv) = oneshot::channel(); let (messenger_send, mut messenger_recv) = mpsc::unbounded_channel::>(); let runtime = NonDetachingJoinHandle::from(tokio::spawn(async move { - let mut bg = BackgroundJobs::default(); - actor.init(&mut bg); + let (queue, mut runner) = BackgroundJobQueue::new(); + actor.init(&queue); loop { tokio::select! { - _ = &mut bg => (), + _ = &mut runner => (), msg = messenger_recv.recv() => match msg { Some((msg, reply)) if shutdown_recv.try_recv() == Err(TryRecvError::Empty) => { - let mut new_bg = BackgroundJobs::default(); tokio::select! { - res = msg.handle_with(&mut actor, &mut new_bg) => { let _ = reply.send(res); }, - _ = &mut bg => (), + res = msg.handle_with(&mut actor, &queue) => { let _ = reply.send(res); }, + _ = &mut runner => (), } - bg.jobs.append(&mut new_bg.jobs); } _ => break, }, @@ -189,9 +116,3 @@ impl SimpleActor { } } } - -pub enum PendingMessageStrategy { - CancelAll, - FinishCurrentCancelPending { timeout: Option }, - FinishAll { timeout: Option }, -} From c13d8f36999d2be7ced9a0c65681ea0a105ee79c Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Mon, 8 Apr 2024 12:07:56 -0600 Subject: [PATCH 006/125] finish dependency autoconfig (#2596) --- core/startos/src/dependencies.rs | 91 +++++++------------------------- 1 file changed, 19 insertions(+), 72 deletions(-) diff --git a/core/startos/src/dependencies.rs b/core/startos/src/dependencies.rs index 61f5af2b4..42a19abe3 100644 --- a/core/startos/src/dependencies.rs +++ b/core/startos/src/dependencies.rs @@ -3,6 +3,7 @@ use std::time::Duration; use clap::Parser; use models::PackageId; +use patch_db::json_patch::merge; use rpc_toolkit::{command, from_fn_async, Empty, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use tracing::instrument; @@ -102,78 +103,24 @@ pub async fn configure_logic( ctx: RpcContext, (dependent_id, dependency_id): (PackageId, PackageId), ) -> Result { - // let db = ctx.db.peek().await; - // let pkg = db - // .as_package_data() - // .as_idx(&pkg_id) - // .or_not_found(&pkg_id)? - // .as_installed() - // .or_not_found(&pkg_id)?; - // let pkg_version = pkg.as_manifest().as_version().de()?; - // let pkg_volumes = pkg.as_manifest().as_volumes().de()?; - // let dependency = db - // .as_package_data() - // .as_idx(&dependency_id) - // .or_not_found(&dependency_id)? - // .as_installed() - // .or_not_found(&dependency_id)?; - // let dependency_config_action = dependency - // .as_manifest() - // .as_config() - // .de()? - // .ok_or_else(|| not_found!("Manifest Config"))?; - // let dependency_version = dependency.as_manifest().as_version().de()?; - // let dependency_volumes = dependency.as_manifest().as_volumes().de()?; - // let dependency = pkg - // .as_manifest() - // .as_dependencies() - // .as_idx(&dependency_id) - // .or_not_found(&dependency_id)?; - - // let ConfigRes { - // config: maybe_config, - // spec, - // } = dependency_config_action - // .get( - // &ctx, - // &dependency_id, - // &dependency_version, - // &dependency_volumes, - // ) - // .await?; - - // let old_config = if let Some(config) = maybe_config { - // config - // } else { - // spec.gen( - // &mut rand::rngs::StdRng::from_entropy(), - // &Some(Duration::new(10, 0)), - // )? - // }; - - // let new_config = dependency - // .as_config() - // .de()? - // .ok_or_else(|| not_found!("Config"))? - // .auto_configure - // .sandboxed( - // &ctx, - // &pkg_id, - // &pkg_version, - // &pkg_volumes, - // Some(&old_config), - // None, - // ProcedureName::AutoConfig(dependency_id.clone()), - // ) - // .await? - // .map_err(|e| Error::new(eyre!("{}", e.1), crate::ErrorKind::AutoConfigure))?; - - // Ok(ConfigDryRes { - // old_config, - // new_config, - // spec, - // }) - todo!() + let dependency_guard = ctx.services.get(&dependency_id).await; + let dependency = dependency_guard.as_ref().or_not_found(&dependency_id)?; + let dependent_guard = ctx.services.get(&dependent_id).await; + let dependent = dependent_guard.as_ref().or_not_found(&dependent_id)?; + let config_res = dependency.get_config().await?; + let diff = Value::Object( + dependent + .dependency_config(dependency_id, config_res.config.clone()) + .await? + .unwrap_or_default(), + ); + let mut new_config = Value::Object(config_res.config.clone().unwrap_or_default()); + merge(&mut new_config, &diff); + Ok(ConfigDryRes { + old_config: config_res.config.unwrap_or_default(), + new_config: new_config.as_object().cloned().unwrap_or_default(), + spec: config_res.spec, + }) } #[instrument(skip_all)] From 313e415ee955176032bc0e354984ff468463dbf1 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Mon, 8 Apr 2024 14:01:16 -0600 Subject: [PATCH 007/125] miscellaneous bugfixes --- core/startos/bindings/GetServicePortForwardParams.ts | 2 ++ core/startos/src/db/mod.rs | 2 +- core/startos/src/s9pk/v1/manifest.rs | 8 ++++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/core/startos/bindings/GetServicePortForwardParams.ts b/core/startos/bindings/GetServicePortForwardParams.ts index 3de7e0219..70d69c674 100644 --- a/core/startos/bindings/GetServicePortForwardParams.ts +++ b/core/startos/bindings/GetServicePortForwardParams.ts @@ -1,6 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HostId } from "./HostId"; export type GetServicePortForwardParams = { packageId: string | null; internalPort: number; + hostId: HostId; }; diff --git a/core/startos/src/db/mod.rs b/core/startos/src/db/mod.rs index ab5d23efb..95e42f46b 100644 --- a/core/startos/src/db/mod.rs +++ b/core/startos/src/db/mod.rs @@ -324,7 +324,7 @@ pub struct UiParams { // #[command(display(display_serializable))] #[instrument(skip_all)] pub async fn ui(ctx: RpcContext, UiParams { pointer, value, .. }: UiParams) -> Result<(), Error> { - let ptr = "/ui" + let ptr = "/public/ui" .parse::() .with_kind(ErrorKind::Database)? + &pointer; diff --git a/core/startos/src/s9pk/v1/manifest.rs b/core/startos/src/s9pk/v1/manifest.rs index ef346ad2b..264843766 100644 --- a/core/startos/src/s9pk/v1/manifest.rs +++ b/core/startos/src/s9pk/v1/manifest.rs @@ -19,7 +19,7 @@ fn current_version() -> Version { } #[derive(Clone, Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "kebab-case")] #[model = "Model"] pub struct Manifest { #[serde(default = "current_version")] @@ -54,7 +54,7 @@ pub struct Manifest { } #[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "kebab-case")] #[serde(tag = "type")] pub enum DependencyRequirement { OptIn { how: String }, @@ -68,7 +68,7 @@ impl DependencyRequirement { } #[derive(Clone, Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "kebab-case")] #[model = "Model"] pub struct DepInfo { pub version: VersionRange, @@ -77,7 +77,7 @@ pub struct DepInfo { } #[derive(Clone, Debug, Default, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "kebab-case")] pub struct Assets { #[serde(default)] pub license: Option, From f07992c09135267c79c7c0ae305fbe04a1cda0c9 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Tue, 9 Apr 2024 14:04:31 -0600 Subject: [PATCH 008/125] misc fixes --- core/startos/bindings/ServerInfo.ts | 2 +- core/startos/bindings/WifiInfo.ts | 1 + core/startos/src/backup/restore.rs | 6 +++++- core/startos/src/db/model/mod.rs | 5 ++--- core/startos/src/db/model/public.rs | 10 +++++---- core/startos/src/net/vhost.rs | 21 +++++++++++++++---- core/startos/src/properties.rs | 7 +++++-- core/startos/src/setup.rs | 6 +++++- .../ui/src/app/services/api/mock-patch.ts | 1 + 9 files changed, 43 insertions(+), 16 deletions(-) diff --git a/core/startos/bindings/ServerInfo.ts b/core/startos/bindings/ServerInfo.ts index e505c84e7..130a5bafe 100644 --- a/core/startos/bindings/ServerInfo.ts +++ b/core/startos/bindings/ServerInfo.ts @@ -24,7 +24,7 @@ export type ServerInfo = { torAddress: string; ipInfo: { [key: string]: IpInfo }; statusInfo: ServerStatus; - wifi: WifiInfo; + wifi: WifiInfo | null; unreadNotificationCount: number; passwordHash: string; pubkey: string; diff --git a/core/startos/bindings/WifiInfo.ts b/core/startos/bindings/WifiInfo.ts index c949b1e76..59b1b901d 100644 --- a/core/startos/bindings/WifiInfo.ts +++ b/core/startos/bindings/WifiInfo.ts @@ -1,6 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. export type WifiInfo = { + interface: string; ssids: Array; selected: string | null; connected: string | null; diff --git a/core/startos/src/backup/restore.rs b/core/startos/src/backup/restore.rs index 4753a4290..41aaeca7b 100644 --- a/core/startos/src/backup/restore.rs +++ b/core/startos/src/backup/restore.rs @@ -96,7 +96,11 @@ pub async fn recover_full_embassy( .with_kind(ErrorKind::PasswordHashGeneration)?; let db = ctx.db().await?; - db.put(&ROOT, &Database::init(&os_backup.account)?).await?; + db.put( + &ROOT, + &Database::init(&os_backup.account, ctx.config.wifi_interface.clone())?, + ) + .await?; drop(db); init(&ctx.config).await?; diff --git a/core/startos/src/db/model/mod.rs b/core/startos/src/db/model/mod.rs index 9be0f8b68..8b959cdfd 100644 --- a/core/startos/src/db/model/mod.rs +++ b/core/startos/src/db/model/mod.rs @@ -2,7 +2,6 @@ use std::collections::BTreeMap; use patch_db::HasModel; use serde::{Deserialize, Serialize}; -use ts_rs::TS; use crate::account::AccountInfo; use crate::auth::Sessions; @@ -28,9 +27,9 @@ pub struct Database { pub private: Private, } impl Database { - pub fn init(account: &AccountInfo) -> Result { + pub fn init(account: &AccountInfo, wifi_interface: Option) -> Result { Ok(Self { - public: Public::init(account)?, + public: Public::init(account, wifi_interface)?, private: Private { key_store: KeyStore::new(account)?, password: account.password.clone(), diff --git a/core/startos/src/db/model/public.rs b/core/startos/src/db/model/public.rs index d1eec5443..5b83eb74d 100644 --- a/core/startos/src/db/model/public.rs +++ b/core/startos/src/db/model/public.rs @@ -35,7 +35,7 @@ pub struct Public { pub ui: Value, } impl Public { - pub fn init(account: &AccountInfo) -> Result { + pub fn init(account: &AccountInfo, wifi_interface: Option) -> Result { let lan_address = account.hostname.lan_address().parse().unwrap(); Ok(Self { server_info: ServerInfo { @@ -60,11 +60,12 @@ impl Public { shutting_down: false, restarting: false, }, - wifi: WifiInfo { + wifi: wifi_interface.map(|interface| WifiInfo { + interface, ssids: Vec::new(), connected: None, selected: None, - }, + }), unread_notification_count: 0, password_hash: account.password.clone(), pubkey: ssh_key::PublicKey::from(&account.ssh_key) @@ -131,7 +132,7 @@ pub struct ServerInfo { pub ip_info: BTreeMap, #[serde(default)] pub status_info: ServerStatus, - pub wifi: WifiInfo, + pub wifi: Option, #[ts(type = "number")] pub unread_notification_count: u64, pub password_hash: String, @@ -206,6 +207,7 @@ pub struct UpdateProgress { #[model = "Model"] #[ts(export)] pub struct WifiInfo { + pub interface: String, pub ssids: Vec, pub selected: Option, pub connected: Option, diff --git a/core/startos/src/net/vhost.rs b/core/startos/src/net/vhost.rs index 46838ed51..47cf30ad5 100644 --- a/core/startos/src/net/vhost.rs +++ b/core/startos/src/net/vhost.rs @@ -111,7 +111,7 @@ impl VHostServer { _thread: tokio::spawn(async move { loop { match listener.accept().await { - Ok((stream, sock_addr)) => { + Ok((stream, _)) => { let stream = Box::pin(TimeoutStream::new(stream, Duration::from_secs(300))); let mut stream = BackTrackingReader::new(stream); @@ -195,9 +195,22 @@ impl VHostServer { .as_ref() .into_iter() .map(InternedString::intern) - .chain(std::iter::once(InternedString::from_display( - &sock_addr.ip(), - ))) + .chain( + db.peek() + .await + .into_public() + .into_server_info() + .into_ip_info() + .into_entries()? + .into_iter() + .flat_map(|(_, ips)| [ + ips.as_ipv4().de().map(|ip| ip.map(IpAddr::V4)), + ips.as_ipv6().de().map(|ip| ip.map(IpAddr::V6)) + ]) + .filter_map(|a| a.transpose()) + .map(|a| a.map(|ip| InternedString::from_display(&ip))) + .collect::, _>>()?, + ) .collect(); let key = db .mutate(|v| { diff --git a/core/startos/src/properties.rs b/core/startos/src/properties.rs index 5aa8a01d4..f8753ab10 100644 --- a/core/startos/src/properties.rs +++ b/core/startos/src/properties.rs @@ -1,5 +1,5 @@ use clap::Parser; -use imbl_value::Value; +use imbl_value::{json, Value}; use models::PackageId; use rpc_toolkit::command; use serde::{Deserialize, Serialize}; @@ -24,7 +24,10 @@ pub async fn properties( PropertiesParam { id }: PropertiesParam, ) -> Result { match &*ctx.services.get(&id).await { - Some(service) => service.properties().await, + Some(service) => Ok(json!({ + "version": 2, + "data": service.properties().await? + })), None => Err(Error::new( eyre!("Could not find a service with id {id}"), ErrorKind::NotFound, diff --git a/core/startos/src/setup.rs b/core/startos/src/setup.rs index 9120544e3..b8b761e69 100644 --- a/core/startos/src/setup.rs +++ b/core/startos/src/setup.rs @@ -419,7 +419,11 @@ async fn fresh_setup( ) -> Result<(Hostname, OnionAddressV3, X509), Error> { let account = AccountInfo::new(start_os_password, root_ca_start_time().await?)?; let db = ctx.db().await?; - db.put(&ROOT, &Database::init(&account)?).await?; + db.put( + &ROOT, + &Database::init(&account, ctx.config.wifi_interface.clone())?, + ) + .await?; drop(db); init(&ctx.config).await?; Ok(( diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 8a3f714d1..446bf2424 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -76,6 +76,7 @@ export const mockPatchData: DataModel = { zram: true, governor: 'performance', wifi: { + interface: 'wlan0', ssids: [], selected: null, connected: null, From aee55008337528be4282d07b237166cea0b86c00 Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Tue, 9 Apr 2024 15:10:26 -0600 Subject: [PATCH 009/125] miscellaneous bugfixes (#2597) * miscellaneous bugfixes * misc fixes --- .../bindings/GetServicePortForwardParams.ts | 2 ++ core/startos/bindings/ServerInfo.ts | 2 +- core/startos/bindings/WifiInfo.ts | 1 + core/startos/src/backup/restore.rs | 6 +++++- core/startos/src/db/mod.rs | 2 +- core/startos/src/db/model/mod.rs | 5 ++--- core/startos/src/db/model/public.rs | 10 +++++---- core/startos/src/net/vhost.rs | 21 +++++++++++++++---- core/startos/src/properties.rs | 7 +++++-- core/startos/src/s9pk/v1/manifest.rs | 8 +++---- core/startos/src/setup.rs | 6 +++++- .../ui/src/app/services/api/mock-patch.ts | 1 + 12 files changed, 50 insertions(+), 21 deletions(-) diff --git a/core/startos/bindings/GetServicePortForwardParams.ts b/core/startos/bindings/GetServicePortForwardParams.ts index 3de7e0219..70d69c674 100644 --- a/core/startos/bindings/GetServicePortForwardParams.ts +++ b/core/startos/bindings/GetServicePortForwardParams.ts @@ -1,6 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HostId } from "./HostId"; export type GetServicePortForwardParams = { packageId: string | null; internalPort: number; + hostId: HostId; }; diff --git a/core/startos/bindings/ServerInfo.ts b/core/startos/bindings/ServerInfo.ts index e505c84e7..130a5bafe 100644 --- a/core/startos/bindings/ServerInfo.ts +++ b/core/startos/bindings/ServerInfo.ts @@ -24,7 +24,7 @@ export type ServerInfo = { torAddress: string; ipInfo: { [key: string]: IpInfo }; statusInfo: ServerStatus; - wifi: WifiInfo; + wifi: WifiInfo | null; unreadNotificationCount: number; passwordHash: string; pubkey: string; diff --git a/core/startos/bindings/WifiInfo.ts b/core/startos/bindings/WifiInfo.ts index c949b1e76..59b1b901d 100644 --- a/core/startos/bindings/WifiInfo.ts +++ b/core/startos/bindings/WifiInfo.ts @@ -1,6 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. export type WifiInfo = { + interface: string; ssids: Array; selected: string | null; connected: string | null; diff --git a/core/startos/src/backup/restore.rs b/core/startos/src/backup/restore.rs index 4753a4290..41aaeca7b 100644 --- a/core/startos/src/backup/restore.rs +++ b/core/startos/src/backup/restore.rs @@ -96,7 +96,11 @@ pub async fn recover_full_embassy( .with_kind(ErrorKind::PasswordHashGeneration)?; let db = ctx.db().await?; - db.put(&ROOT, &Database::init(&os_backup.account)?).await?; + db.put( + &ROOT, + &Database::init(&os_backup.account, ctx.config.wifi_interface.clone())?, + ) + .await?; drop(db); init(&ctx.config).await?; diff --git a/core/startos/src/db/mod.rs b/core/startos/src/db/mod.rs index ab5d23efb..95e42f46b 100644 --- a/core/startos/src/db/mod.rs +++ b/core/startos/src/db/mod.rs @@ -324,7 +324,7 @@ pub struct UiParams { // #[command(display(display_serializable))] #[instrument(skip_all)] pub async fn ui(ctx: RpcContext, UiParams { pointer, value, .. }: UiParams) -> Result<(), Error> { - let ptr = "/ui" + let ptr = "/public/ui" .parse::() .with_kind(ErrorKind::Database)? + &pointer; diff --git a/core/startos/src/db/model/mod.rs b/core/startos/src/db/model/mod.rs index 9be0f8b68..8b959cdfd 100644 --- a/core/startos/src/db/model/mod.rs +++ b/core/startos/src/db/model/mod.rs @@ -2,7 +2,6 @@ use std::collections::BTreeMap; use patch_db::HasModel; use serde::{Deserialize, Serialize}; -use ts_rs::TS; use crate::account::AccountInfo; use crate::auth::Sessions; @@ -28,9 +27,9 @@ pub struct Database { pub private: Private, } impl Database { - pub fn init(account: &AccountInfo) -> Result { + pub fn init(account: &AccountInfo, wifi_interface: Option) -> Result { Ok(Self { - public: Public::init(account)?, + public: Public::init(account, wifi_interface)?, private: Private { key_store: KeyStore::new(account)?, password: account.password.clone(), diff --git a/core/startos/src/db/model/public.rs b/core/startos/src/db/model/public.rs index d1eec5443..5b83eb74d 100644 --- a/core/startos/src/db/model/public.rs +++ b/core/startos/src/db/model/public.rs @@ -35,7 +35,7 @@ pub struct Public { pub ui: Value, } impl Public { - pub fn init(account: &AccountInfo) -> Result { + pub fn init(account: &AccountInfo, wifi_interface: Option) -> Result { let lan_address = account.hostname.lan_address().parse().unwrap(); Ok(Self { server_info: ServerInfo { @@ -60,11 +60,12 @@ impl Public { shutting_down: false, restarting: false, }, - wifi: WifiInfo { + wifi: wifi_interface.map(|interface| WifiInfo { + interface, ssids: Vec::new(), connected: None, selected: None, - }, + }), unread_notification_count: 0, password_hash: account.password.clone(), pubkey: ssh_key::PublicKey::from(&account.ssh_key) @@ -131,7 +132,7 @@ pub struct ServerInfo { pub ip_info: BTreeMap, #[serde(default)] pub status_info: ServerStatus, - pub wifi: WifiInfo, + pub wifi: Option, #[ts(type = "number")] pub unread_notification_count: u64, pub password_hash: String, @@ -206,6 +207,7 @@ pub struct UpdateProgress { #[model = "Model"] #[ts(export)] pub struct WifiInfo { + pub interface: String, pub ssids: Vec, pub selected: Option, pub connected: Option, diff --git a/core/startos/src/net/vhost.rs b/core/startos/src/net/vhost.rs index 46838ed51..47cf30ad5 100644 --- a/core/startos/src/net/vhost.rs +++ b/core/startos/src/net/vhost.rs @@ -111,7 +111,7 @@ impl VHostServer { _thread: tokio::spawn(async move { loop { match listener.accept().await { - Ok((stream, sock_addr)) => { + Ok((stream, _)) => { let stream = Box::pin(TimeoutStream::new(stream, Duration::from_secs(300))); let mut stream = BackTrackingReader::new(stream); @@ -195,9 +195,22 @@ impl VHostServer { .as_ref() .into_iter() .map(InternedString::intern) - .chain(std::iter::once(InternedString::from_display( - &sock_addr.ip(), - ))) + .chain( + db.peek() + .await + .into_public() + .into_server_info() + .into_ip_info() + .into_entries()? + .into_iter() + .flat_map(|(_, ips)| [ + ips.as_ipv4().de().map(|ip| ip.map(IpAddr::V4)), + ips.as_ipv6().de().map(|ip| ip.map(IpAddr::V6)) + ]) + .filter_map(|a| a.transpose()) + .map(|a| a.map(|ip| InternedString::from_display(&ip))) + .collect::, _>>()?, + ) .collect(); let key = db .mutate(|v| { diff --git a/core/startos/src/properties.rs b/core/startos/src/properties.rs index 5aa8a01d4..f8753ab10 100644 --- a/core/startos/src/properties.rs +++ b/core/startos/src/properties.rs @@ -1,5 +1,5 @@ use clap::Parser; -use imbl_value::Value; +use imbl_value::{json, Value}; use models::PackageId; use rpc_toolkit::command; use serde::{Deserialize, Serialize}; @@ -24,7 +24,10 @@ pub async fn properties( PropertiesParam { id }: PropertiesParam, ) -> Result { match &*ctx.services.get(&id).await { - Some(service) => service.properties().await, + Some(service) => Ok(json!({ + "version": 2, + "data": service.properties().await? + })), None => Err(Error::new( eyre!("Could not find a service with id {id}"), ErrorKind::NotFound, diff --git a/core/startos/src/s9pk/v1/manifest.rs b/core/startos/src/s9pk/v1/manifest.rs index ef346ad2b..264843766 100644 --- a/core/startos/src/s9pk/v1/manifest.rs +++ b/core/startos/src/s9pk/v1/manifest.rs @@ -19,7 +19,7 @@ fn current_version() -> Version { } #[derive(Clone, Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "kebab-case")] #[model = "Model"] pub struct Manifest { #[serde(default = "current_version")] @@ -54,7 +54,7 @@ pub struct Manifest { } #[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "kebab-case")] #[serde(tag = "type")] pub enum DependencyRequirement { OptIn { how: String }, @@ -68,7 +68,7 @@ impl DependencyRequirement { } #[derive(Clone, Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "kebab-case")] #[model = "Model"] pub struct DepInfo { pub version: VersionRange, @@ -77,7 +77,7 @@ pub struct DepInfo { } #[derive(Clone, Debug, Default, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "kebab-case")] pub struct Assets { #[serde(default)] pub license: Option, diff --git a/core/startos/src/setup.rs b/core/startos/src/setup.rs index 9120544e3..b8b761e69 100644 --- a/core/startos/src/setup.rs +++ b/core/startos/src/setup.rs @@ -419,7 +419,11 @@ async fn fresh_setup( ) -> Result<(Hostname, OnionAddressV3, X509), Error> { let account = AccountInfo::new(start_os_password, root_ca_start_time().await?)?; let db = ctx.db().await?; - db.put(&ROOT, &Database::init(&account)?).await?; + db.put( + &ROOT, + &Database::init(&account, ctx.config.wifi_interface.clone())?, + ) + .await?; drop(db); init(&ctx.config).await?; Ok(( diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 8a3f714d1..446bf2424 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -76,6 +76,7 @@ export const mockPatchData: DataModel = { zram: true, governor: 'performance', wifi: { + interface: 'wlan0', ssids: [], selected: null, connected: null, From e9166c4a7dd20c9a8ed27c6194b8e35a73eabe48 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Tue, 9 Apr 2024 15:24:05 -0600 Subject: [PATCH 010/125] fix wifi types --- core/startos/bindings/ServerInfo.ts | 6 +- core/startos/bindings/WifiInfo.ts | 4 +- core/startos/src/backup/restore.rs | 6 +- core/startos/src/context/config.rs | 3 - core/startos/src/context/rpc.rs | 9 +-- core/startos/src/db/model/mod.rs | 4 +- core/startos/src/db/model/public.rs | 26 +++---- core/startos/src/init.rs | 15 ++-- core/startos/src/net/wifi.rs | 71 +++++++++++++++---- core/startos/src/os_install/mod.rs | 4 +- core/startos/src/setup.rs | 6 +- .../ui/src/app/services/api/mock-patch.ts | 3 +- 12 files changed, 85 insertions(+), 72 deletions(-) diff --git a/core/startos/bindings/ServerInfo.ts b/core/startos/bindings/ServerInfo.ts index 130a5bafe..618d396fd 100644 --- a/core/startos/bindings/ServerInfo.ts +++ b/core/startos/bindings/ServerInfo.ts @@ -11,10 +11,6 @@ export type ServerInfo = { hostname: string; version: string; lastBackup: string | null; - /** - * Used in the wifi to determine the region to set the system to - */ - lastWifiRegion: string | null; eosVersionCompat: string; lanAddress: string; onionAddress: string; @@ -24,7 +20,7 @@ export type ServerInfo = { torAddress: string; ipInfo: { [key: string]: IpInfo }; statusInfo: ServerStatus; - wifi: WifiInfo | null; + wifi: WifiInfo; unreadNotificationCount: number; passwordHash: string; pubkey: string; diff --git a/core/startos/bindings/WifiInfo.ts b/core/startos/bindings/WifiInfo.ts index 59b1b901d..812cc431f 100644 --- a/core/startos/bindings/WifiInfo.ts +++ b/core/startos/bindings/WifiInfo.ts @@ -1,8 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. export type WifiInfo = { - interface: string; + interface: string | null; ssids: Array; selected: string | null; - connected: string | null; + lastRegion: string | null; }; diff --git a/core/startos/src/backup/restore.rs b/core/startos/src/backup/restore.rs index 41aaeca7b..4753a4290 100644 --- a/core/startos/src/backup/restore.rs +++ b/core/startos/src/backup/restore.rs @@ -96,11 +96,7 @@ pub async fn recover_full_embassy( .with_kind(ErrorKind::PasswordHashGeneration)?; let db = ctx.db().await?; - db.put( - &ROOT, - &Database::init(&os_backup.account, ctx.config.wifi_interface.clone())?, - ) - .await?; + db.put(&ROOT, &Database::init(&os_backup.account)?).await?; drop(db); init(&ctx.config).await?; diff --git a/core/startos/src/context/config.rs b/core/startos/src/context/config.rs index 55065e816..0e6d9feff 100644 --- a/core/startos/src/context/config.rs +++ b/core/startos/src/context/config.rs @@ -91,8 +91,6 @@ impl ClientConfig { pub struct ServerConfig { #[arg(short = 'c', long = "config")] pub config: Option, - #[arg(long = "wifi-interface")] - pub wifi_interface: Option, #[arg(long = "ethernet-interface")] pub ethernet_interface: Option, #[arg(skip)] @@ -117,7 +115,6 @@ impl ContextConfig for ServerConfig { self.config.take() } fn merge_with(&mut self, other: Self) { - self.wifi_interface = self.wifi_interface.take().or(other.wifi_interface); self.ethernet_interface = self.ethernet_interface.take().or(other.ethernet_interface); self.os_partitions = self.os_partitions.take().or(other.os_partitions); self.bind_rpc = self.bind_rpc.take().or(other.bind_rpc); diff --git a/core/startos/src/context/rpc.rs b/core/startos/src/context/rpc.rs index a51e23b08..5adab5e58 100644 --- a/core/startos/src/context/rpc.rs +++ b/core/startos/src/context/rpc.rs @@ -26,7 +26,7 @@ use crate::init::check_time_is_synchronized; use crate::lxc::{LxcContainer, LxcManager}; use crate::middleware::auth::HashSessionToken; use crate::net::net_controller::NetController; -use crate::net::utils::find_eth_iface; +use crate::net::utils::{find_eth_iface, find_wifi_iface}; use crate::net::wifi::WpaCli; use crate::prelude::*; use crate::service::ServiceMap; @@ -132,6 +132,8 @@ impl RpcContext { }); } + let wifi_interface = find_wifi_iface().await?; + let seed = Arc::new(RpcContextSeed { is_closed: AtomicBool::new(false), datadir: config.datadir().to_path_buf(), @@ -141,7 +143,7 @@ impl RpcContext { ErrorKind::Filesystem, ) })?, - wifi_interface: config.wifi_interface.clone(), + wifi_interface: wifi_interface.clone(), ethernet_interface: if let Some(eth) = config.ethernet_interface.clone() { eth } else { @@ -158,8 +160,7 @@ impl RpcContext { lxc_manager: Arc::new(LxcManager::new()), open_authed_websockets: Mutex::new(BTreeMap::new()), rpc_stream_continuations: Mutex::new(BTreeMap::new()), - wifi_manager: config - .wifi_interface + wifi_manager: wifi_interface .clone() .map(|i| Arc::new(RwLock::new(WpaCli::init(i)))), current_secret: Arc::new( diff --git a/core/startos/src/db/model/mod.rs b/core/startos/src/db/model/mod.rs index 8b959cdfd..cae1bfe51 100644 --- a/core/startos/src/db/model/mod.rs +++ b/core/startos/src/db/model/mod.rs @@ -27,9 +27,9 @@ pub struct Database { pub private: Private, } impl Database { - pub fn init(account: &AccountInfo, wifi_interface: Option) -> Result { + pub fn init(account: &AccountInfo) -> Result { Ok(Self { - public: Public::init(account, wifi_interface)?, + public: Public::init(account)?, private: Private { key_store: KeyStore::new(account)?, password: account.password.clone(), diff --git a/core/startos/src/db/model/public.rs b/core/startos/src/db/model/public.rs index 5b83eb74d..a51303cf4 100644 --- a/core/startos/src/db/model/public.rs +++ b/core/startos/src/db/model/public.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::net::{Ipv4Addr, Ipv6Addr}; use chrono::{DateTime, Utc}; @@ -35,7 +35,7 @@ pub struct Public { pub ui: Value, } impl Public { - pub fn init(account: &AccountInfo, wifi_interface: Option) -> Result { + pub fn init(account: &AccountInfo) -> Result { let lan_address = account.hostname.lan_address().parse().unwrap(); Ok(Self { server_info: ServerInfo { @@ -45,7 +45,6 @@ impl Public { version: Current::new().semver().into(), hostname: account.hostname.no_dot_host_name(), last_backup: None, - last_wifi_region: None, eos_version_compat: Current::new().compat().clone(), lan_address, onion_address: account.tor_key.public().get_onion_address(), @@ -60,12 +59,7 @@ impl Public { shutting_down: false, restarting: false, }, - wifi: wifi_interface.map(|interface| WifiInfo { - interface, - ssids: Vec::new(), - connected: None, - selected: None, - }), + wifi: WifiInfo::default(), unread_notification_count: 0, password_hash: account.password.clone(), pubkey: ssh_key::PublicKey::from(&account.ssh_key) @@ -117,9 +111,6 @@ pub struct ServerInfo { pub version: Version, #[ts(type = "string | null")] pub last_backup: Option>, - /// Used in the wifi to determine the region to set the system to - #[ts(type = "string | null")] - pub last_wifi_region: Option, #[ts(type = "string")] pub eos_version_compat: VersionRange, #[ts(type = "string")] @@ -132,7 +123,7 @@ pub struct ServerInfo { pub ip_info: BTreeMap, #[serde(default)] pub status_info: ServerStatus, - pub wifi: Option, + pub wifi: WifiInfo, #[ts(type = "number")] pub unread_notification_count: u64, pub password_hash: String, @@ -202,15 +193,16 @@ pub struct UpdateProgress { pub downloaded: u64, } -#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] #[serde(rename_all = "camelCase")] #[model = "Model"] #[ts(export)] pub struct WifiInfo { - pub interface: String, - pub ssids: Vec, + pub interface: Option, + pub ssids: BTreeSet, pub selected: Option, - pub connected: Option, + #[ts(type = "string | null")] + pub last_region: Option, } #[derive(Debug, Deserialize, Serialize, TS)] diff --git a/core/startos/src/init.rs b/core/startos/src/init.rs index d1d5a9943..3f7d7f1bc 100644 --- a/core/startos/src/init.rs +++ b/core/startos/src/init.rs @@ -232,15 +232,12 @@ pub async fn init(cfg: &ServerConfig) -> Result { .invoke(crate::ErrorKind::OpenSsl) .await?; - if let Some(wifi_interface) = &cfg.wifi_interface { - crate::net::wifi::synchronize_wpa_supplicant_conf( - &cfg.datadir().join("main"), - wifi_interface, - &server_info.last_wifi_region, - ) - .await?; - tracing::info!("Synchronized WiFi"); - } + crate::net::wifi::synchronize_wpa_supplicant_conf( + &cfg.datadir().join("main"), + &mut server_info.wifi, + ) + .await?; + tracing::info!("Synchronized WiFi"); let should_rebuild = tokio::fs::metadata(SYSTEM_REBUILD_PATH).await.is_ok() || &*server_info.version < &emver::Version::new(0, 3, 2, 0) diff --git a/core/startos/src/net/wifi.rs b/core/startos/src/net/wifi.rs index 9de177bfe..93f610a42 100644 --- a/core/startos/src/net/wifi.rs +++ b/core/startos/src/net/wifi.rs @@ -16,6 +16,8 @@ use tracing::instrument; use ts_rs::TS; use crate::context::{CliContext, RpcContext}; +use crate::db::model::public::WifiInfo; +use crate::net::utils::find_wifi_iface; use crate::prelude::*; use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat}; use crate::util::Invoke; @@ -137,6 +139,18 @@ pub async fn add(ctx: RpcContext, AddParams { ssid, password }: AddParams) -> Re ErrorKind::Wifi, )); } + ctx.db + .mutate(|db| { + db.as_public_mut() + .as_server_info_mut() + .as_wifi_mut() + .as_ssids_mut() + .mutate(|s| { + s.insert(ssid); + Ok(()) + }) + }) + .await?; Ok(()) } #[derive(Deserialize, Serialize, Parser, TS)] @@ -190,6 +204,17 @@ pub async fn connect(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result ErrorKind::Wifi, )); } + + ctx.db + .mutate(|db| { + let wifi = db.as_public_mut().as_server_info_mut().as_wifi_mut(); + wifi.as_ssids_mut().mutate(|s| { + s.insert(ssid.clone()); + Ok(()) + })?; + wifi.as_selected_mut().ser(&Some(ssid)) + }) + .await?; Ok(()) } @@ -215,11 +240,23 @@ pub async fn delete(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result< } wpa_supplicant.remove_network(ctx.db.clone(), &ssid).await?; + + ctx.db + .mutate(|db| { + let wifi = db.as_public_mut().as_server_info_mut().as_wifi_mut(); + wifi.as_ssids_mut().mutate(|s| { + s.remove(&ssid.0); + Ok(()) + })?; + wifi.as_selected_mut() + .map_mutate(|s| Ok(s.filter(|s| s == &ssid.0))) + }) + .await?; Ok(()) } #[derive(serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] -pub struct WiFiInfo { +pub struct WifiListInfo { ssids: HashMap, connected: Option, country: Option, @@ -228,7 +265,7 @@ pub struct WiFiInfo { } #[derive(serde::Serialize, serde::Deserialize, Clone)] #[serde(rename_all = "camelCase")] -pub struct WifiListInfo { +pub struct WifiListInfoLow { strength: SignalStrength, security: Vec, } @@ -239,8 +276,8 @@ pub struct WifiListOut { strength: SignalStrength, security: Vec, } -pub type WifiList = HashMap; -fn display_wifi_info(params: WithIoFormat, info: WiFiInfo) { +pub type WifiList = HashMap; +fn display_wifi_info(params: WithIoFormat, info: WifiListInfo) { use prettytable::*; if let Some(format) = params.format { @@ -330,7 +367,7 @@ fn display_wifi_list(params: WithIoFormat, info: Vec) { // #[command(display(display_wifi_info))] #[instrument(skip_all)] -pub async fn get(ctx: RpcContext, _: Empty) -> Result { +pub async fn get(ctx: RpcContext, _: Empty) -> Result { let wifi_manager = wifi_manager(&ctx)?; let wpa_supplicant = wifi_manager.read().await; let (list_networks, current_res, country_res, ethernet_res, signal_strengths) = tokio::join!( @@ -368,7 +405,7 @@ pub async fn get(ctx: RpcContext, _: Empty) -> Result { }) .collect(); let current = current_res?; - Ok(WiFiInfo { + Ok(WifiListInfo { ssids, connected: current, country: country_res?, @@ -477,7 +514,7 @@ impl SignalStrength { } #[derive(Debug, Clone)] -pub struct WifiInfo { +pub struct WifiInfoLow { ssid: Ssid, device: Option, } @@ -604,7 +641,7 @@ impl WpaCli { Ok(()) } #[instrument(skip_all)] - pub async fn list_networks_low(&self) -> Result, Error> { + pub async fn list_networks_low(&self) -> Result, Error> { let r = Command::new("nmcli") .arg("-t") .arg("c") @@ -623,13 +660,13 @@ impl WpaCli { if !connection_type.contains("wireless") { return None; } - let info = WifiInfo { + let info = WifiInfoLow { ssid: name, device: device.map(|x| x.to_owned()), }; Some((uuid, info)) }) - .collect::>()) + .collect::>()) } #[instrument(skip_all)] @@ -652,7 +689,7 @@ impl WpaCli { values.next()?.split(' ').map(|x| x.to_owned()).collect(); Some(( ssid, - WifiListInfo { + WifiListInfoLow { strength: signal, security, }, @@ -686,7 +723,8 @@ impl WpaCli { db.mutate(|d| { d.as_public_mut() .as_server_info_mut() - .as_last_wifi_region_mut() + .as_wifi_mut() + .as_last_region_mut() .ser(&new_country) }) .await @@ -837,9 +875,12 @@ impl TypedValueParser for CountryCodeParser { #[instrument(skip_all)] pub async fn synchronize_wpa_supplicant_conf>( main_datadir: P, - wifi_iface: &str, - last_country_code: &Option, + wifi: &mut WifiInfo, ) -> Result<(), Error> { + wifi.interface = find_wifi_iface().await?; + let Some(wifi_iface) = &wifi.interface else { + return Ok(()); + }; let persistent = main_datadir.as_ref().join("system-connections"); tracing::debug!("persistent: {:?}", persistent); // let supplicant = Path::new("/etc/wpa_supplicant.conf"); @@ -863,7 +904,7 @@ pub async fn synchronize_wpa_supplicant_conf>( .arg("up") .invoke(ErrorKind::Wifi) .await?; - if let Some(last_country_code) = last_country_code { + if let Some(last_country_code) = wifi.last_region { tracing::info!("Setting the region"); let _ = Command::new("iw") .arg("reg") diff --git a/core/startos/src/os_install/mod.rs b/core/startos/src/os_install/mod.rs index 0abd1d23e..4bbbe70a6 100644 --- a/core/startos/src/os_install/mod.rs +++ b/core/startos/src/os_install/mod.rs @@ -17,7 +17,7 @@ use crate::disk::mount::filesystem::{MountType, ReadWrite}; use crate::disk::mount::guard::{GenericMountGuard, MountGuard, TmpMountGuard}; use crate::disk::util::{DiskInfo, PartitionTable}; use crate::disk::OsPartitionInfo; -use crate::net::utils::{find_eth_iface, find_wifi_iface}; +use crate::net::utils::find_eth_iface; use crate::util::serde::IoFormat; use crate::util::Invoke; use crate::ARCH; @@ -140,7 +140,6 @@ pub async fn execute( ) })?; let eth_iface = find_eth_iface().await?; - let wifi_iface = find_wifi_iface().await?; overwrite |= disk.guid.is_none() && disk.partitions.iter().all(|p| p.guid.is_none()); @@ -260,7 +259,6 @@ pub async fn execute( IoFormat::Yaml.to_vec(&ServerConfig { os_partitions: Some(part_info.clone()), ethernet_interface: Some(eth_iface), - wifi_interface: wifi_iface, ..Default::default() })?, ) diff --git a/core/startos/src/setup.rs b/core/startos/src/setup.rs index b8b761e69..9120544e3 100644 --- a/core/startos/src/setup.rs +++ b/core/startos/src/setup.rs @@ -419,11 +419,7 @@ async fn fresh_setup( ) -> Result<(Hostname, OnionAddressV3, X509), Error> { let account = AccountInfo::new(start_os_password, root_ca_start_time().await?)?; let db = ctx.db().await?; - db.put( - &ROOT, - &Database::init(&account, ctx.config.wifi_interface.clone())?, - ) - .await?; + db.put(&ROOT, &Database::init(&account)?).await?; drop(db); init(&ctx.config).await?; Ok(( diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 446bf2424..ed0edacbe 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -55,7 +55,6 @@ export const mockPatchData: DataModel = { ipv6Range: 'FE80:CD00:0000:0CDE:1257:0000:211E:729CD/64', }, }, - lastWifiRegion: null, unreadNotificationCount: 4, // password is asdfasdf passwordHash: @@ -79,7 +78,7 @@ export const mockPatchData: DataModel = { interface: 'wlan0', ssids: [], selected: null, - connected: null, + lastRegion: null, }, }, packageData: { From 932b53d92d6118b37faad83190af0cad48a42ca5 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Tue, 9 Apr 2024 21:06:06 -0600 Subject: [PATCH 011/125] deprecate wifi --- web/package-lock.json | 4 +-- .../ui/src/app/app/menu/menu.component.html | 6 +++++ .../ui/src/app/app/menu/menu.component.ts | 2 ++ .../server-show/server-show.page.html | 4 +-- .../server-show/server-show.page.ts | 3 ++- .../pages/server-routes/wifi/wifi.page.html | 27 ++++++++++--------- .../app/pages/server-routes/wifi/wifi.page.ts | 12 ++++++++- 7 files changed, 39 insertions(+), 19 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 90a6ad58d..68156eb21 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "startos-ui", - "version": "0.3.5.1", + "version": "0.3.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "startos-ui", - "version": "0.3.5.1", + "version": "0.3.5.2", "dependencies": { "@angular/animations": "^14.1.0", "@angular/common": "^14.1.0", diff --git a/web/projects/ui/src/app/app/menu/menu.component.html b/web/projects/ui/src/app/app/menu/menu.component.html index 6db19dea6..0814c81ad 100644 --- a/web/projects/ui/src/app/app/menu/menu.component.html +++ b/web/projects/ui/src/app/app/menu/menu.component.html @@ -22,6 +22,12 @@ {{ page.title }} +

{{ button.title }}

{{ button.description }}

-

diff --git a/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts b/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts index b479f1750..73763578e 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts @@ -40,6 +40,7 @@ export class ServerShowPage { readonly server$ = this.patch.watch$('serverInfo') readonly showUpdate$ = this.eosService.showUpdate$ readonly showDiskRepair$ = this.ClientStorageService.showDiskRepair$ + readonly wifiConnected$ = this.patch.watch$('serverInfo', 'wifi', 'selected') constructor( private readonly alertCtrl: AlertController, @@ -553,7 +554,7 @@ export class ServerShowPage { }, { title: 'WiFi', - description: 'Add or remove WiFi networks', + description: 'WiFi is deprecated. Click to learn more.', icon: 'wifi', action: () => this.navCtrl.navigateForward(['wifi'], { relativeTo: this.route }), diff --git a/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html b/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html index 011704cc2..08387fcbc 100644 --- a/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html +++ b/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html @@ -16,21 +16,22 @@ - + + -

- Adding WiFi credentials to your StartOS allows you to remove the - Ethernet cable and move the device anywhere you want. StartOS will - automatically connect to available networks. - - View instructions - -

+

WiFi is deprecated

+

+ WiFi will be eliminated from StartOS in version v0.4.0, expected soon. + Before then, we highly recommend switching your server to a direct, + Ethernet connection, which is faster and more reliable. If using + Ethernet is not possible for you, we recommend using a WiFi extender + instead. +

+ + Learn More + + Country diff --git a/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts b/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts index bac270613..e6d5da252 100644 --- a/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core' +import { Component, Inject } from '@angular/core' import { ActionSheetController, AlertController, @@ -14,6 +14,7 @@ import { RR } from 'src/app/services/api/api.types' import { pauseFor, ErrorToastService } from '@start9labs/shared' import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' import { ConfigService } from 'src/app/services/config.service' +import { WINDOW } from '@ng-web-apis/common' @Component({ selector: 'wifi', @@ -36,12 +37,21 @@ export class WifiPage { private readonly errToast: ErrorToastService, private readonly actionCtrl: ActionSheetController, private readonly config: ConfigService, + @Inject(WINDOW) private readonly windowRef: Window, ) {} async ngOnInit() { await this.getWifi() } + async openDocs() { + this.windowRef.open( + 'https://docs.start9.com/user-manual/wifi.html', + '_blank', + 'noreferrer', + ) + } + async getWifi(timeout: number = 0): Promise { this.loading = true try { From 711c82472c072609323b3d9653ffb695ebec956f Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Mon, 15 Apr 2024 10:00:56 -0600 Subject: [PATCH 012/125] Feature/debian runtime (#2600) * wip * fix build * run debian update in systemd-nspawn * bugfix * fix build * free up space before image build --- .github/workflows/startos-iso.yaml | 8 +++- Makefile | 8 ++-- container-runtime/Dockerfile | 4 -- container-runtime/container-runtime.service | 9 ++++ container-runtime/containerRuntime.rc | 10 ---- container-runtime/deb-install.sh | 19 ++++++++ container-runtime/download-base-image.sh | 6 +-- .../Systems/SystemForEmbassy/MainLoop.ts | 47 +++++++++++++++++++ .../Systems/SystemForEmbassy/matchManifest.ts | 25 +++++++--- container-runtime/update-image.sh | 21 ++++----- .../src/service/service_effect_handler.rs | 16 +++++-- dpkg-build.sh | 1 + patch-db | 2 +- 13 files changed, 131 insertions(+), 45 deletions(-) delete mode 100644 container-runtime/Dockerfile create mode 100644 container-runtime/container-runtime.service delete mode 100644 container-runtime/containerRuntime.rc create mode 100644 container-runtime/deb-install.sh diff --git a/.github/workflows/startos-iso.yaml b/.github/workflows/startos-iso.yaml index 184c2b0c7..7d4a43685 100644 --- a/.github/workflows/startos-iso.yaml +++ b/.github/workflows/startos-iso.yaml @@ -82,8 +82,8 @@ jobs: - name: Set up docker QEMU uses: docker/setup-qemu-action@v2 - - name: Set up system QEMU - run: sudo apt-get update && sudo apt-get install -y qemu-user-static + - name: Set up system dependencies + run: sudo apt-get update && sudo apt-get install -y qemu-user-static systemd-container - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 @@ -140,6 +140,10 @@ jobs: }')[matrix.platform] }} steps: + - name: Free space + run: rm -rf /opt/hostedtoolcache* + if: ${{ github.event.inputs.runner != 'fast' }} + - uses: actions/checkout@v3 with: submodules: recursive diff --git a/Makefile b/Makefile index c988b11b8..e1ba4ed53 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ PATCH_DB_CLIENT_SRC := $(shell git ls-files --recurse-submodules patch-db/client GZIP_BIN := $(shell which pigz || which gzip) TAR_BIN := $(shell which gtar || which tar) COMPILED_TARGETS := $(BINS) system-images/compat/docker-images/$(ARCH).tar system-images/utils/docker-images/$(ARCH).tar system-images/binfmt/docker-images/$(ARCH).tar container-runtime/rootfs.$(ARCH).squashfs -ALL_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo cargo-deps/aarch64-unknown-linux-musl/release/pi-beep; fi) $(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]; then echo cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console; fi') $(PLATFORM_FILE) sdk/lib/test +ALL_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo cargo-deps/aarch64-unknown-linux-musl/release/pi-beep; fi) $(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]; then echo cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console; fi') $(PLATFORM_FILE) ifeq ($(REMOTE),) mkdir = mkdir -p $1 @@ -103,7 +103,7 @@ deb: results/$(BASENAME).deb debian/control: build/lib/depends build/lib/conflicts ./debuild/control.sh -results/$(BASENAME).deb: dpkg-build.sh $(DEBIAN_SRC) $(VERSION_FILE) $(PLATFORM_FILE) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) +results/$(BASENAME).deb: dpkg-build.sh $(DEBIAN_SRC) $(ALL_TARGETS) PLATFORM=$(PLATFORM) ./dpkg-build.sh $(IMAGE_TYPE): results/$(BASENAME).$(IMAGE_TYPE) @@ -173,7 +173,7 @@ emulate-reflash: $(ALL_TARGETS) upload-ota: results/$(BASENAME).squashfs TARGET=$(TARGET) KEY=$(KEY) ./upload-ota.sh -container-runtime/alpine.$(ARCH).squashfs: +container-runtime/debian.$(ARCH).squashfs: ARCH=$(ARCH) ./container-runtime/download-base-image.sh container-runtime/node_modules: container-runtime/package.json container-runtime/package-lock.json sdk/dist @@ -197,7 +197,7 @@ container-runtime/dist/node_modules container-runtime/dist/package.json containe ./container-runtime/install-dist-deps.sh touch container-runtime/dist/node_modules -container-runtime/rootfs.$(ARCH).squashfs: container-runtime/alpine.$(ARCH).squashfs container-runtime/containerRuntime.rc container-runtime/update-image.sh container-runtime/dist/index.js container-runtime/dist/node_modules core/target/$(ARCH)-unknown-linux-musl/release/containerbox | sudo +container-runtime/rootfs.$(ARCH).squashfs: container-runtime/debian.$(ARCH).squashfs container-runtime/container-runtime.service container-runtime/update-image.sh container-runtime/deb-install.sh container-runtime/dist/index.js container-runtime/dist/node_modules core/target/$(ARCH)-unknown-linux-musl/release/containerbox | sudo ARCH=$(ARCH) ./container-runtime/update-image.sh build/lib/depends build/lib/conflicts: build/dpkg-deps/* diff --git a/container-runtime/Dockerfile b/container-runtime/Dockerfile deleted file mode 100644 index f936ee11b..000000000 --- a/container-runtime/Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM node:18-alpine - -ADD ./startInit.js /usr/local/lib/startInit.js -ADD ./entrypoint.sh /usr/local/bin/entrypoint.sh \ No newline at end of file diff --git a/container-runtime/container-runtime.service b/container-runtime/container-runtime.service new file mode 100644 index 000000000..b9d5ec5ae --- /dev/null +++ b/container-runtime/container-runtime.service @@ -0,0 +1,9 @@ +[Unit] +Description=StartOS Container Runtime + +[Service] +Type=simple +ExecStart=/usr/bin/node --experimental-detect-module --unhandled-rejections=warn /usr/lib/startos/init/index.js + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/container-runtime/containerRuntime.rc b/container-runtime/containerRuntime.rc deleted file mode 100644 index 203b99659..000000000 --- a/container-runtime/containerRuntime.rc +++ /dev/null @@ -1,10 +0,0 @@ -#!/sbin/openrc-run - -name=containerRuntime -#cfgfile="/etc/containerRuntime/containerRuntime.conf" -command="/usr/bin/node" -command_args="--experimental-detect-module --unhandled-rejections=warn /usr/lib/startos/init/index.js" -pidfile="/run/containerRuntime.pid" -command_background="yes" -output_log="/var/log/containerRuntime.log" -error_log="/var/log/containerRuntime.err" diff --git a/container-runtime/deb-install.sh b/container-runtime/deb-install.sh new file mode 100644 index 000000000..9a64dbbdd --- /dev/null +++ b/container-runtime/deb-install.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -e + +mkdir -p /run/systemd/resolve +echo "nameserver 8.8.8.8" > /run/systemd/resolve/stub-resolv.conf + +apt-get update +apt-get install -y curl rsync + +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash +source ~/.bashrc +nvm install 20 + +ln -s $(which node) /usr/bin/node + +systemctl enable container-runtime.service + +rm -rf /run/systemd \ No newline at end of file diff --git a/container-runtime/download-base-image.sh b/container-runtime/download-base-image.sh index 23a140ea5..f7dc4c844 100755 --- a/container-runtime/download-base-image.sh +++ b/container-runtime/download-base-image.sh @@ -4,8 +4,8 @@ cd "$(dirname "${BASH_SOURCE[0]}")" set -e -DISTRO=alpine -VERSION=3.19 +DISTRO=debian +VERSION=bookworm ARCH=${ARCH:-$(uname -m)} FLAVOR=default @@ -16,4 +16,4 @@ elif [ "$_ARCH" = "aarch64" ]; then _ARCH=arm64 fi -curl https://images.linuxcontainers.org/$(curl --silent https://images.linuxcontainers.org/meta/1.0/index-system | grep "^$DISTRO;$VERSION;$_ARCH;$FLAVOR;" | head -n1 | sed 's/^.*;//g')/rootfs.squashfs --output alpine.${ARCH}.squashfs \ No newline at end of file +curl https://images.linuxcontainers.org/$(curl --silent https://images.linuxcontainers.org/meta/1.0/index-system | grep "^$DISTRO;$VERSION;$_ARCH;$FLAVOR;" | head -n1 | sed 's/^.*;//g')/rootfs.squashfs --output debian.${ARCH}.squashfs \ No newline at end of file diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts index f2bcc5eba..c28615fc9 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts @@ -40,6 +40,7 @@ export class MainLoop { ...system.manifest.main.args, ] + await this.setupInterfaces(effects) await effects.setMainStatus({ status: "running" }) const jsMain = (this.system.moduleCode as any)?.jsMain const dockerProcedureContainer = await DockerProcedureContainer.of( @@ -69,6 +70,52 @@ export class MainLoop { } } + private async setupInterfaces(effects: HostSystemStartOs) { + for (const interfaceId in this.system.manifest.interfaces) { + const iface = this.system.manifest.interfaces[interfaceId] + const internalPorts = new Set() + for (const port of Object.values( + iface["tor-config"]?.["port-mapping"] || {}, + )) { + internalPorts.add(parseInt(port)) + } + for (const port of Object.values(iface["lan-config"] || {})) { + internalPorts.add(port.internal) + } + for (const internalPort of internalPorts) { + const torConf = Object.entries( + iface["tor-config"]?.["port-mapping"] || {}, + ) + .map(([external, internal]) => ({ + internal: parseInt(internal), + external: parseInt(external), + })) + .find((conf) => conf.internal == internalPort) + const lanConf = Object.entries(iface["lan-config"] || {}) + .map(([external, conf]) => ({ + external: parseInt(external), + ...conf, + })) + .find((conf) => conf.internal == internalPort) + await effects.bind({ + kind: "multi", + id: interfaceId, + internalPort, + preferredExternalPort: torConf?.external || internalPort, + scheme: "http", + secure: null, + addSsl: lanConf?.ssl + ? { + scheme: "https", + preferredExternalPort: lanConf.external, + alpn: { specified: ["http/1.1"] }, + } + : null, + }) + } + } + } + public async clean(options?: { timeout?: number }) { const { mainEvent, healthLoops } = this const main = await mainEvent diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.ts index 9b70f884b..85cd1bac3 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.ts @@ -68,13 +68,24 @@ export const matchManifest = object( volumes: dictionary([string, matchVolume]), interfaces: dictionary([ string, - object({ - name: string, - "tor-config": object({}), - "lan-config": object({}), - ui: boolean, - protocols: array(string), - }), + object( + { + name: string, + "tor-config": object({ + "port-mapping": dictionary([string, string]), + }), + "lan-config": dictionary([ + string, + object({ + ssl: boolean, + internal: number, + }), + ]), + ui: boolean, + protocols: array(string), + }, + ["lan-config", "tor-config"], + ), ]), backup: object({ create: matchProcedure, diff --git a/container-runtime/update-image.sh b/container-runtime/update-image.sh index bef525b3b..1c571408a 100755 --- a/container-runtime/update-image.sh +++ b/container-runtime/update-image.sh @@ -4,12 +4,11 @@ cd "$(dirname "${BASH_SOURCE[0]}")" set -e - - -if mountpoint tmp/combined; then sudo umount tmp/combined; fi +if mountpoint tmp/combined; then sudo umount -R tmp/combined; fi if mountpoint tmp/lower; then sudo umount tmp/lower; fi +sudo rm -rf tmp mkdir -p tmp/lower tmp/upper tmp/work tmp/combined -sudo mount alpine.${ARCH}.squashfs tmp/lower +sudo mount debian.${ARCH}.squashfs tmp/lower sudo mount -t overlay -olowerdir=tmp/lower,upperdir=tmp/upper,workdir=tmp/work overlay tmp/combined QEMU= @@ -18,21 +17,21 @@ if [ "$ARCH" != "$(uname -m)" ]; then sudo cp $(which qemu-$ARCH-static) tmp/combined${QEMU} fi -echo "nameserver 8.8.8.8" | sudo tee tmp/combined/etc/resolv.conf # TODO - delegate to host resolver? -sudo chroot tmp/combined $QEMU /sbin/apk add nodejs rsync sudo mkdir -p tmp/combined/usr/lib/startos/ sudo rsync -a --copy-unsafe-links dist/ tmp/combined/usr/lib/startos/init/ -sudo cp containerRuntime.rc tmp/combined/etc/init.d/containerRuntime +sudo chown -R 0:0 tmp/combined/usr/lib/startos/ +sudo cp container-runtime.service tmp/combined/lib/systemd/system/container-runtime.service +sudo chown 0:0 tmp/combined/lib/systemd/system/container-runtime.service sudo cp ../core/target/$ARCH-unknown-linux-musl/release/containerbox tmp/combined/usr/bin/start-cli -sudo chmod +x tmp/combined/etc/init.d/containerRuntime -sudo chroot tmp/combined $QEMU /sbin/rc-update add containerRuntime default +sudo chown 0:0 tmp/combined/usr/bin/start-cli +echo container-runtime | sha256sum | head -c 32 | cat - <(echo) | sudo tee tmp/combined/etc/machine-id +cat deb-install.sh | sudo systemd-nspawn --console=pipe -D tmp/combined $QEMU /bin/bash +sudo truncate -s 0 tmp/combined/etc/machine-id if [ -n "$QEMU" ]; then sudo rm tmp/combined${QEMU} fi -sudo truncate -s 0 tmp/combined/etc/resolv.conf -sudo chown -R 0:0 tmp/combined rm -f rootfs.${ARCH}.squashfs mkdir -p ../build/lib/container-runtime sudo mksquashfs tmp/combined rootfs.${ARCH}.squashfs diff --git a/core/startos/src/service/service_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs index 0effccd5e..6d0f87c53 100644 --- a/core/startos/src/service/service_effect_handler.rs +++ b/core/startos/src/service/service_effect_handler.rs @@ -326,7 +326,7 @@ async fn get_service_port_forward( data: GetServicePortForwardParams, ) -> Result { let internal_port = data.internal_port as u16; - + let context = context.deref()?; let net_service = context.persistent_container.net_service.lock().await; net_service.get_ext_port(data.host_id, internal_port) @@ -446,8 +446,18 @@ struct BindParams { #[serde(flatten)] options: BindOptions, } -async fn bind(_: AnyContext, BindParams { .. }: BindParams) -> Result { - todo!() +async fn bind( + context: EffectContext, + BindParams { + kind, + id, + internal_port, + options, + }: BindParams, +) -> Result<(), Error> { + let ctx = context.deref()?; + let mut svc = ctx.persistent_container.net_service.lock().await; + svc.bind(kind, id, internal_port, options).await } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] diff --git a/dpkg-build.sh b/dpkg-build.sh index e8ffdb0ac..783c205b3 100755 --- a/dpkg-build.sh +++ b/dpkg-build.sh @@ -17,6 +17,7 @@ fi rm -rf dpkg-workdir/$BASENAME mkdir -p dpkg-workdir/$BASENAME +make make install DESTDIR=dpkg-workdir/$BASENAME DEPENDS=$(cat dpkg-workdir/$BASENAME/usr/lib/startos/depends | tr $'\n' ',' | sed 's/,,\+/,/g' | sed 's/,$//') diff --git a/patch-db b/patch-db index 3dc11afd4..e4a3f7b57 160000 --- a/patch-db +++ b/patch-db @@ -1 +1 @@ -Subproject commit 3dc11afd46d93094ac52ae1fef311a91c4561e8c +Subproject commit e4a3f7b577df56c611b21d5ad03eb459e80fb919 From 9eff920989dc53550e14cbc2aeddad19edfbd90c Mon Sep 17 00:00:00 2001 From: Jade <2364004+Blu-J@users.noreply.github.com> Date: Wed, 17 Apr 2024 15:46:10 -0600 Subject: [PATCH 013/125] Feat/logging (#2602) * wip: Working on something to help * chore: Add in some of the logging now * chore: fix the type to interned instead of id * wip * wip * chore: fix the logging by moving levels * Apply suggestions from code review * mount at machine id for journal * Persistant * limit log size * feat: Actually logging and mounting now * fix: Get the logs from the previous versions of the boot * Chore: Add the boot id --------- Co-authored-by: Aiden McClelland --- container-runtime/RPCSpec.md | 34 +++- container-runtime/deb-install.sh | 6 +- container-runtime/package.json | 2 +- core/startos/src/context/rpc.rs | 4 +- core/startos/src/logs.rs | 163 ++++++++++++------ core/startos/src/lxc/mod.rs | 125 ++++++++++---- core/startos/src/s9pk/v2/mod.rs | 2 +- core/startos/src/service/mod.rs | 58 ++++--- .../src/service/persistent_container.rs | 28 +-- core/startos/src/service/service_map.rs | 6 +- 10 files changed, 308 insertions(+), 120 deletions(-) diff --git a/container-runtime/RPCSpec.md b/container-runtime/RPCSpec.md index 679671614..fd1014add 100644 --- a/container-runtime/RPCSpec.md +++ b/container-runtime/RPCSpec.md @@ -3,38 +3,61 @@ ## Methods ### init + initialize runtime (mount `/proc`, `/sys`, `/dev`, and `/run` to each image in `/media/images`) called after os has mounted js and images to the container + #### args + `[]` + #### response + `null` ### exit + shutdown runtime + #### args + `[]` + #### response + `null` ### start + run main method if not already running + #### args + `[]` + #### response + `null` ### stop + stop main method by sending SIGTERM to child processes, and SIGKILL after timeout + #### args + `{ timeout: millis }` + #### response + `null` ### execute + run a specific package procedure -#### args + +#### args + ```ts { procedure: JsonPath, @@ -42,12 +65,17 @@ run a specific package procedure timeout: millis, } ``` + #### response + `any` ### sandbox + run a specific package procedure in sandbox mode -#### args + +#### args + ```ts { procedure: JsonPath, @@ -55,5 +83,7 @@ run a specific package procedure in sandbox mode timeout: millis, } ``` + #### response + `any` diff --git a/container-runtime/deb-install.sh b/container-runtime/deb-install.sh index 9a64dbbdd..b439c6308 100644 --- a/container-runtime/deb-install.sh +++ b/container-runtime/deb-install.sh @@ -11,9 +11,13 @@ apt-get install -y curl rsync curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash source ~/.bashrc nvm install 20 - ln -s $(which node) /usr/bin/node +sed -i '/\(^\|#\)Storage=/c\Storage=persistent' /etc/systemd/journald.conf +sed -i '/\(^\|#\)Compress=/c\Compress=yes' /etc/systemd/journald.conf +sed -i '/\(^\|#\)SystemMaxUse=/c\SystemMaxUse=1G' /etc/systemd/journald.conf +sed -i '/\(^\|#\)ForwardToSyslog=/c\ForwardToSyslog=no' /etc/systemd/journald.conf + systemctl enable container-runtime.service rm -rf /run/systemd \ No newline at end of file diff --git a/container-runtime/package.json b/container-runtime/package.json index 2fa407408..13cf14ed5 100644 --- a/container-runtime/package.json +++ b/container-runtime/package.json @@ -5,7 +5,7 @@ "module": "./index.js", "scripts": { "check": "tsc --noEmit", - "build": "prettier --write '**/*.ts' && rm -rf dist && tsc", + "build": "prettier . '!tmp/**' --write && rm -rf dist && tsc", "tsc": "rm -rf dist; tsc" }, "author": "", diff --git a/core/startos/src/context/rpc.rs b/core/startos/src/context/rpc.rs index 5adab5e58..f2c859273 100644 --- a/core/startos/src/context/rpc.rs +++ b/core/startos/src/context/rpc.rs @@ -16,7 +16,6 @@ use tokio::time::Instant; use tracing::instrument; use super::setup::CURRENT_SECRET; -use crate::account::AccountInfo; use crate::context::config::ServerConfig; use crate::core::rpc_continuations::{RequestGuid, RestHandler, RpcContinuation, WebSocketHandler}; use crate::db::prelude::PatchDbExt; @@ -33,6 +32,7 @@ use crate::service::ServiceMap; use crate::shutdown::Shutdown; use crate::system::get_mem_info; use crate::util::lshw::{lshw, LshwDevice}; +use crate::{account::AccountInfo, lxc::ContainerId}; pub struct RpcContextSeed { is_closed: AtomicBool, @@ -60,7 +60,7 @@ pub struct RpcContextSeed { } pub struct Dev { - pub lxc: Mutex>, + pub lxc: Mutex>, } pub struct Hardware { diff --git a/core/startos/src/logs.rs b/core/startos/src/logs.rs index 1cd84c331..99bfac250 100644 --- a/core/startos/src/logs.rs +++ b/core/startos/src/logs.rs @@ -18,15 +18,21 @@ use tokio_stream::wrappers::LinesStream; use tokio_tungstenite::tungstenite::Message; use tracing::instrument; -use crate::context::{CliContext, RpcContext}; -use crate::core::rpc_continuations::{RequestGuid, RpcContinuation}; use crate::error::ResultExt; use crate::prelude::*; use crate::util::serde::Reversible; +use crate::{ + context::{CliContext, RpcContext}, + lxc::ContainerId, +}; +use crate::{ + core::rpc_continuations::{RequestGuid, RpcContinuation}, + util::Invoke, +}; #[pin_project::pin_project] pub struct LogStream { - _child: Child, + _child: Option, #[pin] entries: BoxStream<'static, Result>, } @@ -116,9 +122,11 @@ pub struct LogFollowResponse { } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] pub struct LogEntry { timestamp: DateTime, message: String, + boot_id: String, } impl std::fmt::Display for LogEntry { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { @@ -141,6 +149,8 @@ pub struct JournalctlEntry { pub message: String, #[serde(rename = "__CURSOR")] pub cursor: String, + #[serde(rename = "_BOOT_ID")] + pub boot_id: String, } impl JournalctlEntry { fn log_entry(self) -> Result<(String, LogEntry), Error> { @@ -151,6 +161,7 @@ impl JournalctlEntry { UNIX_EPOCH + Duration::from_micros(self.timestamp.parse::()?), ), message: self.message, + boot_id: self.boot_id, }, )) } @@ -200,12 +211,12 @@ fn deserialize_log_message<'de, D: serde::de::Deserializer<'de>>( /// --user-unit=UNIT Show logs from the specified user unit)) /// System: Unit is startd, but we also filter on the comm /// Container: Filtering containers, like podman/docker is done by filtering on the CONTAINER_NAME -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum LogSource { Kernel, Unit(&'static str), System, - Container(PackageId), + Container(ContainerId), } pub const SYSTEM_UNIT: &str = "startd"; @@ -276,7 +287,7 @@ pub async fn cli_logs( } } pub async fn logs_nofollow( - _ctx: (), + ctx: RpcContext, _: Empty, LogsParam { id, @@ -286,14 +297,38 @@ pub async fn logs_nofollow( .. }: LogsParam, ) -> Result { - fetch_logs(LogSource::Container(id), limit, cursor, before).await + let container_id = ctx + .services + .get(&id) + .await + .as_ref() + .map(|x| x.container_id()) + .ok_or_else(|| { + Error::new( + eyre!("No service found with id: {}", id), + ErrorKind::NotFound, + ) + })??; + fetch_logs(LogSource::Container(container_id), limit, cursor, before).await } pub async fn logs_follow( ctx: RpcContext, _: Empty, LogsParam { id, limit, .. }: LogsParam, ) -> Result { - follow_logs(ctx, LogSource::Container(id), limit).await + let container_id = ctx + .services + .get(&id) + .await + .as_ref() + .map(|x| x.container_id()) + .ok_or_else(|| { + Error::new( + eyre!("No service found with id: {}", id), + ErrorKind::NotFound, + ) + })??; + follow_logs(ctx, LogSource::Container(container_id), limit).await } pub async fn cli_logs_generic_nofollow( @@ -358,7 +393,74 @@ pub async fn journalctl( before: bool, follow: bool, ) -> Result { - let mut cmd = Command::new("journalctl"); + let mut cmd = gen_journalctl_command(&id, limit); + + let cursor_formatted = format!("--after-cursor={}", cursor.unwrap_or("")); + if cursor.is_some() { + cmd.arg(&cursor_formatted); + if before { + cmd.arg("--reverse"); + } + } + + let deserialized_entries = String::from_utf8(cmd.invoke(ErrorKind::Journald).await?)? + .lines() + .map(serde_json::from_str::) + .collect::, _>>() + .with_kind(ErrorKind::Deserialization)?; + + if follow { + let mut follow_cmd = gen_journalctl_command(&id, limit); + follow_cmd.arg("-f"); + if let Some(last) = deserialized_entries.last() { + cmd.arg(format!("--after-cursor={}", last.cursor)); + } + let mut child = cmd.stdout(Stdio::piped()).spawn()?; + let out = + BufReader::new(child.stdout.take().ok_or_else(|| { + Error::new(eyre!("No stdout available"), crate::ErrorKind::Journald) + })?); + + let journalctl_entries = LinesStream::new(out.lines()); + + let follow_deserialized_entries = journalctl_entries + .map_err(|e| Error::new(e, crate::ErrorKind::Journald)) + .and_then(|s| { + futures::future::ready( + serde_json::from_str::(&s) + .with_kind(crate::ErrorKind::Deserialization), + ) + }); + + let entries = futures::stream::iter(deserialized_entries) + .map(Ok) + .chain(follow_deserialized_entries) + .boxed(); + Ok(LogStream { + _child: Some(child), + entries, + }) + } else { + let entries = futures::stream::iter(deserialized_entries).map(Ok).boxed(); + + Ok(LogStream { + _child: None, + entries, + }) + } +} + +fn gen_journalctl_command(id: &LogSource, limit: usize) -> Command { + let mut cmd = match id { + LogSource::Container(container_id) => { + let mut cmd = Command::new("lxc-attach"); + cmd.arg(format!("{}", container_id)) + .arg("--") + .arg("journalctl"); + cmd + } + _ => Command::new("journalctl"), + }; cmd.kill_on_drop(true); cmd.arg("--output=json"); @@ -377,48 +479,11 @@ pub async fn journalctl( cmd.arg(SYSTEM_UNIT); cmd.arg(format!("_COMM={}", SYSTEM_UNIT)); } - LogSource::Container(id) => { - #[cfg(not(feature = "docker"))] - cmd.arg(format!("SYSLOG_IDENTIFIER={}.embassy", id)); - #[cfg(feature = "docker")] - cmd.arg(format!("CONTAINER_NAME={}.embassy", id)); + LogSource::Container(_container_id) => { + cmd.arg("-u").arg("container-runtime.service"); } }; - - let cursor_formatted = format!("--after-cursor={}", cursor.unwrap_or("")); - if cursor.is_some() { - cmd.arg(&cursor_formatted); - if before { - cmd.arg("--reverse"); - } - } - if follow { - cmd.arg("--follow"); - } - - let mut child = cmd.stdout(Stdio::piped()).spawn()?; - let out = BufReader::new( - child - .stdout - .take() - .ok_or_else(|| Error::new(eyre!("No stdout available"), crate::ErrorKind::Journald))?, - ); - - let journalctl_entries = LinesStream::new(out.lines()); - - let deserialized_entries = journalctl_entries - .map_err(|e| Error::new(e, crate::ErrorKind::Journald)) - .and_then(|s| { - futures::future::ready( - serde_json::from_str::(&s) - .with_kind(crate::ErrorKind::Deserialization), - ) - }); - - Ok(LogStream { - _child: child, - entries: deserialized_entries.boxed(), - }) + cmd } #[instrument(skip_all)] diff --git a/core/startos/src/lxc/mod.rs b/core/startos/src/lxc/mod.rs index 0ebefdd96..5a6e05500 100644 --- a/core/startos/src/lxc/mod.rs +++ b/core/startos/src/lxc/mod.rs @@ -5,9 +5,11 @@ use std::path::Path; use std::sync::{Arc, Weak}; use std::time::Duration; +use clap::builder::ValueParserFactory; use clap::Parser; use futures::{AsyncWriteExt, FutureExt, StreamExt}; use imbl_value::{InOMap, InternedString}; +use models::InvalidId; use rpc_toolkit::yajrc::{RpcError, RpcResponse}; use rpc_toolkit::{ from_fn_async, AnyContext, CallRemoteHandler, GenericRpcMethod, Handler, HandlerArgs, @@ -28,10 +30,11 @@ use crate::disk::mount::filesystem::bind::Bind; use crate::disk::mount::filesystem::block_dev::BlockDev; use crate::disk::mount::filesystem::idmapped::IdMapped; use crate::disk::mount::filesystem::overlayfs::OverlayGuard; -use crate::disk::mount::filesystem::ReadWrite; -use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; +use crate::disk::mount::filesystem::{MountType, ReadWrite}; +use crate::disk::mount::guard::{GenericMountGuard, MountGuard, TmpMountGuard}; use crate::disk::mount::util::unmount; use crate::prelude::*; +use crate::util::clap::FromStrParser; use crate::util::rpc_client::UnixRpcClient; use crate::util::{new_guid, Invoke}; @@ -41,18 +44,57 @@ pub const CONTAINER_RPC_SERVER_SOCKET: &str = "service.sock"; // must not be abs pub const HOST_RPC_SERVER_SOCKET: &str = "host.sock"; // must not be absolute path const CONTAINER_DHCP_TIMEOUT: Duration = Duration::from_secs(30); -pub struct LxcManager { - containers: Mutex>>, +#[derive( + Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord, Hash, TS, +)] +#[ts(type = "string")] +pub struct ContainerId(InternedString); +impl std::ops::Deref for ContainerId { + type Target = str; + fn deref(&self) -> &Self::Target { + &self.0 + } } +impl std::fmt::Display for ContainerId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &*self.0) + } +} +impl TryFrom<&str> for ContainerId { + type Error = InvalidId; + fn try_from(value: &str) -> Result { + Ok(ContainerId(InternedString::intern(value))) + } +} +impl std::str::FromStr for ContainerId { + type Err = InvalidId; + fn from_str(s: &str) -> Result { + Self::try_from(s) + } +} +impl ValueParserFactory for ContainerId { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + FromStrParser::new() + } +} + +#[derive(Default)] +pub struct LxcManager { + containers: Mutex>>, +} + impl LxcManager { pub fn new() -> Self { - Self { - containers: Default::default(), - } + Self::default() } - pub async fn create(self: &Arc, config: LxcConfig) -> Result { - let container = LxcContainer::new(self, config).await?; + pub async fn create( + self: &Arc, + log_mount: Option<&Path>, + config: LxcConfig, + ) -> Result { + let container = LxcContainer::new(self, log_mount, config).await?; let mut guard = self.containers.lock().await; *guard = std::mem::take(&mut *guard) .into_iter() @@ -69,7 +111,7 @@ impl LxcManager { .await .iter() .filter_map(|g| g.upgrade()) - .map(|g| (&*g).clone()), + .map(|g| (*g).clone()), ); for container in String::from_utf8( Command::new("lxc-ls") @@ -80,7 +122,7 @@ impl LxcManager { .lines() .map(|s| s.trim()) { - if !expected.contains(container) { + if !expected.contains(&ContainerId::try_from(container)?) { let rootfs_path = Path::new(LXC_CONTAINER_DIR).join(container).join("rootfs"); if tokio::fs::metadata(&rootfs_path).await.is_ok() { unmount(Path::new(LXC_CONTAINER_DIR).join(container).join("rootfs")).await?; @@ -112,14 +154,20 @@ impl LxcManager { pub struct LxcContainer { manager: Weak, rootfs: OverlayGuard, - guid: Arc, + pub guid: Arc, rpc_bind: TmpMountGuard, + log_mount: Option, config: LxcConfig, exited: bool, } impl LxcContainer { - async fn new(manager: &Arc, config: LxcConfig) -> Result { + async fn new( + manager: &Arc, + log_mount: Option<&Path>, + config: LxcConfig, + ) -> Result { let guid = new_guid(); + let machine_id = hex::encode(rand::random::<[u8; 16]>()); let container_dir = Path::new(LXC_CONTAINER_DIR).join(&*guid); tokio::fs::create_dir_all(&container_dir).await?; tokio::fs::write( @@ -145,6 +193,7 @@ impl LxcContainer { &rootfs_dir, ) .await?; + tokio::fs::write(rootfs_dir.join("etc/machine-id"), format!("{machine_id}\n")).await?; tokio::fs::write(rootfs_dir.join("etc/hostname"), format!("{guid}\n")).await?; Command::new("sed") .arg("-i") @@ -166,6 +215,20 @@ impl LxcContainer { .arg(rpc_bind.path()) .invoke(ErrorKind::Filesystem) .await?; + let log_mount = if let Some(path) = log_mount { + let log_mount_point = rootfs_dir.join("var/log/journal").join(machine_id); + let log_mount = + MountGuard::mount(&Bind::new(path), &log_mount_point, MountType::ReadWrite).await?; + Command::new("chown") + // This was needed as 100999 because the group id of journald + .arg("100000:100999") + .arg(&log_mount_point) + .invoke(crate::ErrorKind::Filesystem) + .await?; + Some(log_mount) + } else { + None + }; Command::new("lxc-start") .arg("-d") .arg("--name") @@ -175,10 +238,11 @@ impl LxcContainer { Ok(Self { manager: Arc::downgrade(manager), rootfs, - guid: Arc::new(guid), + guid: Arc::new(ContainerId::try_from(&*guid)?), rpc_bind, config, exited: false, + log_mount, }) } @@ -188,11 +252,12 @@ impl LxcContainer { pub async fn ip(&self) -> Result { let start = Instant::now(); + let guid: &str = &self.guid; loop { let output = String::from_utf8( Command::new("lxc-info") .arg("--name") - .arg(&*self.guid) + .arg(guid) .arg("-iH") .invoke(ErrorKind::Docker) .await?, @@ -218,6 +283,9 @@ impl LxcContainer { #[instrument(skip_all)] pub async fn exit(mut self) -> Result<(), Error> { self.rpc_bind.take().unmount().await?; + if let Some(log_mount) = self.log_mount.take() { + log_mount.unmount(true).await?; + } self.rootfs.take().unmount(true).await?; let rootfs_path = self.rootfs_dir(); let err_path = rootfs_path.join("var/log/containerRuntime.err"); @@ -228,17 +296,16 @@ impl LxcContainer { tracing::error!(container, "{}", line); } } - if tokio::fs::metadata(&rootfs_path).await.is_ok() { - if tokio_stream::wrappers::ReadDirStream::new(tokio::fs::read_dir(&rootfs_path).await?) + if tokio::fs::metadata(&rootfs_path).await.is_ok() + && tokio_stream::wrappers::ReadDirStream::new(tokio::fs::read_dir(&rootfs_path).await?) .count() .await > 0 - { - return Err(Error::new( - eyre!("rootfs is not empty, refusing to delete"), - ErrorKind::InvalidRequest, - )); - } + { + return Err(Error::new( + eyre!("rootfs is not empty, refusing to delete"), + ErrorKind::InvalidRequest, + )); } Command::new("lxc-destroy") .arg("--force") @@ -341,21 +408,21 @@ pub fn lxc() -> ParentHandler { .subcommand("connect", from_fn_async(connect_rpc_cli).no_display()) } -pub async fn create(ctx: RpcContext) -> Result { - let container = ctx.lxc_manager.create(LxcConfig::default()).await?; +pub async fn create(ctx: RpcContext) -> Result { + let container = ctx.lxc_manager.create(None, LxcConfig::default()).await?; let guid = container.guid.deref().clone(); ctx.dev.lxc.lock().await.insert(guid.clone(), container); Ok(guid) } -pub async fn list(ctx: RpcContext) -> Result, Error> { +pub async fn list(ctx: RpcContext) -> Result, Error> { Ok(ctx.dev.lxc.lock().await.keys().cloned().collect()) } #[derive(Deserialize, Serialize, Parser, TS)] pub struct RemoveParams { #[ts(type = "string")] - pub guid: InternedString, + pub guid: ContainerId, } pub async fn remove(ctx: RpcContext, RemoveParams { guid }: RemoveParams) -> Result<(), Error> { @@ -368,7 +435,7 @@ pub async fn remove(ctx: RpcContext, RemoveParams { guid }: RemoveParams) -> Res #[derive(Deserialize, Serialize, Parser, TS)] pub struct ConnectParams { #[ts(type = "string")] - pub guid: InternedString, + pub guid: ContainerId, } pub async fn connect_rpc( @@ -502,7 +569,7 @@ pub async fn connect_cli(ctx: &CliContext, guid: RequestGuid) -> Result<(), Erro if let Some((method, rest)) = command.split_first() { let mut params = InOMap::new(); for arg in rest { - if let Some((name, value)) = arg.split_once("=") { + if let Some((name, value)) = arg.split_once('=') { params.insert(InternedString::intern(name), if value.is_empty() { Value::Null } else if let Ok(v) = serde_json::from_str(value) { diff --git a/core/startos/src/s9pk/v2/mod.rs b/core/startos/src/s9pk/v2/mod.rs index af1cd1c17..df82baf7e 100644 --- a/core/startos/src/s9pk/v2/mod.rs +++ b/core/startos/src/s9pk/v2/mod.rs @@ -70,7 +70,7 @@ fn filter(p: &Path) -> bool { #[derive(Clone)] pub struct S9pk> { - manifest: Manifest, + pub manifest: Manifest, manifest_dirty: bool, archive: MerkleArchive, size: Option, diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index 38923d726..e092a59e6 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -13,7 +13,6 @@ use start_stop::StartStop; use tokio::sync::Notify; use ts_rs::TS; -use crate::context::{CliContext, RpcContext}; use crate::core::rpc_continuations::RequestGuid; use crate::db::model::package::{ InstalledState, PackageDataEntry, PackageState, PackageStateMatchModelRef, UpdatingState, @@ -32,6 +31,10 @@ use crate::util::actor::concurrent::ConcurrentActor; use crate::util::actor::Actor; use crate::util::serde::Pem; use crate::volume::data_dir; +use crate::{ + context::{CliContext, RpcContext}, + lxc::ContainerId, +}; mod action; pub mod cli; @@ -193,8 +196,8 @@ impl Service { |db| { db.as_public_mut() .as_package_data_mut() - .as_idx_mut(&id) - .or_not_found(&id)? + .as_idx_mut(id) + .or_not_found(id)? .as_state_info_mut() .map_mutate(|s| { if let PackageState::Updating(UpdatingState { @@ -223,16 +226,12 @@ impl Service { tracing::debug!("{e:?}") }) { - if service - .uninstall(None) - .await - .map_err(|e| { + match service.uninstall(None).await { + Err(e) => { tracing::error!("Error uninstalling service: {e}"); tracing::debug!("{e:?}") - }) - .is_ok() - { - return Ok(None); + } + Ok(()) => return Ok(None), } } } @@ -299,10 +298,10 @@ impl Service { } pub async fn restore( - ctx: RpcContext, - s9pk: S9pk, - guard: impl GenericMountGuard, - progress: Option, + _ctx: RpcContext, + _s9pk: S9pk, + _guard: impl GenericMountGuard, + _progress: Option, ) -> Result { // TODO Err(Error::new(eyre!("not yet implemented"), ErrorKind::Unknown)) @@ -341,21 +340,36 @@ impl Service { .execute(ProcedureName::Uninit, to_value(&target_version)?, None) // TODO timeout .await?; let id = self.seed.persistent_container.s9pk.as_manifest().id.clone(); - self.seed - .ctx - .db - .mutate(|d| d.as_public_mut().as_package_data_mut().remove(&id)) - .await?; - self.shutdown().await + let ctx = self.seed.ctx.clone(); + self.shutdown().await?; + if target_version.is_none() { + ctx.db + .mutate(|d| d.as_public_mut().as_package_data_mut().remove(&id)) + .await?; + } + Ok(()) } pub async fn backup(&self, _guard: impl GenericMountGuard) -> Result { // TODO Err(Error::new(eyre!("not yet implemented"), ErrorKind::Unknown)) } + + pub fn container_id(&self) -> Result { + let id = &self.seed.id; + let container_id = (*self + .seed + .persistent_container + .lxc_container + .get() + .or_not_found(format!("container for {id}"))? + .guid) + .clone(); + Ok(container_id) + } } #[derive(Debug, Clone)] -struct RunningStatus { +pub struct RunningStatus { health: OrdMap, started: DateTime, } diff --git a/core/startos/src/service/persistent_container.rs b/core/startos/src/service/persistent_container.rs index 038661ace..d1303b0f9 100644 --- a/core/startos/src/service/persistent_container.rs +++ b/core/startos/src/service/persistent_container.rs @@ -10,7 +10,7 @@ use imbl_value::InternedString; use models::{ProcedureName, VolumeId}; use rpc_toolkit::{Empty, Server, ShutdownHandle}; use serde::de::DeserializeOwned; -use tokio::fs::File; +use tokio::fs::{create_dir_all, File}; use tokio::process::Command; use tokio::sync::{oneshot, watch, Mutex, OnceCell}; use tracing::instrument; @@ -39,8 +39,6 @@ use crate::ARCH; const RPC_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); -struct ProcedureId(u64); - #[derive(Debug)] pub struct ServiceState { // This contains the start time and health check information for when the service is running. Note: Will be overwritting to the db, @@ -99,7 +97,17 @@ pub struct PersistentContainer { impl PersistentContainer { #[instrument(skip_all)] pub async fn new(ctx: &RpcContext, s9pk: S9pk, start: StartStop) -> Result { - let lxc_container = ctx.lxc_manager.create(LxcConfig::default()).await?; + let lxc_container = ctx + .lxc_manager + .create( + Some( + &ctx.datadir + .join("package-data/logs") + .join(&s9pk.as_manifest().id), + ), + LxcConfig::default(), + ) + .await?; let rpc_client = lxc_container.connect_rpc(Some(RPC_CONNECT_TIMEOUT)).await?; let js_mount = MountGuard::mount( &LoopDev::from( @@ -114,6 +122,7 @@ impl PersistentContainer { ReadOnly, ) .await?; + let mut volumes = BTreeMap::new(); for volume in &s9pk.as_manifest().volumes { let mountpoint = lxc_container @@ -175,7 +184,7 @@ impl PersistentContainer { if let Some(env) = s9pk .as_archive() .contents() - .get_path(Path::new("images").join(&*ARCH).join(&env_filename)) + .get_path(Path::new("images").join(*ARCH).join(&env_filename)) .and_then(|e| e.as_file()) { env.copy(&mut File::create(image_path.join(&env_filename)).await?) @@ -185,7 +194,7 @@ impl PersistentContainer { if let Some(json) = s9pk .as_archive() .contents() - .get_path(Path::new("images").join(&*ARCH).join(&json_filename)) + .get_path(Path::new("images").join(*ARCH).join(&json_filename)) .and_then(|e| e.as_file()) { json.copy(&mut File::create(image_path.join(&json_filename)).await?) @@ -231,7 +240,7 @@ impl PersistentContainer { .join(HOST_RPC_SERVER_SOCKET); let (send, recv) = oneshot::channel(); let handle = NonDetachingJoinHandle::from(tokio::spawn(async move { - let (shutdown, fut) = match async { + let chown_status = async { let res = server.run_unix(&path, |err| { tracing::error!("error on unix socket {}: {err}", path.display()) })?; @@ -241,9 +250,8 @@ impl PersistentContainer { .invoke(ErrorKind::Filesystem) .await?; Ok::<_, Error>(res) - } - .await - { + }; + let (shutdown, fut) = match chown_status.await { Ok((shutdown, fut)) => (Ok(shutdown), Some(fut)), Err(e) => (Err(e), None), }; diff --git a/core/startos/src/service/service_map.rs b/core/startos/src/service/service_map.rs index 68e1fc8a8..4386b39b7 100644 --- a/core/startos/src/service/service_map.rs +++ b/core/startos/src/service/service_map.rs @@ -32,9 +32,9 @@ use crate::util::serde::Pem; pub type DownloadInstallFuture = BoxFuture<'static, Result>; pub type InstallFuture = BoxFuture<'static, Result<(), Error>>; -pub(super) struct InstallProgressHandles { - pub(super) finalization_progress: PhaseProgressTrackerHandle, - pub(super) progress_handle: FullProgressTrackerHandle, +pub struct InstallProgressHandles { + pub finalization_progress: PhaseProgressTrackerHandle, + pub progress_handle: FullProgressTrackerHandle, } /// This is the structure to contain all the services From 003d11094857c18f887f7923ee04cf0957cb9e03 Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Mon, 22 Apr 2024 11:40:10 -0600 Subject: [PATCH 014/125] build multi-arch s9pks (#2601) * build multi-arch s9pks * remove images incrementally * wip * prevent rebuild * fix sdk makefile * fix hanging on uninstall * fix build * fix build * fix build * fix build (for real this time) * fix git hash computation --- .github/workflows/startos-iso.yaml | 2 + Makefile | 47 +- build/lib/scripts/chroot-and-upgrade | 2 + check-git-hash.sh | 2 +- container-runtime/package-lock.json | 615 +----------------- container-runtime/package.json | 2 +- container-runtime/src/Adapters/RpcListener.ts | 12 +- .../Systems/SystemForEmbassy/index.ts | 4 +- container-runtime/tsconfig.json | 8 +- core/.gitignore | 1 + core/startos/bindings/ActionMetadata.ts | 12 - core/startos/bindings/AddressInfo.ts | 10 - core/startos/bindings/BindOptions.ts | 10 - core/startos/bindings/BindParams.ts | 15 - core/startos/bindings/Dependencies.ts | 5 - .../bindings/DestroyOverlayedImageParams.ts | 3 - .../bindings/ExportServiceInterfaceParams.ts | 14 - core/startos/bindings/ExportedHostnameInfo.ts | 12 - .../bindings/ExposeForDependentsParams.ts | 3 - core/startos/bindings/GetSystemSmtpParams.ts | 4 - core/startos/bindings/Host.ts | 11 - core/startos/bindings/HostAddress.ts | 3 - core/startos/bindings/HostInfo.ts | 5 - core/startos/bindings/InstalledState.ts | 4 - core/startos/bindings/MainStatus.ts | 20 - core/startos/bindings/Manifest.ts | 32 - core/startos/bindings/PackageDataEntry.ts | 26 - core/startos/bindings/ParamsMaybePackageId.ts | 3 - core/startos/bindings/ParamsPackageId.ts | 3 - core/startos/bindings/PasswordType.ts | 4 - core/startos/bindings/Public.ts | 9 - core/startos/bindings/RemoveActionParams.ts | 3 - core/startos/bindings/RemoveAddressParams.ts | 3 - core/startos/bindings/ReverseProxyBind.ts | 7 - core/startos/bindings/ReverseProxyParams.ts | 10 - core/startos/bindings/ServerInfo.ts | 31 - core/startos/bindings/ServerStatus.ts | 12 - core/startos/bindings/ServiceInterface.ts | 15 - core/startos/bindings/ServiceInterfaceType.ts | 3 - .../bindings/ServiceInterfaceWithHostInfo.ts | 17 - core/startos/bindings/SetConfigured.ts | 3 - core/startos/bindings/SetMainStatus.ts | 4 - core/startos/bindings/SetStoreParams.ts | 3 - core/startos/bindings/Status.ts | 4 - core/startos/bindings/UpdatingState.ts | 8 - core/startos/bindings/index.ts | 101 --- core/startos/src/backup/restore.rs | 1 + core/startos/src/core/rpc_continuations.rs | 2 +- core/startos/src/disk/main.rs | 6 +- core/startos/src/disk/mount/guard.rs | 3 +- core/startos/src/inspect.rs | 127 ---- core/startos/src/install/mod.rs | 10 +- core/startos/src/lib.rs | 3 +- core/startos/src/logs.rs | 29 +- core/startos/src/lxc/mod.rs | 8 - core/startos/src/registry/admin.rs | 4 +- core/startos/src/s9pk/rpc.rs | 224 ++++--- core/startos/src/s9pk/v1/docker.rs | 68 +- core/startos/src/s9pk/v1/reader.rs | 16 +- core/startos/src/s9pk/v2/compat.rs | 293 +++++---- core/startos/src/s9pk/v2/mod.rs | 18 +- core/startos/src/service/mod.rs | 17 +- .../src/service/persistent_container.rs | 70 +- .../src/service/service_effect_handler.rs | 1 + core/startos/src/service/service_map.rs | 18 +- core/startos/src/util/actor/concurrent.rs | 2 +- core/startos/src/util/mod.rs | 2 +- debian/postinst | 30 +- sdk/Makefile | 33 +- sdk/lib/interfaces/Host.ts | 8 +- .../lib/osBindings}/ActionId.ts | 2 +- sdk/lib/osBindings/ActionMetadata.ts | 12 + .../lib/osBindings}/AddSslOptions.ts | 10 +- sdk/lib/osBindings/AddressInfo.ts | 10 + .../bindings => sdk/lib/osBindings}/Alerts.ts | 12 +- .../lib/osBindings}/Algorithm.ts | 2 +- .../lib/osBindings}/AllPackageData.ts | 6 +- .../lib/osBindings}/AllowedStatuses.ts | 2 +- .../lib/osBindings}/AlpnInfo.ts | 4 +- .../lib/osBindings/BackupProgress.ts | 2 +- .../lib/osBindings}/BindInfo.ts | 4 +- sdk/lib/osBindings/BindOptions.ts | 10 + sdk/lib/osBindings/BindParams.ts | 15 + .../lib/osBindings}/Callback.ts | 2 +- .../lib/osBindings}/ChrootParams.ts | 14 +- .../osBindings/CreateOverlayedImageParams.ts | 3 + .../lib/osBindings}/CurrentDependencies.ts | 6 +- .../lib/osBindings}/CurrentDependencyInfo.ts | 14 +- .../lib/osBindings}/DataUrl.ts | 2 +- .../lib/osBindings}/DepInfo.ts | 2 +- sdk/lib/osBindings/Dependencies.ts | 5 + sdk/lib/osBindings/DependencyKind.ts | 3 + .../lib/osBindings}/DependencyRequirement.ts | 12 +- sdk/lib/osBindings/Description.ts | 3 + .../osBindings/DestroyOverlayedImageParams.ts | 2 +- .../lib/osBindings}/Duration.ts | 2 +- sdk/lib/osBindings/EncryptedWire.ts | 3 + .../lib/osBindings}/ExecuteAction.ts | 8 +- .../lib/osBindings}/ExportActionParams.ts | 4 +- .../ExportServiceInterfaceParams.ts | 14 + .../lib/osBindings}/ExportedHostInfo.ts | 14 +- sdk/lib/osBindings/ExportedHostnameInfo.ts | 12 + .../lib/osBindings}/ExportedIpHostname.ts | 20 +- .../lib/osBindings}/ExportedOnionHostname.ts | 8 +- .../osBindings/ExposeForDependentsParams.ts | 3 + .../lib/osBindings}/FullProgress.ts | 6 +- .../lib/osBindings}/GetHostInfoParams.ts | 14 +- .../lib/osBindings}/GetHostInfoParamsKind.ts | 2 +- .../lib/osBindings}/GetPrimaryUrlParams.ts | 10 +- .../osBindings}/GetServiceInterfaceParams.ts | 10 +- .../GetServicePortForwardParams.ts | 10 +- .../osBindings}/GetSslCertificateParams.ts | 10 +- .../lib/osBindings}/GetSslKeyParams.ts | 10 +- .../lib/osBindings}/GetStoreParams.ts | 2 +- sdk/lib/osBindings/GetSystemSmtpParams.ts | 4 + .../lib/osBindings}/Governor.ts | 2 +- .../lib/osBindings}/HardwareRequirements.ts | 8 +- .../lib/osBindings}/HealthCheckId.ts | 2 +- .../lib/osBindings}/HealthCheckResult.ts | 2 +- sdk/lib/osBindings/Host.ts | 11 + sdk/lib/osBindings/HostAddress.ts | 3 + .../bindings => sdk/lib/osBindings}/HostId.ts | 2 +- sdk/lib/osBindings/HostInfo.ts | 5 + .../lib/osBindings}/HostKind.ts | 2 +- .../lib/osBindings}/ImageId.ts | 2 +- sdk/lib/osBindings/InstalledState.ts | 4 + .../lib/osBindings}/InstallingInfo.ts | 6 +- .../lib/osBindings}/InstallingState.ts | 4 +- .../bindings => sdk/lib/osBindings}/IpInfo.ts | 10 +- .../ListServiceInterfacesParams.ts | 8 +- sdk/lib/osBindings/MainStatus.ts | 20 + sdk/lib/osBindings/Manifest.ts | 32 + .../lib/osBindings/MaybeUtf8String.ts | 2 +- .../lib/osBindings}/MountParams.ts | 4 +- .../lib/osBindings}/MountTarget.ts | 10 +- .../lib/osBindings}/NamedProgress.ts | 4 +- sdk/lib/osBindings/PackageDataEntry.ts | 24 + .../lib/osBindings}/PackageId.ts | 2 +- .../lib/osBindings}/PackageState.ts | 8 +- .../lib/osBindings/ParamsMaybePackageId.ts | 2 +- .../lib/osBindings/ParamsPackageId.ts | 2 +- sdk/lib/osBindings/PasswordType.ts | 4 + .../lib/osBindings}/Progress.ts | 2 +- sdk/lib/osBindings/Public.ts | 9 + sdk/lib/osBindings/RemoveActionParams.ts | 3 + .../lib/osBindings/RemoveAddressParams.ts | 2 +- sdk/lib/osBindings/ReverseProxyBind.ts | 3 + .../osBindings}/ReverseProxyDestination.ts | 8 +- .../lib/osBindings}/ReverseProxyHttp.ts | 2 +- sdk/lib/osBindings/ReverseProxyParams.ts | 10 + .../lib/osBindings}/Security.ts | 2 +- sdk/lib/osBindings/ServerInfo.ts | 31 + .../lib/osBindings}/ServerSpecs.ts | 2 +- sdk/lib/osBindings/ServerStatus.ts | 12 + sdk/lib/osBindings/ServiceInterface.ts | 15 + .../lib/osBindings}/ServiceInterfaceId.ts | 2 +- sdk/lib/osBindings/ServiceInterfaceType.ts | 3 + .../ServiceInterfaceWithHostInfo.ts | 17 + .../lib/osBindings}/Session.ts | 2 +- .../lib/osBindings}/SessionList.ts | 4 +- .../lib/osBindings}/Sessions.ts | 4 +- sdk/lib/osBindings/SetConfigured.ts | 3 + .../lib/osBindings}/SetDependenciesParams.ts | 6 +- .../lib/osBindings}/SetHealth.ts | 4 +- sdk/lib/osBindings/SetMainStatus.ts | 4 + sdk/lib/osBindings/SetStoreParams.ts | 3 + sdk/lib/osBindings/Status.ts | 4 + .../lib/osBindings}/UpdateProgress.ts | 2 +- sdk/lib/osBindings/UpdatingState.ts | 8 + .../lib/osBindings}/VolumeId.ts | 2 +- .../lib/osBindings}/WifiInfo.ts | 10 +- sdk/lib/osBindings/index.ts | 101 +++ sdk/lib/test/startosTypeValidation.test.ts | 48 +- sdk/lib/types.ts | 6 +- sdk/package.json | 8 +- .../app-actions/app-actions.page.ts | 3 +- 176 files changed, 1176 insertions(+), 1799 deletions(-) delete mode 100644 core/startos/bindings/ActionMetadata.ts delete mode 100644 core/startos/bindings/AddressInfo.ts delete mode 100644 core/startos/bindings/BindOptions.ts delete mode 100644 core/startos/bindings/BindParams.ts delete mode 100644 core/startos/bindings/Dependencies.ts delete mode 100644 core/startos/bindings/DestroyOverlayedImageParams.ts delete mode 100644 core/startos/bindings/ExportServiceInterfaceParams.ts delete mode 100644 core/startos/bindings/ExportedHostnameInfo.ts delete mode 100644 core/startos/bindings/ExposeForDependentsParams.ts delete mode 100644 core/startos/bindings/GetSystemSmtpParams.ts delete mode 100644 core/startos/bindings/Host.ts delete mode 100644 core/startos/bindings/HostAddress.ts delete mode 100644 core/startos/bindings/HostInfo.ts delete mode 100644 core/startos/bindings/InstalledState.ts delete mode 100644 core/startos/bindings/MainStatus.ts delete mode 100644 core/startos/bindings/Manifest.ts delete mode 100644 core/startos/bindings/PackageDataEntry.ts delete mode 100644 core/startos/bindings/ParamsMaybePackageId.ts delete mode 100644 core/startos/bindings/ParamsPackageId.ts delete mode 100644 core/startos/bindings/PasswordType.ts delete mode 100644 core/startos/bindings/Public.ts delete mode 100644 core/startos/bindings/RemoveActionParams.ts delete mode 100644 core/startos/bindings/RemoveAddressParams.ts delete mode 100644 core/startos/bindings/ReverseProxyBind.ts delete mode 100644 core/startos/bindings/ReverseProxyParams.ts delete mode 100644 core/startos/bindings/ServerInfo.ts delete mode 100644 core/startos/bindings/ServerStatus.ts delete mode 100644 core/startos/bindings/ServiceInterface.ts delete mode 100644 core/startos/bindings/ServiceInterfaceType.ts delete mode 100644 core/startos/bindings/ServiceInterfaceWithHostInfo.ts delete mode 100644 core/startos/bindings/SetConfigured.ts delete mode 100644 core/startos/bindings/SetMainStatus.ts delete mode 100644 core/startos/bindings/SetStoreParams.ts delete mode 100644 core/startos/bindings/Status.ts delete mode 100644 core/startos/bindings/UpdatingState.ts delete mode 100644 core/startos/bindings/index.ts delete mode 100644 core/startos/src/inspect.rs rename {core/startos/bindings => sdk/lib/osBindings}/ActionId.ts (78%) create mode 100644 sdk/lib/osBindings/ActionMetadata.ts rename {core/startos/bindings => sdk/lib/osBindings}/AddSslOptions.ts (53%) create mode 100644 sdk/lib/osBindings/AddressInfo.ts rename {core/startos/bindings => sdk/lib/osBindings}/Alerts.ts (50%) rename {core/startos/bindings => sdk/lib/osBindings}/Algorithm.ts (70%) rename {core/startos/bindings => sdk/lib/osBindings}/AllPackageData.ts (61%) rename {core/startos/bindings => sdk/lib/osBindings}/AllowedStatuses.ts (97%) rename {core/startos/bindings => sdk/lib/osBindings}/AlpnInfo.ts (71%) rename core/startos/bindings/DependencyKind.ts => sdk/lib/osBindings/BackupProgress.ts (68%) rename {core/startos/bindings => sdk/lib/osBindings}/BindInfo.ts (72%) create mode 100644 sdk/lib/osBindings/BindOptions.ts create mode 100644 sdk/lib/osBindings/BindParams.ts rename {core/startos/bindings => sdk/lib/osBindings}/Callback.ts (75%) rename {core/startos/bindings => sdk/lib/osBindings}/ChrootParams.ts (52%) create mode 100644 sdk/lib/osBindings/CreateOverlayedImageParams.ts rename {core/startos/bindings => sdk/lib/osBindings}/CurrentDependencies.ts (78%) rename {core/startos/bindings => sdk/lib/osBindings}/CurrentDependencyInfo.ts (57%) rename {core/startos/bindings => sdk/lib/osBindings}/DataUrl.ts (78%) rename {core/startos/bindings => sdk/lib/osBindings}/DepInfo.ts (95%) create mode 100644 sdk/lib/osBindings/Dependencies.ts create mode 100644 sdk/lib/osBindings/DependencyKind.ts rename {core/startos/bindings => sdk/lib/osBindings}/DependencyRequirement.ts (61%) create mode 100644 sdk/lib/osBindings/Description.ts rename core/startos/bindings/Description.ts => sdk/lib/osBindings/DestroyOverlayedImageParams.ts (65%) rename {core/startos/bindings => sdk/lib/osBindings}/Duration.ts (78%) create mode 100644 sdk/lib/osBindings/EncryptedWire.ts rename {core/startos/bindings => sdk/lib/osBindings}/ExecuteAction.ts (68%) rename {core/startos/bindings => sdk/lib/osBindings}/ExportActionParams.ts (72%) create mode 100644 sdk/lib/osBindings/ExportServiceInterfaceParams.ts rename {core/startos/bindings => sdk/lib/osBindings}/ExportedHostInfo.ts (55%) create mode 100644 sdk/lib/osBindings/ExportedHostnameInfo.ts rename {core/startos/bindings => sdk/lib/osBindings}/ExportedIpHostname.ts (57%) rename {core/startos/bindings => sdk/lib/osBindings}/ExportedOnionHostname.ts (68%) create mode 100644 sdk/lib/osBindings/ExposeForDependentsParams.ts rename {core/startos/bindings => sdk/lib/osBindings}/FullProgress.ts (60%) rename {core/startos/bindings => sdk/lib/osBindings}/GetHostInfoParams.ts (54%) rename {core/startos/bindings => sdk/lib/osBindings}/GetHostInfoParamsKind.ts (70%) rename {core/startos/bindings => sdk/lib/osBindings}/GetPrimaryUrlParams.ts (53%) rename {core/startos/bindings => sdk/lib/osBindings}/GetServiceInterfaceParams.ts (54%) rename {core/startos/bindings => sdk/lib/osBindings}/GetServicePortForwardParams.ts (57%) rename {core/startos/bindings => sdk/lib/osBindings}/GetSslCertificateParams.ts (54%) rename {core/startos/bindings => sdk/lib/osBindings}/GetSslKeyParams.ts (52%) rename {core/startos/bindings => sdk/lib/osBindings}/GetStoreParams.ts (95%) create mode 100644 sdk/lib/osBindings/GetSystemSmtpParams.ts rename {core/startos/bindings => sdk/lib/osBindings}/Governor.ts (78%) rename {core/startos/bindings => sdk/lib/osBindings}/HardwareRequirements.ts (61%) rename {core/startos/bindings => sdk/lib/osBindings}/HealthCheckId.ts (75%) rename {core/startos/bindings => sdk/lib/osBindings}/HealthCheckResult.ts (99%) create mode 100644 sdk/lib/osBindings/Host.ts create mode 100644 sdk/lib/osBindings/HostAddress.ts rename {core/startos/bindings => sdk/lib/osBindings}/HostId.ts (79%) create mode 100644 sdk/lib/osBindings/HostInfo.ts rename {core/startos/bindings => sdk/lib/osBindings}/HostKind.ts (77%) rename {core/startos/bindings => sdk/lib/osBindings}/ImageId.ts (78%) create mode 100644 sdk/lib/osBindings/InstalledState.ts rename {core/startos/bindings => sdk/lib/osBindings}/InstallingInfo.ts (60%) rename {core/startos/bindings => sdk/lib/osBindings}/InstallingState.ts (75%) rename {core/startos/bindings => sdk/lib/osBindings}/IpInfo.ts (55%) rename {core/startos/bindings => sdk/lib/osBindings}/ListServiceInterfacesParams.ts (61%) create mode 100644 sdk/lib/osBindings/MainStatus.ts create mode 100644 sdk/lib/osBindings/Manifest.ts rename core/startos/bindings/EncryptedWire.ts => sdk/lib/osBindings/MaybeUtf8String.ts (69%) rename {core/startos/bindings => sdk/lib/osBindings}/MountParams.ts (75%) rename {core/startos/bindings => sdk/lib/osBindings}/MountTarget.ts (60%) rename {core/startos/bindings => sdk/lib/osBindings}/NamedProgress.ts (79%) create mode 100644 sdk/lib/osBindings/PackageDataEntry.ts rename {core/startos/bindings => sdk/lib/osBindings}/PackageId.ts (77%) rename {core/startos/bindings => sdk/lib/osBindings}/PackageState.ts (60%) rename core/startos/bindings/CreateOverlayedImageParams.ts => sdk/lib/osBindings/ParamsMaybePackageId.ts (63%) rename core/startos/bindings/BackupProgress.ts => sdk/lib/osBindings/ParamsPackageId.ts (67%) create mode 100644 sdk/lib/osBindings/PasswordType.ts rename {core/startos/bindings => sdk/lib/osBindings}/Progress.ts (95%) create mode 100644 sdk/lib/osBindings/Public.ts create mode 100644 sdk/lib/osBindings/RemoveActionParams.ts rename core/startos/bindings/MaybeUtf8String.ts => sdk/lib/osBindings/RemoveAddressParams.ts (69%) create mode 100644 sdk/lib/osBindings/ReverseProxyBind.ts rename {core/startos/bindings => sdk/lib/osBindings}/ReverseProxyDestination.ts (72%) rename {core/startos/bindings => sdk/lib/osBindings}/ReverseProxyHttp.ts (92%) create mode 100644 sdk/lib/osBindings/ReverseProxyParams.ts rename {core/startos/bindings => sdk/lib/osBindings}/Security.ts (72%) create mode 100644 sdk/lib/osBindings/ServerInfo.ts rename {core/startos/bindings => sdk/lib/osBindings}/ServerSpecs.ts (95%) create mode 100644 sdk/lib/osBindings/ServerStatus.ts create mode 100644 sdk/lib/osBindings/ServiceInterface.ts rename {core/startos/bindings => sdk/lib/osBindings}/ServiceInterfaceId.ts (72%) create mode 100644 sdk/lib/osBindings/ServiceInterfaceType.ts create mode 100644 sdk/lib/osBindings/ServiceInterfaceWithHostInfo.ts rename {core/startos/bindings => sdk/lib/osBindings}/Session.ts (92%) rename {core/startos/bindings => sdk/lib/osBindings}/SessionList.ts (78%) rename {core/startos/bindings => sdk/lib/osBindings}/Sessions.ts (93%) create mode 100644 sdk/lib/osBindings/SetConfigured.ts rename {core/startos/bindings => sdk/lib/osBindings}/SetDependenciesParams.ts (79%) rename {core/startos/bindings => sdk/lib/osBindings}/SetHealth.ts (87%) create mode 100644 sdk/lib/osBindings/SetMainStatus.ts create mode 100644 sdk/lib/osBindings/SetStoreParams.ts create mode 100644 sdk/lib/osBindings/Status.ts rename {core/startos/bindings => sdk/lib/osBindings}/UpdateProgress.ts (94%) create mode 100644 sdk/lib/osBindings/UpdatingState.ts rename {core/startos/bindings => sdk/lib/osBindings}/VolumeId.ts (78%) rename {core/startos/bindings => sdk/lib/osBindings}/WifiInfo.ts (54%) create mode 100644 sdk/lib/osBindings/index.ts diff --git a/.github/workflows/startos-iso.yaml b/.github/workflows/startos-iso.yaml index 7d4a43685..30b2bec0f 100644 --- a/.github/workflows/startos-iso.yaml +++ b/.github/workflows/startos-iso.yaml @@ -175,8 +175,10 @@ jobs: - name: Prevent rebuild of compiled artifacts run: | + mkdir -p web/node_modules mkdir -p web/dist/raw touch core/startos/bindings + touch sdk/lib/osBindings mkdir -p container-runtime/dist PLATFORM=${{ matrix.platform }} make -t compiled-${{ env.ARCH }}.tar diff --git a/Makefile b/Makefile index e1ba4ed53..02328a1d8 100644 --- a/Makefile +++ b/Makefile @@ -9,15 +9,15 @@ IMAGE_TYPE=$(shell if [ "$(PLATFORM)" = raspberrypi ]; then echo img; else echo BINS := core/target/$(ARCH)-unknown-linux-musl/release/startbox core/target/$(ARCH)-unknown-linux-musl/release/containerbox WEB_UIS := web/dist/raw/ui web/dist/raw/setup-wizard web/dist/raw/diagnostic-ui web/dist/raw/install-wizard FIRMWARE_ROMS := ./firmware/$(PLATFORM) $(shell jq --raw-output '.[] | select(.platform[] | contains("$(PLATFORM)")) | "./firmware/$(PLATFORM)/" + .id + ".rom.gz"' build/lib/firmware.json) -BUILD_SRC := $(shell git ls-files build) build/lib/depends build/lib/conflicts container-runtime/rootfs.$(ARCH).squashfs $(FIRMWARE_ROMS) +BUILD_SRC := $(shell git ls-files build) build/lib/depends build/lib/conflicts $(FIRMWARE_ROMS) DEBIAN_SRC := $(shell git ls-files debian/) IMAGE_RECIPE_SRC := $(shell git ls-files image-recipe/) STARTD_SRC := core/startos/startd.service $(BUILD_SRC) COMPAT_SRC := $(shell git ls-files system-images/compat/) UTILS_SRC := $(shell git ls-files system-images/utils/) BINFMT_SRC := $(shell git ls-files system-images/binfmt/) -CORE_SRC := $(shell git ls-files -- core ':!:core/startos/bindings/*') $(shell git ls-files --recurse-submodules patch-db) web/dist/static web/patchdb-ui-seed.json $(GIT_HASH_FILE) -WEB_SHARED_SRC := $(shell git ls-files web/projects/shared) $(shell ls -p web/ | grep -v / | sed 's/^/web\//g') web/node_modules web/config.json patch-db/client/dist web/patchdb-ui-seed.json +CORE_SRC := $(shell git ls-files core) $(shell git ls-files --recurse-submodules patch-db) web/dist/static web/patchdb-ui-seed.json $(GIT_HASH_FILE) +WEB_SHARED_SRC := $(shell git ls-files web/projects/shared) $(shell ls -p web/ | grep -v / | sed 's/^/web\//g') web/node_modules/.package-lock.json web/config.json patch-db/client/dist web/patchdb-ui-seed.json WEB_UI_SRC := $(shell git ls-files web/projects/ui) WEB_SETUP_WIZARD_SRC := $(shell git ls-files web/projects/setup-wizard) WEB_DIAGNOSTIC_UI_SRC := $(shell git ls-files web/projects/diagnostic-ui) @@ -49,7 +49,7 @@ endif .DELETE_ON_ERROR: -.PHONY: all metadata install clean format cli uis ui reflash deb $(IMAGE_TYPE) squashfs sudo wormhole test +.PHONY: all metadata install clean format cli uis ui reflash deb $(IMAGE_TYPE) squashfs sudo wormhole wormhole-deb test all: $(ALL_TARGETS) @@ -65,6 +65,7 @@ clean: rm -f system-images/**/*.tar rm -rf system-images/compat/target rm -rf core/target + rm -rf core/startos/bindings rm -rf web/.angular rm -f web/config.json rm -rf web/node_modules @@ -80,8 +81,8 @@ clean: rm -rf container-runtime/dist rm -rf container-runtime/node_modules rm -f container-runtime/*.squashfs - rm -rf sdk/dist - rm -rf sdk/node_modules + rm -rf container-runtime/tmp + (cd sdk && make clean) rm -f ENVIRONMENT.txt rm -f PLATFORM.txt rm -f GIT_HASH.txt @@ -92,7 +93,6 @@ format: test: $(CORE_SRC) $(ENVIRONMENT_FILE) (cd core && cargo build && cargo test) - npm --prefix sdk exec -- prettier -w ./core/startos/bindings/*.ts (cd sdk && make test) cli: @@ -158,6 +158,10 @@ wormhole: core/target/$(ARCH)-unknown-linux-musl/release/startbox @echo "Paste the following command into the shell of your start-os server:" @wormhole send core/target/$(ARCH)-unknown-linux-musl/release/startbox 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo /usr/lib/startos/scripts/chroot-and-upgrade \"cd /usr/bin && rm startbox && wormhole receive --accept-file %s && chmod +x startbox\"\n", $$3 }' +wormhole-deb: results/$(BASENAME).deb + @echo "Paste the following command into the shell of your start-os server:" + @wormhole send results/$(BASENAME).deb 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo /usr/lib/startos/scripts/chroot-and-upgrade '"'"'cd $$(mktemp -d) && wormhole receive --accept-file %s && apt-get install -y --reinstall ./$(BASENAME).deb'"'"'\n", $$3 }' + update: $(ALL_TARGETS) @if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi $(call ssh,"sudo rsync -a --delete --force --info=progress2 /media/embassy/embassyfs/current/ /media/embassy/next/") @@ -180,13 +184,19 @@ container-runtime/node_modules: container-runtime/package.json container-runtime npm --prefix container-runtime ci touch container-runtime/node_modules -core/startos/bindings: $(shell git ls-files -- core ':!:core/startos/bindings/*') $(ENVIRONMENT_FILE) - rm -rf core/startos/bindings - (cd core/ && cargo test --features=test) +sdk/lib/osBindings: core/startos/bindings + mkdir -p sdk/lib/osBindings ls core/startos/bindings/*.ts | sed 's/core\/startos\/bindings\/\([^.]*\)\.ts/export { \1 } from ".\/\1";/g' > core/startos/bindings/index.ts - npm --prefix sdk exec -- prettier -w ./core/startos/bindings/*.ts + npm --prefix sdk exec -- prettier --config ./sdk/package.json -w ./core/startos/bindings/*.ts + rsync -ac --delete core/startos/bindings/ sdk/lib/osBindings/ + touch sdk/lib/osBindings -sdk/dist: $(shell git ls-files sdk) core/startos/bindings +core/startos/bindings: $(shell git ls-files core) $(ENVIRONMENT_FILE) + rm -rf core/startos/bindings + (cd core/ && cargo test --features=test '::export_bindings_') + touch core/startos/bindings + +sdk/dist: $(shell git ls-files sdk) sdk/lib/osBindings (cd sdk && make bundle) # TODO: make container-runtime its own makefile? @@ -219,21 +229,25 @@ $(BINS): $(CORE_SRC) $(ENVIRONMENT_FILE) cd core && ARCH=$(ARCH) ./build-prod.sh touch $(BINS) -web/node_modules: web/package.json sdk/dist - (cd sdk && make bundle) +web/node_modules/.package-lock.json: web/package.json sdk/dist npm --prefix web ci + touch web/node_modules/.package-lock.json web/dist/raw/ui: $(WEB_UI_SRC) $(WEB_SHARED_SRC) npm --prefix web run build:ui + touch web/dist/raw/ui web/dist/raw/setup-wizard: $(WEB_SETUP_WIZARD_SRC) $(WEB_SHARED_SRC) npm --prefix web run build:setup + touch web/dist/raw/setup-wizard web/dist/raw/diagnostic-ui: $(WEB_DIAGNOSTIC_UI_SRC) $(WEB_SHARED_SRC) npm --prefix web run build:dui + touch web/dist/raw/diagnostic-ui web/dist/raw/install-wizard: $(WEB_INSTALL_WIZARD_SRC) $(WEB_SHARED_SRC) npm --prefix web run build:install-wiz + touch web/dist/raw/install-wizard web/dist/static: $(WEB_UIS) $(ENVIRONMENT_FILE) ./compress-uis.sh @@ -247,10 +261,11 @@ web/patchdb-ui-seed.json: web/package.json patch-db/client/node_modules: patch-db/client/package.json npm --prefix patch-db/client ci + touch patch-db/client/node_modules patch-db/client/dist: $(PATCH_DB_CLIENT_SRC) patch-db/client/node_modules - ! test -d patch-db/client/dist || rm -rf patch-db/client/dist - npm --prefix web run build:deps + rm -rf patch-db/client/dist + npm --prefix patch-db/client run build # used by github actions compiled-$(ARCH).tar: $(COMPILED_TARGETS) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) diff --git a/build/lib/scripts/chroot-and-upgrade b/build/lib/scripts/chroot-and-upgrade index f95e49924..a7fafb8bc 100755 --- a/build/lib/scripts/chroot-and-upgrade +++ b/build/lib/scripts/chroot-and-upgrade @@ -16,6 +16,7 @@ mkdir -p /media/embassy/next/sys mkdir -p /media/embassy/next/proc mkdir -p /media/embassy/next/boot mount --bind /run /media/embassy/next/run +mount --bind /tmp /media/embassy/next/tmp mount --bind /dev /media/embassy/next/dev mount --bind /sys /media/embassy/next/sys mount --bind /proc /media/embassy/next/proc @@ -30,6 +31,7 @@ else fi umount /media/embassy/next/run +umount /media/embassy/next/tmp umount /media/embassy/next/dev umount /media/embassy/next/sys umount /media/embassy/next/proc diff --git a/check-git-hash.sh b/check-git-hash.sh index 874dcc8bf..2f59b9198 100755 --- a/check-git-hash.sh +++ b/check-git-hash.sh @@ -1,7 +1,7 @@ #!/bin/bash if [ "$GIT_BRANCH_AS_HASH" != 1 ]; then - GIT_HASH="$(git describe --always --abbrev=40 --dirty=-modified)" + GIT_HASH="$(git rev-parse HEAD)$(if ! git diff-index --quiet HEAD --; then echo '-modified'; fi)" else GIT_HASH="@$(git rev-parse --abbrev-ref HEAD)" fi diff --git a/container-runtime/package-lock.json b/container-runtime/package-lock.json index 74551217e..dd8ba7855 100644 --- a/container-runtime/package-lock.json +++ b/container-runtime/package-lock.json @@ -1,11 +1,11 @@ { - "name": "start-init", + "name": "container-runtime", "version": "0.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "start-init", + "name": "container-runtime", "version": "0.0.0", "dependencies": { "@iarna/toml": "^2.2.5", @@ -23,396 +23,28 @@ "@swc/cli": "^0.1.62", "@swc/core": "^1.3.65", "@types/node": "^20.11.13", - "esbuild": "^0.20.0", "prettier": "^3.2.5", "typescript": ">5.2" } }, "../sdk/dist": { "name": "@start9labs/start-sdk", - "version": "0.4.0-rev0.lib0.rc8.beta7", + "version": "0.4.0-rev0.lib0.rc8.beta10", "license": "MIT", "dependencies": { - "@iarna/toml": "^2.2.5", "isomorphic-fetch": "^3.0.0", - "ts-matches": "^5.4.1", - "yaml": "^2.2.2" + "ts-matches": "^5.4.1" }, "devDependencies": { + "@iarna/toml": "^2.2.5", "@types/jest": "^29.4.0", "jest": "^29.4.3", + "prettier": "^3.2.5", "ts-jest": "^29.0.5", "ts-node": "^10.9.1", "tsx": "^4.7.1", - "typescript": "^5.0.4" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.0.tgz", - "integrity": "sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.0.tgz", - "integrity": "sha512-3bMAfInvByLHfJwYPJRlpTeaQA75n8C/QKpEaiS4HrFWFiJlNI0vzq/zCjBrhAYcPyVPG7Eo9dMrcQXuqmNk5g==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.0.tgz", - "integrity": "sha512-aVpnM4lURNkp0D3qPoAzSG92VXStYmoVPOgXveAUoQBWRSuQzt51yvSju29J6AHPmwY1BjH49uR29oyfH1ra8Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.0.tgz", - "integrity": "sha512-uK7wAnlRvjkCPzh8jJ+QejFyrP8ObKuR5cBIsQZ+qbMunwR8sbd8krmMbxTLSrDhiPZaJYKQAU5Y3iMDcZPhyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.0.tgz", - "integrity": "sha512-AjEcivGAlPs3UAcJedMa9qYg9eSfU6FnGHJjT8s346HSKkrcWlYezGE8VaO2xKfvvlZkgAhyvl06OJOxiMgOYQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.0.tgz", - "integrity": "sha512-bsgTPoyYDnPv8ER0HqnJggXK6RyFy4PH4rtsId0V7Efa90u2+EifxytE9pZnsDgExgkARy24WUQGv9irVbTvIw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.0.tgz", - "integrity": "sha512-kQ7jYdlKS335mpGbMW5tEe3IrQFIok9r84EM3PXB8qBFJPSc6dpWfrtsC/y1pyrz82xfUIn5ZrnSHQQsd6jebQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.0.tgz", - "integrity": "sha512-uG8B0WSepMRsBNVXAQcHf9+Ko/Tr+XqmK7Ptel9HVmnykupXdS4J7ovSQUIi0tQGIndhbqWLaIL/qO/cWhXKyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.0.tgz", - "integrity": "sha512-2ezuhdiZw8vuHf1HKSf4TIk80naTbP9At7sOqZmdVwvvMyuoDiZB49YZKLsLOfKIr77+I40dWpHVeY5JHpIEIg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.0.tgz", - "integrity": "sha512-uTtyYAP5veqi2z9b6Gr0NUoNv9F/rOzI8tOD5jKcCvRUn7T60Bb+42NDBCWNhMjkQzI0qqwXkQGo1SY41G52nw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.0.tgz", - "integrity": "sha512-c88wwtfs8tTffPaoJ+SQn3y+lKtgTzyjkD8NgsyCtCmtoIC8RDL7PrJU05an/e9VuAke6eJqGkoMhJK1RY6z4w==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.0.tgz", - "integrity": "sha512-lR2rr/128/6svngnVta6JN4gxSXle/yZEZL3o4XZ6esOqhyR4wsKyfu6qXAL04S4S5CgGfG+GYZnjFd4YiG3Aw==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.0.tgz", - "integrity": "sha512-9Sycc+1uUsDnJCelDf6ZNqgZQoK1mJvFtqf2MUz4ujTxGhvCWw+4chYfDLPepMEvVL9PDwn6HrXad5yOrNzIsQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.0.tgz", - "integrity": "sha512-CoWSaaAXOZd+CjbUTdXIJE/t7Oz+4g90A3VBCHLbfuc5yUQU/nFDLOzQsN0cdxgXd97lYW/psIIBdjzQIwTBGw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.0.tgz", - "integrity": "sha512-mlb1hg/eYRJUpv8h/x+4ShgoNLL8wgZ64SUr26KwglTYnwAWjkhR2GpoKftDbPOCnodA9t4Y/b68H4J9XmmPzA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.0.tgz", - "integrity": "sha512-fgf9ubb53xSnOBqyvWEY6ukBNRl1mVX1srPNu06B6mNsNK20JfH6xV6jECzrQ69/VMiTLvHMicQR/PgTOgqJUQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.0.tgz", - "integrity": "sha512-H9Eu6MGse++204XZcYsse1yFHmRXEWgadk2N58O/xd50P9EvFMLJTQLg+lB4E1cF2xhLZU5luSWtGTb0l9UeSg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.0.tgz", - "integrity": "sha512-lCT675rTN1v8Fo+RGrE5KjSnfY0x9Og4RN7t7lVrN3vMSjy34/+3na0q7RIfWDAj0e0rCh0OL+P88lu3Rt21MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.0.tgz", - "integrity": "sha512-HKoUGXz/TOVXKQ+67NhxyHv+aDSZf44QpWLa3I1lLvAwGq8x1k0T+e2HHSRvxWhfJrFxaaqre1+YyzQ99KixoA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.0.tgz", - "integrity": "sha512-GDwAqgHQm1mVoPppGsoq4WJwT3vhnz/2N62CzhvApFD1eJyTroob30FPpOZabN+FgCjhG+AgcZyOPIkR8dfD7g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.0.tgz", - "integrity": "sha512-0vYsP8aC4TvMlOQYozoksiaxjlvUcQrac+muDqj1Fxy6jh9l9CZJzj7zmh8JGfiV49cYLTorFLxg7593pGldwQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.0.tgz", - "integrity": "sha512-p98u4rIgfh4gdpV00IqknBD5pC84LCub+4a3MO+zjqvU5MVXOc3hqR2UgT2jI2nh3h8s9EQxmOsVI3tyzv1iFg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.0.tgz", - "integrity": "sha512-NgJnesu1RtWihtTtXGFMU5YSE6JyyHPMxCwBZK7a6/8d31GuSo9l0Ss7w1Jw5QnKUawG6UEehs883kcXf5fYwg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "typescript": "^5.0.4", + "yaml": "^2.2.2" } }, "node_modules/@iarna/toml": { @@ -1304,44 +936,6 @@ "node": ">= 0.4" } }, - "node_modules/esbuild": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.0.tgz", - "integrity": "sha512-6iwE3Y2RVYCME1jLpBqq7LQWK3MW6vjV2bZy6gt/WrqkY+WE74Spyc0ThAOYpMtITvnjX09CrC6ym7A/m9mebA==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.0", - "@esbuild/android-arm": "0.20.0", - "@esbuild/android-arm64": "0.20.0", - "@esbuild/android-x64": "0.20.0", - "@esbuild/darwin-arm64": "0.20.0", - "@esbuild/darwin-x64": "0.20.0", - "@esbuild/freebsd-arm64": "0.20.0", - "@esbuild/freebsd-x64": "0.20.0", - "@esbuild/linux-arm": "0.20.0", - "@esbuild/linux-arm64": "0.20.0", - "@esbuild/linux-ia32": "0.20.0", - "@esbuild/linux-loong64": "0.20.0", - "@esbuild/linux-mips64el": "0.20.0", - "@esbuild/linux-ppc64": "0.20.0", - "@esbuild/linux-riscv64": "0.20.0", - "@esbuild/linux-s390x": "0.20.0", - "@esbuild/linux-x64": "0.20.0", - "@esbuild/netbsd-x64": "0.20.0", - "@esbuild/openbsd-x64": "0.20.0", - "@esbuild/sunos-x64": "0.20.0", - "@esbuild/win32-arm64": "0.20.0", - "@esbuild/win32-ia32": "0.20.0", - "@esbuild/win32-x64": "0.20.0" - } - }, "node_modules/esbuild-plugin-resolve": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/esbuild-plugin-resolve/-/esbuild-plugin-resolve-2.0.0.tgz", @@ -2965,167 +2559,6 @@ } }, "dependencies": { - "@esbuild/aix-ppc64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.0.tgz", - "integrity": "sha512-fGFDEctNh0CcSwsiRPxiaqX0P5rq+AqE0SRhYGZ4PX46Lg1FNR6oCxJghf8YgY0WQEgQuh3lErUFE4KxLeRmmw==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.0.tgz", - "integrity": "sha512-3bMAfInvByLHfJwYPJRlpTeaQA75n8C/QKpEaiS4HrFWFiJlNI0vzq/zCjBrhAYcPyVPG7Eo9dMrcQXuqmNk5g==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.0.tgz", - "integrity": "sha512-aVpnM4lURNkp0D3qPoAzSG92VXStYmoVPOgXveAUoQBWRSuQzt51yvSju29J6AHPmwY1BjH49uR29oyfH1ra8Q==", - "dev": true, - "optional": true - }, - "@esbuild/android-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.0.tgz", - "integrity": "sha512-uK7wAnlRvjkCPzh8jJ+QejFyrP8ObKuR5cBIsQZ+qbMunwR8sbd8krmMbxTLSrDhiPZaJYKQAU5Y3iMDcZPhyQ==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.0.tgz", - "integrity": "sha512-AjEcivGAlPs3UAcJedMa9qYg9eSfU6FnGHJjT8s346HSKkrcWlYezGE8VaO2xKfvvlZkgAhyvl06OJOxiMgOYQ==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.0.tgz", - "integrity": "sha512-bsgTPoyYDnPv8ER0HqnJggXK6RyFy4PH4rtsId0V7Efa90u2+EifxytE9pZnsDgExgkARy24WUQGv9irVbTvIw==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.0.tgz", - "integrity": "sha512-kQ7jYdlKS335mpGbMW5tEe3IrQFIok9r84EM3PXB8qBFJPSc6dpWfrtsC/y1pyrz82xfUIn5ZrnSHQQsd6jebQ==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.0.tgz", - "integrity": "sha512-uG8B0WSepMRsBNVXAQcHf9+Ko/Tr+XqmK7Ptel9HVmnykupXdS4J7ovSQUIi0tQGIndhbqWLaIL/qO/cWhXKyQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.0.tgz", - "integrity": "sha512-2ezuhdiZw8vuHf1HKSf4TIk80naTbP9At7sOqZmdVwvvMyuoDiZB49YZKLsLOfKIr77+I40dWpHVeY5JHpIEIg==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.0.tgz", - "integrity": "sha512-uTtyYAP5veqi2z9b6Gr0NUoNv9F/rOzI8tOD5jKcCvRUn7T60Bb+42NDBCWNhMjkQzI0qqwXkQGo1SY41G52nw==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ia32": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.0.tgz", - "integrity": "sha512-c88wwtfs8tTffPaoJ+SQn3y+lKtgTzyjkD8NgsyCtCmtoIC8RDL7PrJU05an/e9VuAke6eJqGkoMhJK1RY6z4w==", - "dev": true, - "optional": true - }, - "@esbuild/linux-loong64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.0.tgz", - "integrity": "sha512-lR2rr/128/6svngnVta6JN4gxSXle/yZEZL3o4XZ6esOqhyR4wsKyfu6qXAL04S4S5CgGfG+GYZnjFd4YiG3Aw==", - "dev": true, - "optional": true - }, - "@esbuild/linux-mips64el": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.0.tgz", - "integrity": "sha512-9Sycc+1uUsDnJCelDf6ZNqgZQoK1mJvFtqf2MUz4ujTxGhvCWw+4chYfDLPepMEvVL9PDwn6HrXad5yOrNzIsQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ppc64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.0.tgz", - "integrity": "sha512-CoWSaaAXOZd+CjbUTdXIJE/t7Oz+4g90A3VBCHLbfuc5yUQU/nFDLOzQsN0cdxgXd97lYW/psIIBdjzQIwTBGw==", - "dev": true, - "optional": true - }, - "@esbuild/linux-riscv64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.0.tgz", - "integrity": "sha512-mlb1hg/eYRJUpv8h/x+4ShgoNLL8wgZ64SUr26KwglTYnwAWjkhR2GpoKftDbPOCnodA9t4Y/b68H4J9XmmPzA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-s390x": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.0.tgz", - "integrity": "sha512-fgf9ubb53xSnOBqyvWEY6ukBNRl1mVX1srPNu06B6mNsNK20JfH6xV6jECzrQ69/VMiTLvHMicQR/PgTOgqJUQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.0.tgz", - "integrity": "sha512-H9Eu6MGse++204XZcYsse1yFHmRXEWgadk2N58O/xd50P9EvFMLJTQLg+lB4E1cF2xhLZU5luSWtGTb0l9UeSg==", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.0.tgz", - "integrity": "sha512-lCT675rTN1v8Fo+RGrE5KjSnfY0x9Og4RN7t7lVrN3vMSjy34/+3na0q7RIfWDAj0e0rCh0OL+P88lu3Rt21MQ==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.0.tgz", - "integrity": "sha512-HKoUGXz/TOVXKQ+67NhxyHv+aDSZf44QpWLa3I1lLvAwGq8x1k0T+e2HHSRvxWhfJrFxaaqre1+YyzQ99KixoA==", - "dev": true, - "optional": true - }, - "@esbuild/sunos-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.0.tgz", - "integrity": "sha512-GDwAqgHQm1mVoPppGsoq4WJwT3vhnz/2N62CzhvApFD1eJyTroob30FPpOZabN+FgCjhG+AgcZyOPIkR8dfD7g==", - "dev": true, - "optional": true - }, - "@esbuild/win32-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.0.tgz", - "integrity": "sha512-0vYsP8aC4TvMlOQYozoksiaxjlvUcQrac+muDqj1Fxy6jh9l9CZJzj7zmh8JGfiV49cYLTorFLxg7593pGldwQ==", - "dev": true, - "optional": true - }, - "@esbuild/win32-ia32": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.0.tgz", - "integrity": "sha512-p98u4rIgfh4gdpV00IqknBD5pC84LCub+4a3MO+zjqvU5MVXOc3hqR2UgT2jI2nh3h8s9EQxmOsVI3tyzv1iFg==", - "dev": true, - "optional": true - }, - "@esbuild/win32-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.0.tgz", - "integrity": "sha512-NgJnesu1RtWihtTtXGFMU5YSE6JyyHPMxCwBZK7a6/8d31GuSo9l0Ss7w1Jw5QnKUawG6UEehs883kcXf5fYwg==", - "dev": true, - "optional": true - }, "@iarna/toml": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", @@ -3186,6 +2619,7 @@ "@types/jest": "^29.4.0", "isomorphic-fetch": "^3.0.0", "jest": "^29.4.3", + "prettier": "^3.2.5", "ts-jest": "^29.0.5", "ts-matches": "^5.4.1", "ts-node": "^10.9.1", @@ -3732,37 +3166,6 @@ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" }, - "esbuild": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.0.tgz", - "integrity": "sha512-6iwE3Y2RVYCME1jLpBqq7LQWK3MW6vjV2bZy6gt/WrqkY+WE74Spyc0ThAOYpMtITvnjX09CrC6ym7A/m9mebA==", - "dev": true, - "requires": { - "@esbuild/aix-ppc64": "0.20.0", - "@esbuild/android-arm": "0.20.0", - "@esbuild/android-arm64": "0.20.0", - "@esbuild/android-x64": "0.20.0", - "@esbuild/darwin-arm64": "0.20.0", - "@esbuild/darwin-x64": "0.20.0", - "@esbuild/freebsd-arm64": "0.20.0", - "@esbuild/freebsd-x64": "0.20.0", - "@esbuild/linux-arm": "0.20.0", - "@esbuild/linux-arm64": "0.20.0", - "@esbuild/linux-ia32": "0.20.0", - "@esbuild/linux-loong64": "0.20.0", - "@esbuild/linux-mips64el": "0.20.0", - "@esbuild/linux-ppc64": "0.20.0", - "@esbuild/linux-riscv64": "0.20.0", - "@esbuild/linux-s390x": "0.20.0", - "@esbuild/linux-x64": "0.20.0", - "@esbuild/netbsd-x64": "0.20.0", - "@esbuild/openbsd-x64": "0.20.0", - "@esbuild/sunos-x64": "0.20.0", - "@esbuild/win32-arm64": "0.20.0", - "@esbuild/win32-ia32": "0.20.0", - "@esbuild/win32-x64": "0.20.0" - } - }, "esbuild-plugin-resolve": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/esbuild-plugin-resolve/-/esbuild-plugin-resolve-2.0.0.tgz", diff --git a/container-runtime/package.json b/container-runtime/package.json index 13cf14ed5..f5e34a618 100644 --- a/container-runtime/package.json +++ b/container-runtime/package.json @@ -1,5 +1,5 @@ { - "name": "start-init", + "name": "container-runtime", "version": "0.0.0", "description": "We want to be the sdk intermitent for the system", "module": "./index.js", diff --git a/container-runtime/src/Adapters/RpcListener.ts b/container-runtime/src/Adapters/RpcListener.ts index 202e942b5..db5b786fc 100644 --- a/container-runtime/src/Adapters/RpcListener.ts +++ b/container-runtime/src/Adapters/RpcListener.ts @@ -101,7 +101,7 @@ const evalType = object({ }), }) -const jsonParse = (x: Buffer) => JSON.parse(x.toString()) +const jsonParse = (x: string) => JSON.parse(x) function reduceMethod( methodArgs: object, effects: HostSystem, @@ -160,21 +160,21 @@ export class RpcListener { details: error?.message ?? String(error), debug: error?.stack, }, - code: 0, + code: 1, }, }) const writeDataToSocket = (x: SocketResponse) => - new Promise((resolve) => s.write(JSON.stringify(x), resolve)) + new Promise((resolve) => s.write(JSON.stringify(x) + "\n", resolve)) s.on("data", (a) => Promise.resolve(a) + .then((b) => b.toString()) .then(logData("dataIn")) .then(jsonParse) .then(captureId) .then((x) => this.dealWithInput(x)) .catch(mapError) .then(logData("response")) - .then(writeDataToSocket) - .finally(() => void s.end()), + .then(writeDataToSocket), ) }) } @@ -244,7 +244,7 @@ export class RpcListener { })), ) .when(exitType, async ({ id }) => { - if (this._system) this._system.exit(this.effects) + if (this._system) await this._system.exit(this.effects) delete this._system delete this._effects diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index a41047ace..a76307368 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -30,7 +30,7 @@ import { import { HostSystemStartOs } from "../../HostSystemStartOs" import { JsonPath, unNestPath } from "../../../Models/JsonPath" import { RpcResult, matchRpcResult } from "../../RpcListener" -import { InputSpec } from "@start9labs/start-sdk/cjs/sdk/lib/config/configTypes" +import { CT } from "@start9labs/start-sdk" type Optional = A | undefined | null function todo(): never { @@ -326,7 +326,7 @@ export class SystemForEmbassy implements System { name: action.name, description: action.description, warning: action.warning || null, - input: action["input-spec"] as InputSpec, + input: action["input-spec"] as CT.InputSpec, disabled: false, allowedStatuses, group: null, diff --git a/container-runtime/tsconfig.json b/container-runtime/tsconfig.json index fd93d5154..0b2fe6e32 100644 --- a/container-runtime/tsconfig.json +++ b/container-runtime/tsconfig.json @@ -1,11 +1,5 @@ { - "include": [ - "./**/*.mjs", - "./**/*.js", - "src/Adapters/RpcListener.ts", - "src/index.ts", - "effects.ts" - ], + "include": ["./**/*.ts"], "exclude": ["dist"], "inputs": ["./src/index.ts"], "compilerOptions": { diff --git a/core/.gitignore b/core/.gitignore index ad8368a86..9673044e6 100644 --- a/core/.gitignore +++ b/core/.gitignore @@ -8,3 +8,4 @@ secrets.db .env .editorconfig proptest-regressions/**/* +/startos/bindings/* \ No newline at end of file diff --git a/core/startos/bindings/ActionMetadata.ts b/core/startos/bindings/ActionMetadata.ts deleted file mode 100644 index c9373a5b8..000000000 --- a/core/startos/bindings/ActionMetadata.ts +++ /dev/null @@ -1,12 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AllowedStatuses } from "./AllowedStatuses"; - -export type ActionMetadata = { - name: string; - description: string; - warning: string | null; - input: any; - disabled: boolean; - allowedStatuses: AllowedStatuses; - group: string | null; -}; diff --git a/core/startos/bindings/AddressInfo.ts b/core/startos/bindings/AddressInfo.ts deleted file mode 100644 index 1355f72a2..000000000 --- a/core/startos/bindings/AddressInfo.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { BindOptions } from "./BindOptions"; -import type { HostId } from "./HostId"; - -export type AddressInfo = { - username: string | null; - hostId: HostId; - bindOptions: BindOptions; - suffix: string; -}; diff --git a/core/startos/bindings/BindOptions.ts b/core/startos/bindings/BindOptions.ts deleted file mode 100644 index 6b12139a7..000000000 --- a/core/startos/bindings/BindOptions.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AddSslOptions } from "./AddSslOptions"; -import type { Security } from "./Security"; - -export type BindOptions = { - scheme: string | null; - preferredExternalPort: number; - addSsl: AddSslOptions | null; - secure: Security | null; -}; diff --git a/core/startos/bindings/BindParams.ts b/core/startos/bindings/BindParams.ts deleted file mode 100644 index 4aa78e522..000000000 --- a/core/startos/bindings/BindParams.ts +++ /dev/null @@ -1,15 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AddSslOptions } from "./AddSslOptions"; -import type { HostId } from "./HostId"; -import type { HostKind } from "./HostKind"; -import type { Security } from "./Security"; - -export type BindParams = { - kind: HostKind; - id: HostId; - internalPort: number; - scheme: string | null; - preferredExternalPort: number; - addSsl: AddSslOptions | null; - secure: Security | null; -}; diff --git a/core/startos/bindings/Dependencies.ts b/core/startos/bindings/Dependencies.ts deleted file mode 100644 index 974495b7b..000000000 --- a/core/startos/bindings/Dependencies.ts +++ /dev/null @@ -1,5 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DepInfo } from "./DepInfo"; -import type { PackageId } from "./PackageId"; - -export type Dependencies = { [key: PackageId]: DepInfo }; diff --git a/core/startos/bindings/DestroyOverlayedImageParams.ts b/core/startos/bindings/DestroyOverlayedImageParams.ts deleted file mode 100644 index 47671604a..000000000 --- a/core/startos/bindings/DestroyOverlayedImageParams.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type DestroyOverlayedImageParams = { guid: string }; diff --git a/core/startos/bindings/ExportServiceInterfaceParams.ts b/core/startos/bindings/ExportServiceInterfaceParams.ts deleted file mode 100644 index 93deb0ce5..000000000 --- a/core/startos/bindings/ExportServiceInterfaceParams.ts +++ /dev/null @@ -1,14 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AddressInfo } from "./AddressInfo"; -import type { ServiceInterfaceType } from "./ServiceInterfaceType"; - -export type ExportServiceInterfaceParams = { - id: string; - name: string; - description: string; - hasPrimary: boolean; - disabled: boolean; - masked: boolean; - addressInfo: AddressInfo; - type: ServiceInterfaceType; -}; diff --git a/core/startos/bindings/ExportedHostnameInfo.ts b/core/startos/bindings/ExportedHostnameInfo.ts deleted file mode 100644 index 23cbdd487..000000000 --- a/core/startos/bindings/ExportedHostnameInfo.ts +++ /dev/null @@ -1,12 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExportedIpHostname } from "./ExportedIpHostname"; -import type { ExportedOnionHostname } from "./ExportedOnionHostname"; - -export type ExportedHostnameInfo = - | { - kind: "ip"; - networkInterfaceId: string; - public: boolean; - hostname: ExportedIpHostname; - } - | { kind: "onion"; hostname: ExportedOnionHostname }; diff --git a/core/startos/bindings/ExposeForDependentsParams.ts b/core/startos/bindings/ExposeForDependentsParams.ts deleted file mode 100644 index 714771c1e..000000000 --- a/core/startos/bindings/ExposeForDependentsParams.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ExposeForDependentsParams = { paths: string[] }; diff --git a/core/startos/bindings/GetSystemSmtpParams.ts b/core/startos/bindings/GetSystemSmtpParams.ts deleted file mode 100644 index b96b9f595..000000000 --- a/core/startos/bindings/GetSystemSmtpParams.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Callback } from "./Callback"; - -export type GetSystemSmtpParams = { callback: Callback }; diff --git a/core/startos/bindings/Host.ts b/core/startos/bindings/Host.ts deleted file mode 100644 index 4188cb404..000000000 --- a/core/startos/bindings/Host.ts +++ /dev/null @@ -1,11 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { BindInfo } from "./BindInfo"; -import type { HostAddress } from "./HostAddress"; -import type { HostKind } from "./HostKind"; - -export type Host = { - kind: HostKind; - bindings: { [key: number]: BindInfo }; - addresses: Array; - primary: HostAddress | null; -}; diff --git a/core/startos/bindings/HostAddress.ts b/core/startos/bindings/HostAddress.ts deleted file mode 100644 index 9cddf778b..000000000 --- a/core/startos/bindings/HostAddress.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type HostAddress = { kind: "onion"; address: string }; diff --git a/core/startos/bindings/HostInfo.ts b/core/startos/bindings/HostInfo.ts deleted file mode 100644 index b69dbf6b4..000000000 --- a/core/startos/bindings/HostInfo.ts +++ /dev/null @@ -1,5 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Host } from "./Host"; -import type { HostId } from "./HostId"; - -export type HostInfo = { [key: HostId]: Host }; diff --git a/core/startos/bindings/InstalledState.ts b/core/startos/bindings/InstalledState.ts deleted file mode 100644 index 053c3ae66..000000000 --- a/core/startos/bindings/InstalledState.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Manifest } from "./Manifest"; - -export type InstalledState = { manifest: Manifest }; diff --git a/core/startos/bindings/MainStatus.ts b/core/startos/bindings/MainStatus.ts deleted file mode 100644 index 878213087..000000000 --- a/core/startos/bindings/MainStatus.ts +++ /dev/null @@ -1,20 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Duration } from "./Duration"; -import type { HealthCheckId } from "./HealthCheckId"; -import type { HealthCheckResult } from "./HealthCheckResult"; - -export type MainStatus = - | { status: "stopped" } - | { status: "restarting" } - | { status: "stopping"; timeout: Duration } - | { status: "starting" } - | { - status: "running"; - started: string; - health: { [key: HealthCheckId]: HealthCheckResult }; - } - | { - status: "backingUp"; - started: string | null; - health: { [key: HealthCheckId]: HealthCheckResult }; - }; diff --git a/core/startos/bindings/Manifest.ts b/core/startos/bindings/Manifest.ts deleted file mode 100644 index 688486a7d..000000000 --- a/core/startos/bindings/Manifest.ts +++ /dev/null @@ -1,32 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Alerts } from "./Alerts"; -import type { Dependencies } from "./Dependencies"; -import type { Description } from "./Description"; -import type { HardwareRequirements } from "./HardwareRequirements"; -import type { ImageId } from "./ImageId"; -import type { PackageId } from "./PackageId"; -import type { VolumeId } from "./VolumeId"; - -export type Manifest = { - id: PackageId; - title: string; - version: string; - releaseNotes: string; - license: string; - replaces: Array; - wrapperRepo: string; - upstreamRepo: string; - supportSite: string; - marketingSite: string; - donationUrl: string | null; - description: Description; - images: Array; - assets: Array; - volumes: Array; - alerts: Alerts; - dependencies: Dependencies; - hardwareRequirements: HardwareRequirements; - gitHash: string | null; - osVersion: string; - hasConfig: boolean; -}; diff --git a/core/startos/bindings/PackageDataEntry.ts b/core/startos/bindings/PackageDataEntry.ts deleted file mode 100644 index 8b906bb90..000000000 --- a/core/startos/bindings/PackageDataEntry.ts +++ /dev/null @@ -1,26 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ActionId } from "./ActionId"; -import type { ActionMetadata } from "./ActionMetadata"; -import type { CurrentDependencies } from "./CurrentDependencies"; -import type { DataUrl } from "./DataUrl"; -import type { HostInfo } from "./HostInfo"; -import type { PackageState } from "./PackageState"; -import type { ServiceInterfaceId } from "./ServiceInterfaceId"; -import type { ServiceInterfaceWithHostInfo } from "./ServiceInterfaceWithHostInfo"; -import type { Status } from "./Status"; - -export type PackageDataEntry = { - stateInfo: PackageState; - status: Status; - marketplaceUrl: string | null; - developerKey: string; - icon: DataUrl; - lastBackup: string | null; - currentDependencies: CurrentDependencies; - actions: { [key: ActionId]: ActionMetadata }; - serviceInterfaces: { - [key: ServiceInterfaceId]: ServiceInterfaceWithHostInfo; - }; - hosts: HostInfo; - storeExposedDependents: string[]; -}; diff --git a/core/startos/bindings/ParamsMaybePackageId.ts b/core/startos/bindings/ParamsMaybePackageId.ts deleted file mode 100644 index e9f0f170c..000000000 --- a/core/startos/bindings/ParamsMaybePackageId.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ParamsMaybePackageId = { packageId: string | null }; diff --git a/core/startos/bindings/ParamsPackageId.ts b/core/startos/bindings/ParamsPackageId.ts deleted file mode 100644 index 7bc919843..000000000 --- a/core/startos/bindings/ParamsPackageId.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ParamsPackageId = { packageId: string }; diff --git a/core/startos/bindings/PasswordType.ts b/core/startos/bindings/PasswordType.ts deleted file mode 100644 index 0f36f60a2..000000000 --- a/core/startos/bindings/PasswordType.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { EncryptedWire } from "./EncryptedWire"; - -export type PasswordType = EncryptedWire | string; diff --git a/core/startos/bindings/Public.ts b/core/startos/bindings/Public.ts deleted file mode 100644 index 442176303..000000000 --- a/core/startos/bindings/Public.ts +++ /dev/null @@ -1,9 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AllPackageData } from "./AllPackageData"; -import type { ServerInfo } from "./ServerInfo"; - -export type Public = { - serverInfo: ServerInfo; - packageData: AllPackageData; - ui: any; -}; diff --git a/core/startos/bindings/RemoveActionParams.ts b/core/startos/bindings/RemoveActionParams.ts deleted file mode 100644 index fcd567c3f..000000000 --- a/core/startos/bindings/RemoveActionParams.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RemoveActionParams = { id: string }; diff --git a/core/startos/bindings/RemoveAddressParams.ts b/core/startos/bindings/RemoveAddressParams.ts deleted file mode 100644 index 578631d39..000000000 --- a/core/startos/bindings/RemoveAddressParams.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RemoveAddressParams = { id: string }; diff --git a/core/startos/bindings/ReverseProxyBind.ts b/core/startos/bindings/ReverseProxyBind.ts deleted file mode 100644 index c9d67b127..000000000 --- a/core/startos/bindings/ReverseProxyBind.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ReverseProxyBind = { - ip: string | null; - port: number; - ssl: boolean; -}; diff --git a/core/startos/bindings/ReverseProxyParams.ts b/core/startos/bindings/ReverseProxyParams.ts deleted file mode 100644 index 6f684b780..000000000 --- a/core/startos/bindings/ReverseProxyParams.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ReverseProxyBind } from "./ReverseProxyBind"; -import type { ReverseProxyDestination } from "./ReverseProxyDestination"; -import type { ReverseProxyHttp } from "./ReverseProxyHttp"; - -export type ReverseProxyParams = { - bind: ReverseProxyBind; - dst: ReverseProxyDestination; - http: ReverseProxyHttp; -}; diff --git a/core/startos/bindings/ServerInfo.ts b/core/startos/bindings/ServerInfo.ts deleted file mode 100644 index 618d396fd..000000000 --- a/core/startos/bindings/ServerInfo.ts +++ /dev/null @@ -1,31 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Governor } from "./Governor"; -import type { IpInfo } from "./IpInfo"; -import type { ServerStatus } from "./ServerStatus"; -import type { WifiInfo } from "./WifiInfo"; - -export type ServerInfo = { - arch: string; - platform: string; - id: string; - hostname: string; - version: string; - lastBackup: string | null; - eosVersionCompat: string; - lanAddress: string; - onionAddress: string; - /** - * for backwards compatibility - */ - torAddress: string; - ipInfo: { [key: string]: IpInfo }; - statusInfo: ServerStatus; - wifi: WifiInfo; - unreadNotificationCount: number; - passwordHash: string; - pubkey: string; - caFingerprint: string; - ntpSynced: boolean; - zram: boolean; - governor: Governor | null; -}; diff --git a/core/startos/bindings/ServerStatus.ts b/core/startos/bindings/ServerStatus.ts deleted file mode 100644 index e72d5d6de..000000000 --- a/core/startos/bindings/ServerStatus.ts +++ /dev/null @@ -1,12 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { BackupProgress } from "./BackupProgress"; -import type { PackageId } from "./PackageId"; -import type { UpdateProgress } from "./UpdateProgress"; - -export type ServerStatus = { - backupProgress: { [key: PackageId]: BackupProgress } | null; - updated: boolean; - updateProgress: UpdateProgress | null; - shuttingDown: boolean; - restarting: boolean; -}; diff --git a/core/startos/bindings/ServiceInterface.ts b/core/startos/bindings/ServiceInterface.ts deleted file mode 100644 index 4167257bb..000000000 --- a/core/startos/bindings/ServiceInterface.ts +++ /dev/null @@ -1,15 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AddressInfo } from "./AddressInfo"; -import type { ServiceInterfaceId } from "./ServiceInterfaceId"; -import type { ServiceInterfaceType } from "./ServiceInterfaceType"; - -export type ServiceInterface = { - id: ServiceInterfaceId; - name: string; - description: string; - hasPrimary: boolean; - disabled: boolean; - masked: boolean; - addressInfo: AddressInfo; - type: ServiceInterfaceType; -}; diff --git a/core/startos/bindings/ServiceInterfaceType.ts b/core/startos/bindings/ServiceInterfaceType.ts deleted file mode 100644 index fadd11f9d..000000000 --- a/core/startos/bindings/ServiceInterfaceType.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ServiceInterfaceType = "ui" | "p2p" | "api"; diff --git a/core/startos/bindings/ServiceInterfaceWithHostInfo.ts b/core/startos/bindings/ServiceInterfaceWithHostInfo.ts deleted file mode 100644 index bef83abe2..000000000 --- a/core/startos/bindings/ServiceInterfaceWithHostInfo.ts +++ /dev/null @@ -1,17 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AddressInfo } from "./AddressInfo"; -import type { ExportedHostInfo } from "./ExportedHostInfo"; -import type { ServiceInterfaceId } from "./ServiceInterfaceId"; -import type { ServiceInterfaceType } from "./ServiceInterfaceType"; - -export type ServiceInterfaceWithHostInfo = { - hostInfo: ExportedHostInfo; - id: ServiceInterfaceId; - name: string; - description: string; - hasPrimary: boolean; - disabled: boolean; - masked: boolean; - addressInfo: AddressInfo; - type: ServiceInterfaceType; -}; diff --git a/core/startos/bindings/SetConfigured.ts b/core/startos/bindings/SetConfigured.ts deleted file mode 100644 index ff9eaf11d..000000000 --- a/core/startos/bindings/SetConfigured.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SetConfigured = { configured: boolean }; diff --git a/core/startos/bindings/SetMainStatus.ts b/core/startos/bindings/SetMainStatus.ts deleted file mode 100644 index 653342e5f..000000000 --- a/core/startos/bindings/SetMainStatus.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Status } from "./Status"; - -export type SetMainStatus = { status: Status }; diff --git a/core/startos/bindings/SetStoreParams.ts b/core/startos/bindings/SetStoreParams.ts deleted file mode 100644 index 8737295bd..000000000 --- a/core/startos/bindings/SetStoreParams.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SetStoreParams = { value: any; path: string }; diff --git a/core/startos/bindings/Status.ts b/core/startos/bindings/Status.ts deleted file mode 100644 index 6b240347d..000000000 --- a/core/startos/bindings/Status.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { MainStatus } from "./MainStatus"; - -export type Status = { configured: boolean; main: MainStatus }; diff --git a/core/startos/bindings/UpdatingState.ts b/core/startos/bindings/UpdatingState.ts deleted file mode 100644 index 37a83f0df..000000000 --- a/core/startos/bindings/UpdatingState.ts +++ /dev/null @@ -1,8 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { InstallingInfo } from "./InstallingInfo"; -import type { Manifest } from "./Manifest"; - -export type UpdatingState = { - manifest: Manifest; - installingInfo: InstallingInfo; -}; diff --git a/core/startos/bindings/index.ts b/core/startos/bindings/index.ts deleted file mode 100644 index fd422a2f5..000000000 --- a/core/startos/bindings/index.ts +++ /dev/null @@ -1,101 +0,0 @@ -export { ActionId } from "./ActionId"; -export { ActionMetadata } from "./ActionMetadata"; -export { AddressInfo } from "./AddressInfo"; -export { AddSslOptions } from "./AddSslOptions"; -export { Alerts } from "./Alerts"; -export { Algorithm } from "./Algorithm"; -export { AllowedStatuses } from "./AllowedStatuses"; -export { AllPackageData } from "./AllPackageData"; -export { AlpnInfo } from "./AlpnInfo"; -export { BackupProgress } from "./BackupProgress"; -export { BindInfo } from "./BindInfo"; -export { BindOptions } from "./BindOptions"; -export { BindParams } from "./BindParams"; -export { Callback } from "./Callback"; -export { ChrootParams } from "./ChrootParams"; -export { CreateOverlayedImageParams } from "./CreateOverlayedImageParams"; -export { CurrentDependencies } from "./CurrentDependencies"; -export { CurrentDependencyInfo } from "./CurrentDependencyInfo"; -export { DataUrl } from "./DataUrl"; -export { Dependencies } from "./Dependencies"; -export { DependencyKind } from "./DependencyKind"; -export { DependencyRequirement } from "./DependencyRequirement"; -export { DepInfo } from "./DepInfo"; -export { Description } from "./Description"; -export { DestroyOverlayedImageParams } from "./DestroyOverlayedImageParams"; -export { Duration } from "./Duration"; -export { EncryptedWire } from "./EncryptedWire"; -export { ExecuteAction } from "./ExecuteAction"; -export { ExportActionParams } from "./ExportActionParams"; -export { ExportedHostInfo } from "./ExportedHostInfo"; -export { ExportedHostnameInfo } from "./ExportedHostnameInfo"; -export { ExportedIpHostname } from "./ExportedIpHostname"; -export { ExportedOnionHostname } from "./ExportedOnionHostname"; -export { ExportServiceInterfaceParams } from "./ExportServiceInterfaceParams"; -export { ExposeForDependentsParams } from "./ExposeForDependentsParams"; -export { FullProgress } from "./FullProgress"; -export { GetHostInfoParamsKind } from "./GetHostInfoParamsKind"; -export { GetHostInfoParams } from "./GetHostInfoParams"; -export { GetPrimaryUrlParams } from "./GetPrimaryUrlParams"; -export { GetServiceInterfaceParams } from "./GetServiceInterfaceParams"; -export { GetServicePortForwardParams } from "./GetServicePortForwardParams"; -export { GetSslCertificateParams } from "./GetSslCertificateParams"; -export { GetSslKeyParams } from "./GetSslKeyParams"; -export { GetStoreParams } from "./GetStoreParams"; -export { GetSystemSmtpParams } from "./GetSystemSmtpParams"; -export { Governor } from "./Governor"; -export { HardwareRequirements } from "./HardwareRequirements"; -export { HealthCheckId } from "./HealthCheckId"; -export { HealthCheckResult } from "./HealthCheckResult"; -export { HostAddress } from "./HostAddress"; -export { HostId } from "./HostId"; -export { HostInfo } from "./HostInfo"; -export { HostKind } from "./HostKind"; -export { Host } from "./Host"; -export { ImageId } from "./ImageId"; -export { InstalledState } from "./InstalledState"; -export { InstallingInfo } from "./InstallingInfo"; -export { InstallingState } from "./InstallingState"; -export { IpInfo } from "./IpInfo"; -export { ListServiceInterfacesParams } from "./ListServiceInterfacesParams"; -export { MainStatus } from "./MainStatus"; -export { Manifest } from "./Manifest"; -export { MaybeUtf8String } from "./MaybeUtf8String"; -export { MountParams } from "./MountParams"; -export { MountTarget } from "./MountTarget"; -export { NamedProgress } from "./NamedProgress"; -export { PackageDataEntry } from "./PackageDataEntry"; -export { PackageId } from "./PackageId"; -export { PackageState } from "./PackageState"; -export { ParamsMaybePackageId } from "./ParamsMaybePackageId"; -export { ParamsPackageId } from "./ParamsPackageId"; -export { PasswordType } from "./PasswordType"; -export { Progress } from "./Progress"; -export { Public } from "./Public"; -export { RemoveActionParams } from "./RemoveActionParams"; -export { RemoveAddressParams } from "./RemoveAddressParams"; -export { ReverseProxyBind } from "./ReverseProxyBind"; -export { ReverseProxyDestination } from "./ReverseProxyDestination"; -export { ReverseProxyHttp } from "./ReverseProxyHttp"; -export { ReverseProxyParams } from "./ReverseProxyParams"; -export { Security } from "./Security"; -export { ServerInfo } from "./ServerInfo"; -export { ServerSpecs } from "./ServerSpecs"; -export { ServerStatus } from "./ServerStatus"; -export { ServiceInterfaceId } from "./ServiceInterfaceId"; -export { ServiceInterface } from "./ServiceInterface"; -export { ServiceInterfaceType } from "./ServiceInterfaceType"; -export { ServiceInterfaceWithHostInfo } from "./ServiceInterfaceWithHostInfo"; -export { SessionList } from "./SessionList"; -export { Sessions } from "./Sessions"; -export { Session } from "./Session"; -export { SetConfigured } from "./SetConfigured"; -export { SetDependenciesParams } from "./SetDependenciesParams"; -export { SetHealth } from "./SetHealth"; -export { SetMainStatus } from "./SetMainStatus"; -export { SetStoreParams } from "./SetStoreParams"; -export { Status } from "./Status"; -export { UpdateProgress } from "./UpdateProgress"; -export { UpdatingState } from "./UpdatingState"; -export { VolumeId } from "./VolumeId"; -export { WifiInfo } from "./WifiInfo"; diff --git a/core/startos/src/backup/restore.rs b/core/startos/src/backup/restore.rs index 4753a4290..774b1f1cf 100644 --- a/core/startos/src/backup/restore.rs +++ b/core/startos/src/backup/restore.rs @@ -149,6 +149,7 @@ async fn restore_packages( S9pk::open( backup_dir.path().join(&id).with_extension("s9pk"), Some(&id), + true, ) .await?, Some(backup_dir), diff --git a/core/startos/src/core/rpc_continuations.rs b/core/startos/src/core/rpc_continuations.rs index 9a82cb1fe..0f25dd383 100644 --- a/core/startos/src/core/rpc_continuations.rs +++ b/core/startos/src/core/rpc_continuations.rs @@ -19,7 +19,7 @@ impl RequestGuid { } pub fn from(r: &str) -> Option { - if r.len() != 64 { + if r.len() != 32 { return None; } for c in r.chars() { diff --git a/core/startos/src/disk/main.rs b/core/startos/src/disk/main.rs index a337a4473..c7c634303 100644 --- a/core/startos/src/disk/main.rs +++ b/core/startos/src/disk/main.rs @@ -64,10 +64,10 @@ where .await?; } let mut guid = format!( - "EMBASSY_{}", + "STARTOS_{}", base32::encode( base32::Alphabet::RFC4648 { padding: false }, - &rand::random::<[u8; 32]>(), + &rand::random::<[u8; 20]>(), ) ); if !encrypted { @@ -219,7 +219,7 @@ pub async fn import>( if scan .values() .filter_map(|a| a.as_ref()) - .filter(|a| a.starts_with("EMBASSY_")) + .filter(|a| a.starts_with("STARTOS_") || a.starts_with("EMBASSY_")) .next() .is_none() { diff --git a/core/startos/src/disk/mount/guard.rs b/core/startos/src/disk/mount/guard.rs index af46904fd..dc73f5527 100644 --- a/core/startos/src/disk/mount/guard.rs +++ b/core/startos/src/disk/mount/guard.rs @@ -2,6 +2,7 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::sync::{Arc, Weak}; +use bytes::Buf; use lazy_static::lazy_static; use models::ResultExt; use tokio::sync::Mutex; @@ -115,7 +116,7 @@ impl GenericMountGuard for MountGuard { async fn tmp_mountpoint(source: &impl FileSystem) -> Result { Ok(Path::new(TMP_MOUNTPOINT).join(base32::encode( base32::Alphabet::RFC4648 { padding: false }, - &source.source_hash().await?, + &source.source_hash().await?[0..20], ))) } diff --git a/core/startos/src/inspect.rs b/core/startos/src/inspect.rs deleted file mode 100644 index 6d24fc32a..000000000 --- a/core/startos/src/inspect.rs +++ /dev/null @@ -1,127 +0,0 @@ -use std::path::PathBuf; - -use clap::Parser; -use rpc_toolkit::{command, from_fn_async, AnyContext, HandlerExt, ParentHandler}; -use serde::{Deserialize, Serialize}; - -use crate::context::CliContext; -use crate::s9pk::manifest::Manifest; -// use crate::s9pk::reader::S9pkReader; -use crate::util::serde::HandlerExtSerde; -use crate::Error; - -pub fn inspect() -> ParentHandler { - ParentHandler::new() - .subcommand("hash", from_fn_async(hash)) - .subcommand( - "manifest", - from_fn_async(manifest).with_display_serializable(), - ) - .subcommand("license", from_fn_async(license).no_display()) - .subcommand("icon", from_fn_async(icon).no_display()) - .subcommand("instructions", from_fn_async(instructions).no_display()) - .subcommand("docker-images", from_fn_async(docker_images).no_display()) -} - -#[derive(Deserialize, Serialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "kebab-case")] -pub struct HashParams { - path: PathBuf, -} - -pub async fn hash(_: CliContext, HashParams { path }: HashParams) -> Result { - Ok(S9pkReader::open(path, true) - .await? - .hash_str() - .unwrap() - .to_owned()) -} - -#[derive(Deserialize, Serialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "kebab-case")] -pub struct ManifestParams { - path: PathBuf, - #[arg(long = "no-verify")] - no_verify: bool, -} - -// #[command(cli_only, display(display_serializable))] -pub async fn manifest( - _: CliContext, - ManifestParams { .. }: ManifestParams, -) -> Result { - // S9pkReader::open(path, !no_verify).await?.manifest().await - todo!() -} - -#[derive(Deserialize, Serialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "kebab-case")] -pub struct InspectParams { - path: PathBuf, - #[arg(long = "no-verify")] - no_verify: bool, -} - -pub async fn license( - _: AnyContext, - InspectParams { path, no_verify }: InspectParams, -) -> Result<(), Error> { - tokio::io::copy( - &mut S9pkReader::open(path, !no_verify).await?.license().await?, - &mut tokio::io::stdout(), - ) - .await?; - Ok(()) -} - -pub async fn icon( - _: AnyContext, - InspectParams { path, no_verify }: InspectParams, -) -> Result<(), Error> { - tokio::io::copy( - &mut S9pkReader::open(path, !no_verify).await?.icon().await?, - &mut tokio::io::stdout(), - ) - .await?; - Ok(()) -} -#[derive(Deserialize, Serialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "kebab-case")] -pub struct InstructionParams { - path: PathBuf, - #[arg(long = "no-verify")] - no_verify: bool, -} - -pub async fn instructions( - _: CliContext, - InstructionParams { path, no_verify }: InstructionParams, -) -> Result<(), Error> { - tokio::io::copy( - &mut S9pkReader::open(path, !no_verify) - .await? - .instructions() - .await?, - &mut tokio::io::stdout(), - ) - .await?; - Ok(()) -} -pub async fn docker_images( - _: AnyContext, - InspectParams { path, no_verify }: InspectParams, -) -> Result<(), Error> { - tokio::io::copy( - &mut S9pkReader::open(path, !no_verify) - .await? - .docker_images() - .await?, - &mut tokio::io::stdout(), - ) - .await?; - Ok(()) -} diff --git a/core/startos/src/install/mod.rs b/core/startos/src/install/mod.rs index f20f5a5b0..ca8f1edbd 100644 --- a/core/startos/src/install/mod.rs +++ b/core/startos/src/install/mod.rs @@ -148,6 +148,7 @@ pub async fn install( .parse()?, ) .await?, + true, ) .await?; @@ -257,7 +258,7 @@ pub async fn sideload(ctx: RpcContext) -> Result { .await; tokio::spawn(async move { if let Err(e) = async { - let s9pk = S9pk::deserialize(&file).await?; + let s9pk = S9pk::deserialize(&file, true).await?; let _ = id_send.send(s9pk.as_manifest().id.clone()); ctx.services .install(ctx.clone(), s9pk, None::) @@ -423,7 +424,12 @@ pub async fn uninstall( let return_id = id.clone(); - tokio::spawn(async move { ctx.services.uninstall(&ctx, &id).await }); + tokio::spawn(async move { + if let Err(e) = ctx.services.uninstall(&ctx, &id).await { + tracing::error!("Error uninstalling service {id}: {e}"); + tracing::debug!("{e:?}"); + } + }); Ok(return_id) } diff --git a/core/startos/src/lib.rs b/core/startos/src/lib.rs index 448db9ff2..d6dad2c87 100644 --- a/core/startos/src/lib.rs +++ b/core/startos/src/lib.rs @@ -38,8 +38,6 @@ pub mod error; pub mod firmware; pub mod hostname; pub mod init; -pub mod progress; -// pub mod inspect; pub mod install; pub mod logs; pub mod lxc; @@ -48,6 +46,7 @@ pub mod net; pub mod notifications; pub mod os_install; pub mod prelude; +pub mod progress; pub mod properties; pub mod registry; pub mod s9pk; diff --git a/core/startos/src/logs.rs b/core/startos/src/logs.rs index 99bfac250..7c4ed1c28 100644 --- a/core/startos/src/logs.rs +++ b/core/startos/src/logs.rs @@ -18,17 +18,13 @@ use tokio_stream::wrappers::LinesStream; use tokio_tungstenite::tungstenite::Message; use tracing::instrument; +use crate::context::{CliContext, RpcContext}; +use crate::core::rpc_continuations::{RequestGuid, RpcContinuation}; use crate::error::ResultExt; +use crate::lxc::ContainerId; use crate::prelude::*; use crate::util::serde::Reversible; -use crate::{ - context::{CliContext, RpcContext}, - lxc::ContainerId, -}; -use crate::{ - core::rpc_continuations::{RequestGuid, RpcContinuation}, - util::Invoke, -}; +use crate::util::Invoke; #[pin_project::pin_project] pub struct LogStream { @@ -393,7 +389,9 @@ pub async fn journalctl( before: bool, follow: bool, ) -> Result { - let mut cmd = gen_journalctl_command(&id, limit); + let mut cmd = gen_journalctl_command(&id); + + cmd.arg(format!("--lines={}", limit)); let cursor_formatted = format!("--after-cursor={}", cursor.unwrap_or("")); if cursor.is_some() { @@ -410,12 +408,15 @@ pub async fn journalctl( .with_kind(ErrorKind::Deserialization)?; if follow { - let mut follow_cmd = gen_journalctl_command(&id, limit); + let mut follow_cmd = gen_journalctl_command(&id); follow_cmd.arg("-f"); if let Some(last) = deserialized_entries.last() { - cmd.arg(format!("--after-cursor={}", last.cursor)); + follow_cmd.arg(format!("--after-cursor={}", last.cursor)); + follow_cmd.arg("--lines=all"); + } else { + follow_cmd.arg("--lines=0"); } - let mut child = cmd.stdout(Stdio::piped()).spawn()?; + let mut child = follow_cmd.stdout(Stdio::piped()).spawn()?; let out = BufReader::new(child.stdout.take().ok_or_else(|| { Error::new(eyre!("No stdout available"), crate::ErrorKind::Journald) @@ -450,7 +451,7 @@ pub async fn journalctl( } } -fn gen_journalctl_command(id: &LogSource, limit: usize) -> Command { +fn gen_journalctl_command(id: &LogSource) -> Command { let mut cmd = match id { LogSource::Container(container_id) => { let mut cmd = Command::new("lxc-attach"); @@ -465,7 +466,6 @@ fn gen_journalctl_command(id: &LogSource, limit: usize) -> Command { cmd.arg("--output=json"); cmd.arg("--output-fields=MESSAGE"); - cmd.arg(format!("-n{}", limit)); match id { LogSource::Kernel => { cmd.arg("-k"); @@ -477,7 +477,6 @@ fn gen_journalctl_command(id: &LogSource, limit: usize) -> Command { LogSource::System => { cmd.arg("-u"); cmd.arg(SYSTEM_UNIT); - cmd.arg(format!("_COMM={}", SYSTEM_UNIT)); } LogSource::Container(_container_id) => { cmd.arg("-u").arg("container-runtime.service"); diff --git a/core/startos/src/lxc/mod.rs b/core/startos/src/lxc/mod.rs index 5a6e05500..9dfdff7f9 100644 --- a/core/startos/src/lxc/mod.rs +++ b/core/startos/src/lxc/mod.rs @@ -288,14 +288,6 @@ impl LxcContainer { } self.rootfs.take().unmount(true).await?; let rootfs_path = self.rootfs_dir(); - let err_path = rootfs_path.join("var/log/containerRuntime.err"); - if tokio::fs::metadata(&err_path).await.is_ok() { - let mut lines = BufReader::new(File::open(&err_path).await?).lines(); - while let Some(line) = lines.next_line().await? { - let container = &**self.guid; - tracing::error!(container, "{}", line); - } - } if tokio::fs::metadata(&rootfs_path).await.is_ok() && tokio_stream::wrappers::ReadDirStream::new(tokio::fs::read_dir(&rootfs_path).await?) .count() diff --git a/core/startos/src/registry/admin.rs b/core/startos/src/registry/admin.rs index 968393e4e..5fc538602 100644 --- a/core/startos/src/registry/admin.rs +++ b/core/startos/src/registry/admin.rs @@ -134,7 +134,7 @@ pub async fn publish( .with_prefix("[1/3]") .with_message("Querying s9pk"); pb.enable_steady_tick(Duration::from_millis(200)); - let s9pk = S9pk::open(&path, None).await?; + let s9pk = S9pk::open(&path, None, false).await?; let m = s9pk.as_manifest().clone(); pb.set_style(plain_line_style.clone()); pb.abandon(); @@ -145,7 +145,7 @@ pub async fn publish( .with_prefix("[1/3]") .with_message("Verifying s9pk"); pb.enable_steady_tick(Duration::from_millis(200)); - let s9pk = S9pk::open(&path, None).await?; + let s9pk = S9pk::open(&path, None, false).await?; // s9pk.validate().await?; todo!(); let m = s9pk.as_manifest().clone(); diff --git a/core/startos/src/s9pk/rpc.rs b/core/startos/src/s9pk/rpc.rs index 582a0a9d9..312d663fd 100644 --- a/core/startos/src/s9pk/rpc.rs +++ b/core/startos/src/s9pk/rpc.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeSet; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -60,6 +61,10 @@ fn inspect() -> ParentHandler { .with_inherited(only_parent) .with_display_serializable(), ) + .subcommand( + "cat", + from_fn_async(cat).with_inherited(only_parent).no_display(), + ) .subcommand( "manifest", from_fn_async(inspect_manifest) @@ -72,109 +77,116 @@ fn inspect() -> ParentHandler { struct AddImageParams { id: ImageId, image: String, + arches: Option>, } async fn add_image( ctx: CliContext, - AddImageParams { id, image }: AddImageParams, + AddImageParams { id, image, arches }: AddImageParams, S9pkPath { s9pk: s9pk_path }: S9pkPath, ) -> Result<(), Error> { + let mut s9pk = S9pk::from_file(super::load(&ctx, &s9pk_path).await?, false) + .await? + .into_dyn(); + let arches: BTreeSet<_> = arches + .unwrap_or_else(|| vec!["x86_64".to_owned(), "aarch64".to_owned()]) + .into_iter() + .collect(); let tmpdir = TmpDir::new().await?; - let sqfs_path = tmpdir.join("image.squashfs"); - let arch = String::from_utf8( - Command::new(CONTAINER_TOOL) - .arg("run") - .arg("--rm") - .arg("--entrypoint") - .arg("uname") - .arg(&image) - .arg("-m") - .invoke(ErrorKind::Docker) - .await?, - )?; - let env = String::from_utf8( - Command::new(CONTAINER_TOOL) - .arg("run") - .arg("--rm") - .arg("--entrypoint") - .arg("env") - .arg(&image) - .invoke(ErrorKind::Docker) - .await?, - )? - .lines() - .filter(|l| { - l.trim() - .split_once("=") - .map_or(false, |(v, _)| !SKIP_ENV.contains(&v)) - }) - .join("\n") - + "\n"; - let workdir = Path::new( - String::from_utf8( + for arch in arches { + let sqfs_path = tmpdir.join(format!("image.{arch}.squashfs")); + let docker_platform = if arch == "x86_64" { + "--platform=linux/amd64".to_owned() + } else if arch == "aarch64" { + "--platform=linux/arm64".to_owned() + } else { + format!("--platform=linux/{arch}") + }; + let env = String::from_utf8( Command::new(CONTAINER_TOOL) .arg("run") .arg("--rm") + .arg(&docker_platform) .arg("--entrypoint") - .arg("pwd") + .arg("env") .arg(&image) .invoke(ErrorKind::Docker) .await?, )? - .trim(), - ) - .to_owned(); - let container_id = String::from_utf8( - Command::new(CONTAINER_TOOL) - .arg("create") - .arg(&image) + .lines() + .filter(|l| { + l.trim() + .split_once("=") + .map_or(false, |(v, _)| !SKIP_ENV.contains(&v)) + }) + .join("\n") + + "\n"; + let workdir = Path::new( + String::from_utf8( + Command::new(CONTAINER_TOOL) + .arg("run") + .arg(&docker_platform) + .arg("--rm") + .arg("--entrypoint") + .arg("pwd") + .arg(&image) + .invoke(ErrorKind::Docker) + .await?, + )? + .trim(), + ) + .to_owned(); + let container_id = String::from_utf8( + Command::new(CONTAINER_TOOL) + .arg("create") + .arg(&docker_platform) + .arg(&image) + .invoke(ErrorKind::Docker) + .await?, + )?; + Command::new("bash") + .arg("-c") + .arg(format!( + "{CONTAINER_TOOL} export {container_id} | mksquashfs - {sqfs} -tar", + container_id = container_id.trim(), + sqfs = sqfs_path.display() + )) .invoke(ErrorKind::Docker) - .await?, - )?; - Command::new("bash") - .arg("-c") - .arg(format!( - "{CONTAINER_TOOL} export {container_id} | mksquashfs - {sqfs} -tar", - container_id = container_id.trim(), - sqfs = sqfs_path.display() - )) - .invoke(ErrorKind::Docker) - .await?; - Command::new(CONTAINER_TOOL) - .arg("rm") - .arg(container_id.trim()) - .invoke(ErrorKind::Docker) - .await?; - let mut s9pk = S9pk::from_file(super::load(&ctx, &s9pk_path).await?) - .await? - .into_dyn(); - let archive = s9pk.as_archive_mut(); - archive.set_signer(ctx.developer_key()?.clone()); - archive.contents_mut().insert_path( - Path::new("images") - .join(arch.trim()) - .join(&id) - .with_extension("squashfs"), - Entry::file(DynFileSource::new(sqfs_path)), - )?; - archive.contents_mut().insert_path( - Path::new("images") - .join(arch.trim()) - .join(&id) - .with_extension("env"), - Entry::file(DynFileSource::new(Arc::from(Vec::from(env)))), - )?; - archive.contents_mut().insert_path( - Path::new("images") - .join(arch.trim()) - .join(&id) - .with_extension("json"), - Entry::file(DynFileSource::new(Arc::from( - serde_json::to_vec(&serde_json::json!({ - "workdir": workdir - })) - .with_kind(ErrorKind::Serialization)?, - ))), - )?; + .await?; + Command::new(CONTAINER_TOOL) + .arg("rm") + .arg(container_id.trim()) + .invoke(ErrorKind::Docker) + .await?; + let archive = s9pk.as_archive_mut(); + archive.set_signer(ctx.developer_key()?.clone()); + archive.contents_mut().insert_path( + Path::new("images") + .join(&arch) + .join(&id) + .with_extension("squashfs"), + Entry::file(DynFileSource::new(sqfs_path)), + )?; + archive.contents_mut().insert_path( + Path::new("images") + .join(&arch) + .join(&id) + .with_extension("env"), + Entry::file(DynFileSource::new(Arc::from(Vec::from(env)))), + )?; + archive.contents_mut().insert_path( + Path::new("images") + .join(&arch) + .join(&id) + .with_extension("json"), + Entry::file(DynFileSource::new(Arc::from( + serde_json::to_vec(&serde_json::json!({ + "workdir": workdir + })) + .with_kind(ErrorKind::Serialization)?, + ))), + )?; + } + s9pk.as_manifest_mut().images.insert(id); let tmp_path = s9pk_path.with_extension("s9pk.tmp"); let mut tmp_file = File::create(&tmp_path).await?; s9pk.serialize(&mut tmp_file, true).await?; @@ -193,7 +205,7 @@ async fn edit_manifest( EditManifestParams { expression }: EditManifestParams, S9pkPath { s9pk: s9pk_path }: S9pkPath, ) -> Result { - let mut s9pk = S9pk::from_file(super::load(&ctx, &s9pk_path).await?).await?; + let mut s9pk = S9pk::from_file(super::load(&ctx, &s9pk_path).await?, false).await?; let old = serde_json::to_value(s9pk.as_manifest()).with_kind(ErrorKind::Serialization)?; *s9pk.as_manifest_mut() = serde_json::from_value(apply_expr(old.into(), &expression)?.into()) .with_kind(ErrorKind::Serialization)?; @@ -214,15 +226,45 @@ async fn file_tree( _: Empty, S9pkPath { s9pk }: S9pkPath, ) -> Result, Error> { - let s9pk = S9pk::from_file(super::load(&ctx, &s9pk).await?).await?; + let s9pk = S9pk::from_file(super::load(&ctx, &s9pk).await?, false).await?; Ok(s9pk.as_archive().contents().file_paths("")) } +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +struct CatParams { + file_path: PathBuf, +} +async fn cat( + ctx: CliContext, + CatParams { file_path }: CatParams, + S9pkPath { s9pk }: S9pkPath, +) -> Result<(), Error> { + use crate::s9pk::merkle_archive::source::FileSource; + + let s9pk = S9pk::from_file(super::load(&ctx, &s9pk).await?, false).await?; + tokio::io::copy( + &mut s9pk + .as_archive() + .contents() + .get_path(&file_path) + .or_not_found(&file_path.display())? + .as_file() + .or_not_found(&file_path.display())? + .reader() + .await?, + &mut tokio::io::stdout(), + ) + .await?; + Ok(()) +} + async fn inspect_manifest( ctx: CliContext, _: Empty, S9pkPath { s9pk }: S9pkPath, ) -> Result { - let s9pk = S9pk::from_file(super::load(&ctx, &s9pk).await?).await?; + let s9pk = S9pk::from_file(super::load(&ctx, &s9pk).await?, false).await?; Ok(s9pk.as_manifest().clone()) } diff --git a/core/startos/src/s9pk/v1/docker.rs b/core/startos/src/s9pk/v1/docker.rs index 7f1507703..bbb1f2f09 100644 --- a/core/startos/src/s9pk/v1/docker.rs +++ b/core/startos/src/s9pk/v1/docker.rs @@ -1,4 +1,3 @@ -use std::borrow::Cow; use std::collections::BTreeSet; use std::io::SeekFrom; use std::path::Path; @@ -10,7 +9,7 @@ use tokio::io::{AsyncRead, AsyncSeek, AsyncSeekExt}; use tokio_tar::{Archive, Entry}; use crate::util::io::from_cbor_async_reader; -use crate::{Error, ErrorKind, ARCH}; +use crate::{Error, ErrorKind}; #[derive(Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -26,8 +25,8 @@ pub enum DockerReader { MultiArch(#[pin] Entry>), } impl DockerReader { - pub async fn new(mut rdr: R) -> Result { - let arch = if let Some(multiarch) = tokio_tar::Archive::new(&mut rdr) + pub async fn list_arches(rdr: &mut R) -> Result, Error> { + if let Some(multiarch) = tokio_tar::Archive::new(rdr) .entries()? .try_filter_map(|e| { async move { @@ -43,41 +42,38 @@ impl DockerReader { .await? { let multiarch: DockerMultiArch = from_cbor_async_reader(multiarch).await?; - Some(if multiarch.available.contains(&**ARCH) { - Cow::Borrowed(&**ARCH) - } else { - Cow::Owned(multiarch.default) - }) + Ok(multiarch.available) } else { - None - }; + Err(Error::new( + eyre!("Single arch legacy s9pks not supported"), + ErrorKind::ParseS9pk, + )) + } + } + pub async fn new(mut rdr: R, arch: &str) -> Result { rdr.seek(SeekFrom::Start(0)).await?; - if let Some(arch) = arch { - if let Some(image) = tokio_tar::Archive::new(rdr) - .entries()? - .try_filter_map(|e| { - let arch = arch.clone(); - async move { - Ok(if &*e.path()? == Path::new(&format!("{}.tar", arch)) { - Some(e) - } else { - None - }) - } - .boxed() - }) - .try_next() - .await? - { - Ok(Self::MultiArch(image)) - } else { - Err(Error::new( - eyre!("Docker image section does not contain tarball for architecture"), - ErrorKind::ParseS9pk, - )) - } + if let Some(image) = tokio_tar::Archive::new(rdr) + .entries()? + .try_filter_map(|e| { + let arch = arch.clone(); + async move { + Ok(if &*e.path()? == Path::new(&format!("{}.tar", arch)) { + Some(e) + } else { + None + }) + } + .boxed() + }) + .try_next() + .await? + { + Ok(Self::MultiArch(image)) } else { - Ok(Self::SingleArch(rdr)) + Err(Error::new( + eyre!("Docker image section does not contain tarball for architecture"), + ErrorKind::ParseS9pk, + )) } } } diff --git a/core/startos/src/s9pk/v1/reader.rs b/core/startos/src/s9pk/v1/reader.rs index 82f62e1df..4288ac0b0 100644 --- a/core/startos/src/s9pk/v1/reader.rs +++ b/core/startos/src/s9pk/v1/reader.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeSet; use std::io::SeekFrom; use std::ops::Range; use std::path::Path; @@ -158,8 +159,8 @@ impl S9pkReader { } impl S9pkReader { #[instrument(skip_all)] - pub async fn image_tags(&mut self) -> Result, Error> { - let mut tar = tokio_tar::Archive::new(self.docker_images().await?); + pub async fn image_tags(&mut self, arch: &str) -> Result, Error> { + let mut tar = tokio_tar::Archive::new(self.docker_images(arch).await?); let mut entries = tar.entries()?; while let Some(mut entry) = entries.try_next().await? { if &*entry.path()? != Path::new("manifest.json") { @@ -280,8 +281,15 @@ impl S9pkReader { self.read_handle(self.toc.icon).await } - pub async fn docker_images(&mut self) -> Result>, Error> { - DockerReader::new(self.read_handle(self.toc.docker_images).await?).await + pub async fn docker_arches(&mut self) -> Result, Error> { + DockerReader::list_arches(&mut self.read_handle(self.toc.docker_images).await?).await + } + + pub async fn docker_images( + &mut self, + arch: &str, + ) -> Result>, Error> { + DockerReader::new(self.read_handle(self.toc.docker_images).await?, arch).await } pub async fn assets(&mut self) -> Result, Error> { diff --git a/core/startos/src/s9pk/v2/compat.rs b/core/startos/src/s9pk/v2/compat.rs index b4c2e1c14..f95a7e4bf 100644 --- a/core/startos/src/s9pk/v2/compat.rs +++ b/core/startos/src/s9pk/v2/compat.rs @@ -21,7 +21,6 @@ use crate::s9pk::v1::reader::S9pkReader; use crate::s9pk::v2::S9pk; use crate::util::io::TmpDir; use crate::util::Invoke; -use crate::ARCH; pub const MAGIC_AND_VERSION: &[u8] = &[0x3b, 0x3b, 0x01]; @@ -96,155 +95,166 @@ impl S9pk> { )?; // images - let images_dir = scratch_dir.join("images"); - tokio::fs::create_dir_all(&images_dir).await?; - Command::new(CONTAINER_TOOL) - .arg("load") - .input(Some(&mut reader.docker_images().await?)) - .invoke(ErrorKind::Docker) - .await?; - #[derive(serde::Deserialize)] - #[serde(rename_all = "PascalCase")] - struct DockerImagesOut { - repository: Option, - tag: Option, - #[serde(default)] - names: Vec, - } - for image in { - #[cfg(feature = "docker")] - let images = std::str::from_utf8( - &Command::new(CONTAINER_TOOL) - .arg("images") - .arg("--format=json") - .invoke(ErrorKind::Docker) - .await?, - )? - .lines() - .map(|l| serde_json::from_str::(l)) - .collect::, _>>() - .with_kind(ErrorKind::Deserialization)? - .into_iter(); - #[cfg(not(feature = "docker"))] - let images = serde_json::from_slice::>( - &Command::new(CONTAINER_TOOL) - .arg("images") - .arg("--format=json") - .invoke(ErrorKind::Docker) - .await?, - ) - .with_kind(ErrorKind::Deserialization)? - .into_iter(); - images - } - .flat_map(|i| { - if let (Some(repository), Some(tag)) = (i.repository, i.tag) { - vec![format!("{repository}:{tag}")] + for arch in reader.docker_arches().await? { + let images_dir = scratch_dir.join("images").join(&arch); + let docker_platform = if arch == "x86_64" { + "--platform=linux/amd64".to_owned() + } else if arch == "aarch64" { + "--platform=linux/arm64".to_owned() } else { - i.names - .into_iter() - .filter_map(|i| i.strip_prefix("docker.io/").map(|s| s.to_owned())) - .collect() + format!("--platform=linux/{arch}") + }; + tokio::fs::create_dir_all(&images_dir).await?; + Command::new(CONTAINER_TOOL) + .arg("load") + .input(Some(&mut reader.docker_images(&arch).await?)) + .invoke(ErrorKind::Docker) + .await?; + #[derive(serde::Deserialize)] + #[serde(rename_all = "PascalCase")] + struct DockerImagesOut { + repository: Option, + tag: Option, + #[serde(default)] + names: Vec, } - }) - .filter_map(|i| { - i.strip_suffix(&format!(":{}", manifest.version)) - .map(|s| s.to_owned()) - }) - .filter_map(|i| { - i.strip_prefix(&format!("start9/{}/", manifest.id)) - .map(|s| s.to_owned()) - }) { - new_manifest.images.insert(image.parse()?); - let sqfs_path = images_dir.join(&image).with_extension("squashfs"); - let image_name = format!("start9/{}/{}:{}", manifest.id, image, manifest.version); - let id = String::from_utf8( - Command::new(CONTAINER_TOOL) - .arg("create") - .arg(&image_name) - .invoke(ErrorKind::Docker) - .await?, - )?; - let env = String::from_utf8( - Command::new(CONTAINER_TOOL) - .arg("run") - .arg("--rm") - .arg("--entrypoint") - .arg("env") - .arg(&image_name) - .invoke(ErrorKind::Docker) - .await?, - )? - .lines() - .filter(|l| { - l.trim() - .split_once("=") - .map_or(false, |(v, _)| !SKIP_ENV.contains(&v)) + for image in { + #[cfg(feature = "docker")] + let images = std::str::from_utf8( + &Command::new(CONTAINER_TOOL) + .arg("images") + .arg("--format=json") + .invoke(ErrorKind::Docker) + .await?, + )? + .lines() + .map(|l| serde_json::from_str::(l)) + .collect::, _>>() + .with_kind(ErrorKind::Deserialization)? + .into_iter(); + #[cfg(not(feature = "docker"))] + let images = serde_json::from_slice::>( + &Command::new(CONTAINER_TOOL) + .arg("images") + .arg("--format=json") + .invoke(ErrorKind::Docker) + .await?, + ) + .with_kind(ErrorKind::Deserialization)? + .into_iter(); + images + } + .flat_map(|i| { + if let (Some(repository), Some(tag)) = (i.repository, i.tag) { + vec![format!("{repository}:{tag}")] + } else { + i.names + .into_iter() + .filter_map(|i| i.strip_prefix("docker.io/").map(|s| s.to_owned())) + .collect() + } }) - .join("\n") - + "\n"; - let workdir = Path::new( - String::from_utf8( + .filter_map(|i| { + i.strip_suffix(&format!(":{}", manifest.version)) + .map(|s| s.to_owned()) + }) + .filter_map(|i| { + i.strip_prefix(&format!("start9/{}/", manifest.id)) + .map(|s| s.to_owned()) + }) { + new_manifest.images.insert(image.parse()?); + let sqfs_path = images_dir.join(&image).with_extension("squashfs"); + let image_name = format!("start9/{}/{}:{}", manifest.id, image, manifest.version); + let id = String::from_utf8( + Command::new(CONTAINER_TOOL) + .arg("create") + .arg(&docker_platform) + .arg(&image_name) + .invoke(ErrorKind::Docker) + .await?, + )?; + let env = String::from_utf8( Command::new(CONTAINER_TOOL) .arg("run") .arg("--rm") + .arg(&docker_platform) .arg("--entrypoint") - .arg("pwd") + .arg("env") .arg(&image_name) .invoke(ErrorKind::Docker) .await?, )? - .trim(), - ) - .to_owned(); - Command::new("bash") - .arg("-c") - .arg(format!( - "{CONTAINER_TOOL} export {id} | mksquashfs - {sqfs} -tar", - id = id.trim(), - sqfs = sqfs_path.display() - )) - .invoke(ErrorKind::Docker) - .await?; - Command::new(CONTAINER_TOOL) - .arg("rm") - .arg(id.trim()) - .invoke(ErrorKind::Docker) - .await?; - archive.insert_path( - Path::new("images") - .join(&*ARCH) - .join(&image) - .with_extension("squashfs"), - Entry::file(CompatSource::File(sqfs_path)), - )?; - archive.insert_path( - Path::new("images") - .join(&*ARCH) - .join(&image) - .with_extension("env"), - Entry::file(CompatSource::Buffered(Vec::from(env).into())), - )?; - archive.insert_path( - Path::new("images") - .join(&*ARCH) - .join(&image) - .with_extension("json"), - Entry::file(CompatSource::Buffered( - serde_json::to_vec(&serde_json::json!({ - "workdir": workdir - })) - .with_kind(ErrorKind::Serialization)? - .into(), - )), - )?; + .lines() + .filter(|l| { + l.trim() + .split_once("=") + .map_or(false, |(v, _)| !SKIP_ENV.contains(&v)) + }) + .join("\n") + + "\n"; + let workdir = Path::new( + String::from_utf8( + Command::new(CONTAINER_TOOL) + .arg("run") + .arg("--rm") + .arg(&docker_platform) + .arg("--entrypoint") + .arg("pwd") + .arg(&image_name) + .invoke(ErrorKind::Docker) + .await?, + )? + .trim(), + ) + .to_owned(); + Command::new("bash") + .arg("-c") + .arg(format!( + "{CONTAINER_TOOL} export {id} | mksquashfs - {sqfs} -tar", + id = id.trim(), + sqfs = sqfs_path.display() + )) + .invoke(ErrorKind::Docker) + .await?; + Command::new(CONTAINER_TOOL) + .arg("rm") + .arg(id.trim()) + .invoke(ErrorKind::Docker) + .await?; + archive.insert_path( + Path::new("images") + .join(&arch) + .join(&image) + .with_extension("squashfs"), + Entry::file(CompatSource::File(sqfs_path)), + )?; + archive.insert_path( + Path::new("images") + .join(&arch) + .join(&image) + .with_extension("env"), + Entry::file(CompatSource::Buffered(Vec::from(env).into())), + )?; + archive.insert_path( + Path::new("images") + .join(&arch) + .join(&image) + .with_extension("json"), + Entry::file(CompatSource::Buffered( + serde_json::to_vec(&serde_json::json!({ + "workdir": workdir + })) + .with_kind(ErrorKind::Serialization)? + .into(), + )), + )?; + Command::new(CONTAINER_TOOL) + .arg("rmi") + .arg(&image_name) + .invoke(ErrorKind::Docker) + .await?; + } } - Command::new(CONTAINER_TOOL) - .arg("image") - .arg("prune") - .arg("-af") - .invoke(ErrorKind::Docker) - .await?; // assets let asset_dir = scratch_dir.join("assets"); @@ -312,9 +322,10 @@ impl S9pk> { scratch_dir.delete().await?; - Ok(S9pk::deserialize(&MultiCursorFile::from( - File::open(destination.as_ref()).await?, - )) + Ok(S9pk::deserialize( + &MultiCursorFile::from(File::open(destination.as_ref()).await?), + false, + ) .await?) } } diff --git a/core/startos/src/s9pk/v2/mod.rs b/core/startos/src/s9pk/v2/mod.rs index df82baf7e..1051aaaf8 100644 --- a/core/startos/src/s9pk/v2/mod.rs +++ b/core/startos/src/s9pk/v2/mod.rs @@ -173,7 +173,7 @@ impl S9pk { impl S9pk> { #[instrument(skip_all)] - pub async fn deserialize(source: &S) -> Result { + pub async fn deserialize(source: &S, apply_filter: bool) -> Result { use tokio::io::AsyncReadExt; let mut header = source @@ -193,7 +193,9 @@ impl S9pk> { let mut archive = MerkleArchive::deserialize(source, &mut header).await?; - archive.filter(filter)?; + if apply_filter { + archive.filter(filter)?; + } archive.sort_by(|a, b| match (priority(a), priority(b)) { (Some(a), Some(b)) => a.cmp(&b), @@ -206,11 +208,15 @@ impl S9pk> { } } impl S9pk { - pub async fn from_file(file: File) -> Result { - Self::deserialize(&MultiCursorFile::from(file)).await + pub async fn from_file(file: File, apply_filter: bool) -> Result { + Self::deserialize(&MultiCursorFile::from(file), apply_filter).await } - pub async fn open(path: impl AsRef, id: Option<&PackageId>) -> Result { - let res = Self::from_file(tokio::fs::File::open(path).await?).await?; + pub async fn open( + path: impl AsRef, + id: Option<&PackageId>, + apply_filter: bool, + ) -> Result { + let res = Self::from_file(tokio::fs::File::open(path).await?, apply_filter).await?; if let Some(id) = id { ensure_code!( &res.as_manifest().id == id, diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index e092a59e6..a4e3a4858 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -13,12 +13,14 @@ use start_stop::StartStop; use tokio::sync::Notify; use ts_rs::TS; +use crate::context::{CliContext, RpcContext}; use crate::core::rpc_continuations::RequestGuid; use crate::db::model::package::{ InstalledState, PackageDataEntry, PackageState, PackageStateMatchModelRef, UpdatingState, }; use crate::disk::mount::guard::GenericMountGuard; use crate::install::PKG_ARCHIVE_DIR; +use crate::lxc::ContainerId; use crate::prelude::*; use crate::progress::{NamedProgress, Progress}; use crate::s9pk::S9pk; @@ -31,10 +33,6 @@ use crate::util::actor::concurrent::ConcurrentActor; use crate::util::actor::Actor; use crate::util::serde::Pem; use crate::volume::data_dir; -use crate::{ - context::{CliContext, RpcContext}, - lxc::ContainerId, -}; mod action; pub mod cli; @@ -138,7 +136,7 @@ impl Service { match entry.as_state_info().as_match() { PackageStateMatchModelRef::Installing(_) => { if disposition == LoadDisposition::Retry { - if let Ok(s9pk) = S9pk::open(s9pk_path, Some(id)).await.map_err(|e| { + if let Ok(s9pk) = S9pk::open(s9pk_path, Some(id), true).await.map_err(|e| { tracing::error!("Error opening s9pk for install: {e}"); tracing::debug!("{e:?}") }) { @@ -171,7 +169,7 @@ impl Service { && progress == &Progress::Complete(true) }) { - if let Ok(s9pk) = S9pk::open(&s9pk_path, Some(id)).await.map_err(|e| { + if let Ok(s9pk) = S9pk::open(&s9pk_path, Some(id), true).await.map_err(|e| { tracing::error!("Error opening s9pk for update: {e}"); tracing::debug!("{e:?}") }) { @@ -190,7 +188,7 @@ impl Service { } } } - let s9pk = S9pk::open(s9pk_path, Some(id)).await?; + let s9pk = S9pk::open(s9pk_path, Some(id), true).await?; ctx.db .mutate({ |db| { @@ -215,7 +213,7 @@ impl Service { handle_installed(s9pk, entry).await } PackageStateMatchModelRef::Removing(_) | PackageStateMatchModelRef::Restoring(_) => { - if let Ok(s9pk) = S9pk::open(s9pk_path, Some(id)).await.map_err(|e| { + if let Ok(s9pk) = S9pk::open(s9pk_path, Some(id), true).await.map_err(|e| { tracing::error!("Error opening s9pk for removal: {e}"); tracing::debug!("{e:?}") }) { @@ -243,7 +241,7 @@ impl Service { Ok(None) } PackageStateMatchModelRef::Installed(_) => { - handle_installed(S9pk::open(s9pk_path, Some(id)).await?, entry).await + handle_installed(S9pk::open(s9pk_path, Some(id), true).await?, entry).await } PackageStateMatchModelRef::Error(e) => Err(Error::new( eyre!("Failed to parse PackageDataEntry, found {e:?}"), @@ -349,6 +347,7 @@ impl Service { } Ok(()) } + pub async fn backup(&self, _guard: impl GenericMountGuard) -> Result { // TODO Err(Error::new(eyre!("not yet implemented"), ErrorKind::Unknown)) diff --git a/core/startos/src/service/persistent_container.rs b/core/startos/src/service/persistent_container.rs index d1303b0f9..e69498e5f 100644 --- a/core/startos/src/service/persistent_container.rs +++ b/core/startos/src/service/persistent_container.rs @@ -4,7 +4,7 @@ use std::sync::{Arc, Weak}; use std::time::Duration; use futures::future::ready; -use futures::Future; +use futures::{Future, FutureExt}; use helpers::NonDetachingJoinHandle; use imbl_value::InternedString; use models::{ProcedureName, VolumeId}; @@ -92,6 +92,7 @@ pub struct PersistentContainer { pub(super) overlays: Arc>>, pub(super) state: Arc>, pub(super) net_service: Mutex, + destroyed: bool, } impl PersistentContainer { @@ -217,6 +218,7 @@ impl PersistentContainer { overlays: Arc::new(Mutex::new(BTreeMap::new())), state: Arc::new(watch::channel(ServiceState::new(start)).0), net_service: Mutex::new(net_service), + destroyed: false, }) } @@ -285,7 +287,10 @@ impl PersistentContainer { } #[instrument(skip_all)] - fn destroy(&mut self) -> impl Future> + 'static { + fn destroy(&mut self) -> Option> + 'static> { + if self.destroyed { + return None; + } let rpc_client = self.rpc_client.clone(); let rpc_server = self.rpc_server.send_replace(None); let js_mount = self.js_mount.take(); @@ -293,33 +298,45 @@ impl PersistentContainer { let assets = std::mem::take(&mut self.assets); let overlays = self.overlays.clone(); let lxc_container = self.lxc_container.take(); - async move { - let mut errs = ErrorCollection::new(); - if let Some((hdl, shutdown)) = rpc_server { - errs.handle(rpc_client.request(rpc::Exit, Empty {}).await); - shutdown.shutdown(); - errs.handle(hdl.await.with_kind(ErrorKind::Cancelled)); + self.destroyed = true; + Some( + async move { + dbg!( + async move { + let mut errs = ErrorCollection::new(); + if let Some((hdl, shutdown)) = rpc_server { + errs.handle(rpc_client.request(rpc::Exit, Empty {}).await); + shutdown.shutdown(); + errs.handle(hdl.await.with_kind(ErrorKind::Cancelled)); + } + for (_, volume) in volumes { + errs.handle(volume.unmount(true).await); + } + for (_, assets) in assets { + errs.handle(assets.unmount(true).await); + } + for (_, overlay) in std::mem::take(&mut *overlays.lock().await) { + errs.handle(overlay.unmount(true).await); + } + errs.handle(js_mount.unmount(true).await); + if let Some(lxc_container) = lxc_container { + errs.handle(lxc_container.exit().await); + } + dbg!(errs.into_result()) + } + .await + ) } - for (_, volume) in volumes { - errs.handle(volume.unmount(true).await); - } - for (_, assets) in assets { - errs.handle(assets.unmount(true).await); - } - for (_, overlay) in std::mem::take(&mut *overlays.lock().await) { - errs.handle(overlay.unmount(true).await); - } - errs.handle(js_mount.unmount(true).await); - if let Some(lxc_container) = lxc_container { - errs.handle(lxc_container.exit().await); - } - errs.into_result() - } + .map(|a| dbg!(a)), + ) } #[instrument(skip_all)] pub async fn exit(mut self) -> Result<(), Error> { - self.destroy().await?; + if let Some(destroy) = self.destroy() { + dbg!(destroy.await)?; + } + tracing::info!("Service for {} exited", self.s9pk.as_manifest().id); Ok(()) } @@ -416,7 +433,8 @@ impl PersistentContainer { impl Drop for PersistentContainer { fn drop(&mut self) { - let destroy = self.destroy(); - tokio::spawn(async move { destroy.await.unwrap() }); + if let Some(destroy) = self.destroy() { + tokio::spawn(async move { destroy.await.unwrap() }); + } } } diff --git a/core/startos/src/service/service_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs index 6d0f87c53..b1411450c 100644 --- a/core/startos/src/service/service_effect_handler.rs +++ b/core/startos/src/service/service_effect_handler.rs @@ -1165,6 +1165,7 @@ async fn set_dependencies( .join(&format!("package/v2/{}.s9pk?spec={}", dep_id, version_spec))?, ) .await?, + true, ) .await?; diff --git a/core/startos/src/service/service_map.rs b/core/startos/src/service/service_map.rs index 4386b39b7..891741754 100644 --- a/core/startos/src/service/service_map.rs +++ b/core/startos/src/service/service_map.rs @@ -42,10 +42,9 @@ pub struct InstallProgressHandles { pub struct ServiceMap(Mutex>>>>); impl ServiceMap { async fn entry(&self, id: &PackageId) -> Arc>> { - self.0 - .lock() - .await - .entry(id.clone()) + let mut lock = self.0.lock().await; + dbg!(lock.keys().collect::>()); + lock.entry(id.clone()) .or_insert_with(|| Arc::new(RwLock::new(None))) .clone() } @@ -230,7 +229,7 @@ impl ServiceMap { .await?; Ok(reload_guard .handle_last(async move { - let s9pk = S9pk::open(&installed_path, Some(&id)).await?; + let s9pk = S9pk::open(&installed_path, Some(&id), true).await?; let prev = if let Some(service) = service.take() { ensure_code!( recovery_source.is_none(), @@ -293,9 +292,14 @@ impl ServiceMap { /// This is ran during the cleanup, so when we are uninstalling the service #[instrument(skip_all)] pub async fn uninstall(&self, ctx: &RpcContext, id: &PackageId) -> Result<(), Error> { - if let Some(service) = self.get_mut(id).await.take() { + let mut guard = self.get_mut(id).await; + if let Some(service) = guard.take() { ServiceReloadGuard::new(ctx.clone(), id.clone(), "Uninstall") - .handle_last(service.uninstall(None)) + .handle_last(async move { + let res = service.uninstall(None).await; + drop(guard); + res + }) .await?; } Ok(()) diff --git a/core/startos/src/util/actor/concurrent.rs b/core/startos/src/util/actor/concurrent.rs index d330102ca..e32470d80 100644 --- a/core/startos/src/util/actor/concurrent.rs +++ b/core/startos/src/util/actor/concurrent.rs @@ -96,7 +96,7 @@ impl Future for ConcurrentRunner { cont } {} let _ = this.bg_runner.as_mut().poll(cx); - if this.waiting.is_empty() && this.handlers.is_empty() && this.bg_runner.is_empty() { + if this.waiting.is_empty() && this.handlers.is_empty() && this.recv.is_closed() { std::task::Poll::Ready(()) } else { std::task::Poll::Pending diff --git a/core/startos/src/util/mod.rs b/core/startos/src/util/mod.rs index 772a64a32..63cbd323a 100644 --- a/core/startos/src/util/mod.rs +++ b/core/startos/src/util/mod.rs @@ -498,7 +498,7 @@ impl<'a, T> From<&'a T> for MaybeOwned<'a, T> { pub fn new_guid() -> InternedString { use rand::RngCore; - let mut buf = [0; 40]; + let mut buf = [0; 20]; rand::thread_rng().fill_bytes(&mut buf); InternedString::intern(base32::encode( base32::Alphabet::RFC4648 { padding: false }, diff --git a/debian/postinst b/debian/postinst index 731298af9..e26f7cfee 100755 --- a/debian/postinst +++ b/debian/postinst @@ -6,7 +6,7 @@ if [ -n "$DPKG_MAINTSCRIPT_PACKAGE" ]; then SYSTEMCTL=deb-systemd-helper fi -if [ -f /usr/sbin/grub-probe ]; then +if [ -f /usr/sbin/grub-probe ] && ! [ -L /usr/sbin/grub-probe ]; then mv /usr/sbin/grub-probe /usr/sbin/grub-probe-default ln -s /usr/lib/startos/scripts/grub-probe-eos /usr/sbin/grub-probe fi @@ -84,7 +84,9 @@ if cat /usr/lib/startos/ENVIRONMENT.txt | grep '(^|-)docker(-|$)'; then mkdir -p /etc/docker echo '{ "storage-driver": "overlay2" }' > /etc/docker/daemon.json else - podman network create -d bridge --subnet 172.18.0.1/24 --opt com.docker.network.bridge.name=br-start9 start9 + if ! podman network ls | grep start9; then + podman network create -d bridge --subnet 172.18.0.1/24 --opt com.docker.network.bridge.name=br-start9 start9 + fi fi mkdir -p /etc/nginx/ssl @@ -110,12 +112,14 @@ echo "fs.inotify.max_user_watches=1048576" > /etc/sysctl.d/97-embassy.conf locale-gen en_GB en_GB.UTF-8 echo "locales locales/locales_to_be_generated multiselect en_GB.UTF-8 UTF-8" | debconf-set-selections update-locale LANGUAGE -rm "/etc/locale.gen" +rm -f "/etc/locale.gen" dpkg-reconfigure --frontend noninteractive locales -groupadd embassy +if ! getent group | grep '^embassy:'; then + groupadd embassy +fi -ln -s /usr/lib/startos/scripts/dhclient-exit-hook /etc/dhcp/dhclient-exit-hooks.d/embassy +ln -sf /usr/lib/startos/scripts/dhclient-exit-hook /etc/dhcp/dhclient-exit-hooks.d/embassy rm -f /etc/motd ln -sf /usr/lib/startos/motd /etc/update-motd.d/00-embassy @@ -123,7 +127,15 @@ chmod -x /etc/update-motd.d/* chmod +x /etc/update-motd.d/00-embassy # LXC -echo "root:100000:65536" >>/etc/subuid -echo "root:100000:65536" >>/etc/subgid -echo "lxc.idmap = u 0 100000 65536" >>/etc/lxc/default.conf -echo "lxc.idmap = g 0 100000 65536" >>/etc/lxc/default.conf +cat /etc/subuid | grep -v '^root:' > /etc/subuid.tmp || true +echo "root:100000:65536" >> /etc/subuid.tmp +mv /etc/subuid.tmp /etc/subuid + +cat /etc/subgid | grep -v '^root:' > /etc/subgid.tmp || true +echo "root:100000:65536" >> /etc/subgid.tmp +mv /etc/subgid.tmp /etc/subgid + +cat /etc/lxc/default.conf | grep -v '^lxc\.idmap = [ug]' > /etc/lxc/default.conf.tmp || true +echo "lxc.idmap = u 0 100000 65536" >> /etc/lxc/default.conf.tmp +echo "lxc.idmap = g 0 100000 65536" >> /etc/lxc/default.conf.tmp +mv /etc/lxc/default.conf.tmp /etc/lxc/default.conf \ No newline at end of file diff --git a/sdk/Makefile b/sdk/Makefile index 8370650b8..091f4c7bb 100644 --- a/sdk/Makefile +++ b/sdk/Makefile @@ -1,19 +1,23 @@ -TS_FILES := $(shell find ./**/*.ts ) +TS_FILES := $(shell git ls-files lib) lib/test/output.ts version = $(shell git tag --sort=committerdate | tail -1) + +.PHONY: test clean bundle fmt buildOutput check + test: $(TS_FILES) lib/test/output.ts npm test clean: - rm -rf dist/* | true + rm -rf dist + rm -f lib/test/output.ts + rm -rf node_modules -lib/test/output.ts: lib/test/makeOutput.ts scripts/oldSpecToBuilder.ts +lib/test/output.ts: node_modules lib/test/makeOutput.ts scripts/oldSpecToBuilder.ts npm run buildOutput -buildOutput: lib/test/output.ts fmt - echo 'done' +bundle: dist | test fmt + touch dist - -bundle: $(TS_FILES) package.json .FORCE node_modules test fmt +dist: $(TS_FILES) package.json node_modules README.md LICENSE npx tsc npx tsc --project tsconfig-cjs.json cp package.json dist/package.json @@ -21,9 +25,7 @@ bundle: $(TS_FILES) package.json .FORCE node_modules test fmt cp LICENSE dist/LICENSE touch dist -full-bundle: - make clean - make bundle +full-bundle: clean bundle check: npm run check @@ -32,13 +34,10 @@ fmt: node_modules npx prettier --write "**/*.ts" node_modules: package.json - npm install + npm ci -publish: clean bundle package.json README.md LICENSE +publish: clean bundle package.json README.md LICENSE cd dist && npm publish --access=public -link: bundle - cp package.json dist/package.json - cp README.md dist/README.md - cp LICENSE dist/LICENSE + +link: bundle cd dist && npm link -.FORCE: diff --git a/sdk/lib/interfaces/Host.ts b/sdk/lib/interfaces/Host.ts index de54f7dc0..ab64cd7b0 100644 --- a/sdk/lib/interfaces/Host.ts +++ b/sdk/lib/interfaces/Host.ts @@ -1,10 +1,10 @@ import { object, string } from "ts-matches" import { Effects } from "../types" import { Origin } from "./Origin" -import { AddSslOptions } from "../../../core/startos/bindings/AddSslOptions" -import { Security } from "../../../core/startos/bindings/Security" -import { BindOptions } from "../../../core/startos/bindings/BindOptions" -import { AlpnInfo } from "../../../core/startos/bindings/AlpnInfo" +import { AddSslOptions } from ".././osBindings" +import { Security } from ".././osBindings" +import { BindOptions } from ".././osBindings" +import { AlpnInfo } from ".././osBindings" export { AddSslOptions, Security, BindOptions } diff --git a/core/startos/bindings/ActionId.ts b/sdk/lib/osBindings/ActionId.ts similarity index 78% rename from core/startos/bindings/ActionId.ts rename to sdk/lib/osBindings/ActionId.ts index 9bb1f9990..ebfaba49e 100644 --- a/core/startos/bindings/ActionId.ts +++ b/sdk/lib/osBindings/ActionId.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type ActionId = string; +export type ActionId = string diff --git a/sdk/lib/osBindings/ActionMetadata.ts b/sdk/lib/osBindings/ActionMetadata.ts new file mode 100644 index 000000000..b103b82b0 --- /dev/null +++ b/sdk/lib/osBindings/ActionMetadata.ts @@ -0,0 +1,12 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AllowedStatuses } from "./AllowedStatuses" + +export type ActionMetadata = { + name: string + description: string + warning: string | null + input: any + disabled: boolean + allowedStatuses: AllowedStatuses + group: string | null +} diff --git a/core/startos/bindings/AddSslOptions.ts b/sdk/lib/osBindings/AddSslOptions.ts similarity index 53% rename from core/startos/bindings/AddSslOptions.ts rename to sdk/lib/osBindings/AddSslOptions.ts index 984a1a7c6..daa9aee0e 100644 --- a/core/startos/bindings/AddSslOptions.ts +++ b/sdk/lib/osBindings/AddSslOptions.ts @@ -1,8 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AlpnInfo } from "./AlpnInfo"; +import type { AlpnInfo } from "./AlpnInfo" export type AddSslOptions = { - scheme: string | null; - preferredExternalPort: number; - alpn: AlpnInfo; -}; + scheme: string | null + preferredExternalPort: number + alpn: AlpnInfo +} diff --git a/sdk/lib/osBindings/AddressInfo.ts b/sdk/lib/osBindings/AddressInfo.ts new file mode 100644 index 000000000..818b570bb --- /dev/null +++ b/sdk/lib/osBindings/AddressInfo.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { BindOptions } from "./BindOptions" +import type { HostId } from "./HostId" + +export type AddressInfo = { + username: string | null + hostId: HostId + bindOptions: BindOptions + suffix: string +} diff --git a/core/startos/bindings/Alerts.ts b/sdk/lib/osBindings/Alerts.ts similarity index 50% rename from core/startos/bindings/Alerts.ts rename to sdk/lib/osBindings/Alerts.ts index c6a671e83..819d1c407 100644 --- a/core/startos/bindings/Alerts.ts +++ b/sdk/lib/osBindings/Alerts.ts @@ -1,9 +1,9 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. export type Alerts = { - install: string | null; - uninstall: string | null; - restore: string | null; - start: string | null; - stop: string | null; -}; + install: string | null + uninstall: string | null + restore: string | null + start: string | null + stop: string | null +} diff --git a/core/startos/bindings/Algorithm.ts b/sdk/lib/osBindings/Algorithm.ts similarity index 70% rename from core/startos/bindings/Algorithm.ts rename to sdk/lib/osBindings/Algorithm.ts index 877325fdf..94f2040e1 100644 --- a/core/startos/bindings/Algorithm.ts +++ b/sdk/lib/osBindings/Algorithm.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Algorithm = "ecdsa" | "ed25519"; +export type Algorithm = "ecdsa" | "ed25519" diff --git a/core/startos/bindings/AllPackageData.ts b/sdk/lib/osBindings/AllPackageData.ts similarity index 61% rename from core/startos/bindings/AllPackageData.ts rename to sdk/lib/osBindings/AllPackageData.ts index c7698d10d..b51b41bf5 100644 --- a/core/startos/bindings/AllPackageData.ts +++ b/sdk/lib/osBindings/AllPackageData.ts @@ -1,5 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PackageDataEntry } from "./PackageDataEntry"; -import type { PackageId } from "./PackageId"; +import type { PackageDataEntry } from "./PackageDataEntry" +import type { PackageId } from "./PackageId" -export type AllPackageData = { [key: PackageId]: PackageDataEntry }; +export type AllPackageData = { [key: PackageId]: PackageDataEntry } diff --git a/core/startos/bindings/AllowedStatuses.ts b/sdk/lib/osBindings/AllowedStatuses.ts similarity index 97% rename from core/startos/bindings/AllowedStatuses.ts rename to sdk/lib/osBindings/AllowedStatuses.ts index a04ee2bac..960187fd9 100644 --- a/core/startos/bindings/AllowedStatuses.ts +++ b/sdk/lib/osBindings/AllowedStatuses.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type AllowedStatuses = "onlyRunning" | "onlyStopped" | "any"; +export type AllowedStatuses = "onlyRunning" | "onlyStopped" | "any" diff --git a/core/startos/bindings/AlpnInfo.ts b/sdk/lib/osBindings/AlpnInfo.ts similarity index 71% rename from core/startos/bindings/AlpnInfo.ts rename to sdk/lib/osBindings/AlpnInfo.ts index eac072fc0..2dacb3d3f 100644 --- a/core/startos/bindings/AlpnInfo.ts +++ b/sdk/lib/osBindings/AlpnInfo.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { MaybeUtf8String } from "./MaybeUtf8String"; +import type { MaybeUtf8String } from "./MaybeUtf8String" -export type AlpnInfo = "reflect" | { specified: Array }; +export type AlpnInfo = "reflect" | { specified: Array } diff --git a/core/startos/bindings/DependencyKind.ts b/sdk/lib/osBindings/BackupProgress.ts similarity index 68% rename from core/startos/bindings/DependencyKind.ts rename to sdk/lib/osBindings/BackupProgress.ts index 71fd49762..407a4195a 100644 --- a/core/startos/bindings/DependencyKind.ts +++ b/sdk/lib/osBindings/BackupProgress.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type DependencyKind = "exists" | "running"; +export type BackupProgress = { complete: boolean } diff --git a/core/startos/bindings/BindInfo.ts b/sdk/lib/osBindings/BindInfo.ts similarity index 72% rename from core/startos/bindings/BindInfo.ts rename to sdk/lib/osBindings/BindInfo.ts index 8bda6d37e..d7b37a70f 100644 --- a/core/startos/bindings/BindInfo.ts +++ b/sdk/lib/osBindings/BindInfo.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { BindOptions } from "./BindOptions"; +import type { BindOptions } from "./BindOptions" -export type BindInfo = { options: BindOptions; assignedLanPort: number | null }; +export type BindInfo = { options: BindOptions; assignedLanPort: number | null } diff --git a/sdk/lib/osBindings/BindOptions.ts b/sdk/lib/osBindings/BindOptions.ts new file mode 100644 index 000000000..a462a8cfc --- /dev/null +++ b/sdk/lib/osBindings/BindOptions.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AddSslOptions } from "./AddSslOptions" +import type { Security } from "./Security" + +export type BindOptions = { + scheme: string | null + preferredExternalPort: number + addSsl: AddSslOptions | null + secure: Security | null +} diff --git a/sdk/lib/osBindings/BindParams.ts b/sdk/lib/osBindings/BindParams.ts new file mode 100644 index 000000000..544f65a40 --- /dev/null +++ b/sdk/lib/osBindings/BindParams.ts @@ -0,0 +1,15 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AddSslOptions } from "./AddSslOptions" +import type { HostId } from "./HostId" +import type { HostKind } from "./HostKind" +import type { Security } from "./Security" + +export type BindParams = { + kind: HostKind + id: HostId + internalPort: number + scheme: string | null + preferredExternalPort: number + addSsl: AddSslOptions | null + secure: Security | null +} diff --git a/core/startos/bindings/Callback.ts b/sdk/lib/osBindings/Callback.ts similarity index 75% rename from core/startos/bindings/Callback.ts rename to sdk/lib/osBindings/Callback.ts index e6c5b004f..1e5cb1af5 100644 --- a/core/startos/bindings/Callback.ts +++ b/sdk/lib/osBindings/Callback.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Callback = () => void; +export type Callback = () => void diff --git a/core/startos/bindings/ChrootParams.ts b/sdk/lib/osBindings/ChrootParams.ts similarity index 52% rename from core/startos/bindings/ChrootParams.ts rename to sdk/lib/osBindings/ChrootParams.ts index 9ee6e8959..19131b224 100644 --- a/core/startos/bindings/ChrootParams.ts +++ b/sdk/lib/osBindings/ChrootParams.ts @@ -1,10 +1,10 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. export type ChrootParams = { - env: string | null; - workdir: string | null; - user: string | null; - path: string; - command: string; - args: string[]; -}; + env: string | null + workdir: string | null + user: string | null + path: string + command: string + args: string[] +} diff --git a/sdk/lib/osBindings/CreateOverlayedImageParams.ts b/sdk/lib/osBindings/CreateOverlayedImageParams.ts new file mode 100644 index 000000000..b8579ac7d --- /dev/null +++ b/sdk/lib/osBindings/CreateOverlayedImageParams.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type CreateOverlayedImageParams = { imageId: string } diff --git a/core/startos/bindings/CurrentDependencies.ts b/sdk/lib/osBindings/CurrentDependencies.ts similarity index 78% rename from core/startos/bindings/CurrentDependencies.ts rename to sdk/lib/osBindings/CurrentDependencies.ts index 75645e200..029a2f018 100644 --- a/core/startos/bindings/CurrentDependencies.ts +++ b/sdk/lib/osBindings/CurrentDependencies.ts @@ -1,5 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CurrentDependencyInfo } from "./CurrentDependencyInfo"; -import type { PackageId } from "./PackageId"; +import type { CurrentDependencyInfo } from "./CurrentDependencyInfo" +import type { PackageId } from "./PackageId" -export type CurrentDependencies = { [key: PackageId]: CurrentDependencyInfo }; +export type CurrentDependencies = { [key: PackageId]: CurrentDependencyInfo } diff --git a/core/startos/bindings/CurrentDependencyInfo.ts b/sdk/lib/osBindings/CurrentDependencyInfo.ts similarity index 57% rename from core/startos/bindings/CurrentDependencyInfo.ts rename to sdk/lib/osBindings/CurrentDependencyInfo.ts index 8b2b0100d..de46e4b52 100644 --- a/core/startos/bindings/CurrentDependencyInfo.ts +++ b/sdk/lib/osBindings/CurrentDependencyInfo.ts @@ -1,10 +1,10 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DataUrl } from "./DataUrl"; +import type { DataUrl } from "./DataUrl" export type CurrentDependencyInfo = { - title: string; - icon: DataUrl; - registryUrl: string; - versionSpec: string; - configSatisfied: boolean; -} & ({ kind: "exists" } | { kind: "running"; healthChecks: string[] }); + title: string + icon: DataUrl + registryUrl: string + versionSpec: string + configSatisfied: boolean +} & ({ kind: "exists" } | { kind: "running"; healthChecks: string[] }) diff --git a/core/startos/bindings/DataUrl.ts b/sdk/lib/osBindings/DataUrl.ts similarity index 78% rename from core/startos/bindings/DataUrl.ts rename to sdk/lib/osBindings/DataUrl.ts index 65fa15059..cf79cb4a4 100644 --- a/core/startos/bindings/DataUrl.ts +++ b/sdk/lib/osBindings/DataUrl.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type DataUrl = string; +export type DataUrl = string diff --git a/core/startos/bindings/DepInfo.ts b/sdk/lib/osBindings/DepInfo.ts similarity index 95% rename from core/startos/bindings/DepInfo.ts rename to sdk/lib/osBindings/DepInfo.ts index 3c01e0939..20b9eb4cd 100644 --- a/core/startos/bindings/DepInfo.ts +++ b/sdk/lib/osBindings/DepInfo.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type DepInfo = { description: string | null; optional: boolean }; +export type DepInfo = { description: string | null; optional: boolean } diff --git a/sdk/lib/osBindings/Dependencies.ts b/sdk/lib/osBindings/Dependencies.ts new file mode 100644 index 000000000..ad4c9b745 --- /dev/null +++ b/sdk/lib/osBindings/Dependencies.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DepInfo } from "./DepInfo" +import type { PackageId } from "./PackageId" + +export type Dependencies = { [key: PackageId]: DepInfo } diff --git a/sdk/lib/osBindings/DependencyKind.ts b/sdk/lib/osBindings/DependencyKind.ts new file mode 100644 index 000000000..e7021ba16 --- /dev/null +++ b/sdk/lib/osBindings/DependencyKind.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type DependencyKind = "exists" | "running" diff --git a/core/startos/bindings/DependencyRequirement.ts b/sdk/lib/osBindings/DependencyRequirement.ts similarity index 61% rename from core/startos/bindings/DependencyRequirement.ts rename to sdk/lib/osBindings/DependencyRequirement.ts index e6224ce48..d0415bee9 100644 --- a/core/startos/bindings/DependencyRequirement.ts +++ b/sdk/lib/osBindings/DependencyRequirement.ts @@ -2,10 +2,10 @@ export type DependencyRequirement = | { - kind: "running"; - id: string; - healthChecks: string[]; - versionSpec: string; - registryUrl: string; + kind: "running" + id: string + healthChecks: string[] + versionSpec: string + registryUrl: string } - | { kind: "exists"; id: string; versionSpec: string; registryUrl: string }; + | { kind: "exists"; id: string; versionSpec: string; registryUrl: string } diff --git a/sdk/lib/osBindings/Description.ts b/sdk/lib/osBindings/Description.ts new file mode 100644 index 000000000..bcb92071f --- /dev/null +++ b/sdk/lib/osBindings/Description.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Description = { short: string; long: string } diff --git a/core/startos/bindings/Description.ts b/sdk/lib/osBindings/DestroyOverlayedImageParams.ts similarity index 65% rename from core/startos/bindings/Description.ts rename to sdk/lib/osBindings/DestroyOverlayedImageParams.ts index 918bd09c5..82fc1cc67 100644 --- a/core/startos/bindings/Description.ts +++ b/sdk/lib/osBindings/DestroyOverlayedImageParams.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Description = { short: string; long: string }; +export type DestroyOverlayedImageParams = { guid: string } diff --git a/core/startos/bindings/Duration.ts b/sdk/lib/osBindings/Duration.ts similarity index 78% rename from core/startos/bindings/Duration.ts rename to sdk/lib/osBindings/Duration.ts index 889c729b8..b44758a28 100644 --- a/core/startos/bindings/Duration.ts +++ b/sdk/lib/osBindings/Duration.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Duration = string; +export type Duration = string diff --git a/sdk/lib/osBindings/EncryptedWire.ts b/sdk/lib/osBindings/EncryptedWire.ts new file mode 100644 index 000000000..16a2f95b8 --- /dev/null +++ b/sdk/lib/osBindings/EncryptedWire.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type EncryptedWire = { encrypted: any } diff --git a/core/startos/bindings/ExecuteAction.ts b/sdk/lib/osBindings/ExecuteAction.ts similarity index 68% rename from core/startos/bindings/ExecuteAction.ts rename to sdk/lib/osBindings/ExecuteAction.ts index aaa340747..abd8c151f 100644 --- a/core/startos/bindings/ExecuteAction.ts +++ b/sdk/lib/osBindings/ExecuteAction.ts @@ -1,7 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. export type ExecuteAction = { - serviceId: string | null; - actionId: string; - input: any; -}; + serviceId: string | null + actionId: string + input: any +} diff --git a/core/startos/bindings/ExportActionParams.ts b/sdk/lib/osBindings/ExportActionParams.ts similarity index 72% rename from core/startos/bindings/ExportActionParams.ts rename to sdk/lib/osBindings/ExportActionParams.ts index 4961c4a11..5eee8fc63 100644 --- a/core/startos/bindings/ExportActionParams.ts +++ b/sdk/lib/osBindings/ExportActionParams.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ActionMetadata } from "./ActionMetadata"; +import type { ActionMetadata } from "./ActionMetadata" -export type ExportActionParams = { id: string; metadata: ActionMetadata }; +export type ExportActionParams = { id: string; metadata: ActionMetadata } diff --git a/sdk/lib/osBindings/ExportServiceInterfaceParams.ts b/sdk/lib/osBindings/ExportServiceInterfaceParams.ts new file mode 100644 index 000000000..152a800bb --- /dev/null +++ b/sdk/lib/osBindings/ExportServiceInterfaceParams.ts @@ -0,0 +1,14 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AddressInfo } from "./AddressInfo" +import type { ServiceInterfaceType } from "./ServiceInterfaceType" + +export type ExportServiceInterfaceParams = { + id: string + name: string + description: string + hasPrimary: boolean + disabled: boolean + masked: boolean + addressInfo: AddressInfo + type: ServiceInterfaceType +} diff --git a/core/startos/bindings/ExportedHostInfo.ts b/sdk/lib/osBindings/ExportedHostInfo.ts similarity index 55% rename from core/startos/bindings/ExportedHostInfo.ts rename to sdk/lib/osBindings/ExportedHostInfo.ts index d8339a074..5e8a25ad6 100644 --- a/core/startos/bindings/ExportedHostInfo.ts +++ b/sdk/lib/osBindings/ExportedHostInfo.ts @@ -1,10 +1,10 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExportedHostnameInfo } from "./ExportedHostnameInfo"; -import type { HostId } from "./HostId"; -import type { HostKind } from "./HostKind"; +import type { ExportedHostnameInfo } from "./ExportedHostnameInfo" +import type { HostId } from "./HostId" +import type { HostKind } from "./HostKind" export type ExportedHostInfo = { - id: HostId; - kind: HostKind; - hostnames: Array; -}; + id: HostId + kind: HostKind + hostnames: Array +} diff --git a/sdk/lib/osBindings/ExportedHostnameInfo.ts b/sdk/lib/osBindings/ExportedHostnameInfo.ts new file mode 100644 index 000000000..42f849529 --- /dev/null +++ b/sdk/lib/osBindings/ExportedHostnameInfo.ts @@ -0,0 +1,12 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ExportedIpHostname } from "./ExportedIpHostname" +import type { ExportedOnionHostname } from "./ExportedOnionHostname" + +export type ExportedHostnameInfo = + | { + kind: "ip" + networkInterfaceId: string + public: boolean + hostname: ExportedIpHostname + } + | { kind: "onion"; hostname: ExportedOnionHostname } diff --git a/core/startos/bindings/ExportedIpHostname.ts b/sdk/lib/osBindings/ExportedIpHostname.ts similarity index 57% rename from core/startos/bindings/ExportedIpHostname.ts rename to sdk/lib/osBindings/ExportedIpHostname.ts index 1e02ab2be..ea06fe05b 100644 --- a/core/startos/bindings/ExportedIpHostname.ts +++ b/sdk/lib/osBindings/ExportedIpHostname.ts @@ -4,15 +4,15 @@ export type ExportedIpHostname = | { kind: "ipv4"; value: string; port: number | null; sslPort: number | null } | { kind: "ipv6"; value: string; port: number | null; sslPort: number | null } | { - kind: "local"; - value: string; - port: number | null; - sslPort: number | null; + kind: "local" + value: string + port: number | null + sslPort: number | null } | { - kind: "domain"; - domain: string; - subdomain: string | null; - port: number | null; - sslPort: number | null; - }; + kind: "domain" + domain: string + subdomain: string | null + port: number | null + sslPort: number | null + } diff --git a/core/startos/bindings/ExportedOnionHostname.ts b/sdk/lib/osBindings/ExportedOnionHostname.ts similarity index 68% rename from core/startos/bindings/ExportedOnionHostname.ts rename to sdk/lib/osBindings/ExportedOnionHostname.ts index af072289f..2c7d67cec 100644 --- a/core/startos/bindings/ExportedOnionHostname.ts +++ b/sdk/lib/osBindings/ExportedOnionHostname.ts @@ -1,7 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. export type ExportedOnionHostname = { - value: string; - port: number | null; - sslPort: number | null; -}; + value: string + port: number | null + sslPort: number | null +} diff --git a/sdk/lib/osBindings/ExposeForDependentsParams.ts b/sdk/lib/osBindings/ExposeForDependentsParams.ts new file mode 100644 index 000000000..5b55368b9 --- /dev/null +++ b/sdk/lib/osBindings/ExposeForDependentsParams.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ExposeForDependentsParams = { paths: string[] } diff --git a/core/startos/bindings/FullProgress.ts b/sdk/lib/osBindings/FullProgress.ts similarity index 60% rename from core/startos/bindings/FullProgress.ts rename to sdk/lib/osBindings/FullProgress.ts index 56f6a1a15..8961ea0e7 100644 --- a/core/startos/bindings/FullProgress.ts +++ b/sdk/lib/osBindings/FullProgress.ts @@ -1,5 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { NamedProgress } from "./NamedProgress"; -import type { Progress } from "./Progress"; +import type { NamedProgress } from "./NamedProgress" +import type { Progress } from "./Progress" -export type FullProgress = { overall: Progress; phases: Array }; +export type FullProgress = { overall: Progress; phases: Array } diff --git a/core/startos/bindings/GetHostInfoParams.ts b/sdk/lib/osBindings/GetHostInfoParams.ts similarity index 54% rename from core/startos/bindings/GetHostInfoParams.ts rename to sdk/lib/osBindings/GetHostInfoParams.ts index 7c2b1b6b6..1ffb95de0 100644 --- a/core/startos/bindings/GetHostInfoParams.ts +++ b/sdk/lib/osBindings/GetHostInfoParams.ts @@ -1,10 +1,10 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Callback } from "./Callback"; -import type { GetHostInfoParamsKind } from "./GetHostInfoParamsKind"; +import type { Callback } from "./Callback" +import type { GetHostInfoParamsKind } from "./GetHostInfoParamsKind" export type GetHostInfoParams = { - kind: GetHostInfoParamsKind | null; - serviceInterfaceId: string; - packageId: string | null; - callback: Callback; -}; + kind: GetHostInfoParamsKind | null + serviceInterfaceId: string + packageId: string | null + callback: Callback +} diff --git a/core/startos/bindings/GetHostInfoParamsKind.ts b/sdk/lib/osBindings/GetHostInfoParamsKind.ts similarity index 70% rename from core/startos/bindings/GetHostInfoParamsKind.ts rename to sdk/lib/osBindings/GetHostInfoParamsKind.ts index 6353044ce..482eb7177 100644 --- a/core/startos/bindings/GetHostInfoParamsKind.ts +++ b/sdk/lib/osBindings/GetHostInfoParamsKind.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type GetHostInfoParamsKind = "multi"; +export type GetHostInfoParamsKind = "multi" diff --git a/core/startos/bindings/GetPrimaryUrlParams.ts b/sdk/lib/osBindings/GetPrimaryUrlParams.ts similarity index 53% rename from core/startos/bindings/GetPrimaryUrlParams.ts rename to sdk/lib/osBindings/GetPrimaryUrlParams.ts index c5aaa0ebe..d9394f4ce 100644 --- a/core/startos/bindings/GetPrimaryUrlParams.ts +++ b/sdk/lib/osBindings/GetPrimaryUrlParams.ts @@ -1,8 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Callback } from "./Callback"; +import type { Callback } from "./Callback" export type GetPrimaryUrlParams = { - packageId: string | null; - serviceInterfaceId: string; - callback: Callback; -}; + packageId: string | null + serviceInterfaceId: string + callback: Callback +} diff --git a/core/startos/bindings/GetServiceInterfaceParams.ts b/sdk/lib/osBindings/GetServiceInterfaceParams.ts similarity index 54% rename from core/startos/bindings/GetServiceInterfaceParams.ts rename to sdk/lib/osBindings/GetServiceInterfaceParams.ts index 03990c10b..ee3a0c03d 100644 --- a/core/startos/bindings/GetServiceInterfaceParams.ts +++ b/sdk/lib/osBindings/GetServiceInterfaceParams.ts @@ -1,8 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Callback } from "./Callback"; +import type { Callback } from "./Callback" export type GetServiceInterfaceParams = { - packageId: string | null; - serviceInterfaceId: string; - callback: Callback; -}; + packageId: string | null + serviceInterfaceId: string + callback: Callback +} diff --git a/core/startos/bindings/GetServicePortForwardParams.ts b/sdk/lib/osBindings/GetServicePortForwardParams.ts similarity index 57% rename from core/startos/bindings/GetServicePortForwardParams.ts rename to sdk/lib/osBindings/GetServicePortForwardParams.ts index 70d69c674..beb423d9a 100644 --- a/core/startos/bindings/GetServicePortForwardParams.ts +++ b/sdk/lib/osBindings/GetServicePortForwardParams.ts @@ -1,8 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { HostId } from "./HostId"; +import type { HostId } from "./HostId" export type GetServicePortForwardParams = { - packageId: string | null; - internalPort: number; - hostId: HostId; -}; + packageId: string | null + internalPort: number + hostId: HostId +} diff --git a/core/startos/bindings/GetSslCertificateParams.ts b/sdk/lib/osBindings/GetSslCertificateParams.ts similarity index 54% rename from core/startos/bindings/GetSslCertificateParams.ts rename to sdk/lib/osBindings/GetSslCertificateParams.ts index a1fd17bdd..a33eff540 100644 --- a/core/startos/bindings/GetSslCertificateParams.ts +++ b/sdk/lib/osBindings/GetSslCertificateParams.ts @@ -1,8 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Algorithm } from "./Algorithm"; +import type { Algorithm } from "./Algorithm" export type GetSslCertificateParams = { - packageId: string | null; - hostId: string; - algorithm: Algorithm | null; -}; + packageId: string | null + hostId: string + algorithm: Algorithm | null +} diff --git a/core/startos/bindings/GetSslKeyParams.ts b/sdk/lib/osBindings/GetSslKeyParams.ts similarity index 52% rename from core/startos/bindings/GetSslKeyParams.ts rename to sdk/lib/osBindings/GetSslKeyParams.ts index 8c5170c9c..0438c345a 100644 --- a/core/startos/bindings/GetSslKeyParams.ts +++ b/sdk/lib/osBindings/GetSslKeyParams.ts @@ -1,8 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Algorithm } from "./Algorithm"; +import type { Algorithm } from "./Algorithm" export type GetSslKeyParams = { - packageId: string | null; - hostId: string; - algorithm: Algorithm | null; -}; + packageId: string | null + hostId: string + algorithm: Algorithm | null +} diff --git a/core/startos/bindings/GetStoreParams.ts b/sdk/lib/osBindings/GetStoreParams.ts similarity index 95% rename from core/startos/bindings/GetStoreParams.ts rename to sdk/lib/osBindings/GetStoreParams.ts index bfd97377b..dc3d4e211 100644 --- a/core/startos/bindings/GetStoreParams.ts +++ b/sdk/lib/osBindings/GetStoreParams.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type GetStoreParams = { packageId: string | null; path: string }; +export type GetStoreParams = { packageId: string | null; path: string } diff --git a/sdk/lib/osBindings/GetSystemSmtpParams.ts b/sdk/lib/osBindings/GetSystemSmtpParams.ts new file mode 100644 index 000000000..650d59c49 --- /dev/null +++ b/sdk/lib/osBindings/GetSystemSmtpParams.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Callback } from "./Callback" + +export type GetSystemSmtpParams = { callback: Callback } diff --git a/core/startos/bindings/Governor.ts b/sdk/lib/osBindings/Governor.ts similarity index 78% rename from core/startos/bindings/Governor.ts rename to sdk/lib/osBindings/Governor.ts index 82c9e39f1..25e5f757f 100644 --- a/core/startos/bindings/Governor.ts +++ b/sdk/lib/osBindings/Governor.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Governor = string; +export type Governor = string diff --git a/core/startos/bindings/HardwareRequirements.ts b/sdk/lib/osBindings/HardwareRequirements.ts similarity index 61% rename from core/startos/bindings/HardwareRequirements.ts rename to sdk/lib/osBindings/HardwareRequirements.ts index 079483289..bbe7c3ef5 100644 --- a/core/startos/bindings/HardwareRequirements.ts +++ b/sdk/lib/osBindings/HardwareRequirements.ts @@ -1,7 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. export type HardwareRequirements = { - device: { [key: string]: string }; - ram: bigint | null; - arch: Array | null; -}; + device: { [key: string]: string } + ram: bigint | null + arch: Array | null +} diff --git a/core/startos/bindings/HealthCheckId.ts b/sdk/lib/osBindings/HealthCheckId.ts similarity index 75% rename from core/startos/bindings/HealthCheckId.ts rename to sdk/lib/osBindings/HealthCheckId.ts index eeefafbb1..bc5300473 100644 --- a/core/startos/bindings/HealthCheckId.ts +++ b/sdk/lib/osBindings/HealthCheckId.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type HealthCheckId = string; +export type HealthCheckId = string diff --git a/core/startos/bindings/HealthCheckResult.ts b/sdk/lib/osBindings/HealthCheckResult.ts similarity index 99% rename from core/startos/bindings/HealthCheckResult.ts rename to sdk/lib/osBindings/HealthCheckResult.ts index ae4d3c3c2..6fa3d3f8c 100644 --- a/core/startos/bindings/HealthCheckResult.ts +++ b/sdk/lib/osBindings/HealthCheckResult.ts @@ -6,4 +6,4 @@ export type HealthCheckResult = { name: string } & ( | { result: "starting"; message: string | null } | { result: "loading"; message: string } | { result: "failure"; message: string } -); +) diff --git a/sdk/lib/osBindings/Host.ts b/sdk/lib/osBindings/Host.ts new file mode 100644 index 000000000..a476c0adc --- /dev/null +++ b/sdk/lib/osBindings/Host.ts @@ -0,0 +1,11 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { BindInfo } from "./BindInfo" +import type { HostAddress } from "./HostAddress" +import type { HostKind } from "./HostKind" + +export type Host = { + kind: HostKind + bindings: { [key: number]: BindInfo } + addresses: Array + primary: HostAddress | null +} diff --git a/sdk/lib/osBindings/HostAddress.ts b/sdk/lib/osBindings/HostAddress.ts new file mode 100644 index 000000000..0388e49c7 --- /dev/null +++ b/sdk/lib/osBindings/HostAddress.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type HostAddress = { kind: "onion"; address: string } diff --git a/core/startos/bindings/HostId.ts b/sdk/lib/osBindings/HostId.ts similarity index 79% rename from core/startos/bindings/HostId.ts rename to sdk/lib/osBindings/HostId.ts index 23049a51d..f18dc2498 100644 --- a/core/startos/bindings/HostId.ts +++ b/sdk/lib/osBindings/HostId.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type HostId = string; +export type HostId = string diff --git a/sdk/lib/osBindings/HostInfo.ts b/sdk/lib/osBindings/HostInfo.ts new file mode 100644 index 000000000..d39b56ebe --- /dev/null +++ b/sdk/lib/osBindings/HostInfo.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Host } from "./Host" +import type { HostId } from "./HostId" + +export type HostInfo = { [key: HostId]: Host } diff --git a/core/startos/bindings/HostKind.ts b/sdk/lib/osBindings/HostKind.ts similarity index 77% rename from core/startos/bindings/HostKind.ts rename to sdk/lib/osBindings/HostKind.ts index 09e61b91b..3c2f5ad87 100644 --- a/core/startos/bindings/HostKind.ts +++ b/sdk/lib/osBindings/HostKind.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type HostKind = "multi"; +export type HostKind = "multi" diff --git a/core/startos/bindings/ImageId.ts b/sdk/lib/osBindings/ImageId.ts similarity index 78% rename from core/startos/bindings/ImageId.ts rename to sdk/lib/osBindings/ImageId.ts index 408331f72..330b812b9 100644 --- a/core/startos/bindings/ImageId.ts +++ b/sdk/lib/osBindings/ImageId.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type ImageId = string; +export type ImageId = string diff --git a/sdk/lib/osBindings/InstalledState.ts b/sdk/lib/osBindings/InstalledState.ts new file mode 100644 index 000000000..48177287e --- /dev/null +++ b/sdk/lib/osBindings/InstalledState.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Manifest } from "./Manifest" + +export type InstalledState = { manifest: Manifest } diff --git a/core/startos/bindings/InstallingInfo.ts b/sdk/lib/osBindings/InstallingInfo.ts similarity index 60% rename from core/startos/bindings/InstallingInfo.ts rename to sdk/lib/osBindings/InstallingInfo.ts index 7627f4502..6b77b49d9 100644 --- a/core/startos/bindings/InstallingInfo.ts +++ b/sdk/lib/osBindings/InstallingInfo.ts @@ -1,5 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FullProgress } from "./FullProgress"; -import type { Manifest } from "./Manifest"; +import type { FullProgress } from "./FullProgress" +import type { Manifest } from "./Manifest" -export type InstallingInfo = { newManifest: Manifest; progress: FullProgress }; +export type InstallingInfo = { newManifest: Manifest; progress: FullProgress } diff --git a/core/startos/bindings/InstallingState.ts b/sdk/lib/osBindings/InstallingState.ts similarity index 75% rename from core/startos/bindings/InstallingState.ts rename to sdk/lib/osBindings/InstallingState.ts index 3203cfd6b..db752df90 100644 --- a/core/startos/bindings/InstallingState.ts +++ b/sdk/lib/osBindings/InstallingState.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { InstallingInfo } from "./InstallingInfo"; +import type { InstallingInfo } from "./InstallingInfo" -export type InstallingState = { installingInfo: InstallingInfo }; +export type InstallingState = { installingInfo: InstallingInfo } diff --git a/core/startos/bindings/IpInfo.ts b/sdk/lib/osBindings/IpInfo.ts similarity index 55% rename from core/startos/bindings/IpInfo.ts rename to sdk/lib/osBindings/IpInfo.ts index 1208fafb9..ae8c88d1b 100644 --- a/core/startos/bindings/IpInfo.ts +++ b/sdk/lib/osBindings/IpInfo.ts @@ -1,8 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. export type IpInfo = { - ipv4Range: string | null; - ipv4: string | null; - ipv6Range: string | null; - ipv6: string | null; -}; + ipv4Range: string | null + ipv4: string | null + ipv6Range: string | null + ipv6: string | null +} diff --git a/core/startos/bindings/ListServiceInterfacesParams.ts b/sdk/lib/osBindings/ListServiceInterfacesParams.ts similarity index 61% rename from core/startos/bindings/ListServiceInterfacesParams.ts rename to sdk/lib/osBindings/ListServiceInterfacesParams.ts index 03d1cdfc8..4140831d0 100644 --- a/core/startos/bindings/ListServiceInterfacesParams.ts +++ b/sdk/lib/osBindings/ListServiceInterfacesParams.ts @@ -1,7 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Callback } from "./Callback"; +import type { Callback } from "./Callback" export type ListServiceInterfacesParams = { - packageId: string | null; - callback: Callback; -}; + packageId: string | null + callback: Callback +} diff --git a/sdk/lib/osBindings/MainStatus.ts b/sdk/lib/osBindings/MainStatus.ts new file mode 100644 index 000000000..534cad76f --- /dev/null +++ b/sdk/lib/osBindings/MainStatus.ts @@ -0,0 +1,20 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Duration } from "./Duration" +import type { HealthCheckId } from "./HealthCheckId" +import type { HealthCheckResult } from "./HealthCheckResult" + +export type MainStatus = + | { status: "stopped" } + | { status: "restarting" } + | { status: "stopping"; timeout: Duration } + | { status: "starting" } + | { + status: "running" + started: string + health: { [key: HealthCheckId]: HealthCheckResult } + } + | { + status: "backingUp" + started: string | null + health: { [key: HealthCheckId]: HealthCheckResult } + } diff --git a/sdk/lib/osBindings/Manifest.ts b/sdk/lib/osBindings/Manifest.ts new file mode 100644 index 000000000..14f2224ef --- /dev/null +++ b/sdk/lib/osBindings/Manifest.ts @@ -0,0 +1,32 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Alerts } from "./Alerts" +import type { Dependencies } from "./Dependencies" +import type { Description } from "./Description" +import type { HardwareRequirements } from "./HardwareRequirements" +import type { ImageId } from "./ImageId" +import type { PackageId } from "./PackageId" +import type { VolumeId } from "./VolumeId" + +export type Manifest = { + id: PackageId + title: string + version: string + releaseNotes: string + license: string + replaces: Array + wrapperRepo: string + upstreamRepo: string + supportSite: string + marketingSite: string + donationUrl: string | null + description: Description + images: Array + assets: Array + volumes: Array + alerts: Alerts + dependencies: Dependencies + hardwareRequirements: HardwareRequirements + gitHash: string | null + osVersion: string + hasConfig: boolean +} diff --git a/core/startos/bindings/EncryptedWire.ts b/sdk/lib/osBindings/MaybeUtf8String.ts similarity index 69% rename from core/startos/bindings/EncryptedWire.ts rename to sdk/lib/osBindings/MaybeUtf8String.ts index efffd7112..9532e9b97 100644 --- a/core/startos/bindings/EncryptedWire.ts +++ b/sdk/lib/osBindings/MaybeUtf8String.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type EncryptedWire = { encrypted: any }; +export type MaybeUtf8String = string | number[] diff --git a/core/startos/bindings/MountParams.ts b/sdk/lib/osBindings/MountParams.ts similarity index 75% rename from core/startos/bindings/MountParams.ts rename to sdk/lib/osBindings/MountParams.ts index daa4ddf32..778983fd6 100644 --- a/core/startos/bindings/MountParams.ts +++ b/sdk/lib/osBindings/MountParams.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { MountTarget } from "./MountTarget"; +import type { MountTarget } from "./MountTarget" -export type MountParams = { location: string; target: MountTarget }; +export type MountParams = { location: string; target: MountTarget } diff --git a/core/startos/bindings/MountTarget.ts b/sdk/lib/osBindings/MountTarget.ts similarity index 60% rename from core/startos/bindings/MountTarget.ts rename to sdk/lib/osBindings/MountTarget.ts index 3009861fb..e4888d075 100644 --- a/core/startos/bindings/MountTarget.ts +++ b/sdk/lib/osBindings/MountTarget.ts @@ -1,8 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. export type MountTarget = { - packageId: string; - volumeId: string; - subpath: string | null; - readonly: boolean; -}; + packageId: string + volumeId: string + subpath: string | null + readonly: boolean +} diff --git a/core/startos/bindings/NamedProgress.ts b/sdk/lib/osBindings/NamedProgress.ts similarity index 79% rename from core/startos/bindings/NamedProgress.ts rename to sdk/lib/osBindings/NamedProgress.ts index 42104c48b..52a410a51 100644 --- a/core/startos/bindings/NamedProgress.ts +++ b/sdk/lib/osBindings/NamedProgress.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Progress } from "./Progress"; +import type { Progress } from "./Progress" -export type NamedProgress = { name: string; progress: Progress }; +export type NamedProgress = { name: string; progress: Progress } diff --git a/sdk/lib/osBindings/PackageDataEntry.ts b/sdk/lib/osBindings/PackageDataEntry.ts new file mode 100644 index 000000000..28074a08d --- /dev/null +++ b/sdk/lib/osBindings/PackageDataEntry.ts @@ -0,0 +1,24 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ActionId } from "./ActionId" +import type { ActionMetadata } from "./ActionMetadata" +import type { CurrentDependencies } from "./CurrentDependencies" +import type { DataUrl } from "./DataUrl" +import type { HostInfo } from "./HostInfo" +import type { PackageState } from "./PackageState" +import type { ServiceInterfaceId } from "./ServiceInterfaceId" +import type { ServiceInterfaceWithHostInfo } from "./ServiceInterfaceWithHostInfo" +import type { Status } from "./Status" + +export type PackageDataEntry = { + stateInfo: PackageState + status: Status + marketplaceUrl: string | null + developerKey: string + icon: DataUrl + lastBackup: string | null + currentDependencies: CurrentDependencies + actions: { [key: ActionId]: ActionMetadata } + serviceInterfaces: { [key: ServiceInterfaceId]: ServiceInterfaceWithHostInfo } + hosts: HostInfo + storeExposedDependents: string[] +} diff --git a/core/startos/bindings/PackageId.ts b/sdk/lib/osBindings/PackageId.ts similarity index 77% rename from core/startos/bindings/PackageId.ts rename to sdk/lib/osBindings/PackageId.ts index 349bf44d7..9e3403303 100644 --- a/core/startos/bindings/PackageId.ts +++ b/sdk/lib/osBindings/PackageId.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type PackageId = string; +export type PackageId = string diff --git a/core/startos/bindings/PackageState.ts b/sdk/lib/osBindings/PackageState.ts similarity index 60% rename from core/startos/bindings/PackageState.ts rename to sdk/lib/osBindings/PackageState.ts index 6c90bff09..fd15076ca 100644 --- a/core/startos/bindings/PackageState.ts +++ b/sdk/lib/osBindings/PackageState.ts @@ -1,11 +1,11 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { InstalledState } from "./InstalledState"; -import type { InstallingState } from "./InstallingState"; -import type { UpdatingState } from "./UpdatingState"; +import type { InstalledState } from "./InstalledState" +import type { InstallingState } from "./InstallingState" +import type { UpdatingState } from "./UpdatingState" export type PackageState = | ({ state: "installing" } & InstallingState) | ({ state: "restoring" } & InstallingState) | ({ state: "updating" } & UpdatingState) | ({ state: "installed" } & InstalledState) - | ({ state: "removing" } & InstalledState); + | ({ state: "removing" } & InstalledState) diff --git a/core/startos/bindings/CreateOverlayedImageParams.ts b/sdk/lib/osBindings/ParamsMaybePackageId.ts similarity index 63% rename from core/startos/bindings/CreateOverlayedImageParams.ts rename to sdk/lib/osBindings/ParamsMaybePackageId.ts index d17f2e8c1..a20bb9aa5 100644 --- a/core/startos/bindings/CreateOverlayedImageParams.ts +++ b/sdk/lib/osBindings/ParamsMaybePackageId.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type CreateOverlayedImageParams = { imageId: string }; +export type ParamsMaybePackageId = { packageId: string | null } diff --git a/core/startos/bindings/BackupProgress.ts b/sdk/lib/osBindings/ParamsPackageId.ts similarity index 67% rename from core/startos/bindings/BackupProgress.ts rename to sdk/lib/osBindings/ParamsPackageId.ts index 42812a7b7..f4dd1c1eb 100644 --- a/core/startos/bindings/BackupProgress.ts +++ b/sdk/lib/osBindings/ParamsPackageId.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type BackupProgress = { complete: boolean }; +export type ParamsPackageId = { packageId: string } diff --git a/sdk/lib/osBindings/PasswordType.ts b/sdk/lib/osBindings/PasswordType.ts new file mode 100644 index 000000000..7fdcc0f5d --- /dev/null +++ b/sdk/lib/osBindings/PasswordType.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { EncryptedWire } from "./EncryptedWire" + +export type PasswordType = EncryptedWire | string diff --git a/core/startos/bindings/Progress.ts b/sdk/lib/osBindings/Progress.ts similarity index 95% rename from core/startos/bindings/Progress.ts rename to sdk/lib/osBindings/Progress.ts index c8e5e4431..3ece4307c 100644 --- a/core/startos/bindings/Progress.ts +++ b/sdk/lib/osBindings/Progress.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Progress = boolean | { done: number; total: number | null }; +export type Progress = boolean | { done: number; total: number | null } diff --git a/sdk/lib/osBindings/Public.ts b/sdk/lib/osBindings/Public.ts new file mode 100644 index 000000000..c77ae05e3 --- /dev/null +++ b/sdk/lib/osBindings/Public.ts @@ -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 { AllPackageData } from "./AllPackageData" +import type { ServerInfo } from "./ServerInfo" + +export type Public = { + serverInfo: ServerInfo + packageData: AllPackageData + ui: any +} diff --git a/sdk/lib/osBindings/RemoveActionParams.ts b/sdk/lib/osBindings/RemoveActionParams.ts new file mode 100644 index 000000000..c343620b8 --- /dev/null +++ b/sdk/lib/osBindings/RemoveActionParams.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type RemoveActionParams = { id: string } diff --git a/core/startos/bindings/MaybeUtf8String.ts b/sdk/lib/osBindings/RemoveAddressParams.ts similarity index 69% rename from core/startos/bindings/MaybeUtf8String.ts rename to sdk/lib/osBindings/RemoveAddressParams.ts index a77f8ce4e..bdc781837 100644 --- a/core/startos/bindings/MaybeUtf8String.ts +++ b/sdk/lib/osBindings/RemoveAddressParams.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type MaybeUtf8String = string | number[]; +export type RemoveAddressParams = { id: string } diff --git a/sdk/lib/osBindings/ReverseProxyBind.ts b/sdk/lib/osBindings/ReverseProxyBind.ts new file mode 100644 index 000000000..bd07b9489 --- /dev/null +++ b/sdk/lib/osBindings/ReverseProxyBind.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ReverseProxyBind = { ip: string | null; port: number; ssl: boolean } diff --git a/core/startos/bindings/ReverseProxyDestination.ts b/sdk/lib/osBindings/ReverseProxyDestination.ts similarity index 72% rename from core/startos/bindings/ReverseProxyDestination.ts rename to sdk/lib/osBindings/ReverseProxyDestination.ts index 216d1310f..88b5dd650 100644 --- a/core/startos/bindings/ReverseProxyDestination.ts +++ b/sdk/lib/osBindings/ReverseProxyDestination.ts @@ -1,7 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. export type ReverseProxyDestination = { - ip: string | null; - port: number; - ssl: boolean; -}; + ip: string | null + port: number + ssl: boolean +} diff --git a/core/startos/bindings/ReverseProxyHttp.ts b/sdk/lib/osBindings/ReverseProxyHttp.ts similarity index 92% rename from core/startos/bindings/ReverseProxyHttp.ts rename to sdk/lib/osBindings/ReverseProxyHttp.ts index 07cf41862..ba49e81dc 100644 --- a/core/startos/bindings/ReverseProxyHttp.ts +++ b/sdk/lib/osBindings/ReverseProxyHttp.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type ReverseProxyHttp = { headers: null | { [key: string]: string } }; +export type ReverseProxyHttp = { headers: null | { [key: string]: string } } diff --git a/sdk/lib/osBindings/ReverseProxyParams.ts b/sdk/lib/osBindings/ReverseProxyParams.ts new file mode 100644 index 000000000..00062cbdf --- /dev/null +++ b/sdk/lib/osBindings/ReverseProxyParams.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ReverseProxyBind } from "./ReverseProxyBind" +import type { ReverseProxyDestination } from "./ReverseProxyDestination" +import type { ReverseProxyHttp } from "./ReverseProxyHttp" + +export type ReverseProxyParams = { + bind: ReverseProxyBind + dst: ReverseProxyDestination + http: ReverseProxyHttp +} diff --git a/core/startos/bindings/Security.ts b/sdk/lib/osBindings/Security.ts similarity index 72% rename from core/startos/bindings/Security.ts rename to sdk/lib/osBindings/Security.ts index e722707df..333783cd2 100644 --- a/core/startos/bindings/Security.ts +++ b/sdk/lib/osBindings/Security.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Security = { ssl: boolean }; +export type Security = { ssl: boolean } diff --git a/sdk/lib/osBindings/ServerInfo.ts b/sdk/lib/osBindings/ServerInfo.ts new file mode 100644 index 000000000..284eba336 --- /dev/null +++ b/sdk/lib/osBindings/ServerInfo.ts @@ -0,0 +1,31 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Governor } from "./Governor" +import type { IpInfo } from "./IpInfo" +import type { ServerStatus } from "./ServerStatus" +import type { WifiInfo } from "./WifiInfo" + +export type ServerInfo = { + arch: string + platform: string + id: string + hostname: string + version: string + lastBackup: string | null + eosVersionCompat: string + lanAddress: string + onionAddress: string + /** + * for backwards compatibility + */ + torAddress: string + ipInfo: { [key: string]: IpInfo } + statusInfo: ServerStatus + wifi: WifiInfo + unreadNotificationCount: number + passwordHash: string + pubkey: string + caFingerprint: string + ntpSynced: boolean + zram: boolean + governor: Governor | null +} diff --git a/core/startos/bindings/ServerSpecs.ts b/sdk/lib/osBindings/ServerSpecs.ts similarity index 95% rename from core/startos/bindings/ServerSpecs.ts rename to sdk/lib/osBindings/ServerSpecs.ts index b2278873c..c736ca5c8 100644 --- a/core/startos/bindings/ServerSpecs.ts +++ b/sdk/lib/osBindings/ServerSpecs.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type ServerSpecs = { cpu: string; disk: string; memory: string }; +export type ServerSpecs = { cpu: string; disk: string; memory: string } diff --git a/sdk/lib/osBindings/ServerStatus.ts b/sdk/lib/osBindings/ServerStatus.ts new file mode 100644 index 000000000..2d3a362d7 --- /dev/null +++ b/sdk/lib/osBindings/ServerStatus.ts @@ -0,0 +1,12 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { BackupProgress } from "./BackupProgress" +import type { PackageId } from "./PackageId" +import type { UpdateProgress } from "./UpdateProgress" + +export type ServerStatus = { + backupProgress: { [key: PackageId]: BackupProgress } | null + updated: boolean + updateProgress: UpdateProgress | null + shuttingDown: boolean + restarting: boolean +} diff --git a/sdk/lib/osBindings/ServiceInterface.ts b/sdk/lib/osBindings/ServiceInterface.ts new file mode 100644 index 000000000..91ac77515 --- /dev/null +++ b/sdk/lib/osBindings/ServiceInterface.ts @@ -0,0 +1,15 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AddressInfo } from "./AddressInfo" +import type { ServiceInterfaceId } from "./ServiceInterfaceId" +import type { ServiceInterfaceType } from "./ServiceInterfaceType" + +export type ServiceInterface = { + id: ServiceInterfaceId + name: string + description: string + hasPrimary: boolean + disabled: boolean + masked: boolean + addressInfo: AddressInfo + type: ServiceInterfaceType +} diff --git a/core/startos/bindings/ServiceInterfaceId.ts b/sdk/lib/osBindings/ServiceInterfaceId.ts similarity index 72% rename from core/startos/bindings/ServiceInterfaceId.ts rename to sdk/lib/osBindings/ServiceInterfaceId.ts index 55315ab60..3ad454e04 100644 --- a/core/startos/bindings/ServiceInterfaceId.ts +++ b/sdk/lib/osBindings/ServiceInterfaceId.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type ServiceInterfaceId = string; +export type ServiceInterfaceId = string diff --git a/sdk/lib/osBindings/ServiceInterfaceType.ts b/sdk/lib/osBindings/ServiceInterfaceType.ts new file mode 100644 index 000000000..109691474 --- /dev/null +++ b/sdk/lib/osBindings/ServiceInterfaceType.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ServiceInterfaceType = "ui" | "p2p" | "api" diff --git a/sdk/lib/osBindings/ServiceInterfaceWithHostInfo.ts b/sdk/lib/osBindings/ServiceInterfaceWithHostInfo.ts new file mode 100644 index 000000000..979e6b500 --- /dev/null +++ b/sdk/lib/osBindings/ServiceInterfaceWithHostInfo.ts @@ -0,0 +1,17 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AddressInfo } from "./AddressInfo" +import type { ExportedHostInfo } from "./ExportedHostInfo" +import type { ServiceInterfaceId } from "./ServiceInterfaceId" +import type { ServiceInterfaceType } from "./ServiceInterfaceType" + +export type ServiceInterfaceWithHostInfo = { + hostInfo: ExportedHostInfo + id: ServiceInterfaceId + name: string + description: string + hasPrimary: boolean + disabled: boolean + masked: boolean + addressInfo: AddressInfo + type: ServiceInterfaceType +} diff --git a/core/startos/bindings/Session.ts b/sdk/lib/osBindings/Session.ts similarity index 92% rename from core/startos/bindings/Session.ts rename to sdk/lib/osBindings/Session.ts index 05a3ae65d..f9cffaf36 100644 --- a/core/startos/bindings/Session.ts +++ b/sdk/lib/osBindings/Session.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Session = { loggedIn: string; lastActive: string; metadata: any }; +export type Session = { loggedIn: string; lastActive: string; metadata: any } diff --git a/core/startos/bindings/SessionList.ts b/sdk/lib/osBindings/SessionList.ts similarity index 78% rename from core/startos/bindings/SessionList.ts rename to sdk/lib/osBindings/SessionList.ts index ef1561188..a85a42dee 100644 --- a/core/startos/bindings/SessionList.ts +++ b/sdk/lib/osBindings/SessionList.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Sessions } from "./Sessions"; +import type { Sessions } from "./Sessions" -export type SessionList = { current: string; sessions: Sessions }; +export type SessionList = { current: string; sessions: Sessions } diff --git a/core/startos/bindings/Sessions.ts b/sdk/lib/osBindings/Sessions.ts similarity index 93% rename from core/startos/bindings/Sessions.ts rename to sdk/lib/osBindings/Sessions.ts index a5ac67b65..bb153ddb4 100644 --- a/core/startos/bindings/Sessions.ts +++ b/sdk/lib/osBindings/Sessions.ts @@ -1,5 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. export type Sessions = { - [key: string]: { loggedIn: string; lastActive: string; metadata: any }; -}; + [key: string]: { loggedIn: string; lastActive: string; metadata: any } +} diff --git a/sdk/lib/osBindings/SetConfigured.ts b/sdk/lib/osBindings/SetConfigured.ts new file mode 100644 index 000000000..e1478422a --- /dev/null +++ b/sdk/lib/osBindings/SetConfigured.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SetConfigured = { configured: boolean } diff --git a/core/startos/bindings/SetDependenciesParams.ts b/sdk/lib/osBindings/SetDependenciesParams.ts similarity index 79% rename from core/startos/bindings/SetDependenciesParams.ts rename to sdk/lib/osBindings/SetDependenciesParams.ts index d4d2d45cb..7b34b50c9 100644 --- a/core/startos/bindings/SetDependenciesParams.ts +++ b/sdk/lib/osBindings/SetDependenciesParams.ts @@ -1,6 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DependencyRequirement } from "./DependencyRequirement"; +import type { DependencyRequirement } from "./DependencyRequirement" export type SetDependenciesParams = { - dependencies: Array; -}; + dependencies: Array +} diff --git a/core/startos/bindings/SetHealth.ts b/sdk/lib/osBindings/SetHealth.ts similarity index 87% rename from core/startos/bindings/SetHealth.ts rename to sdk/lib/osBindings/SetHealth.ts index 8a4a1bcff..47f67886a 100644 --- a/core/startos/bindings/SetHealth.ts +++ b/sdk/lib/osBindings/SetHealth.ts @@ -1,5 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { HealthCheckId } from "./HealthCheckId"; +import type { HealthCheckId } from "./HealthCheckId" export type SetHealth = { id: HealthCheckId; name: string } & ( | { result: "success"; message: string | null } @@ -7,4 +7,4 @@ export type SetHealth = { id: HealthCheckId; name: string } & ( | { result: "starting"; message: string | null } | { result: "loading"; message: string } | { result: "failure"; message: string } -); +) diff --git a/sdk/lib/osBindings/SetMainStatus.ts b/sdk/lib/osBindings/SetMainStatus.ts new file mode 100644 index 000000000..32a839984 --- /dev/null +++ b/sdk/lib/osBindings/SetMainStatus.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Status } from "./Status" + +export type SetMainStatus = { status: Status } diff --git a/sdk/lib/osBindings/SetStoreParams.ts b/sdk/lib/osBindings/SetStoreParams.ts new file mode 100644 index 000000000..ecdd7b042 --- /dev/null +++ b/sdk/lib/osBindings/SetStoreParams.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SetStoreParams = { value: any; path: string } diff --git a/sdk/lib/osBindings/Status.ts b/sdk/lib/osBindings/Status.ts new file mode 100644 index 000000000..b784f4d6a --- /dev/null +++ b/sdk/lib/osBindings/Status.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { MainStatus } from "./MainStatus" + +export type Status = { configured: boolean; main: MainStatus } diff --git a/core/startos/bindings/UpdateProgress.ts b/sdk/lib/osBindings/UpdateProgress.ts similarity index 94% rename from core/startos/bindings/UpdateProgress.ts rename to sdk/lib/osBindings/UpdateProgress.ts index 3d07c56b4..19b55f262 100644 --- a/core/startos/bindings/UpdateProgress.ts +++ b/sdk/lib/osBindings/UpdateProgress.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type UpdateProgress = { size: number | null; downloaded: number }; +export type UpdateProgress = { size: number | null; downloaded: number } diff --git a/sdk/lib/osBindings/UpdatingState.ts b/sdk/lib/osBindings/UpdatingState.ts new file mode 100644 index 000000000..117124f91 --- /dev/null +++ b/sdk/lib/osBindings/UpdatingState.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { InstallingInfo } from "./InstallingInfo" +import type { Manifest } from "./Manifest" + +export type UpdatingState = { + manifest: Manifest + installingInfo: InstallingInfo +} diff --git a/core/startos/bindings/VolumeId.ts b/sdk/lib/osBindings/VolumeId.ts similarity index 78% rename from core/startos/bindings/VolumeId.ts rename to sdk/lib/osBindings/VolumeId.ts index e5c63994e..b4657af4c 100644 --- a/core/startos/bindings/VolumeId.ts +++ b/sdk/lib/osBindings/VolumeId.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type VolumeId = string; +export type VolumeId = string diff --git a/core/startos/bindings/WifiInfo.ts b/sdk/lib/osBindings/WifiInfo.ts similarity index 54% rename from core/startos/bindings/WifiInfo.ts rename to sdk/lib/osBindings/WifiInfo.ts index 812cc431f..c5103e7e2 100644 --- a/core/startos/bindings/WifiInfo.ts +++ b/sdk/lib/osBindings/WifiInfo.ts @@ -1,8 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. export type WifiInfo = { - interface: string | null; - ssids: Array; - selected: string | null; - lastRegion: string | null; -}; + interface: string | null + ssids: Array + selected: string | null + lastRegion: string | null +} diff --git a/sdk/lib/osBindings/index.ts b/sdk/lib/osBindings/index.ts new file mode 100644 index 000000000..67129fba1 --- /dev/null +++ b/sdk/lib/osBindings/index.ts @@ -0,0 +1,101 @@ +export { ActionId } from "./ActionId" +export { ActionMetadata } from "./ActionMetadata" +export { AddressInfo } from "./AddressInfo" +export { AddSslOptions } from "./AddSslOptions" +export { Alerts } from "./Alerts" +export { Algorithm } from "./Algorithm" +export { AllowedStatuses } from "./AllowedStatuses" +export { AllPackageData } from "./AllPackageData" +export { AlpnInfo } from "./AlpnInfo" +export { BackupProgress } from "./BackupProgress" +export { BindInfo } from "./BindInfo" +export { BindOptions } from "./BindOptions" +export { BindParams } from "./BindParams" +export { Callback } from "./Callback" +export { ChrootParams } from "./ChrootParams" +export { CreateOverlayedImageParams } from "./CreateOverlayedImageParams" +export { CurrentDependencies } from "./CurrentDependencies" +export { CurrentDependencyInfo } from "./CurrentDependencyInfo" +export { DataUrl } from "./DataUrl" +export { Dependencies } from "./Dependencies" +export { DependencyKind } from "./DependencyKind" +export { DependencyRequirement } from "./DependencyRequirement" +export { DepInfo } from "./DepInfo" +export { Description } from "./Description" +export { DestroyOverlayedImageParams } from "./DestroyOverlayedImageParams" +export { Duration } from "./Duration" +export { EncryptedWire } from "./EncryptedWire" +export { ExecuteAction } from "./ExecuteAction" +export { ExportActionParams } from "./ExportActionParams" +export { ExportedHostInfo } from "./ExportedHostInfo" +export { ExportedHostnameInfo } from "./ExportedHostnameInfo" +export { ExportedIpHostname } from "./ExportedIpHostname" +export { ExportedOnionHostname } from "./ExportedOnionHostname" +export { ExportServiceInterfaceParams } from "./ExportServiceInterfaceParams" +export { ExposeForDependentsParams } from "./ExposeForDependentsParams" +export { FullProgress } from "./FullProgress" +export { GetHostInfoParamsKind } from "./GetHostInfoParamsKind" +export { GetHostInfoParams } from "./GetHostInfoParams" +export { GetPrimaryUrlParams } from "./GetPrimaryUrlParams" +export { GetServiceInterfaceParams } from "./GetServiceInterfaceParams" +export { GetServicePortForwardParams } from "./GetServicePortForwardParams" +export { GetSslCertificateParams } from "./GetSslCertificateParams" +export { GetSslKeyParams } from "./GetSslKeyParams" +export { GetStoreParams } from "./GetStoreParams" +export { GetSystemSmtpParams } from "./GetSystemSmtpParams" +export { Governor } from "./Governor" +export { HardwareRequirements } from "./HardwareRequirements" +export { HealthCheckId } from "./HealthCheckId" +export { HealthCheckResult } from "./HealthCheckResult" +export { HostAddress } from "./HostAddress" +export { HostId } from "./HostId" +export { HostInfo } from "./HostInfo" +export { HostKind } from "./HostKind" +export { Host } from "./Host" +export { ImageId } from "./ImageId" +export { InstalledState } from "./InstalledState" +export { InstallingInfo } from "./InstallingInfo" +export { InstallingState } from "./InstallingState" +export { IpInfo } from "./IpInfo" +export { ListServiceInterfacesParams } from "./ListServiceInterfacesParams" +export { MainStatus } from "./MainStatus" +export { Manifest } from "./Manifest" +export { MaybeUtf8String } from "./MaybeUtf8String" +export { MountParams } from "./MountParams" +export { MountTarget } from "./MountTarget" +export { NamedProgress } from "./NamedProgress" +export { PackageDataEntry } from "./PackageDataEntry" +export { PackageId } from "./PackageId" +export { PackageState } from "./PackageState" +export { ParamsMaybePackageId } from "./ParamsMaybePackageId" +export { ParamsPackageId } from "./ParamsPackageId" +export { PasswordType } from "./PasswordType" +export { Progress } from "./Progress" +export { Public } from "./Public" +export { RemoveActionParams } from "./RemoveActionParams" +export { RemoveAddressParams } from "./RemoveAddressParams" +export { ReverseProxyBind } from "./ReverseProxyBind" +export { ReverseProxyDestination } from "./ReverseProxyDestination" +export { ReverseProxyHttp } from "./ReverseProxyHttp" +export { ReverseProxyParams } from "./ReverseProxyParams" +export { Security } from "./Security" +export { ServerInfo } from "./ServerInfo" +export { ServerSpecs } from "./ServerSpecs" +export { ServerStatus } from "./ServerStatus" +export { ServiceInterfaceId } from "./ServiceInterfaceId" +export { ServiceInterface } from "./ServiceInterface" +export { ServiceInterfaceType } from "./ServiceInterfaceType" +export { ServiceInterfaceWithHostInfo } from "./ServiceInterfaceWithHostInfo" +export { SessionList } from "./SessionList" +export { Sessions } from "./Sessions" +export { Session } from "./Session" +export { SetConfigured } from "./SetConfigured" +export { SetDependenciesParams } from "./SetDependenciesParams" +export { SetHealth } from "./SetHealth" +export { SetMainStatus } from "./SetMainStatus" +export { SetStoreParams } from "./SetStoreParams" +export { Status } from "./Status" +export { UpdateProgress } from "./UpdateProgress" +export { UpdatingState } from "./UpdatingState" +export { VolumeId } from "./VolumeId" +export { WifiInfo } from "./WifiInfo" diff --git a/sdk/lib/test/startosTypeValidation.test.ts b/sdk/lib/test/startosTypeValidation.test.ts index 5fa3f74b3..0743db4fa 100644 --- a/sdk/lib/test/startosTypeValidation.test.ts +++ b/sdk/lib/test/startosTypeValidation.test.ts @@ -1,28 +1,28 @@ import { Effects } from "../types" -import { ExecuteAction } from "../../../core/startos/bindings/ExecuteAction" -import { CreateOverlayedImageParams } from "../../../core/startos/bindings/CreateOverlayedImageParams" -import { DestroyOverlayedImageParams } from "../../../core/startos/bindings/DestroyOverlayedImageParams" -import { BindParams } from "../../../core/startos/bindings/BindParams" -import { GetHostInfoParams } from "../../../core/startos/bindings/GetHostInfoParams" -import { ParamsPackageId } from "../../../core/startos/bindings/ParamsPackageId" -import { ParamsMaybePackageId } from "../../../core/startos/bindings/ParamsMaybePackageId" -import { SetConfigured } from "../../../core/startos/bindings/SetConfigured" -import { SetHealth } from "../../../core/startos/bindings/SetHealth" -import { ExposeForDependentsParams } from "../../../core/startos/bindings/ExposeForDependentsParams" -import { GetSslCertificateParams } from "../../../core/startos/bindings/GetSslCertificateParams" -import { GetSslKeyParams } from "../../../core/startos/bindings/GetSslKeyParams" -import { GetServiceInterfaceParams } from "../../../core/startos/bindings/GetServiceInterfaceParams" -import { SetDependenciesParams } from "../../../core/startos/bindings/SetDependenciesParams" -import { GetSystemSmtpParams } from "../../../core/startos/bindings/GetSystemSmtpParams" -import { GetServicePortForwardParams } from "../../../core/startos/bindings/GetServicePortForwardParams" -import { ExportServiceInterfaceParams } from "../../../core/startos/bindings/ExportServiceInterfaceParams" -import { GetPrimaryUrlParams } from "../../../core/startos/bindings/GetPrimaryUrlParams" -import { ListServiceInterfacesParams } from "../../../core/startos/bindings/ListServiceInterfacesParams" -import { RemoveAddressParams } from "../../../core/startos/bindings/RemoveAddressParams" -import { ExportActionParams } from "../../../core/startos/bindings/ExportActionParams" -import { RemoveActionParams } from "../../../core/startos/bindings/RemoveActionParams" -import { ReverseProxyParams } from "../../../core/startos/bindings/ReverseProxyParams" -import { MountParams } from "../../../core/startos/bindings/MountParams" +import { ExecuteAction } from ".././osBindings" +import { CreateOverlayedImageParams } from ".././osBindings" +import { DestroyOverlayedImageParams } from ".././osBindings" +import { BindParams } from ".././osBindings" +import { GetHostInfoParams } from ".././osBindings" +import { ParamsPackageId } from ".././osBindings" +import { ParamsMaybePackageId } from ".././osBindings" +import { SetConfigured } from ".././osBindings" +import { SetHealth } from ".././osBindings" +import { ExposeForDependentsParams } from ".././osBindings" +import { GetSslCertificateParams } from ".././osBindings" +import { GetSslKeyParams } from ".././osBindings" +import { GetServiceInterfaceParams } from ".././osBindings" +import { SetDependenciesParams } from ".././osBindings" +import { GetSystemSmtpParams } from ".././osBindings" +import { GetServicePortForwardParams } from ".././osBindings" +import { ExportServiceInterfaceParams } from ".././osBindings" +import { GetPrimaryUrlParams } from ".././osBindings" +import { ListServiceInterfacesParams } from ".././osBindings" +import { RemoveAddressParams } from ".././osBindings" +import { ExportActionParams } from ".././osBindings" +import { RemoveActionParams } from ".././osBindings" +import { ReverseProxyParams } from ".././osBindings" +import { MountParams } from ".././osBindings" function typeEquality(_a: ExpectedType) {} describe("startosTypeValidation ", () => { test(`checking the params match`, () => { diff --git a/sdk/lib/types.ts b/sdk/lib/types.ts index f266d2870..d33bda79f 100644 --- a/sdk/lib/types.ts +++ b/sdk/lib/types.ts @@ -1,6 +1,6 @@ export * as configTypes from "./config/configTypes" -import { HealthCheckId } from "../../core/startos/bindings/HealthCheckId" -import { HealthCheckResult } from "../../core/startos/bindings/HealthCheckResult" +import { HealthCheckId } from "./osBindings" +import { HealthCheckResult } from "./osBindings" import { MainEffects, ServiceInterfaceType, Signals } from "./StartSdk" import { InputSpec } from "./config/configTypes" import { DependenciesReceipt } from "./config/setupConfig" @@ -9,7 +9,7 @@ import { Daemons } from "./mainFn/Daemons" import { PathBuilder, StorePath } from "./store/PathBuilder" import { ExposedStorePaths } from "./store/setupExposeStore" import { UrlString } from "./util/getServiceInterface" -export * from "../../core/startos/bindings" +export * from "./osBindings" export { SDKManifest } from "./manifest/ManifestTypes" export type ExportedAction = (options: { diff --git a/sdk/package.json b/sdk/package.json index 6176f4d8e..409c5bb02 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -2,10 +2,10 @@ "name": "@start9labs/start-sdk", "version": "0.4.0-rev0.lib0.rc8.beta10", "description": "Software development kit to facilitate packaging services for StartOS", - "main": "./cjs/sdk/lib/index.js", - "types": "./cjs/sdk/lib/index.d.ts", - "module": "./mjs/sdk/lib/index.js", - "browser": "./mjs/sdk/lib/index.browser.js", + "main": "./cjs/lib/index.js", + "types": "./cjs/lib/index.d.ts", + "module": "./mjs/lib/index.js", + "browser": "./mjs/lib/index.browser.js", "sideEffects": true, "typesVersion": { ">=3.1": { diff --git a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts index 082f8aa0d..a48ac8185 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts @@ -17,7 +17,6 @@ import { isEmptyObject, ErrorToastService, getPkgId } from '@start9labs/shared' import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page' import { hasCurrentDeps } from 'src/app/util/has-deps' import { getAllPackages, getManifest } from 'src/app/util/get-package-data' -import { ActionMetadata } from '@start9labs/start-sdk/cjs/sdk/lib/types' import { T } from '@start9labs/start-sdk' @Component({ @@ -43,7 +42,7 @@ export class AppActionsPage { async handleAction( status: T.Status, - action: { key: string; value: ActionMetadata }, + action: { key: string; value: T.ActionMetadata }, ) { if ( status && From 7b8a0114f5d872b437fdba9bef785cdb1de11c32 Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Tue, 23 Apr 2024 14:01:29 -0600 Subject: [PATCH 015/125] fix log response (#2607) --- core/startos/src/logs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/startos/src/logs.rs b/core/startos/src/logs.rs index 7c4ed1c28..d8b7c898f 100644 --- a/core/startos/src/logs.rs +++ b/core/startos/src/logs.rs @@ -242,7 +242,7 @@ pub fn logs() -> ParentHandler { .with_inherited(|params, _| params), ) .root_handler( - from_fn_async(logs_follow) + from_fn_async(logs_nofollow) .with_inherited(|params, _| params) .no_cli(), ) From 3a5ee4a296e1f08edfb0a5cdb9b40f834608405a Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Tue, 23 Apr 2024 14:01:40 -0600 Subject: [PATCH 016/125] kill process by session, and add timeout (#2608) --- .../DockerProcedureContainer.ts | 15 +- .../Systems/SystemForEmbassy/MainLoop.ts | 2 +- .../Systems/SystemForEmbassy/index.ts | 181 ++++++++++++------ .../src/service/service_effect_handler.rs | 1 + sdk/lib/mainFn/Daemons.ts | 29 +-- sdk/lib/util/Overlay.ts | 48 ++++- 6 files changed, 191 insertions(+), 85 deletions(-) diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts index 1a8b54e22..c8388d151 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts @@ -84,10 +84,19 @@ export class DockerProcedureContainer { } } - async execSpawn(commands: string[]) { + async execFail(commands: string[], timeoutMs: number | null) { try { - const spawned = await this.overlay.spawn(commands) - return spawned + const res = await this.overlay.exec(commands, {}, timeoutMs) + if (res.exitCode !== 0) { + const codeOrSignal = + res.exitCode !== null + ? `code ${res.exitCode}` + : `signal ${res.exitSignal}` + throw new Error( + `Process exited with ${codeOrSignal}: ${res.stderr.toString()}`, + ) + } + return res } finally { await this.overlay.destroy() } diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts index c28615fc9..0b4f92002 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts @@ -140,7 +140,7 @@ export class MainLoop { actionProcedure, manifest.volumes, ) - const executed = await container.execSpawn([ + const executed = await container.exec([ actionProcedure.entrypoint, ...actionProcedure.args, JSON.stringify(timeChanged), diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index a76307368..4cd3d1bfe 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -263,37 +263,65 @@ export class SystemForEmbassy implements System { const input = options.input switch (options.procedure) { case "/backup/create": - return this.createBackup(effects) + return this.createBackup(effects, options.timeout || null) case "/backup/restore": - return this.restoreBackup(effects) + return this.restoreBackup(effects, options.timeout || null) case "/config/get": - return this.getConfig(effects) + return this.getConfig(effects, options.timeout || null) case "/config/set": - return this.setConfig(effects, input) + return this.setConfig(effects, input, options.timeout || null) case "/properties": - return this.properties(effects) + return this.properties(effects, options.timeout || null) case "/actions/metadata": return todo() case "/init": - return this.init(effects, string.optional().unsafeCast(input)) + return this.init( + effects, + string.optional().unsafeCast(input), + options.timeout || null, + ) case "/uninit": - return this.uninit(effects, string.optional().unsafeCast(input)) + return this.uninit( + effects, + string.optional().unsafeCast(input), + options.timeout || null, + ) case "/main/start": - return this.mainStart(effects) + return this.mainStart(effects, options.timeout || null) case "/main/stop": - return this.mainStop(effects) + return this.mainStop(effects, options.timeout || null) default: const procedures = unNestPath(options.procedure) switch (true) { case procedures[1] === "actions" && procedures[3] === "get": - return this.action(effects, procedures[2], input) + return this.action( + effects, + procedures[2], + input, + options.timeout || null, + ) case procedures[1] === "actions" && procedures[3] === "run": - return this.action(effects, procedures[2], input) + return this.action( + effects, + procedures[2], + input, + options.timeout || null, + ) case procedures[1] === "dependencies" && procedures[3] === "query": - return this.dependenciesAutoconfig(effects, procedures[2], input) + return this.dependenciesAutoconfig( + effects, + procedures[2], + input, + options.timeout || null, + ) case procedures[1] === "dependencies" && procedures[3] === "update": - return this.dependenciesAutoconfig(effects, procedures[2], input) + return this.dependenciesAutoconfig( + effects, + procedures[2], + input, + options.timeout || null, + ) } } throw new Error(`Could not find the path for ${options.procedure}`) @@ -301,8 +329,10 @@ export class SystemForEmbassy implements System { private async init( effects: HostSystemStartOs, previousVersion: Optional, + timeoutMs: number | null, ): Promise { - if (previousVersion) await this.migration(effects, previousVersion) + if (previousVersion) + await this.migration(effects, previousVersion, timeoutMs) await effects.setMainStatus({ status: "stopped" }) await this.exportActions(effects) } @@ -337,29 +367,36 @@ export class SystemForEmbassy implements System { private async uninit( effects: HostSystemStartOs, nextVersion: Optional, + timeoutMs: number | null, ): Promise { // TODO Do a migration down if the version exists await effects.setMainStatus({ status: "stopped" }) } - private async mainStart(effects: HostSystemStartOs): Promise { + private async mainStart( + effects: HostSystemStartOs, + timeoutMs: number | null, + ): Promise { if (!!this.currentRunning) return this.currentRunning = new MainLoop(this, effects) } private async mainStop( effects: HostSystemStartOs, - options?: { timeout?: number }, + timeoutMs: number | null, ): Promise { const { currentRunning } = this delete this.currentRunning if (currentRunning) { await currentRunning.clean({ - timeout: options?.timeout || this.manifest.main["sigterm-timeout"], + timeout: this.manifest.main["sigterm-timeout"], }) } return duration(this.manifest.main["sigterm-timeout"], "s") } - private async createBackup(effects: HostSystemStartOs): Promise { + private async createBackup( + effects: HostSystemStartOs, + timeoutMs: number | null, + ): Promise { const backup = this.manifest.backup.create if (backup.type === "docker") { const container = await DockerProcedureContainer.of( @@ -367,7 +404,7 @@ export class SystemForEmbassy implements System { backup, this.manifest.volumes, ) - await container.exec([backup.entrypoint, ...backup.args]) + await container.execFail([backup.entrypoint, ...backup.args], timeoutMs) } else { const moduleCode = await this.moduleCode await moduleCode.createBackup?.( @@ -375,7 +412,10 @@ export class SystemForEmbassy implements System { ) } } - private async restoreBackup(effects: HostSystemStartOs): Promise { + private async restoreBackup( + effects: HostSystemStartOs, + timeoutMs: number | null, + ): Promise { const restoreBackup = this.manifest.backup.restore if (restoreBackup.type === "docker") { const container = await DockerProcedureContainer.of( @@ -383,7 +423,10 @@ export class SystemForEmbassy implements System { restoreBackup, this.manifest.volumes, ) - await container.exec([restoreBackup.entrypoint, ...restoreBackup.args]) + await container.execFail( + [restoreBackup.entrypoint, ...restoreBackup.args], + timeoutMs, + ) } else { const moduleCode = await this.moduleCode await moduleCode.restoreBackup?.( @@ -391,11 +434,15 @@ export class SystemForEmbassy implements System { ) } } - private async getConfig(effects: HostSystemStartOs): Promise { - return this.getConfigUncleaned(effects).then(removePointers) + private async getConfig( + effects: HostSystemStartOs, + timeoutMs: number | null, + ): Promise { + return this.getConfigUncleaned(effects, timeoutMs).then(removePointers) } private async getConfigUncleaned( effects: HostSystemStartOs, + timeoutMs: number | null, ): Promise { const config = this.manifest.config?.get if (!config) return { spec: {} } @@ -408,7 +455,10 @@ export class SystemForEmbassy implements System { // TODO: yaml return JSON.parse( ( - await container.exec([config.entrypoint, ...config.args]) + await container.execFail( + [config.entrypoint, ...config.args], + timeoutMs, + ) ).stdout.toString(), ) } else { @@ -427,11 +477,12 @@ export class SystemForEmbassy implements System { private async setConfig( effects: HostSystemStartOs, newConfigWithoutPointers: unknown, + timeoutMs: number | null, ): Promise { const newConfig = structuredClone(newConfigWithoutPointers) await updateConfig( effects, - await this.getConfigUncleaned(effects).then((x) => x.spec), + await this.getConfigUncleaned(effects, timeoutMs).then((x) => x.spec), newConfig, ) const setConfigValue = this.manifest.config?.set @@ -445,11 +496,14 @@ export class SystemForEmbassy implements System { const answer = matchSetResult.unsafeCast( JSON.parse( ( - await container.exec([ - setConfigValue.entrypoint, - ...setConfigValue.args, - JSON.stringify(newConfig), - ]) + await container.execFail( + [ + setConfigValue.entrypoint, + ...setConfigValue.args, + JSON.stringify(newConfig), + ], + timeoutMs, + ) ).stdout.toString(), ), ) @@ -508,6 +562,7 @@ export class SystemForEmbassy implements System { private async migration( effects: HostSystemStartOs, fromVersion: string, + timeoutMs: number | null, ): Promise { const fromEmver = EmVer.from(fromVersion) const currentEmver = EmVer.from(this.manifest.version) @@ -542,11 +597,14 @@ export class SystemForEmbassy implements System { ) return JSON.parse( ( - await container.exec([ - procedure.entrypoint, - ...procedure.args, - JSON.stringify(fromVersion), - ]) + await container.execFail( + [ + procedure.entrypoint, + ...procedure.args, + JSON.stringify(fromVersion), + ], + timeoutMs, + ) ).stdout.toString(), ) } else if (procedure.type === "script") { @@ -568,6 +626,7 @@ export class SystemForEmbassy implements System { } private async properties( effects: HostSystemStartOs, + timeoutMs: number | null, ): Promise> { // TODO BLU-J set the properties ever so often const setConfigValue = this.manifest.properties @@ -581,10 +640,10 @@ export class SystemForEmbassy implements System { const properties = matchProperties.unsafeCast( JSON.parse( ( - await container.exec([ - setConfigValue.entrypoint, - ...setConfigValue.args, - ]) + await container.execFail( + [setConfigValue.entrypoint, ...setConfigValue.args], + timeoutMs, + ) ).stdout.toString(), ), ) @@ -609,6 +668,7 @@ export class SystemForEmbassy implements System { effects: HostSystemStartOs, healthId: string, timeSinceStarted: unknown, + timeoutMs: number | null, ): Promise { const healthProcedure = this.manifest["health-checks"][healthId] if (!healthProcedure) return @@ -620,11 +680,14 @@ export class SystemForEmbassy implements System { ) return JSON.parse( ( - await container.exec([ - healthProcedure.entrypoint, - ...healthProcedure.args, - JSON.stringify(timeSinceStarted), - ]) + await container.execFail( + [ + healthProcedure.entrypoint, + ...healthProcedure.args, + JSON.stringify(timeSinceStarted), + ], + timeoutMs, + ) ).stdout.toString(), ) } else if (healthProcedure.type === "script") { @@ -645,6 +708,7 @@ export class SystemForEmbassy implements System { effects: HostSystemStartOs, actionId: string, formData: unknown, + timeoutMs: number | null, ): Promise { const actionProcedure = this.manifest.actions?.[actionId]?.implementation if (!actionProcedure) return { message: "Action not found", value: null } @@ -656,11 +720,14 @@ export class SystemForEmbassy implements System { ) return JSON.parse( ( - await container.exec([ - actionProcedure.entrypoint, - ...actionProcedure.args, - JSON.stringify(formData), - ]) + await container.execFail( + [ + actionProcedure.entrypoint, + ...actionProcedure.args, + JSON.stringify(formData), + ], + timeoutMs, + ) ).stdout.toString(), ) } else { @@ -681,6 +748,7 @@ export class SystemForEmbassy implements System { effects: HostSystemStartOs, id: string, oldConfig: unknown, + timeoutMs: number | null, ): Promise { const actionProcedure = this.manifest.dependencies?.[id]?.config?.check if (!actionProcedure) return { message: "Action not found", value: null } @@ -692,11 +760,14 @@ export class SystemForEmbassy implements System { ) return JSON.parse( ( - await container.exec([ - actionProcedure.entrypoint, - ...actionProcedure.args, - JSON.stringify(oldConfig), - ]) + await container.execFail( + [ + actionProcedure.entrypoint, + ...actionProcedure.args, + JSON.stringify(oldConfig), + ], + timeoutMs, + ) ).stdout.toString(), ) } else if (actionProcedure.type === "script") { @@ -722,7 +793,9 @@ export class SystemForEmbassy implements System { effects: HostSystemStartOs, id: string, oldConfig: unknown, + timeoutMs: number | null, ): Promise { + // TODO: docker const moduleCode = await this.moduleCode const method = moduleCode.dependencies?.[id]?.autoConfigure if (!method) diff --git a/core/startos/src/service/service_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs index b1411450c..b0d4f4ffd 100644 --- a/core/startos/src/service/service_effect_handler.rs +++ b/core/startos/src/service/service_effect_handler.rs @@ -539,6 +539,7 @@ fn chroot( cmd.env(k, v); } } + nix::unistd::setsid().with_kind(ErrorKind::Lxc)?; // TODO: error code std::os::unix::fs::chroot(path)?; if let Some(uid) = user.as_deref().and_then(|u| u.parse::().ok()) { cmd.uid(uid); diff --git a/sdk/lib/mainFn/Daemons.ts b/sdk/lib/mainFn/Daemons.ts index 640ab8c4b..01fd7f2c7 100644 --- a/sdk/lib/mainFn/Daemons.ts +++ b/sdk/lib/mainFn/Daemons.ts @@ -15,13 +15,6 @@ import * as CP from "node:child_process" const cpExec = promisify(CP.exec) const cpExecFile = promisify(CP.execFile) -async function psTree(pid: number, overlay: Overlay): Promise { - const { stdout } = await cpExec(`pstree -p ${pid}`) - const regex: RegExp = /\((\d+)\)/g - return [...stdout.toString().matchAll(regex)].map(([_all, pid]) => - parseInt(pid), - ) -} type Daemon< Manifest extends SDKManifest, Ids extends string, @@ -81,19 +74,15 @@ export const runDaemon = const pid = childProcess.pid return { async wait() { - const pids = pid ? await psTree(pid, overlay) : [] try { return await answer } finally { - for (const process of pids) { - cpExecFile("kill", [`-9`, String(process)]).catch((_) => {}) - } + await cpExecFile("pkill", ["-9", "-s", String(pid)]).catch((_) => {}) } }, async term({ signal = SIGTERM, timeout = NO_TIMEOUT } = {}) { - const pids = pid ? await psTree(pid, overlay) : [] try { - childProcess.kill(signal) + await cpExecFile("pkill", [`-${signal}`, "-s", String(pid)]) if (timeout > NO_TIMEOUT) { const didTimeout = await Promise.race([ @@ -103,7 +92,9 @@ export const runDaemon = answer.then(() => false), ]) if (didTimeout) { - childProcess.kill(SIGKILL) + await cpExecFile("pkill", [`-9`, "-s", String(pid)]).catch( + (_) => {}, + ) } } else { await answer @@ -111,16 +102,6 @@ export const runDaemon = } finally { await overlay.destroy() } - - try { - for (const process of pids) { - await cpExecFile("kill", [`-${signal}`, String(process)]) - } - } finally { - for (const process of pids) { - cpExecFile("kill", [`-9`, String(process)]).catch((_) => {}) - } - } }, } } diff --git a/sdk/lib/util/Overlay.ts b/sdk/lib/util/Overlay.ts index f5ff0e0d1..50c596801 100644 --- a/sdk/lib/util/Overlay.ts +++ b/sdk/lib/util/Overlay.ts @@ -70,7 +70,13 @@ export class Overlay { async exec( command: string[], options?: CommandOptions, - ): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> { + timeoutMs: number | null = 30000, + ): Promise<{ + exitCode: number | null + exitSignal: NodeJS.Signals | null + stdout: string | Buffer + stderr: string | Buffer + }> { const imageMeta = await fs .readFile(`/media/startos/images/${this.imageId}.json`, { encoding: "utf8", @@ -87,7 +93,7 @@ export class Overlay { workdir = options.cwd delete options.cwd } - return await execFile( + const child = cp.spawn( "start-cli", [ "chroot", @@ -97,8 +103,44 @@ export class Overlay { this.rootfs, ...command, ], - options, + options || {}, ) + const pid = child.pid + const stdout = { data: "" as string | Buffer } + const stderr = { data: "" as string | Buffer } + const appendData = + (appendTo: { data: string | Buffer }) => + (chunk: string | Buffer | any) => { + if (typeof appendTo.data === "string" && typeof chunk === "string") { + appendTo.data += chunk + } else if (typeof chunk === "string" || chunk instanceof Buffer) { + appendTo.data = Buffer.concat([ + Buffer.from(appendTo.data), + Buffer.from(chunk), + ]) + } else { + console.error("received unexpected chunk", chunk) + } + } + return new Promise((resolve, reject) => { + child.on("error", reject) + if (timeoutMs !== null && pid) { + setTimeout( + () => execFile("pkill", ["-9", "-s", String(pid)]).catch((_) => {}), + timeoutMs, + ) + } + child.stdout.on("data", appendData(stdout)) + child.stderr.on("data", appendData(stderr)) + child.on("exit", (code, signal) => + resolve({ + exitCode: code, + exitSignal: signal, + stdout: stdout.data, + stderr: stderr.data, + }), + ) + }) } async spawn( From e08d93b2aaf0ae68fdbc2f6b40ee162568eded0a Mon Sep 17 00:00:00 2001 From: Dominion5254 Date: Thu, 25 Apr 2024 17:16:20 -0600 Subject: [PATCH 017/125] complete export_service_interface and list_service_interfaces fns (#2595) * complete export_service_interface fn * refactor export_service_interface fn * complete list_service_interfaces fn * call insert on model and remove unnecessary code * Refactor export_service_interface Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> * Refactor list_service_interfaces Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> * get ServiceInterfaceId and HostId from params * formatting --------- Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> --- .../src/service/service_effect_handler.rs | 104 +++++++++++++----- 1 file changed, 78 insertions(+), 26 deletions(-) diff --git a/core/startos/src/service/service_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs index b0d4f4ffd..c60f8b230 100644 --- a/core/startos/src/service/service_effect_handler.rs +++ b/core/startos/src/service/service_effect_handler.rs @@ -11,7 +11,9 @@ use clap::Parser; use emver::VersionRange; use imbl::OrdMap; use imbl_value::{json, InternedString}; -use models::{ActionId, DataUrl, HealthCheckId, HostId, ImageId, PackageId, VolumeId}; +use models::{ + ActionId, DataUrl, HealthCheckId, HostId, Id, ImageId, PackageId, ServiceInterfaceId, VolumeId, +}; use patch_db::json_ptr::JsonPointer; use rpc_toolkit::{from_fn, from_fn_async, AnyContext, Context, Empty, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; @@ -26,7 +28,11 @@ use crate::disk::mount::filesystem::idmapped::IdMapped; use crate::disk::mount::filesystem::loop_dev::LoopDev; use crate::disk::mount::filesystem::overlayfs::OverlayGuard; use crate::net::host::binding::BindOptions; -use crate::net::host::HostKind; +use crate::net::host::{self, HostKind}; +use crate::net::service_interface::{ + AddressInfo, ExportedHostInfo, ExportedHostnameInfo, ServiceInterface, ServiceInterfaceType, + ServiceInterfaceWithHostInfo, +}; use crate::prelude::*; use crate::s9pk::merkle_archive::source::http::{HttpReader, HttpSource}; use crate::s9pk::rpc::SKIP_ENV; @@ -191,29 +197,11 @@ struct GetServicePortForwardParams { host_id: HostId, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct AddressInfo { - username: Option, - host_id: String, - bind_options: BindOptions, - suffix: String, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -enum ServiceInterfaceType { - Ui, - P2p, - Api, -} #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] struct ExportServiceInterfaceParams { - id: String, + id: ServiceInterfaceId, name: String, description: String, has_primary: bool, @@ -221,6 +209,8 @@ struct ExportServiceInterfaceParams { masked: bool, address_info: AddressInfo, r#type: ServiceInterfaceType, + host_kind: HostKind, + hostnames: Vec, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] #[ts(export)] @@ -336,9 +326,57 @@ async fn clear_network_interfaces(context: EffectContext, _: Empty) -> Result Result { - todo!() + ExportServiceInterfaceParams { + id, + name, + description, + has_primary, + disabled, + masked, + address_info, + r#type, + host_kind, + hostnames, + }: ExportServiceInterfaceParams, +) -> Result<(), Error> { + let context = context.deref()?; + let package_id = context.id.clone(); + let host_id = address_info.host_id.clone(); + + let service_interface = ServiceInterface { + id: id.clone(), + name, + description, + has_primary, + disabled, + masked, + address_info, + interface_type: r#type, + }; + let host_info = ExportedHostInfo { + id: host_id, + kind: host_kind, + hostnames, + }; + let svc_interface_with_host_info = ServiceInterfaceWithHostInfo { + service_interface, + host_info, + }; + + context + .ctx + .db + .mutate(|db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(&package_id) + .or_not_found(&package_id)? + .as_service_interfaces_mut() + .insert(&id, &svc_interface_with_host_info)?; + Ok(()) + }) + .await?; + Ok(()) } async fn get_primary_url( context: EffectContext, @@ -349,9 +387,23 @@ async fn get_primary_url( async fn list_service_interfaces( context: EffectContext, data: ListServiceInterfacesParams, -) -> Result { - todo!() +) -> Result, Error> { + let context = context.deref()?; + let package_id = context.id.clone(); + + context + .ctx + .db + .peek() + .await + .into_public() + .into_package_data() + .into_idx(&package_id) + .or_not_found(&package_id)? + .into_service_interfaces() + .de() } + async fn remove_address(context: EffectContext, data: RemoveAddressParams) -> Result { todo!() } From 8a38666105b6daf782fea2648f456d24ffaa927f Mon Sep 17 00:00:00 2001 From: Jade <2364004+Blu-J@users.noreply.github.com> Date: Fri, 26 Apr 2024 17:51:33 -0600 Subject: [PATCH 018/125] Feature/sdk040dependencies (#2609) * update registry upload to take id for new admin permissions (#2605) * wip * wip: Get the get dependencies * wip check_dependencies * wip: Get the build working to the vm * wip: Add in the last of the things that where needed for the new sdk * Add fix * wip: implement the changes * wip: Fix the naming --------- Co-authored-by: Lucy <12953208+elvece@users.noreply.github.com> --- .../src/Adapters/HostSystemStartOs.ts | 12 ++ core/Cargo.lock | 4 +- core/models/src/version.rs | 4 +- core/startos/src/registry/admin.rs | 3 + .../src/service/service_effect_handler.rs | 150 ++++++++++++++++++ core/startos/src/status/health_check.rs | 44 +++++ core/startos/src/status/mod.rs | 9 ++ sdk/lib/StartSdk.ts | 10 +- sdk/lib/config/configDependencies.ts | 13 +- sdk/lib/config/setupConfig.ts | 8 +- .../DependencyConfig.ts | 0 sdk/lib/dependencies/dependencies.ts | 115 ++++++++++++++ .../index.ts | 0 .../setupDependencyConfig.ts | 0 sdk/lib/health/HealthCheck.ts | 4 +- sdk/lib/index.browser.ts | 2 +- sdk/lib/index.ts | 2 +- sdk/lib/interfaces/Host.ts | 9 +- sdk/lib/osBindings/CheckDependenciesParam.ts | 4 + sdk/lib/osBindings/CheckDependenciesResult.ts | 11 ++ sdk/lib/osBindings/index.ts | 2 + sdk/lib/test/startosTypeValidation.test.ts | 4 +- sdk/lib/types.ts | 42 +++-- sdk/lib/util/Overlay.ts | 4 +- 24 files changed, 417 insertions(+), 39 deletions(-) rename sdk/lib/{dependencyConfig => dependencies}/DependencyConfig.ts (100%) create mode 100644 sdk/lib/dependencies/dependencies.ts rename sdk/lib/{dependencyConfig => dependencies}/index.ts (100%) rename sdk/lib/{dependencyConfig => dependencies}/setupDependencyConfig.ts (100%) create mode 100644 sdk/lib/osBindings/CheckDependenciesParam.ts create mode 100644 sdk/lib/osBindings/CheckDependenciesResult.ts diff --git a/container-runtime/src/Adapters/HostSystemStartOs.ts b/container-runtime/src/Adapters/HostSystemStartOs.ts index e7db00e07..b745799e1 100644 --- a/container-runtime/src/Adapters/HostSystemStartOs.ts +++ b/container-runtime/src/Adapters/HostSystemStartOs.ts @@ -256,6 +256,18 @@ export class HostSystemStartOs implements Effects { T.Effects["setDependencies"] > } + checkDependencies( + options: Parameters[0], + ): ReturnType { + return this.rpcRound("checkDependencies", options) as ReturnType< + T.Effects["checkDependencies"] + > + } + getDependencies(): ReturnType { + return this.rpcRound("getDependencies", null) as ReturnType< + T.Effects["getDependencies"] + > + } setHealth(...[options]: Parameters) { return this.rpcRound("setHealth", options) as ReturnType< T.Effects["setHealth"] diff --git a/core/Cargo.lock b/core/Cargo.lock index 455272344..a93a77e1e 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -3121,9 +3121,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" diff --git a/core/models/src/version.rs b/core/models/src/version.rs index 1e4798ba1..012f362aa 100644 --- a/core/models/src/version.rs +++ b/core/models/src/version.rs @@ -3,8 +3,10 @@ use std::ops::Deref; use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use ts_rs::TS; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, TS)] +#[ts(type = "string")] pub struct Version { version: emver::Version, string: String, diff --git a/core/startos/src/registry/admin.rs b/core/startos/src/registry/admin.rs index 5fc538602..d416ebdbd 100644 --- a/core/startos/src/registry/admin.rs +++ b/core/startos/src/registry/admin.rs @@ -74,12 +74,14 @@ async fn do_upload( mut url: Url, user: &str, pass: &str, + pkg_id: &str, body: Body, ) -> Result<(), Error> { url.set_path("/admin/v0/upload"); let req = httpc .post(url) .header(header::ACCEPT, "text/plain") + .query(&["id", pkg_id]) .basic_auth(user, Some(pass)) .body(body) .build()?; @@ -198,6 +200,7 @@ pub async fn publish( registry.clone(), &user, &pass, + &pkg.id, Body::wrap_stream(file_stream), ) .await?; diff --git a/core/startos/src/service/service_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs index c60f8b230..0ad45aad1 100644 --- a/core/startos/src/service/service_effect_handler.rs +++ b/core/startos/src/service/service_effect_handler.rs @@ -11,6 +11,7 @@ use clap::Parser; use emver::VersionRange; use imbl::OrdMap; use imbl_value::{json, InternedString}; +use itertools::Itertools; use models::{ ActionId, DataUrl, HealthCheckId, HostId, Id, ImageId, PackageId, ServiceInterfaceId, VolumeId, }; @@ -23,6 +24,7 @@ use url::Url; use crate::db::model::package::{ ActionMetadata, CurrentDependencies, CurrentDependencyInfo, CurrentDependencyKind, + ManifestPreference, }; use crate::disk::mount::filesystem::idmapped::IdMapped; use crate::disk::mount::filesystem::loop_dev::LoopDev; @@ -154,6 +156,18 @@ pub fn service_effect_handler() -> ParentHandler { .no_display() .with_remote_cli::(), ) + .subcommand( + "getDependencies", + from_fn_async(get_dependencies) + .no_display() + .with_remote_cli::(), + ) + .subcommand( + "checkDependencies", + from_fn_async(check_dependencies) + .no_display() + .with_remote_cli::(), + ) .subcommand("getSystemSmtp", from_fn_async(get_system_smtp).no_cli()) .subcommand("getContainerIp", from_fn_async(get_container_ip).no_cli()) .subcommand( @@ -178,6 +192,7 @@ pub fn service_effect_handler() -> ParentHandler { .subcommand("removeAction", from_fn_async(remove_action).no_cli()) .subcommand("reverseProxy", from_fn_async(reverse_proxy).no_cli()) .subcommand("mount", from_fn_async(mount).no_cli()) + // TODO Callbacks } @@ -1270,3 +1285,138 @@ async fn set_dependencies( }) .await } + +async fn get_dependencies(ctx: EffectContext) -> Result, Error> { + let ctx = ctx.deref()?; + let id = &ctx.id; + let db = ctx.ctx.db.peek().await; + let data = db + .as_public() + .as_package_data() + .as_idx(id) + .or_not_found(id)? + .as_current_dependencies() + .de()?; + + data.0 + .into_iter() + .map(|(id, current_dependency_info)| { + let CurrentDependencyInfo { + registry_url, + version_spec, + kind, + .. + } = current_dependency_info; + Ok::<_, Error>(match kind { + CurrentDependencyKind::Exists => DependencyRequirement::Exists { + id, + registry_url, + version_spec, + }, + CurrentDependencyKind::Running { health_checks } => { + DependencyRequirement::Running { + id, + health_checks, + version_spec, + registry_url, + } + } + }) + }) + .try_collect() +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "camelCase")] +#[ts(export)] +struct CheckDependenciesParam { + package_ids: Option>, +} +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +struct CheckDependenciesResult { + package_id: PackageId, + is_installed: bool, + is_running: bool, + health_checks: Vec, + #[ts(type = "string | null")] + version: Option, +} + +async fn check_dependencies( + ctx: EffectContext, + CheckDependenciesParam { package_ids }: CheckDependenciesParam, +) -> Result, Error> { + let ctx = ctx.deref()?; + let db = ctx.ctx.db.peek().await; + let current_dependencies = db + .as_public() + .as_package_data() + .as_idx(&ctx.id) + .or_not_found(&ctx.id)? + .as_current_dependencies() + .de()?; + let package_ids: Vec<_> = package_ids + .unwrap_or_else(|| current_dependencies.0.keys().cloned().collect()) + .into_iter() + .filter_map(|x| { + let info = current_dependencies.0.get(&x)?; + Some((x, info)) + }) + .collect(); + let mut results = Vec::with_capacity(package_ids.len()); + + for (package_id, dependency_info) in package_ids { + let Some(package) = db.as_public().as_package_data().as_idx(&package_id) else { + results.push(CheckDependenciesResult { + package_id, + is_installed: false, + is_running: false, + health_checks: vec![], + version: None, + }); + continue; + }; + let installed_version = package + .as_state_info() + .as_manifest(ManifestPreference::New) + .as_version() + .de()? + .into_version(); + let version = Some(installed_version.clone()); + if !installed_version.satisfies(&dependency_info.version_spec) { + results.push(CheckDependenciesResult { + package_id, + is_installed: false, + is_running: false, + health_checks: vec![], + version, + }); + continue; + } + let is_installed = true; + let status = package.as_status().as_main().de()?; + let is_running = if is_installed { + status.running() + } else { + false + }; + let health_checks = status + .health() + .cloned() + .unwrap_or_default() + .into_iter() + .map(|(_, val)| val) + .collect(); + results.push(CheckDependenciesResult { + package_id, + is_installed, + is_running, + health_checks, + version, + }); + } + Ok(results) +} diff --git a/core/startos/src/status/health_check.rs b/core/startos/src/status/health_check.rs index cd5616527..90b20f8c5 100644 --- a/core/startos/src/status/health_check.rs +++ b/core/startos/src/status/health_check.rs @@ -1,7 +1,12 @@ +use std::str::FromStr; + +use clap::builder::ValueParserFactory; pub use models::HealthCheckId; use serde::{Deserialize, Serialize}; use ts_rs::TS; +use crate::util::clap::FromStrParser; + #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, TS)] #[serde(rename_all = "camelCase")] pub struct HealthCheckResult { @@ -9,6 +14,45 @@ pub struct HealthCheckResult { #[serde(flatten)] pub kind: HealthCheckResultKind, } +// healthCheckName:kind:message OR healthCheckName:kind +impl FromStr for HealthCheckResult { + type Err = color_eyre::eyre::Report; + fn from_str(s: &str) -> Result { + let from_parts = |name: &str, kind: &str, message: Option<&str>| { + let message = message.map(|x| x.to_string()); + let kind = match kind { + "success" => HealthCheckResultKind::Success { message }, + "disabled" => HealthCheckResultKind::Disabled { message }, + "starting" => HealthCheckResultKind::Starting { message }, + "loading" => HealthCheckResultKind::Loading { + message: message.unwrap_or_default(), + }, + "failure" => HealthCheckResultKind::Failure { + message: message.unwrap_or_default(), + }, + _ => return Err(color_eyre::eyre::eyre!("Invalid health check kind")), + }; + Ok(Self { + name: name.to_string(), + kind, + }) + }; + let parts = s.split(':').collect::>(); + match &*parts { + [name, kind, message] => from_parts(name, kind, Some(message)), + [name, kind] => from_parts(name, kind, None), + _ => Err(color_eyre::eyre::eyre!( + "Could not match the shape of the result ${parts:?}" + )), + } + } +} +impl ValueParserFactory for HealthCheckResult { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + FromStrParser::new() + } +} #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, TS)] #[serde(rename_all = "camelCase")] diff --git a/core/startos/src/status/mod.rs b/core/startos/src/status/mod.rs index 645c082f4..09d10fb2d 100644 --- a/core/startos/src/status/mod.rs +++ b/core/startos/src/status/mod.rs @@ -91,4 +91,13 @@ impl MainStatus { }; MainStatus::BackingUp { started, health } } + + pub fn health(&self) -> Option<&OrdMap> { + match self { + MainStatus::Running { health, .. } => Some(health), + MainStatus::BackingUp { health, .. } => Some(health), + MainStatus::Stopped | MainStatus::Stopping { .. } | MainStatus::Restarting => None, + MainStatus::Starting { .. } => None, + } + } } diff --git a/sdk/lib/StartSdk.ts b/sdk/lib/StartSdk.ts index 43dc3ba5f..4d7514d88 100644 --- a/sdk/lib/StartSdk.ts +++ b/sdk/lib/StartSdk.ts @@ -24,7 +24,7 @@ import { ValidIfNoStupidEscape, } from "./types" import * as patterns from "./util/patterns" -import { DependencyConfig, Update } from "./dependencyConfig/DependencyConfig" +import { DependencyConfig, Update } from "./dependencies/DependencyConfig" import { BackupSet, Backups } from "./backup/Backups" import { smtpConfig } from "./config/configConstants" import { Daemons } from "./mainFn/Daemons" @@ -35,7 +35,7 @@ import { List } from "./config/builder/list" import { Migration } from "./inits/migrations/Migration" import { Install, InstallFn } from "./inits/setupInstall" import { setupActions } from "./actions/setupActions" -import { setupDependencyConfig } from "./dependencyConfig/setupDependencyConfig" +import { setupDependencyConfig } from "./dependencies/setupDependencyConfig" import { SetupBackupsParams, setupBackups } from "./backup/setupBackups" import { setupInit } from "./inits/setupInit" import { @@ -77,6 +77,7 @@ import * as T from "./types" import { Checker, EmVer } from "./emverLite/mod" import { ExposedStorePaths } from "./store/setupExposeStore" import { PathBuilder, extractJsonPath, pathBuilder } from "./store/PathBuilder" +import { checkAllDependencies } from "./dependencies/dependencies" // prettier-ignore type AnyNeverCond = @@ -124,6 +125,7 @@ export class StartSdk { } return { + checkAllDependencies, serviceInterface: { getOwn: (effects: E, id: ServiceInterfaceId) => removeConstType()( @@ -284,7 +286,7 @@ export class StartSdk { Type extends Record = ExtractConfigType, >( spec: ConfigType, - write: Save, + write: Save, read: Read, ) => setupConfig(spec, write, read), setupConfigRead: < @@ -301,7 +303,7 @@ export class StartSdk { | Config, never>, >( _configSpec: ConfigSpec, - fn: Save, + fn: Save, ) => fn, setupDependencyConfig: >( config: Config | Config, diff --git a/sdk/lib/config/configDependencies.ts b/sdk/lib/config/configDependencies.ts index be0475b0f..2ab091e18 100644 --- a/sdk/lib/config/configDependencies.ts +++ b/sdk/lib/config/configDependencies.ts @@ -1,9 +1,12 @@ import { SDKManifest } from "../manifest/ManifestTypes" -import { Dependency } from "../types" +import { Dependencies } from "../types" export type ConfigDependencies = { - exists(id: keyof T["dependencies"]): Dependency - running(id: keyof T["dependencies"], healthChecks: string[]): Dependency + exists(id: keyof T["dependencies"]): Dependencies[number] + running( + id: keyof T["dependencies"], + healthChecks: string[], + ): Dependencies[number] } export const configDependenciesSet = < @@ -13,7 +16,7 @@ export const configDependenciesSet = < return { id, kind: "exists", - } as Dependency + } as Dependencies[number] }, running(id: keyof T["dependencies"], healthChecks: string[]) { @@ -21,6 +24,6 @@ export const configDependenciesSet = < id, kind: "running", healthChecks, - } as Dependency + } as Dependencies[number] }, }) diff --git a/sdk/lib/config/setupConfig.ts b/sdk/lib/config/setupConfig.ts index a49e545a8..ba82dbad6 100644 --- a/sdk/lib/config/setupConfig.ts +++ b/sdk/lib/config/setupConfig.ts @@ -11,16 +11,13 @@ export type DependenciesReceipt = void & { } export type Save< - Store, A extends | Record | Config, any> | Config, never>, - Manifest extends SDKManifest, > = (options: { effects: Effects input: ExtractConfigType & Record - dependencies: D.ConfigDependencies }) => Promise<{ dependenciesReceipt: DependenciesReceipt interfacesReceipt: InterfacesReceipt @@ -53,7 +50,7 @@ export function setupConfig< Type extends Record = ExtractConfigType, >( spec: Config | Config, - write: Save, + write: Save, read: Read, ) { const validator = spec.validator @@ -66,9 +63,8 @@ export function setupConfig< await effects.clearBindings() await effects.clearServiceInterfaces() const { restart } = await write({ - input: JSON.parse(JSON.stringify(input)), + input: JSON.parse(JSON.stringify(input)) as any, effects, - dependencies: D.configDependenciesSet(), }) if (restart) { await effects.restart() diff --git a/sdk/lib/dependencyConfig/DependencyConfig.ts b/sdk/lib/dependencies/DependencyConfig.ts similarity index 100% rename from sdk/lib/dependencyConfig/DependencyConfig.ts rename to sdk/lib/dependencies/DependencyConfig.ts diff --git a/sdk/lib/dependencies/dependencies.ts b/sdk/lib/dependencies/dependencies.ts new file mode 100644 index 000000000..c074d2ad7 --- /dev/null +++ b/sdk/lib/dependencies/dependencies.ts @@ -0,0 +1,115 @@ +import { + Effects, + PackageId, + DependencyRequirement, + SetHealth, + CheckDependencyResult, +} from "../types" + +export type CheckAllDependencies = { + notRunning: () => Promise + + notInstalled: () => Promise + + healthErrors: () => Promise<{ [id: string]: SetHealth[] }> + throwIfNotRunning: () => Promise + throwIfNotValid: () => Promise + throwIfNotInstalled: () => Promise + throwIfError: () => Promise + isValid: () => Promise +} +export function checkAllDependencies(effects: Effects): CheckAllDependencies { + const dependenciesPromise = effects.getDependencies() + const resultsPromise = dependenciesPromise.then((dependencies) => + effects.checkDependencies({ + packageIds: dependencies.map((dep) => dep.id), + }), + ) + + const dependenciesByIdPromise = dependenciesPromise.then((d) => + d.reduce( + (acc, dep) => { + acc[dep.id] = dep + return acc + }, + {} as { [id: PackageId]: DependencyRequirement }, + ), + ) + + const healthErrors = async () => { + const results = await resultsPromise + const dependenciesById = await dependenciesByIdPromise + const answer: { [id: PackageId]: SetHealth[] } = {} + for (const result of results) { + const dependency = dependenciesById[result.packageId] + if (!dependency) continue + if (dependency.kind !== "running") continue + + const healthChecks = result.healthChecks + .filter((x) => dependency.healthChecks.includes(x.id)) + .filter((x) => !!x.message) + if (healthChecks.length === 0) continue + answer[result.packageId] = healthChecks + } + return answer + } + const notInstalled = () => + resultsPromise.then((x) => x.filter((x) => !x.isInstalled)) + const notRunning = async () => { + const results = await resultsPromise + const dependenciesById = await dependenciesByIdPromise + return results.filter((x) => { + const dependency = dependenciesById[x.packageId] + if (!dependency) return false + if (dependency.kind !== "running") return false + return !x.isRunning + }) + } + const entries = (x: { [k: string]: B }) => Object.entries(x) + const first = (x: A[]): A | undefined => x[0] + const sinkVoid = (x: A) => void 0 + const throwIfError = () => + healthErrors() + .then(entries) + .then(first) + .then((x) => { + if (!x) return + const [id, healthChecks] = x + if (healthChecks.length > 0) + throw `Package ${id} has the following errors: ${healthChecks.map((x) => x.message).join(", ")}` + }) + const throwIfNotRunning = () => + notRunning().then((results) => { + if (results[0]) + throw new Error(`Package ${results[0].packageId} is not running`) + }) + + const throwIfNotInstalled = () => + notInstalled().then((results) => { + if (results[0]) + throw new Error(`Package ${results[0].packageId} is not installed`) + }) + const throwIfNotValid = async () => + Promise.all([ + throwIfNotRunning(), + throwIfNotInstalled(), + throwIfError(), + ]).then(sinkVoid) + + const isValid = () => + throwIfNotValid().then( + () => true, + () => false, + ) + + return { + notRunning, + notInstalled, + healthErrors, + throwIfNotRunning, + throwIfNotValid, + throwIfNotInstalled, + throwIfError, + isValid, + } +} diff --git a/sdk/lib/dependencyConfig/index.ts b/sdk/lib/dependencies/index.ts similarity index 100% rename from sdk/lib/dependencyConfig/index.ts rename to sdk/lib/dependencies/index.ts diff --git a/sdk/lib/dependencyConfig/setupDependencyConfig.ts b/sdk/lib/dependencies/setupDependencyConfig.ts similarity index 100% rename from sdk/lib/dependencyConfig/setupDependencyConfig.ts rename to sdk/lib/dependencies/setupDependencyConfig.ts diff --git a/sdk/lib/health/HealthCheck.ts b/sdk/lib/health/HealthCheck.ts index e1fbee97b..d7bbc2f44 100644 --- a/sdk/lib/health/HealthCheck.ts +++ b/sdk/lib/health/HealthCheck.ts @@ -7,6 +7,7 @@ import { TriggerInput } from "../trigger/TriggerInput" import { defaultTrigger } from "../trigger/defaultTrigger" import { once } from "../util/once" import { Overlay } from "../util/Overlay" +import { object, unknown } from "ts-matches" export function healthCheck(o: { effects: Effects @@ -66,8 +67,7 @@ export function healthCheck(o: { return {} as HealthReceipt } function asMessage(e: unknown) { - if (typeof e === "object" && e != null && "message" in e) - return String(e.message) + if (object({ message: unknown }).test(e)) return String(e.message) const value = String(e) if (value.length == null) return null return value diff --git a/sdk/lib/index.browser.ts b/sdk/lib/index.browser.ts index ff422a7f2..d7c10093f 100644 --- a/sdk/lib/index.browser.ts +++ b/sdk/lib/index.browser.ts @@ -4,7 +4,7 @@ export { setupExposeStore } from "./store/setupExposeStore" export * as config from "./config" export * as CB from "./config/builder" export * as CT from "./config/configTypes" -export * as dependencyConfig from "./dependencyConfig" +export * as dependencyConfig from "./dependencies" export * as manifest from "./manifest" export * as types from "./types" export * as T from "./types" diff --git a/sdk/lib/index.ts b/sdk/lib/index.ts index ef3d2ccb0..89d147ed1 100644 --- a/sdk/lib/index.ts +++ b/sdk/lib/index.ts @@ -12,7 +12,7 @@ export * as backup from "./backup" export * as config from "./config" export * as CB from "./config/builder" export * as CT from "./config/configTypes" -export * as dependencyConfig from "./dependencyConfig" +export * as dependencyConfig from "./dependencies" export * as daemons from "./mainFn/Daemons" export * as health from "./health" export * as healthFns from "./health/checkFns" diff --git a/sdk/lib/interfaces/Host.ts b/sdk/lib/interfaces/Host.ts index ab64cd7b0..2f1353cc1 100644 --- a/sdk/lib/interfaces/Host.ts +++ b/sdk/lib/interfaces/Host.ts @@ -161,7 +161,7 @@ export class Host { options: BindOptionsByKnownProtocol, protoInfo: KnownProtocols[keyof KnownProtocols], ): AddSslOptions | null { - if ("noAddSsl" in options && options.noAddSsl) return null + if (inObject("noAddSsl", options) && options.noAddSsl) return null if ("withSsl" in protoInfo && protoInfo.withSsl) return { // addXForwardedHeaders: null, @@ -174,6 +174,13 @@ export class Host { } } +function inObject( + key: Key, + obj: any, +): obj is { [K in Key]: unknown } { + return key in obj +} + export class StaticHost extends Host { constructor(options: { effects: Effects; id: string }) { super({ ...options, kind: "static" }) diff --git a/sdk/lib/osBindings/CheckDependenciesParam.ts b/sdk/lib/osBindings/CheckDependenciesParam.ts new file mode 100644 index 000000000..54580a7ff --- /dev/null +++ b/sdk/lib/osBindings/CheckDependenciesParam.ts @@ -0,0 +1,4 @@ +// 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" + +export type CheckDependenciesParam = { packageIds: Array | null } diff --git a/sdk/lib/osBindings/CheckDependenciesResult.ts b/sdk/lib/osBindings/CheckDependenciesResult.ts new file mode 100644 index 000000000..c102c733a --- /dev/null +++ b/sdk/lib/osBindings/CheckDependenciesResult.ts @@ -0,0 +1,11 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HealthCheckResult } from "./HealthCheckResult" +import type { PackageId } from "./PackageId" + +export type CheckDependenciesResult = { + packageId: PackageId + isInstalled: boolean + isRunning: boolean + healthChecks: Array + version: string | null +} diff --git a/sdk/lib/osBindings/index.ts b/sdk/lib/osBindings/index.ts index 67129fba1..215f16b8b 100644 --- a/sdk/lib/osBindings/index.ts +++ b/sdk/lib/osBindings/index.ts @@ -12,6 +12,8 @@ export { BindInfo } from "./BindInfo" export { BindOptions } from "./BindOptions" export { BindParams } from "./BindParams" export { Callback } from "./Callback" +export { CheckDependenciesParam } from "./CheckDependenciesParam" +export { CheckDependenciesResult } from "./CheckDependenciesResult" export { ChrootParams } from "./ChrootParams" export { CreateOverlayedImageParams } from "./CreateOverlayedImageParams" export { CurrentDependencies } from "./CurrentDependencies" diff --git a/sdk/lib/test/startosTypeValidation.test.ts b/sdk/lib/test/startosTypeValidation.test.ts index 0743db4fa..fd11ab5b6 100644 --- a/sdk/lib/test/startosTypeValidation.test.ts +++ b/sdk/lib/test/startosTypeValidation.test.ts @@ -1,5 +1,5 @@ import { Effects } from "../types" -import { ExecuteAction } from ".././osBindings" +import { CheckDependenciesParam, ExecuteAction } from ".././osBindings" import { CreateOverlayedImageParams } from ".././osBindings" import { DestroyOverlayedImageParams } from ".././osBindings" import { BindParams } from ".././osBindings" @@ -64,6 +64,8 @@ describe("startosTypeValidation ", () => { removeAction: {} as RemoveActionParams, reverseProxy: {} as ReverseProxyParams, mount: {} as MountParams, + checkDependencies: {} as CheckDependenciesParam, + getDependencies: undefined, }) typeEquality[0]>( testInput as ExecuteAction, diff --git a/sdk/lib/types.ts b/sdk/lib/types.ts index d33bda79f..96e256766 100644 --- a/sdk/lib/types.ts +++ b/sdk/lib/types.ts @@ -1,6 +1,11 @@ export * as configTypes from "./config/configTypes" -import { HealthCheckId } from "./osBindings" -import { HealthCheckResult } from "./osBindings" + +import { + DependencyRequirement, + SetHealth, + HealthCheckResult, +} from "./osBindings" + import { MainEffects, ServiceInterfaceType, Signals } from "./StartSdk" import { InputSpec } from "./config/configTypes" import { DependenciesReceipt } from "./config/setupConfig" @@ -11,6 +16,7 @@ import { ExposedStorePaths } from "./store/setupExposeStore" import { UrlString } from "./util/getServiceInterface" export * from "./osBindings" export { SDKManifest } from "./manifest/ManifestTypes" +export { HealthReceipt } from "./health/HealthReceipt" export type ExportedAction = (options: { effects: Effects @@ -471,16 +477,22 @@ export type Effects = { algorithm: "ecdsa" | "ed25519" | null }) => Promise - setHealth( - o: HealthCheckResult & { - id: HealthCheckId - }, - ): Promise + setHealth(o: SetHealth): Promise /** Set the dependencies of what the service needs, usually ran during the set config as a best practice */ setDependencies(options: { dependencies: Dependencies }): Promise + + /** Get the list of the dependencies, both the dynamic set by the effect of setDependencies and the end result any required in the manifest */ + getDependencies(): Promise + + /** When one wants to checks the status of several services during the checking of dependencies. The result will include things like the status + * of the service and what the current health checks are. + */ + checkDependencies(options: { + packageIds: PackageId[] | null + }): Promise /** Exists could be useful during the runtime to know if some service exists, option dep */ exists(options: { packageId: PackageId }): Promise /** Exists could be useful during the runtime to know if some service is running, option dep */ @@ -578,13 +590,17 @@ export type KnownError = errorCode: [number, string] | readonly [number, string] } -export type Dependency = { - id: PackageId - versionSpec: string - registryUrl: string -} & ({ kind: "exists" } | { kind: "running"; healthChecks: string[] }) -export type Dependencies = Array +export type Dependencies = Array export type DeepPartial = T extends {} ? { [P in keyof T]?: DeepPartial } : T + +export type CheckDependencyResult = { + packageId: PackageId + isInstalled: boolean + isRunning: boolean + healthChecks: SetHealth[] + version: string | null +} +export type CheckResults = CheckDependencyResult[] diff --git a/sdk/lib/util/Overlay.ts b/sdk/lib/util/Overlay.ts index 50c596801..fecb718d5 100644 --- a/sdk/lib/util/Overlay.ts +++ b/sdk/lib/util/Overlay.ts @@ -77,7 +77,7 @@ export class Overlay { stdout: string | Buffer stderr: string | Buffer }> { - const imageMeta = await fs + const imageMeta: any = await fs .readFile(`/media/startos/images/${this.imageId}.json`, { encoding: "utf8", }) @@ -147,7 +147,7 @@ export class Overlay { command: string[], options?: CommandOptions, ): Promise { - const imageMeta = await fs + const imageMeta: any = await fs .readFile(`/media/startos/images/${this.imageId}.json`, { encoding: "utf8", }) From 9b14d714ca70d709579cba16a2964b94b5338fe8 Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Mon, 6 May 2024 10:20:44 -0600 Subject: [PATCH 019/125] Feature/new registry (#2612) * wip * overhaul boot process * wip: new registry * wip * wip * wip * wip * wip * wip * os registry complete * ui fixes * fixes * fixes * more fixes * fix merkle archive --- .gitignore | 1 - Makefile | 14 +- build/README.md | 107 -- build/RELEASE.md | 76 - build/dpkg-deps/depends | 1 - build/lib/scripts/chroot-and-upgrade | 104 +- build/lib/scripts/embassy-initramfs-module | 98 - build/lib/scripts/grub-probe-eos | 4 +- build/lib/scripts/startos-initramfs-module | 114 ++ build/raspberrypi/make-image.sh | 2 +- core/Cargo.lock | 1675 ++++++++--------- core/build-prod.sh | 2 +- core/build-reg.sh | 42 + core/helpers/Cargo.toml | 2 +- core/install-cli.sh | 2 +- core/models/Cargo.toml | 5 +- core/models/src/errors.rs | 10 +- core/startos/Cargo.toml | 21 +- .../s9pk/merkle_archive/test.txt | 7 + core/startos/src/action.rs | 1 - core/startos/src/auth.rs | 82 +- core/startos/src/backup/backup_bulk.rs | 8 +- core/startos/src/backup/mod.rs | 14 +- core/startos/src/backup/target/cifs.rs | 10 +- core/startos/src/backup/target/mod.rs | 12 +- core/startos/src/bins/mod.rs | 4 + core/startos/src/bins/registry.rs | 86 + core/startos/src/bins/start_cli.rs | 2 +- core/startos/src/bins/start_init.rs | 12 +- core/startos/src/bins/startd.rs | 6 +- core/startos/src/config/mod.rs | 15 +- core/startos/src/context/cli.rs | 62 +- core/startos/src/context/config.rs | 6 +- core/startos/src/context/diagnostic.rs | 9 +- core/startos/src/context/rpc.rs | 91 +- core/startos/src/control.rs | 1 - core/startos/src/core/mod.rs | 1 - core/startos/src/core/rpc_continuations.rs | 75 - core/startos/src/db/mod.rs | 87 +- core/startos/src/db/model/package.rs | 8 +- core/startos/src/db/model/public.rs | 14 +- core/startos/src/db/prelude.rs | 67 +- core/startos/src/dependencies.rs | 8 +- core/startos/src/developer/mod.rs | 5 + core/startos/src/diagnostic.rs | 77 +- core/startos/src/disk/fsck/ext4.rs | 2 +- core/startos/src/disk/main.rs | 2 +- core/startos/src/disk/mod.rs | 19 +- .../src/disk/mount/filesystem/overlayfs.rs | 50 +- core/startos/src/disk/mount/guard.rs | 3 +- core/startos/src/error.rs | 2 +- core/startos/src/init.rs | 9 +- core/startos/src/install/mod.rs | 81 +- core/startos/src/lib.rs | 168 +- core/startos/src/logs.rs | 345 ++-- core/startos/src/lxc/mod.rs | 134 +- core/startos/src/middleware/auth.rs | 6 +- core/startos/src/middleware/cors.rs | 3 +- core/startos/src/middleware/db.rs | 1 - core/startos/src/middleware/diagnostic.rs | 1 - core/startos/src/net/dhcp.rs | 6 +- core/startos/src/net/mod.rs | 8 +- core/startos/src/net/net_controller.rs | 28 +- core/startos/src/net/ssl.rs | 1 - core/startos/src/net/static_server.rs | 29 +- core/startos/src/net/tor.rs | 107 +- core/startos/src/net/vhost.rs | 7 +- core/startos/src/net/wifi.rs | 58 +- core/startos/src/notifications.rs | 12 +- core/startos/src/os_install/mod.rs | 163 +- core/startos/src/progress.rs | 34 +- core/startos/src/properties.rs | 1 - core/startos/src/registry/admin.rs | 427 +++-- core/startos/src/registry/asset.rs | 36 + core/startos/src/registry/auth.rs | 214 +++ core/startos/src/registry/context.rs | 242 +++ core/startos/src/registry/db.rs | 171 ++ core/startos/src/registry/marketplace.rs | 101 - core/startos/src/registry/mod.rs | 126 +- core/startos/src/registry/os/asset/add.rs | 341 ++++ core/startos/src/registry/os/asset/get.rs | 182 ++ core/startos/src/registry/os/asset/mod.rs | 14 + core/startos/src/registry/os/asset/sign.rs | 188 ++ core/startos/src/registry/os/index.rs | 44 + core/startos/src/registry/os/mod.rs | 22 + core/startos/src/registry/os/version/mod.rs | 183 ++ .../startos/src/registry/os/version/signer.rs | 133 ++ core/startos/src/registry/signer.rs | 477 +++++ core/startos/src/rpc_continuations.rs | 142 ++ .../s9pk/merkle_archive/directory_contents.rs | 28 +- .../src/s9pk/merkle_archive/file_contents.rs | 35 +- core/startos/src/s9pk/merkle_archive/hash.rs | 118 +- core/startos/src/s9pk/merkle_archive/mod.rs | 81 +- core/startos/src/s9pk/merkle_archive/sink.rs | 3 + .../src/s9pk/merkle_archive/source/http.rs | 8 +- .../src/s9pk/merkle_archive/source/mod.rs | 98 +- .../source/multi_cursor_file.rs | 11 +- core/startos/src/s9pk/merkle_archive/test.rs | 4 +- core/startos/src/s9pk/rpc.rs | 15 +- core/startos/src/s9pk/v1/docker.rs | 1 - core/startos/src/s9pk/v2/compat.rs | 5 +- core/startos/src/s9pk/v2/mod.rs | 4 +- core/startos/src/service/cli.rs | 8 +- core/startos/src/service/mod.rs | 26 +- .../src/service/persistent_container.rs | 2 +- .../src/service/service_effect_handler.rs | 49 +- core/startos/src/service/service_map.rs | 5 +- core/startos/src/setup.rs | 22 +- core/startos/src/ssh.rs | 12 +- core/startos/src/system.rs | 188 +- core/startos/src/update/latest_information.rs | 23 - core/startos/src/update/mod.rs | 490 +++-- core/startos/src/upload.rs | 110 +- core/startos/src/util/io.rs | 265 ++- core/startos/src/util/mod.rs | 1 + core/startos/src/util/rpc.rs | 72 + core/startos/src/util/serde.rs | 149 +- core/startos/src/version/mod.rs | 18 +- core/startos/src/version/v0_3_5.rs | 5 +- core/startos/src/version/v0_3_5_1.rs | 5 +- core/startos/src/version/v0_3_5_2.rs | 5 +- core/startos/src/version/v0_3_6.rs | 5 +- core/startos/src/volume.rs | 2 +- debian/postinst | 13 +- foo | 1 + image-recipe/build.sh | 2 +- .../usr/lib/startos/scripts/init_resize.sh | 2 +- .../raspberrypi/squashfs/boot/cmdline.txt | 2 +- patch-db | 2 +- sdk/lib/osBindings/AcceptSigners.ts | 7 + .../{UpdateProgress.ts => AddAdminParams.ts} | 2 +- sdk/lib/osBindings/AddAssetParams.ts | 12 + sdk/lib/osBindings/AddVersionParams.ts | 8 + sdk/lib/osBindings/Base64.ts | 3 + sdk/lib/osBindings/Blake3Ed25519Signature.ts | 10 + .../osBindings/Blake3Ed2551SignatureInfo.ts | 9 + sdk/lib/osBindings/ContactInfo.ts | 6 + sdk/lib/osBindings/FullIndex.ts | 5 + sdk/lib/osBindings/GetOsAssetParams.ts | 3 + sdk/lib/osBindings/GetVersionParams.ts | 3 + .../osBindings/ListVersionSignersParams.ts | 3 + sdk/lib/osBindings/OsIndex.ts | 4 + sdk/lib/osBindings/OsVersionInfo.ts | 12 + sdk/lib/osBindings/PackageDataEntry.ts | 2 +- sdk/lib/osBindings/Pem.ts | 3 + sdk/lib/osBindings/Progress.ts | 2 +- sdk/lib/osBindings/RegistryAsset.ts | 4 + sdk/lib/osBindings/RemoveVersionParams.ts | 3 + sdk/lib/osBindings/ServerStatus.ts | 4 +- sdk/lib/osBindings/SignAssetParams.ts | 8 + sdk/lib/osBindings/Signature.ts | 4 + sdk/lib/osBindings/SignatureInfo.ts | 7 + sdk/lib/osBindings/SignerInfo.ts | 9 + sdk/lib/osBindings/SignerKey.ts | 4 + sdk/lib/osBindings/VersionSignerParams.ts | 3 + sdk/lib/osBindings/index.ts | 24 +- .../src/app/app/footer/footer.component.html | 6 +- .../app-show-progress.component.html | 2 +- .../app-show/pipes/to-buttons.pipe.ts | 2 +- .../marketplace-show-controls.component.ts | 2 +- .../install-progress/install-progress.pipe.ts | 5 +- .../ui/src/app/services/api/api.fixures.ts | 6 +- .../ui/src/app/services/api/api.types.ts | 4 +- .../services/api/embassy-live-api.service.ts | 4 +- .../services/api/embassy-mock-api.service.ts | 18 +- .../ui/src/app/services/api/mock-patch.ts | 4 +- .../src/app/services/marketplace.service.ts | 2 +- 167 files changed, 6297 insertions(+), 3190 deletions(-) delete mode 100644 build/README.md delete mode 100644 build/RELEASE.md delete mode 100755 build/lib/scripts/embassy-initramfs-module create mode 100755 build/lib/scripts/startos-initramfs-module create mode 100755 core/build-reg.sh create mode 100644 core/startos/proptest-regressions/s9pk/merkle_archive/test.txt create mode 100644 core/startos/src/bins/registry.rs delete mode 100644 core/startos/src/core/mod.rs delete mode 100644 core/startos/src/core/rpc_continuations.rs create mode 100644 core/startos/src/registry/asset.rs create mode 100644 core/startos/src/registry/auth.rs create mode 100644 core/startos/src/registry/context.rs create mode 100644 core/startos/src/registry/db.rs delete mode 100644 core/startos/src/registry/marketplace.rs create mode 100644 core/startos/src/registry/os/asset/add.rs create mode 100644 core/startos/src/registry/os/asset/get.rs create mode 100644 core/startos/src/registry/os/asset/mod.rs create mode 100644 core/startos/src/registry/os/asset/sign.rs create mode 100644 core/startos/src/registry/os/index.rs create mode 100644 core/startos/src/registry/os/mod.rs create mode 100644 core/startos/src/registry/os/version/mod.rs create mode 100644 core/startos/src/registry/os/version/signer.rs create mode 100644 core/startos/src/registry/signer.rs create mode 100644 core/startos/src/rpc_continuations.rs delete mode 100644 core/startos/src/update/latest_information.rs create mode 100644 core/startos/src/util/rpc.rs create mode 120000 foo create mode 100644 sdk/lib/osBindings/AcceptSigners.ts rename sdk/lib/osBindings/{UpdateProgress.ts => AddAdminParams.ts} (60%) create mode 100644 sdk/lib/osBindings/AddAssetParams.ts create mode 100644 sdk/lib/osBindings/AddVersionParams.ts create mode 100644 sdk/lib/osBindings/Base64.ts create mode 100644 sdk/lib/osBindings/Blake3Ed25519Signature.ts create mode 100644 sdk/lib/osBindings/Blake3Ed2551SignatureInfo.ts create mode 100644 sdk/lib/osBindings/ContactInfo.ts create mode 100644 sdk/lib/osBindings/FullIndex.ts create mode 100644 sdk/lib/osBindings/GetOsAssetParams.ts create mode 100644 sdk/lib/osBindings/GetVersionParams.ts create mode 100644 sdk/lib/osBindings/ListVersionSignersParams.ts create mode 100644 sdk/lib/osBindings/OsIndex.ts create mode 100644 sdk/lib/osBindings/OsVersionInfo.ts create mode 100644 sdk/lib/osBindings/Pem.ts create mode 100644 sdk/lib/osBindings/RegistryAsset.ts create mode 100644 sdk/lib/osBindings/RemoveVersionParams.ts create mode 100644 sdk/lib/osBindings/SignAssetParams.ts create mode 100644 sdk/lib/osBindings/Signature.ts create mode 100644 sdk/lib/osBindings/SignatureInfo.ts create mode 100644 sdk/lib/osBindings/SignerInfo.ts create mode 100644 sdk/lib/osBindings/SignerKey.ts create mode 100644 sdk/lib/osBindings/VersionSignerParams.ts diff --git a/.gitignore b/.gitignore index 1df3692ee..766d876e8 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,6 @@ secrets.db /ENVIRONMENT.txt /GIT_HASH.txt /VERSION.txt -/eos-*.tar.gz /*.deb /target /*.squashfs diff --git a/Makefile b/Makefile index 02328a1d8..dbe10d677 100644 --- a/Makefile +++ b/Makefile @@ -120,7 +120,6 @@ install: $(ALL_TARGETS) $(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/startd) $(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-cli) $(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-sdk) - $(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/embassy-cli) if [ "$(PLATFORM)" = "raspberrypi" ]; then $(call cp,cargo-deps/aarch64-unknown-linux-musl/release/pi-beep,$(DESTDIR)/usr/bin/pi-beep); fi if /bin/bash -c '[[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]'; then $(call cp,cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console,$(DESTDIR)/usr/bin/tokio-console); fi @@ -164,15 +163,16 @@ wormhole-deb: results/$(BASENAME).deb update: $(ALL_TARGETS) @if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi - $(call ssh,"sudo rsync -a --delete --force --info=progress2 /media/embassy/embassyfs/current/ /media/embassy/next/") - $(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/embassy/next PLATFORM=$(PLATFORM) - $(call ssh,'sudo NO_SYNC=1 /media/embassy/next/usr/lib/startos/scripts/chroot-and-upgrade "apt-get install -y $(shell cat ./build/lib/depends)"') + $(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create') + $(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/startos/next PLATFORM=$(PLATFORM) + $(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync "apt-get install -y $(shell cat ./build/lib/depends)"') emulate-reflash: $(ALL_TARGETS) @if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi - $(call ssh,"sudo rsync -a --delete --force --info=progress2 /media/embassy/embassyfs/current/ /media/embassy/next/") - $(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/embassy/next PLATFORM=$(PLATFORM) - $(call ssh,"sudo touch /media/embassy/config/upgrade && sudo rm -f /media/embassy/config/disk.guid && sudo sync && sudo reboot") + $(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create') + $(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/startos/next PLATFORM=$(PLATFORM) + $(call ssh,'sudo rm -f /media/startos/config/disk.guid') + $(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync "apt-get install -y $(shell cat ./build/lib/depends)"') upload-ota: results/$(BASENAME).squashfs TARGET=$(TARGET) KEY=$(KEY) ./upload-ota.sh diff --git a/build/README.md b/build/README.md deleted file mode 100644 index 3bab01866..000000000 --- a/build/README.md +++ /dev/null @@ -1,107 +0,0 @@ -# Building StartOS - -⚠️ The commands given assume a Debian or Ubuntu-based environment. _Building in -a VM is NOT yet supported_ ⚠️ - -## Prerequisites - -1. Install dependencies - -- Avahi - - `sudo apt install -y avahi-daemon` - - Installed by default on most Debian systems - https://avahi.org -- Build Essentials (needed to run `make`) - - `sudo apt install -y build-essential` -- Docker - - `curl -fsSL https://get.docker.com | sh` - - https://docs.docker.com/get-docker - - Add your user to the docker group: `sudo usermod -a -G docker $USER` - - Reload user environment `exec sudo su -l $USER` -- Prepare Docker environment - - Setup buildx (https://docs.docker.com/buildx/working-with-buildx/) - - Create a builder: `docker buildx create --use` - - Add multi-arch build ability: - `docker run --rm --privileged linuxkit/binfmt:v0.8` -- Node Version 12+ - - snap: `sudo snap install node` - - [nvm](https://github.com/nvm-sh/nvm#installing-and-updating): - `nvm install --lts` - - https://nodejs.org/en/docs -- NPM Version 7+ - - apt: `sudo apt install -y npm` - - [nvm](https://github.com/nvm-sh/nvm#installing-and-updating): - `nvm install --lts` - - https://docs.npmjs.com/downloading-and-installing-node-js-and-npm -- jq - - `sudo apt install -y jq` - - https://stedolan.github.io/jq -- yq - - snap: `sudo snap install yq` - - binaries: https://github.com/mikefarah/yq/releases/ - - https://mikefarah.gitbook.io/yq - -2. Clone the latest repo with required submodules - > :information_source: You chan check latest available version - > [here](https://github.com/Start9Labs/start-os/releases) - ``` - git clone --recursive https://github.com/Start9Labs/start-os.git --branch latest - ``` - -## Build Raspberry Pi Image - -``` -cd start-os -make embassyos-raspi.img ARCH=aarch64 -``` - -## Flash - -Flash the resulting `embassyos-raspi.img` to your SD Card - -We recommend [Balena Etcher](https://www.balena.io/etcher/) - -## Setup - -Visit http://start.local from any web browser - We recommend -[Firefox](https://www.mozilla.org/firefox/browsers) - -Enter your product key. This is generated during the build process and can be -found in `product_key.txt`, located in the root directory. - -## Troubleshooting - -1. I just flashed my SD card, fired up StartOS, bootup sounds and all, but my - browser is saying "Unable to connect" with start.local. - -- Try doing a hard refresh on your browser, or opening the url in a - private/incognito window. If you've ran an instance of StartOS before, - sometimes you can have a stale cache that will block you from navigating to - the page. - -2. Flashing the image isn't working with balenaEtcher. I'm getting - `Cannot read property 'message' of null` when I try. - -- The latest versions of Balena may not flash properly. This version here: - https://github.com/balena-io/etcher/releases/tag/v1.5.122 should work - properly. - -3. Startup isn't working properly and I'm curious as to why. How can I view logs - regarding startup for debugging? - -- Find the IP of your device -- Run `nc 8080` and it will print the logs - -4. I need to ssh into my server to fix something, but I cannot get to the - console to add ssh keys normally. - -- During the Build step, instead of running just - `make embassyos-raspi.img ARCH=aarch64` run - `ENVIRONMENT=dev make embassyos-raspi.img ARCH=aarch64`. Flash like normal, - and insert into your server. Boot up StartOS, then on another computer on - the same network, ssh into the the server with the username `start9` password - `embassy`. - -4. I need to reset my password, how can I do that? - -- You will need to reflash your device. Select "Use Existing Drive" once you are - in setup, and it will prompt you to set a new password. diff --git a/build/RELEASE.md b/build/RELEASE.md deleted file mode 100644 index bdbecc00e..000000000 --- a/build/RELEASE.md +++ /dev/null @@ -1,76 +0,0 @@ -# Release Process - -## `embassyos_0.3.x-1_amd64.deb` - -- Description: debian package for x86_64 - intended to be installed on pureos -- Destination: GitHub Release Tag -- Requires: N/A -- Build steps: - - Clone `https://github.com/Start9Labs/embassy-os-deb` at `master` - - Run `make TAG=master` from that folder -- Artifact: `./embassyos_0.3.x-1_amd64.deb` - -## `eos---_amd64.iso` - -- Description: live usb image for x86_64 -- Destination: GitHub Release Tag -- Requires: `embassyos_0.3.x-1_amd64.deb` -- Build steps: - - Clone `https://github.com/Start9Labs/eos-image-recipes` at `master` - - Copy `embassyos_0.3.x-1_amd64.deb` to - `overlays/vendor/root/embassyos_0.3.x-1_amd64.deb` - - Run `./run-local-build.sh byzantium` from that folder -- Artifact: `./results/eos---_amd64.iso` - -## `eos.x86_64.squashfs` - -- Description: compressed embassyOS x86_64 filesystem image -- Destination: GitHub Release Tag, Registry @ - `resources/eos//eos.x86_64.squashfs` -- Requires: `eos---_amd64.iso` -- Build steps: - - From `https://github.com/Start9Labs/eos-image-recipes` at `master` - - `./extract-squashfs.sh results/eos---_amd64.iso` -- Artifact: `./results/eos.x86_64.squashfs` - -## `eos.raspberrypi.squashfs` - -- Description: compressed embassyOS raspberrypi filesystem image -- Destination: GitHub Release Tag, Registry @ - `resources/eos//eos.raspberrypi.squashfs` -- Requires: N/A -- Build steps: - - Clone `https://github.com/Start9Labs/embassy-os` at `master` - - `make embassyos-raspi.img` - - flash `embassyos-raspi.img` to raspberry pi - - boot raspberry pi with ethernet - - wait for chime - - you can watch logs using `nc 8080` - - unplug raspberry pi, put sd card back in build machine - - `./build/raspberry-pi/rip-image.sh` -- Artifact: `./eos.raspberrypi.squashfs` - -## `lite-upgrade.img` - -- Description: update image for users coming from 0.3.2.1 and before -- Destination: Registry @ `resources/eos//eos.img` -- Requires: `eos.raspberrypi.squashfs` -- Build steps: - - From `https://github.com/Start9Labs/embassy-os` at `master` - - `make lite-upgrade.img` -- Artifact `./lite-upgrade.img` - -## `eos---_raspberrypi.tar.gz` - -- Description: pre-initialized raspberrypi image -- Destination: GitHub Release Tag (as tar.gz) -- Requires: `eos.raspberrypi.squashfs` -- Build steps: - - From `https://github.com/Start9Labs/embassy-os` at `master` - - `make eos_raspberrypi.img` - - `tar --format=posix -cS -f- eos---_raspberrypi.img | gzip > eos---_raspberrypi.tar.gz` -- Artifact `./eos---_raspberrypi.tar.gz` - -## `embassy-sdk` - -- Build and deploy to all registries \ No newline at end of file diff --git a/build/dpkg-deps/depends b/build/dpkg-deps/depends index 5209b5421..38d9b1a58 100644 --- a/build/dpkg-deps/depends +++ b/build/dpkg-deps/depends @@ -34,7 +34,6 @@ network-manager nvme-cli nyx openssh-server -podman postgresql psmisc qemu-guest-agent diff --git a/build/lib/scripts/chroot-and-upgrade b/build/lib/scripts/chroot-and-upgrade index a7fafb8bc..ef3208806 100755 --- a/build/lib/scripts/chroot-and-upgrade +++ b/build/lib/scripts/chroot-and-upgrade @@ -5,44 +5,104 @@ if [ "$UID" -ne 0 ]; then exit 1 fi +POSITIONAL_ARGS=() + +while [[ $# -gt 0 ]]; do + case $1 in + --no-sync) + NO_SYNC=1 + shift + ;; + --create) + ONLY_CREATE=1 + shift + ;; + -*|--*) + echo "Unknown option $1" + exit 1 + ;; + *) + POSITIONAL_ARGS+=("$1") # save positional arg + shift # past argument + ;; + esac +done + +set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters + if [ -z "$NO_SYNC" ]; then echo 'Syncing...' - rsync -a --delete --force --info=progress2 /media/embassy/embassyfs/current/ /media/embassy/next + umount -R /media/startos/next 2> /dev/null + rm -rf /media/startos/upper /media/startos/next + mkdir /media/startos/upper + mount -t tmpfs tmpfs /media/startos/upper + mkdir -p /media/startos/upper/data /media/startos/upper/work /media/startos/next + mount -t overlay \ + -olowerdir=/media/startos/current,upperdir=/media/startos/upper/data,workdir=/media/startos/upper/work \ + overlay /media/startos/next fi -mkdir -p /media/embassy/next/run -mkdir -p /media/embassy/next/dev -mkdir -p /media/embassy/next/sys -mkdir -p /media/embassy/next/proc -mkdir -p /media/embassy/next/boot -mount --bind /run /media/embassy/next/run -mount --bind /tmp /media/embassy/next/tmp -mount --bind /dev /media/embassy/next/dev -mount --bind /sys /media/embassy/next/sys -mount --bind /proc /media/embassy/next/proc -mount --bind /boot /media/embassy/next/boot +if [ -n "$ONLY_CREATE" ]; then + exit 0 +fi + +mkdir -p /media/startos/next/run +mkdir -p /media/startos/next/dev +mkdir -p /media/startos/next/sys +mkdir -p /media/startos/next/proc +mkdir -p /media/startos/next/boot +mount --bind /run /media/startos/next/run +mount --bind /tmp /media/startos/next/tmp +mount --bind /dev /media/startos/next/dev +mount --bind /sys /media/startos/next/sys +mount --bind /proc /media/startos/next/proc +mount --bind /boot /media/startos/next/boot if [ -z "$*" ]; then - chroot /media/embassy/next + chroot /media/startos/next CHROOT_RES=$? else - chroot /media/embassy/next "$SHELL" -c "$*" + chroot /media/startos/next "$SHELL" -c "$*" CHROOT_RES=$? fi -umount /media/embassy/next/run -umount /media/embassy/next/tmp -umount /media/embassy/next/dev -umount /media/embassy/next/sys -umount /media/embassy/next/proc -umount /media/embassy/next/boot +umount /media/startos/next/run +umount /media/startos/next/tmp +umount /media/startos/next/dev +umount /media/startos/next/sys +umount /media/startos/next/proc +umount /media/startos/next/boot if [ "$CHROOT_RES" -eq 0 ]; then + + if [ -h /media/startos/config/current.rootfs ] && [ -e /media/startos/config/current.rootfs ]; then + echo 'Pruning...' + current="$(readlink -f /media/startos/config/current.rootfs)" + needed=$(du -s --bytes /media/startos/next | awk '{print $1}') + while [[ "$(df -B1 --output=avail --sync /media/startos/images | tail -n1)" -lt "$needed" ]]; do + to_prune="$(ls -t1 /media/startos/images/*.rootfs | grep -v "$current" | tail -n1)" + if [ -e "$to_prune" ]; then + echo " Pruning $to_prune" + rm -rf "$to_prune" + else + >&2 echo "Not enough space and nothing to prune!" + exit 1 + fi + done + echo 'done.' + fi + echo 'Upgrading...' - touch /media/embassy/config/upgrade + time mksquashfs /media/startos/next /media/startos/images/next.squashfs -b 4096 -comp gzip + hash=$(start-cli util b3sum /media/startos/images/next.squashfs | head -c 32) + mv /media/startos/images/next.squashfs /media/startos/images/${hash}.rootfs + ln -rsf /media/startos/images/${hash}.rootfs /media/startos/config/current.rootfs sync reboot -fi \ No newline at end of file +fi + +umount -R /media/startos/next +rm -rf /media/startos/upper /media/startos/next \ No newline at end of file diff --git a/build/lib/scripts/embassy-initramfs-module b/build/lib/scripts/embassy-initramfs-module deleted file mode 100755 index 2a2f08a07..000000000 --- a/build/lib/scripts/embassy-initramfs-module +++ /dev/null @@ -1,98 +0,0 @@ -# Local filesystem mounting -*- shell-script -*- - -# -# This script overrides local_mount_root() in /scripts/local -# and mounts root as a read-only filesystem with a temporary (rw) -# overlay filesystem. -# - -. /scripts/local - -local_mount_root() -{ - echo 'using embassy initramfs module' - - local_top - local_device_setup "${ROOT}" "root file system" - ROOT="${DEV}" - - # Get the root filesystem type if not set - if [ -z "${ROOTFSTYPE}" ]; then - FSTYPE=$(get_fstype "${ROOT}") - else - FSTYPE=${ROOTFSTYPE} - fi - - local_premount - - # CHANGES TO THE ORIGINAL FUNCTION BEGIN HERE - # N.B. this code still lacks error checking - - modprobe ${FSTYPE} - checkfs ${ROOT} root "${FSTYPE}" - - ROOTFLAGS="$(echo "${ROOTFLAGS}" | sed 's/subvol=\(next\|current\)//' | sed 's/^-o *$//')" - - if [ "${FSTYPE}" != "unknown" ]; then - mount -t ${FSTYPE} ${ROOTFLAGS} ${ROOT} ${rootmnt} - else - mount ${ROOTFLAGS} ${ROOT} ${rootmnt} - fi - - echo 'mounting embassyfs' - - mkdir /embassyfs - - mount --move ${rootmnt} /embassyfs - - if ! [ -d /embassyfs/current ] && [ -d /embassyfs/prev ]; then - mv /embassyfs/prev /embassyfs/current - fi - - if ! [ -d /embassyfs/current ]; then - mkdir /embassyfs/current - for FILE in $(ls /embassyfs); do - if [ "$FILE" != current ]; then - mv /embassyfs/$FILE /embassyfs/current/ - fi - done - fi - - mkdir -p /embassyfs/config - - if [ -f /embassyfs/config/upgrade ] && [ -d /embassyfs/next ]; then - mv /embassyfs/current /embassyfs/prev - mv /embassyfs/next /embassyfs/current - rm /embassyfs/config/upgrade - fi - - if ! [ -d /embassyfs/next ]; then - if [ -d /embassyfs/prev ]; then - mv /embassyfs/prev /embassyfs/next - else - mkdir /embassyfs/next - fi - fi - - mkdir /lower /upper - - mount -r --bind /embassyfs/current /lower - - modprobe overlay || insmod "/lower/lib/modules/$(uname -r)/kernel/fs/overlayfs/overlay.ko" - - # Mount a tmpfs for the overlay in /upper - mount -t tmpfs tmpfs /upper - mkdir /upper/data /upper/work - - # Mount the final overlay-root in $rootmnt - mount -t overlay \ - -olowerdir=/lower,upperdir=/upper/data,workdir=/upper/work \ - overlay ${rootmnt} - - mkdir -p ${rootmnt}/media/embassy/config - mount --bind /embassyfs/config ${rootmnt}/media/embassy/config - mkdir -p ${rootmnt}/media/embassy/next - mount --bind /embassyfs/next ${rootmnt}/media/embassy/next - mkdir -p ${rootmnt}/media/embassy/embassyfs - mount -r --bind /embassyfs ${rootmnt}/media/embassy/embassyfs -} \ No newline at end of file diff --git a/build/lib/scripts/grub-probe-eos b/build/lib/scripts/grub-probe-eos index ed37eefaa..aa2e1cacc 100755 --- a/build/lib/scripts/grub-probe-eos +++ b/build/lib/scripts/grub-probe-eos @@ -3,8 +3,8 @@ ARGS= for ARG in $@; do - if [ -d "/media/embassy/embassyfs" ] && [ "$ARG" = "/" ]; then - ARG=/media/embassy/embassyfs + if [ -d "/media/startos/root" ] && [ "$ARG" = "/" ]; then + ARG=/media/startos/root fi ARGS="$ARGS $ARG" done diff --git a/build/lib/scripts/startos-initramfs-module b/build/lib/scripts/startos-initramfs-module new file mode 100755 index 000000000..e13c887e2 --- /dev/null +++ b/build/lib/scripts/startos-initramfs-module @@ -0,0 +1,114 @@ +# Local filesystem mounting -*- shell-script -*- + +# +# This script overrides local_mount_root() in /scripts/local +# and mounts root as a read-only filesystem with a temporary (rw) +# overlay filesystem. +# + +. /scripts/local + +local_mount_root() +{ + echo 'using startos initramfs module' + + local_top + local_device_setup "${ROOT}" "root file system" + ROOT="${DEV}" + + # Get the root filesystem type if not set + if [ -z "${ROOTFSTYPE}" ]; then + FSTYPE=$(get_fstype "${ROOT}") + else + FSTYPE=${ROOTFSTYPE} + fi + + local_premount + + # CHANGES TO THE ORIGINAL FUNCTION BEGIN HERE + # N.B. this code still lacks error checking + + modprobe ${FSTYPE} + checkfs ${ROOT} root "${FSTYPE}" + + echo 'mounting startos' + mkdir /startos + + ROOTFLAGS="$(echo "${ROOTFLAGS}" | sed 's/subvol=\(next\|current\)//' | sed 's/^-o *$//')" + + if [ "${FSTYPE}" != "unknown" ]; then + mount -t ${FSTYPE} ${ROOTFLAGS} ${ROOT} /startos + else + mount ${ROOTFLAGS} ${ROOT} /startos + fi + + if [ -d /startos/images ]; then + if [ -h /startos/config/current.rootfs ] && [ -e /startos/config/current.rootfs ]; then + image=$(readlink -f /startos/config/current.rootfs) + else + image="$(ls -t1 /startos/images/*.rootfs | head -n1)" + fi + if ! [ -f "$image" ]; then + >&2 echo "image $image not available to boot" + exit 1 + fi + else + if [ -f /startos/config/upgrade ] && [ -d /startos/next ]; then + oldroot=/startos/next + elif [ -d /startos/current ]; then + oldroot=/startos/current + elif [ -d /startos/prev ]; then + oldroot=/startos/prev + else + >&2 echo no StartOS filesystem found + exit 1 + fi + + mkdir -p /startos/config/overlay/etc + mv $oldroot/etc/fstab /startos/config/overlay/etc/fstab + mv $oldroot/etc/machine-id /startos/config/overlay/etc/machine-id + mv $oldroot/etc/ssh /startos/config/overlay/etc/ssh + + mkdir -p /startos/images + mv $oldroot /startos/images/legacy.rootfs + + rm -rf /startos/next /startos/current /startos/prev + + ln -rsf /startos/images/old.squashfs /startos/config/current.rootfs + image=$(readlink -f /startos/config/current.rootfs) + fi + + mkdir /lower /upper + + if [ -d "$image" ]; then + mount -r --bind $image /lower + elif [ -f "$image" ]; then + modprobe squashfs + mount -r $image /lower + else + >&2 echo "not a regular file or directory: $image" + exit 1 + fi + + modprobe overlay || insmod "/lower/lib/modules/$(uname -r)/kernel/fs/overlayfs/overlay.ko" + + # Mount a tmpfs for the overlay in /upper + mount -t tmpfs tmpfs /upper + mkdir /upper/data /upper/work + + mkdir -p /startos/config/overlay + + # Mount the final overlay-root in $rootmnt + mount -t overlay \ + -olowerdir=/startos/config/overlay:/lower,upperdir=/upper/data,workdir=/upper/work \ + overlay ${rootmnt} + + mkdir -p ${rootmnt}/media/startos/config + mount --bind /startos/config ${rootmnt}/media/startos/config + mkdir -p ${rootmnt}/media/startos/images + mount --bind /startos/images ${rootmnt}/media/startos/images + mkdir -p ${rootmnt}/media/startos/root + mount -r --bind /startos ${rootmnt}/media/startos/root + mkdir -p ${rootmnt}/media/startos/current + mount -r --bind /lower ${rootmnt}/media/startos/current +} \ No newline at end of file diff --git a/build/raspberrypi/make-image.sh b/build/raspberrypi/make-image.sh index 3b07cb3a8..ec5ea4297 100755 --- a/build/raspberrypi/make-image.sh +++ b/build/raspberrypi/make-image.sh @@ -63,7 +63,7 @@ sudo unsquashfs -f -d $TMPDIR startos.raspberrypi.squashfs REAL_GIT_HASH=$(cat $TMPDIR/usr/lib/startos/GIT_HASH.txt) REAL_VERSION=$(cat $TMPDIR/usr/lib/startos/VERSION.txt) REAL_ENVIRONMENT=$(cat $TMPDIR/usr/lib/startos/ENVIRONMENT.txt) -sudo sed -i 's| boot=embassy| init=/usr/lib/startos/scripts/init_resize\.sh|' $TMPDIR/boot/cmdline.txt +sudo sed -i 's| boot=startos| init=/usr/lib/startos/scripts/init_resize\.sh|' $TMPDIR/boot/cmdline.txt sudo cp ./build/raspberrypi/fstab $TMPDIR/etc/ sudo cp ./build/raspberrypi/init_resize.sh $TMPDIR/usr/lib/startos/scripts/init_resize.sh sudo umount $TMPDIR/boot diff --git a/core/Cargo.lock b/core/Cargo.lock index a93a77e1e..920390d65 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -38,23 +38,23 @@ dependencies = [ [[package]] name = "ahash" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.11", + "getrandom 0.2.14", "once_cell", "version_check", ] [[package]] name = "ahash" -version = "0.8.6" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom 0.2.11", + "getrandom 0.2.14", "once_cell", "version_check", "zerocopy", @@ -62,9 +62,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -86,9 +86,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "android-tzdata" @@ -107,47 +107,48 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -155,9 +156,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] name = "arrayref" @@ -193,9 +194,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.4" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f658e2baef915ba0f26f1f7c42bfb8e12f532a01f449a090ded75ae7a07e9ba2" +checksum = "4e9eabd7a98fe442131a17c316bd9349c43695e49e730c3c8e12cfb5f4da2693" dependencies = [ "brotli", "flate2", @@ -224,18 +225,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.60", ] [[package]] name = "async-trait" -version = "0.1.74" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.60", ] [[package]] @@ -260,9 +261,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "axum" @@ -275,9 +276,9 @@ dependencies = [ "bitflags 1.3.2", "bytes", "futures-util", - "http 0.2.11", - "http-body 0.4.5", - "hyper 0.14.27", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.28", "itoa", "matchit", "memchr", @@ -300,13 +301,13 @@ checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" dependencies = [ "async-trait", "axum-core 0.4.3", - "base64 0.21.5", + "base64 0.21.7", "bytes", "futures-util", "http 1.1.0", "http-body 1.0.0", "http-body-util", - "hyper 1.2.0", + "hyper 1.3.1", "hyper-util", "itoa", "matchit", @@ -320,7 +321,7 @@ dependencies = [ "serde_path_to_error", "serde_urlencoded", "sha1", - "sync_wrapper 1.0.0", + "sync_wrapper 1.0.1", "tokio", "tokio-tungstenite", "tower", @@ -338,8 +339,8 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http 0.2.11", - "http-body 0.4.5", + "http 0.2.12", + "http-body 0.4.6", "mime", "rustversion", "tower-layer", @@ -378,7 +379,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.0", "http-body-util", - "hyper 1.2.0", + "hyper 1.3.1", "hyper-util", "pin-project-lite", "tokio", @@ -388,9 +389,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -415,15 +416,15 @@ checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" [[package]] name = "base64" -version = "0.13.1" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" -version = "0.21.5" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" @@ -433,9 +434,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "basic-cookies" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb53b6b315f924c7f113b162e53b3901c05fc9966baf84d201dfcc7432a4bb38" +checksum = "67bd8fd42c16bdb08688243dc5f0cc117a3ca9efeeaba3a345a18a6159ad96f7" dependencies = [ "lalrpop", "lalrpop-util", @@ -474,18 +475,18 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" dependencies = [ "serde", ] [[package]] name = "bitmaps" -version = "3.2.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703642b98a00b3b90513279a8ede3fcfa479c126c5fb46e78f3051522f021403" +checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6" [[package]] name = "bitvec" @@ -512,15 +513,17 @@ dependencies = [ [[package]] name = "blake3" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0231f06152bf547e9c2b5194f247cd97aacf6dcd8b15d8e5ec0663f64580da87" +checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", + "memmap2", + "rayon", ] [[package]] @@ -543,9 +546,9 @@ dependencies = [ [[package]] name = "brotli" -version = "3.4.0" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" +checksum = "19483b140a7ac7174d34b5a581b406c64f84da5409d3e09cf4fff604f9270e67" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -554,9 +557,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "2.5.1" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +checksum = "e6221fe77a248b9117d431ad93761222e1cf8ff282d9d1d5d9f53d6299a1cf76" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -564,9 +567,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" @@ -576,18 +579,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cc" -version = "1.0.84" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856" -dependencies = [ - "libc", -] +checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd" [[package]] name = "cfg-if" @@ -597,9 +597,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", @@ -607,7 +607,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] @@ -616,14 +616,14 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" dependencies = [ - "hashbrown 0.14.2", + "hashbrown 0.14.5", ] [[package]] name = "ciborium" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ "ciborium-io", "ciborium-ll", @@ -632,18 +632,18 @@ dependencies = [ [[package]] name = "ciborium-io" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" [[package]] name = "ciborium-ll" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", - "half", + "half 2.4.1", ] [[package]] @@ -665,21 +665,6 @@ dependencies = [ "inout", ] -[[package]] -name = "clap" -version = "3.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" -dependencies = [ - "atty", - "bitflags 1.3.2", - "clap_lex 0.2.4", - "indexmap 1.9.3", - "strsim 0.10.0", - "termcolor", - "textwrap", -] - [[package]] name = "clap" version = "4.5.4" @@ -698,8 +683,8 @@ checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", - "clap_lex 0.7.0", - "strsim 0.11.0", + "clap_lex", + "strsim 0.11.1", ] [[package]] @@ -711,16 +696,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.39", -] - -[[package]] -name = "clap_lex" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", + "syn 2.0.60", ] [[package]] @@ -731,9 +707,9 @@ checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "color-eyre" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204" +checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" dependencies = [ "backtrace", "color-spantrace", @@ -746,9 +722,9 @@ dependencies = [ [[package]] name = "color-spantrace" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ba75b3d9449ecdccb27ecbc479fdc0b87fa2dd43d2f8298f9bf0e59aacc8dce" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" dependencies = [ "once_cell", "owo-colors", @@ -758,30 +734,30 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "concurrent-queue" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] [[package]] name = "console" -version = "0.15.7" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode 0.3.6", "lazy_static", "libc", "unicode-width", - "windows-sys 0.45.0", + "windows-sys 0.52.0", ] [[package]] @@ -823,9 +799,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const_format" @@ -881,9 +857,9 @@ dependencies = [ [[package]] name = "cookie" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd91cf61412820176e137621345ee43b3f4423e589e7ae4e50d601d93e35ef8" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ "time", "version_check", @@ -908,9 +884,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -918,24 +894,24 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crc" -version = "3.0.1" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" dependencies = [ "crc-catalog", ] @@ -948,41 +924,55 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.8" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-queue" -version = "0.3.8" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.16" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" -dependencies = [ - "cfg-if", -] +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crossterm" @@ -990,7 +980,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "crossterm_winapi", "futures-core", "libc", @@ -1018,9 +1008,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-bigint" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28f85c3514d2a6e64160359b45a3918c3b4178bcbf4ae5d03ab2d02e521c479a" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -1089,9 +1079,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.1.1" +version = "4.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" +checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348" dependencies = [ "cfg-if", "cpufeatures", @@ -1112,14 +1102,14 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.60", ] [[package]] name = "darling" -version = "0.20.3" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" dependencies = [ "darling_core", "darling_macro", @@ -1127,40 +1117,40 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.3" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.39", + "syn 2.0.60", ] [[package]] name = "darling_macro" -version = "0.20.3" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", - "syn 2.0.39", + "syn 2.0.60", ] [[package]] name = "data-encoding" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "der" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", "pem-rfc7468", @@ -1169,9 +1159,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.9" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", "serde", @@ -1190,12 +1180,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "diff" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" - [[package]] name = "digest" version = "0.9.0" @@ -1252,30 +1236,30 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "drain" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f1a0abf3fcefad9b4dd0e414207a7408e12b68414a01e6bb19b897d5bd7632d" +checksum = "9d105028bd2b5dfcb33318fd79a445001ead36004dd8dffef1bdd7e493d8bc1e" dependencies = [ "tokio", ] [[package]] name = "dyn-clone" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "ecdsa" -version = "0.16.8" +version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", "digest 0.10.7", "elliptic-curve", "rfc6979", - "signature 2.0.0", + "signature 2.2.0", "spki", ] @@ -1296,7 +1280,7 @@ checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8", "serde", - "signature 2.0.0", + "signature 2.2.0", ] [[package]] @@ -1319,30 +1303,30 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ - "curve25519-dalek 4.1.1", + "curve25519-dalek 4.1.2", "ed25519 2.2.3", "rand_core 0.6.4", "serde", "sha2 0.10.8", - "signature 2.0.0", + "signature 2.2.0", "subtle", "zeroize", ] [[package]] name = "either" -version = "1.9.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" dependencies = [ "serde", ] [[package]] name = "elliptic-curve" -version = "0.13.6" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97ca172ae9dc9f9b779a6e3a65d308f2af74e5b8c921299075bdb4a0370e914" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", @@ -1392,9 +1376,9 @@ checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] @@ -1408,7 +1392,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.60", ] [[package]] @@ -1419,12 +1403,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1446,9 +1430,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "eyre" -version = "0.6.8" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" dependencies = [ "indenter", "once_cell", @@ -1456,9 +1440,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "fd-lock-rs" @@ -1481,20 +1465,20 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.2.3" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f69037fe1b785e84986b4f2cbcf647381876a00671d25ceef715d7812dd7e1dd" +checksum = "38793c55593b33412e3ae40c2c9781ffaa6f438f6f8c10f24e71846fbd7ae01e" [[package]] name = "filetime" -version = "0.2.22" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.3.5", - "windows-sys 0.48.0", + "redox_syscall 0.4.1", + "windows-sys 0.52.0", ] [[package]] @@ -1511,9 +1495,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", @@ -1553,9 +1537,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -1577,9 +1561,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -1592,9 +1576,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -1602,15 +1586,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -1630,38 +1614,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.60", ] [[package]] name = "futures-sink" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -1699,9 +1683,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.11" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if", "libc", @@ -1710,9 +1694,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "gpt" @@ -1720,7 +1704,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8283e7331b8c93b9756e0cfdbcfb90312852f953c6faf9bf741e684cc3b6ad69" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "crc", "log", "uuid", @@ -1739,17 +1723,17 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.21" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", - "http 0.2.11", - "indexmap 1.9.3", + "http 0.2.12", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -1758,9 +1742,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51ee2dd2e4f378392eeff5d51618cd9a63166a2513846bbc55f21cfacd9199d4" +checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069" dependencies = [ "bytes", "fnv", @@ -1768,7 +1752,7 @@ dependencies = [ "futures-sink", "futures-util", "http 1.1.0", - "indexmap 2.1.0", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -1777,9 +1761,19 @@ dependencies = [ [[package]] name = "half" -version = "1.8.2" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] [[package]] name = "hashbrown" @@ -1793,16 +1787,16 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash 0.8.6", + "ahash 0.8.11", ] [[package]] name = "hashbrown" -version = "0.14.2" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash 0.8.6", + "ahash 0.8.11", "allocator-api2", ] @@ -1812,16 +1806,16 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.2", + "hashbrown 0.14.5", ] [[package]] name = "hdrhistogram" -version = "7.5.3" +version = "7.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5b38e5c02b7c7be48c8dc5217c4f1634af2ea221caae2e024bffc7a7651c691" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" dependencies = [ - "base64 0.13.1", + "base64 0.21.7", "byteorder", "flate2", "nom", @@ -1852,7 +1846,7 @@ dependencies = [ "lazy_async_pool", "models", "pin-project", - "rpc-toolkit 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rpc-toolkit", "serde", "serde_json", "tokio", @@ -1871,9 +1865,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -1883,15 +1877,15 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hifijson" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85ef6b41c333e6dd2a4aaa59125a19b633cd17e7aaf372b2260809777bcdef4a" +checksum = "18ae468bcb4dfecf0e4949ee28abbc99076b6a0077f51ddbc94dbfff8e6a870c" [[package]] name = "hkdf" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ "hmac", ] @@ -1907,18 +1901,18 @@ dependencies = [ [[package]] name = "home" -version = "0.5.5" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "http" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", @@ -1938,12 +1932,12 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http 0.2.11", + "http 0.2.12", "pin-project-lite", ] @@ -1990,22 +1984,22 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.27" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", - "h2 0.3.21", - "http 0.2.11", - "http-body 0.4.5", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2", "tokio", "tower-service", "tracing", @@ -2014,14 +2008,14 @@ dependencies = [ [[package]] name = "hyper" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.3", + "h2 0.4.4", "http 1.1.0", "http-body 1.0.0", "httparse", @@ -2030,6 +2024,7 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", ] [[package]] @@ -2038,7 +2033,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper 0.14.27", + "hyper 0.14.28", "pin-project-lite", "tokio", "tokio-io-timeout", @@ -2046,15 +2041,18 @@ dependencies = [ [[package]] name = "hyper-tls" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", - "hyper 0.14.27", + "http-body-util", + "hyper 1.3.1", + "hyper-util", "native-tls", "tokio", "tokio-native-tls", + "tower-service", ] [[package]] @@ -2064,20 +2062,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" dependencies = [ "bytes", + "futures-channel", "futures-util", "http 1.1.0", "http-body 1.0.0", - "hyper 1.2.0", + "hyper 1.3.1", "pin-project-lite", - "socket2 0.5.5", + "socket2", "tokio", + "tower", + "tower-service", + "tracing", ] [[package]] name = "iana-time-zone" -version = "0.1.58" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2131,6 +2133,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "imbl" version = "2.0.3" @@ -2147,9 +2159,9 @@ dependencies = [ [[package]] name = "imbl-sized-chunks" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6957ea0b2541c5ca561d3ef4538044af79f8a05a1eb3a3b148936aaceaa1076" +checksum = "144006fb58ed787dcae3f54575ff4349755b00ccc99f4b4873860b654be1ed63" dependencies = [ "bitmaps", ] @@ -2157,7 +2169,7 @@ dependencies = [ [[package]] name = "imbl-value" version = "0.1.0" -source = "git+https://github.com/Start9Labs/imbl-value.git#929395141c3a882ac366c12ac9402d0ebaa2201b" +source = "git+https://github.com/Start9Labs/imbl-value.git#48dc39a762a3b4f9300d3b9f850cbd394e777ae0" dependencies = [ "imbl", "serde", @@ -2204,20 +2216,20 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.2", + "hashbrown 0.14.5", "serde", ] [[package]] name = "indicatif" -version = "0.17.7" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb28741c9db9a713d93deb3bb9515c20788cef5815265bee4980e87bde7e0f25" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" dependencies = [ "console", "instant", @@ -2276,15 +2288,21 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ - "hermit-abi 0.3.3", - "rustix", - "windows-sys 0.48.0", + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "isocountry" version = "0.3.2" @@ -2333,9 +2351,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jaq-core" @@ -2343,7 +2361,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb52eeac20f256459e909bd4a03bb8c4fab6a1fdbb8ed52d00f644152df48ece" dependencies = [ - "ahash 0.7.7", + "ahash 0.7.8", "dyn-clone", "hifijson", "indexmap 1.9.3", @@ -2377,12 +2395,12 @@ dependencies = [ [[package]] name = "josekit" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5754487a088f527b1407df470db8e654e4064dccbbe1fe850e0773721e9962b7" +checksum = "0953340cf63354cec4a385f1fbcb3f409a5823778cae236078892f6030ed4565" dependencies = [ "anyhow", - "base64 0.21.5", + "base64 0.21.7", "flate2", "once_cell", "openssl", @@ -2395,9 +2413,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.65" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -2435,42 +2453,42 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" dependencies = [ "cpufeatures", ] [[package]] name = "lalrpop" -version = "0.19.12" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a1cbf952127589f2851ab2046af368fd20645491bb4b376f04b7f94d7a9837b" +checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" dependencies = [ "ascii-canvas", "bit-set", - "diff", "ena", - "is-terminal", - "itertools 0.10.5", + "itertools 0.11.0", "lalrpop-util", "petgraph", + "pico-args", "regex", - "regex-syntax 0.6.29", + "regex-syntax 0.8.3", "string_cache", "term", "tiny-keccak", "unicode-xid", + "walkdir", ] [[package]] name = "lalrpop-util" -version = "0.19.12" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3c48237b9604c5a4702de6b824e02006c3214327564636aef27c1028a8fa0ed" +checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" dependencies = [ - "regex", + "regex-automata 0.4.6", ] [[package]] @@ -2500,9 +2518,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.150" +version = "0.2.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" [[package]] name = "libm" @@ -2512,20 +2530,19 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libredox" -version = "0.0.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "libc", - "redox_syscall 0.4.1", ] [[package]] name = "libsqlite3-sys" -version = "0.26.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" dependencies = [ "cc", "pkg-config", @@ -2534,15 +2551,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -2550,9 +2567,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "matchers" @@ -2594,9 +2611,18 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "memmap2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +dependencies = [ + "libc", +] [[package]] name = "memoffset" @@ -2630,18 +2656,18 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.9" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", @@ -2653,7 +2679,8 @@ dependencies = [ name = "models" version = "0.1.0" dependencies = [ - "base64 0.21.5", + "axum 0.7.5", + "base64 0.21.7", "color-eyre", "ed25519-dalek 2.1.1", "emver", @@ -2666,7 +2693,7 @@ dependencies = [ "rand 0.8.5", "regex", "reqwest", - "rpc-toolkit 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rpc-toolkit", "serde", "serde_json", "sqlx", @@ -2699,9 +2726,9 @@ dependencies = [ [[package]] name = "new_debug_unreachable" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "new_mime_guess" @@ -2744,7 +2771,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "cfg-if", "libc", ] @@ -2771,9 +2798,9 @@ dependencies = [ [[package]] name = "num" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +checksum = "3135b08af27d103b0a51f2ae0f8632117b7b185ccf931445affa8df530576a41" dependencies = [ "num-bigint", "num-complex", @@ -2813,28 +2840,33 @@ dependencies = [ [[package]] name = "num-complex" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" dependencies = [ "num-traits", ] [[package]] -name = "num-integer" -version = "0.1.45" +name = "num-conv" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] [[package]] name = "num-iter" -version = "0.1.43" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", @@ -2855,9 +2887,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", @@ -2869,29 +2901,29 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.3", + "hermit-abi 0.3.9", "libc", ] [[package]] name = "num_enum" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683751d591e6d81200c39fb0d1032608b77724f34114db54f571ff1317b337c0" +checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c11e44798ad209ccdd91fc192f0526a369a01234f7373e1b141c96d7cee4f0e" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.60", ] [[package]] @@ -2902,9 +2934,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "object" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] @@ -2917,17 +2949,17 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssh-keys" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75a0ec2d1b302412fb503224289325fcc0e44600176864804c7211b055cfd58" +checksum = "e9939566c441a6542e87c74310d4ac713c17679c76f296932e732f405bc25e3d" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", "byteorder", "md-5", "sha2 0.10.8", @@ -2936,11 +2968,11 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.59" +version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a257ad03cd8fb16ad4172fedf8094451e1af1c4b70097636ef2eac9a5f0cc33" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "cfg-if", "foreign-types", "libc", @@ -2957,7 +2989,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.60", ] [[package]] @@ -2968,18 +3000,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.1.6+3.1.4" +version = "300.2.3+3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439fac53e092cd7442a3660c85dde4643ab3b5bd39040912388dcdabf6b88085" +checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.95" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a4130519a360279579c2053038317e40eff64d13fd3f004f9e1b72b8a6aaf9" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", @@ -2988,12 +3020,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "os_str_bytes" -version = "6.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" - [[package]] name = "overload" version = "0.1.1" @@ -3031,10 +3057,24 @@ dependencies = [ ] [[package]] -name = "parking_lot" -version = "0.12.1" +name = "p521" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primeorder", + "rand_core 0.6.4", + "sha2 0.10.8", +] + +[[package]] +name = "parking_lot" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" dependencies = [ "lock_api", "parking_lot_core", @@ -3042,15 +3082,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.4.1", + "redox_syscall 0.5.1", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] @@ -3074,7 +3114,7 @@ dependencies = [ "nix 0.26.4", "patch-db-macro", "serde", - "serde_cbor 0.11.1", + "serde_cbor", "thiserror", "tokio", "tracing", @@ -3132,7 +3172,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 2.1.0", + "indexmap 2.2.6", ] [[package]] @@ -3145,30 +3185,36 @@ dependencies = [ ] [[package]] -name = "pin-project" -version = "1.1.3" +name = "pico-args" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.60", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -3199,21 +3245,21 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "platforms" -version = "3.2.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14e6ab3f592e6fb464fc9712d8d6e6912de6473954635fd76a589d832cffcbb0" +checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7" [[package]] name = "portable-atomic" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bccab0e7fd7cc19f820a1c8c91720af652d0c88dc9664dd72aef2614f04af3b" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" [[package]] name = "powerfmt" @@ -3249,27 +3295,27 @@ dependencies = [ [[package]] name = "primeorder" -version = "0.13.3" +version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7dbe9ed3b56368bd99483eb32fe9c17fdd3730aebadc906918ce78d54c7eeb4" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" dependencies = [ "elliptic-curve", ] [[package]] name = "proc-macro-crate" -version = "2.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" dependencies = [ - "toml_edit 0.20.7", + "toml_edit 0.21.1", ] [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] @@ -3282,13 +3328,13 @@ checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.4.1", + "bitflags 2.5.0", "lazy_static", "num-traits", "rand 0.8.5", "rand_chacha 0.3.1", "rand_xorshift", - "regex-syntax 0.8.2", + "regex-syntax 0.8.3", "rusty-fork", "tempfile", "unarray", @@ -3307,9 +3353,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.12.1" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fdd22f3b9c31b53c060df4a0613a1c7f062d4115a2b984dd15b1858f7e340d" +checksum = "d0f5d036824e4761737860779c906171497f6d55681139d8312388f8fe398922" dependencies = [ "bytes", "prost-derive", @@ -3317,22 +3363,22 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.12.1" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "265baba7fabd416cf5078179f7d2cbeca4ce7a9041111900675ea7c4cb8a4c32" +checksum = "19de2de2a00075bf566bee3bd4db014b11587e84184d3f7a791bc17f1a8e9e48" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.60", ] [[package]] name = "prost-types" -version = "0.12.1" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e081b29f63d83a4bc75cfc9f3fe424f9156cf92d8a4f0c9407cce9a1b67327cf" +checksum = "3235c33eb02c1f1e212abdbe34c78b264b038fb58ca612664343271e36e55ffe" dependencies = [ "prost", ] @@ -3361,9 +3407,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.33" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -3433,7 +3479,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.11", + "getrandom 0.2.14", ] [[package]] @@ -3463,6 +3509,26 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.1.57" @@ -3478,15 +3544,6 @@ dependencies = [ "bitflags 1.3.2", ] -[[package]] -name = "redox_syscall" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.4.1" @@ -3497,26 +3554,35 @@ dependencies = [ ] [[package]] -name = "redox_users" -version = "0.4.4" +name = "redox_syscall" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" dependencies = [ - "getrandom 0.2.11", + "bitflags 2.5.0", +] + +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom 0.2.14", "libredox", "thiserror", ] [[package]] name = "regex" -version = "1.10.2" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.3", - "regex-syntax 0.8.2", + "regex-automata 0.4.6", + "regex-syntax 0.8.3", ] [[package]] @@ -3530,13 +3596,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.2", + "regex-syntax 0.8.3", ] [[package]] @@ -3547,28 +3613,30 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "reqwest" -version = "0.11.27" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" dependencies = [ - "base64 0.21.5", + "base64 0.22.1", "bytes", "cookie 0.17.0", "cookie_store", "encoding_rs", "futures-core", "futures-util", - "h2 0.3.21", - "http 0.2.11", - "http-body 0.4.5", - "hyper 0.14.27", + "h2 0.4.4", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.3.1", "hyper-tls", + "hyper-util", "ipnet", "js-sys", "log", @@ -3577,7 +3645,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", + "rustls-pemfile 2.1.2", "serde", "serde_json", "serde_urlencoded", @@ -3598,9 +3666,9 @@ dependencies = [ [[package]] name = "reqwest_cookie_store" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba529055ea150e42e4eb9c11dcd380a41025ad4d594b0cb4904ef28b037e1061" +checksum = "93ea5c6f30c19d766efe8d823c88f9abd1c56516648a0d4264ab2dc04cc19472" dependencies = [ "bytes", "cookie_store", @@ -3620,16 +3688,17 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.5" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", - "getrandom 0.2.11", + "cfg-if", + "getrandom 0.2.14", "libc", "spin 0.9.8", "untrusted", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -3646,34 +3715,12 @@ dependencies = [ [[package]] name = "rpc-toolkit" version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c48252a30abb9426a3239fa8dfd2c8dd2647bb24db0b6145db2df04ae53fe647" -dependencies = [ - "clap 3.2.25", - "futures", - "hyper 0.14.27", - "lazy_static", - "openssl", - "reqwest", - "rpc-toolkit-macro 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "serde", - "serde_cbor 0.11.2", - "serde_json", - "thiserror", - "tokio", - "url", - "yajrc", -] - -[[package]] -name = "rpc-toolkit" -version = "0.2.3" -source = "git+https://github.com/Start9Labs/rpc-toolkit.git?branch=refactor/traits#c89e0abdb15dd3bed9adb5339cf0b61a96f32b50" +source = "git+https://github.com/Start9Labs/rpc-toolkit.git?branch=refactor/no-dyn-ctx#f5566840bb9af0743612fede4034f5a390cd1eee" dependencies = [ "async-stream", "async-trait", "axum 0.7.5", - "clap 4.5.4", + "clap", "futures", "http 1.1.0", "http-body-util", @@ -3684,7 +3731,6 @@ dependencies = [ "openssl", "pin-project", "reqwest", - "rpc-toolkit-macro 0.2.2 (git+https://github.com/Start9Labs/rpc-toolkit.git?branch=refactor/traits)", "serde", "serde_json", "thiserror", @@ -3694,54 +3740,11 @@ dependencies = [ "yajrc", ] -[[package]] -name = "rpc-toolkit-macro" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8e4b9cb00baf2d61bcd35e98d67dcb760382a3b4540df7e63b38d053c8a7b8b" -dependencies = [ - "proc-macro2", - "rpc-toolkit-macro-internals 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.109", -] - -[[package]] -name = "rpc-toolkit-macro" -version = "0.2.2" -source = "git+https://github.com/Start9Labs/rpc-toolkit.git?branch=refactor/traits#c89e0abdb15dd3bed9adb5339cf0b61a96f32b50" -dependencies = [ - "proc-macro2", - "rpc-toolkit-macro-internals 0.2.2 (git+https://github.com/Start9Labs/rpc-toolkit.git?branch=refactor/traits)", - "syn 1.0.109", -] - -[[package]] -name = "rpc-toolkit-macro-internals" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e2ce21b936feaecdab9c9a8e75b9dca64374ccc11951a58045ad6559b75f42" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "rpc-toolkit-macro-internals" -version = "0.2.2" -source = "git+https://github.com/Start9Labs/rpc-toolkit.git?branch=refactor/traits#c89e0abdb15dd3bed9adb5339cf0b61a96f32b50" -dependencies = [ - "itertools 0.12.1", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "rsa" -version = "0.9.3" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ef35bf3e7fe15a53c4ab08a998e42271eab13eb0db224126bc7bc4c4bad96d" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" dependencies = [ "const-oid", "digest 0.10.7", @@ -3752,7 +3755,7 @@ dependencies = [ "pkcs8", "rand_core 0.6.4", "sha2 0.10.8", - "signature 2.0.0", + "signature 2.2.0", "spki", "subtle", "zeroize", @@ -3770,11 +3773,11 @@ dependencies = [ [[package]] name = "rust-argon2" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e71971821b3ae0e769e4a4328dbcb517607b434db7697e9aba17203ec14e46a" +checksum = "9d9848531d60c9cbbcf9d166c885316c24bc0e2a9d3eba0956bb6cbbd79bc6e8" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", "blake2b_simd", "constant_time_eq", ] @@ -3796,22 +3799,22 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.21" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "rustls" -version = "0.21.8" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "ring", "rustls-webpki 0.101.7", @@ -3820,14 +3823,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.22.3" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99008d7ad0bbbea527ec27bddbc0e432c5b87d8175178cee68d2eec9c4a1813c" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" dependencies = [ "log", "ring", "rustls-pki-types", - "rustls-webpki 0.102.2", + "rustls-webpki 0.102.3", "subtle", "zeroize", ] @@ -3838,14 +3841,24 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64 0.22.1", + "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" +checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54" [[package]] name = "rustls-webpki" @@ -3859,9 +3872,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.2" +version = "0.102.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" dependencies = [ "ring", "rustls-pki-types", @@ -3870,9 +3883,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" [[package]] name = "rusty-fork" @@ -3904,17 +3917,26 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] [[package]] name = "schannel" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -3949,9 +3971,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.9.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -3962,9 +3984,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" dependencies = [ "core-foundation-sys", "libc", @@ -3972,18 +3994,18 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" dependencies = [ "serde", ] [[package]] name = "serde" -version = "1.0.192" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" dependencies = [ "serde_derive", ] @@ -4001,38 +4023,28 @@ dependencies = [ name = "serde_cbor" version = "0.11.1" dependencies = [ - "half", - "serde", -] - -[[package]] -name = "serde_cbor" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" -dependencies = [ - "half", + "half 1.8.3", "serde", ] [[package]] name = "serde_derive" -version = "1.0.192" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.60", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ - "indexmap 2.1.0", + "indexmap 2.2.6", "itoa", "ryu", "serde", @@ -4040,9 +4052,9 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.14" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" dependencies = [ "itoa", "serde", @@ -4050,9 +4062,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" dependencies = [ "serde", ] @@ -4071,16 +4083,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.4.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" +checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" dependencies = [ - "base64 0.21.5", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.1.0", + "indexmap 2.2.6", "serde", + "serde_derive", "serde_json", "serde_with_macros", "time", @@ -4088,23 +4101,23 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.4.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" +checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.60", ] [[package]] name = "serde_yaml" -version = "0.9.27" +version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cc7a1570e38322cfe4154732e5110f887ea57e22b76f4bfd32b5bdd3368666c" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.1.0", + "indexmap 2.2.6", "itoa", "ryu", "serde", @@ -4194,9 +4207,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -4209,9 +4222,9 @@ checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" [[package]] name = "signature" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fe458c98333f9c8152221191a77e2a44e8325d0193484af2e9421a53019e57d" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest 0.10.7", "rand_core 0.6.4", @@ -4251,22 +4264,12 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.4.10" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", - "winapi", -] - -[[package]] -name = "socket2" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" -dependencies = [ - "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -4286,9 +4289,9 @@ dependencies = [ [[package]] name = "spki" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der", @@ -4296,20 +4299,20 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" +checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" dependencies = [ - "itertools 0.11.0", + "itertools 0.12.1", "nom", "unicode_categories", ] [[package]] name = "sqlx" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e50c216e3624ec8e7ecd14c6a6a6370aad6ee5d8cfc3ab30b5162eeeef2ed33" +checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" dependencies = [ "sqlx-core", "sqlx-macros", @@ -4320,18 +4323,17 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d" +checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" dependencies = [ - "ahash 0.8.6", + "ahash 0.8.11", "atoi", "byteorder", "bytes", "chrono", "crc", "crossbeam-queue", - "dotenvy", "either", "event-listener", "futures-channel", @@ -4341,14 +4343,14 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.1.0", + "indexmap 2.2.6", "log", "memchr", "once_cell", "paste", "percent-encoding", - "rustls 0.21.8", - "rustls-pemfile", + "rustls 0.21.12", + "rustls-pemfile 1.0.4", "serde", "serde_json", "sha2 0.10.8", @@ -4364,9 +4366,9 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a793bb3ba331ec8359c1853bd39eed32cdd7baaf22c35ccf5c92a7e8d1189ec" +checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" dependencies = [ "proc-macro2", "quote", @@ -4377,9 +4379,9 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4ee1e104e00dedb6aa5ffdd1343107b0a4702e862a84320ee7cc74782d96fc" +checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" dependencies = [ "dotenvy", "either", @@ -4403,13 +4405,13 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db" +checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" dependencies = [ "atoi", - "base64 0.21.5", - "bitflags 2.4.1", + "base64 0.21.7", + "bitflags 2.5.0", "byteorder", "bytes", "chrono", @@ -4446,13 +4448,13 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624" +checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" dependencies = [ "atoi", - "base64 0.21.5", - "bitflags 2.4.1", + "base64 0.21.7", + "bitflags 2.5.0", "byteorder", "chrono", "crc", @@ -4474,7 +4476,6 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "sha1", "sha2 0.10.8", "smallvec", "sqlx-core", @@ -4486,9 +4487,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f" +checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" dependencies = [ "atoi", "chrono", @@ -4505,6 +4506,7 @@ dependencies = [ "sqlx-core", "tracing", "url", + "urlencoding", ] [[package]] @@ -4530,7 +4532,7 @@ dependencies = [ "quote", "regex-syntax 0.6.29", "strsim 0.10.0", - "syn 2.0.39", + "syn 2.0.60", "unicode-width", ] @@ -4557,18 +4559,19 @@ dependencies = [ [[package]] name = "ssh-key" -version = "0.6.2" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2180b3bc4955efd5661a97658d3cf4c8107e0d132f619195afe9486c13cca313" +checksum = "ca9b366a80cf18bb6406f4cf4d10aebfb46140a8c0c33f666a144c5c76ecbafc" dependencies = [ "ed25519-dalek 2.1.1", "p256", "p384", + "p521", "rand_core 0.6.4", "rsa", "sec1", "sha2 0.10.8", - "signature 2.0.0", + "signature 2.2.0", "ssh-cipher", "ssh-encoding", "subtle", @@ -4586,18 +4589,18 @@ dependencies = [ "axum 0.7.5", "axum-server", "base32", - "base64 0.21.5", + "base64 0.21.7", "base64ct", "basic-cookies", "blake3", "bytes", "chrono", "ciborium", - "clap 4.5.4", + "clap", "color-eyre", "console", "console-subscriber", - "cookie 0.18.0", + "cookie 0.18.1", "cookie_store", "current_platform", "digest 0.10.7", @@ -4613,11 +4616,12 @@ dependencies = [ "hex", "hmac", "http 1.1.0", + "http-body-util", "id-pool", "imbl", "imbl-value", "include_dir", - "indexmap 2.1.0", + "indexmap 2.2.6", "indicatif", "integer-encoding", "ipnet", @@ -4656,12 +4660,13 @@ dependencies = [ "reqwest", "reqwest_cookie_store", "rpassword", - "rpc-toolkit 0.2.3 (git+https://github.com/Start9Labs/rpc-toolkit.git?branch=refactor/traits)", + "rpc-toolkit", "rust-argon2", "rustyline-async", "semver", "serde", "serde_json", + "serde_urlencoded", "serde_with", "serde_yaml", "sha2 0.10.8", @@ -4680,7 +4685,7 @@ dependencies = [ "tokio-tar", "tokio-tungstenite", "tokio-util", - "toml 0.8.8", + "toml 0.8.12", "torut", "tracing", "tracing-error", @@ -4741,9 +4746,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" @@ -4764,9 +4769,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", @@ -4781,9 +4786,9 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "sync_wrapper" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384595c11a4e2969895cad5a8c4029115f5ab956a9e5ef4de79d11a426e5f20c" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" [[package]] name = "system-configuration" @@ -4820,20 +4825,19 @@ checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" dependencies = [ "filetime", "libc", - "xattr 1.0.1", + "xattr 1.3.1", ] [[package]] name = "tempfile" -version = "3.8.1" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", - "redox_syscall 0.4.1", "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -4856,17 +4860,11 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "textwrap" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" - [[package]] name = "thingbuf" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4706f1bfb859af03f099ada2de3cea3e515843c2d3e93b7893f16d94a37f9415" +checksum = "662b54ef6f7b4e71f683dadc787bbb2d8e8ef2f91b682ebed3164a5a7abca905" dependencies = [ "parking_lot", "pin-project", @@ -4874,22 +4872,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.60", ] [[package]] @@ -4905,9 +4903,9 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", @@ -4915,12 +4913,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.30" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", + "num-conv", "powerfmt", "serde", "time-core", @@ -4935,10 +4934,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.15" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ + "num-conv", "time-core", ] @@ -4980,7 +4980,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.5", + "socket2", "tokio-macros", "tracing", "windows-sys 0.48.0", @@ -5004,7 +5004,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.60", ] [[package]] @@ -5023,7 +5023,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ - "rustls 0.22.3", + "rustls 0.22.4", "rustls-pki-types", "tokio", ] @@ -5042,9 +5042,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", @@ -5108,14 +5108,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.8" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.21.0", + "toml_edit 0.22.12", ] [[package]] @@ -5133,35 +5133,35 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.1.0", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.20.7" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.1.0", + "indexmap 2.2.6", "toml_datetime", - "winnow", + "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.21.0" +version = "0.22.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" dependencies = [ - "indexmap 2.1.0", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.6.7", ] [[package]] @@ -5173,12 +5173,12 @@ dependencies = [ "async-stream", "async-trait", "axum 0.6.20", - "base64 0.21.5", + "base64 0.21.7", "bytes", - "h2 0.3.21", - "http 0.2.11", - "http-body 0.4.5", - "hyper 0.14.27", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.28", "hyper-timeout", "percent-encoding", "pin-project", @@ -5197,7 +5197,7 @@ version = "0.2.1" source = "git+https://github.com/Start9Labs/torut.git?branch=update/dependencies#cc7a1425a01214465e106975e6690794d8551bdb" dependencies = [ "base32", - "base64 0.21.5", + "base64 0.21.7", "derive_more", "ed25519-dalek 1.0.1", "hex", @@ -5262,7 +5262,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.60", ] [[package]] @@ -5337,9 +5337,9 @@ dependencies = [ [[package]] name = "treediff" -version = "4.0.2" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52984d277bdf2a751072b5df30ec0377febdb02f7696d64c2d7d54630bac4303" +checksum = "4d127780145176e2b5d16611cc25a900150e86e9fd79d3bde6ff3a37359c9cb5" dependencies = [ "serde_json", ] @@ -5393,14 +5393,14 @@ dependencies = [ [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "ts-rs" version = "8.1.0" -source = "git+https://github.com/dr-bonez/ts-rs.git?branch=feature/top-level-as#d5f359d803158e06f47977a6f785878a82221d4f" +source = "git+https://github.com/dr-bonez/ts-rs.git?branch=feature/top-level-as#7ae88ade90b5e724159048a663a0bdb04bed27f7" dependencies = [ "thiserror", "ts-rs-macros", @@ -5409,12 +5409,12 @@ dependencies = [ [[package]] name = "ts-rs-macros" version = "8.1.0" -source = "git+https://github.com/dr-bonez/ts-rs.git?branch=feature/top-level-as#d5f359d803158e06f47977a6f785878a82221d4f" +source = "git+https://github.com/dr-bonez/ts-rs.git?branch=feature/top-level-as#7ae88ade90b5e724159048a663a0bdb04bed27f7" dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.60", "termcolor", ] @@ -5440,22 +5440,22 @@ dependencies = [ [[package]] name = "typed-builder" -version = "0.18.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444d8748011b93cb168770e8092458cb0f8854f931ff82fdf6ddfbd72a9c933e" +checksum = "77739c880e00693faef3d65ea3aad725f196da38b22fdc7ea6ded6e1ce4d3add" dependencies = [ "typed-builder-macro", ] [[package]] name = "typed-builder-macro" -version = "0.18.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "563b3b88238ec95680aef36bdece66896eaa7ce3c0f1b4f39d38fb2435261352" +checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.60", ] [[package]] @@ -5481,9 +5481,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" @@ -5493,24 +5493,24 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "unicode-xid" @@ -5526,9 +5526,9 @@ checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" [[package]] name = "unsafe-libyaml" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "untrusted" @@ -5538,12 +5538,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", - "idna 0.4.0", + "idna 0.5.0", "percent-encoding", "serde", ] @@ -5568,11 +5568,11 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.5.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ - "getrandom 0.2.11", + "getrandom 0.2.14", ] [[package]] @@ -5602,6 +5602,16 @@ dependencies = [ "libc", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -5624,10 +5634,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "wasm-bindgen" -version = "0.2.88" +name = "wasite" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -5635,24 +5651,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.88" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.60", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.38" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -5662,9 +5678,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.88" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5672,22 +5688,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.88" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.60", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.88" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wasm-streams" @@ -5704,9 +5720,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.65" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", @@ -5714,18 +5730,19 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.24.0" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b291546d5d9d1eab74f069c77749f2cb8504a12caa20f0f2de93ddbf6f411888" -dependencies = [ - "rustls-webpki 0.101.7", -] +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "whoami" -version = "1.4.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" +checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +dependencies = [ + "redox_syscall 0.4.1", + "wasite", +] [[package]] name = "winapi" @@ -5745,11 +5762,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -5760,20 +5777,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.51.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", + "windows-targets 0.52.5", ] [[package]] @@ -5791,22 +5799,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows-targets 0.52.5", ] [[package]] @@ -5826,25 +5819,20 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -5853,15 +5841,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -5871,15 +5853,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -5889,15 +5865,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" [[package]] -name = "windows_i686_msvc" -version = "0.42.2" +name = "windows_i686_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -5907,15 +5883,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -5925,15 +5895,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -5943,15 +5907,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -5961,24 +5919,33 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.5.19" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b9415ee827af173ebb3f15f9083df5a122eb93572ec28741fb153356ea2578" dependencies = [ "memchr", ] [[package]] name = "winreg" -version = "0.50.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" dependencies = [ "cfg-if", "windows-sys 0.48.0", @@ -6004,11 +5971,13 @@ dependencies = [ [[package]] name = "xattr" -version = "1.0.1" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4686009f71ff3e5c4dbcf1a282d0a44db3f021ba69350cd42086b3e5f1c6985" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" dependencies = [ "libc", + "linux-raw-sys", + "rustix", ] [[package]] @@ -6029,7 +5998,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f355ab62ebe30b758c1f4ab096a306722c4b7dbfb9d8c07d18c70d71a945588" dependencies = [ - "ahash 0.8.6", + "ahash 0.8.11", "hashbrown 0.13.2", "lazy_static", "serde", @@ -6037,29 +6006,29 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.25" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.25" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.60", ] [[package]] name = "zeroize" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" dependencies = [ "zeroize_derive", ] @@ -6072,5 +6041,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.60", ] diff --git a/core/build-prod.sh b/core/build-prod.sh index 8b6184942..f81ddb093 100755 --- a/core/build-prod.sh +++ b/core/build-prod.sh @@ -28,7 +28,7 @@ set +e fail= echo "FEATURES=\"$FEATURES\"" echo "RUSTFLAGS=\"$RUSTFLAGS\"" -if ! rust-musl-builder sh -c "(cd core && cargo build --release $(if [ -n "$FEATURES" ]; then echo "--features $FEATURES"; fi) --locked --bin startbox --target=$ARCH-unknown-linux-musl)"; then +if ! rust-musl-builder sh -c "(cd core && cargo build --release --no-default-features --features cli,daemon,$FEATURES --locked --bin startbox --target=$ARCH-unknown-linux-musl)"; then fail=true fi if ! rust-musl-builder sh -c "(cd core && cargo build --release --no-default-features --features container-runtime,$FEATURES --locked --bin containerbox --target=$ARCH-unknown-linux-musl)"; then diff --git a/core/build-reg.sh b/core/build-reg.sh new file mode 100755 index 000000000..9928b6714 --- /dev/null +++ b/core/build-reg.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +cd "$(dirname "${BASH_SOURCE[0]}")" + +set -e +shopt -s expand_aliases + +if [ -z "$ARCH" ]; then + ARCH=$(uname -m) +fi + +USE_TTY= +if tty -s; then + USE_TTY="-it" +fi + +cd .. +FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')" +RUSTFLAGS="" + +if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then + RUSTFLAGS="--cfg tokio_unstable" +fi + +alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl' + +set +e +fail= +echo "FEATURES=\"$FEATURES\"" +echo "RUSTFLAGS=\"$RUSTFLAGS\"" +if ! rust-musl-builder sh -c "(cd core && cargo build --release --no-default-features --features cli,registry,$FEATURES --locked --bin registrybox --target=$ARCH-unknown-linux-musl)"; then + fail=true +fi +set -e +cd core + +sudo chown -R $USER target +sudo chown -R $USER ~/.cargo + +if [ -n "$fail" ]; then + exit 1 +fi diff --git a/core/helpers/Cargo.toml b/core/helpers/Cargo.toml index 228f3ef54..9af19018e 100644 --- a/core/helpers/Cargo.toml +++ b/core/helpers/Cargo.toml @@ -11,7 +11,7 @@ futures = "0.3.28" lazy_async_pool = "0.3.3" models = { path = "../models" } pin-project = "1.1.3" -rpc-toolkit = "0.2.3" +rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", branch = "refactor/no-dyn-ctx" } serde = { version = "1.0", features = ["derive", "rc"] } serde_json = "1.0" tokio = { version = "1", features = ["full"] } diff --git a/core/install-cli.sh b/core/install-cli.sh index f4fe712ee..620600d92 100755 --- a/core/install-cli.sh +++ b/core/install-cli.sh @@ -12,4 +12,4 @@ if [ -z "$PLATFORM" ]; then export PLATFORM=$(uname -m) fi -cargo install --path=./startos --no-default-features --features=cli,docker --bin start-cli --locked +cargo install --path=./startos --no-default-features --features=cli,docker,registry --bin start-cli --locked diff --git a/core/models/Cargo.toml b/core/models/Cargo.toml index 3611f45d5..76c66b4f2 100644 --- a/core/models/Cargo.toml +++ b/core/models/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +axum = "0.7.5" base64 = "0.21.4" color-eyre = "0.6.2" ed25519-dalek = { version = "2.0.0", features = ["serde"] } @@ -22,8 +23,8 @@ patch-db = { version = "*", path = "../../patch-db/patch-db", features = [ ] } rand = "0.8.5" regex = "1.10.2" -reqwest = "0.11.22" -rpc-toolkit = "0.2.2" +reqwest = "0.12" +rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", branch = "refactor/no-dyn-ctx" } serde = { version = "1.0", features = ["derive", "rc"] } serde_json = "1.0" sqlx = { version = "0.7.2", features = [ diff --git a/core/models/src/errors.rs b/core/models/src/errors.rs index 2362b6dba..340fd15ca 100644 --- a/core/models/src/errors.rs +++ b/core/models/src/errors.rs @@ -1,9 +1,10 @@ use std::fmt::{Debug, Display}; +use axum::http::uri::InvalidUri; +use axum::http::StatusCode; use color_eyre::eyre::eyre; use num_enum::TryFromPrimitive; use patch_db::Revision; -use rpc_toolkit::hyper::http::uri::InvalidUri; use rpc_toolkit::reqwest; use rpc_toolkit::yajrc::{ RpcError, INVALID_PARAMS_ERROR, INVALID_REQUEST_ERROR, METHOD_NOT_FOUND_ERROR, PARSE_ERROR, @@ -207,6 +208,13 @@ impl Error { } } } +impl axum::response::IntoResponse for Error { + fn into_response(self) -> axum::response::Response { + let mut res = axum::Json(RpcError::from(self)).into_response(); + *res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; + res + } +} impl From for Error { fn from(value: std::convert::Infallible) -> Self { match value {} diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index cded2ab41..d88b51f96 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -22,7 +22,7 @@ name = "startos" path = "src/lib.rs" [[bin]] -name = "containerbox" +name = "startbox" path = "src/main.rs" [[bin]] @@ -30,14 +30,19 @@ name = "start-cli" path = "src/main.rs" [[bin]] -name = "startbox" +name = "containerbox" +path = "src/main.rs" + +[[bin]] +name = "registrybox" path = "src/main.rs" [features] cli = [] container-runtime = [] daemon = [] -default = ["cli", "daemon"] +registry = [] +default = ["cli", "daemon", "registry"] dev = [] unstable = ["console-subscriber", "tokio/tracing"] docker = [] @@ -58,7 +63,7 @@ base32 = "0.4.0" base64 = "0.21.4" base64ct = "1.6.0" basic-cookies = "0.1.4" -blake3 = "1.5.0" +blake3 = { version = "1.5.0", features = ["mmap", "rayon"] } bytes = "1" chrono = { version = "0.4.31", features = ["serde"] } clap = "4.4.12" @@ -89,6 +94,7 @@ helpers = { path = "../helpers" } hex = "0.4.3" hmac = "0.12.1" http = "1.0.0" +http-body-util = "0.1" id-pool = { version = "0.2.2", default-features = false, features = [ "serde", "u16", @@ -134,10 +140,10 @@ proptest = "1.3.1" proptest-derive = "0.4.0" rand = { version = "0.8.5", features = ["std"] } regex = "1.10.2" -reqwest = { version = "0.11.23", features = ["stream", "json", "socks"] } -reqwest_cookie_store = "0.6.0" +reqwest = { version = "0.12.4", features = ["stream", "json", "socks"] } +reqwest_cookie_store = "0.7.0" rpassword = "7.2.0" -rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", branch = "refactor/traits" } +rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", branch = "refactor/no-dyn-ctx" } rust-argon2 = "2.0.0" rustyline-async = "0.4.1" semver = { version = "1.0.20", features = ["serde"] } @@ -145,6 +151,7 @@ serde = { version = "1.0", features = ["derive", "rc"] } serde_cbor = { package = "ciborium", version = "0.2.1" } serde_json = "1.0" serde_toml = { package = "toml", version = "0.8.2" } +serde_urlencoded = "0.7" serde_with = { version = "3.4.0", features = ["macros", "json"] } serde_yaml = "0.9.25" sha2 = "0.10.2" diff --git a/core/startos/proptest-regressions/s9pk/merkle_archive/test.txt b/core/startos/proptest-regressions/s9pk/merkle_archive/test.txt new file mode 100644 index 000000000..116de6aba --- /dev/null +++ b/core/startos/proptest-regressions/s9pk/merkle_archive/test.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc dbb4790c31f9e400ed29a9ba2dbd61e3c55ce8a3fbae16601ca3512e803020ed # shrinks to files = [] diff --git a/core/startos/src/action.rs b/core/startos/src/action.rs index 396e7ed50..87ff317f8 100644 --- a/core/startos/src/action.rs +++ b/core/startos/src/action.rs @@ -1,7 +1,6 @@ use clap::Parser; pub use models::ActionId; use models::PackageId; -use rpc_toolkit::command; use serde::{Deserialize, Serialize}; use tracing::instrument; use ts_rs::TS; diff --git a/core/startos/src/auth.rs b/core/startos/src/auth.rs index 915ac10bd..4838f2ea2 100644 --- a/core/startos/src/auth.rs +++ b/core/startos/src/auth.rs @@ -4,9 +4,10 @@ use chrono::{DateTime, Utc}; use clap::Parser; use color_eyre::eyre::eyre; use imbl_value::{json, InternedString}; +use itertools::Itertools; use josekit::jwk::Jwk; use rpc_toolkit::yajrc::RpcError; -use rpc_toolkit::{command, from_fn_async, AnyContext, CallRemote, HandlerExt, ParentHandler}; +use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use tracing::instrument; use ts_rs::TS; @@ -82,7 +83,7 @@ impl std::str::FromStr for PasswordType { }) } } -pub fn auth() -> ParentHandler { +pub fn auth() -> ParentHandler { ParentHandler::new() .subcommand( "login", @@ -94,11 +95,11 @@ pub fn auth() -> ParentHandler { .subcommand( "logout", from_fn_async(logout) - .with_metadata("get-session", Value::Bool(true)) - .with_remote_cli::() - .no_display(), + .with_metadata("get_session", Value::Bool(true)) + .no_display() + .with_call_remote::(), ) - .subcommand("session", session()) + .subcommand("session", session::()) .subcommand( "reset-password", from_fn_async(reset_password_impl).no_cli(), @@ -112,7 +113,7 @@ pub fn auth() -> ParentHandler { from_fn_async(get_pubkey) .with_metadata("authenticated", Value::Bool(false)) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) } @@ -128,26 +129,20 @@ fn gen_pwd() { .unwrap() ) } -#[derive(Deserialize, Serialize, Parser)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "kebab-case")] -pub struct CliLoginParams { - password: Option, -} #[instrument(skip_all)] async fn cli_login( - ctx: CliContext, - CliLoginParams { password }: CliLoginParams, + HandlerArgs { + context: ctx, + parent_method, + method, + .. + }: HandlerArgs, ) -> Result<(), RpcError> { - let password = if let Some(password) = password { - password.decrypt(&ctx)? - } else { - rpassword::prompt_password("Password: ")? - }; + let password = rpassword::prompt_password("Password: ")?; - ctx.call_remote( - "auth.login", + ctx.call_remote::( + &parent_method.into_iter().chain(method).join("."), json!({ "password": password, "metadata": { @@ -185,7 +180,8 @@ pub fn check_password_against_db(db: &DatabaseModel, password: &str) -> Result<( #[command(rename_all = "kebab-case")] pub struct LoginParams { password: Option, - #[serde(default)] + #[ts(skip)] + #[serde(rename = "__auth_userAgent")] // from Auth middleware user_agent: Option, #[serde(default)] #[ts(type = "any")] @@ -226,7 +222,8 @@ pub async fn login_impl( #[serde(rename_all = "camelCase")] #[command(rename_all = "kebab-case")] pub struct LogoutParams { - #[ts(type = "string")] + #[ts(skip)] + #[serde(rename = "__auth_session")] // from Auth middleware session: InternedString, } @@ -262,23 +259,23 @@ pub struct SessionList { sessions: Sessions, } -pub fn session() -> ParentHandler { +pub fn session() -> ParentHandler { ParentHandler::new() .subcommand( "list", from_fn_async(list) - .with_metadata("get-session", Value::Bool(true)) + .with_metadata("get_session", Value::Bool(true)) .with_display_serializable() - .with_custom_display_fn::(|handle, result| { + .with_custom_display_fn(|handle, result| { Ok(display_sessions(handle.params, result)) }) - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "kill", from_fn_async(kill) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) } @@ -374,21 +371,16 @@ pub struct ResetPasswordParams { #[instrument(skip_all)] async fn cli_reset_password( - ctx: CliContext, - ResetPasswordParams { - old_password, - new_password, - }: ResetPasswordParams, + HandlerArgs { + context: ctx, + parent_method, + method, + .. + }: HandlerArgs, ) -> Result<(), RpcError> { - let old_password = if let Some(old_password) = old_password { - old_password.decrypt(&ctx)? - } else { - rpassword::prompt_password("Current Password: ")? - }; + let old_password = rpassword::prompt_password("Current Password: ")?; - let new_password = if let Some(new_password) = new_password { - new_password.decrypt(&ctx)? - } else { + let new_password = { let new_password = rpassword::prompt_password("New Password: ")?; if new_password != rpassword::prompt_password("Confirm: ")? { return Err(Error::new( @@ -400,8 +392,8 @@ async fn cli_reset_password( new_password }; - ctx.call_remote( - "auth.reset-password", + ctx.call_remote::( + &parent_method.into_iter().chain(method).join("."), imbl_value::json!({ "old-password": old_password, "new-password": new_password }), ) .await?; @@ -447,7 +439,7 @@ pub async fn reset_password_impl( #[instrument(skip_all)] pub async fn get_pubkey(ctx: RpcContext) -> Result { - let secret = ctx.as_ref().clone(); + let secret = >::as_ref(&ctx).clone(); let pub_key = secret.to_public_key()?; Ok(pub_key) } diff --git a/core/startos/src/backup/backup_bulk.rs b/core/startos/src/backup/backup_bulk.rs index 928e4811a..b72ce7b7b 100644 --- a/core/startos/src/backup/backup_bulk.rs +++ b/core/startos/src/backup/backup_bulk.rs @@ -20,7 +20,7 @@ use crate::backup::os::OsBackup; use crate::backup::{BackupReport, ServerBackupReport}; use crate::context::RpcContext; use crate::db::model::public::BackupProgress; -use crate::db::model::DatabaseModel; +use crate::db::model::{Database, DatabaseModel}; use crate::disk::mount::backup::BackupMountGuard; use crate::disk::mount::filesystem::ReadWrite; use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; @@ -42,9 +42,9 @@ pub struct BackupParams { password: crate::auth::PasswordType, } -struct BackupStatusGuard(Option); +struct BackupStatusGuard(Option>); impl BackupStatusGuard { - fn new(db: PatchDb) -> Self { + fn new(db: TypedPatchDb) -> Self { Self(Some(db)) } async fn handle_result( @@ -296,7 +296,7 @@ async fn perform_backup( if tokio::fs::metadata(&luks_folder_bak).await.is_ok() { tokio::fs::rename(&luks_folder_bak, &luks_folder_old).await?; } - let luks_folder = Path::new("/media/embassy/config/luks"); + let luks_folder = Path::new("/media/startos/config/luks"); if tokio::fs::metadata(&luks_folder).await.is_ok() { dir_copy(&luks_folder, &luks_folder_bak, None).await?; } diff --git a/core/startos/src/backup/mod.rs b/core/startos/src/backup/mod.rs index c963118c4..8afafaa33 100644 --- a/core/startos/src/backup/mod.rs +++ b/core/startos/src/backup/mod.rs @@ -3,7 +3,7 @@ use std::collections::BTreeMap; use chrono::{DateTime, Utc}; use models::{HostId, PackageId}; use reqwest::Url; -use rpc_toolkit::{from_fn_async, HandlerExt, ParentHandler}; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use crate::context::CliContext; @@ -34,23 +34,23 @@ pub struct PackageBackupReport { } // #[command(subcommands(backup_bulk::backup_all, target::target))] -pub fn backup() -> ParentHandler { +pub fn backup() -> ParentHandler { ParentHandler::new() .subcommand( "create", from_fn_async(backup_bulk::backup_all) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) - .subcommand("target", target::target()) + .subcommand("target", target::target::()) } -pub fn package_backup() -> ParentHandler { +pub fn package_backup() -> ParentHandler { ParentHandler::new().subcommand( "restore", from_fn_async(restore::restore_packages_rpc) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) } @@ -61,5 +61,5 @@ struct BackupMetadata { pub network_keys: BTreeMap>, #[serde(default)] pub tor_keys: BTreeMap>, // DEPRECATED - pub marketplace_url: Option, + pub registry: Option, } diff --git a/core/startos/src/backup/target/cifs.rs b/core/startos/src/backup/target/cifs.rs index ab2e91c0e..ad4f81aa0 100644 --- a/core/startos/src/backup/target/cifs.rs +++ b/core/startos/src/backup/target/cifs.rs @@ -4,7 +4,7 @@ use std::path::{Path, PathBuf}; use clap::Parser; use color_eyre::eyre::eyre; use imbl_value::InternedString; -use rpc_toolkit::{command, from_fn_async, HandlerExt, ParentHandler}; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use ts_rs::TS; @@ -46,25 +46,25 @@ pub struct CifsBackupTarget { start_os: Option, } -pub fn cifs() -> ParentHandler { +pub fn cifs() -> ParentHandler { ParentHandler::new() .subcommand( "add", from_fn_async(add) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "update", from_fn_async(update) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "remove", from_fn_async(remove) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) } diff --git a/core/startos/src/backup/target/mod.rs b/core/startos/src/backup/target/mod.rs index c0f2ef10e..4d00a2501 100644 --- a/core/startos/src/backup/target/mod.rs +++ b/core/startos/src/backup/target/mod.rs @@ -8,7 +8,7 @@ use color_eyre::eyre::eyre; use digest::generic_array::GenericArray; use digest::OutputSizeUser; use models::PackageId; -use rpc_toolkit::{command, from_fn_async, AnyContext, HandlerExt, ParentHandler}; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use sha2::Sha256; use tokio::sync::Mutex; @@ -138,23 +138,23 @@ impl FileSystem for BackupTargetFS { } // #[command(subcommands(cifs::cifs, list, info, mount, umount))] -pub fn target() -> ParentHandler { +pub fn target() -> ParentHandler { ParentHandler::new() - .subcommand("cifs", cifs::cifs()) + .subcommand("cifs", cifs::cifs::()) .subcommand( "list", from_fn_async(list) .with_display_serializable() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "info", from_fn_async(info) .with_display_serializable() - .with_custom_display_fn::(|params, info| { + .with_custom_display_fn::(|params, info| { Ok(display_backup_info(params.params, info)) }) - .with_remote_cli::(), + .with_call_remote::(), ) } diff --git a/core/startos/src/bins/mod.rs b/core/startos/src/bins/mod.rs index 68f2802e0..4a4670a5b 100644 --- a/core/startos/src/bins/mod.rs +++ b/core/startos/src/bins/mod.rs @@ -5,6 +5,8 @@ use std::path::Path; #[cfg(feature = "container-runtime")] pub mod container_cli; pub mod deprecated; +#[cfg(feature = "registry")] +pub mod registry; #[cfg(feature = "cli")] pub mod start_cli; #[cfg(feature = "daemon")] @@ -20,6 +22,8 @@ fn select_executable(name: &str) -> Option)> { "start-cli" => Some(container_cli::main), #[cfg(feature = "daemon")] "startd" => Some(startd::main), + #[cfg(feature = "registry")] + "registry" => Some(registry::main), "embassy-cli" => Some(|_| deprecated::renamed("embassy-cli", "start-cli")), "embassy-sdk" => Some(|_| deprecated::renamed("embassy-sdk", "start-sdk")), "embassyd" => Some(|_| deprecated::renamed("embassyd", "startd")), diff --git a/core/startos/src/bins/registry.rs b/core/startos/src/bins/registry.rs new file mode 100644 index 000000000..3028d1766 --- /dev/null +++ b/core/startos/src/bins/registry.rs @@ -0,0 +1,86 @@ +use std::ffi::OsString; + +use clap::Parser; +use futures::FutureExt; +use tokio::signal::unix::signal; +use tracing::instrument; + +use crate::net::web_server::WebServer; +use crate::prelude::*; +use crate::registry::context::{RegistryConfig, RegistryContext}; +use crate::util::logger::EmbassyLogger; + +#[instrument(skip_all)] +async fn inner_main(config: &RegistryConfig) -> Result<(), Error> { + let server = async { + let ctx = RegistryContext::init(config).await?; + let server = WebServer::registry(ctx.listen, ctx.clone()); + + let mut shutdown_recv = ctx.shutdown.subscribe(); + + let sig_handler_ctx = ctx; + let sig_handler = tokio::spawn(async move { + use tokio::signal::unix::SignalKind; + futures::future::select_all( + [ + SignalKind::interrupt(), + SignalKind::quit(), + SignalKind::terminate(), + ] + .iter() + .map(|s| { + async move { + signal(*s) + .unwrap_or_else(|_| panic!("register {:?} handler", s)) + .recv() + .await + } + .boxed() + }), + ) + .await; + sig_handler_ctx + .shutdown + .send(()) + .map_err(|_| ()) + .expect("send shutdown signal"); + }); + + shutdown_recv + .recv() + .await + .with_kind(crate::ErrorKind::Unknown)?; + + sig_handler.abort(); + + Ok::<_, Error>(server) + } + .await?; + server.shutdown().await; + + Ok(()) +} + +pub fn main(args: impl IntoIterator) { + EmbassyLogger::init(); + + let config = RegistryConfig::parse_from(args).load().unwrap(); + + let res = { + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("failed to initialize runtime"); + rt.block_on(inner_main(&config)) + }; + + match res { + Ok(()) => (), + Err(e) => { + eprintln!("{}", e.source); + tracing::debug!("{:?}", e.source); + drop(e.source); + std::process::exit(e.kind as i32) + } + } +} diff --git a/core/startos/src/bins/start_cli.rs b/core/startos/src/bins/start_cli.rs index 374247f2e..17cc095a3 100644 --- a/core/startos/src/bins/start_cli.rs +++ b/core/startos/src/bins/start_cli.rs @@ -16,7 +16,7 @@ pub fn main(args: impl IntoIterator) { EmbassyLogger::init(); if let Err(e) = CliApp::new( |cfg: ClientConfig| Ok(CliContext::init(cfg.load()?)?), - crate::main_api(), + crate::expanded_api(), ) .run(args) { diff --git a/core/startos/src/bins/start_init.rs b/core/startos/src/bins/start_init.rs index 284748339..8e60884f1 100644 --- a/core/startos/src/bins/start_init.rs +++ b/core/startos/src/bins/start_init.rs @@ -104,7 +104,7 @@ async fn setup_or_init(config: &ServerConfig) -> Result, Error> Command::new("reboot") .invoke(crate::ErrorKind::Unknown) .await?; - } else if tokio::fs::metadata("/media/embassy/config/disk.guid") + } else if tokio::fs::metadata("/media/startos/config/disk.guid") .await .is_err() { @@ -136,7 +136,7 @@ async fn setup_or_init(config: &ServerConfig) -> Result, Error> tracing::debug!("{:?}", e); } } else { - let guid_string = tokio::fs::read_to_string("/media/embassy/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy + let guid_string = tokio::fs::read_to_string("/media/startos/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy .await?; let guid = guid_string.trim(); let requires_reboot = crate::disk::main::import( @@ -202,7 +202,7 @@ async fn inner_main(config: &ServerConfig) -> Result, Error> { crate::sound::BEP.play().await?; - run_script_if_exists("/media/embassy/config/preinit.sh").await; + run_script_if_exists("/media/startos/config/preinit.sh").await; let res = match setup_or_init(config).await { Err(e) => { @@ -213,12 +213,12 @@ async fn inner_main(config: &ServerConfig) -> Result, Error> { let ctx = DiagnosticContext::init( config, - if tokio::fs::metadata("/media/embassy/config/disk.guid") + if tokio::fs::metadata("/media/startos/config/disk.guid") .await .is_ok() { Some(Arc::new( - tokio::fs::read_to_string("/media/embassy/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy + tokio::fs::read_to_string("/media/startos/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy .await? .trim() .to_owned(), @@ -245,7 +245,7 @@ async fn inner_main(config: &ServerConfig) -> Result, Error> { Ok(s) => Ok(s), }; - run_script_if_exists("/media/embassy/config/postinit.sh").await; + run_script_if_exists("/media/startos/config/postinit.sh").await; res } diff --git a/core/startos/src/bins/startd.rs b/core/startos/src/bins/startd.rs index 3e571d6b2..f0bc428be 100644 --- a/core/startos/src/bins/startd.rs +++ b/core/startos/src/bins/startd.rs @@ -23,7 +23,7 @@ async fn inner_main(config: &ServerConfig) -> Result, Error> { let rpc_ctx = RpcContext::init( config, Arc::new( - tokio::fs::read_to_string("/media/embassy/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy + tokio::fs::read_to_string("/media/startos/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy .await? .trim() .to_owned(), @@ -129,12 +129,12 @@ pub fn main(args: impl IntoIterator) { crate::sound::BEETHOVEN.play().await?; let ctx = DiagnosticContext::init( &config, - if tokio::fs::metadata("/media/embassy/config/disk.guid") + if tokio::fs::metadata("/media/startos/config/disk.guid") .await .is_ok() { Some(Arc::new( - tokio::fs::read_to_string("/media/embassy/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy + tokio::fs::read_to_string("/media/startos/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy .await? .trim() .to_owned(), diff --git a/core/startos/src/config/mod.rs b/core/startos/src/config/mod.rs index c600f590c..e61517794 100644 --- a/core/startos/src/config/mod.rs +++ b/core/startos/src/config/mod.rs @@ -9,7 +9,7 @@ use models::{ErrorKind, OptionExt, PackageId}; use patch_db::value::InternedString; use patch_db::Value; use regex::Regex; -use rpc_toolkit::{from_fn_async, Empty, HandlerExt, ParentHandler}; +use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use tracing::instrument; use ts_rs::TS; @@ -134,16 +134,19 @@ pub struct ConfigParams { } // #[command(subcommands(get, set))] -pub fn config() -> ParentHandler { +pub fn config() -> ParentHandler { ParentHandler::new() .subcommand( "get", from_fn_async(get) .with_inherited(|ConfigParams { id }, _| id) .with_display_serializable() - .with_remote_cli::(), + .with_call_remote::(), + ) + .subcommand( + "set", + set::().with_inherited(|ConfigParams { id }, _| id), ) - .subcommand("set", set().with_inherited(|ConfigParams { id }, _| id)) } #[instrument(skip_all)] @@ -173,13 +176,13 @@ pub struct SetParams { // metadata(sync_db = true) // )] #[instrument(skip_all)] -pub fn set() -> ParentHandler { +pub fn set() -> ParentHandler { ParentHandler::new().root_handler( from_fn_async(set_impl) .with_metadata("sync_db", Value::Bool(true)) .with_inherited(|set_params, id| (id, set_params)) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) } diff --git a/core/startos/src/context/cli.rs b/core/startos/src/context/cli.rs index cc2fe232b..166ab3fd7 100644 --- a/core/startos/src/context/cli.rs +++ b/core/startos/src/context/cli.rs @@ -10,7 +10,7 @@ use reqwest::Proxy; use reqwest_cookie_store::CookieStoreMutex; use rpc_toolkit::reqwest::{Client, Url}; use rpc_toolkit::yajrc::RpcError; -use rpc_toolkit::{call_remote_http, CallRemote, Context}; +use rpc_toolkit::{call_remote_http, CallRemote, Context, Empty}; use tokio::net::TcpStream; use tokio::runtime::Runtime; use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; @@ -18,15 +18,17 @@ use tracing::instrument; use super::setup::CURRENT_SECRET; use crate::context::config::{local_config_path, ClientConfig}; -use crate::core::rpc_continuations::RequestGuid; +use crate::context::{DiagnosticContext, InstallContext, RpcContext, SetupContext}; use crate::middleware::auth::LOCAL_AUTH_COOKIE_PATH; use crate::prelude::*; +use crate::rpc_continuations::RequestGuid; #[derive(Debug)] pub struct CliContextSeed { pub runtime: OnceCell, pub base_url: Url, pub rpc_url: Url, + pub registry_url: Option, pub client: Client, pub cookie_store: Arc, pub cookie_path: PathBuf, @@ -66,6 +68,8 @@ impl CliContext { "http://localhost".parse()? }; + let registry = config.registry.clone(); + let cookie_path = config.cookie_path.unwrap_or_else(|| { local_config_path() .as_deref() @@ -104,6 +108,17 @@ impl CliContext { .push("v1"); url }, + registry_url: registry + .map(|mut registry| { + registry + .path_segments_mut() + .map_err(|_| eyre!("Url cannot be base")) + .with_kind(crate::ErrorKind::ParseUrl)? + .push("rpc") + .push("v0"); + Ok::<_, Error>(registry) + }) + .transpose()?, client: { let mut builder = Client::builder().cookie_provider(cookie_store.clone()); if let Some(proxy) = config.proxy { @@ -198,6 +213,29 @@ impl CliContext { .await .with_kind(ErrorKind::Network) } + + pub async fn call_remote( + &self, + method: &str, + params: Value, + ) -> Result + where + Self: CallRemote, + { + >::call_remote(&self, method, params, Empty {}) + .await + } + pub async fn call_remote_with( + &self, + method: &str, + params: Value, + extra: T, + ) -> Result + where + Self: CallRemote, + { + >::call_remote(&self, method, params, extra).await + } } impl AsRef for CliContext { fn as_ref(&self) -> &Jwk { @@ -223,9 +261,23 @@ impl Context for CliContext { .clone() } } -#[async_trait::async_trait] -impl CallRemote for CliContext { - async fn call_remote(&self, method: &str, params: Value) -> Result { +impl CallRemote for CliContext { + async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { + call_remote_http(&self.client, self.rpc_url.clone(), method, params).await + } +} +impl CallRemote for CliContext { + async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { + call_remote_http(&self.client, self.rpc_url.clone(), method, params).await + } +} +impl CallRemote for CliContext { + async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { + call_remote_http(&self.client, self.rpc_url.clone(), method, params).await + } +} +impl CallRemote for CliContext { + async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { call_remote_http(&self.client, self.rpc_url.clone(), method, params).await } } diff --git a/core/startos/src/context/config.rs b/core/startos/src/context/config.rs index 0e6d9feff..bc2da00e2 100644 --- a/core/startos/src/context/config.rs +++ b/core/startos/src/context/config.rs @@ -14,7 +14,7 @@ use crate::init::init_postgres; use crate::prelude::*; use crate::util::serde::IoFormat; -pub const DEVICE_CONFIG_PATH: &str = "/media/embassy/config/config.yaml"; // "/media/startos/config/config.yaml"; +pub const DEVICE_CONFIG_PATH: &str = "/media/startos/config/config.yaml"; // "/media/startos/config/config.yaml"; pub const CONFIG_PATH: &str = "/etc/startos/config.yaml"; pub const CONFIG_PATH_LOCAL: &str = ".startos/config.yaml"; @@ -58,6 +58,8 @@ pub struct ClientConfig { pub config: Option, #[arg(short = 'h', long = "host")] pub host: Option, + #[arg(short = 'r', long = "registry")] + pub registry: Option, #[arg(short = 'p', long = "proxy")] pub proxy: Option, #[arg(long = "cookie-path")] @@ -71,8 +73,10 @@ impl ContextConfig for ClientConfig { } fn merge_with(&mut self, other: Self) { self.host = self.host.take().or(other.host); + self.registry = self.registry.take().or(other.registry); self.proxy = self.proxy.take().or(other.proxy); self.cookie_path = self.cookie_path.take().or(other.cookie_path); + self.developer_key_path = self.developer_key_path.take().or(other.developer_key_path); } } impl ClientConfig { diff --git a/core/startos/src/context/diagnostic.rs b/core/startos/src/context/diagnostic.rs index 117e56061..10379dcf3 100644 --- a/core/startos/src/context/diagnostic.rs +++ b/core/startos/src/context/diagnostic.rs @@ -8,6 +8,7 @@ use tokio::sync::broadcast::Sender; use tracing::instrument; use crate::context::config::ServerConfig; +use crate::rpc_continuations::RpcContinuations; use crate::shutdown::Shutdown; use crate::Error; @@ -16,6 +17,7 @@ pub struct DiagnosticContextSeed { pub shutdown: Sender>, pub error: Arc, pub disk_guid: Option>, + pub rpc_continuations: RpcContinuations, } #[derive(Clone)] @@ -37,10 +39,15 @@ impl DiagnosticContext { shutdown, disk_guid, error: Arc::new(error.into()), + rpc_continuations: RpcContinuations::new(), }))) } } - +impl AsRef for DiagnosticContext { + fn as_ref(&self) -> &RpcContinuations { + &self.rpc_continuations + } +} impl Context for DiagnosticContext {} impl Deref for DiagnosticContext { type Target = DiagnosticContextSeed; diff --git a/core/startos/src/context/rpc.rs b/core/startos/src/context/rpc.rs index f2c859273..cf2d28085 100644 --- a/core/startos/src/context/rpc.rs +++ b/core/startos/src/context/rpc.rs @@ -6,33 +6,32 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; -use imbl_value::InternedString; use josekit::jwk::Jwk; -use patch_db::PatchDb; use reqwest::{Client, Proxy}; -use rpc_toolkit::Context; +use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::{CallRemote, Context, Empty}; use tokio::sync::{broadcast, oneshot, Mutex, RwLock}; use tokio::time::Instant; use tracing::instrument; use super::setup::CURRENT_SECRET; +use crate::account::AccountInfo; use crate::context::config::ServerConfig; -use crate::core::rpc_continuations::{RequestGuid, RestHandler, RpcContinuation, WebSocketHandler}; -use crate::db::prelude::PatchDbExt; +use crate::db::model::Database; use crate::dependencies::compute_dependency_config_errs; use crate::disk::OsPartitionInfo; use crate::init::check_time_is_synchronized; -use crate::lxc::{LxcContainer, LxcManager}; +use crate::lxc::{ContainerId, LxcContainer, LxcManager}; use crate::middleware::auth::HashSessionToken; use crate::net::net_controller::NetController; use crate::net::utils::{find_eth_iface, find_wifi_iface}; use crate::net::wifi::WpaCli; use crate::prelude::*; +use crate::rpc_continuations::RpcContinuations; use crate::service::ServiceMap; use crate::shutdown::Shutdown; use crate::system::get_mem_info; use crate::util::lshw::{lshw, LshwDevice}; -use crate::{account::AccountInfo, lxc::ContainerId}; pub struct RpcContextSeed { is_closed: AtomicBool, @@ -41,7 +40,7 @@ pub struct RpcContextSeed { pub ethernet_interface: String, pub datadir: PathBuf, pub disk_guid: Arc, - pub db: PatchDb, + pub db: TypedPatchDb, pub account: RwLock, pub net_controller: Arc, pub services: ServiceMap, @@ -50,7 +49,7 @@ pub struct RpcContextSeed { pub tor_socks: SocketAddr, pub lxc_manager: Arc, pub open_authed_websockets: Mutex>>>, - pub rpc_stream_continuations: Mutex>, + pub rpc_continuations: RpcContinuations, pub wifi_manager: Option>>, pub current_secret: Arc, pub client: Client, @@ -80,7 +79,7 @@ impl RpcContext { ))); let (shutdown, _) = tokio::sync::broadcast::channel(1); - let db = config.db().await?; + let db = TypedPatchDb::::load(config.db().await?).await?; let peek = db.peek().await; let account = AccountInfo::load(&peek)?; tracing::info!("Opened PatchDB"); @@ -159,7 +158,7 @@ impl RpcContext { tor_socks: tor_proxy, lxc_manager: Arc::new(LxcManager::new()), open_authed_websockets: Mutex::new(BTreeMap::new()), - rpc_stream_continuations: Mutex::new(BTreeMap::new()), + rpc_continuations: RpcContinuations::new(), wifi_manager: wifi_interface .clone() .map(|i| Arc::new(RwLock::new(WpaCli::init(i)))), @@ -236,54 +235,27 @@ impl RpcContext { Ok(()) } - - #[instrument(skip_all)] - pub async fn clean_continuations(&self) { - let mut continuations = self.rpc_stream_continuations.lock().await; - let mut to_remove = Vec::new(); - for (guid, cont) in &*continuations { - if cont.is_timed_out() { - to_remove.push(guid.clone()); - } - } - for guid in to_remove { - continuations.remove(&guid); - } - } - - #[instrument(skip_all)] - pub async fn add_continuation(&self, guid: RequestGuid, handler: RpcContinuation) { - self.clean_continuations().await; - self.rpc_stream_continuations - .lock() - .await - .insert(guid, handler); - } - - pub async fn get_ws_continuation_handler( + pub async fn call_remote( &self, - guid: &RequestGuid, - ) -> Option { - let mut continuations = self.rpc_stream_continuations.lock().await; - if !matches!(continuations.get(guid), Some(RpcContinuation::WebSocket(_))) { - return None; - } - let Some(RpcContinuation::WebSocket(x)) = continuations.remove(guid) else { - return None; - }; - x.get().await + method: &str, + params: Value, + ) -> Result + where + Self: CallRemote, + { + >::call_remote(&self, method, params, Empty {}) + .await } - - pub async fn get_rest_continuation_handler(&self, guid: &RequestGuid) -> Option { - let mut continuations: tokio::sync::MutexGuard<'_, BTreeMap> = - self.rpc_stream_continuations.lock().await; - if !matches!(continuations.get(guid), Some(RpcContinuation::Rest(_))) { - return None; - } - let Some(RpcContinuation::Rest(x)) = continuations.remove(guid) else { - return None; - }; - x.get().await + pub async fn call_remote_with( + &self, + method: &str, + params: Value, + extra: T, + ) -> Result + where + Self: CallRemote, + { + >::call_remote(&self, method, params, extra).await } } impl AsRef for RpcContext { @@ -291,6 +263,11 @@ impl AsRef for RpcContext { &CURRENT_SECRET } } +impl AsRef for RpcContext { + fn as_ref(&self) -> &RpcContinuations { + &self.rpc_continuations + } +} impl Context for RpcContext {} impl Deref for RpcContext { type Target = RpcContextSeed; diff --git a/core/startos/src/control.rs b/core/startos/src/control.rs index 3ef7d6030..d4a595a61 100644 --- a/core/startos/src/control.rs +++ b/core/startos/src/control.rs @@ -1,7 +1,6 @@ use clap::Parser; use color_eyre::eyre::eyre; use models::PackageId; -use rpc_toolkit::command; use serde::{Deserialize, Serialize}; use tracing::instrument; use ts_rs::TS; diff --git a/core/startos/src/core/mod.rs b/core/startos/src/core/mod.rs deleted file mode 100644 index 7c2dbbb06..000000000 --- a/core/startos/src/core/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod rpc_continuations; diff --git a/core/startos/src/core/rpc_continuations.rs b/core/startos/src/core/rpc_continuations.rs deleted file mode 100644 index 0f25dd383..000000000 --- a/core/startos/src/core/rpc_continuations.rs +++ /dev/null @@ -1,75 +0,0 @@ -use std::time::Duration; - -use axum::extract::ws::WebSocket; -use axum::extract::Request; -use axum::response::Response; -use futures::future::BoxFuture; -use helpers::TimedResource; -use imbl_value::InternedString; - -#[allow(unused_imports)] -use crate::prelude::*; -use crate::util::new_guid; - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)] -pub struct RequestGuid(InternedString); -impl RequestGuid { - pub fn new() -> Self { - Self(new_guid()) - } - - pub fn from(r: &str) -> Option { - if r.len() != 32 { - return None; - } - for c in r.chars() { - if !(c >= 'A' && c <= 'Z' || c >= '2' && c <= '7') { - return None; - } - } - Some(RequestGuid(InternedString::intern(r))) - } -} -impl AsRef for RequestGuid { - fn as_ref(&self) -> &str { - self.0.as_ref() - } -} - -#[test] -fn parse_guid() { - println!( - "{:?}", - RequestGuid::from(&format!("{}", RequestGuid::new())) - ) -} - -impl std::fmt::Display for RequestGuid { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -pub type RestHandler = - Box BoxFuture<'static, Result> + Send>; - -pub type WebSocketHandler = Box BoxFuture<'static, ()> + Send>; - -pub enum RpcContinuation { - Rest(TimedResource), - WebSocket(TimedResource), -} -impl RpcContinuation { - pub fn rest(handler: RestHandler, timeout: Duration) -> Self { - RpcContinuation::Rest(TimedResource::new(handler, timeout)) - } - pub fn ws(handler: WebSocketHandler, timeout: Duration) -> Self { - RpcContinuation::WebSocket(TimedResource::new(handler, timeout)) - } - pub fn is_timed_out(&self) -> bool { - match self { - RpcContinuation::Rest(a) => a.is_timed_out(), - RpcContinuation::WebSocket(a) => a.is_timed_out(), - } - } -} diff --git a/core/startos/src/db/mod.rs b/core/startos/src/db/mod.rs index 95e42f46b..0bb8a23db 100644 --- a/core/startos/src/db/mod.rs +++ b/core/startos/src/db/mod.rs @@ -11,10 +11,11 @@ use clap::Parser; use futures::{FutureExt, StreamExt}; use http::header::COOKIE; use http::HeaderMap; +use itertools::Itertools; use patch_db::json_ptr::{JsonPointer, ROOT}; use patch_db::{Dump, Revision}; use rpc_toolkit::yajrc::RpcError; -use rpc_toolkit::{command, from_fn_async, CallRemote, HandlerExt, ParentHandler}; +use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use serde_json::Value; use tokio::sync::oneshot; @@ -167,11 +168,11 @@ pub async fn subscribe( })) } -pub fn db() -> ParentHandler { +pub fn db() -> ParentHandler { ParentHandler::new() .subcommand("dump", from_fn_async(cli_dump).with_display_serializable()) .subcommand("dump", from_fn_async(dump).no_cli()) - .subcommand("put", put()) + .subcommand("put", put::()) .subcommand("apply", from_fn_async(cli_apply).no_display()) .subcommand("apply", from_fn_async(apply).no_cli()) } @@ -195,21 +196,28 @@ pub struct CliDumpParams { #[instrument(skip_all)] async fn cli_dump( - ctx: CliContext, - CliDumpParams { - path, - include_private, - }: CliDumpParams, + HandlerArgs { + context, + parent_method, + method, + params: CliDumpParams { + include_private, + path, + }, + .. + }: HandlerArgs, ) -> Result { let dump = if let Some(path) = path { PatchDb::open(path).await?.dump(&ROOT).await } else { + let method = parent_method.into_iter().chain(method).join("."); from_value::( - ctx.call_remote( - "db.dump", - imbl_value::json!({ "includePrivate":include_private }), - ) - .await?, + context + .call_remote::( + &method, + imbl_value::json!({ "includePrivate":include_private }), + ) + .await?, )? }; @@ -237,36 +245,54 @@ pub async fn dump( }) } +#[derive(Deserialize, Serialize, Parser)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct CliApplyParams { + expr: String, + path: Option, +} + #[instrument(skip_all)] async fn cli_apply( - ctx: CliContext, - ApplyParams { expr, path }: ApplyParams, + HandlerArgs { + context, + parent_method, + method, + params: CliApplyParams { expr, path }, + .. + }: HandlerArgs, ) -> Result<(), RpcError> { if let Some(path) = path { PatchDb::open(path) .await? - .mutate(|db| { + .apply_function(|db| { let res = apply_expr( - serde_json::to_value(patch_db::Value::from(db.clone())) + serde_json::to_value(patch_db::Value::from(db)) .with_kind(ErrorKind::Deserialization)? .into(), &expr, )?; - db.ser( - &serde_json::from_value::(res.clone().into()).with_ctx( - |_| { - ( - crate::ErrorKind::Deserialization, - "result does not match database model", - ) - }, + Ok::<_, Error>(( + to_value( + &serde_json::from_value::(res.clone().into()).with_ctx( + |_| { + ( + crate::ErrorKind::Deserialization, + "result does not match database model", + ) + }, + )?, )?, - ) + (), + )) }) .await?; } else { - ctx.call_remote("db.apply", imbl_value::json!({ "expr": expr })) + let method = parent_method.into_iter().chain(method).join("."); + context + .call_remote::(&method, imbl_value::json!({ "expr": expr })) .await?; } @@ -278,10 +304,9 @@ async fn cli_apply( #[command(rename_all = "kebab-case")] pub struct ApplyParams { expr: String, - path: Option, } -pub async fn apply(ctx: RpcContext, ApplyParams { expr, .. }: ApplyParams) -> Result<(), Error> { +pub async fn apply(ctx: RpcContext, ApplyParams { expr }: ApplyParams) -> Result<(), Error> { ctx.db .mutate(|db| { let res = apply_expr( @@ -303,12 +328,12 @@ pub async fn apply(ctx: RpcContext, ApplyParams { expr, .. }: ApplyParams) -> Re .await } -pub fn put() -> ParentHandler { +pub fn put() -> ParentHandler { ParentHandler::new().subcommand( "ui", from_fn_async(ui) .with_display_serializable() - .with_remote_cli::(), + .with_call_remote::(), ) } #[derive(Deserialize, Serialize, Parser, TS)] diff --git a/core/startos/src/db/model/package.rs b/core/startos/src/db/model/package.rs index 29b7bb90f..3fad0ad9f 100644 --- a/core/startos/src/db/model/package.rs +++ b/core/startos/src/db/model/package.rs @@ -54,7 +54,7 @@ impl PackageState { pub fn expect_installed(&self) -> Result<&InstalledState, Error> { match self { Self::Installed(a) => Ok(a), - a => Err(Error::new( + _ => Err(Error::new( eyre!( "Package {} is not in installed state", self.as_manifest(ManifestPreference::Old).id @@ -161,7 +161,7 @@ impl Model { pub fn expect_installed(&self) -> Result<&Model, Error> { match self.as_match() { PackageStateMatchModelRef::Installed(a) => Ok(a), - a => Err(Error::new( + _ => Err(Error::new( eyre!( "Package {} is not in installed state", self.as_manifest(ManifestPreference::Old).as_id().de()? @@ -251,7 +251,7 @@ impl Model { PackageStateMatchModelMut::Installed(s) | PackageStateMatchModelMut::Removing(s) => { s.as_manifest_mut() } - PackageStateMatchModelMut::Error(s) => { + PackageStateMatchModelMut::Error(_) => { return Err(Error::new( eyre!("could not determine package state to get manifest"), ErrorKind::Database, @@ -325,7 +325,7 @@ pub struct PackageDataEntry { pub state_info: PackageState, pub status: Status, #[ts(type = "string | null")] - pub marketplace_url: Option, + pub registry: Option, #[ts(type = "string")] pub developer_key: Pem, pub icon: DataUrl<'static>, diff --git a/core/startos/src/db/model/public.rs b/core/startos/src/db/model/public.rs index a51303cf4..d79948c6f 100644 --- a/core/startos/src/db/model/public.rs +++ b/core/startos/src/db/model/public.rs @@ -19,6 +19,7 @@ use crate::account::AccountInfo; use crate::db::model::package::AllPackageData; use crate::net::utils::{get_iface_ipv4_addr, get_iface_ipv6_addr}; use crate::prelude::*; +use crate::progress::FullProgress; use crate::util::cpupower::Governor; use crate::util::Version; use crate::version::{Current, VersionT}; @@ -175,24 +176,13 @@ pub struct BackupProgress { pub struct ServerStatus { pub backup_progress: Option>, pub updated: bool, - pub update_progress: Option, + pub update_progress: Option, #[serde(default)] pub shutting_down: bool, #[serde(default)] pub restarting: bool, } -#[derive(Debug, Deserialize, Serialize, HasModel, TS)] -#[serde(rename_all = "camelCase")] -#[model = "Model"] -#[ts(export)] -pub struct UpdateProgress { - #[ts(type = "number | null")] - pub size: Option, - #[ts(type = "number")] - pub downloaded: u64, -} - #[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] #[serde(rename_all = "camelCase")] #[model = "Model"] diff --git a/core/startos/src/db/prelude.rs b/core/startos/src/db/prelude.rs index 43dd59002..d40c3c17c 100644 --- a/core/startos/src/db/prelude.rs +++ b/core/startos/src/db/prelude.rs @@ -1,22 +1,16 @@ use std::collections::BTreeMap; -use std::future::Future; use std::marker::PhantomData; -use std::panic::UnwindSafe; use std::str::FromStr; use chrono::{DateTime, Utc}; pub use imbl_value::Value; -use patch_db::json_ptr::ROOT; use patch_db::value::InternedString; pub use patch_db::{HasModel, PatchDb}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; -use crate::db::model::DatabaseModel; use crate::prelude::*; -pub type Peeked = Model; - pub fn to_value(value: &T) -> Result where T: Serialize, @@ -31,45 +25,7 @@ where patch_db::value::from_value(value).with_kind(ErrorKind::Deserialization) } -pub trait PatchDbExt { - fn peek(&self) -> impl Future + Send; - fn mutate( - &self, - f: impl FnOnce(&mut DatabaseModel) -> Result + UnwindSafe + Send, - ) -> impl Future> + Send; - fn map_mutate( - &self, - f: impl FnOnce(DatabaseModel) -> Result + UnwindSafe + Send, - ) -> impl Future> + Send; -} -impl PatchDbExt for PatchDb { - async fn peek(&self) -> DatabaseModel { - DatabaseModel::from(self.dump(&ROOT).await.value) - } - async fn mutate( - &self, - f: impl FnOnce(&mut DatabaseModel) -> Result + UnwindSafe + Send, - ) -> Result { - Ok(self - .apply_function(|mut v| { - let model = <&mut DatabaseModel>::from(&mut v); - let res = f(model)?; - Ok::<_, Error>((v, res)) - }) - .await? - .1) - } - async fn map_mutate( - &self, - f: impl FnOnce(DatabaseModel) -> Result + UnwindSafe + Send, - ) -> Result { - Ok(DatabaseModel::from( - self.apply_function(|v| f(DatabaseModel::from(v)).map(|a| (a.into(), ()))) - .await? - .0, - )) - } -} +pub type TypedPatchDb = patch_db::TypedPatchDb; /// &mut Model <=> &mut Value #[repr(transparent)] @@ -125,7 +81,7 @@ impl Model { Ok(res) } pub fn map_mutate(&mut self, f: impl FnOnce(T) -> Result) -> Result { - let mut orig = self.de()?; + let orig = self.de()?; let res = f(orig)?; self.ser(&res)?; Ok(res) @@ -262,10 +218,9 @@ where .into()), } } - pub fn upsert(&mut self, key: &T::Key, value: F) -> Result<&mut Model, Error> + pub fn upsert(&mut self, key: &T::Key, value: F) -> Result<&mut Model, Error> where - F: FnOnce() -> D, - D: AsRef, + F: FnOnce() -> T::Value, { use serde::ser::Error; match &mut self.value { @@ -278,7 +233,7 @@ where s.as_ref().index_or_insert(v) }); if !exists { - res.ser(value().as_ref())?; + res.ser(&value())?; } Ok(res) } @@ -375,6 +330,18 @@ where } } impl Model { + pub fn contains_key(&self, key: &T::Key) -> Result { + use serde::de::Error; + let s = T::key_str(key)?; + match &self.value { + Value::Object(o) => Ok(o.contains_key(s.as_ref())), + v => Err(patch_db::value::Error { + source: patch_db::value::ErrorSource::custom(format!("expected object found {v}")), + kind: patch_db::value::ErrorKind::Deserialization, + } + .into()), + } + } pub fn into_idx(self, key: &T::Key) -> Option> { use patch_db::ModelExt; let s = T::key_str(key).ok()?; diff --git a/core/startos/src/dependencies.rs b/core/startos/src/dependencies.rs index 42a19abe3..a746a42c9 100644 --- a/core/startos/src/dependencies.rs +++ b/core/startos/src/dependencies.rs @@ -4,7 +4,7 @@ use std::time::Duration; use clap::Parser; use models::PackageId; use patch_db::json_patch::merge; -use rpc_toolkit::{command, from_fn_async, Empty, HandlerExt, ParentHandler}; +use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use tracing::instrument; use ts_rs::TS; @@ -15,8 +15,8 @@ use crate::db::model::package::CurrentDependencies; use crate::prelude::*; use crate::Error; -pub fn dependency() -> ParentHandler { - ParentHandler::new().subcommand("configure", configure()) +pub fn dependency() -> ParentHandler { + ParentHandler::new().subcommand("configure", configure::()) } #[derive(Clone, Debug, Default, Deserialize, Serialize, HasModel, TS)] @@ -50,7 +50,7 @@ pub struct ConfigureParams { dependent_id: PackageId, dependency_id: PackageId, } -pub fn configure() -> ParentHandler { +pub fn configure() -> ParentHandler { ParentHandler::new().root_handler( from_fn_async(configure_impl) .with_inherited(|params, _| params) diff --git a/core/startos/src/developer/mod.rs b/core/startos/src/developer/mod.rs index 596957445..79a875dfc 100644 --- a/core/startos/src/developer/mod.rs +++ b/core/startos/src/developer/mod.rs @@ -8,6 +8,7 @@ use ed25519_dalek::{SigningKey, VerifyingKey}; use tracing::instrument; use crate::context::CliContext; +use crate::util::serde::Pem; use crate::{Error, ResultExt}; #[instrument(skip_all)] @@ -45,3 +46,7 @@ pub fn init(ctx: CliContext) -> Result<(), Error> { } Ok(()) } + +pub fn pubkey(ctx: CliContext) -> Result, Error> { + Ok(Pem(ctx.developer_key()?.verifying_key())) +} diff --git a/core/startos/src/diagnostic.rs b/core/startos/src/diagnostic.rs index 1cef6d7ef..485f2359f 100644 --- a/core/startos/src/diagnostic.rs +++ b/core/startos/src/diagnostic.rs @@ -1,38 +1,48 @@ use std::path::Path; use std::sync::Arc; -use clap::Parser; use rpc_toolkit::yajrc::RpcError; -use rpc_toolkit::{command, from_fn, from_fn_async, AnyContext, HandlerExt, ParentHandler}; -use serde::{Deserialize, Serialize}; -use ts_rs::TS; +use rpc_toolkit::{ + from_fn, from_fn_async, CallRemoteHandler, Context, Empty, HandlerExt, ParentHandler, +}; -use crate::context::{CliContext, DiagnosticContext}; +use crate::context::{CliContext, DiagnosticContext, RpcContext}; use crate::init::SYSTEM_REBUILD_PATH; -use crate::logs::{fetch_logs, LogResponse, LogSource}; use crate::shutdown::Shutdown; use crate::Error; -pub fn diagnostic() -> ParentHandler { +pub fn diagnostic() -> ParentHandler { ParentHandler::new() - .subcommand("error", from_fn(error).with_remote_cli::()) - .subcommand("logs", from_fn_async(logs).no_cli()) + .subcommand("error", from_fn(error).with_call_remote::()) + .subcommand("logs", crate::system::logs::()) + .subcommand( + "logs", + from_fn_async(crate::logs::cli_logs::).no_display(), + ) + .subcommand( + "kernel-logs", + crate::system::kernel_logs::(), + ) + .subcommand( + "kernel-logs", + from_fn_async(crate::logs::cli_logs::).no_display(), + ) .subcommand( "exit", - from_fn(exit).no_display().with_remote_cli::(), + from_fn(exit).no_display().with_call_remote::(), ) .subcommand( "restart", from_fn(restart) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) - .subcommand("disk", disk()) + .subcommand("disk", disk::()) .subcommand( "rebuild", from_fn_async(rebuild) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) } @@ -41,26 +51,6 @@ pub fn error(ctx: DiagnosticContext) -> Result, Error> { Ok(ctx.error.clone()) } -#[derive(Deserialize, Serialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "kebab-case")] -pub struct LogsParams { - #[ts(type = "number | null")] - limit: Option, - cursor: Option, - before: bool, -} -pub async fn logs( - _: AnyContext, - LogsParams { - limit, - cursor, - before, - }: LogsParams, -) -> Result { - Ok(fetch_logs(LogSource::System, limit, cursor, before).await?) -} - pub fn exit(ctx: DiagnosticContext) -> Result<(), Error> { ctx.shutdown.send(None).expect("receiver dropped"); Ok(()) @@ -83,17 +73,20 @@ pub async fn rebuild(ctx: DiagnosticContext) -> Result<(), Error> { restart(ctx) } -pub fn disk() -> ParentHandler { - ParentHandler::new().subcommand( - "forget", - from_fn_async(forget_disk) - .no_display() - .with_remote_cli::(), - ) +pub fn disk() -> ParentHandler { + ParentHandler::new() + .subcommand("forget", from_fn_async(forget_disk::).no_cli()) + .subcommand( + "forget", + CallRemoteHandler::::new( + from_fn_async(forget_disk::).no_display(), + ) + .no_display(), + ) } -pub async fn forget_disk(_: AnyContext) -> Result<(), Error> { - let disk_guid = Path::new("/media/embassy/config/disk.guid"); +pub async fn forget_disk(_: C) -> Result<(), Error> { + let disk_guid = Path::new("/media/startos/config/disk.guid"); if tokio::fs::metadata(disk_guid).await.is_ok() { tokio::fs::remove_file(disk_guid).await?; } diff --git a/core/startos/src/disk/fsck/ext4.rs b/core/startos/src/disk/fsck/ext4.rs index 7bcbbc8b3..a068749fa 100644 --- a/core/startos/src/disk/fsck/ext4.rs +++ b/core/startos/src/disk/fsck/ext4.rs @@ -38,7 +38,7 @@ fn backup_existing_undo_file<'a>(path: &'a Path) -> BoxFuture<'a, Result<(), Err pub async fn e2fsck_aggressive( logicalname: impl AsRef + std::fmt::Debug, ) -> Result { - let undo_path = Path::new("/media/embassy/config") + let undo_path = Path::new("/media/startos/config") .join( logicalname .as_ref() diff --git a/core/startos/src/disk/main.rs b/core/startos/src/disk/main.rs index c7c634303..d414c247e 100644 --- a/core/startos/src/disk/main.rs +++ b/core/startos/src/disk/main.rs @@ -302,7 +302,7 @@ pub async fn mount_fs>( if !guid.ends_with("_UNENC") { // Backup LUKS header if e2fsck succeeded - let luks_folder = Path::new("/media/embassy/config/luks"); + let luks_folder = Path::new("/media/startos/config/luks"); tokio::fs::create_dir_all(luks_folder).await?; let tmp_luks_bak = luks_folder.join(format!(".{full_name}.luks.bak.tmp")); if tokio::fs::metadata(&tmp_luks_bak).await.is_ok() { diff --git a/core/startos/src/disk/mod.rs b/core/startos/src/disk/mod.rs index f74c944a4..68eb2b187 100644 --- a/core/startos/src/disk/mod.rs +++ b/core/startos/src/disk/mod.rs @@ -1,6 +1,8 @@ use std::path::{Path, PathBuf}; -use rpc_toolkit::{from_fn_async, AnyContext, Empty, HandlerExt, ParentHandler}; +use rpc_toolkit::{ + from_fn_async, CallRemoteHandler, Context, Empty, HandlerExt, ParentHandler, +}; use serde::{Deserialize, Serialize}; use crate::context::{CliContext, RpcContext}; @@ -14,7 +16,7 @@ pub mod mount; pub mod util; pub const BOOT_RW_PATH: &str = "/media/boot-rw"; -pub const REPAIR_DISK_PATH: &str = "/media/embassy/config/repair-disk"; +pub const REPAIR_DISK_PATH: &str = "/media/startos/config/repair-disk"; #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -40,22 +42,23 @@ impl OsPartitionInfo { } } -pub fn disk() -> ParentHandler { +pub fn disk() -> ParentHandler { ParentHandler::new() .subcommand( "list", from_fn_async(list) .with_display_serializable() - .with_custom_display_fn::(|handle, result| { + .with_custom_display_fn(|handle, result| { Ok(display_disk_info(handle.params, result)) }) - .with_remote_cli::(), + .with_call_remote::(), ) + .subcommand("repair", from_fn_async(|_: C| repair()).no_cli()) .subcommand( "repair", - from_fn_async(repair) - .no_display() - .with_remote_cli::(), + CallRemoteHandler::::new( + from_fn_async(|_: RpcContext| repair()).no_display(), + ), ) } diff --git a/core/startos/src/disk/mount/filesystem/overlayfs.rs b/core/startos/src/disk/mount/filesystem/overlayfs.rs index ad5eec501..f96de7b11 100644 --- a/core/startos/src/disk/mount/filesystem/overlayfs.rs +++ b/core/startos/src/disk/mount/filesystem/overlayfs.rs @@ -11,17 +11,21 @@ use crate::disk::mount::guard::{GenericMountGuard, MountGuard, TmpMountGuard}; use crate::prelude::*; use crate::util::io::TmpDir; -struct OverlayFs, P1: AsRef> { +pub struct OverlayFs, P1: AsRef, P2: AsRef> { lower: P0, upper: P1, + work: P2, } -impl, P1: AsRef> OverlayFs { - pub fn new(lower: P0, upper: P1) -> Self { - Self { lower, upper } +impl, P1: AsRef, P2: AsRef> OverlayFs { + pub fn new(lower: P0, upper: P1, work: P2) -> Self { + Self { lower, upper, work } } } -impl + Send + Sync, P1: AsRef + Send + Sync> FileSystem - for OverlayFs +impl< + P0: AsRef + Send + Sync, + P1: AsRef + Send + Sync, + P2: AsRef + Send + Sync, + > FileSystem for OverlayFs { fn mount_type(&self) -> Option> { Some("overlay") @@ -33,24 +37,20 @@ impl + Send + Sync, P1: AsRef + Send + Sync> FileSystem [ Box::new(lazy_format!("lowerdir={}", self.lower.as_ref().display())) as Box, - Box::new(lazy_format!( - "upperdir={}/upper", - self.upper.as_ref().display() - )), - Box::new(lazy_format!( - "workdir={}/work", - self.upper.as_ref().display() - )), + Box::new(lazy_format!("upperdir={}", self.upper.as_ref().display())), + Box::new(lazy_format!("workdir={}", self.work.as_ref().display())), ] } async fn pre_mount(&self) -> Result<(), Error> { - tokio::fs::create_dir_all(self.upper.as_ref().join("upper")).await?; - tokio::fs::create_dir_all(self.upper.as_ref().join("work")).await?; + tokio::fs::create_dir_all(self.upper.as_ref()).await?; + tokio::fs::create_dir_all(self.work.as_ref()).await?; Ok(()) } async fn source_hash( &self, ) -> Result::OutputSize>, Error> { + tokio::fs::create_dir_all(self.upper.as_ref()).await?; + tokio::fs::create_dir_all(self.work.as_ref()).await?; let mut sha = Sha256::new(); sha.update("OverlayFs"); sha.update( @@ -77,6 +77,18 @@ impl + Send + Sync, P1: AsRef + Send + Sync> FileSystem .as_os_str() .as_bytes(), ); + sha.update( + tokio::fs::canonicalize(self.work.as_ref()) + .await + .with_ctx(|_| { + ( + crate::ErrorKind::Filesystem, + self.upper.as_ref().display().to_string(), + ) + })? + .as_os_str() + .as_bytes(), + ); Ok(sha.finalize()) } } @@ -95,7 +107,11 @@ impl OverlayGuard { let lower = TmpMountGuard::mount(base, ReadOnly).await?; let upper = TmpDir::new().await?; let inner_guard = MountGuard::mount( - &OverlayFs::new(lower.path(), upper.as_ref()), + &OverlayFs::new( + lower.path(), + upper.as_ref().join("upper"), + upper.as_ref().join("work"), + ), mountpoint, ReadWrite, ) diff --git a/core/startos/src/disk/mount/guard.rs b/core/startos/src/disk/mount/guard.rs index dc73f5527..d08b04881 100644 --- a/core/startos/src/disk/mount/guard.rs +++ b/core/startos/src/disk/mount/guard.rs @@ -2,7 +2,6 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::sync::{Arc, Weak}; -use bytes::Buf; use lazy_static::lazy_static; use models::ResultExt; use tokio::sync::Mutex; @@ -13,7 +12,7 @@ use super::util::unmount; use crate::util::{Invoke, Never}; use crate::Error; -pub const TMP_MOUNTPOINT: &'static str = "/media/embassy/tmp"; +pub const TMP_MOUNTPOINT: &'static str = "/media/startos/tmp"; #[async_trait::async_trait] pub trait GenericMountGuard: std::fmt::Debug + Send + Sync + 'static { diff --git a/core/startos/src/error.rs b/core/startos/src/error.rs index 9f0493f10..a0ca5707c 100644 --- a/core/startos/src/error.rs +++ b/core/startos/src/error.rs @@ -58,7 +58,7 @@ impl std::error::Error for ErrorCollection {} macro_rules! ensure_code { ($x:expr, $c:expr, $fmt:expr $(, $arg:expr)*) => { if !($x) { - return Err(crate::error::Error::new(color_eyre::eyre::eyre!($fmt, $($arg, )*), $c)); + Err::<(), _>(crate::error::Error::new(color_eyre::eyre::eyre!($fmt, $($arg, )*), $c))?; } }; } diff --git a/core/startos/src/init.rs b/core/startos/src/init.rs index 3f7d7f1bc..361288c10 100644 --- a/core/startos/src/init.rs +++ b/core/startos/src/init.rs @@ -12,6 +12,7 @@ use tracing::instrument; use crate::account::AccountInfo; use crate::context::config::ServerConfig; use crate::db::model::public::ServerStatus; +use crate::db::model::Database; use crate::disk::mount::util::unmount; use crate::middleware::auth::LOCAL_AUTH_COOKIE_PATH; use crate::prelude::*; @@ -20,8 +21,8 @@ use crate::util::cpupower::{get_available_governors, get_preferred_governor, set use crate::util::Invoke; use crate::{Error, ARCH}; -pub const SYSTEM_REBUILD_PATH: &str = "/media/embassy/config/system-rebuild"; -pub const STANDBY_MODE_PATH: &str = "/media/embassy/config/standby"; +pub const SYSTEM_REBUILD_PATH: &str = "/media/startos/config/system-rebuild"; +pub const STANDBY_MODE_PATH: &str = "/media/startos/config/standby"; pub async fn check_time_is_synchronized() -> Result { Ok(String::from_utf8( @@ -179,7 +180,7 @@ pub async fn init_postgres(datadir: impl AsRef) -> Result<(), Error> { } pub struct InitResult { - pub db: patch_db::PatchDb, + pub db: TypedPatchDb, } #[instrument(skip_all)] @@ -207,7 +208,7 @@ pub async fn init(cfg: &ServerConfig) -> Result { .await?; } - let db = cfg.db().await?; + let db = TypedPatchDb::::load_unchecked(cfg.db().await?); let peek = db.peek().await; tracing::info!("Opened PatchDB"); diff --git a/core/startos/src/install/mod.rs b/core/startos/src/install/mod.rs index ca8f1edbd..f5cff36d9 100644 --- a/core/startos/src/install/mod.rs +++ b/core/startos/src/install/mod.rs @@ -6,11 +6,12 @@ use clap::{value_parser, CommandFactory, FromArgMatches, Parser}; use color_eyre::eyre::eyre; use emver::VersionRange; use futures::{FutureExt, StreamExt}; +use itertools::Itertools; use patch_db::json_ptr::JsonPointer; use reqwest::header::{HeaderMap, CONTENT_LENGTH}; use reqwest::Url; use rpc_toolkit::yajrc::RpcError; -use rpc_toolkit::CallRemote; +use rpc_toolkit::HandlerArgs; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use tokio::sync::oneshot; @@ -18,10 +19,10 @@ use tracing::instrument; use ts_rs::TS; use crate::context::{CliContext, RpcContext}; -use crate::core::rpc_continuations::{RequestGuid, RpcContinuation}; use crate::db::model::package::{ManifestPreference, PackageState, PackageStateMatchModelRef}; use crate::prelude::*; use crate::progress::{FullProgress, PhasedProgressBar}; +use crate::rpc_continuations::{RequestGuid, RpcContinuation}; use crate::s9pk::manifest::PackageId; use crate::s9pk::merkle_archive::source::http::HttpSource; use crate::s9pk::S9pk; @@ -110,7 +111,7 @@ pub struct InstallParams { id: PackageId, #[arg(short = 'm', long = "marketplace-url")] #[ts(type = "string | null")] - marketplace_url: Option, + registry: Option, #[arg(short = 'v', long = "version-spec")] version_spec: Option, #[arg(long = "version-priority")] @@ -125,7 +126,7 @@ pub async fn install( ctx: RpcContext, InstallParams { id, - marketplace_url, + registry, version_spec, version_priority, }: InstallParams, @@ -135,15 +136,14 @@ pub async fn install( Some(v) => &*v, }; let version: VersionRange = version_str.parse()?; - let marketplace_url = - marketplace_url.unwrap_or_else(|| crate::DEFAULT_MARKETPLACE.parse().unwrap()); + let registry = registry.unwrap_or_else(|| crate::DEFAULT_MARKETPLACE.parse().unwrap()); let version_priority = version_priority.unwrap_or_default(); let s9pk = S9pk::deserialize( &HttpSource::new( ctx.client.clone(), format!( "{}/package/v0/{}.s9pk?spec={}&version-priority={}", - marketplace_url, id, version, version_priority, + registry, id, version, version_priority, ) .parse()?, ) @@ -188,7 +188,7 @@ pub async fn sideload(ctx: RpcContext) -> Result { .with_kind(ErrorKind::Database)?, ) .await; - ctx.add_continuation( + ctx.rpc_continuations.add( progress.clone(), RpcContinuation::ws( Box::new(|mut ws| { @@ -203,28 +203,26 @@ pub async fn sideload(ctx: RpcContext) -> Result { })?; tokio::select! { res = async { - while let Some(rev) = sub.recv().await { - if !rev.patch.0.is_empty() { // TODO: don't send empty patches? - ws.send(Message::Text( - serde_json::to_string(&if let Some(p) = db - .peek() - .await - .as_public() - .as_package_data() - .as_idx(&id) - .and_then(|e| e.as_state_info().as_installing_info()).map(|i| i.as_progress()) - { - Ok::<_, ()>(p.de()?) - } else { - let mut p = FullProgress::new(); - p.overall.complete(); - Ok(p) - }) - .with_kind(ErrorKind::Serialization)?, - )) - .await - .with_kind(ErrorKind::Network)?; - } + while let Some(_) = sub.recv().await { + ws.send(Message::Text( + serde_json::to_string(&if let Some(p) = db + .peek() + .await + .as_public() + .as_package_data() + .as_idx(&id) + .and_then(|e| e.as_state_info().as_installing_info()).map(|i| i.as_progress()) + { + Ok::<_, ()>(p.de()?) + } else { + let mut p = FullProgress::new(); + p.overall.complete(); + Ok(p) + }) + .with_kind(ErrorKind::Serialization)?, + )) + .await + .with_kind(ErrorKind::Network)?; } Ok::<_, Error>(()) } => res?, @@ -322,15 +320,30 @@ impl FromArgMatches for CliInstallParams { } #[instrument(skip_all)] -pub async fn cli_install(ctx: CliContext, params: CliInstallParams) -> Result<(), RpcError> { +pub async fn cli_install( + HandlerArgs { + context: ctx, + parent_method, + method, + params, + .. + }: HandlerArgs, +) -> Result<(), RpcError> { + let method = parent_method.into_iter().chain(method).collect_vec(); match params { CliInstallParams::Sideload(path) => { let file = crate::s9pk::load(&ctx, path).await?; // rpc call remote sideload let SideloadResponse { upload, progress } = from_value::( - ctx.call_remote("package.sideload", imbl_value::json!({})) - .await?, + ctx.call_remote::( + &method[..method.len() - 1] + .into_iter() + .chain(std::iter::once(&"sideload")) + .join("."), + imbl_value::json!({}), + ) + .await?, )?; let upload = async { @@ -387,7 +400,7 @@ pub async fn cli_install(ctx: CliContext, params: CliInstallParams) -> Result<() upload?; } CliInstallParams::Marketplace(params) => { - ctx.call_remote("package.install", to_value(¶ms)?) + ctx.call_remote::(&method.join("."), to_value(¶ms)?) .await?; } } diff --git a/core/startos/src/lib.rs b/core/startos/src/lib.rs index d6dad2c87..3152fcab3 100644 --- a/core/startos/src/lib.rs +++ b/core/startos/src/lib.rs @@ -28,7 +28,6 @@ pub mod bins; pub mod config; pub mod context; pub mod control; -pub mod core; pub mod db; pub mod dependencies; pub mod developer; @@ -49,6 +48,7 @@ pub mod prelude; pub mod progress; pub mod properties; pub mod registry; +pub mod rpc_continuations; pub mod s9pk; pub mod service; pub mod setup; @@ -71,12 +71,14 @@ pub use error::{Error, ErrorKind, ResultExt}; use imbl_value::Value; use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::{ - command, from_fn, from_fn_async, from_fn_blocking, AnyContext, HandlerExt, ParentHandler, + from_fn, from_fn_async, from_fn_blocking, CallRemoteHandler, Context, Empty, HandlerExt, + ParentHandler, }; use serde::{Deserialize, Serialize}; use ts_rs::TS; -use crate::context::CliContext; +use crate::context::{CliContext, DiagnosticContext, InstallContext, RpcContext, SetupContext}; +use crate::registry::context::{RegistryContext, RegistryUrlParams}; use crate::util::serde::HandlerExtSerde; #[derive(Deserialize, Serialize, Parser, TS)] @@ -86,102 +88,117 @@ pub struct EchoParams { message: String, } -pub fn echo(_: AnyContext, EchoParams { message }: EchoParams) -> Result { +pub fn echo(_: C, EchoParams { message }: EchoParams) -> Result { Ok(message) } -pub fn main_api() -> ParentHandler { +pub fn main_api() -> ParentHandler { ParentHandler::new() - .subcommand("git-info", from_fn(version::git_info)) + .subcommand::("git-info", from_fn(version::git_info)) .subcommand( "echo", - from_fn(echo) + from_fn(echo::) .with_metadata("authenticated", Value::Bool(false)) - .with_remote_cli::(), + .with_call_remote::(), ) - .subcommand("init", from_fn_blocking(developer::init).no_display()) - .subcommand("server", server()) - .subcommand("package", package()) - .subcommand("net", net::net()) - .subcommand("auth", auth::auth()) - .subcommand("db", db::db()) - .subcommand("ssh", ssh::ssh()) - .subcommand("wifi", net::wifi::wifi()) - .subcommand("disk", disk::disk()) - .subcommand("notification", notifications::notification()) - .subcommand("backup", backup::backup()) - .subcommand("marketplace", registry::marketplace::marketplace()) - .subcommand("lxc", lxc::lxc()) + .subcommand("server", server::()) + .subcommand("package", package::()) + .subcommand("net", net::net::()) + .subcommand("auth", auth::auth::()) + .subcommand("db", db::db::()) + .subcommand("ssh", ssh::ssh::()) + .subcommand("wifi", net::wifi::wifi::()) + .subcommand("disk", disk::disk::()) + .subcommand("notification", notifications::notification::()) + .subcommand("backup", backup::backup::()) + .subcommand( + "registry", + CallRemoteHandler::::new( + registry::registry_api::(), + ) + .no_cli(), + ) + .subcommand("lxc", lxc::lxc::()) .subcommand("s9pk", s9pk::rpc::s9pk()) + .subcommand("util", util::rpc::util::()) } -pub fn server() -> ParentHandler { +pub fn server() -> ParentHandler { ParentHandler::new() .subcommand( "time", from_fn_async(system::time) .with_display_serializable() - .with_custom_display_fn::(|handle, result| { + .with_custom_display_fn(|handle, result| { Ok(system::display_time(handle.params, result)) }) - .with_remote_cli::(), + .with_call_remote::(), + ) + .subcommand("experimental", system::experimental::()) + .subcommand("logs", system::logs::()) + .subcommand( + "logs", + from_fn_async(logs::cli_logs::).no_display(), + ) + .subcommand("kernel-logs", system::kernel_logs::()) + .subcommand( + "kernel-logs", + from_fn_async(logs::cli_logs::).no_display(), ) - .subcommand("experimental", system::experimental()) - .subcommand("logs", system::logs()) - .subcommand("kernel-logs", system::kernel_logs()) .subcommand( "metrics", from_fn_async(system::metrics) .with_display_serializable() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "shutdown", from_fn_async(shutdown::shutdown) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "restart", from_fn_async(shutdown::restart) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "rebuild", from_fn_async(shutdown::rebuild) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "update", from_fn_async(update::update_system) .with_metadata("sync_db", Value::Bool(true)) - .with_custom_display_fn::(|handle, result| { - Ok(update::display_update_result(handle.params, result)) - }) - .with_remote_cli::(), + .no_cli(), + ) + .subcommand( + "update", + from_fn_async(update::cli_update_system).no_display(), ) .subcommand( "update-firmware", - from_fn_async(firmware::update_firmware) - .with_custom_display_fn::(|_handle, result| { + from_fn_async(|_: RpcContext| firmware::update_firmware()) + .with_custom_display_fn(|_handle, result| { Ok(firmware::display_firmware_update_result(result)) }) - .with_remote_cli::(), + .with_call_remote::(), ) } -pub fn package() -> ParentHandler { +pub fn package() -> ParentHandler { ParentHandler::new() .subcommand( "action", from_fn_async(action::action) .with_display_serializable() - .with_custom_display_fn::(|handle, result| { + .with_custom_display_fn(|handle, result| { Ok(action::display_action_result(handle.params, result)) }) - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "install", @@ -196,47 +213,51 @@ pub fn package() -> ParentHandler { from_fn_async(install::uninstall) .with_metadata("sync_db", Value::Bool(true)) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "list", from_fn_async(install::list) .with_display_serializable() - .with_remote_cli::(), + .with_call_remote::(), ) - .subcommand("config", config::config()) + .subcommand("config", config::config::()) .subcommand( "start", from_fn_async(control::start) .with_metadata("sync_db", Value::Bool(true)) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "stop", from_fn_async(control::stop) .with_metadata("sync_db", Value::Bool(true)) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "restart", from_fn_async(control::restart) .with_metadata("sync_db", Value::Bool(true)) .no_display() - .with_remote_cli::(), + .with_call_remote::(), + ) + .subcommand("logs", logs::package_logs()) + .subcommand( + "logs", + from_fn_async(logs::cli_logs::).no_display(), ) - .subcommand("logs", logs::logs()) .subcommand( "properties", from_fn_async(properties::properties) - .with_custom_display_fn::(|_handle, result| { + .with_custom_display_fn(|_handle, result| { Ok(properties::display_properties(result)) }) - .with_remote_cli::(), + .with_call_remote::(), ) - .subcommand("dependency", dependencies::dependency()) - .subcommand("backup", backup::package_backup()) + .subcommand("dependency", dependencies::dependency::()) + .subcommand("backup", backup::package_backup::()) .subcommand("connect", from_fn_async(service::connect_rpc).no_cli()) .subcommand( "connect", @@ -244,32 +265,51 @@ pub fn package() -> ParentHandler { ) } -pub fn diagnostic_api() -> ParentHandler { +pub fn diagnostic_api() -> ParentHandler { ParentHandler::new() - .subcommand( + .subcommand::( "git-info", from_fn(version::git_info).with_metadata("authenticated", Value::Bool(false)), ) - .subcommand("echo", from_fn(echo).with_remote_cli::()) - .subcommand("diagnostic", diagnostic::diagnostic()) + .subcommand( + "echo", + from_fn(echo::).with_call_remote::(), + ) + .subcommand("diagnostic", diagnostic::diagnostic::()) } -pub fn setup_api() -> ParentHandler { +pub fn setup_api() -> ParentHandler { ParentHandler::new() - .subcommand( + .subcommand::( "git-info", from_fn(version::git_info).with_metadata("authenticated", Value::Bool(false)), ) - .subcommand("echo", from_fn(echo).with_remote_cli::()) - .subcommand("setup", setup::setup()) + .subcommand( + "echo", + from_fn(echo::).with_call_remote::(), + ) + .subcommand("setup", setup::setup::()) } -pub fn install_api() -> ParentHandler { +pub fn install_api() -> ParentHandler { ParentHandler::new() - .subcommand( + .subcommand::( "git-info", from_fn(version::git_info).with_metadata("authenticated", Value::Bool(false)), ) - .subcommand("echo", from_fn(echo).with_remote_cli::()) - .subcommand("install", os_install::install()) + .subcommand( + "echo", + from_fn(echo::).with_call_remote::(), + ) + .subcommand("install", os_install::install::()) +} + +pub fn expanded_api() -> ParentHandler { + main_api() + .subcommand("init", from_fn_blocking(developer::init).no_display()) + .subcommand("pubkey", from_fn_blocking(developer::pubkey)) + .subcommand("diagnostic", diagnostic::diagnostic::()) + .subcommand("setup", setup::setup::()) + .subcommand("install", os_install::install::()) + .subcommand("registry", registry::registry_api::()) } diff --git a/core/startos/src/logs.rs b/core/startos/src/logs.rs index d8b7c898f..340b04b8b 100644 --- a/core/startos/src/logs.rs +++ b/core/startos/src/logs.rs @@ -4,13 +4,17 @@ use std::time::{Duration, UNIX_EPOCH}; use axum::extract::ws::{self, WebSocket}; use chrono::{DateTime, Utc}; -use clap::Parser; +use clap::{Args, FromArgMatches, Parser}; use color_eyre::eyre::eyre; use futures::stream::BoxStream; -use futures::{FutureExt, Stream, StreamExt, TryStreamExt}; +use futures::{Future, FutureExt, Stream, StreamExt, TryStreamExt}; +use itertools::Itertools; use models::PackageId; use rpc_toolkit::yajrc::RpcError; -use rpc_toolkit::{command, from_fn_async, CallRemote, Empty, HandlerExt, ParentHandler}; +use rpc_toolkit::{ + from_fn_async, CallRemote, Context, Empty, HandlerArgs, HandlerExt, HandlerFor, ParentHandler, +}; +use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::{Child, Command}; @@ -19,10 +23,10 @@ use tokio_tungstenite::tungstenite::Message; use tracing::instrument; use crate::context::{CliContext, RpcContext}; -use crate::core::rpc_continuations::{RequestGuid, RpcContinuation}; use crate::error::ResultExt; use crate::lxc::ContainerId; use crate::prelude::*; +use crate::rpc_continuations::{RequestGuid, RpcContinuation, RpcContinuations}; use crate::util::serde::Reversible; use crate::util::Invoke; @@ -211,7 +215,6 @@ fn deserialize_log_message<'de, D: serde::de::Deserializer<'de>>( pub enum LogSource { Kernel, Unit(&'static str), - System, Container(ContainerId), } @@ -220,168 +223,195 @@ pub const SYSTEM_UNIT: &str = "startd"; #[derive(Deserialize, Serialize, Parser)] #[serde(rename_all = "camelCase")] #[command(rename_all = "kebab-case")] -pub struct LogsParam { +pub struct PackageIdParams { id: PackageId, +} + +#[derive(Deserialize, Serialize, Parser)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct LogsParams { + #[command(flatten)] + #[serde(flatten)] + extra: Extra, #[arg(short = 'l', long = "limit")] limit: Option, - #[arg(short = 'c', long = "cursor")] + #[arg(short = 'c', long = "cursor", conflicts_with = "follow")] cursor: Option, - #[arg(short = 'B', long = "before")] + #[arg(short = 'B', long = "before", conflicts_with = "follow")] #[serde(default)] before: bool, +} + +#[derive(Deserialize, Serialize, Parser)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct CliLogsParams { + #[command(flatten)] + #[serde(flatten)] + rpc_params: LogsParams, #[arg(short = 'f', long = "follow")] #[serde(default)] follow: bool, } -pub fn logs() -> ParentHandler { - ParentHandler::::new() +#[allow(private_bounds)] +pub fn logs< + C: Context + AsRef, + Extra: FromArgMatches + Serialize + DeserializeOwned + Args + Send + Sync + 'static, +>( + source: impl for<'a> LogSourceFn<'a, C, Extra>, +) -> ParentHandler> { + ParentHandler::new() .root_handler( - from_fn_async(cli_logs) - .no_display() - .with_inherited(|params, _| params), - ) - .root_handler( - from_fn_async(logs_nofollow) + logs_nofollow::(source.clone()) .with_inherited(|params, _| params) .no_cli(), ) .subcommand( "follow", - from_fn_async(logs_follow) + logs_follow::(source) .with_inherited(|params, _| params) .no_cli(), ) } -pub async fn cli_logs( - ctx: CliContext, - _: Empty, - LogsParam { - id, - limit, - cursor, - before, - follow, - }: LogsParam, -) -> Result<(), RpcError> { - if follow { - if cursor.is_some() { - return Err(RpcError::from(Error::new( - eyre!("The argument '--cursor ' cannot be used with '--follow'"), - crate::ErrorKind::InvalidRequest, - ))); - } - if before { - return Err(RpcError::from(Error::new( - eyre!("The argument '--before' cannot be used with '--follow'"), - crate::ErrorKind::InvalidRequest, - ))); - } - cli_logs_generic_follow(ctx, "package.logs.follow", Some(id), limit).await - } else { - cli_logs_generic_nofollow(ctx, "package.logs", Some(id), limit, cursor, before).await - } -} -pub async fn logs_nofollow( - ctx: RpcContext, - _: Empty, - LogsParam { - id, - limit, - cursor, - before, + +pub async fn cli_logs( + HandlerArgs { + context: ctx, + parent_method, + method, + params: CliLogsParams { rpc_params, follow }, .. - }: LogsParam, -) -> Result { - let container_id = ctx - .services - .get(&id) - .await - .as_ref() - .map(|x| x.container_id()) - .ok_or_else(|| { - Error::new( - eyre!("No service found with id: {}", id), - ErrorKind::NotFound, - ) - })??; - fetch_logs(LogSource::Container(container_id), limit, cursor, before).await -} -pub async fn logs_follow( - ctx: RpcContext, - _: Empty, - LogsParam { id, limit, .. }: LogsParam, -) -> Result { - let container_id = ctx - .services - .get(&id) - .await - .as_ref() - .map(|x| x.container_id()) - .ok_or_else(|| { - Error::new( - eyre!("No service found with id: {}", id), - ErrorKind::NotFound, - ) - })??; - follow_logs(ctx, LogSource::Container(container_id), limit).await -} + }: HandlerArgs>, +) -> Result<(), RpcError> +where + CliContext: CallRemote, + Extra: FromArgMatches + Args + Serialize + Send + Sync, +{ + let method = parent_method + .into_iter() + .chain(method) + .chain(follow.then_some("follow")) + .join("."); -pub async fn cli_logs_generic_nofollow( - ctx: CliContext, - method: &str, - id: Option, - limit: Option, - cursor: Option, - before: bool, -) -> Result<(), RpcError> { - let res = from_value::( - ctx.call_remote( - method, - imbl_value::json!({ - "id": id, - "limit": limit, - "cursor": cursor, - "before": before, - }), - ) - .await?, - )?; + if follow { + let res = from_value::( + ctx.call_remote::(&method, to_value(&rpc_params)?) + .await?, + )?; - for entry in res.entries.iter() { - println!("{}", entry); - } + let mut stream = ctx.ws_continuation(res.guid).await?; + while let Some(log) = stream.try_next().await? { + if let Message::Text(log) = log { + println!("{}", serde_json::from_str::(&log)?); + } + } + } else { + let res = from_value::( + ctx.call_remote::(&method, to_value(&rpc_params)?) + .await?, + )?; - Ok(()) -} - -pub async fn cli_logs_generic_follow( - ctx: CliContext, - method: &str, - id: Option, - limit: Option, -) -> Result<(), RpcError> { - let res = from_value::( - ctx.call_remote( - method, - imbl_value::json!({ - "id": id, - "limit": limit, - }), - ) - .await?, - )?; - - let mut stream = ctx.ws_continuation(res.guid).await?; - while let Some(log) = stream.try_next().await? { - if let Message::Text(log) = log { - println!("{}", serde_json::from_str::(&log)?); + for entry in res.entries.iter() { + println!("{}", entry); } } Ok(()) } +trait LogSourceFn<'a, Context, Extra>: Clone + Send + Sync + 'static { + type Fut: Future> + Send + 'a; + fn call(&self, ctx: &'a Context, extra: Extra) -> Self::Fut; +} + +impl<'a, C: Context, Extra, F, Fut> LogSourceFn<'a, C, Extra> for F +where + F: Fn(&'a C, Extra) -> Fut + Clone + Send + Sync + 'static, + Fut: Future> + Send + 'a, +{ + type Fut = Fut; + fn call(&self, ctx: &'a C, extra: Extra) -> Self::Fut { + self(ctx, extra) + } +} + +fn logs_nofollow( + f: impl for<'a> LogSourceFn<'a, C, Extra>, +) -> impl HandlerFor, Ok = LogResponse, Err = Error> +where + C: Context, + Extra: FromArgMatches + Args + Send + Sync + 'static, +{ + from_fn_async( + move |HandlerArgs { + context, + inherited_params: + LogsParams { + extra, + limit, + cursor, + before, + }, + .. + }: HandlerArgs>| { + let f = f.clone(); + async move { fetch_logs(f.call(&context, extra).await?, limit, cursor, before).await } + }, + ) +} + +fn logs_follow< + C: Context + AsRef, + Extra: FromArgMatches + Args + Send + Sync + 'static, +>( + f: impl for<'a> LogSourceFn<'a, C, Extra>, +) -> impl HandlerFor< + C, + Params = Empty, + InheritedParams = LogsParams, + Ok = LogFollowResponse, + Err = Error, +> { + from_fn_async( + move |HandlerArgs { + context, + inherited_params: LogsParams { extra, limit, .. }, + .. + }: HandlerArgs>| { + let f = f.clone(); + async move { + let src = f.call(&context, extra).await?; + follow_logs(context, src, limit).await + } + }, + ) +} + +async fn get_package_id( + ctx: &RpcContext, + PackageIdParams { id }: PackageIdParams, +) -> Result { + let container_id = ctx + .services + .get(&id) + .await + .as_ref() + .map(|x| x.container_id()) + .ok_or_else(|| { + Error::new( + eyre!("No service found with id: {}", id), + ErrorKind::NotFound, + ) + })??; + Ok(LogSource::Container(container_id)) +} + +pub fn package_logs() -> ParentHandler> { + logs::(get_package_id) +} + pub async fn journalctl( id: LogSource, limit: usize, @@ -474,10 +504,6 @@ fn gen_journalctl_command(id: &LogSource) -> Command { cmd.arg("-u"); cmd.arg(id); } - LogSource::System => { - cmd.arg("-u"); - cmd.arg(SYSTEM_UNIT); - } LogSource::Container(_container_id) => { cmd.arg("-u").arg("container-runtime.service"); } @@ -533,8 +559,8 @@ pub async fn fetch_logs( } #[instrument(skip_all)] -pub async fn follow_logs( - ctx: RpcContext, +pub async fn follow_logs>( + ctx: Context, id: LogSource, limit: Option, ) -> Result { @@ -556,23 +582,24 @@ pub async fn follow_logs( } let guid = RequestGuid::new(); - ctx.add_continuation( - guid.clone(), - RpcContinuation::ws( - Box::new(move |socket| { - ws_handler(first_entry, stream, socket) - .map(|x| match x { - Ok(_) => (), - Err(e) => { - tracing::error!("Error in log stream: {}", e); - } - }) - .boxed() - }), - Duration::from_secs(30), - ), - ) - .await; + ctx.as_ref() + .add( + guid.clone(), + RpcContinuation::ws( + Box::new(move |socket| { + ws_handler(first_entry, stream, socket) + .map(|x| match x { + Ok(_) => (), + Err(e) => { + tracing::error!("Error in log stream: {}", e); + } + }) + .boxed() + }), + Duration::from_secs(30), + ), + ) + .await; Ok(LogFollowResponse { start_cursor, guid }) } diff --git a/core/startos/src/lxc/mod.rs b/core/startos/src/lxc/mod.rs index 9dfdff7f9..084364c4a 100644 --- a/core/startos/src/lxc/mod.rs +++ b/core/startos/src/lxc/mod.rs @@ -12,8 +12,8 @@ use imbl_value::{InOMap, InternedString}; use models::InvalidId; use rpc_toolkit::yajrc::{RpcError, RpcResponse}; use rpc_toolkit::{ - from_fn_async, AnyContext, CallRemoteHandler, GenericRpcMethod, Handler, HandlerArgs, - HandlerExt, ParentHandler, RpcRequest, + from_fn_async, CallRemoteHandler, Context, Empty, GenericRpcMethod, HandlerArgs, HandlerExt, + HandlerFor, ParentHandler, RpcRequest, }; use rustyline_async::{ReadlineEvent, SharedWriter}; use serde::{Deserialize, Serialize}; @@ -25,7 +25,6 @@ use tokio::time::Instant; use ts_rs::TS; use crate::context::{CliContext, RpcContext}; -use crate::core::rpc_continuations::{RequestGuid, RpcContinuation}; use crate::disk::mount::filesystem::bind::Bind; use crate::disk::mount::filesystem::block_dev::BlockDev; use crate::disk::mount::filesystem::idmapped::IdMapped; @@ -34,6 +33,7 @@ use crate::disk::mount::filesystem::{MountType, ReadWrite}; use crate::disk::mount::guard::{GenericMountGuard, MountGuard, TmpMountGuard}; use crate::disk::mount::util::unmount; use crate::prelude::*; +use crate::rpc_continuations::{RequestGuid, RpcContinuation}; use crate::util::clap::FromStrParser; use crate::util::rpc_client::UnixRpcClient; use crate::util::{new_guid, Invoke}; @@ -370,16 +370,16 @@ impl Drop for LxcContainer { #[derive(Default, Serialize)] pub struct LxcConfig {} -pub fn lxc() -> ParentHandler { +pub fn lxc() -> ParentHandler { ParentHandler::new() .subcommand( "create", - from_fn_async(create).with_remote_cli::(), + from_fn_async(create).with_call_remote::(), ) .subcommand( "list", from_fn_async(list) - .with_custom_display_fn::(|_, res| { + .with_custom_display_fn(|_, res| { use prettytable::*; let mut table = table!([bc => "GUID"]); for guid in res { @@ -388,13 +388,13 @@ pub fn lxc() -> ParentHandler { table.printstd(); Ok(()) }) - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "remove", from_fn_async(remove) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand("connect", from_fn_async(connect_rpc).no_cli()) .subcommand("connect", from_fn_async(connect_rpc_cli).no_display()) @@ -448,59 +448,59 @@ pub async fn connect(ctx: &RpcContext, container: &LxcContainer) -> Result break, - Some(Ok(Message::Text(txt))) => { - let mut id = None; - let result = async { - let req: RpcRequest = - serde_json::from_str(&txt).map_err(|e| RpcError { - data: Some(serde_json::Value::String( - e.to_string(), - )), - ..rpc_toolkit::yajrc::PARSE_ERROR - })?; - id = req.id; - rpc.request(req.method, req.params).await + ctx.rpc_continuations + .add( + guid.clone(), + RpcContinuation::ws( + Box::new(|mut ws| { + async move { + if let Err(e) = async { + loop { + match ws.next().await { + None => break, + Some(Ok(Message::Text(txt))) => { + let mut id = None; + let result = async { + let req: RpcRequest = serde_json::from_str(&txt) + .map_err(|e| RpcError { + data: Some(serde_json::Value::String( + e.to_string(), + )), + ..rpc_toolkit::yajrc::PARSE_ERROR + })?; + id = req.id; + rpc.request(req.method, req.params).await + } + .await; + ws.send(Message::Text( + serde_json::to_string( + &RpcResponse:: { id, result }, + ) + .with_kind(ErrorKind::Serialization)?, + )) + .await + .with_kind(ErrorKind::Network)?; + } + Some(Ok(_)) => (), + Some(Err(e)) => { + return Err(Error::new(e, ErrorKind::Network)); } - .await; - ws.send(Message::Text( - serde_json::to_string(&RpcResponse:: { - id, - result, - }) - .with_kind(ErrorKind::Serialization)?, - )) - .await - .with_kind(ErrorKind::Network)?; - } - Some(Ok(_)) => (), - Some(Err(e)) => { - return Err(Error::new(e, ErrorKind::Network)); } } + Ok::<_, Error>(()) + } + .await + { + tracing::error!("{e}"); + tracing::debug!("{e:?}"); } - Ok::<_, Error>(()) } - .await - { - tracing::error!("{e}"); - tracing::debug!("{e:?}"); - } - } - .boxed() - }), - Duration::from_secs(30), - ), - ) - .await; + .boxed() + }), + Duration::from_secs(30), + ), + ) + .await; Ok(guid) } @@ -614,11 +614,25 @@ pub async fn connect_cli(ctx: &CliContext, guid: RequestGuid) -> Result<(), Erro } pub async fn connect_rpc_cli( - handle_args: HandlerArgs, + HandlerArgs { + context, + parent_method, + method, + params, + inherited_params, + raw_params, + }: HandlerArgs, ) -> Result<(), Error> { - let ctx = handle_args.context.clone(); - let guid = CallRemoteHandler::::new(from_fn_async(connect_rpc)) - .handle_async(handle_args) + let ctx = context.clone(); + let guid = CallRemoteHandler::::new(from_fn_async(connect_rpc)) + .handle_async(HandlerArgs { + context, + parent_method, + method, + params: rpc_toolkit::util::Flat(params, Empty {}), + inherited_params, + raw_params, + }) .await?; connect_cli(&ctx, guid).await diff --git a/core/startos/src/middleware/auth.rs b/core/startos/src/middleware/auth.rs index 30ae56744..1852c4890 100644 --- a/core/startos/src/middleware/auth.rs +++ b/core/startos/src/middleware/auth.rs @@ -245,7 +245,6 @@ impl Borrow for HashSessionToken { } #[derive(Deserialize)] -#[serde(rename_all = "camelCase")] pub struct Metadata { #[serde(default = "const_true")] authenticated: bool, @@ -274,7 +273,6 @@ impl Auth { } } } -#[async_trait::async_trait] impl Middleware for Auth { type Metadata = Metadata; async fn process_http_request( @@ -306,7 +304,7 @@ impl Middleware for Auth { }); } if let Some(user_agent) = self.user_agent.as_ref().and_then(|h| h.to_str().ok()) { - request.params["user-agent"] = Value::String(Arc::new(user_agent.to_owned())) + request.params["__auth_userAgent"] = Value::String(Arc::new(user_agent.to_owned())) // TODO: will this panic? } } else if metadata.authenticated { @@ -318,7 +316,7 @@ impl Middleware for Auth { }) } Ok(HasValidSession(SessionType::Session(s))) if metadata.get_session => { - request.params["session"] = + request.params["__auth_session"] = Value::String(Arc::new(s.hashed().deref().to_owned())); // TODO: will this panic? } diff --git a/core/startos/src/middleware/cors.rs b/core/startos/src/middleware/cors.rs index 60a472cdd..a8c406a8a 100644 --- a/core/startos/src/middleware/cors.rs +++ b/core/startos/src/middleware/cors.rs @@ -44,8 +44,7 @@ impl Cors { } } } -#[async_trait::async_trait] -impl Middleware for Cors { +impl Middleware for Cors { type Metadata = Empty; async fn process_http_request( &mut self, diff --git a/core/startos/src/middleware/db.rs b/core/startos/src/middleware/db.rs index b8cdaa231..dca0418e5 100644 --- a/core/startos/src/middleware/db.rs +++ b/core/startos/src/middleware/db.rs @@ -23,7 +23,6 @@ impl SyncDb { } } -#[async_trait::async_trait] impl Middleware for SyncDb { type Metadata = Metadata; async fn process_rpc_request( diff --git a/core/startos/src/middleware/diagnostic.rs b/core/startos/src/middleware/diagnostic.rs index f779d632f..2a7467e34 100644 --- a/core/startos/src/middleware/diagnostic.rs +++ b/core/startos/src/middleware/diagnostic.rs @@ -14,7 +14,6 @@ impl DiagnosticMode { } } -#[async_trait::async_trait] impl Middleware for DiagnosticMode { type Metadata = Empty; async fn process_rpc_request( diff --git a/core/startos/src/net/dhcp.rs b/core/startos/src/net/dhcp.rs index b0d538e81..104cda961 100644 --- a/core/startos/src/net/dhcp.rs +++ b/core/startos/src/net/dhcp.rs @@ -3,7 +3,7 @@ use std::net::IpAddr; use clap::Parser; use futures::TryStreamExt; -use rpc_toolkit::{from_fn_async, HandlerExt, ParentHandler}; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; use ts_rs::TS; @@ -53,12 +53,12 @@ pub async fn init_ips() -> Result, Error> { } // #[command(subcommands(update))] -pub fn dhcp() -> ParentHandler { +pub fn dhcp() -> ParentHandler { ParentHandler::new().subcommand( "update", from_fn_async::<_, _, (), Error, (RpcContext, UpdateParams)>(update) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) } #[derive(Deserialize, Serialize, Parser, TS)] diff --git a/core/startos/src/net/mod.rs b/core/startos/src/net/mod.rs index aaf019e66..e55da4206 100644 --- a/core/startos/src/net/mod.rs +++ b/core/startos/src/net/mod.rs @@ -1,4 +1,4 @@ -use rpc_toolkit::ParentHandler; +use rpc_toolkit::{Context, ParentHandler}; pub mod dhcp; pub mod dns; @@ -18,8 +18,8 @@ pub mod wifi; pub const PACKAGE_CERT_PATH: &str = "/var/lib/embassy/ssl"; -pub fn net() -> ParentHandler { +pub fn net() -> ParentHandler { ParentHandler::new() - .subcommand("tor", tor::tor()) - .subcommand("dhcp", dhcp::dhcp()) + .subcommand("tor", tor::tor::()) + .subcommand("dhcp", dhcp::dhcp::()) } diff --git a/core/startos/src/net/net_controller.rs b/core/startos/src/net/net_controller.rs index 97f38567d..aae98ae96 100644 --- a/core/startos/src/net/net_controller.rs +++ b/core/startos/src/net/net_controller.rs @@ -6,12 +6,10 @@ use color_eyre::eyre::eyre; use imbl::OrdMap; use lazy_format::lazy_format; use models::{HostId, OptionExt, PackageId}; -use patch_db::PatchDb; -use tokio::sync::Mutex; use torut::onion::{OnionAddressV3, TorSecretKeyV3}; use tracing::instrument; -use crate::db::prelude::PatchDbExt; +use crate::db::model::Database; use crate::error::ErrorCollection; use crate::hostname::Hostname; use crate::net::dns::DnsController; @@ -21,11 +19,12 @@ use crate::net::host::binding::{AddSslOptions, BindOptions}; use crate::net::host::{Host, HostKind}; use crate::net::tor::TorController; use crate::net::vhost::{AlpnInfo, VHostController}; +use crate::prelude::*; use crate::util::serde::MaybeUtf8String; -use crate::{Error, HOST_IP}; +use crate::HOST_IP; pub struct NetController { - db: PatchDb, + db: TypedPatchDb, pub(super) tor: TorController, pub(super) vhost: VHostController, pub(super) dns: DnsController, @@ -36,7 +35,7 @@ pub struct NetController { impl NetController { #[instrument(skip_all)] pub async fn init( - db: PatchDb, + db: TypedPatchDb, tor_control: SocketAddr, tor_socks: SocketAddr, dns_bind: &[SocketAddr], @@ -394,14 +393,23 @@ impl NetService { pub fn get_ext_port(&self, host_id: HostId, internal_port: u16) -> Result { let host_id_binds = self.binds.get_key_value(&host_id); match host_id_binds { - Some((id, binds)) => { + Some((_, binds)) => { if let Some(ext_port_info) = binds.lan.get(&internal_port) { Ok(ext_port_info.0) } else { - Err(Error::new(eyre!("Internal Port {} not found in NetService binds", internal_port), crate::ErrorKind::NotFound)) + Err(Error::new( + eyre!( + "Internal Port {} not found in NetService binds", + internal_port + ), + crate::ErrorKind::NotFound, + )) } - }, - None => Err(Error::new(eyre!("HostID {} not found in NetService binds", host_id), crate::ErrorKind::NotFound)) + } + None => Err(Error::new( + eyre!("HostID {} not found in NetService binds", host_id), + crate::ErrorKind::NotFound, + )), } } } diff --git a/core/startos/src/net/ssl.rs b/core/startos/src/net/ssl.rs index 245881c55..382006072 100644 --- a/core/startos/src/net/ssl.rs +++ b/core/startos/src/net/ssl.rs @@ -17,7 +17,6 @@ use openssl::x509::{X509Builder, X509Extension, X509NameBuilder, X509}; use openssl::*; use patch_db::HasModel; use serde::{Deserialize, Serialize}; -use tokio::sync::Mutex; use tracing::instrument; use crate::account::AccountInfo; diff --git a/core/startos/src/net/static_server.rs b/core/startos/src/net/static_server.rs index fa71672b3..85737598b 100644 --- a/core/startos/src/net/static_server.rs +++ b/core/startos/src/net/static_server.rs @@ -24,18 +24,19 @@ use tokio::io::BufReader; use tokio_util::io::ReaderStream; use crate::context::{DiagnosticContext, InstallContext, RpcContext, SetupContext}; -use crate::core::rpc_continuations::RequestGuid; use crate::db::subscribe; use crate::hostname::Hostname; use crate::middleware::auth::{Auth, HasValidSession}; use crate::middleware::cors::Cors; use crate::middleware::db::SyncDb; use crate::middleware::diagnostic::DiagnosticMode; +use crate::rpc_continuations::RequestGuid; use crate::{diagnostic_api, install_api, main_api, setup_api, Error, ErrorKind, ResultExt}; const NOT_FOUND: &[u8] = b"Not Found"; const METHOD_NOT_ALLOWED: &[u8] = b"Method Not Allowed"; const NOT_AUTHORIZED: &[u8] = b"Not Authorized"; +const INTERNAL_SERVER_ERROR: &[u8] = b"Internal Server Error"; #[cfg(all(feature = "daemon", not(feature = "test")))] const EMBEDDED_UIS: Dir<'_> = @@ -112,7 +113,7 @@ pub fn main_ui_server_router(ctx: RpcContext) -> Router { .route("/rpc/*path", { let ctx = ctx.clone(); post( - Server::new(move || ready(Ok(ctx.clone())), main_api()) + Server::new(move || ready(Ok(ctx.clone())), main_api::()) .middleware(Cors::new()) .middleware(Auth::new()) .middleware(SyncDb::new()), @@ -140,7 +141,7 @@ pub fn main_ui_server_router(ctx: RpcContext) -> Router { tracing::debug!("No Guid Path"); bad_request() } - Some(guid) => match ctx.get_ws_continuation_handler(&guid).await { + Some(guid) => match ctx.rpc_continuations.get_ws_handler(&guid).await { Some(cont) => ws.on_upgrade(cont), _ => not_found(), }, @@ -163,7 +164,7 @@ pub fn main_ui_server_router(ctx: RpcContext) -> Router { tracing::debug!("No Guid Path"); bad_request() } - Some(guid) => match ctx.get_rest_continuation_handler(&guid).await { + Some(guid) => match ctx.rpc_continuations.get_rest_handler(&guid).await { None => not_found(), Some(cont) => cont(request).await.unwrap_or_else(server_error), }, @@ -216,7 +217,7 @@ async fn if_authorized< ) -> Result { if let Err(e) = HasValidSession::from_header(parts.headers.get(http::header::COOKIE), ctx).await { - un_authorized(e, parts.uri.path()) + Ok(unauthorized(e, parts.uri.path())) } else { f().await } @@ -305,17 +306,17 @@ async fn main_start_os_ui(req: Request, ctx: RpcContext) -> Result Result { +pub fn unauthorized(err: Error, path: &str) -> Response { tracing::warn!("unauthorized for {} @{:?}", err, path); tracing::debug!("{:?}", err); - Ok(Response::builder() + Response::builder() .status(StatusCode::UNAUTHORIZED) .body(NOT_AUTHORIZED.into()) - .unwrap()) + .unwrap() } /// HTTP status code 404 -fn not_found() -> Response { +pub fn not_found() -> Response { Response::builder() .status(StatusCode::NOT_FOUND) .body(NOT_FOUND.into()) @@ -323,21 +324,23 @@ fn not_found() -> Response { } /// HTTP status code 405 -fn method_not_allowed() -> Response { +pub fn method_not_allowed() -> Response { Response::builder() .status(StatusCode::METHOD_NOT_ALLOWED) .body(METHOD_NOT_ALLOWED.into()) .unwrap() } -fn server_error(err: Error) -> Response { +pub fn server_error(err: Error) -> Response { + tracing::error!("internal server error: {}", err); + tracing::debug!("{:?}", err); Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(err.to_string().into()) + .body(INTERNAL_SERVER_ERROR.into()) .unwrap() } -fn bad_request() -> Response { +pub fn bad_request() -> Response { Response::builder() .status(StatusCode::BAD_REQUEST) .body(Body::empty()) diff --git a/core/startos/src/net/tor.rs b/core/startos/src/net/tor.rs index dfca0fc0b..31eae5fab 100644 --- a/core/startos/src/net/tor.rs +++ b/core/startos/src/net/tor.rs @@ -12,8 +12,7 @@ use helpers::NonDetachingJoinHandle; use itertools::Itertools; use lazy_static::lazy_static; use regex::Regex; -use rpc_toolkit::yajrc::RpcError; -use rpc_toolkit::{command, from_fn_async, AnyContext, Empty, HandlerExt, ParentHandler}; +use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use tokio::net::TcpStream; use tokio::process::Command; @@ -25,10 +24,7 @@ use tracing::instrument; use ts_rs::TS; use crate::context::{CliContext, RpcContext}; -use crate::logs::{ - cli_logs_generic_follow, cli_logs_generic_nofollow, fetch_logs, follow_logs, journalctl, - LogFollowResponse, LogResponse, LogSource, -}; +use crate::logs::{journalctl, LogSource, LogsParams}; use crate::prelude::*; use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat}; use crate::util::Invoke; @@ -86,23 +82,27 @@ lazy_static! { static ref PROGRESS_REGEX: Regex = Regex::new("PROGRESS=([0-9]+)").unwrap(); } -pub fn tor() -> ParentHandler { +pub fn tor() -> ParentHandler { ParentHandler::new() .subcommand( "list-services", from_fn_async(list_services) .with_display_serializable() - .with_custom_display_fn::(|handle, result| { + .with_custom_display_fn(|handle, result| { Ok(display_services(handle.params, result)) }) - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand("logs", logs()) + .subcommand( + "logs", + from_fn_async(crate::logs::cli_logs::).no_display(), + ) .subcommand( "reset", from_fn_async(reset) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) } #[derive(Deserialize, Serialize, Parser, TS)] @@ -143,89 +143,10 @@ pub async fn list_services(ctx: RpcContext, _: Empty) -> Result, - #[arg(short = 'c', long = "cursor")] - cursor: Option, - #[arg(short = 'B', long = "before")] - #[serde(default)] - before: bool, - #[arg(short = 'f', long = "follow")] - #[serde(default)] - follow: bool, -} - -pub fn logs() -> ParentHandler { - ParentHandler::new() - .root_handler( - from_fn_async(cli_logs) - .no_display() - .with_inherited(|params, _| params), - ) - .root_handler( - from_fn_async(logs_nofollow) - .with_inherited(|params, _| params) - .no_cli(), - ) - .subcommand( - "follow", - from_fn_async(logs_follow) - .with_inherited(|params, _| params) - .no_cli(), - ) -} -pub async fn cli_logs( - ctx: CliContext, - _: Empty, - LogsParams { - limit, - cursor, - before, - follow, - }: LogsParams, -) -> Result<(), RpcError> { - if follow { - if cursor.is_some() { - return Err(RpcError::from(Error::new( - eyre!("The argument '--cursor ' cannot be used with '--follow'"), - crate::ErrorKind::InvalidRequest, - ))); - } - if before { - return Err(RpcError::from(Error::new( - eyre!("The argument '--before' cannot be used with '--follow'"), - crate::ErrorKind::InvalidRequest, - ))); - } - cli_logs_generic_follow(ctx, "net.tor.logs.follow", None, limit).await - } else { - cli_logs_generic_nofollow(ctx, "net.tor.logs", None, limit, cursor, before).await - } -} -pub async fn logs_nofollow( - _: AnyContext, - _: Empty, - LogsParams { - limit, - cursor, - before, - .. - }: LogsParams, -) -> Result { - fetch_logs(LogSource::Unit(SYSTEMD_UNIT), limit, cursor, before).await -} - -pub async fn logs_follow( - ctx: RpcContext, - _: Empty, - LogsParams { limit, .. }: LogsParams, -) -> Result { - follow_logs(ctx, LogSource::Unit(SYSTEMD_UNIT), limit).await +pub fn logs() -> ParentHandler { + crate::logs::logs::(|_: &RpcContext, _| async { + Ok(LogSource::Unit(SYSTEMD_UNIT)) + }) } fn event_handler(_event: AsyncEvent<'static>) -> BoxFuture<'static, Result<(), ConnError>> { diff --git a/core/startos/src/net/vhost.rs b/core/startos/src/net/vhost.rs index 47cf30ad5..8dc1e6da3 100644 --- a/core/startos/src/net/vhost.rs +++ b/core/startos/src/net/vhost.rs @@ -19,6 +19,7 @@ use tokio_rustls::{LazyConfigAcceptor, TlsConnector}; use tracing::instrument; use ts_rs::TS; +use crate::db::model::Database; use crate::prelude::*; use crate::util::io::{BackTrackingReader, TimeoutStream}; use crate::util::serde::MaybeUtf8String; @@ -26,11 +27,11 @@ use crate::util::serde::MaybeUtf8String; // not allowed: <=1024, >=32768, 5355, 5432, 9050, 6010, 9051, 5353 pub struct VHostController { - db: PatchDb, + db: TypedPatchDb, servers: Mutex>, } impl VHostController { - pub fn new(db: PatchDb) -> Self { + pub fn new(db: TypedPatchDb) -> Self { Self { db, servers: Mutex::new(BTreeMap::new()), @@ -100,7 +101,7 @@ struct VHostServer { } impl VHostServer { #[instrument(skip_all)] - async fn new(port: u16, db: PatchDb) -> Result { + async fn new(port: u16, db: TypedPatchDb) -> Result { // check if port allowed let listener = TcpListener::bind(SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), port)) .await diff --git a/core/startos/src/net/wifi.rs b/core/startos/src/net/wifi.rs index 93f610a42..298fad71f 100644 --- a/core/startos/src/net/wifi.rs +++ b/core/startos/src/net/wifi.rs @@ -8,7 +8,7 @@ use clap::Parser; use isocountry::CountryCode; use lazy_static::lazy_static; use regex::Regex; -use rpc_toolkit::{command, from_fn_async, AnyContext, Empty, HandlerExt, ParentHandler}; +use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use tokio::process::Command; use tokio::sync::RwLock; @@ -17,6 +17,7 @@ use ts_rs::TS; use crate::context::{CliContext, RpcContext}; use crate::db::model::public::WifiInfo; +use crate::db::model::Database; use crate::net::utils::find_wifi_iface; use crate::prelude::*; use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat}; @@ -36,57 +37,55 @@ pub fn wifi_manager(ctx: &RpcContext) -> Result<&WifiManager, Error> { } } -pub fn wifi() -> ParentHandler { +pub fn wifi() -> ParentHandler { ParentHandler::new() .subcommand( "add", from_fn_async(add) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "connect", from_fn_async(connect) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "delete", from_fn_async(delete) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "get", from_fn_async(get) .with_display_serializable() - .with_custom_display_fn::(|handle, result| { + .with_custom_display_fn(|handle, result| { Ok(display_wifi_info(handle.params, result)) }) - .with_remote_cli::(), + .with_call_remote::(), ) - .subcommand("country", country()) - .subcommand("available", available()) + .subcommand("country", country::()) + .subcommand("available", available::()) } -pub fn available() -> ParentHandler { +pub fn available() -> ParentHandler { ParentHandler::new().subcommand( "get", from_fn_async(get_available) .with_display_serializable() - .with_custom_display_fn::(|handle, result| { - Ok(display_wifi_list(handle.params, result)) - }) - .with_remote_cli::(), + .with_custom_display_fn(|handle, result| Ok(display_wifi_list(handle.params, result))) + .with_call_remote::(), ) } -pub fn country() -> ParentHandler { +pub fn country() -> ParentHandler { ParentHandler::new().subcommand( "set", from_fn_async(set_country) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) } @@ -113,7 +112,7 @@ pub async fn add(ctx: RpcContext, AddParams { ssid, password }: AddParams) -> Re )); } async fn add_procedure( - db: PatchDb, + db: TypedPatchDb, wifi_manager: WifiManager, ssid: &Ssid, password: &Psk, @@ -170,7 +169,7 @@ pub async fn connect(ctx: RpcContext, SsidParams { ssid }: SsidParams) -> Result )); } async fn connect_procedure( - db: PatchDb, + db: TypedPatchDb, wifi_manager: WifiManager, ssid: &Ssid, ) -> Result<(), Error> { @@ -718,7 +717,7 @@ impl WpaCli { Ok(()) } - pub async fn save_config(&mut self, db: PatchDb) -> Result<(), Error> { + pub async fn save_config(&mut self, db: TypedPatchDb) -> Result<(), Error> { let new_country = self.get_country_low().await?; db.mutate(|d| { d.as_public_mut() @@ -758,7 +757,11 @@ impl WpaCli { .collect()) } #[instrument(skip_all)] - pub async fn select_network(&mut self, db: PatchDb, ssid: &Ssid) -> Result { + pub async fn select_network( + &mut self, + db: TypedPatchDb, + ssid: &Ssid, + ) -> Result { let m_id = self.check_active_network(ssid).await?; match m_id { None => Err(Error::new( @@ -810,7 +813,11 @@ impl WpaCli { } } #[instrument(skip_all)] - pub async fn remove_network(&mut self, db: PatchDb, ssid: &Ssid) -> Result { + pub async fn remove_network( + &mut self, + db: TypedPatchDb, + ssid: &Ssid, + ) -> Result { let found_networks = self.find_networks(ssid).await?; if found_networks.is_empty() { return Ok(true); @@ -824,7 +831,7 @@ impl WpaCli { #[instrument(skip_all)] pub async fn set_add_network( &mut self, - db: PatchDb, + db: TypedPatchDb, ssid: &Ssid, psk: &Psk, ) -> Result<(), Error> { @@ -833,7 +840,12 @@ impl WpaCli { Ok(()) } #[instrument(skip_all)] - pub async fn add_network(&mut self, db: PatchDb, ssid: &Ssid, psk: &Psk) -> Result<(), Error> { + pub async fn add_network( + &mut self, + db: TypedPatchDb, + ssid: &Ssid, + psk: &Psk, + ) -> Result<(), Error> { self.add_network_low(ssid, psk).await?; self.save_config(db).await?; Ok(()) diff --git a/core/startos/src/notifications.rs b/core/startos/src/notifications.rs index 8a5732462..fde3d5e80 100644 --- a/core/startos/src/notifications.rs +++ b/core/startos/src/notifications.rs @@ -8,7 +8,7 @@ use clap::Parser; use color_eyre::eyre::eyre; use imbl_value::InternedString; use models::PackageId; -use rpc_toolkit::{command, from_fn_async, HandlerExt, ParentHandler}; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use tracing::instrument; use ts_rs::TS; @@ -21,31 +21,31 @@ use crate::util::clap::FromStrParser; use crate::util::serde::HandlerExtSerde; // #[command(subcommands(list, delete, delete_before, create))] -pub fn notification() -> ParentHandler { +pub fn notification() -> ParentHandler { ParentHandler::new() .subcommand( "list", from_fn_async(list) .with_display_serializable() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "delete", from_fn_async(delete) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "delete-before", from_fn_async(delete_before) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "create", from_fn_async(create) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) } diff --git a/core/startos/src/os_install/mod.rs b/core/startos/src/os_install/mod.rs index 4bbbe70a6..c3931b236 100644 --- a/core/startos/src/os_install/mod.rs +++ b/core/startos/src/os_install/mod.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use clap::Parser; use color_eyre::eyre::eyre; use models::Error; -use rpc_toolkit::{command, from_fn_async, AnyContext, HandlerExt, ParentHandler}; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use tokio::process::Command; use ts_rs::TS; @@ -13,11 +13,15 @@ use crate::context::{CliContext, InstallContext}; use crate::disk::mount::filesystem::bind::Bind; use crate::disk::mount::filesystem::block_dev::BlockDev; use crate::disk::mount::filesystem::efivarfs::EfiVarFs; +use crate::disk::mount::filesystem::overlayfs::OverlayFs; use crate::disk::mount::filesystem::{MountType, ReadWrite}; use crate::disk::mount::guard::{GenericMountGuard, MountGuard, TmpMountGuard}; use crate::disk::util::{DiskInfo, PartitionTable}; use crate::disk::OsPartitionInfo; use crate::net::utils::find_eth_iface; +use crate::prelude::*; +use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; +use crate::util::io::TmpDir; use crate::util::serde::IoFormat; use crate::util::Invoke; use crate::ARCH; @@ -25,33 +29,33 @@ use crate::ARCH; mod gpt; mod mbr; -pub fn install() -> ParentHandler { +pub fn install() -> ParentHandler { ParentHandler::new() - .subcommand("disk", disk()) + .subcommand("disk", disk::()) .subcommand( "execute", - from_fn_async(execute) + from_fn_async(execute::) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "reboot", from_fn_async(reboot) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) } -pub fn disk() -> ParentHandler { +pub fn disk() -> ParentHandler { ParentHandler::new().subcommand( "list", from_fn_async(list) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) } -pub async fn list() -> Result, Error> { +pub async fn list(_: InstallContext) -> Result, Error> { let skip = match async { Ok::<_, Error>( Path::new( @@ -122,8 +126,8 @@ pub struct ExecuteParams { overwrite: bool, } -pub async fn execute( - _: AnyContext, +pub async fn execute( + _: C, ExecuteParams { logicalname, mut overwrite, @@ -215,44 +219,62 @@ pub async fn execute( .arg("rootfs") .invoke(crate::ErrorKind::DiskManagement) .await?; - let rootfs = TmpMountGuard::mount(&BlockDev::new(&part_info.root), ReadWrite).await?; + + let config_path = rootfs.path().join("config"); + if tokio::fs::metadata("/tmp/config.bak").await.is_ok() { + if tokio::fs::metadata(&config_path).await.is_ok() { + tokio::fs::remove_dir_all(&config_path).await?; + } Command::new("cp") .arg("-r") .arg("/tmp/config.bak") - .arg(rootfs.path().join("config")) + .arg(&config_path) .invoke(crate::ErrorKind::Filesystem) .await?; } else { - tokio::fs::create_dir(rootfs.path().join("config")).await?; + tokio::fs::create_dir_all(&config_path).await?; } - tokio::fs::create_dir(rootfs.path().join("next")).await?; - let current = rootfs.path().join("current"); - tokio::fs::create_dir(¤t).await?; - tokio::fs::create_dir(current.join("boot")).await?; - let boot = MountGuard::mount( + let images_path = rootfs.path().join("images"); + tokio::fs::create_dir_all(&images_path).await?; + let image_path = images_path + .join(hex::encode( + &MultiCursorFile::from( + tokio::fs::File::open("/run/live/medium/live/filesystem.squashfs").await?, + ) + .blake3_mmap() + .await? + .as_bytes()[..16], + )) + .with_extension("rootfs"); + tokio::fs::copy("/run/live/medium/live/filesystem.squashfs", &image_path).await?; + // TODO: check hash of fs + let unsquash_target = TmpDir::new().await?; + let bootfs = MountGuard::mount( &BlockDev::new(&part_info.boot), - current.join("boot"), + unsquash_target.join("boot"), ReadWrite, ) .await?; - - let efi = if let Some(efi) = &part_info.efi { - Some(MountGuard::mount(&BlockDev::new(efi), current.join("boot/efi"), ReadWrite).await?) - } else { - None - }; - Command::new("unsquashfs") .arg("-n") .arg("-f") .arg("-d") - .arg(¤t) + .arg(&*unsquash_target) .arg("/run/live/medium/live/filesystem.squashfs") + .arg("boot") .invoke(crate::ErrorKind::Filesystem) .await?; + bootfs.unmount(true).await?; + unsquash_target.delete().await?; + Command::new("ln") + .arg("-rsf") + .arg(&image_path) + .arg(config_path.join("current.rootfs")) + .invoke(ErrorKind::DiskManagement) + .await?; tokio::fs::write( rootfs.path().join("config/config.yaml"), @@ -264,8 +286,55 @@ pub async fn execute( ) .await?; + let lower = TmpMountGuard::mount(&BlockDev::new(&image_path), MountType::ReadOnly).await?; + let work = config_path.join("work"); + let upper = config_path.join("overlay"); + let overlay = + TmpMountGuard::mount(&OverlayFs::new(&lower.path(), &upper, &work), ReadWrite).await?; + + let boot = MountGuard::mount( + &BlockDev::new(&part_info.boot), + overlay.path().join("boot"), + ReadWrite, + ) + .await?; + let efi = if let Some(efi) = &part_info.efi { + Some( + MountGuard::mount( + &BlockDev::new(efi), + overlay.path().join("boot/efi"), + ReadWrite, + ) + .await?, + ) + } else { + None + }; + let start_os_fs = MountGuard::mount( + &Bind::new(rootfs.path()), + overlay.path().join("media/startos/root"), + MountType::ReadOnly, + ) + .await?; + let dev = MountGuard::mount(&Bind::new("/dev"), overlay.path().join("dev"), ReadWrite).await?; + let proc = + MountGuard::mount(&Bind::new("/proc"), overlay.path().join("proc"), ReadWrite).await?; + let sys = MountGuard::mount(&Bind::new("/sys"), overlay.path().join("sys"), ReadWrite).await?; + let efivarfs = if tokio::fs::metadata("/sys/firmware/efi").await.is_ok() { + Some( + MountGuard::mount( + &EfiVarFs, + overlay.path().join("sys/firmware/efi/efivars"), + ReadWrite, + ) + .await?, + ) + } else { + None + }; + tokio::fs::write( - current.join("etc/fstab"), + overlay.path().join("etc/fstab"), format!( include_str!("fstab.template"), boot = part_info.boot.display(), @@ -280,42 +349,20 @@ pub async fn execute( .await?; Command::new("chroot") - .arg(¤t) + .arg(overlay.path()) .arg("systemd-machine-id-setup") .invoke(crate::ErrorKind::Systemd) .await?; Command::new("chroot") - .arg(¤t) + .arg(overlay.path()) .arg("ssh-keygen") .arg("-A") .invoke(crate::ErrorKind::OpenSsh) .await?; - let start_os_fs = MountGuard::mount( - &Bind::new(rootfs.path()), - current.join("media/embassy/embassyfs"), - MountType::ReadOnly, - ) - .await?; - let dev = MountGuard::mount(&Bind::new("/dev"), current.join("dev"), ReadWrite).await?; - let proc = MountGuard::mount(&Bind::new("/proc"), current.join("proc"), ReadWrite).await?; - let sys = MountGuard::mount(&Bind::new("/sys"), current.join("sys"), ReadWrite).await?; - let efivarfs = if tokio::fs::metadata("/sys/firmware/efi").await.is_ok() { - Some( - MountGuard::mount( - &EfiVarFs, - current.join("sys/firmware/efi/efivars"), - ReadWrite, - ) - .await?, - ) - } else { - None - }; - let mut install = Command::new("chroot"); - install.arg(¤t).arg("grub-install"); + install.arg(overlay.path()).arg("grub-install"); if tokio::fs::metadata("/sys/firmware/efi").await.is_err() { install.arg("--target=i386-pc"); } else { @@ -331,7 +378,7 @@ pub async fn execute( .await?; Command::new("chroot") - .arg(¤t) + .arg(overlay.path()) .arg("update-grub2") .invoke(crate::ErrorKind::Grub) .await?; @@ -346,7 +393,13 @@ pub async fn execute( efi.unmount(false).await?; } boot.unmount(false).await?; + + overlay.unmount().await?; + tokio::fs::remove_dir_all(&work).await?; + lower.unmount().await?; + rootfs.unmount().await?; + Ok(()) } diff --git a/core/startos/src/progress.rs b/core/startos/src/progress.rs index eec637575..9a22405ca 100644 --- a/core/startos/src/progress.rs +++ b/core/startos/src/progress.rs @@ -11,7 +11,7 @@ use tokio::io::{AsyncSeek, AsyncWrite}; use tokio::sync::{mpsc, watch}; use ts_rs::TS; -use crate::db::model::DatabaseModel; +use crate::db::model::{Database, DatabaseModel}; use crate::prelude::*; lazy_static::lazy_static! { @@ -23,6 +23,7 @@ lazy_static::lazy_static! { #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, TS)] #[serde(untagged)] pub enum Progress { + NotStarted(()), Complete(bool), Progress { #[ts(type = "number")] @@ -33,10 +34,13 @@ pub enum Progress { } impl Progress { pub fn new() -> Self { - Progress::Complete(false) + Progress::NotStarted(()) } pub fn update_bar(self, bar: &ProgressBar) { match self { + Self::NotStarted(()) => { + bar.set_style(SPINNER.clone()); + } Self::Complete(false) => { bar.set_style(SPINNER.clone()); bar.tick(); @@ -60,9 +64,15 @@ impl Progress { } } } + pub fn start(&mut self) { + *self = match *self { + Self::NotStarted(()) => Self::Complete(false), + a => a, + }; + } pub fn set_done(&mut self, done: u64) { *self = match *self { - Self::Complete(false) => Self::Progress { done, total: None }, + Self::Complete(false) | Self::NotStarted(()) => Self::Progress { done, total: None }, Self::Progress { mut done, total } => { if let Some(total) = total { if done > total { @@ -76,7 +86,7 @@ impl Progress { } pub fn set_total(&mut self, total: u64) { *self = match *self { - Self::Complete(false) => Self::Progress { + Self::Complete(false) | Self::NotStarted(()) => Self::Progress { done: 0, total: Some(total), }, @@ -104,12 +114,15 @@ impl Progress { pub fn complete(&mut self) { *self = Self::Complete(true); } + pub fn is_complete(&self) -> bool { + matches!(self, Self::Complete(true)) + } } impl std::ops::Add for Progress { type Output = Self; fn add(self, rhs: u64) -> Self::Output { match self { - Self::Complete(false) => Self::Progress { + Self::Complete(false) | Self::NotStarted(()) => Self::Progress { done: rhs, total: None, }, @@ -218,7 +231,7 @@ impl FullProgressTracker { } pub fn sync_to_db( mut self, - db: PatchDb, + db: TypedPatchDb, deref: DerefFn, min_interval: Option, ) -> impl Future> + 'static @@ -308,6 +321,9 @@ impl PhaseProgressTrackerHandle { } } } + pub fn start(&mut self) { + self.progress.send_modify(|p| p.start()); + } pub fn set_done(&mut self, done: u64) { self.progress.send_modify(|p| p.set_done(done)); self.update_overall(); @@ -324,6 +340,12 @@ impl PhaseProgressTrackerHandle { self.progress.send_modify(|p| p.complete()); self.update_overall(); } + pub fn writer(self, writer: W) -> ProgressTrackerWriter { + ProgressTrackerWriter { + writer, + progress: self, + } + } } impl std::ops::AddAssign for PhaseProgressTrackerHandle { fn add_assign(&mut self, rhs: u64) { diff --git a/core/startos/src/properties.rs b/core/startos/src/properties.rs index f8753ab10..e24b14965 100644 --- a/core/startos/src/properties.rs +++ b/core/startos/src/properties.rs @@ -1,7 +1,6 @@ use clap::Parser; use imbl_value::{json, Value}; use models::PackageId; -use rpc_toolkit::command; use serde::{Deserialize, Serialize}; use crate::context::RpcContext; diff --git a/core/startos/src/registry/admin.rs b/core/startos/src/registry/admin.rs index d416ebdbd..b09fa05aa 100644 --- a/core/startos/src/registry/admin.rs +++ b/core/startos/src/registry/admin.rs @@ -1,234 +1,255 @@ +use std::collections::BTreeMap; use std::path::PathBuf; -use std::time::Duration; use clap::Parser; -use color_eyre::eyre::eyre; -use console::style; -use futures::StreamExt; -use indicatif::{ProgressBar, ProgressStyle}; -use reqwest::{header, Body, Client, Url}; -use rpc_toolkit::command; +use itertools::Itertools; +use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use ts_rs::TS; use crate::context::CliContext; -use crate::s9pk::S9pk; -use crate::{Error, ErrorKind}; +use crate::prelude::*; +use crate::registry::context::RegistryContext; +use crate::registry::signer::{ContactInfo, SignerInfo, SignerKey}; +use crate::registry::RegistryDatabase; +use crate::rpc_continuations::RequestGuid; +use crate::util::serde::{display_serializable, HandlerExtSerde, Pem, WithIoFormat}; -async fn registry_user_pass(location: &str) -> Result<(Url, String, String), Error> { - let mut url = Url::parse(location)?; - let user = url.username().to_string(); - let pass = url.password().map(str::to_string); - if user.is_empty() || url.path() != "/" { - return Err(Error::new( - eyre!("{location:?} is not like \"https://user@registry.example.com/\""), - ErrorKind::ParseUrl, - )); +pub fn admin_api() -> ParentHandler { + ParentHandler::new() + .subcommand("signer", signers_api::()) + .subcommand("add", from_fn_async(add_admin).no_cli()) + .subcommand("add", from_fn_async(cli_add_admin).no_display()) + .subcommand( + "list", + from_fn_async(list_admins) + .with_display_serializable() + .with_custom_display_fn(|handle, result| Ok(display_signers(handle.params, result))) + .with_call_remote::(), + ) +} + +fn signers_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "list", + from_fn_async(list_signers) + .with_metadata("admin", Value::Bool(true)) + .with_display_serializable() + .with_custom_display_fn(|handle, result| Ok(display_signers(handle.params, result))) + .with_call_remote::(), + ) + .subcommand( + "add", + from_fn_async(add_signer) + .with_metadata("admin", Value::Bool(true)) + .no_cli(), + ) + .subcommand("add", from_fn_async(cli_add_signer).no_display()) +} + +impl Model> { + pub fn get_signer(&self, key: &SignerKey) -> Result { + self.as_entries()? + .into_iter() + .map(|(guid, s)| Ok::<_, Error>((guid, s.as_keys().de()?))) + .filter_ok(|(_, s)| s.contains(key)) + .next() + .transpose()? + .map(|(a, _)| a) + .ok_or_else(|| Error::new(eyre!("unknown signer"), ErrorKind::Authorization)) } - let _ = url.set_username(""); - let _ = url.set_password(None); - let pass = match pass { - Some(p) => p, - None => { - let pass_prompt = format!("{} Password for {user}: ", style("?").yellow()); - tokio::task::spawn_blocking(move || rpassword::prompt_password(pass_prompt)) - .await - .unwrap()? + pub fn get_signer_info(&self, key: &SignerKey) -> Result<(RequestGuid, SignerInfo), Error> { + self.as_entries()? + .into_iter() + .map(|(guid, s)| Ok::<_, Error>((guid, s.de()?))) + .filter_ok(|(_, s)| s.keys.contains(key)) + .next() + .transpose()? + .ok_or_else(|| Error::new(eyre!("unknown signer"), ErrorKind::Authorization)) + } + + pub fn add_signer(&mut self, signer: &SignerInfo) -> Result<(), Error> { + if let Some((guid, s)) = self + .as_entries()? + .into_iter() + .map(|(guid, s)| Ok::<_, Error>((guid, s.de()?))) + .filter_ok(|(_, s)| !s.keys.is_disjoint(&signer.keys)) + .next() + .transpose()? + { + return Err(Error::new( + eyre!( + "A signer {} ({}) already exists with a matching key", + guid, + s.name + ), + ErrorKind::InvalidRequest, + )); } - }; - Ok((url, user.to_string(), pass.to_string())) -} - -#[derive(serde::Serialize, Debug)] -struct Package { - id: String, - version: String, - arches: Option>, -} - -async fn do_index( - httpc: &Client, - mut url: Url, - user: &str, - pass: &str, - pkg: &Package, -) -> Result<(), Error> { - url.set_path("/admin/v0/index"); - let req = httpc - .post(url) - .header(header::ACCEPT, "text/plain") - .basic_auth(user, Some(pass)) - .json(pkg) - .build()?; - let res = httpc.execute(req).await?; - if !res.status().is_success() { - let info = res.text().await?; - return Err(Error::new(eyre!("{}", info), ErrorKind::Registry)); + self.insert(&RequestGuid::new(), signer) } - Ok(()) } -async fn do_upload( - httpc: &Client, - mut url: Url, - user: &str, - pass: &str, - pkg_id: &str, - body: Body, -) -> Result<(), Error> { - url.set_path("/admin/v0/upload"); - let req = httpc - .post(url) - .header(header::ACCEPT, "text/plain") - .query(&["id", pkg_id]) - .basic_auth(user, Some(pass)) - .body(body) - .build()?; - let res = httpc.execute(req).await?; - if !res.status().is_success() { - let info = res.text().await?; - return Err(Error::new(eyre!("{}", info), ErrorKind::Registry)); +pub async fn list_signers( + ctx: RegistryContext, +) -> Result, Error> { + ctx.db.peek().await.into_index().into_signers().de() +} + +pub fn display_signers(params: WithIoFormat, signers: BTreeMap) { + use prettytable::*; + + if let Some(format) = params.format { + return display_serializable(format, signers); } - Ok(()) + + let mut table = Table::new(); + table.add_row(row![bc => + "ID", + "NAME", + "CONTACT", + "KEYS", + ]); + for (id, info) in signers { + table.add_row(row![ + id.as_ref(), + &info.name, + &info.contact.into_iter().join("\n"), + &info.keys.into_iter().join("\n"), + ]); + } + table.print_tty(false).unwrap(); } -#[derive(Deserialize, Serialize, Parser)] -#[serde(rename_all = "camelCase")] +pub async fn add_signer(ctx: RegistryContext, signer: SignerInfo) -> Result<(), Error> { + ctx.db + .mutate(|db| db.as_index_mut().as_signers_mut().add_signer(&signer)) + .await +} + +#[derive(Debug, Deserialize, Serialize, Parser)] #[command(rename_all = "kebab-case")] -pub struct PublishParams { - location: String, - path: PathBuf, - #[arg(name = "no-verify", long = "no-verify")] - no_verify: bool, - #[arg(name = "no-upload", long = "no-upload")] - no_upload: bool, - #[arg(name = "no-index", long = "no-index")] - no_index: bool, +#[serde(rename_all = "camelCase")] +pub struct CliAddSignerParams { + #[arg(long = "name", short = 'n')] + pub name: String, + #[arg(long = "contact", short = 'c')] + pub contact: Vec, + #[arg(long = "ed25519-key")] + pub ed25519_keys: Vec>, + pub database: Option, } -pub async fn publish( - _: CliContext, - PublishParams { - location, - no_index, - no_upload, - no_verify, - path, - }: PublishParams, +pub async fn cli_add_signer( + HandlerArgs { + context: ctx, + parent_method, + method, + params: + CliAddSignerParams { + name, + contact, + ed25519_keys, + database, + }, + .. + }: HandlerArgs, ) -> Result<(), Error> { - // Prepare for progress bars. - let bytes_bar_style = - ProgressStyle::with_template("{percent}% {wide_bar} [{bytes}/{total_bytes}] [{eta}]") - .unwrap(); - let plain_line_style = - ProgressStyle::with_template("{prefix:.bold.dim} {wide_msg}...").unwrap(); - let spinner_line_style = - ProgressStyle::with_template("{prefix:.bold.dim} {spinner} {wide_msg}...").unwrap(); - - // Read the file to get manifest information and check validity.. - // Open file right away so it can not change out from under us. - let file = tokio::fs::File::open(&path).await?; - - let manifest = if no_verify { - let pb = ProgressBar::new(1) - .with_style(spinner_line_style.clone()) - .with_prefix("[1/3]") - .with_message("Querying s9pk"); - pb.enable_steady_tick(Duration::from_millis(200)); - let s9pk = S9pk::open(&path, None, false).await?; - let m = s9pk.as_manifest().clone(); - pb.set_style(plain_line_style.clone()); - pb.abandon(); - m - } else { - let pb = ProgressBar::new(1) - .with_style(spinner_line_style.clone()) - .with_prefix("[1/3]") - .with_message("Verifying s9pk"); - pb.enable_steady_tick(Duration::from_millis(200)); - let s9pk = S9pk::open(&path, None, false).await?; - // s9pk.validate().await?; - todo!(); - let m = s9pk.as_manifest().clone(); - pb.set_style(plain_line_style.clone()); - pb.abandon(); - m + let signer = SignerInfo { + name, + contact, + keys: ed25519_keys.into_iter().map(SignerKey::Ed25519).collect(), }; - let pkg = Package { - id: manifest.id.to_string(), - version: manifest.version.to_string(), - arches: manifest.hardware_requirements.arch.clone(), - }; - println!("{} id = {}", style(">").green(), pkg.id); - println!("{} version = {}", style(">").green(), pkg.version); - if let Some(arches) = &pkg.arches { - println!("{} arches = {:?}", style(">").green(), arches); + if let Some(database) = database { + TypedPatchDb::::load(PatchDb::open(database).await?) + .await? + .mutate(|db| db.as_index_mut().as_signers_mut().add_signer(&signer)) + .await?; } else { - println!( - "{} No architecture listed in hardware_requirements", - style(">").red() - ); - } - - // Process the url and get the user's password. - let (registry, user, pass) = registry_user_pass(&location).await?; - - // Now prepare a stream of the file which will show a progress bar as it is consumed. - let file_size = file.metadata().await?.len(); - let file_stream = tokio_util::io::ReaderStream::new(file); - ProgressBar::new(0) - .with_style(plain_line_style.clone()) - .with_prefix("[2/3]") - .with_message("Uploading s9pk") - .abandon(); - let pb = ProgressBar::new(file_size).with_style(bytes_bar_style.clone()); - let stream_pb = pb.clone(); - let file_stream = file_stream.inspect(move |bytes| { - if let Ok(bytes) = bytes { - stream_pb.inc(bytes.len() as u64); - } - }); - - let httpc = Client::builder().build().unwrap(); - // And upload! - if no_upload { - println!("{} Skipping upload", style(">").yellow()); - } else { - do_upload( - &httpc, - registry.clone(), - &user, - &pass, - &pkg.id, - Body::wrap_stream(file_stream), + ctx.call_remote::( + &parent_method.into_iter().chain(method).join("."), + to_value(&signer)?, ) .await?; } - pb.finish_and_clear(); + Ok(()) +} - // Also index, so it will show up in the registry. - let pb = ProgressBar::new(0) - .with_style(spinner_line_style.clone()) - .with_prefix("[3/3]") - .with_message("Indexing registry"); - pb.enable_steady_tick(Duration::from_millis(200)); - if no_index { - println!("{} Skipping index", style(">").yellow()); +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct AddAdminParams { + #[ts(type = "string")] + pub signer: RequestGuid, +} + +pub async fn add_admin( + ctx: RegistryContext, + AddAdminParams { signer }: AddAdminParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + ensure_code!( + db.as_index().as_signers().contains_key(&signer)?, + ErrorKind::InvalidRequest, + "unknown signer {signer}" + ); + db.as_admins_mut().mutate(|a| Ok(a.insert(signer)))?; + Ok(()) + }) + .await +} + +#[derive(Debug, Deserialize, Serialize, Parser)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +pub struct CliAddAdminParams { + pub signer: RequestGuid, + pub database: Option, +} + +pub async fn cli_add_admin( + HandlerArgs { + context: ctx, + parent_method, + method, + params: CliAddAdminParams { signer, database }, + .. + }: HandlerArgs, +) -> Result<(), Error> { + if let Some(database) = database { + TypedPatchDb::::load(PatchDb::open(database).await?) + .await? + .mutate(|db| { + ensure_code!( + db.as_index().as_signers().contains_key(&signer)?, + ErrorKind::InvalidRequest, + "unknown signer {signer}" + ); + db.as_admins_mut().mutate(|a| Ok(a.insert(signer)))?; + Ok(()) + }) + .await?; } else { - do_index(&httpc, registry.clone(), &user, &pass, &pkg).await?; - } - pb.set_style(plain_line_style.clone()); - pb.abandon(); - - // All done - if !no_index { - println!( - "{} Package {} is now published to {}", - style(">").green(), - pkg.id, - registry - ); + ctx.call_remote::( + &parent_method.into_iter().chain(method).join("."), + to_value(&AddAdminParams { signer })?, + ) + .await?; } Ok(()) } + +pub async fn list_admins(ctx: RegistryContext) -> Result, Error> { + let db = ctx.db.peek().await; + let admins = db.as_admins().de()?; + Ok(db + .into_index() + .into_signers() + .de()? + .into_iter() + .filter(|(id, _)| admins.contains(id)) + .collect()) +} diff --git a/core/startos/src/registry/asset.rs b/core/startos/src/registry/asset.rs new file mode 100644 index 000000000..c9acced45 --- /dev/null +++ b/core/startos/src/registry/asset.rs @@ -0,0 +1,36 @@ +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use tokio::io::AsyncWrite; +use ts_rs::TS; +use url::Url; + +use crate::prelude::*; +use crate::registry::signer::{AcceptSigners, FileValidator, SignatureInfo}; + +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct RegistryAsset { + #[ts(type = "string")] + pub url: Url, + pub signature_info: SignatureInfo, +} +impl AsRef for RegistryAsset { + fn as_ref(&self) -> &RegistryAsset { + self + } +} +impl RegistryAsset { + pub fn validate(&self, accept: AcceptSigners) -> Result { + self.signature_info.validate(accept) + } + pub async fn download( + &self, + client: Client, + dst: &mut (impl AsyncWrite + Unpin + Send + ?Sized), + validator: &FileValidator, + ) -> Result<(), Error> { + validator.download(self.url.clone(), client, dst).await + } +} diff --git a/core/startos/src/registry/auth.rs b/core/startos/src/registry/auth.rs new file mode 100644 index 000000000..187750870 --- /dev/null +++ b/core/startos/src/registry/auth.rs @@ -0,0 +1,214 @@ +use std::collections::BTreeMap; +use std::sync::Arc; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use axum::body::Body; +use axum::extract::Request; +use axum::response::Response; +use chrono::Utc; +use http_body_util::BodyExt; +use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::{Middleware, RpcRequest, RpcResponse}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha512}; +use tokio::io::AsyncWriteExt; +use tokio::sync::Mutex; +use ts_rs::TS; + +use crate::prelude::*; +use crate::registry::context::RegistryContext; +use crate::registry::signer::SignerKey; +use crate::util::serde::{Base64, Pem}; + +pub const AUTH_SIG_HEADER: &str = "X-StartOS-Registry-Auth-Sig"; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Metadata { + #[serde(default)] + admin: bool, + #[serde(default)] + get_signer: bool, +} + +#[derive(Clone)] +pub struct Auth { + nonce_cache: Arc>>, // for replay protection + signer: Option>, +} +impl Auth { + pub fn new() -> Self { + Self { + nonce_cache: Arc::new(Mutex::new(BTreeMap::new())), + signer: None, + } + } + async fn handle_nonce(&mut self, nonce: u64) -> Result<(), Error> { + let mut cache = self.nonce_cache.lock().await; + if cache.values().any(|n| *n == nonce) { + return Err(Error::new( + eyre!("replay attack detected"), + ErrorKind::Authorization, + )); + } + while let Some(entry) = cache.first_entry() { + if entry.key().elapsed() > Duration::from_secs(60) { + entry.remove_entry(); + } else { + break; + } + } + Ok(()) + } +} + +#[derive(Serialize, Deserialize, TS)] +pub struct RegistryAdminLogRecord { + pub timestamp: String, + pub name: String, + #[ts(type = "{ id: string | number | null; method: string; params: any }")] + pub request: RpcRequest, + pub key: SignerKey, +} + +#[derive(Serialize, Deserialize)] +pub struct SignatureHeader { + pub timestamp: i64, + pub nonce: u64, + #[serde(flatten)] + pub signer: SignerKey, + pub signature: Base64<[u8; 64]>, +} +impl SignatureHeader { + pub fn sign_ed25519( + key: &ed25519_dalek::SigningKey, + body: &[u8], + context: &str, + ) -> Result { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or_else(|e| e.duration().as_secs() as i64 * -1); + let nonce = rand::random(); + let signer = SignerKey::Ed25519(Pem(key.verifying_key())); + let mut hasher = Sha512::new(); + hasher.update(&i64::to_be_bytes(timestamp)); + hasher.update(&u64::to_be_bytes(nonce)); + hasher.update(body); + let signature = Base64( + key.sign_prehashed(hasher, Some(context.as_bytes()))? + .to_bytes(), + ); + Ok(Self { + timestamp, + nonce, + signer, + signature, + }) + } +} + +impl Middleware for Auth { + type Metadata = Metadata; + async fn process_http_request( + &mut self, + ctx: &RegistryContext, + request: &mut Request, + ) -> Result<(), Response> { + if request.headers().contains_key(AUTH_SIG_HEADER) { + self.signer = Some( + async { + let request = request; + let SignatureHeader { + timestamp, + nonce, + signer, + signature, + } = serde_urlencoded::from_str( + request + .headers() + .get(AUTH_SIG_HEADER) + .or_not_found("missing X-StartOS-Registry-Auth-Sig") + .with_kind(ErrorKind::InvalidRequest)? + .to_str() + .with_kind(ErrorKind::Utf8)?, + ) + .with_kind(ErrorKind::Deserialization)?; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or_else(|e| e.duration().as_secs() as i64 * -1); + if (now - timestamp).abs() > 30 { + return Err(Error::new( + eyre!("timestamp not within 30s of now"), + ErrorKind::InvalidSignature, + )); + } + self.handle_nonce(nonce).await?; + let body = std::mem::replace(request.body_mut(), Body::empty()) + .collect() + .await + .with_kind(ErrorKind::Network)? + .to_bytes(); + let mut verifier = signer.verifier(); + verifier.update(&i64::to_be_bytes(timestamp)); + verifier.update(&u64::to_be_bytes(nonce)); + verifier.update(&body); + *request.body_mut() = Body::from(body); + + verifier.verify(&*signature, &ctx.hostname)?; + Ok(signer) + } + .await + .map_err(RpcError::from), + ); + } + Ok(()) + } + async fn process_rpc_request( + &mut self, + ctx: &RegistryContext, + metadata: Self::Metadata, + request: &mut RpcRequest, + ) -> Result<(), RpcResponse> { + async move { + let signer = self.signer.take().transpose()?; + if metadata.get_signer { + if let Some(signer) = &signer { + request.params["__auth_signer"] = to_value(signer)?; + } + } + if metadata.admin { + let signer = signer + .ok_or_else(|| Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization))?; + let db = ctx.db.peek().await; + let (guid, admin) = db.as_index().as_signers().get_signer_info(&signer)?; + if db.into_admins().de()?.contains(&guid) { + let mut log = tokio::fs::OpenOptions::new() + .create(true) + .append(true) + .open(ctx.datadir.join("admin.log")) + .await?; + log.write_all( + (serde_json::to_string(&RegistryAdminLogRecord { + timestamp: Utc::now().to_rfc3339(), + name: admin.name, + request: request.clone(), + key: signer, + }) + .with_kind(ErrorKind::Serialization)? + + "\n") + .as_bytes(), + ) + .await?; + } else { + return Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization)); + } + } + + Ok(()) + } + .await + .map_err(|e| RpcResponse::from_result(Err(e))) + } +} diff --git a/core/startos/src/registry/context.rs b/core/startos/src/registry/context.rs new file mode 100644 index 000000000..dac833434 --- /dev/null +++ b/core/startos/src/registry/context.rs @@ -0,0 +1,242 @@ +use std::net::{Ipv4Addr, SocketAddr}; +use std::ops::Deref; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use clap::Parser; +use imbl_value::InternedString; +use patch_db::PatchDb; +use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::{CallRemote, Context, Empty}; +use serde::{Deserialize, Serialize}; +use tokio::sync::broadcast::Sender; +use tracing::instrument; +use url::Url; + +use crate::context::config::{ContextConfig, CONFIG_PATH}; +use crate::context::{CliContext, RpcContext}; +use crate::prelude::*; +use crate::registry::auth::{SignatureHeader, AUTH_SIG_HEADER}; +use crate::registry::RegistryDatabase; +use crate::rpc_continuations::RpcContinuations; +use crate::version::VersionT; + +#[derive(Debug, Clone, Default, Deserialize, Serialize, Parser)] +#[serde(rename_all = "kebab-case")] +#[command(rename_all = "kebab-case")] +pub struct RegistryConfig { + #[arg(short = 'c', long = "config")] + pub config: Option, + #[arg(short = 'l', long = "listen")] + pub listen: Option, + #[arg(short = 'h', long = "hostname")] + pub hostname: InternedString, + #[arg(short = 'd', long = "datadir")] + pub datadir: Option, +} +impl ContextConfig for RegistryConfig { + fn next(&mut self) -> Option { + self.config.take() + } + fn merge_with(&mut self, other: Self) { + self.datadir = self.datadir.take().or(other.datadir); + } +} + +impl RegistryConfig { + pub fn load(mut self) -> Result { + let path = self.next(); + self.load_path_rec(path)?; + self.load_path_rec(Some(CONFIG_PATH))?; + Ok(self) + } +} + +pub struct RegistryContextSeed { + pub hostname: InternedString, + pub listen: SocketAddr, + pub db: TypedPatchDb, + pub datadir: PathBuf, + pub rpc_continuations: RpcContinuations, + pub shutdown: Sender<()>, +} + +#[derive(Clone)] +pub struct RegistryContext(Arc); +impl RegistryContext { + #[instrument(skip_all)] + pub async fn init(config: &RegistryConfig) -> Result { + let (shutdown, _) = tokio::sync::broadcast::channel(1); + let datadir = config + .datadir + .as_deref() + .unwrap_or_else(|| Path::new("/var/lib/startos")) + .to_owned(); + if tokio::fs::metadata(&datadir).await.is_err() { + tokio::fs::create_dir_all(&datadir).await?; + } + let db_path = datadir.join("registry.db"); + let db = TypedPatchDb::::load_or_init( + PatchDb::open(&db_path).await?, + || async { Ok(Default::default()) }, + ) + .await?; + Ok(Self(Arc::new(RegistryContextSeed { + hostname: config.hostname.clone(), + listen: config + .listen + .unwrap_or(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 5959)), + db, + datadir, + rpc_continuations: RpcContinuations::new(), + shutdown, + }))) + } +} +impl AsRef for RegistryContext { + fn as_ref(&self) -> &RpcContinuations { + &self.rpc_continuations + } +} + +impl Context for RegistryContext {} +impl Deref for RegistryContext { + type Target = RegistryContextSeed; + fn deref(&self) -> &Self::Target { + &*self.0 + } +} + +#[derive(Debug, Deserialize, Serialize, Parser)] +pub struct RegistryUrlParams { + pub registry: Url, +} + +impl CallRemote for CliContext { + async fn call_remote( + &self, + mut method: &str, + params: Value, + _: Empty, + ) -> Result { + use reqwest::header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE}; + use reqwest::Method; + use rpc_toolkit::yajrc::{GenericRpcMethod, Id, RpcRequest}; + use rpc_toolkit::RpcResponse; + + let url = self + .registry_url + .clone() + .ok_or_else(|| Error::new(eyre!("`--registry` required"), ErrorKind::InvalidRequest))?; + method = method.strip_prefix("registry.").unwrap_or(method); + + let rpc_req = RpcRequest { + id: Some(Id::Number(0.into())), + method: GenericRpcMethod::<_, _, Value>::new(method), + params, + }; + let body = serde_json::to_vec(&rpc_req)?; + let host = url.host().or_not_found("registry hostname")?.to_string(); + let res = self + .client + .request(Method::POST, url) + .header(CONTENT_TYPE, "application/json") + .header(ACCEPT, "application/json") + .header(CONTENT_LENGTH, body.len()) + .header( + AUTH_SIG_HEADER, + serde_urlencoded::to_string(&SignatureHeader::sign_ed25519( + self.developer_key()?, + &body, + &host, + )?) + .with_kind(ErrorKind::Serialization)?, + ) + .body(body) + .send() + .await?; + + match res + .headers() + .get(CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + { + Some("application/json") => { + serde_json::from_slice::(&*res.bytes().await?) + .with_kind(ErrorKind::Deserialization)? + .result + } + _ => Err(Error::new(eyre!("missing content type"), ErrorKind::Network).into()), + } + } +} + +fn hardware_header(ctx: &RpcContext) -> String { + let mut url: Url = "http://localhost".parse().unwrap(); + url.query_pairs_mut() + .append_pair( + "os.version", + &crate::version::Current::new().semver().to_string(), + ) + .append_pair( + "os.compat", + &crate::version::Current::new().compat().to_string(), + ) + .append_pair("os.arch", &*crate::PLATFORM) + .append_pair("hardware.arch", &*crate::ARCH) + .append_pair("hardware.ram", &ctx.hardware.ram.to_string()); + + for hw in &ctx.hardware.devices { + url.query_pairs_mut() + .append_pair(&format!("hardware.device.{}", hw.class()), hw.product()); + } + + url.query().unwrap_or_default().to_string() +} + +impl CallRemote for RpcContext { + async fn call_remote( + &self, + mut method: &str, + params: Value, + RegistryUrlParams { registry }: RegistryUrlParams, + ) -> Result { + use reqwest::header::{ACCEPT, CONTENT_LENGTH, CONTENT_TYPE}; + use reqwest::Method; + use rpc_toolkit::yajrc::{GenericRpcMethod, Id, RpcRequest}; + use rpc_toolkit::RpcResponse; + + let url = registry.join("rpc/v0")?; + method = method.strip_prefix("registry.").unwrap_or(method); + + let rpc_req = RpcRequest { + id: Some(Id::Number(0.into())), + method: GenericRpcMethod::<_, _, Value>::new(method), + params, + }; + let body = serde_json::to_vec(&rpc_req)?; + let res = self + .client + .request(Method::POST, url) + .header(CONTENT_TYPE, "application/json") + .header(ACCEPT, "application/json") + .header(CONTENT_LENGTH, body.len()) + .header("X-StartOS-Hardware", &hardware_header(self)) + .body(body) + .send() + .await?; + + match res + .headers() + .get(CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + { + Some("application/json") => { + serde_json::from_slice::(&*res.bytes().await?) + .with_kind(ErrorKind::Deserialization)? + .result + } + _ => Err(Error::new(eyre!("missing content type"), ErrorKind::Network).into()), + } + } +} diff --git a/core/startos/src/registry/db.rs b/core/startos/src/registry/db.rs new file mode 100644 index 000000000..df39604f1 --- /dev/null +++ b/core/startos/src/registry/db.rs @@ -0,0 +1,171 @@ +use std::path::PathBuf; + +use clap::Parser; +use itertools::Itertools; +use patch_db::json_ptr::{JsonPointer, ROOT}; +use patch_db::Dump; +use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use tracing::instrument; +use ts_rs::TS; + +use crate::context::CliContext; +use crate::prelude::*; +use crate::registry::context::RegistryContext; +use crate::registry::RegistryDatabase; +use crate::util::serde::{apply_expr, HandlerExtSerde}; + +pub fn db_api() -> ParentHandler { + ParentHandler::new() + .subcommand("dump", from_fn_async(cli_dump).with_display_serializable()) + .subcommand( + "dump", + from_fn_async(dump) + .with_metadata("admin", Value::Bool(true)) + .no_cli(), + ) + .subcommand("apply", from_fn_async(cli_apply).no_display()) + .subcommand( + "apply", + from_fn_async(apply) + .with_metadata("admin", Value::Bool(true)) + .no_cli(), + ) +} + +#[derive(Deserialize, Serialize, Parser)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct CliDumpParams { + #[arg(long = "pointer", short = 'p')] + pointer: Option, + path: Option, +} + +#[instrument(skip_all)] +async fn cli_dump( + HandlerArgs { + context, + parent_method, + method, + params: CliDumpParams { pointer, path }, + .. + }: HandlerArgs, +) -> Result { + let dump = if let Some(path) = path { + PatchDb::open(path).await?.dump(&ROOT).await + } else { + let method = parent_method.into_iter().chain(method).join("."); + from_value::( + context + .call_remote::(&method, imbl_value::json!({ "pointer": pointer })) + .await?, + )? + }; + + Ok(dump) +} + +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct DumpParams { + #[arg(long = "pointer", short = 'p')] + #[ts(type = "string | null")] + pointer: Option, +} + +pub async fn dump(ctx: RegistryContext, DumpParams { pointer }: DumpParams) -> Result { + Ok(ctx + .db + .dump(&pointer.as_ref().map_or(ROOT, |p| p.borrowed())) + .await) +} + +#[derive(Deserialize, Serialize, Parser)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct CliApplyParams { + expr: String, + path: Option, +} + +#[instrument(skip_all)] +async fn cli_apply( + HandlerArgs { + context, + parent_method, + method, + params: CliApplyParams { expr, path }, + .. + }: HandlerArgs, +) -> Result<(), RpcError> { + if let Some(path) = path { + PatchDb::open(path) + .await? + .apply_function(|db| { + let res = apply_expr( + serde_json::to_value(patch_db::Value::from(db)) + .with_kind(ErrorKind::Deserialization)? + .into(), + &expr, + )?; + + Ok::<_, Error>(( + to_value( + &serde_json::from_value::(res.clone().into()).with_ctx( + |_| { + ( + crate::ErrorKind::Deserialization, + "result does not match database model", + ) + }, + )?, + )?, + (), + )) + }) + .await?; + } else { + let method = parent_method.into_iter().chain(method).join("."); + context + .call_remote::(&method, imbl_value::json!({ "expr": expr })) + .await?; + } + + Ok(()) +} + +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +pub struct ApplyParams { + expr: String, + path: Option, +} + +pub async fn apply( + ctx: RegistryContext, + ApplyParams { expr, .. }: ApplyParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + let res = apply_expr( + serde_json::to_value(patch_db::Value::from(db.clone())) + .with_kind(ErrorKind::Deserialization)? + .into(), + &expr, + )?; + + db.ser( + &serde_json::from_value::(res.clone().into()).with_ctx(|_| { + ( + crate::ErrorKind::Deserialization, + "result does not match database model", + ) + })?, + ) + }) + .await +} diff --git a/core/startos/src/registry/marketplace.rs b/core/startos/src/registry/marketplace.rs deleted file mode 100644 index 82ead81cb..000000000 --- a/core/startos/src/registry/marketplace.rs +++ /dev/null @@ -1,101 +0,0 @@ -use base64::Engine; -use clap::Parser; -use color_eyre::eyre::eyre; -use reqwest::{StatusCode, Url}; -use rpc_toolkit::{command, from_fn_async, HandlerExt, ParentHandler}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use ts_rs::TS; - -use crate::context::{CliContext, RpcContext}; -use crate::version::VersionT; -use crate::{Error, ResultExt}; - -pub fn marketplace() -> ParentHandler { - ParentHandler::new().subcommand("get", from_fn_async(get).with_remote_cli::()) -} - -pub fn with_query_params(ctx: RpcContext, mut url: Url) -> Url { - url.query_pairs_mut() - .append_pair( - "os.version", - &crate::version::Current::new().semver().to_string(), - ) - .append_pair( - "os.compat", - &crate::version::Current::new().compat().to_string(), - ) - .append_pair("os.arch", &*crate::PLATFORM) - .append_pair("hardware.arch", &*crate::ARCH) - .append_pair("hardware.ram", &ctx.hardware.ram.to_string()); - - for hw in &ctx.hardware.devices { - url.query_pairs_mut() - .append_pair(&format!("hardware.device.{}", hw.class()), hw.product()); - } - - url -} - -#[derive(Deserialize, Serialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "kebab-case")] -pub struct GetParams { - #[ts(type = "string")] - url: Url, -} - -pub async fn get(ctx: RpcContext, GetParams { url }: GetParams) -> Result { - let mut response = ctx - .client - .get(with_query_params(ctx.clone(), url)) - .send() - .await - .with_kind(crate::ErrorKind::Network)?; - let status = response.status(); - if status.is_success() { - match response - .headers_mut() - .remove("Content-Type") - .as_ref() - .and_then(|h| h.to_str().ok()) - .and_then(|h| h.split(";").next()) - .map(|h| h.trim()) - { - Some("application/json") => response - .json() - .await - .with_kind(crate::ErrorKind::Deserialization), - Some("text/plain") => Ok(Value::String( - response - .text() - .await - .with_kind(crate::ErrorKind::Registry)?, - )), - Some(ctype) => Ok(Value::String(format!( - "data:{};base64,{}", - ctype, - base64::engine::general_purpose::URL_SAFE.encode( - &response - .bytes() - .await - .with_kind(crate::ErrorKind::Registry)? - ) - ))), - _ => Err(Error::new( - eyre!("missing Content-Type"), - crate::ErrorKind::Registry, - )), - } - } else { - let message = response.text().await.with_kind(crate::ErrorKind::Network)?; - Err(Error::new( - eyre!("{}", message), - match status { - StatusCode::BAD_REQUEST => crate::ErrorKind::InvalidRequest, - StatusCode::NOT_FOUND => crate::ErrorKind::NotFound, - _ => crate::ErrorKind::Registry, - }, - )) - } -} diff --git a/core/startos/src/registry/mod.rs b/core/startos/src/registry/mod.rs index 27f541f1d..c063e5823 100644 --- a/core/startos/src/registry/mod.rs +++ b/core/startos/src/registry/mod.rs @@ -1,2 +1,126 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::net::SocketAddr; + +use axum::Router; +use futures::future::ready; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler, Server}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::context::{CliContext}; +use crate::middleware::cors::Cors; +use crate::net::static_server::{bad_request, not_found, server_error}; +use crate::net::web_server::WebServer; +use crate::prelude::*; +use crate::registry::auth::Auth; +use crate::registry::context::{RegistryContext}; +use crate::registry::os::index::OsIndex; +use crate::registry::signer::SignerInfo; +use crate::rpc_continuations::RequestGuid; +use crate::util::serde::HandlerExtSerde; + pub mod admin; -pub mod marketplace; +pub mod asset; +pub mod auth; +pub mod context; +pub mod db; +pub mod os; +pub mod signer; + +#[derive(Debug, Default, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +pub struct RegistryDatabase { + pub admins: BTreeSet, + pub index: FullIndex, +} + +#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct FullIndex { + // pub package: PackageIndex, + pub os: OsIndex, + #[ts(as = "BTreeMap::")] + pub signers: BTreeMap, +} + +pub async fn get_full_index(ctx: RegistryContext) -> Result { + ctx.db.peek().await.into_index().de() +} + +pub fn registry_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "index", + from_fn_async(get_full_index) + .with_display_serializable() + .with_call_remote::(), + ) + .subcommand("os", os::os_api::()) + .subcommand("admin", admin::admin_api::()) + .subcommand("db", db::db_api::()) +} + +pub fn registry_server_router(ctx: RegistryContext) -> Router { + use axum::extract as x; + use axum::routing::{any, get, post}; + Router::new() + .route("/rpc/*path", { + let ctx = ctx.clone(); + post( + Server::new(move || ready(Ok(ctx.clone())), registry_api()) + .middleware(Cors::new()) + .middleware(Auth::new()), + ) + }) + .route( + "/ws/rpc/*path", + get({ + let ctx = ctx.clone(); + move |x::Path(path): x::Path, + ws: axum::extract::ws::WebSocketUpgrade| async move { + match RequestGuid::from(&path) { + None => { + tracing::debug!("No Guid Path"); + bad_request() + } + Some(guid) => match ctx.rpc_continuations.get_ws_handler(&guid).await { + Some(cont) => ws.on_upgrade(cont), + _ => not_found(), + }, + } + } + }), + ) + .route( + "/rest/rpc/*path", + any({ + let ctx = ctx.clone(); + move |request: x::Request| async move { + let path = request + .uri() + .path() + .strip_prefix("/rest/rpc/") + .unwrap_or_default(); + match RequestGuid::from(&path) { + None => { + tracing::debug!("No Guid Path"); + bad_request() + } + Some(guid) => match ctx.rpc_continuations.get_rest_handler(&guid).await { + None => not_found(), + Some(cont) => cont(request).await.unwrap_or_else(server_error), + }, + } + } + }), + ) +} + +impl WebServer { + pub fn registry(bind: SocketAddr, ctx: RegistryContext) -> Self { + Self::new(bind, registry_server_router(ctx)) + } +} diff --git a/core/startos/src/registry/os/asset/add.rs b/core/startos/src/registry/os/asset/add.rs new file mode 100644 index 000000000..6e259e314 --- /dev/null +++ b/core/startos/src/registry/os/asset/add.rs @@ -0,0 +1,341 @@ +use std::collections::BTreeMap; +use std::panic::UnwindSafe; +use std::path::PathBuf; +use std::time::Duration; + +use axum::response::Response; +use clap::Parser; +use futures::{FutureExt, TryStreamExt}; +use helpers::NonDetachingJoinHandle; +use imbl_value::InternedString; +use itertools::Itertools; +use rpc_toolkit::{from_fn_async, CallRemote, Context, HandlerArgs, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha512}; +use ts_rs::TS; +use url::Url; + +use crate::context::CliContext; +use crate::prelude::*; +use crate::progress::{FullProgressTracker, PhasedProgressBar}; +use crate::registry::asset::RegistryAsset; +use crate::registry::context::RegistryContext; +use crate::registry::os::index::OsVersionInfo; +use crate::registry::os::SIG_CONTEXT; +use crate::registry::signer::{Blake3Ed25519Signature, Signature, SignatureInfo, SignerKey}; +use crate::rpc_continuations::{RequestGuid, RpcContinuation}; +use crate::s9pk::merkle_archive::source::ArchiveSource; +use crate::util::{Apply, Version}; + +pub fn add_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "iso", + from_fn_async(add_iso) + .with_metadata("getSigner", Value::Bool(true)) + .no_cli(), + ) + .subcommand( + "img", + from_fn_async(add_img) + .with_metadata("getSigner", Value::Bool(true)) + .no_cli(), + ) + .subcommand( + "squashfs", + from_fn_async(add_squashfs) + .with_metadata("getSigner", Value::Bool(true)) + .no_cli(), + ) +} + +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct AddAssetParams { + #[ts(type = "string")] + pub url: Url, + pub signature: Signature, + #[ts(type = "string")] + pub version: Version, + #[ts(type = "string")] + pub platform: InternedString, + #[serde(default)] + pub upload: bool, + #[serde(rename = "__auth_signer")] + pub signer: SignerKey, +} + +async fn add_asset( + ctx: RegistryContext, + AddAssetParams { + url, + signature, + version, + platform, + upload, + signer, + }: AddAssetParams, + accessor: impl FnOnce(&mut Model) -> &mut Model> + + UnwindSafe + + Send, +) -> Result, Error> { + ensure_code!( + signature.signer() == signer, + ErrorKind::InvalidSignature, + "asset signature does not match request signer" + ); + + ctx.db + .mutate(|db| { + let signer_guid = db.as_index().as_signers().get_signer(&signer)?; + if db + .as_index() + .as_os() + .as_versions() + .as_idx(&version) + .or_not_found(&version)? + .as_signers() + .de()? + .contains(&signer_guid) + { + accessor( + db.as_index_mut() + .as_os_mut() + .as_versions_mut() + .as_idx_mut(&version) + .or_not_found(&version)?, + ) + .upsert(&platform, || RegistryAsset { + url, + signature_info: SignatureInfo::new(SIG_CONTEXT), + })? + .as_signature_info_mut() + .mutate(|s| s.add_sig(&signature))?; + Ok(()) + } else { + Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization)) + } + }) + .await?; + + let guid = if upload { + let guid = RequestGuid::new(); + let auth_guid = guid.clone(); + let signer = signature.signer(); + let hostname = ctx.hostname.clone(); + ctx.rpc_continuations + .add( + guid.clone(), + RpcContinuation::rest( + Box::new(|req| { + async move { + Ok( + if async move { + let auth_sig = base64::decode( + req.headers().get("X-StartOS-Registry-Auth-Sig")?, + ) + .ok()?; + signer + .verify_message( + auth_guid.as_ref().as_bytes(), + &auth_sig, + &hostname, + ) + .ok()?; + + Some(()) + } + .await + .is_some() + { + Response::builder() + .status(200) + .body(axum::body::Body::empty()) + .with_kind(ErrorKind::Network)? + } else { + Response::builder() + .status(401) + .body(axum::body::Body::empty()) + .with_kind(ErrorKind::Network)? + }, + ) + } + .boxed() + }), + Duration::from_secs(30), + ), + ) + .await; + Some(guid) + } else { + None + }; + + Ok(guid) +} + +pub async fn add_iso( + ctx: RegistryContext, + params: AddAssetParams, +) -> Result, Error> { + add_asset(ctx, params, |m| m.as_iso_mut()).await +} + +pub async fn add_img( + ctx: RegistryContext, + params: AddAssetParams, +) -> Result, Error> { + add_asset(ctx, params, |m| m.as_img_mut()).await +} + +pub async fn add_squashfs( + ctx: RegistryContext, + params: AddAssetParams, +) -> Result, Error> { + add_asset(ctx, params, |m| m.as_squashfs_mut()).await +} + +#[derive(Debug, Deserialize, Serialize, Parser)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +pub struct CliAddAssetParams { + #[arg(short = 'p', long = "platform")] + pub platform: InternedString, + #[arg(short = 'v', long = "version")] + pub version: Version, + pub file: PathBuf, + pub url: Url, + #[arg(short = 'u', long = "upload")] + pub upload: bool, +} + +pub async fn cli_add_asset( + HandlerArgs { + context: ctx, + parent_method, + method, + params: + CliAddAssetParams { + platform, + version, + file: path, + url, + upload, + }, + .. + }: HandlerArgs, +) -> Result<(), Error> { + let ext = match path.extension().and_then(|e| e.to_str()) { + Some("iso") => "iso", + Some("img") => "img", + Some("squashfs") => "squashfs", + _ => { + return Err(Error::new( + eyre!("Unknown extension"), + ErrorKind::InvalidRequest, + )) + } + }; + + let file = tokio::fs::File::open(&path).await?.into(); + + let mut progress = FullProgressTracker::new(); + let progress_handle = progress.handle(); + let mut sign_phase = + progress_handle.add_phase(InternedString::intern("Signing File"), Some(10)); + let mut index_phase = progress_handle.add_phase( + InternedString::intern("Adding File to Registry Index"), + Some(1), + ); + let mut upload_phase = if upload { + Some(progress_handle.add_phase(InternedString::intern("Uploading File"), Some(100))) + } else { + None + }; + + let progress_task: NonDetachingJoinHandle<()> = tokio::spawn(async move { + let mut bar = PhasedProgressBar::new(&format!("Adding {} to registry...", path.display())); + loop { + let snap = progress.snapshot(); + bar.update(&snap); + if snap.overall.is_complete() { + break; + } + progress.changed().await + } + }) + .into(); + + sign_phase.start(); + let blake3_sig = + Blake3Ed25519Signature::sign_file(ctx.developer_key()?, &file, SIG_CONTEXT).await?; + let size = blake3_sig.size; + let signature = Signature::Blake3Ed25519(blake3_sig); + sign_phase.complete(); + + index_phase.start(); + let add_res = from_value::>( + ctx.call_remote::( + &parent_method + .into_iter() + .chain(method) + .chain([ext]) + .join("."), + imbl_value::json!({ + "platform": platform, + "version": version, + "url": &url, + "signature": signature, + "upload": upload, + }), + ) + .await?, + )?; + index_phase.complete(); + + if let Some(guid) = add_res { + upload_phase.as_mut().map(|p| p.start()); + upload_phase.as_mut().map(|p| p.set_total(size)); + let reg_url = ctx.registry_url.as_ref().or_not_found("--registry")?; + ctx.client + .post(url) + .header("X-StartOS-Registry-Token", guid.as_ref()) + .header( + "X-StartOS-Registry-Auth-Sig", + base64::encode( + ctx.developer_key()? + .sign_prehashed( + Sha512::new_with_prefix(guid.as_ref().as_bytes()), + Some( + reg_url + .host() + .or_not_found("registry hostname")? + .to_string() + .as_bytes(), + ), + )? + .to_bytes(), + ), + ) + .body(reqwest::Body::wrap_stream( + tokio_util::io::ReaderStream::new(file.fetch(0, size).await?).inspect_ok( + move |b| { + upload_phase + .as_mut() + .map(|p| *p += b.len() as u64) + .apply(|_| ()) + }, + ), + )) + .send() + .await?; + // upload_phase.as_mut().map(|p| p.complete()); + } + + progress_handle.complete(); + + progress_task.await.with_kind(ErrorKind::Unknown)?; + + Ok(()) +} diff --git a/core/startos/src/registry/os/asset/get.rs b/core/startos/src/registry/os/asset/get.rs new file mode 100644 index 000000000..52e95bc98 --- /dev/null +++ b/core/startos/src/registry/os/asset/get.rs @@ -0,0 +1,182 @@ +use std::collections::BTreeMap; +use std::panic::UnwindSafe; +use std::path::{Path, PathBuf}; + +use clap::Parser; +use helpers::{AtomicFile, NonDetachingJoinHandle}; +use imbl_value::{json, InternedString}; +use itertools::Itertools; +use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::context::CliContext; +use crate::prelude::*; +use crate::progress::{FullProgressTracker, PhasedProgressBar}; +use crate::registry::asset::RegistryAsset; +use crate::registry::context::RegistryContext; +use crate::registry::os::index::OsVersionInfo; +use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; +use crate::util::Version; + +pub fn get_api() -> ParentHandler { + ParentHandler::new() + .subcommand("iso", from_fn_async(get_iso).no_cli()) + .subcommand("iso", from_fn_async(cli_get_os_asset).no_display()) + .subcommand("img", from_fn_async(get_img).no_cli()) + .subcommand("img", from_fn_async(cli_get_os_asset).no_display()) + .subcommand("squashfs", from_fn_async(get_squashfs).no_cli()) + .subcommand("squashfs", from_fn_async(cli_get_os_asset).no_display()) +} + +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct GetOsAssetParams { + #[ts(type = "string")] + pub version: Version, + #[ts(type = "string")] + pub platform: InternedString, +} + +async fn get_os_asset( + ctx: RegistryContext, + GetOsAssetParams { version, platform }: GetOsAssetParams, + accessor: impl FnOnce(&Model) -> &Model> + + UnwindSafe + + Send, +) -> Result { + accessor( + ctx.db + .peek() + .await + .as_index() + .as_os() + .as_versions() + .as_idx(&version) + .or_not_found(&version)?, + ) + .as_idx(&platform) + .or_not_found(&platform)? + .de() +} + +pub async fn get_iso( + ctx: RegistryContext, + params: GetOsAssetParams, +) -> Result { + get_os_asset(ctx, params, |info| info.as_iso()).await +} + +pub async fn get_img( + ctx: RegistryContext, + params: GetOsAssetParams, +) -> Result { + get_os_asset(ctx, params, |info| info.as_img()).await +} + +pub async fn get_squashfs( + ctx: RegistryContext, + params: GetOsAssetParams, +) -> Result { + get_os_asset(ctx, params, |info| info.as_squashfs()).await +} + +#[derive(Debug, Deserialize, Serialize, Parser)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +pub struct CliGetOsAssetParams { + pub version: Version, + pub platform: InternedString, + #[arg(long = "download", short = 'd')] + pub download: Option, + #[arg( + long = "reverify", + short = 'r', + help = "verify the hash of the file a second time after download" + )] + pub reverify: bool, +} + +async fn cli_get_os_asset( + HandlerArgs { + context: ctx, + parent_method, + method, + params: + CliGetOsAssetParams { + version, + platform, + download, + reverify, + }, + .. + }: HandlerArgs, +) -> Result { + let res = from_value::( + ctx.call_remote::( + &parent_method.into_iter().chain(method).join("."), + json!({ + "version": version, + "platform": platform, + }), + ) + .await?, + )?; + + let validator = res.validate(res.signature_info.all_signers())?; + + if let Some(download) = download { + let mut file = AtomicFile::new(&download, None::<&Path>) + .await + .with_kind(ErrorKind::Filesystem)?; + + let mut progress = FullProgressTracker::new(); + let progress_handle = progress.handle(); + let mut download_phase = + progress_handle.add_phase(InternedString::intern("Downloading File"), Some(100)); + download_phase.set_total(validator.size()?); + let reverify_phase = if reverify { + Some(progress_handle.add_phase(InternedString::intern("Reverifying File"), Some(10))) + } else { + None + }; + + let progress_task: NonDetachingJoinHandle<()> = tokio::spawn(async move { + let mut bar = PhasedProgressBar::new("Downloading..."); + loop { + let snap = progress.snapshot(); + bar.update(&snap); + if snap.overall.is_complete() { + break; + } + progress.changed().await + } + }) + .into(); + + download_phase.start(); + let mut download_writer = download_phase.writer(&mut *file); + res.download(ctx.client.clone(), &mut download_writer, &validator) + .await?; + let (_, mut download_phase) = download_writer.into_inner(); + file.save().await.with_kind(ErrorKind::Filesystem)?; + download_phase.complete(); + + if let Some(mut reverify_phase) = reverify_phase { + reverify_phase.start(); + validator + .validate_file(&MultiCursorFile::from( + tokio::fs::File::open(download).await?, + )) + .await?; + reverify_phase.complete(); + } + + progress_handle.complete(); + + progress_task.await.with_kind(ErrorKind::Unknown)?; + } + + Ok(res) +} diff --git a/core/startos/src/registry/os/asset/mod.rs b/core/startos/src/registry/os/asset/mod.rs new file mode 100644 index 000000000..ec9d6cae7 --- /dev/null +++ b/core/startos/src/registry/os/asset/mod.rs @@ -0,0 +1,14 @@ +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; + +pub mod add; +pub mod get; +pub mod sign; + +pub fn asset_api() -> ParentHandler { + ParentHandler::new() + .subcommand("add", add::add_api::()) + .subcommand("add", from_fn_async(add::cli_add_asset).no_display()) + .subcommand("sign", sign::sign_api::()) + .subcommand("sign", from_fn_async(sign::cli_sign_asset).no_display()) + .subcommand("get", get::get_api::()) +} diff --git a/core/startos/src/registry/os/asset/sign.rs b/core/startos/src/registry/os/asset/sign.rs new file mode 100644 index 000000000..e19a82899 --- /dev/null +++ b/core/startos/src/registry/os/asset/sign.rs @@ -0,0 +1,188 @@ +use std::collections::BTreeMap; +use std::panic::UnwindSafe; +use std::path::PathBuf; + +use clap::Parser; +use helpers::NonDetachingJoinHandle; +use imbl_value::InternedString; +use itertools::Itertools; +use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::context::CliContext; +use crate::prelude::*; +use crate::progress::{FullProgressTracker, PhasedProgressBar}; +use crate::registry::asset::RegistryAsset; +use crate::registry::context::RegistryContext; +use crate::registry::os::index::OsVersionInfo; +use crate::registry::os::SIG_CONTEXT; +use crate::registry::signer::{Blake3Ed25519Signature, Signature}; +use crate::util::Version; + +pub fn sign_api() -> ParentHandler { + ParentHandler::new() + .subcommand("iso", from_fn_async(sign_iso).no_cli()) + .subcommand("img", from_fn_async(sign_img).no_cli()) + .subcommand("squashfs", from_fn_async(sign_squashfs).no_cli()) +} + +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct SignAssetParams { + #[ts(type = "string")] + version: Version, + #[ts(type = "string")] + platform: InternedString, + signature: Signature, +} + +async fn sign_asset( + ctx: RegistryContext, + SignAssetParams { + version, + platform, + signature, + }: SignAssetParams, + accessor: impl FnOnce(&mut Model) -> &mut Model> + + UnwindSafe + + Send, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + let guid = db.as_index().as_signers().get_signer(&signature.signer())?; + if !db + .as_index() + .as_os() + .as_versions() + .as_idx(&version) + .or_not_found(&version)? + .as_signers() + .de()? + .contains(&guid) + { + return Err(Error::new( + eyre!("signer {guid} is not authorized"), + ErrorKind::Authorization, + )); + } + + accessor( + db.as_index_mut() + .as_os_mut() + .as_versions_mut() + .as_idx_mut(&version) + .or_not_found(&version)?, + ) + .as_idx_mut(&platform) + .or_not_found(&platform)? + .as_signature_info_mut() + .mutate(|s| s.add_sig(&signature))?; + + Ok(()) + }) + .await +} + +pub async fn sign_iso(ctx: RegistryContext, params: SignAssetParams) -> Result<(), Error> { + sign_asset(ctx, params, |m| m.as_iso_mut()).await +} + +pub async fn sign_img(ctx: RegistryContext, params: SignAssetParams) -> Result<(), Error> { + sign_asset(ctx, params, |m| m.as_img_mut()).await +} + +pub async fn sign_squashfs(ctx: RegistryContext, params: SignAssetParams) -> Result<(), Error> { + sign_asset(ctx, params, |m| m.as_squashfs_mut()).await +} + +#[derive(Debug, Deserialize, Serialize, Parser)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +pub struct CliSignAssetParams { + #[arg(short = 'p', long = "platform")] + pub platform: InternedString, + #[arg(short = 'v', long = "version")] + pub version: Version, + pub file: PathBuf, +} + +pub async fn cli_sign_asset( + HandlerArgs { + context: ctx, + parent_method, + method, + params: + CliSignAssetParams { + platform, + version, + file: path, + }, + .. + }: HandlerArgs, +) -> Result<(), Error> { + let ext = match path.extension().and_then(|e| e.to_str()) { + Some("iso") => "iso", + Some("img") => "img", + Some("squashfs") => "squashfs", + _ => { + return Err(Error::new( + eyre!("Unknown extension"), + ErrorKind::InvalidRequest, + )) + } + }; + + let file = tokio::fs::File::open(&path).await?.into(); + + let mut progress = FullProgressTracker::new(); + let progress_handle = progress.handle(); + let mut sign_phase = + progress_handle.add_phase(InternedString::intern("Signing File"), Some(10)); + let mut index_phase = progress_handle.add_phase( + InternedString::intern("Adding Signature to Registry Index"), + Some(1), + ); + + let progress_task: NonDetachingJoinHandle<()> = tokio::spawn(async move { + let mut bar = PhasedProgressBar::new(&format!("Adding {} to registry...", path.display())); + loop { + let snap = progress.snapshot(); + bar.update(&snap); + if snap.overall.is_complete() { + break; + } + progress.changed().await + } + }) + .into(); + + sign_phase.start(); + let blake3_sig = + Blake3Ed25519Signature::sign_file(ctx.developer_key()?, &file, SIG_CONTEXT).await?; + let signature = Signature::Blake3Ed25519(blake3_sig); + sign_phase.complete(); + + index_phase.start(); + ctx.call_remote::( + &parent_method + .into_iter() + .chain(method) + .chain([ext]) + .join("."), + imbl_value::json!({ + "platform": platform, + "version": version, + "signature": signature, + }), + ) + .await?; + index_phase.complete(); + + progress_handle.complete(); + + progress_task.await.with_kind(ErrorKind::Unknown)?; + + Ok(()) +} diff --git a/core/startos/src/registry/os/index.rs b/core/startos/src/registry/os/index.rs new file mode 100644 index 000000000..186a7e95e --- /dev/null +++ b/core/startos/src/registry/os/index.rs @@ -0,0 +1,44 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use emver::VersionRange; +use imbl_value::InternedString; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::prelude::*; +use crate::registry::asset::RegistryAsset; +use crate::registry::context::RegistryContext; +use crate::rpc_continuations::RequestGuid; +use crate::util::Version; + +#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct OsIndex { + #[ts(as = "BTreeMap::")] + pub versions: BTreeMap, +} + +#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct OsVersionInfo { + pub headline: String, + pub release_notes: String, + #[ts(type = "string")] + pub source_version: VersionRange, + #[ts(type = "string[]")] + pub signers: BTreeSet, + #[ts(as = "BTreeMap::")] + pub iso: BTreeMap, // platform (i.e. x86_64-nonfree) -> asset + #[ts(as = "BTreeMap::")] + pub squashfs: BTreeMap, // platform (i.e. x86_64-nonfree) -> asset + #[ts(as = "BTreeMap::")] + pub img: BTreeMap, // platform (i.e. raspberrypi) -> asset +} + +pub async fn get_os_index(ctx: RegistryContext) -> Result { + ctx.db.peek().await.into_index().into_os().de() +} diff --git a/core/startos/src/registry/os/mod.rs b/core/startos/src/registry/os/mod.rs new file mode 100644 index 000000000..64ce44eaf --- /dev/null +++ b/core/startos/src/registry/os/mod.rs @@ -0,0 +1,22 @@ +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; + +use crate::context::CliContext; +use crate::util::serde::HandlerExtSerde; + +pub const SIG_CONTEXT: &str = "startos"; + +pub mod asset; +pub mod index; +pub mod version; + +pub fn os_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "index", + from_fn_async(index::get_os_index) + .with_display_serializable() + .with_call_remote::(), + ) + .subcommand("asset", asset::asset_api::()) + .subcommand("version", version::version_api::()) +} diff --git a/core/startos/src/registry/os/version/mod.rs b/core/startos/src/registry/os/version/mod.rs new file mode 100644 index 000000000..414994875 --- /dev/null +++ b/core/startos/src/registry/os/version/mod.rs @@ -0,0 +1,183 @@ +use std::collections::BTreeMap; + +use clap::Parser; +use emver::VersionRange; +use itertools::Itertools; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::context::CliContext; +use crate::prelude::*; +use crate::registry::context::RegistryContext; +use crate::registry::os::index::OsVersionInfo; +use crate::registry::signer::SignerKey; +use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat}; +use crate::util::Version; + +pub mod signer; + +pub fn version_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "add", + from_fn_async(add_version) + .with_metadata("admin", Value::Bool(true)) + .with_metadata("getSigner", Value::Bool(true)) + .no_display() + .with_call_remote::(), + ) + .subcommand( + "remove", + from_fn_async(remove_version) + .with_metadata("admin", Value::Bool(true)) + .no_display() + .with_call_remote::(), + ) + .subcommand("signer", signer::signer_api::()) + .subcommand( + "get", + from_fn_async(get_version) + .with_display_serializable() + .with_custom_display_fn(|handle, result| { + Ok(display_version_info(handle.params, result)) + }) + .with_call_remote::(), + ) +} + +#[derive(Debug, Deserialize, Serialize, Parser, TS)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct AddVersionParams { + #[ts(type = "string")] + pub version: Version, + pub headline: String, + pub release_notes: String, + #[ts(type = "string")] + pub source_version: VersionRange, + #[arg(skip)] + #[ts(skip)] + #[serde(rename = "__auth_signer")] + pub signer: Option, +} + +pub async fn add_version( + ctx: RegistryContext, + AddVersionParams { + version, + headline, + release_notes, + source_version, + signer, + }: AddVersionParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + let signer = signer + .map(|s| db.as_index().as_signers().get_signer(&s)) + .transpose()?; + db.as_index_mut() + .as_os_mut() + .as_versions_mut() + .upsert(&version, || OsVersionInfo::default())? + .mutate(|i| { + i.headline = headline; + i.release_notes = release_notes; + i.source_version = source_version; + i.signers.extend(signer); + Ok(()) + }) + }) + .await +} + +#[derive(Debug, Deserialize, Serialize, Parser, TS)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct RemoveVersionParams { + #[ts(type = "string")] + pub version: Version, +} + +pub async fn remove_version( + ctx: RegistryContext, + RemoveVersionParams { version }: RemoveVersionParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + db.as_index_mut() + .as_os_mut() + .as_versions_mut() + .remove(&version)?; + Ok(()) + }) + .await +} + +#[derive(Debug, Deserialize, Serialize, Parser, TS)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct GetVersionParams { + #[ts(type = "string | null")] + #[arg(long = "src")] + pub source: Option, + #[ts(type = "string | null")] + #[arg(long = "target")] + pub target: Option, +} + +pub async fn get_version( + ctx: RegistryContext, + GetVersionParams { source, target }: GetVersionParams, +) -> Result, Error> { + let target = target.unwrap_or(VersionRange::Any); + ctx.db + .peek() + .await + .into_index() + .into_os() + .into_versions() + .into_entries()? + .into_iter() + .map(|(v, i)| i.de().map(|i| (v, i))) + .filter_ok(|(version, info)| { + version.satisfies(&target) + && source + .as_ref() + .map_or(true, |s| s.satisfies(&info.source_version)) + }) + .collect() +} + +pub fn display_version_info(params: WithIoFormat, info: BTreeMap) { + use prettytable::*; + + if let Some(format) = params.format { + return display_serializable(format, info); + } + + let mut table = Table::new(); + table.add_row(row![bc => + "VERSION", + "HEADLINE", + "RELEASE NOTES", + "ISO PLATFORMS", + "IMG PLATFORMS", + "SQUASHFS PLATFORMS", + ]); + for (version, info) in &info { + table.add_row(row![ + version.as_str(), + &info.headline, + &info.release_notes, + &info.iso.keys().into_iter().join(", "), + &info.img.keys().into_iter().join(", "), + &info.squashfs.keys().into_iter().join(", "), + ]); + } + table.print_tty(false).unwrap(); +} diff --git a/core/startos/src/registry/os/version/signer.rs b/core/startos/src/registry/os/version/signer.rs new file mode 100644 index 000000000..01173ad18 --- /dev/null +++ b/core/startos/src/registry/os/version/signer.rs @@ -0,0 +1,133 @@ +use std::collections::BTreeMap; + +use clap::Parser; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::context::CliContext; +use crate::prelude::*; +use crate::registry::admin::display_signers; +use crate::registry::context::RegistryContext; +use crate::registry::signer::SignerInfo; +use crate::rpc_continuations::RequestGuid; +use crate::util::serde::HandlerExtSerde; +use crate::util::Version; + +pub fn signer_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "add", + from_fn_async(add_version_signer) + .with_metadata("admin", Value::Bool(true)) + .no_display() + .with_call_remote::(), + ) + .subcommand( + "remove", + from_fn_async(remove_version_signer) + .with_metadata("admin", Value::Bool(true)) + .no_display() + .with_call_remote::(), + ) + .subcommand( + "list", + from_fn_async(list_version_signers) + .with_display_serializable() + .with_custom_display_fn(|handle, result| Ok(display_signers(handle.params, result))) + .with_call_remote::(), + ) +} + +#[derive(Debug, Deserialize, Serialize, Parser, TS)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct VersionSignerParams { + #[ts(type = "string")] + pub version: Version, + #[ts(type = "string")] + pub signer: RequestGuid, +} + +pub async fn add_version_signer( + ctx: RegistryContext, + VersionSignerParams { version, signer }: VersionSignerParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + ensure_code!( + db.as_index().as_signers().contains_key(&signer)?, + ErrorKind::InvalidRequest, + "unknown signer {signer}" + ); + + db.as_index_mut() + .as_os_mut() + .as_versions_mut() + .as_idx_mut(&version) + .or_not_found(&version)? + .as_signers_mut() + .mutate(|s| Ok(s.insert(signer)))?; + + Ok(()) + }) + .await +} + +pub async fn remove_version_signer( + ctx: RegistryContext, + VersionSignerParams { version, signer }: VersionSignerParams, +) -> Result<(), Error> { + ctx.db + .mutate(|db| { + if !db + .as_index_mut() + .as_os_mut() + .as_versions_mut() + .as_idx_mut(&version) + .or_not_found(&version)? + .as_signers_mut() + .mutate(|s| Ok(s.remove(&signer)))? + { + return Err(Error::new( + eyre!("signer {signer} is not authorized to sign for v{version}"), + ErrorKind::NotFound, + )); + } + + Ok(()) + }) + .await +} + +#[derive(Debug, Deserialize, Serialize, Parser, TS)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct ListVersionSignersParams { + #[ts(type = "string")] + pub version: Version, +} + +pub async fn list_version_signers( + ctx: RegistryContext, + ListVersionSignersParams { version }: ListVersionSignersParams, +) -> Result, Error> { + let db = ctx.db.peek().await; + db.as_index() + .as_os() + .as_versions() + .as_idx(&version) + .or_not_found(&version)? + .as_signers() + .de()? + .into_iter() + .filter_map(|guid| { + db.as_index() + .as_signers() + .as_idx(&guid) + .map(|s| s.de().map(|s| (guid, s))) + }) + .collect() +} diff --git a/core/startos/src/registry/signer.rs b/core/startos/src/registry/signer.rs new file mode 100644 index 000000000..bf5374d75 --- /dev/null +++ b/core/startos/src/registry/signer.rs @@ -0,0 +1,477 @@ +use std::collections::{HashMap, HashSet}; +use std::path::Path; +use std::str::FromStr; + +use clap::builder::ValueParserFactory; +use imbl_value::InternedString; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha512}; +use tokio::io::AsyncWrite; +use ts_rs::TS; +use url::Url; + +use crate::prelude::*; +use crate::s9pk::merkle_archive::source::http::HttpSource; +use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; +use crate::s9pk::merkle_archive::source::{ArchiveSource, FileSource}; +use crate::util::clap::FromStrParser; +use crate::util::serde::{Base64, Pem}; + +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct SignerInfo { + pub name: String, + pub contact: Vec, + pub keys: HashSet, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +#[serde(tag = "alg", content = "pubkey")] +pub enum SignerKey { + Ed25519(Pem), +} +impl SignerKey { + pub fn verifier(&self) -> Verifier { + match self { + Self::Ed25519(k) => Verifier::Ed25519(*k, Sha512::new()), + } + } + pub fn verify_message( + &self, + message: &[u8], + signature: &[u8], + context: &str, + ) -> Result<(), Error> { + let mut v = self.verifier(); + v.update(message); + v.verify(signature, context) + } +} +impl std::fmt::Display for SignerKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Ed25519(k) => write!(f, "{k}"), + } + } +} + +pub enum Verifier { + Ed25519(Pem, Sha512), +} +impl Verifier { + pub fn update(&mut self, data: &[u8]) { + match self { + Self::Ed25519(_, h) => h.update(data), + } + } + pub fn verify(self, signature: &[u8], context: &str) -> Result<(), Error> { + match self { + Self::Ed25519(k, h) => k.verify_prehashed_strict( + h, + Some(context.as_bytes()), + &ed25519_dalek::Signature::from_slice(signature)?, + )?, + } + Ok(()) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +// TODO: better types +pub enum ContactInfo { + Email(String), + Matrix(String), + Website(#[ts(type = "string")] Url), +} +impl std::fmt::Display for ContactInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Email(e) => write!(f, "mailto:{e}"), + Self::Matrix(m) => write!(f, "https://matrix.to/#/{m}"), + Self::Website(w) => write!(f, "{w}"), + } + } +} +impl FromStr for ContactInfo { + type Err = Error; + fn from_str(s: &str) -> Result { + Ok(if let Some(s) = s.strip_prefix("mailto:") { + Self::Email(s.to_owned()) + } else if let Some(s) = s.strip_prefix("https://matrix.to/#/") { + Self::Matrix(s.to_owned()) + } else { + Self::Website(s.parse()?) + }) + } +} +impl ValueParserFactory for ContactInfo { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + Self::Parser::new() + } +} + +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "kebab-case")] +#[model = "Model"] +#[ts(export)] +pub struct SignatureInfo { + #[ts(type = "string")] + pub context: InternedString, + pub blake3_ed255i9: Option, +} +impl SignatureInfo { + pub fn new(context: &str) -> Self { + Self { + context: context.into(), + blake3_ed255i9: None, + } + } + pub fn validate(&self, accept: AcceptSigners) -> Result { + FileValidator::from_signatures(self.signatures(), accept, &self.context) + } + pub fn all_signers(&self) -> AcceptSigners { + AcceptSigners::All( + self.signatures() + .map(|s| AcceptSigners::Signer(s.signer())) + .collect(), + ) + .flatten() + } + pub fn signatures(&self) -> impl Iterator + '_ { + self.blake3_ed255i9.iter().flat_map(|info| { + info.signatures + .iter() + .map(|(k, s)| (k.clone(), *s)) + .map(|(pubkey, signature)| { + Signature::Blake3Ed25519(Blake3Ed25519Signature { + hash: info.hash, + size: info.size, + pubkey, + signature, + }) + }) + }) + } + pub fn add_sig(&mut self, signature: &Signature) -> Result<(), Error> { + signature.validate(&self.context)?; + match signature { + Signature::Blake3Ed25519(s) => { + if self + .blake3_ed255i9 + .as_ref() + .map_or(true, |info| info.hash == s.hash) + { + let new = if let Some(mut info) = self.blake3_ed255i9.take() { + info.signatures.insert(s.pubkey, s.signature); + info + } else { + s.info() + }; + self.blake3_ed255i9 = Some(new); + Ok(()) + } else { + Err(Error::new( + eyre!("hash sum mismatch"), + ErrorKind::InvalidSignature, + )) + } + } + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub enum AcceptSigners { + #[serde(skip)] + Accepted(Signature), + Signer(SignerKey), + Any(Vec), + All(Vec), +} +impl AcceptSigners { + const fn null() -> Self { + Self::Any(Vec::new()) + } + pub fn flatten(self) -> Self { + match self { + Self::Any(mut s) | Self::All(mut s) if s.len() == 1 => s.swap_remove(0).flatten(), + s => s, + } + } + pub fn accepted(&self) -> bool { + match self { + Self::Accepted(_) => true, + Self::All(s) => s.iter().all(|s| s.accepted()), + _ => false, + } + } + pub fn try_accept( + self, + context: &str, + ) -> Box> + Send + Sync + '_> { + match self { + Self::Accepted(s) => Box::new(std::iter::once(s).map(|s| { + s.validate(context)?; + Ok(s) + })), + Self::All(s) => Box::new(s.into_iter().flat_map(|s| s.try_accept(context))), + _ => Box::new(std::iter::once(Err(Error::new( + eyre!("signer(s) not accepted"), + ErrorKind::InvalidSignature, + )))), + } + } + pub fn process_signature(&mut self, sig: &Signature) { + let new = match std::mem::replace(self, Self::null()) { + Self::Accepted(s) => Self::Accepted(s), + Self::Signer(s) => { + if s == sig.signer() { + Self::Accepted(sig.clone()) + } else { + Self::Signer(s) + } + } + Self::All(mut s) => { + s.iter_mut().for_each(|s| s.process_signature(sig)); + + Self::All(s) + } + Self::Any(mut s) => { + if let Some(s) = s + .iter_mut() + .map(|s| { + s.process_signature(sig); + s + }) + .filter(|s| s.accepted()) + .next() + { + std::mem::replace(s, Self::null()) + } else { + Self::Any(s) + } + } + }; + *self = new; + } +} + +#[must_use] +pub struct FileValidator { + blake3: Option, + size: Option, +} +impl FileValidator { + fn add_blake3(&mut self, hash: [u8; 32], size: u64) -> Result<(), Error> { + if let Some(h) = self.blake3 { + ensure_code!(h == hash, ErrorKind::InvalidSignature, "hash sum mismatch"); + } + self.blake3 = Some(blake3::Hash::from_bytes(hash)); + if let Some(s) = self.size { + ensure_code!(s == size, ErrorKind::InvalidSignature, "file size mismatch"); + } + self.size = Some(size); + Ok(()) + } + pub fn blake3(&self) -> Result { + if let Some(hash) = self.blake3 { + Ok(hash) + } else { + Err(Error::new( + eyre!("no BLAKE3 signatures found"), + ErrorKind::InvalidSignature, + )) + } + } + pub fn size(&self) -> Result { + if let Some(size) = self.size { + Ok(size) + } else { + Err(Error::new( + eyre!("no signatures found"), + ErrorKind::InvalidSignature, + )) + } + } + pub fn from_signatures( + signatures: impl IntoIterator, + mut accept: AcceptSigners, + context: &str, + ) -> Result { + let mut res = Self { + blake3: None, + size: None, + }; + for signature in signatures { + accept.process_signature(&signature); + } + for signature in accept.try_accept(context) { + match signature? { + Signature::Blake3Ed25519(s) => res.add_blake3(*s.hash, s.size)?, + } + } + + Ok(res) + } + pub async fn download( + &self, + url: Url, + client: Client, + dst: &mut (impl AsyncWrite + Unpin + Send + ?Sized), + ) -> Result<(), Error> { + let src = HttpSource::new(client, url).await?; + let (Some(hash), Some(size)) = (self.blake3, self.size) else { + return Err(Error::new( + eyre!("no BLAKE3 signatures found"), + ErrorKind::InvalidSignature, + )); + }; + src.section(0, size) + .copy_verify(dst, Some((hash, size))) + .await?; + + Ok(()) + } + pub async fn validate_file(&self, file: &MultiCursorFile) -> Result<(), Error> { + ensure_code!( + file.size().await == Some(self.size()?), + ErrorKind::InvalidSignature, + "file size mismatch" + ); + ensure_code!( + file.blake3_mmap().await? == self.blake3()?, + ErrorKind::InvalidSignature, + "hash sum mismatch" + ); + Ok(()) + } +} + +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct Blake3Ed2551SignatureInfo { + pub hash: Base64<[u8; 32]>, + pub size: u64, + pub signatures: HashMap, Base64<[u8; 64]>>, +} +impl Blake3Ed2551SignatureInfo { + pub fn validate(&self, context: &str) -> Result>, Error> { + self.signatures + .iter() + .map(|(k, s)| { + let sig = Blake3Ed25519Signature { + hash: self.hash, + size: self.size, + pubkey: k.clone(), + signature: *s, + }; + sig.validate(context)?; + Ok(sig.pubkey) + }) + .collect() + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub enum Signature { + Blake3Ed25519(Blake3Ed25519Signature), +} +impl Signature { + pub fn validate(&self, context: &str) -> Result<(), Error> { + match self { + Self::Blake3Ed25519(a) => a.validate(context), + } + } + pub fn signer(&self) -> SignerKey { + match self { + Self::Blake3Ed25519(s) => SignerKey::Ed25519(s.pubkey.clone()), + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct Blake3Ed25519Signature { + pub hash: Base64<[u8; 32]>, + pub size: u64, + pub pubkey: Pem, + // ed25519-sig(sha512(blake3(file) + len_u64_be(file))) + pub signature: Base64<[u8; 64]>, +} +impl Blake3Ed25519Signature { + pub async fn sign_file( + key: &ed25519_dalek::SigningKey, + file: &MultiCursorFile, + context: &str, + ) -> Result { + let size = file + .size() + .await + .ok_or_else(|| Error::new(eyre!("failed to get file size"), ErrorKind::Filesystem))?; + let hash = file.blake3_mmap().await?; + let signature = key.sign_prehashed( + Sha512::new_with_prefix(hash.as_bytes()).chain_update(u64::to_be_bytes(size)), + Some(context.as_bytes()), + )?; + Ok(Self { + hash: Base64(*hash.as_bytes()), + size, + pubkey: Pem::new(key.verifying_key()), + signature: Base64(signature.to_bytes()), + }) + } + + pub fn validate(&self, context: &str) -> Result<(), Error> { + let sig = ed25519_dalek::Signature::from_bytes(&*self.signature); + self.pubkey.verify_prehashed_strict( + Sha512::new_with_prefix(*self.hash).chain_update(u64::to_be_bytes(self.size)), + Some(context.as_bytes()), + &sig, + )?; + Ok(()) + } + + pub async fn check_file(&self, file: &MultiCursorFile) -> Result<(), Error> { + let size = file + .size() + .await + .ok_or_else(|| Error::new(eyre!("failed to get file size"), ErrorKind::Filesystem))?; + if self.size != size { + return Err(Error::new( + eyre!("incorrect file size: expected {} got {}", self.size, size), + ErrorKind::InvalidSignature, + )); + } + let hash = file.blake3_mmap().await?; + if &*self.hash != hash.as_bytes() { + return Err(Error::new( + eyre!("hash sum mismatch"), + ErrorKind::InvalidSignature, + )); + } + Ok(()) + } + + pub fn info(&self) -> Blake3Ed2551SignatureInfo { + Blake3Ed2551SignatureInfo { + hash: self.hash, + size: self.size, + signatures: [(self.pubkey, self.signature)].into_iter().collect(), + } + } +} diff --git a/core/startos/src/rpc_continuations.rs b/core/startos/src/rpc_continuations.rs new file mode 100644 index 000000000..04b88ea8e --- /dev/null +++ b/core/startos/src/rpc_continuations.rs @@ -0,0 +1,142 @@ +use std::collections::BTreeMap; +use std::str::FromStr; +use std::time::Duration; + +use axum::extract::ws::WebSocket; +use axum::extract::Request; +use axum::response::Response; +use clap::builder::ValueParserFactory; +use futures::future::BoxFuture; +use helpers::TimedResource; +use imbl_value::InternedString; +use tokio::sync::Mutex; + +#[allow(unused_imports)] +use crate::prelude::*; +use crate::util::clap::FromStrParser; +use crate::util::new_guid; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)] +pub struct RequestGuid(InternedString); +impl RequestGuid { + pub fn new() -> Self { + Self(new_guid()) + } + + pub fn from(r: &str) -> Option { + if r.len() != 32 { + return None; + } + for c in r.chars() { + if !(c >= 'A' && c <= 'Z' || c >= '2' && c <= '7') { + return None; + } + } + Some(RequestGuid(InternedString::intern(r))) + } +} +impl AsRef for RequestGuid { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} +impl FromStr for RequestGuid { + type Err = Error; + fn from_str(s: &str) -> Result { + Self::from(s).ok_or_else(|| Error::new(eyre!("invalid guid"), ErrorKind::Deserialization)) + } +} +impl ValueParserFactory for RequestGuid { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + Self::Parser::new() + } +} + +#[test] +fn parse_guid() { + println!( + "{:?}", + RequestGuid::from(&format!("{}", RequestGuid::new())) + ) +} + +impl std::fmt::Display for RequestGuid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +pub type RestHandler = + Box BoxFuture<'static, Result> + Send>; + +pub type WebSocketHandler = Box BoxFuture<'static, ()> + Send>; + +pub enum RpcContinuation { + Rest(TimedResource), + WebSocket(TimedResource), +} +impl RpcContinuation { + pub fn rest(handler: RestHandler, timeout: Duration) -> Self { + RpcContinuation::Rest(TimedResource::new(handler, timeout)) + } + pub fn ws(handler: WebSocketHandler, timeout: Duration) -> Self { + RpcContinuation::WebSocket(TimedResource::new(handler, timeout)) + } + pub fn is_timed_out(&self) -> bool { + match self { + RpcContinuation::Rest(a) => a.is_timed_out(), + RpcContinuation::WebSocket(a) => a.is_timed_out(), + } + } +} + +pub struct RpcContinuations(Mutex>); +impl RpcContinuations { + pub fn new() -> Self { + RpcContinuations(Mutex::new(BTreeMap::new())) + } + + #[instrument(skip_all)] + pub async fn clean(&self) { + let mut continuations = self.0.lock().await; + let mut to_remove = Vec::new(); + for (guid, cont) in &*continuations { + if cont.is_timed_out() { + to_remove.push(guid.clone()); + } + } + for guid in to_remove { + continuations.remove(&guid); + } + } + + #[instrument(skip_all)] + pub async fn add(&self, guid: RequestGuid, handler: RpcContinuation) { + self.clean().await; + self.0.lock().await.insert(guid, handler); + } + + pub async fn get_ws_handler(&self, guid: &RequestGuid) -> Option { + let mut continuations = self.0.lock().await; + if !matches!(continuations.get(guid), Some(RpcContinuation::WebSocket(_))) { + return None; + } + let Some(RpcContinuation::WebSocket(x)) = continuations.remove(guid) else { + return None; + }; + x.get().await + } + + pub async fn get_rest_handler(&self, guid: &RequestGuid) -> Option { + let mut continuations: tokio::sync::MutexGuard<'_, BTreeMap> = + self.0.lock().await; + if !matches!(continuations.get(guid), Some(RpcContinuation::Rest(_))) { + return None; + } + let Some(RpcContinuation::Rest(x)) = continuations.remove(guid) else { + return None; + }; + x.get().await + } +} diff --git a/core/startos/src/s9pk/merkle_archive/directory_contents.rs b/core/startos/src/s9pk/merkle_archive/directory_contents.rs index c5373a31b..19821cf32 100644 --- a/core/startos/src/s9pk/merkle_archive/directory_contents.rs +++ b/core/startos/src/s9pk/merkle_archive/directory_contents.rs @@ -3,6 +3,7 @@ use std::fmt::Debug; use std::path::{Path, PathBuf}; use std::sync::Arc; +use blake3::Hash; use futures::future::BoxFuture; use futures::FutureExt; use imbl::OrdMap; @@ -11,11 +12,11 @@ use itertools::Itertools; use tokio::io::AsyncRead; use crate::prelude::*; -use crate::s9pk::merkle_archive::hash::{Hash, HashWriter}; use crate::s9pk::merkle_archive::sink::{Sink, TrackingWriter}; use crate::s9pk::merkle_archive::source::{ArchiveSource, DynFileSource, FileSource, Section}; use crate::s9pk::merkle_archive::write_queue::WriteQueue; use crate::s9pk::merkle_archive::{varint, Entry, EntryContents}; +use crate::util::io::ParallelBlake3Writer; #[derive(Clone)] pub struct DirectoryContents { @@ -155,7 +156,7 @@ impl DirectoryContents> { pub fn deserialize<'a>( source: &'a S, header: &'a mut (impl AsyncRead + Unpin + Send), - sighash: Hash, + (sighash, max_size): (Hash, u64), ) -> BoxFuture<'a, Result> { async move { use tokio::io::AsyncReadExt; @@ -168,15 +169,20 @@ impl DirectoryContents> { header.read_exact(&mut size).await?; let size = u64::from_be_bytes(size); + ensure_code!( + size <= max_size, + ErrorKind::InvalidSignature, + "size is greater than signed" + ); + let mut toc_reader = source.fetch(position, size).await?; let len = varint::deserialize_varint(&mut toc_reader).await?; let mut entries = OrdMap::new(); for _ in 0..len { - entries.insert( - varint::deserialize_varstring(&mut toc_reader).await?.into(), - Entry::deserialize(source, &mut toc_reader).await?, - ); + let name = varint::deserialize_varstring(&mut toc_reader).await?; + let entry = Entry::deserialize(source, &mut toc_reader).await?; + entries.insert(name.into(), entry); } let res = Self { @@ -233,7 +239,8 @@ impl DirectoryContents { #[instrument(skip_all)] pub fn sighash<'a>(&'a self) -> BoxFuture<'a, Result> { async move { - let mut hasher = TrackingWriter::new(0, HashWriter::new()); + let mut hasher = + TrackingWriter::new(0, ParallelBlake3Writer::new(super::hash::BUFFER_CAPACITY)); let mut sig_contents = OrdMap::new(); for (name, entry) in &**self { sig_contents.insert(name.clone(), entry.to_missing().await?); @@ -244,7 +251,8 @@ impl DirectoryContents { } .serialize_toc(&mut WriteQueue::new(0), &mut hasher) .await?; - Ok(hasher.into_inner().finalize()) + let hash = hasher.into_inner().finalize().await?; + Ok(hash) } .boxed() } @@ -267,7 +275,9 @@ impl DirectoryContents { _ => std::cmp::Ordering::Equal, }) { varint::serialize_varstring(&**name, w).await?; - entry.serialize_header(queue.add(entry).await?, w).await?; + if let Some(pos) = entry.serialize_header(queue.add(entry).await?, w).await? { + eprintln!("DEBUG ====> {name} @ {pos}"); + } } Ok(()) diff --git a/core/startos/src/s9pk/merkle_archive/file_contents.rs b/core/startos/src/s9pk/merkle_archive/file_contents.rs index 7529fd2d0..a2d19ed88 100644 --- a/core/startos/src/s9pk/merkle_archive/file_contents.rs +++ b/core/startos/src/s9pk/merkle_archive/file_contents.rs @@ -1,9 +1,10 @@ +use blake3::Hash; use tokio::io::AsyncRead; use crate::prelude::*; -use crate::s9pk::merkle_archive::hash::{Hash, HashWriter}; use crate::s9pk::merkle_archive::sink::{Sink, TrackingWriter}; use crate::s9pk::merkle_archive::source::{ArchiveSource, DynFileSource, FileSource, Section}; +use crate::util::io::ParallelBlake3Writer; #[derive(Debug, Clone)] pub struct FileContents(S); @@ -13,7 +14,6 @@ impl FileContents { } pub const fn header_size() -> u64 { 8 // position: u64 BE - + 8 // size: u64 BE } } impl FileContents> { @@ -21,6 +21,7 @@ impl FileContents> { pub async fn deserialize( source: &S, header: &mut (impl AsyncRead + Unpin + Send), + size: u64, ) -> Result { use tokio::io::AsyncReadExt; @@ -28,27 +29,23 @@ impl FileContents> { header.read_exact(&mut position).await?; let position = u64::from_be_bytes(position); - let mut size = [0u8; 8]; - header.read_exact(&mut size).await?; - let size = u64::from_be_bytes(size); - Ok(Self(source.section(position, size))) } } impl FileContents { - pub async fn hash(&self) -> Result { - let mut hasher = TrackingWriter::new(0, HashWriter::new()); + pub async fn hash(&self) -> Result<(Hash, u64), Error> { + let mut hasher = + TrackingWriter::new(0, ParallelBlake3Writer::new(super::hash::BUFFER_CAPACITY)); self.serialize_body(&mut hasher, None).await?; - Ok(hasher.into_inner().finalize()) + let size = hasher.position(); + let hash = hasher.into_inner().finalize().await?; + Ok((hash, size)) } #[instrument(skip_all)] pub async fn serialize_header(&self, position: u64, w: &mut W) -> Result { use tokio::io::AsyncWriteExt; - let size = self.0.size().await?; - w.write_all(&position.to_be_bytes()).await?; - w.write_all(&size.to_be_bytes()).await?; Ok(position) } @@ -56,21 +53,9 @@ impl FileContents { pub async fn serialize_body( &self, w: &mut W, - verify: Option, + verify: Option<(Hash, u64)>, ) -> Result<(), Error> { - let start = if verify.is_some() { - Some(w.current_position().await?) - } else { - None - }; self.0.copy_verify(w, verify).await?; - if let Some(start) = start { - ensure_code!( - w.current_position().await? - start == self.0.size().await?, - ErrorKind::Pack, - "FileSource::copy wrote a number of bytes that does not match FileSource::size" - ); - } Ok(()) } pub fn into_dyn(self) -> FileContents { diff --git a/core/startos/src/s9pk/merkle_archive/hash.rs b/core/startos/src/s9pk/merkle_archive/hash.rs index ae2829012..fc635c435 100644 --- a/core/startos/src/s9pk/merkle_archive/hash.rs +++ b/core/startos/src/s9pk/merkle_archive/hash.rs @@ -1,68 +1,57 @@ -pub use blake3::Hash; -use blake3::Hasher; +use std::task::Poll; + +use blake3::Hash; use tokio::io::AsyncWrite; +use tokio_util::either::Either; use crate::prelude::*; +use crate::util::io::{ParallelBlake3Writer, TeeWriter}; -#[pin_project::pin_project] -pub struct HashWriter { - hasher: Hasher, -} -impl HashWriter { - pub fn new() -> Self { - Self { - hasher: Hasher::new(), - } - } - pub fn finalize(self) -> Hash { - self.hasher.finalize() - } -} -impl AsyncWrite for HashWriter { - fn poll_write( - self: std::pin::Pin<&mut Self>, - _cx: &mut std::task::Context<'_>, - buf: &[u8], - ) -> std::task::Poll> { - self.project().hasher.update(buf); - std::task::Poll::Ready(Ok(buf.len())) - } - fn poll_flush( - self: std::pin::Pin<&mut Self>, - _cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - std::task::Poll::Ready(Ok(())) - } - fn poll_shutdown( - self: std::pin::Pin<&mut Self>, - _cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - std::task::Poll::Ready(Ok(())) - } -} +pub const BUFFER_CAPACITY: usize = 10 * 1024 * 1024; // 10MiB #[pin_project::pin_project] pub struct VerifyingWriter { - verify: Option<(Hasher, Hash)>, + verify: Option<(Hash, u64)>, #[pin] - writer: W, + writer: Either, W>, } impl VerifyingWriter { - pub fn new(w: W, verify: Option) -> Self { + pub fn new(w: W, verify: Option<(Hash, u64)>) -> Self { Self { - verify: verify.map(|v| (Hasher::new(), v)), - writer: w, + writer: if verify.is_some() { + Either::Left(TeeWriter::new( + w, + ParallelBlake3Writer::new(BUFFER_CAPACITY), + BUFFER_CAPACITY, + )) + } else { + Either::Right(w) + }, + verify, } } - pub fn verify(self) -> Result { - if let Some((actual, expected)) = self.verify { - ensure_code!( - actual.finalize() == expected, - ErrorKind::InvalidSignature, - "hash sum does not match" - ); +} +impl VerifyingWriter { + pub async fn verify(self) -> Result { + match self.writer { + Either::Left(writer) => { + let (writer, actual) = writer.into_inner().await?; + if let Some((expected, remaining)) = self.verify { + ensure_code!( + actual.finalize().await? == expected, + ErrorKind::InvalidSignature, + "hash sum mismatch" + ); + ensure_code!( + remaining == 0, + ErrorKind::InvalidSignature, + "file size mismatch" + ); + } + Ok(writer) + } + Either::Right(writer) => Ok(writer), } - Ok(self.writer) } } impl AsyncWrite for VerifyingWriter { @@ -70,28 +59,35 @@ impl AsyncWrite for VerifyingWriter { self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, buf: &[u8], - ) -> std::task::Poll> { + ) -> Poll> { let this = self.project(); - match this.writer.poll_write(cx, buf) { - std::task::Poll::Ready(Ok(written)) => { - if let Some((h, _)) = this.verify { - h.update(&buf[..written]); - } - std::task::Poll::Ready(Ok(written)) + if let Some((_, remaining)) = this.verify { + if *remaining < buf.len() as u64 { + return Poll::Ready(Err(std::io::Error::other(eyre!( + "attempted to write more bytes than signed" + )))); + } + } + match this.writer.poll_write(cx, buf)? { + Poll::Pending => Poll::Pending, + Poll::Ready(n) => { + if let Some((_, remaining)) = this.verify { + *remaining -= n as u64; + } + Poll::Ready(Ok(n)) } - a => a, } } fn poll_flush( self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { + ) -> Poll> { self.project().writer.poll_flush(cx) } fn poll_shutdown( self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { + ) -> Poll> { self.project().writer.poll_shutdown(cx) } } diff --git a/core/startos/src/s9pk/merkle_archive/mod.rs b/core/startos/src/s9pk/merkle_archive/mod.rs index afd00032a..3863ceeaf 100644 --- a/core/startos/src/s9pk/merkle_archive/mod.rs +++ b/core/startos/src/s9pk/merkle_archive/mod.rs @@ -1,12 +1,14 @@ use std::path::Path; +use blake3::Hash; use ed25519_dalek::{Signature, SigningKey, VerifyingKey}; +use imbl_value::InternedString; +use sha2::{Digest, Sha512}; use tokio::io::AsyncRead; use crate::prelude::*; use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; use crate::s9pk::merkle_archive::file_contents::FileContents; -use crate::s9pk::merkle_archive::hash::Hash; use crate::s9pk::merkle_archive::sink::Sink; use crate::s9pk::merkle_archive::source::{ArchiveSource, DynFileSource, FileSource, Section}; use crate::s9pk::merkle_archive::write_queue::WriteQueue; @@ -23,8 +25,8 @@ pub mod write_queue; #[derive(Debug, Clone)] enum Signer { - Signed(VerifyingKey, Signature), - Signer(SigningKey), + Signed(VerifyingKey, Signature, u64, InternedString), + Signer(SigningKey, InternedString), } #[derive(Debug, Clone)] @@ -33,22 +35,23 @@ pub struct MerkleArchive { contents: DirectoryContents, } impl MerkleArchive { - pub fn new(contents: DirectoryContents, signer: SigningKey) -> Self { + pub fn new(contents: DirectoryContents, signer: SigningKey, context: &str) -> Self { Self { - signer: Signer::Signer(signer), + signer: Signer::Signer(signer, context.into()), contents, } } pub fn signer(&self) -> VerifyingKey { match &self.signer { - Signer::Signed(k, _) => *k, - Signer::Signer(k) => k.verifying_key(), + Signer::Signed(k, _, _, _) => *k, + Signer::Signer(k, _) => k.verifying_key(), } } pub const fn header_size() -> u64 { 32 // pubkey + 64 // signature + 32 // sighash + + 8 // size + DirectoryContents::>::header_size() } pub fn contents(&self) -> &DirectoryContents { @@ -57,8 +60,8 @@ impl MerkleArchive { pub fn contents_mut(&mut self) -> &mut DirectoryContents { &mut self.contents } - pub fn set_signer(&mut self, key: SigningKey) { - self.signer = Signer::Signer(key); + pub fn set_signer(&mut self, key: SigningKey, context: &str) { + self.signer = Signer::Signer(key, context.into()); } pub fn sort_by( &mut self, @@ -71,6 +74,7 @@ impl MerkleArchive> { #[instrument(skip_all)] pub async fn deserialize( source: &S, + context: &str, header: &mut (impl AsyncRead + Unpin + Send), ) -> Result { use tokio::io::AsyncReadExt; @@ -87,12 +91,20 @@ impl MerkleArchive> { header.read_exact(&mut sighash).await?; let sighash = Hash::from_bytes(sighash); - let contents = DirectoryContents::deserialize(source, header, sighash).await?; + let mut max_size = [0u8; 8]; + header.read_exact(&mut max_size).await?; + let max_size = u64::from_be_bytes(max_size); - pubkey.verify_strict(contents.sighash().await?.as_bytes(), &signature)?; + pubkey.verify_prehashed_strict( + Sha512::new_with_prefix(sighash.as_bytes()).chain_update(&u64::to_be_bytes(max_size)), + Some(context.as_bytes()), + &signature, + )?; + + let contents = DirectoryContents::deserialize(source, header, (sighash, max_size)).await?; Ok(Self { - signer: Signer::Signed(pubkey, signature), + signer: Signer::Signed(pubkey, signature, max_size, context.into()), contents, }) } @@ -109,15 +121,26 @@ impl MerkleArchive { use tokio::io::AsyncWriteExt; let sighash = self.contents.sighash().await?; + let size = self.contents.toc_size(); - let (pubkey, signature) = match &self.signer { - Signer::Signed(pubkey, signature) => (*pubkey, *signature), - Signer::Signer(s) => (s.into(), ed25519_dalek::Signer::sign(s, sighash.as_bytes())), + let (pubkey, signature, max_size) = match &self.signer { + Signer::Signed(pubkey, signature, max_size, _) => (*pubkey, *signature, *max_size), + Signer::Signer(s, context) => ( + s.into(), + ed25519_dalek::SigningKey::sign_prehashed( + s, + Sha512::new_with_prefix(sighash.as_bytes()) + .chain_update(&u64::to_be_bytes(size)), + Some(context.as_bytes()), + )?, + size, + ), }; w.write_all(pubkey.as_bytes()).await?; w.write_all(&signature.to_bytes()).await?; w.write_all(sighash.as_bytes()).await?; + w.write_all(&u64::to_be_bytes(max_size)).await?; let mut next_pos = w.current_position().await?; next_pos += DirectoryContents::::header_size(); self.contents.serialize_header(next_pos, w).await?; @@ -137,7 +160,7 @@ impl MerkleArchive { #[derive(Debug, Clone)] pub struct Entry { - hash: Option, + hash: Option<(Hash, u64)>, contents: EntryContents, } impl Entry { @@ -150,7 +173,7 @@ impl Entry { pub fn file(source: S) -> Self { Self::new(EntryContents::File(FileContents::new(source))) } - pub fn hash(&self) -> Option { + pub fn hash(&self) -> Option<(Hash, u64)> { self.hash } pub fn as_contents(&self) -> &EntryContents { @@ -189,6 +212,7 @@ impl Entry { } pub fn header_size(&self) -> u64 { 32 // hash + + 8 // size: u64 BE + self.contents.header_size() } } @@ -205,10 +229,14 @@ impl Entry> { header.read_exact(&mut hash).await?; let hash = Hash::from_bytes(hash); - let contents = EntryContents::deserialize(source, header, hash).await?; + let mut size = [0u8; 8]; + header.read_exact(&mut size).await?; + let size = u64::from_be_bytes(size); + + let contents = EntryContents::deserialize(source, header, (hash, size)).await?; Ok(Self { - hash: Some(hash), + hash: Some((hash, size)), contents, }) } @@ -258,12 +286,13 @@ impl Entry { ) -> Result, Error> { use tokio::io::AsyncWriteExt; - let hash = if let Some(hash) = self.hash { + let (hash, size) = if let Some(hash) = self.hash { hash } else { self.contents.hash().await? }; w.write_all(hash.as_bytes()).await?; + w.write_all(&u64::to_be_bytes(size)).await?; self.contents.serialize_header(position, w).await } pub fn into_dyn(self) -> Entry { @@ -305,7 +334,7 @@ impl EntryContents> { pub async fn deserialize( source: &S, header: &mut (impl AsyncRead + Unpin + Send), - hash: Hash, + (hash, size): (Hash, u64), ) -> Result { use tokio::io::AsyncReadExt; @@ -313,9 +342,11 @@ impl EntryContents> { header.read_exact(&mut type_id).await?; match type_id[0] { 0 => Ok(Self::Missing), - 1 => Ok(Self::File(FileContents::deserialize(source, header).await?)), + 1 => Ok(Self::File( + FileContents::deserialize(source, header, size).await?, + )), 2 => Ok(Self::Directory( - DirectoryContents::deserialize(source, header, hash).await?, + DirectoryContents::deserialize(source, header, (hash, size)).await?, )), id => Err(Error::new( eyre!("Unknown type id {id} found in MerkleArchive"), @@ -325,14 +356,14 @@ impl EntryContents> { } } impl EntryContents { - pub async fn hash(&self) -> Result { + pub async fn hash(&self) -> Result<(Hash, u64), Error> { match self { Self::Missing => Err(Error::new( eyre!("Cannot compute hash of missing file"), ErrorKind::Pack, )), Self::File(f) => f.hash().await, - Self::Directory(d) => d.sighash().await, + Self::Directory(d) => Ok((d.sighash().await?, d.toc_size())), } } #[instrument(skip_all)] diff --git a/core/startos/src/s9pk/merkle_archive/sink.rs b/core/startos/src/s9pk/merkle_archive/sink.rs index c71377808..ec1431b8e 100644 --- a/core/startos/src/s9pk/merkle_archive/sink.rs +++ b/core/startos/src/s9pk/merkle_archive/sink.rs @@ -36,6 +36,9 @@ impl TrackingWriter { writer: w, } } + pub fn position(&self) -> u64 { + self.position + } pub fn into_inner(self) -> W { self.writer } diff --git a/core/startos/src/s9pk/merkle_archive/source/http.rs b/core/startos/src/s9pk/merkle_archive/source/http.rs index 1cb9ba961..738c480ed 100644 --- a/core/startos/src/s9pk/merkle_archive/source/http.rs +++ b/core/startos/src/s9pk/merkle_archive/source/http.rs @@ -3,7 +3,7 @@ use futures::stream::BoxStream; use futures::{StreamExt, TryStreamExt}; use reqwest::header::{ACCEPT_RANGES, CONTENT_LENGTH, RANGE}; use reqwest::{Client, Url}; -use tokio::io::AsyncRead; +use tokio::io::{AsyncRead, AsyncReadExt, Take}; use tokio_util::io::StreamReader; use crate::prelude::*; @@ -50,9 +50,8 @@ impl HttpSource { }) } } -#[async_trait::async_trait] impl ArchiveSource for HttpSource { - type Reader = HttpReader; + type Reader = Take; async fn size(&self) -> Option { self.size } @@ -72,7 +71,8 @@ impl ArchiveSource for HttpSource { .boxed() } else { futures::stream::empty().boxed() - }))), + })) + .take(size)), _ => todo!(), } } diff --git a/core/startos/src/s9pk/merkle_archive/source/mod.rs b/core/startos/src/s9pk/merkle_archive/source/mod.rs index 97c94b480..6e3b1e584 100644 --- a/core/startos/src/s9pk/merkle_archive/source/mod.rs +++ b/core/startos/src/s9pk/merkle_archive/source/mod.rs @@ -2,6 +2,8 @@ use std::path::PathBuf; use std::sync::Arc; use blake3::Hash; +use futures::future::BoxFuture; +use futures::{Future, FutureExt}; use tokio::fs::File; use tokio::io::{AsyncRead, AsyncWrite}; @@ -11,29 +13,51 @@ use crate::s9pk::merkle_archive::hash::VerifyingWriter; pub mod http; pub mod multi_cursor_file; -#[async_trait::async_trait] pub trait FileSource: Clone + Send + Sync + Sized + 'static { type Reader: AsyncRead + Unpin + Send; - async fn size(&self) -> Result; - async fn reader(&self) -> Result; - async fn copy(&self, w: &mut W) -> Result<(), Error> { - tokio::io::copy(&mut self.reader().await?, w).await?; - Ok(()) - } - async fn copy_verify( + fn size(&self) -> impl Future> + Send; + fn reader(&self) -> impl Future> + Send; + fn copy( &self, w: &mut W, - verify: Option, - ) -> Result<(), Error> { - let mut w = VerifyingWriter::new(w, verify); - tokio::io::copy(&mut self.reader().await?, &mut w).await?; - w.verify()?; - Ok(()) + ) -> impl Future> + Send { + async move { + tokio::io::copy(&mut self.reader().await?, w).await?; + Ok(()) + } } - async fn to_vec(&self, verify: Option) -> Result, Error> { - let mut vec = Vec::with_capacity(self.size().await? as usize); - self.copy_verify(&mut vec, verify).await?; - Ok(vec) + fn copy_verify( + &self, + w: &mut W, + verify: Option<(Hash, u64)>, + ) -> impl Future> + Send { + async move { + let mut w = VerifyingWriter::new(w, verify); + tokio::io::copy(&mut self.reader().await?, &mut w).await?; + w.verify().await?; + Ok(()) + } + } + fn to_vec( + &self, + verify: Option<(Hash, u64)>, + ) -> impl Future, Error>> + Send { + fn to_vec( + src: &impl FileSource, + verify: Option<(Hash, u64)>, + ) -> BoxFuture, Error>> { + async move { + let mut vec = Vec::with_capacity(if let Some((_, size)) = &verify { + *size + } else { + src.size().await? + } as usize); + src.copy_verify(&mut vec, verify).await?; + Ok(vec) + } + .boxed() + } + to_vec(self, verify) } } @@ -44,7 +68,6 @@ impl DynFileSource { Self(Arc::new(source)) } } -#[async_trait::async_trait] impl FileSource for DynFileSource { type Reader = Box; async fn size(&self) -> Result { @@ -62,11 +85,11 @@ impl FileSource for DynFileSource { async fn copy_verify( &self, mut w: &mut W, - verify: Option, + verify: Option<(Hash, u64)>, ) -> Result<(), Error> { self.0.copy_verify(&mut w, verify).await } - async fn to_vec(&self, verify: Option) -> Result, Error> { + async fn to_vec(&self, verify: Option<(Hash, u64)>) -> Result, Error> { self.0.to_vec(verify).await } } @@ -79,9 +102,9 @@ trait DynableFileSource: Send + Sync + 'static { async fn copy_verify( &self, w: &mut (dyn AsyncWrite + Unpin + Send), - verify: Option, + verify: Option<(Hash, u64)>, ) -> Result<(), Error>; - async fn to_vec(&self, verify: Option) -> Result, Error>; + async fn to_vec(&self, verify: Option<(Hash, u64)>) -> Result, Error>; } #[async_trait::async_trait] impl DynableFileSource for T { @@ -97,16 +120,15 @@ impl DynableFileSource for T { async fn copy_verify( &self, w: &mut (dyn AsyncWrite + Unpin + Send), - verify: Option, + verify: Option<(Hash, u64)>, ) -> Result<(), Error> { FileSource::copy_verify(self, w, verify).await } - async fn to_vec(&self, verify: Option) -> Result, Error> { + async fn to_vec(&self, verify: Option<(Hash, u64)>) -> Result, Error> { FileSource::to_vec(self, verify).await } } -#[async_trait::async_trait] impl FileSource for PathBuf { type Reader = File; async fn size(&self) -> Result { @@ -117,7 +139,6 @@ impl FileSource for PathBuf { } } -#[async_trait::async_trait] impl FileSource for Arc<[u8]> { type Reader = std::io::Cursor; async fn size(&self) -> Result { @@ -134,21 +155,26 @@ impl FileSource for Arc<[u8]> { } } -#[async_trait::async_trait] pub trait ArchiveSource: Clone + Send + Sync + Sized + 'static { type Reader: AsyncRead + Unpin + Send; - async fn size(&self) -> Option { - None + fn size(&self) -> impl Future> + Send { + async { None } } - async fn fetch(&self, position: u64, size: u64) -> Result; - async fn copy_to( + fn fetch( + &self, + position: u64, + size: u64, + ) -> impl Future> + Send; + fn copy_to( &self, position: u64, size: u64, w: &mut W, - ) -> Result<(), Error> { - tokio::io::copy(&mut self.fetch(position, size).await?, w).await?; - Ok(()) + ) -> impl Future> + Send { + async move { + tokio::io::copy(&mut self.fetch(position, size).await?, w).await?; + Ok(()) + } } fn section(&self, position: u64, size: u64) -> Section { Section { @@ -159,7 +185,6 @@ pub trait ArchiveSource: Clone + Send + Sync + Sized + 'static { } } -#[async_trait::async_trait] impl ArchiveSource for Arc<[u8]> { type Reader = tokio::io::Take>; async fn fetch(&self, position: u64, size: u64) -> Result { @@ -177,7 +202,6 @@ pub struct Section { position: u64, size: u64, } -#[async_trait::async_trait] impl FileSource for Section { type Reader = S::Reader; async fn size(&self) -> Result { diff --git a/core/startos/src/s9pk/merkle_archive/source/multi_cursor_file.rs b/core/startos/src/s9pk/merkle_archive/source/multi_cursor_file.rs index 7add68e6f..00188559a 100644 --- a/core/startos/src/s9pk/merkle_archive/source/multi_cursor_file.rs +++ b/core/startos/src/s9pk/merkle_archive/source/multi_cursor_file.rs @@ -31,6 +31,16 @@ impl MultiCursorFile { file: Arc::new(Mutex::new(File::open(path_from_fd(fd)).await?)), }) } + pub async fn blake3_mmap(&self) -> Result { + let path = self.path(); + tokio::task::spawn_blocking(move || { + let mut hasher = blake3::Hasher::new(); + hasher.update_mmap_rayon(path)?; + Ok(hasher.finalize()) + }) + .await + .with_kind(ErrorKind::Unknown)? + } } impl From for MultiCursorFile { fn from(value: File) -> Self { @@ -67,7 +77,6 @@ impl AsyncRead for FileSectionReader { } } -#[async_trait::async_trait] impl ArchiveSource for MultiCursorFile { type Reader = FileSectionReader; async fn size(&self) -> Option { diff --git a/core/startos/src/s9pk/merkle_archive/test.rs b/core/startos/src/s9pk/merkle_archive/test.rs index 430ab4f31..e902aafd9 100644 --- a/core/startos/src/s9pk/merkle_archive/test.rs +++ b/core/startos/src/s9pk/merkle_archive/test.rs @@ -52,7 +52,7 @@ fn test(files: Vec<(PathBuf, String)>) -> Result<(), Error> { } } let key = SigningKey::generate(&mut rand::thread_rng()); - let mut a1 = MerkleArchive::new(root, key); + let mut a1 = MerkleArchive::new(root, key, "test"); tokio::runtime::Builder::new_current_thread() .enable_io() .build() @@ -63,7 +63,7 @@ fn test(files: Vec<(PathBuf, String)>) -> Result<(), Error> { a1.serialize(&mut TrackingWriter::new(0, &mut s1), true) .await?; let s1: Arc<[u8]> = s1.into(); - let a2 = MerkleArchive::deserialize(&s1, &mut Cursor::new(s1.clone())).await?; + let a2 = MerkleArchive::deserialize(&s1, "test", &mut Cursor::new(s1.clone())).await?; for (path, content) in check_set { match a2 diff --git a/core/startos/src/s9pk/rpc.rs b/core/startos/src/s9pk/rpc.rs index 312d663fd..ca149c3e3 100644 --- a/core/startos/src/s9pk/rpc.rs +++ b/core/startos/src/s9pk/rpc.rs @@ -17,6 +17,7 @@ use crate::s9pk::manifest::Manifest; use crate::s9pk::merkle_archive::source::DynFileSource; use crate::s9pk::merkle_archive::Entry; use crate::s9pk::v2::compat::CONTAINER_TOOL; +use crate::s9pk::v2::SIG_CONTEXT; use crate::s9pk::S9pk; use crate::util::io::TmpDir; use crate::util::serde::{apply_expr, HandlerExtSerde}; @@ -24,7 +25,7 @@ use crate::util::Invoke; pub const SKIP_ENV: &[&str] = &["TERM", "container", "HOME", "HOSTNAME"]; -pub fn s9pk() -> ParentHandler { +pub fn s9pk() -> ParentHandler { ParentHandler::new() .subcommand("edit", edit()) .subcommand("inspect", inspect()) @@ -35,9 +36,9 @@ struct S9pkPath { s9pk: PathBuf, } -fn edit() -> ParentHandler { +fn edit() -> ParentHandler { let only_parent = |a, _| a; - ParentHandler::::new() + ParentHandler::new() .subcommand( "add-image", from_fn_async(add_image) @@ -52,9 +53,9 @@ fn edit() -> ParentHandler { ) } -fn inspect() -> ParentHandler { +fn inspect() -> ParentHandler { let only_parent = |a, _| a; - ParentHandler::::new() + ParentHandler::new() .subcommand( "file-tree", from_fn_async(file_tree) @@ -158,7 +159,7 @@ async fn add_image( .invoke(ErrorKind::Docker) .await?; let archive = s9pk.as_archive_mut(); - archive.set_signer(ctx.developer_key()?.clone()); + archive.set_signer(ctx.developer_key()?.clone(), SIG_CONTEXT); archive.contents_mut().insert_path( Path::new("images") .join(&arch) @@ -213,7 +214,7 @@ async fn edit_manifest( let tmp_path = s9pk_path.with_extension("s9pk.tmp"); let mut tmp_file = File::create(&tmp_path).await?; s9pk.as_archive_mut() - .set_signer(ctx.developer_key()?.clone()); + .set_signer(ctx.developer_key()?.clone(), SIG_CONTEXT); s9pk.serialize(&mut tmp_file, true).await?; tmp_file.sync_all().await?; tokio::fs::rename(&tmp_path, &s9pk_path).await?; diff --git a/core/startos/src/s9pk/v1/docker.rs b/core/startos/src/s9pk/v1/docker.rs index bbb1f2f09..96c532479 100644 --- a/core/startos/src/s9pk/v1/docker.rs +++ b/core/startos/src/s9pk/v1/docker.rs @@ -55,7 +55,6 @@ impl DockerReader { if let Some(image) = tokio_tar::Archive::new(rdr) .entries()? .try_filter_map(|e| { - let arch = arch.clone(); async move { Ok(if &*e.path()? == Path::new(&format!("{}.tar", arch)) { Some(e) diff --git a/core/startos/src/s9pk/v2/compat.rs b/core/startos/src/s9pk/v2/compat.rs index f95a7e4bf..109ea43b4 100644 --- a/core/startos/src/s9pk/v2/compat.rs +++ b/core/startos/src/s9pk/v2/compat.rs @@ -18,7 +18,7 @@ use crate::s9pk::merkle_archive::{Entry, MerkleArchive}; use crate::s9pk::rpc::SKIP_ENV; use crate::s9pk::v1::manifest::Manifest as ManifestV1; use crate::s9pk::v1::reader::S9pkReader; -use crate::s9pk::v2::S9pk; +use crate::s9pk::v2::{S9pk, SIG_CONTEXT}; use crate::util::io::TmpDir; use crate::util::Invoke; @@ -40,7 +40,6 @@ enum CompatSource { Buffered(Arc<[u8]>), File(PathBuf), } -#[async_trait::async_trait] impl FileSource for CompatSource { type Reader = Box; async fn size(&self) -> Result { @@ -315,7 +314,7 @@ impl S9pk> { )), )?; - let mut s9pk = S9pk::new(MerkleArchive::new(archive, signer), None).await?; + let mut s9pk = S9pk::new(MerkleArchive::new(archive, signer, SIG_CONTEXT), None).await?; let mut dest_file = File::create(destination.as_ref()).await?; s9pk.serialize(&mut dest_file, false).await?; dest_file.sync_all().await?; diff --git a/core/startos/src/s9pk/v2/mod.rs b/core/startos/src/s9pk/v2/mod.rs index 1051aaaf8..9452b7a72 100644 --- a/core/startos/src/s9pk/v2/mod.rs +++ b/core/startos/src/s9pk/v2/mod.rs @@ -17,6 +17,8 @@ use crate::ARCH; const MAGIC_AND_VERSION: &[u8] = &[0x3b, 0x3b, 0x02]; +pub const SIG_CONTEXT: &str = "s9pk"; + pub mod compat; pub mod manifest; @@ -191,7 +193,7 @@ impl S9pk> { "Invalid Magic or Unexpected Version" ); - let mut archive = MerkleArchive::deserialize(source, &mut header).await?; + let mut archive = MerkleArchive::deserialize(source, SIG_CONTEXT, &mut header).await?; if apply_filter { archive.filter(filter)?; diff --git a/core/startos/src/service/cli.rs b/core/startos/src/service/cli.rs index d3bdccd72..82c63d6a7 100644 --- a/core/startos/src/service/cli.rs +++ b/core/startos/src/service/cli.rs @@ -5,10 +5,11 @@ use clap::Parser; use imbl_value::Value; use once_cell::sync::OnceCell; use rpc_toolkit::yajrc::RpcError; -use rpc_toolkit::{call_remote_socket, yajrc, CallRemote, Context}; +use rpc_toolkit::{call_remote_socket, yajrc, CallRemote, Context, Empty}; use tokio::runtime::Runtime; use crate::lxc::HOST_RPC_SERVER_SOCKET; +use crate::service::service_effect_handler::EffectContext; #[derive(Debug, Default, Parser)] pub struct ContainerClientConfig { @@ -48,9 +49,8 @@ impl Context for ContainerCliContext { } } -#[async_trait::async_trait] -impl CallRemote for ContainerCliContext { - async fn call_remote(&self, method: &str, params: Value) -> Result { +impl CallRemote for ContainerCliContext { + async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { call_remote_socket( tokio::net::UnixStream::connect(&self.0.socket) .await diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index a4e3a4858..a9ee0bd81 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -7,14 +7,13 @@ use futures::future::BoxFuture; use imbl::OrdMap; use models::{HealthCheckId, PackageId, ProcedureName}; use persistent_container::PersistentContainer; -use rpc_toolkit::{from_fn_async, CallRemoteHandler, Empty, Handler, HandlerArgs}; +use rpc_toolkit::{from_fn_async, CallRemoteHandler, Empty, HandlerArgs, HandlerFor}; use serde::{Deserialize, Serialize}; use start_stop::StartStop; use tokio::sync::Notify; use ts_rs::TS; use crate::context::{CliContext, RpcContext}; -use crate::core::rpc_continuations::RequestGuid; use crate::db::model::package::{ InstalledState, PackageDataEntry, PackageState, PackageStateMatchModelRef, UpdatingState, }; @@ -23,6 +22,7 @@ use crate::install::PKG_ARCHIVE_DIR; use crate::lxc::ContainerId; use crate::prelude::*; use crate::progress::{NamedProgress, Progress}; +use crate::rpc_continuations::RequestGuid; use crate::s9pk::S9pk; use crate::service::service_map::InstallProgressHandles; use crate::service::transition::TransitionKind; @@ -510,11 +510,25 @@ pub async fn connect_rpc( } pub async fn connect_rpc_cli( - handle_args: HandlerArgs, + HandlerArgs { + context, + parent_method, + method, + params, + inherited_params, + raw_params, + }: HandlerArgs, ) -> Result<(), Error> { - let ctx = handle_args.context.clone(); - let guid = CallRemoteHandler::::new(from_fn_async(connect_rpc)) - .handle_async(handle_args) + let ctx = context.clone(); + let guid = CallRemoteHandler::::new(from_fn_async(connect_rpc)) + .handle_async(HandlerArgs { + context, + parent_method, + method, + params: rpc_toolkit::util::Flat(params, Empty {}), + inherited_params, + raw_params, + }) .await?; crate::lxc::connect_cli(&ctx, guid).await diff --git a/core/startos/src/service/persistent_container.rs b/core/startos/src/service/persistent_container.rs index e69498e5f..2cb68a121 100644 --- a/core/startos/src/service/persistent_container.rs +++ b/core/startos/src/service/persistent_container.rs @@ -10,7 +10,7 @@ use imbl_value::InternedString; use models::{ProcedureName, VolumeId}; use rpc_toolkit::{Empty, Server, ShutdownHandle}; use serde::de::DeserializeOwned; -use tokio::fs::{create_dir_all, File}; +use tokio::fs::{ File}; use tokio::process::Command; use tokio::sync::{oneshot, watch, Mutex, OnceCell}; use tracing::instrument; diff --git a/core/startos/src/service/service_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs index 0ad45aad1..c5e5433e6 100644 --- a/core/startos/src/service/service_effect_handler.rs +++ b/core/startos/src/service/service_effect_handler.rs @@ -16,7 +16,7 @@ use models::{ ActionId, DataUrl, HealthCheckId, HostId, Id, ImageId, PackageId, ServiceInterfaceId, VolumeId, }; use patch_db::json_ptr::JsonPointer; -use rpc_toolkit::{from_fn, from_fn_async, AnyContext, Context, Empty, HandlerExt, ParentHandler}; +use rpc_toolkit::{from_fn, from_fn_async, Context, Empty, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use tokio::process::Command; use ts_rs::TS; @@ -36,7 +36,7 @@ use crate::net::service_interface::{ ServiceInterfaceWithHostInfo, }; use crate::prelude::*; -use crate::s9pk::merkle_archive::source::http::{HttpReader, HttpSource}; +use crate::s9pk::merkle_archive::source::http::HttpSource; use crate::s9pk::rpc::SKIP_ENV; use crate::s9pk::S9pk; use crate::service::cli::ContainerCliContext; @@ -74,14 +74,17 @@ struct RpcData { method: String, params: Value, } -pub fn service_effect_handler() -> ParentHandler { +pub fn service_effect_handler() -> ParentHandler { ParentHandler::new() - .subcommand("gitInfo", from_fn(crate::version::git_info)) + .subcommand("gitInfo", from_fn(|_: C| crate::version::git_info())) .subcommand( "echo", - from_fn(echo).with_remote_cli::(), + from_fn(echo::).with_call_remote::(), + ) + .subcommand( + "chroot", + from_fn(chroot::).no_display(), ) - .subcommand("chroot", from_fn(chroot).no_display()) .subcommand("exists", from_fn_async(exists).no_cli()) .subcommand("executeAction", from_fn_async(execute_action).no_cli()) .subcommand("getConfigured", from_fn_async(get_configured).no_cli()) @@ -89,35 +92,35 @@ pub fn service_effect_handler() -> ParentHandler { "stopped", from_fn_async(stopped) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "running", from_fn_async(running) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "restart", from_fn_async(restart) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "shutdown", from_fn_async(shutdown) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "setConfigured", from_fn_async(set_configured) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "setMainStatus", - from_fn_async(set_main_status).with_remote_cli::(), + from_fn_async(set_main_status).with_call_remote::(), ) .subcommand("setHealth", from_fn_async(set_health).no_cli()) .subcommand("getStore", from_fn_async(get_store).no_cli()) @@ -129,10 +132,8 @@ pub fn service_effect_handler() -> ParentHandler { .subcommand( "createOverlayedImage", from_fn_async(create_overlayed_image) - .with_custom_display_fn::(|_, (path, _)| { - Ok(println!("{}", path.display())) - }) - .with_remote_cli::(), + .with_custom_display_fn(|_, (path, _)| Ok(println!("{}", path.display()))) + .with_call_remote::(), ) .subcommand( "destroyOverlayedImage", @@ -154,19 +155,19 @@ pub fn service_effect_handler() -> ParentHandler { "setDependencies", from_fn_async(set_dependencies) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "getDependencies", from_fn_async(get_dependencies) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "checkDependencies", from_fn_async(check_dependencies) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand("getSystemSmtp", from_fn_async(get_system_smtp).no_cli()) .subcommand("getContainerIp", from_fn_async(get_container_ip).no_cli()) @@ -493,7 +494,7 @@ struct GetHostInfoParams { callback: Callback, } async fn get_host_info( - _: AnyContext, + _: EffectContext, GetHostInfoParams { .. }: GetHostInfoParams, ) -> Result { todo!() @@ -537,7 +538,7 @@ struct GetServiceInterfaceParams { callback: Callback, } async fn get_service_interface( - _: AnyContext, + _: EffectContext, GetServiceInterfaceParams { callback, package_id, @@ -584,8 +585,8 @@ struct ChrootParams { #[ts(type = "string[]")] args: Vec, } -fn chroot( - _: AnyContext, +fn chroot( + _: C, ChrootParams { env, workdir, @@ -734,7 +735,7 @@ async fn set_store( let model = db .as_private_mut() .as_package_stores_mut() - .upsert(&package_id, || Box::new(json!({})))?; + .upsert(&package_id, || json!({}))?; let mut model_value = model.de()?; if model_value.is_null() { model_value = json!({}); diff --git a/core/startos/src/service/service_map.rs b/core/startos/src/service/service_map.rs index 891741754..02b0538ec 100644 --- a/core/startos/src/service/service_map.rs +++ b/core/startos/src/service/service_map.rs @@ -165,7 +165,7 @@ impl ServiceMap { configured: false, main: MainStatus::Stopped, }, - marketplace_url: None, + registry: None, developer_key: Pem::new(developer_key), icon, last_backup: None, @@ -206,6 +206,7 @@ impl ServiceMap { Some(Duration::from_millis(100)), ))); + download_progress.start(); let mut progress_writer = ProgressTrackerWriter::new( crate::util::io::create_file(&download_path).await?, download_progress, @@ -229,6 +230,7 @@ impl ServiceMap { .await?; Ok(reload_guard .handle_last(async move { + finalization_progress.start(); let s9pk = S9pk::open(&installed_path, Some(&id), true).await?; let prev = if let Some(service) = service.take() { ensure_code!( @@ -246,7 +248,6 @@ impl ServiceMap { service .uninstall(Some(s9pk.as_manifest().version.clone())) .await?; - finalization_progress.complete(); progress_handle.complete(); Some(version) } else { diff --git a/core/startos/src/setup.rs b/core/startos/src/setup.rs index 9120544e3..a035e932f 100644 --- a/core/startos/src/setup.rs +++ b/core/startos/src/setup.rs @@ -7,7 +7,7 @@ use josekit::jwk::Jwk; use openssl::x509::X509; use patch_db::json_ptr::ROOT; use rpc_toolkit::yajrc::RpcError; -use rpc_toolkit::{from_fn_async, HandlerExt, ParentHandler}; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use tokio::fs::File; use tokio::io::AsyncWriteExt; @@ -37,7 +37,7 @@ use crate::util::crypto::EncryptedWire; use crate::util::io::{dir_copy, dir_size, Counter}; use crate::{Error, ErrorKind, ResultExt}; -pub fn setup() -> ParentHandler { +pub fn setup() -> ParentHandler { ParentHandler::new() .subcommand( "status", @@ -45,10 +45,10 @@ pub fn setup() -> ParentHandler { .with_metadata("authenticated", Value::Bool(false)) .no_cli(), ) - .subcommand("disk", disk()) + .subcommand("disk", disk::()) .subcommand("attach", from_fn_async(attach).no_cli()) .subcommand("execute", from_fn_async(execute).no_cli()) - .subcommand("cifs", cifs()) + .subcommand("cifs", cifs::()) .subcommand("complete", from_fn_async(complete).no_cli()) .subcommand( "get-pubkey", @@ -59,7 +59,7 @@ pub fn setup() -> ParentHandler { .subcommand("exit", from_fn_async(exit).no_cli()) } -pub fn disk() -> ParentHandler { +pub fn disk() -> ParentHandler { ParentHandler::new().subcommand( "list", from_fn_async(list_disks) @@ -207,7 +207,7 @@ pub async fn get_pubkey(ctx: SetupContext) -> Result { Ok(pub_key) } -pub fn cifs() -> ParentHandler { +pub fn cifs() -> ParentHandler { ParentHandler::new().subcommand("verify", from_fn_async(verify_cifs).no_cli()) } @@ -360,7 +360,7 @@ pub async fn complete(ctx: SetupContext) -> Result { crate::ErrorKind::InvalidRequest, )); }; - let mut guid_file = File::create("/media/embassy/config/disk.guid").await?; + let mut guid_file = File::create("/media/startos/config/disk.guid").await?; guid_file.write_all(guid.as_bytes()).await?; guid_file.sync_all().await?; Ok(setup_result) @@ -463,7 +463,7 @@ async fn migrate( let _ = crate::disk::main::import( &old_guid, - "/media/embassy/migrate", + "/media/startos/migrate", RepairStrategy::Preen, if guid.ends_with("_UNENC") { None @@ -473,9 +473,9 @@ async fn migrate( ) .await?; - let main_transfer_args = ("/media/embassy/migrate/main/", "/embassy-data/main/"); + let main_transfer_args = ("/media/startos/migrate/main/", "/embassy-data/main/"); let package_data_transfer_args = ( - "/media/embassy/migrate/package-data/", + "/media/startos/migrate/package-data/", "/embassy-data/package-data/", ); @@ -540,7 +540,7 @@ async fn migrate( let (hostname, tor_addr, root_ca) = setup_init(&ctx, Some(start_os_password)).await?; - crate::disk::main::export(&old_guid, "/media/embassy/migrate").await?; + crate::disk::main::export(&old_guid, "/media/startos/migrate").await?; Ok((guid, hostname, tor_addr, root_ca)) } diff --git a/core/startos/src/ssh.rs b/core/startos/src/ssh.rs index 787d54056..82d06be4c 100644 --- a/core/startos/src/ssh.rs +++ b/core/startos/src/ssh.rs @@ -5,7 +5,7 @@ use clap::builder::ValueParserFactory; use clap::Parser; use color_eyre::eyre::eyre; use imbl_value::InternedString; -use rpc_toolkit::{command, from_fn_async, AnyContext, Empty, HandlerExt, ParentHandler}; +use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use tracing::instrument; use ts_rs::TS; @@ -79,28 +79,28 @@ impl std::str::FromStr for SshPubKey { } // #[command(subcommands(add, delete, list,))] -pub fn ssh() -> ParentHandler { +pub fn ssh() -> ParentHandler { ParentHandler::new() .subcommand( "add", from_fn_async(add) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "delete", from_fn_async(delete) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "list", from_fn_async(list) .with_display_serializable() - .with_custom_display_fn::(|handle, result| { + .with_custom_display_fn(|handle, result| { Ok(display_all_ssh_keys(handle.params, result)) }) - .with_remote_cli::(), + .with_call_remote::(), ) } diff --git a/core/startos/src/system.rs b/core/startos/src/system.rs index 1a851fd2e..50b17b54a 100644 --- a/core/startos/src/system.rs +++ b/core/startos/src/system.rs @@ -5,8 +5,7 @@ use chrono::Utc; use clap::Parser; use color_eyre::eyre::eyre; use futures::FutureExt; -use rpc_toolkit::yajrc::RpcError; -use rpc_toolkit::{command, from_fn_async, AnyContext, Empty, HandlerExt, ParentHandler}; +use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use tokio::process::Command; use tokio::sync::broadcast::Receiver; @@ -16,33 +15,30 @@ use ts_rs::TS; use crate::context::{CliContext, RpcContext}; use crate::disk::util::{get_available, get_used}; -use crate::logs::{ - cli_logs_generic_follow, cli_logs_generic_nofollow, fetch_logs, follow_logs, LogFollowResponse, - LogResponse, LogSource, -}; +use crate::logs::{LogSource, LogsParams, SYSTEM_UNIT}; use crate::prelude::*; +use crate::rpc_continuations::RpcContinuations; use crate::shutdown::Shutdown; use crate::util::cpupower::{get_available_governors, set_governor, Governor}; use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat}; use crate::util::Invoke; -use crate::{Error, ErrorKind, ResultExt}; -pub fn experimental() -> ParentHandler { +pub fn experimental() -> ParentHandler { ParentHandler::new() .subcommand( "zram", from_fn_async(zram) .no_display() - .with_remote_cli::(), + .with_call_remote::(), ) .subcommand( "governor", from_fn_async(governor) .with_display_serializable() - .with_custom_display_fn::(|handle, result| { + .with_custom_display_fn(|handle, result| { Ok(display_governor_info(handle.params, result)) }) - .with_remote_cli::(), + .with_call_remote::(), ) } @@ -230,173 +226,13 @@ pub async fn time(ctx: RpcContext, _: Empty) -> Result { uptime: ctx.start_time.elapsed().as_secs(), }) } -#[derive(Deserialize, Serialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "kebab-case")] -pub struct LogsParams { - #[arg(short = 'l', long = "limit")] - #[ts(type = "number | null")] - limit: Option, - #[arg(short = 'c', long = "cursor")] - cursor: Option, - #[arg(short = 'B', long = "before")] - #[serde(default)] - before: bool, - #[arg(short = 'f', long = "follow")] - #[serde(default)] - follow: bool, + +pub fn logs>() -> ParentHandler { + crate::logs::logs(|_: &C, _| async { Ok(LogSource::Unit(SYSTEM_UNIT)) }) } -pub fn logs() -> ParentHandler { - ParentHandler::new() - .root_handler( - from_fn_async(cli_logs) - .no_display() - .with_inherited(|params, _| params), - ) - .root_handler( - from_fn_async(logs_nofollow) - .with_inherited(|params, _| params) - .no_cli(), - ) - .subcommand( - "follow", - from_fn_async(logs_follow) - .with_inherited(|params, _| params) - .no_cli(), - ) -} - -pub async fn cli_logs( - ctx: CliContext, - _: Empty, - LogsParams { - limit, - cursor, - before, - follow, - }: LogsParams, -) -> Result<(), RpcError> { - if follow { - if cursor.is_some() { - return Err(RpcError::from(Error::new( - eyre!("The argument '--cursor ' cannot be used with '--follow'"), - crate::ErrorKind::InvalidRequest, - ))); - } - if before { - return Err(RpcError::from(Error::new( - eyre!("The argument '--before' cannot be used with '--follow'"), - crate::ErrorKind::InvalidRequest, - ))); - } - cli_logs_generic_follow(ctx, "server.logs.follow", None, limit).await - } else { - cli_logs_generic_nofollow(ctx, "server.logs", None, limit, cursor, before).await - } -} -pub async fn logs_nofollow( - _ctx: AnyContext, - _: Empty, - LogsParams { - limit, - cursor, - before, - .. - }: LogsParams, -) -> Result { - fetch_logs(LogSource::System, limit, cursor, before).await -} - -pub async fn logs_follow( - ctx: RpcContext, - _: Empty, - LogsParams { limit, .. }: LogsParams, -) -> Result { - follow_logs(ctx, LogSource::System, limit).await -} -#[derive(Deserialize, Serialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "kebab-case")] -pub struct KernelLogsParams { - #[arg(short = 'l', long = "limit")] - #[ts(type = "number | null")] - limit: Option, - #[arg(short = 'c', long = "cursor")] - cursor: Option, - #[arg(short = 'B', long = "before")] - #[serde(default)] - before: bool, - #[arg(short = 'f', long = "follow")] - #[serde(default)] - follow: bool, -} -pub fn kernel_logs() -> ParentHandler { - ParentHandler::new() - .root_handler( - from_fn_async(cli_kernel_logs) - .no_display() - .with_inherited(|params, _| params), - ) - .root_handler( - from_fn_async(kernel_logs_nofollow) - .with_inherited(|params, _| params) - .no_cli(), - ) - .subcommand( - "follow", - from_fn_async(kernel_logs_follow) - .with_inherited(|params, _| params) - .no_cli(), - ) -} -pub async fn cli_kernel_logs( - ctx: CliContext, - _: Empty, - KernelLogsParams { - limit, - cursor, - before, - follow, - }: KernelLogsParams, -) -> Result<(), RpcError> { - if follow { - if cursor.is_some() { - return Err(RpcError::from(Error::new( - eyre!("The argument '--cursor ' cannot be used with '--follow'"), - crate::ErrorKind::InvalidRequest, - ))); - } - if before { - return Err(RpcError::from(Error::new( - eyre!("The argument '--before' cannot be used with '--follow'"), - crate::ErrorKind::InvalidRequest, - ))); - } - cli_logs_generic_follow(ctx, "server.kernel-logs.follow", None, limit).await - } else { - cli_logs_generic_nofollow(ctx, "server.kernel-logs", None, limit, cursor, before).await - } -} -pub async fn kernel_logs_nofollow( - _ctx: AnyContext, - _: Empty, - KernelLogsParams { - limit, - cursor, - before, - .. - }: KernelLogsParams, -) -> Result { - fetch_logs(LogSource::Kernel, limit, cursor, before).await -} - -pub async fn kernel_logs_follow( - ctx: RpcContext, - _: Empty, - KernelLogsParams { limit, .. }: KernelLogsParams, -) -> Result { - follow_logs(ctx, LogSource::Kernel, limit).await +pub fn kernel_logs>() -> ParentHandler { + crate::logs::logs(|_: &C, _| async { Ok(LogSource::Kernel) }) } #[derive(Serialize, Deserialize)] diff --git a/core/startos/src/update/latest_information.rs b/core/startos/src/update/latest_information.rs deleted file mode 100644 index e897dd40a..000000000 --- a/core/startos/src/update/latest_information.rs +++ /dev/null @@ -1,23 +0,0 @@ -use std::collections::HashMap; - -use emver::Version; -use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, DisplayFromStr}; - -#[serde_as] -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all = "kebab-case")] -pub struct LatestInformation { - release_notes: HashMap, - headline: String, - #[serde_as(as = "DisplayFromStr")] - pub version: Version, -} - -/// Captured from https://beta-registry-0-3.start9labs.com/eos/latest 2021-09-24 -#[test] -fn latest_information_from_server() { - let data_from_server = r#"{"release-notes":{"0.3.0":"This major software release encapsulates the optimal performance, security, and management enhancments to the embassyOS experience."},"headline":"Major embassyOS release","version":"0.3.0"}"#; - let latest_information: LatestInformation = serde_json::from_str(data_from_server).unwrap(); - assert_eq!(latest_information.version.minor(), 3); -} diff --git a/core/startos/src/update/mod.rs b/core/startos/src/update/mod.rs index 05303182d..5bcf8445d 100644 --- a/core/startos/src/update/mod.rs +++ b/core/startos/src/update/mod.rs @@ -1,46 +1,60 @@ -use std::path::PathBuf; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::collections::BTreeMap; +use std::path::Path; +use std::time::Duration; -use clap::Parser; +use clap::{ArgAction, Parser}; use color_eyre::eyre::{eyre, Result}; -use emver::Version; -use helpers::{Rsync, RsyncOptions}; -use lazy_static::lazy_static; +use emver::{Version, VersionRange}; +use futures::{FutureExt, TryStreamExt}; +use helpers::{AtomicFile, NonDetachingJoinHandle}; +use imbl_value::json; +use itertools::Itertools; +use patch_db::json_ptr::JsonPointer; use reqwest::Url; -use rpc_toolkit::command; +use rpc_toolkit::HandlerArgs; use serde::{Deserialize, Serialize}; use tokio::process::Command; -use tokio_stream::StreamExt; use tracing::instrument; use ts_rs::TS; -use crate::context::RpcContext; -use crate::db::model::public::UpdateProgress; -use crate::disk::mount::filesystem::bind::Bind; -use crate::disk::mount::filesystem::ReadWrite; -use crate::disk::mount::guard::MountGuard; +use crate::context::{CliContext, RpcContext}; use crate::notifications::{notify, NotificationLevel}; use crate::prelude::*; -use crate::registry::marketplace::with_query_params; +use crate::progress::{ + FullProgressTracker, FullProgressTrackerHandle, PhaseProgressTrackerHandle, PhasedProgressBar, +}; +use crate::registry::asset::RegistryAsset; +use crate::registry::context::{RegistryContext, RegistryUrlParams}; +use crate::registry::os::index::OsVersionInfo; +use crate::registry::signer::FileValidator; +use crate::rpc_continuations::{RequestGuid, RpcContinuation}; +use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::sound::{ CIRCLE_OF_5THS_SHORT, UPDATE_FAILED_1, UPDATE_FAILED_2, UPDATE_FAILED_3, UPDATE_FAILED_4, }; -use crate::update::latest_information::LatestInformation; use crate::util::Invoke; -use crate::{Error, ErrorKind, ResultExt, PLATFORM}; - -mod latest_information; - -lazy_static! { - static ref UPDATED: AtomicBool = AtomicBool::new(false); -} +use crate::PLATFORM; #[derive(Deserialize, Serialize, Parser, TS)] #[serde(rename_all = "camelCase")] #[command(rename_all = "kebab-case")] pub struct UpdateSystemParams { #[ts(type = "string")] - marketplace_url: Url, + registry: Url, + #[ts(type = "string | null")] + #[arg(long = "to")] + target: Option, + #[arg(long = "no-progress", action = ArgAction::SetFalse)] + #[serde(default)] + progress: bool, +} + +#[derive(Deserialize, Serialize, TS)] +pub struct UpdateSystemRes { + #[ts(type = "string | null")] + target: Option, + #[ts(type = "string | null")] + progress: Option, } /// An user/ daemon would call this to update the system to the latest version and do the updates available, @@ -48,16 +62,137 @@ pub struct UpdateSystemParams { #[instrument(skip_all)] pub async fn update_system( ctx: RpcContext, - UpdateSystemParams { marketplace_url }: UpdateSystemParams, -) -> Result { - if UPDATED.load(Ordering::SeqCst) { - return Ok(UpdateResult::NoUpdates); + UpdateSystemParams { + target, + registry, + progress, + }: UpdateSystemParams, +) -> Result { + if ctx + .db + .peek() + .await + .into_public() + .into_server_info() + .into_status_info() + .into_updated() + .de()? + { + return Err(Error::new(eyre!("Server was already updated. Please restart your device before attempting to update again."), ErrorKind::InvalidRequest)); } - Ok(if maybe_do_update(ctx, marketplace_url).await?.is_some() { - UpdateResult::Updating + let target = + maybe_do_update(ctx.clone(), registry, target.unwrap_or(VersionRange::Any)).await?; + let progress = if progress && target.is_some() { + let guid = RequestGuid::new(); + ctx.clone() + .rpc_continuations + .add( + guid.clone(), + RpcContinuation::ws( + Box::new(|mut ws| { + async move { + if let Err(e) = async { + let mut sub = ctx + .db + .subscribe( + "/public/serverInfo/statusInfo/updateProgress" + .parse::() + .with_kind(ErrorKind::Database)?, + ) + .await; + while { + let progress = ctx + .db + .peek() + .await + .into_public() + .into_server_info() + .into_status_info() + .into_update_progress() + .de()?; + ws.send(axum::extract::ws::Message::Text( + serde_json::to_string(&progress) + .with_kind(ErrorKind::Serialization)?, + )) + .await + .with_kind(ErrorKind::Network)?; + progress.is_some() + } { + sub.recv().await; + } + + ws.close().await.with_kind(ErrorKind::Network)?; + + Ok::<_, Error>(()) + } + .await + { + tracing::error!("Error returning progress of update: {e}"); + tracing::debug!("{e:?}") + } + } + .boxed() + }), + Duration::from_secs(30), + ), + ) + .await; + Some(guid) } else { - UpdateResult::NoUpdates - }) + None + }; + Ok(UpdateSystemRes { target, progress }) +} + +pub async fn cli_update_system( + HandlerArgs { + context, + parent_method, + method, + raw_params, + .. + }: HandlerArgs, +) -> Result<(), Error> { + let res = from_value::( + context + .call_remote::( + &parent_method.into_iter().chain(method).join("."), + raw_params, + ) + .await?, + )?; + match res.target { + None => println!("No updates available"), + Some(v) => { + if let Some(progress) = res.progress { + let mut ws = context.ws_continuation(progress).await?; + let mut progress = PhasedProgressBar::new(&format!("Updating to v{v}...")); + let mut prev = None; + while let Some(msg) = ws.try_next().await.with_kind(ErrorKind::Network)? { + if let tokio_tungstenite::tungstenite::Message::Text(msg) = msg { + if let Some(snap) = + serde_json::from_str(&msg).with_kind(ErrorKind::Deserialization)? + { + progress.update(&snap); + prev = Some(snap); + } else { + break; + } + } + } + if let Some(mut prev) = prev { + for phase in &mut prev.phases { + phase.progress.complete(); + } + prev.overall.complete(); + progress.update(&prev); + } + } else { + println!("Updating to v{v}...") + } + } + } + Ok(()) } /// What is the status of the updates? @@ -80,30 +215,49 @@ pub fn display_update_result(_: UpdateSystemParams, status: UpdateResult) { } #[instrument(skip_all)] -async fn maybe_do_update(ctx: RpcContext, marketplace_url: Url) -> Result, Error> { +async fn maybe_do_update( + ctx: RpcContext, + registry: Url, + target: VersionRange, +) -> Result, Error> { let peeked = ctx.db.peek().await; - let latest_version: Version = ctx - .client - .get(with_query_params( - ctx.clone(), - format!("{}/eos/v0/latest", marketplace_url,).parse()?, - )) - .send() - .await - .with_kind(ErrorKind::Network)? - .json::() - .await - .with_kind(ErrorKind::Network)? - .version; let current_version = peeked.as_public().as_server_info().as_version().de()?; - if latest_version < *current_version { + let mut available = from_value::>( + ctx.call_remote_with::( + "os.version.get", + json!({ + "source": current_version, + "target": target, + }), + RegistryUrlParams { registry }, + ) + .await?, + )?; + let Some((target_version, asset)) = available + .pop_last() + .and_then(|(v, mut info)| info.squashfs.remove(&**PLATFORM).map(|a| (v, a))) + else { return Ok(None); + }; + if !target_version.satisfies(&target) { + return Err(Error::new( + eyre!("got back version from registry that does not satisfy {target}"), + ErrorKind::Registry, + )); } - let eos_url = EosUrl { - base: marketplace_url, - version: latest_version, - }; + let validator = asset.validate(asset.signature_info.all_signers())?; + + let mut progress = FullProgressTracker::new(); + let progress_handle = progress.handle(); + let mut download_phase = progress_handle.add_phase("Downloading File".into(), Some(100)); + download_phase.set_total(validator.size()?); + let reverify_phase = progress_handle.add_phase("Reverifying File".into(), Some(10)); + let sync_boot_phase = progress_handle.add_phase("Syncing Boot Files".into(), Some(1)); + let finalize_phase = progress_handle.add_phase("Finalizing Update".into(), Some(1)); + + let start_progress = progress.snapshot(); + let status = ctx .db .mutate(|db| { @@ -115,10 +269,7 @@ async fn maybe_do_update(ctx: RpcContext, marketplace_url: Url) -> Result( effects: Effects, - imageId: Manifest["images"][number], + image: { id: Manifest["images"][number]; sharedRun?: boolean }, command: ValidIfNoStupidEscape | [string, ...string[]], options: CommandOptions & { mounts?: { path: string; options: MountOptions }[] }, ): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> => { - return runCommand(effects, imageId, command, options) + return runCommand(effects, image, command, options) }, createAction: < @@ -264,7 +265,9 @@ export class StartSdk { ) }, HealthCheck: { - of: healthCheck, + of(o: HealthCheckParams) { + return healthCheck(o) + }, }, Dependency: { of(data: Dependency["data"]) { @@ -740,14 +743,14 @@ export class StartSdk { export async function runCommand( effects: Effects, - imageId: Manifest["images"][number], + image: { id: Manifest["images"][number]; sharedRun?: boolean }, command: string | [string, ...string[]], options: CommandOptions & { mounts?: { path: string; options: MountOptions }[] }, ): Promise<{ stdout: string | Buffer; stderr: string | Buffer }> { const commands = splitCommand(command) - const overlay = await Overlay.of(effects, imageId) + const overlay = await Overlay.of(effects, image) try { for (let mount of options.mounts || []) { await overlay.mount(mount.options, mount.path) diff --git a/sdk/lib/health/HealthCheck.ts b/sdk/lib/health/HealthCheck.ts index d7bbc2f44..ca0e3ebb9 100644 --- a/sdk/lib/health/HealthCheck.ts +++ b/sdk/lib/health/HealthCheck.ts @@ -1,5 +1,5 @@ import { InterfaceReceipt } from "../interfaces/interfaceReceipt" -import { Daemon, Effects } from "../types" +import { Daemon, Effects, SDKManifest } from "../types" import { CheckResult } from "./checkFns/CheckResult" import { HealthReceipt } from "./HealthReceipt" import { Trigger } from "../trigger" @@ -9,16 +9,23 @@ import { once } from "../util/once" import { Overlay } from "../util/Overlay" import { object, unknown } from "ts-matches" -export function healthCheck(o: { +export type HealthCheckParams = { effects: Effects name: string - imageId: string + image: { + id: Manifest["images"][number] + sharedRun?: boolean + } trigger?: Trigger fn(overlay: Overlay): Promise | CheckResult onFirstSuccess?: () => unknown | Promise -}) { +} + +export function healthCheck( + o: HealthCheckParams, +) { new Promise(async () => { - const overlay = await Overlay.of(o.effects, o.imageId) + const overlay = await Overlay.of(o.effects, o.image) try { let currentValue: TriggerInput = { hadSuccess: false, diff --git a/sdk/lib/mainFn/Daemons.ts b/sdk/lib/mainFn/Daemons.ts index 01fd7f2c7..efa1a2484 100644 --- a/sdk/lib/mainFn/Daemons.ts +++ b/sdk/lib/mainFn/Daemons.ts @@ -23,7 +23,7 @@ type Daemon< > = { id: "" extends Id ? never : Id command: ValidIfNoStupidEscape | [string, ...string[]] - imageId: Manifest["images"][number] + image: { id: Manifest["images"][number]; sharedRun?: boolean } mounts: Mounts env?: Record ready: { @@ -40,7 +40,7 @@ export const runDaemon = () => async ( effects: Effects, - imageId: Manifest["images"][number], + image: { id: Manifest["images"][number]; sharedRun?: boolean }, command: ValidIfNoStupidEscape | [string, ...string[]], options: CommandOptions & { mounts?: { path: string; options: MountOptions }[] @@ -48,7 +48,7 @@ export const runDaemon = }, ): Promise => { const commands = splitCommand(command) - const overlay = options.overlay || (await Overlay.of(effects, imageId)) + const overlay = options.overlay || (await Overlay.of(effects, image)) for (let mount of options.mounts || []) { await overlay.mount(mount.options, mount.path) } @@ -183,9 +183,9 @@ export class Daemons { daemon.requires?.map((id) => daemonsStarted[id]) ?? [], ) daemonsStarted[daemon.id] = requiredPromise.then(async () => { - const { command, imageId } = daemon + const { command, image } = daemon - const child = runDaemon()(effects, imageId, command, { + const child = runDaemon()(effects, image, command, { env: daemon.env, mounts: daemon.mounts.build(), }) diff --git a/sdk/lib/osBindings/GetPrimaryUrlParams.ts b/sdk/lib/osBindings/GetPrimaryUrlParams.ts index d9394f4ce..1a68ecc7b 100644 --- a/sdk/lib/osBindings/GetPrimaryUrlParams.ts +++ b/sdk/lib/osBindings/GetPrimaryUrlParams.ts @@ -1,8 +1,10 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Callback } from "./Callback" +import type { HostId } from "./HostId" export type GetPrimaryUrlParams = { packageId: string | null serviceInterfaceId: string callback: Callback + hostId: HostId } diff --git a/sdk/lib/osBindings/RemoveAddressParams.ts b/sdk/lib/osBindings/RemoveAddressParams.ts index bdc781837..14099ebbc 100644 --- a/sdk/lib/osBindings/RemoveAddressParams.ts +++ b/sdk/lib/osBindings/RemoveAddressParams.ts @@ -1,3 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ServiceInterfaceId } from "./ServiceInterfaceId" -export type RemoveAddressParams = { id: string } +export type RemoveAddressParams = { id: ServiceInterfaceId } diff --git a/sdk/lib/osBindings/ServerInfo.ts b/sdk/lib/osBindings/ServerInfo.ts index 284eba336..935e3a99f 100644 --- a/sdk/lib/osBindings/ServerInfo.ts +++ b/sdk/lib/osBindings/ServerInfo.ts @@ -28,4 +28,5 @@ export type ServerInfo = { ntpSynced: boolean zram: boolean governor: Governor | null + smtp: string | null } diff --git a/sdk/lib/osBindings/SetSystemSmtpParams.ts b/sdk/lib/osBindings/SetSystemSmtpParams.ts new file mode 100644 index 000000000..49c66e86c --- /dev/null +++ b/sdk/lib/osBindings/SetSystemSmtpParams.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SetSystemSmtpParams = { smtp: string } diff --git a/sdk/lib/osBindings/index.ts b/sdk/lib/osBindings/index.ts index d14a9fb87..4fcb9a617 100644 --- a/sdk/lib/osBindings/index.ts +++ b/sdk/lib/osBindings/index.ts @@ -113,6 +113,7 @@ export { SetDependenciesParams } from "./SetDependenciesParams" export { SetHealth } from "./SetHealth" export { SetMainStatus } from "./SetMainStatus" export { SetStoreParams } from "./SetStoreParams" +export { SetSystemSmtpParams } from "./SetSystemSmtpParams" export { SignAssetParams } from "./SignAssetParams" export { SignatureInfo } from "./SignatureInfo" export { Signature } from "./Signature" diff --git a/sdk/lib/util/Overlay.ts b/sdk/lib/util/Overlay.ts index 0e2dec58e..794b78732 100644 --- a/sdk/lib/util/Overlay.ts +++ b/sdk/lib/util/Overlay.ts @@ -12,10 +12,19 @@ export class Overlay { readonly rootfs: string, readonly guid: string, ) {} - static async of(effects: T.Effects, imageId: string) { + static async of( + effects: T.Effects, + image: { id: string; sharedRun?: boolean }, + ) { + const { id: imageId, sharedRun } = image const [rootfs, guid] = await effects.createOverlayedImage({ imageId }) - for (const dirPart of ["dev", "sys", "proc", "run"] as const) { + const shared = ["dev", "sys", "proc"] + if (!!sharedRun) { + shared.push("run") + } + + for (const dirPart of shared) { await fs.mkdir(`${rootfs}/${dirPart}`, { recursive: true }) await execFile("mount", [ "--rbind", diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 9f17c22eb..a4ed61278 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -74,6 +74,7 @@ export const mockPatchData: DataModel = { platform: 'x86_64-nonfree', zram: true, governor: 'performance', + smtp: 'todo', wifi: { interface: 'wlan0', ssids: [], From 0b8a142de0c3dc8d791bcc6420e2fde407ed81dd Mon Sep 17 00:00:00 2001 From: Jade <2364004+Blu-J@users.noreply.github.com> Date: Mon, 13 May 2024 10:50:25 -0600 Subject: [PATCH 022/125] fix: Making the daemons keep up the status. (#2617) * complete get_primary_url fn * complete clear_network_interfaces fn * formatting * complete remove_address fn * get_system_smtp wip * complete get_system_smtp and set_system_smtp * add SetSystemSmtpParams struct * add set_system_smtp subcommand * Remove 'Copy' implementation from `HostAddress` Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> * Refactor `get_host_primary` fn and clone resulting `HostAddress` Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> * misc fixes and debug info * seed hosts with a tor address * fix: Making the daemons keep up the status. * wipFix: Making a service start * fix: Both the start + stop of the service. * fix: Weird edge case of failure and kids --------- Co-authored-by: Shadowy Super Coder Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Co-authored-by: Aiden McClelland --- .../Systems/SystemForEmbassy/MainLoop.ts | 16 +- .../Systems/SystemForEmbassy/index.ts | 4 +- .../SystemForEmbassy/polyfillEffects.ts | 2 +- container-runtime/src/Models/Duration.ts | 2 +- container-runtime/src/Models/Effects.ts | 4 +- .../src/service/service_effect_handler.rs | 37 ++- sdk/lib/mainFn/CommandController.ts | 108 +++++++++ sdk/lib/mainFn/Daemon.ts | 79 +++++++ sdk/lib/mainFn/Daemons.ts | 215 ++++++------------ sdk/lib/mainFn/HealthDaemon.ts | 152 +++++++++++++ sdk/lib/osBindings/SetMainStatus.ts | 2 +- sdk/lib/test/startosTypeValidation.test.ts | 7 +- sdk/lib/types.ts | 5 +- sdk/package.json | 2 +- 14 files changed, 467 insertions(+), 168 deletions(-) create mode 100644 sdk/lib/mainFn/CommandController.ts create mode 100644 sdk/lib/mainFn/Daemon.ts create mode 100644 sdk/lib/mainFn/HealthDaemon.ts diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts index 23be7071f..917bfff83 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts @@ -3,6 +3,7 @@ import { DockerProcedureContainer } from "./DockerProcedureContainer" import { SystemForEmbassy } from "." import { HostSystemStartOs } from "../../HostSystemStartOs" import { Daemons, T, daemons } from "@start9labs/start-sdk" +import { Daemon } from "@start9labs/start-sdk/cjs/lib/mainFn/Daemon" const EMBASSY_HEALTH_INTERVAL = 15 * 1000 const EMBASSY_PROPERTIES_LOOP = 30 * 1000 @@ -21,8 +22,7 @@ export class MainLoop { private mainEvent: | Promise<{ - daemon: T.DaemonReturned - wait: Promise + daemon: Daemon }> | undefined constructor( @@ -51,7 +51,7 @@ export class MainLoop { if (jsMain) { throw new Error("Unreachable") } - const daemon = await daemons.runDaemon()( + const daemon = await Daemon.of()( this.effects, { id: this.system.manifest.main.image }, currentCommand, @@ -59,14 +59,9 @@ export class MainLoop { overlay: dockerProcedureContainer.overlay, }, ) + daemon.start() return { daemon, - wait: daemon.wait().finally(() => { - this.clean() - effects - .setMainStatus({ status: "stopped" }) - .catch((e) => console.error("Could not set the status to stopped")) - }), } } @@ -121,7 +116,8 @@ export class MainLoop { const main = await mainEvent delete this.mainEvent delete this.healthLoops - if (mainEvent) await main?.daemon.term() + await main?.daemon.stop().catch((e) => console.error(e)) + this.effects.setMainStatus({ status: "stopped" }) if (healthLoops) healthLoops.forEach((x) => clearInterval(x.interval)) } diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index 8988c5446..ada893b2b 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -385,13 +385,15 @@ export class SystemForEmbassy implements System { timeoutMs: number | null, ): Promise { const { currentRunning } = this + this.currentRunning?.clean() delete this.currentRunning if (currentRunning) { await currentRunning.clean({ timeout: this.manifest.main["sigterm-timeout"], }) } - return duration(this.manifest.main["sigterm-timeout"], "s") + const durationValue = duration(this.manifest.main["sigterm-timeout"], "s") + return durationValue } private async createBackup( effects: HostSystemStartOs, diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts index 30e572d97..12c0a58d5 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts @@ -116,7 +116,7 @@ export class PolyfillEffects implements oet.Effects { this.manifest.volumes, ) const daemon = dockerProcedureContainer.then((dockerProcedureContainer) => - daemons.runDaemon()( + daemons.runCommand()( this.effects, { id: this.manifest.main.image }, [input.command, ...(input.args || [])], diff --git a/container-runtime/src/Models/Duration.ts b/container-runtime/src/Models/Duration.ts index 8c701a703..75154c782 100644 --- a/container-runtime/src/Models/Duration.ts +++ b/container-runtime/src/Models/Duration.ts @@ -2,5 +2,5 @@ export type TimeUnit = "d" | "h" | "s" | "ms" export type Duration = `${number}${TimeUnit}` export function duration(timeValue: number, timeUnit: TimeUnit = "s") { - return `${timeValue}${timeUnit}` as Duration + return `${timeValue > 0 ? timeValue : 0}${timeUnit}` as Duration } diff --git a/container-runtime/src/Models/Effects.ts b/container-runtime/src/Models/Effects.ts index 757d51238..bacf8894a 100644 --- a/container-runtime/src/Models/Effects.ts +++ b/container-runtime/src/Models/Effects.ts @@ -1,5 +1,3 @@ import { types as T } from "@start9labs/start-sdk" -export type Effects = T.Effects & { - setMainStatus(o: { status: "running" | "stopped" }): Promise -} +export type Effects = T.Effects diff --git a/core/startos/src/service/service_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs index 0bd31dc9b..44d17da30 100644 --- a/core/startos/src/service/service_effect_handler.rs +++ b/core/startos/src/service/service_effect_handler.rs @@ -581,10 +581,28 @@ struct GetHostInfoParams { callback: Callback, } async fn get_host_info( - _: EffectContext, + ctx: EffectContext, GetHostInfoParams { .. }: GetHostInfoParams, ) -> Result { - todo!() + let ctx = ctx.deref()?; + Ok(json!({ + "id": "fakeId1", + "kind": "multi", + "hostnames": [{ + "kind": "ip", + "networkInterfaceId": "fakeNetworkInterfaceId1", + "public": true, + "hostname":{ + "kind": "domain", + "domain": format!("{}", ctx.id), + "subdomain": (), + "port": (), + "sslPort": () + } + } + + ] + })) } async fn clear_bindings(context: EffectContext, _: Empty) -> Result { @@ -1011,21 +1029,23 @@ async fn set_configured(context: EffectContext, params: SetConfigured) -> Result #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export)] -enum Status { +enum SetMainStatusStatus { Running, Stopped, + Starting, } -impl FromStr for Status { +impl FromStr for SetMainStatusStatus { type Err = color_eyre::eyre::Report; fn from_str(s: &str) -> Result { match s { "running" => Ok(Self::Running), "stopped" => Ok(Self::Stopped), + "starting" => Ok(Self::Starting), _ => Err(eyre!("unknown status {s}")), } } } -impl ValueParserFactory for Status { +impl ValueParserFactory for SetMainStatusStatus { type Parser = FromStrParser; fn value_parser() -> Self::Parser { FromStrParser::new() @@ -1037,14 +1057,15 @@ impl ValueParserFactory for Status { #[command(rename_all = "camelCase")] #[ts(export)] struct SetMainStatus { - status: Status, + status: SetMainStatusStatus, } async fn set_main_status(context: EffectContext, params: SetMainStatus) -> Result { dbg!(format!("Status for main will be is {params:?}")); let context = context.deref()?; match params.status { - Status::Running => context.started(), - Status::Stopped => context.stopped(), + SetMainStatusStatus::Running => context.started(), + SetMainStatusStatus::Stopped => context.stopped(), + SetMainStatusStatus::Starting => context.stopped(), } Ok(Value::Null) } diff --git a/sdk/lib/mainFn/CommandController.ts b/sdk/lib/mainFn/CommandController.ts new file mode 100644 index 000000000..e2e11dbfc --- /dev/null +++ b/sdk/lib/mainFn/CommandController.ts @@ -0,0 +1,108 @@ +import { NO_TIMEOUT, SIGTERM } from "../StartSdk" +import { SDKManifest } from "../manifest/ManifestTypes" +import { Effects, ValidIfNoStupidEscape } from "../types" +import { MountOptions, Overlay } from "../util/Overlay" +import { splitCommand } from "../util/splitCommand" +import { cpExecFile } from "./Daemons" + +export class CommandController { + private constructor( + readonly runningAnswer: Promise, + readonly overlay: Overlay, + readonly pid: number | undefined, + ) {} + static of() { + return async ( + effects: Effects, + imageId: { + id: Manifest["images"][number] + sharedRun?: boolean + }, + command: ValidIfNoStupidEscape | [string, ...string[]], + options: { + mounts?: { path: string; options: MountOptions }[] + overlay?: Overlay + env?: + | { + [variable: string]: string + } + | undefined + cwd?: string | undefined + user?: string | undefined + onStdout?: (x: Buffer) => void + onStderr?: (x: Buffer) => void + }, + ) => { + const commands = splitCommand(command) + const overlay = options.overlay || (await Overlay.of(effects, imageId)) + for (let mount of options.mounts || []) { + await overlay.mount(mount.options, mount.path) + } + const childProcess = await overlay.spawn(commands, { + env: options.env, + }) + const answer = new Promise((resolve, reject) => { + childProcess.stdout.on( + "data", + options.onStdout ?? + ((data: any) => { + console.log(data.toString()) + }), + ) + childProcess.stderr.on( + "data", + options.onStderr ?? + ((data: any) => { + console.error(data.toString()) + }), + ) + + childProcess.on("exit", (code: any) => { + if (code === 0) { + return resolve(null) + } + return reject(new Error(`${commands[0]} exited with code ${code}`)) + }) + }) + + const pid = childProcess.pid + + return new CommandController(answer, overlay, pid) + } + } + async wait() { + try { + return await this.runningAnswer + } finally { + await cpExecFile("pkill", ["-9", "-s", String(this.pid)]).catch((_) => {}) + await this.overlay.destroy().catch((_) => {}) + } + } + async term({ signal = SIGTERM, timeout = NO_TIMEOUT } = {}) { + try { + await cpExecFile("pkill", [ + `-${signal.replace("SIG", "")}`, + "-s", + String(this.pid), + ]) + + if (timeout > NO_TIMEOUT) { + const didTimeout = await Promise.race([ + new Promise((resolve) => setTimeout(resolve, timeout)).then( + () => true, + ), + this.runningAnswer.then(() => false), + ]) + if (didTimeout) { + await cpExecFile("pkill", [`-9`, "-s", String(this.pid)]).catch( + (_: any) => {}, + ) + } + } else { + await this.runningAnswer + } + } finally { + await this.overlay.destroy() + } + } +} diff --git a/sdk/lib/mainFn/Daemon.ts b/sdk/lib/mainFn/Daemon.ts new file mode 100644 index 000000000..2ec438801 --- /dev/null +++ b/sdk/lib/mainFn/Daemon.ts @@ -0,0 +1,79 @@ +import { SDKManifest } from "../manifest/ManifestTypes" +import { Effects, ValidIfNoStupidEscape } from "../types" +import { MountOptions, Overlay } from "../util/Overlay" +import { CommandController } from "./CommandController" + +const TIMEOUT_INCREMENT_MS = 1000 +const MAX_TIMEOUT_MS = 30000 +/** + * This is a wrapper around CommandController that has a state of off, where the command shouldn't be running + * and the others state of running, where it will keep a living running command + */ + +export class Daemon { + private commandController: CommandController | null = null + private shouldBeRunning = false + private constructor(private startCommand: () => Promise) {} + static of() { + return async ( + effects: Effects, + imageId: { + id: Manifest["images"][number] + sharedRun?: boolean + }, + command: ValidIfNoStupidEscape | [string, ...string[]], + options: { + mounts?: { path: string; options: MountOptions }[] + overlay?: Overlay + env?: + | { + [variable: string]: string + } + | undefined + cwd?: string | undefined + user?: string | undefined + onStdout?: (x: Buffer) => void + onStderr?: (x: Buffer) => void + }, + ) => { + const startCommand = () => + CommandController.of()(effects, imageId, command, options) + return new Daemon(startCommand) + } + } + + async start() { + if (this.commandController) { + return + } + this.shouldBeRunning = true + let timeoutCounter = 0 + new Promise(async () => { + while (this.shouldBeRunning) { + this.commandController = await this.startCommand() + await this.commandController.wait().catch((err) => console.error(err)) + await new Promise((resolve) => setTimeout(resolve, timeoutCounter)) + timeoutCounter += TIMEOUT_INCREMENT_MS + timeoutCounter = Math.max(MAX_TIMEOUT_MS, timeoutCounter) + } + }).catch((err) => { + console.error(err) + }) + } + async term(termOptions?: { + signal?: NodeJS.Signals | undefined + timeout?: number | undefined + }) { + return this.stop(termOptions) + } + async stop(termOptions?: { + signal?: NodeJS.Signals | undefined + timeout?: number | undefined + }) { + this.shouldBeRunning = false + await this.commandController + ?.term(termOptions) + .catch((e) => console.error(e)) + this.commandController = null + } +} diff --git a/sdk/lib/mainFn/Daemons.ts b/sdk/lib/mainFn/Daemons.ts index efa1a2484..5771136c9 100644 --- a/sdk/lib/mainFn/Daemons.ts +++ b/sdk/lib/mainFn/Daemons.ts @@ -13,98 +13,38 @@ import { splitCommand } from "../util/splitCommand" import { promisify } from "node:util" import * as CP from "node:child_process" +export { Daemon } from "./Daemon" +export { CommandController } from "./CommandController" +import { HealthDaemon } from "./HealthDaemon" +import { Daemon } from "./Daemon" +import { CommandController } from "./CommandController" + const cpExec = promisify(CP.exec) -const cpExecFile = promisify(CP.execFile) -type Daemon< +export const cpExecFile = promisify(CP.execFile) +export type Ready = { + display: string | null + fn: () => Promise | CheckResult + trigger?: Trigger +} + +type DaemonsParams< Manifest extends SDKManifest, Ids extends string, Command extends string, Id extends string, > = { - id: "" extends Id ? never : Id command: ValidIfNoStupidEscape | [string, ...string[]] image: { id: Manifest["images"][number]; sharedRun?: boolean } - mounts: Mounts + mounts: { path: string; options: MountOptions }[] env?: Record - ready: { - display: string | null - fn: () => Promise | CheckResult - trigger?: Trigger - } + ready: Ready requires: Exclude[] } type ErrorDuplicateId = `The id '${Id}' is already used` -export const runDaemon = - () => - async ( - effects: Effects, - image: { id: Manifest["images"][number]; sharedRun?: boolean }, - command: ValidIfNoStupidEscape | [string, ...string[]], - options: CommandOptions & { - mounts?: { path: string; options: MountOptions }[] - overlay?: Overlay - }, - ): Promise => { - const commands = splitCommand(command) - const overlay = options.overlay || (await Overlay.of(effects, image)) - for (let mount of options.mounts || []) { - await overlay.mount(mount.options, mount.path) - } - const childProcess = await overlay.spawn(commands, { - env: options.env, - }) - const answer = new Promise((resolve, reject) => { - childProcess.stdout.on("data", (data: any) => { - console.log(data.toString()) - }) - childProcess.stderr.on("data", (data: any) => { - console.error(data.toString()) - }) - - childProcess.on("exit", (code: any) => { - if (code === 0) { - return resolve(null) - } - return reject(new Error(`${commands[0]} exited with code ${code}`)) - }) - }) - - const pid = childProcess.pid - return { - async wait() { - try { - return await answer - } finally { - await cpExecFile("pkill", ["-9", "-s", String(pid)]).catch((_) => {}) - } - }, - async term({ signal = SIGTERM, timeout = NO_TIMEOUT } = {}) { - try { - await cpExecFile("pkill", [`-${signal}`, "-s", String(pid)]) - - if (timeout > NO_TIMEOUT) { - const didTimeout = await Promise.race([ - new Promise((resolve) => setTimeout(resolve, timeout)).then( - () => true, - ), - answer.then(() => false), - ]) - if (didTimeout) { - await cpExecFile("pkill", [`-9`, "-s", String(pid)]).catch( - (_) => {}, - ) - } - } else { - await answer - } - } finally { - await overlay.destroy() - } - }, - } - } +export const runCommand = () => + CommandController.of() /** * A class for defining and controlling the service daemons @@ -133,7 +73,9 @@ export class Daemons { private constructor( readonly effects: Effects, readonly started: (onTerm: () => PromiseLike) => PromiseLike, - readonly daemons?: Daemon[], + readonly daemons: Promise[], + readonly ids: Ids[], + readonly healthDaemons: HealthDaemon[], ) {} /** * Returns an empty new Daemons class with the provided config. @@ -150,7 +92,13 @@ export class Daemons { started: (onTerm: () => PromiseLike) => PromiseLike healthReceipts: HealthReceipt[] }) { - return new Daemons(config.effects, config.started) + return new Daemons( + config.effects, + config.started, + [], + [], + [], + ) } /** * Returns the complete list of daemons, including the one defined here @@ -165,73 +113,60 @@ export class Daemons { ErrorDuplicateId extends Id ? never : Id extends Ids ? ErrorDuplicateId : Id, - newDaemon: Omit, "id">, + options: DaemonsParams, ) { - const daemons = ((this?.daemons ?? []) as any[]).concat({ - ...newDaemon, + const daemonIndex = this.daemons.length + const daemon = Daemon.of()( + this.effects, + options.image, + options.command, + options, + ) + const healthDaemon = new HealthDaemon( + daemon, + daemonIndex, + options.requires + .map((x) => this.ids.indexOf(id as any)) + .filter((x) => x >= 0) + .map((id) => this.healthDaemons[id]), id, - }) - return new Daemons(this.effects, this.started, daemons) + this.ids, + options.ready, + this.effects, + ) + const daemons = this.daemons.concat(daemon) + const ids = [...this.ids, id] as (Ids | Id)[] + const healthDaemons = [...this.healthDaemons, healthDaemon] + return new Daemons( + this.effects, + this.started, + daemons, + ids, + healthDaemons, + ) } async build() { - const daemonsStarted = {} as Record> - const { effects } = this - const daemons = this.daemons ?? [] - for (const daemon of daemons) { - const requiredPromise = Promise.all( - daemon.requires?.map((id) => daemonsStarted[id]) ?? [], - ) - daemonsStarted[daemon.id] = requiredPromise.then(async () => { - const { command, image } = daemon - - const child = runDaemon()(effects, image, command, { - env: daemon.env, - mounts: daemon.mounts.build(), - }) - let currentInput: TriggerInput = {} - const getCurrentInput = () => currentInput - const trigger = (daemon.ready.trigger ?? defaultTrigger)( - getCurrentInput, - ) - return new Promise(async (resolve) => { - for ( - let res = await trigger.next(); - !res.done; - res = await trigger.next() - ) { - const response = await Promise.resolve(daemon.ready.fn()).catch( - (err) => - ({ - status: "failure", - message: "message" in err ? err.message : String(err), - }) as CheckResult, - ) - currentInput.lastResult = response.status || null - if (!currentInput.hadSuccess && response.status === "success") { - currentInput.hadSuccess = true - resolve(child) - } - } - resolve(child) - }) - }) - } + this.updateMainHealth() + this.healthDaemons.forEach((x) => + x.addWatcher(() => this.updateMainHealth()), + ) return { - async term(options?: { signal?: Signals; timeout?: number }) { - await Promise.all( - Object.values>(daemonsStarted).map((x) => - x.then((x) => x.term(options)), - ), - ) - }, - async wait() { - await Promise.all( - Object.values>(daemonsStarted).map((x) => - x.then((x) => x.wait()), - ), - ) + term: async (options?: { signal?: Signals; timeout?: number }) => { + try { + await Promise.all(this.healthDaemons.map((x) => x.term(options))) + } finally { + this.effects.setMainStatus({ status: "stopped" }) + } }, } } + + private updateMainHealth() { + if (this.healthDaemons.every((x) => x.health.status === "success")) { + this.effects.setMainStatus({ status: "running" }) + } else { + this.effects.setMainStatus({ status: "starting" }) + } + } } diff --git a/sdk/lib/mainFn/HealthDaemon.ts b/sdk/lib/mainFn/HealthDaemon.ts new file mode 100644 index 000000000..84e9e34d7 --- /dev/null +++ b/sdk/lib/mainFn/HealthDaemon.ts @@ -0,0 +1,152 @@ +import { CheckResult } from "../health/checkFns" +import { defaultTrigger } from "../trigger/defaultTrigger" +import { Ready } from "./Daemons" +import { Daemon } from "./Daemon" +import { Effects } from "../types" + +const oncePromise = () => { + let resolve: (value: T) => void + const promise = new Promise((res) => { + resolve = res + }) + return { resolve: resolve!, promise } +} + +/** + * Wanted a structure that deals with controlling daemons by their health status + * States: + * -- Waiting for dependencies to be success + * -- Running: Daemon is running and the status is in the health + * + */ +export class HealthDaemon { + #health: CheckResult = { status: "starting", message: null } + #healthWatchers: Array<() => unknown> = [] + #running = false + #hadSuccess = false + constructor( + readonly daemon: Promise, + readonly daemonIndex: number, + readonly dependencies: HealthDaemon[], + readonly id: string, + readonly ids: string[], + readonly ready: Ready, + readonly effects: Effects, + ) { + this.updateStatus() + this.dependencies.forEach((d) => d.addWatcher(() => this.updateStatus())) + } + + /** Run after we want to do cleanup */ + async term(termOptions?: { + signal?: NodeJS.Signals | undefined + timeout?: number | undefined + }) { + this.#healthWatchers = [] + this.#running = false + this.#healthCheckCleanup?.() + + await this.daemon.then((d) => d.stop(termOptions)) + } + + /** Want to add another notifier that the health might have changed */ + addWatcher(watcher: () => unknown) { + this.#healthWatchers.push(watcher) + } + + get health() { + return Object.freeze(this.#health) + } + + private async changeRunning(newStatus: boolean) { + if (this.#running === newStatus) return + + this.#running = newStatus + + if (newStatus) { + ;(await this.daemon).start() + this.setupHealthCheck() + } else { + ;(await this.daemon).stop() + this.turnOffHealthCheck() + + this.setHealth({ status: "starting", message: null }) + } + } + + #healthCheckCleanup: (() => void) | null = null + private turnOffHealthCheck() { + this.#healthCheckCleanup?.() + } + private async setupHealthCheck() { + if (this.#healthCheckCleanup) return + const trigger = (this.ready.trigger ?? defaultTrigger)(() => ({ + hadSuccess: this.#hadSuccess, + lastResult: this.#health.status, + })) + + const { promise: status, resolve: setStatus } = oncePromise<{ + done: true + }>() + new Promise(async () => { + for ( + let res = await Promise.race([status, trigger.next()]); + !res.done; + res = await Promise.race([status, trigger.next()]) + ) { + const response: CheckResult = await Promise.resolve( + this.ready.fn(), + ).catch((err) => { + console.error(err) + return { + status: "failure", + message: "message" in err ? err.message : String(err), + } + }) + this.setHealth(response) + if (response.status === "success") { + this.#hadSuccess = true + } + } + }).catch((err) => console.error(`Daemon ${this.id} failed: ${err}`)) + + this.#healthCheckCleanup = () => { + setStatus({ done: true }) + this.#healthCheckCleanup = null + } + } + + private setHealth(health: CheckResult) { + this.#health = health + this.#healthWatchers.forEach((watcher) => watcher()) + const display = this.ready.display + const status = health.status + if (!display) { + return + } + if ( + status === "success" || + status === "disabled" || + status === "starting" + ) { + this.effects.setHealth({ + result: status, + message: health.message, + id: display, + name: display, + }) + } else { + this.effects.setHealth({ + result: health.status, + message: health.message || "", + id: display, + name: display, + }) + } + } + + private async updateStatus() { + const healths = this.dependencies.map((d) => d.#health) + this.changeRunning(healths.every((x) => x.status === "success")) + } +} diff --git a/sdk/lib/osBindings/SetMainStatus.ts b/sdk/lib/osBindings/SetMainStatus.ts index 32a839984..aa4aff0b2 100644 --- a/sdk/lib/osBindings/SetMainStatus.ts +++ b/sdk/lib/osBindings/SetMainStatus.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Status } from "./Status" -export type SetMainStatus = { status: Status } +export type SetMainStatus = { status: "running" | "stopped" | "starting" } diff --git a/sdk/lib/test/startosTypeValidation.test.ts b/sdk/lib/test/startosTypeValidation.test.ts index fd11ab5b6..4f6d50f53 100644 --- a/sdk/lib/test/startosTypeValidation.test.ts +++ b/sdk/lib/test/startosTypeValidation.test.ts @@ -1,5 +1,9 @@ import { Effects } from "../types" -import { CheckDependenciesParam, ExecuteAction } from ".././osBindings" +import { + CheckDependenciesParam, + ExecuteAction, + SetMainStatus, +} from ".././osBindings" import { CreateOverlayedImageParams } from ".././osBindings" import { DestroyOverlayedImageParams } from ".././osBindings" import { BindParams } from ".././osBindings" @@ -66,6 +70,7 @@ describe("startosTypeValidation ", () => { mount: {} as MountParams, checkDependencies: {} as CheckDependenciesParam, getDependencies: undefined, + setMainStatus: {} as SetMainStatus, }) typeEquality[0]>( testInput as ExecuteAction, diff --git a/sdk/lib/types.ts b/sdk/lib/types.ts index 96e256766..7db42e5a9 100644 --- a/sdk/lib/types.ts +++ b/sdk/lib/types.ts @@ -4,6 +4,7 @@ import { DependencyRequirement, SetHealth, HealthCheckResult, + SetMainStatus, } from "./osBindings" import { MainEffects, ServiceInterfaceType, Signals } from "./StartSdk" @@ -163,7 +164,7 @@ export type CommandType = | [string, ...string[]] export type DaemonReturned = { - wait(): Promise + wait(): Promise term(options?: { signal?: Signals; timeout?: number }): Promise } @@ -380,6 +381,8 @@ export type Effects = { }): Promise } + setMainStatus(o: SetMainStatus): Promise + getSystemSmtp(input: { callback: (config: unknown, previousConfig: unknown) => void }): Promise diff --git a/sdk/package.json b/sdk/package.json index 409c5bb02..db387c036 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@start9labs/start-sdk", - "version": "0.4.0-rev0.lib0.rc8.beta10", + "version": "0.3.6-alpha1", "description": "Software development kit to facilitate packaging services for StartOS", "main": "./cjs/lib/index.js", "types": "./cjs/lib/index.d.ts", From 0ccbb52c1fdf3c3a7e935207d18502ee6bde34f8 Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Thu, 16 May 2024 19:54:36 -0600 Subject: [PATCH 023/125] wait for whole session to exit when sigterm (#2620) * wait for whole session to exit when sigterm * fix lint * rename poorly named variable --- sdk/lib/StartSdk.ts | 2 +- sdk/lib/mainFn/CommandController.ts | 74 ++++++++++++++++++++++------- sdk/lib/mainFn/Daemons.ts | 2 +- 3 files changed, 59 insertions(+), 19 deletions(-) diff --git a/sdk/lib/StartSdk.ts b/sdk/lib/StartSdk.ts index f2eabe6be..381914c13 100644 --- a/sdk/lib/StartSdk.ts +++ b/sdk/lib/StartSdk.ts @@ -91,7 +91,7 @@ export type ServiceInterfaceType = "ui" | "p2p" | "api" export type MainEffects = Effects & { _type: "main" } export type Signals = NodeJS.Signals export const SIGTERM: Signals = "SIGTERM" -export const SIGKILL: Signals = "SIGTERM" +export const SIGKILL: Signals = "SIGKILL" export const NO_TIMEOUT = -1 function removeConstType() { diff --git a/sdk/lib/mainFn/CommandController.ts b/sdk/lib/mainFn/CommandController.ts index e2e11dbfc..a68e1e426 100644 --- a/sdk/lib/mainFn/CommandController.ts +++ b/sdk/lib/mainFn/CommandController.ts @@ -1,9 +1,9 @@ -import { NO_TIMEOUT, SIGTERM } from "../StartSdk" +import { NO_TIMEOUT, SIGKILL, SIGTERM } from "../StartSdk" import { SDKManifest } from "../manifest/ManifestTypes" import { Effects, ValidIfNoStupidEscape } from "../types" import { MountOptions, Overlay } from "../util/Overlay" import { splitCommand } from "../util/splitCommand" -import { cpExecFile } from "./Daemons" +import { cpExecFile, cpExec } from "./Daemons" export class CommandController { private constructor( @@ -74,11 +74,16 @@ export class CommandController { try { return await this.runningAnswer } finally { - await cpExecFile("pkill", ["-9", "-s", String(this.pid)]).catch((_) => {}) + if (this.pid !== undefined) { + await cpExecFile("pkill", ["-9", "-s", String(this.pid)]).catch( + (_) => {}, + ) + } await this.overlay.destroy().catch((_) => {}) } } async term({ signal = SIGTERM, timeout = NO_TIMEOUT } = {}) { + if (this.pid === undefined) return try { await cpExecFile("pkill", [ `-${signal.replace("SIG", "")}`, @@ -86,23 +91,58 @@ export class CommandController { String(this.pid), ]) - if (timeout > NO_TIMEOUT) { - const didTimeout = await Promise.race([ - new Promise((resolve) => setTimeout(resolve, timeout)).then( - () => true, - ), - this.runningAnswer.then(() => false), - ]) - if (didTimeout) { - await cpExecFile("pkill", [`-9`, "-s", String(this.pid)]).catch( - (_: any) => {}, - ) - } - } else { - await this.runningAnswer + const didTimeout = await waitSession(this.pid, timeout) + if (didTimeout) { + await cpExecFile("pkill", [`-9`, "-s", String(this.pid)]).catch( + (_) => {}, + ) } } finally { await this.overlay.destroy() } } } + +function waitSession( + sid: number, + timeout = NO_TIMEOUT, + interval = 100, +): Promise { + let nextInterval = interval * 2 + if (timeout >= 0 && timeout < nextInterval) { + nextInterval = timeout + } + let nextTimeout = timeout + if (timeout > 0) { + if (timeout >= interval) { + nextTimeout -= interval + } else { + nextTimeout = 0 + } + } + return new Promise((resolve, reject) => { + let next: NodeJS.Timeout | null = null + if (timeout !== 0) { + next = setTimeout(() => { + waitSession(sid, nextTimeout, nextInterval).then(resolve, reject) + }, interval) + } + cpExecFile("ps", [`--sid=${sid}`, "-o", "--pid="]).then( + (_) => { + if (timeout === 0) { + resolve(true) + } + }, + (e) => { + if (next) { + clearTimeout(next) + } + if (typeof e === "object" && e && "code" in e && e.code) { + resolve(false) + } else { + reject(e) + } + }, + ) + }) +} diff --git a/sdk/lib/mainFn/Daemons.ts b/sdk/lib/mainFn/Daemons.ts index 5771136c9..8962061f5 100644 --- a/sdk/lib/mainFn/Daemons.ts +++ b/sdk/lib/mainFn/Daemons.ts @@ -19,7 +19,7 @@ import { HealthDaemon } from "./HealthDaemon" import { Daemon } from "./Daemon" import { CommandController } from "./CommandController" -const cpExec = promisify(CP.exec) +export const cpExec = promisify(CP.exec) export const cpExecFile = promisify(CP.execFile) export type Ready = { display: string | null From fd7c2fbe9343eafee33f818e8a4650ce639fe0f2 Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Fri, 31 May 2024 12:13:23 -0600 Subject: [PATCH 024/125] Feature/registry package index (#2623) * include system images in compat s9pk * wip * wip * update types * wip * fix signature serialization * Add SignatureHeader conversions * finish display impl for get --------- Co-authored-by: Shadowy Super Coder --- Makefile | 3 +- core/Cargo.lock | 13 + core/helpers/src/script_dir.rs | 8 +- core/models/src/errors.rs | 2 + core/models/src/id/image.rs | 4 +- core/models/src/version.rs | 44 +- core/startos/Cargo.toml | 1 + core/startos/src/backup/target/mod.rs | 8 +- core/startos/src/context/cli.rs | 6 +- core/startos/src/db/model/public.rs | 7 +- core/startos/src/disk/util.rs | 4 +- core/startos/src/install/mod.rs | 34 +- core/startos/src/lib.rs | 4 +- core/startos/src/logs.rs | 6 +- core/startos/src/lxc/mod.rs | 10 +- core/startos/src/net/static_server.rs | 6 +- core/startos/src/registry/admin.rs | 36 +- core/startos/src/registry/asset.rs | 44 +- core/startos/src/registry/auth.rs | 109 ++-- core/startos/src/registry/context.rs | 54 +- core/startos/src/registry/device_info.rs | 199 ++++++++ core/startos/src/registry/mod.rs | 28 +- core/startos/src/registry/os/asset/add.rs | 253 ++++------ core/startos/src/registry/os/asset/get.rs | 36 +- core/startos/src/registry/os/asset/sign.rs | 76 ++- core/startos/src/registry/os/index.rs | 23 +- core/startos/src/registry/os/version/mod.rs | 23 +- .../startos/src/registry/os/version/signer.rs | 21 +- core/startos/src/registry/package/add.rs | 170 +++++++ core/startos/src/registry/package/get.rs | 387 ++++++++++++++ core/startos/src/registry/package/index.rs | 163 ++++++ core/startos/src/registry/package/mod.rs | 29 ++ core/startos/src/registry/signer.rs | 477 ------------------ .../src/registry/signer/commitment/blake3.rs | 50 ++ .../signer/commitment/merkle_archive.rs | 98 ++++ .../src/registry/signer/commitment/mod.rs | 25 + .../src/registry/signer/commitment/request.rs | 102 ++++ core/startos/src/registry/signer/mod.rs | 154 ++++++ .../src/registry/signer/sign/ed25519.rs | 34 ++ core/startos/src/registry/signer/sign/mod.rs | 348 +++++++++++++ core/startos/src/rpc_continuations.rs | 37 +- core/startos/src/s9pk/{v1 => }/git_hash.rs | 35 +- .../s9pk/merkle_archive/directory_contents.rs | 15 +- .../src/s9pk/merkle_archive/file_contents.rs | 10 +- core/startos/src/s9pk/merkle_archive/hash.rs | 7 +- core/startos/src/s9pk/merkle_archive/mod.rs | 147 ++++-- core/startos/src/s9pk/merkle_archive/sink.rs | 54 +- .../src/s9pk/merkle_archive/source/http.rs | 163 ++++-- .../src/s9pk/merkle_archive/source/mod.rs | 74 ++- .../source/multi_cursor_file.rs | 121 +++-- core/startos/src/s9pk/merkle_archive/test.rs | 16 +- .../src/s9pk/merkle_archive/write_queue.rs | 2 + core/startos/src/s9pk/mod.rs | 1 + core/startos/src/s9pk/rpc.rs | 4 +- core/startos/src/s9pk/v1/manifest.rs | 164 +++++- core/startos/src/s9pk/v1/mod.rs | 1 - core/startos/src/s9pk/v1/reader.rs | 6 +- core/startos/src/s9pk/v2/compat.rs | 81 +-- core/startos/src/s9pk/v2/manifest.rs | 26 +- core/startos/src/s9pk/v2/mod.rs | 16 +- core/startos/src/service/mod.rs | 8 +- .../src/service/service_effect_handler.rs | 15 +- core/startos/src/service/service_map.rs | 2 +- core/startos/src/update/mod.rs | 28 +- core/startos/src/upload.rs | 68 ++- core/startos/src/util/io.rs | 118 ++++- core/startos/src/util/lshw.rs | 7 +- core/startos/src/util/mod.rs | 2 +- core/startos/src/util/rpc.rs | 60 +-- core/startos/src/util/serde.rs | 41 +- core/startos/src/version/mod.rs | 6 +- core/startos/src/volume.rs | 4 +- sdk/lib/osBindings/AcceptSigners.ts | 4 +- sdk/lib/osBindings/AddAdminParams.ts | 3 +- sdk/lib/osBindings/AddAssetParams.ts | 13 +- sdk/lib/osBindings/AddVersionParams.ts | 3 +- .../{SignerKey.ts => AnySignature.ts} | 3 +- sdk/lib/osBindings/AnySigningKey.ts | 3 + sdk/lib/osBindings/AnyVerifyingKey.ts | 3 + sdk/lib/osBindings/Blake3Commitment.ts | 4 + sdk/lib/osBindings/Category.ts | 4 + sdk/lib/osBindings/FullIndex.ts | 10 +- sdk/lib/osBindings/GetOsAssetParams.ts | 3 +- sdk/lib/osBindings/GetPackageParams.ts | 10 + sdk/lib/osBindings/GetPackageResponse.ts | 9 + sdk/lib/osBindings/GetPackageResponseFull.ts | 8 + sdk/lib/osBindings/{Pem.ts => Guid.ts} | 2 +- sdk/lib/osBindings/HardwareRequirements.ts | 2 +- .../osBindings/ListVersionSignersParams.ts | 3 +- sdk/lib/osBindings/Manifest.ts | 6 +- ...ignature.ts => MerkleArchiveCommitment.ts} | 9 +- sdk/lib/osBindings/OsIndex.ts | 3 +- sdk/lib/osBindings/OsVersionInfo.ts | 10 +- sdk/lib/osBindings/PackageDetailLevel.ts | 3 + sdk/lib/osBindings/PackageIndex.ts | 9 + sdk/lib/osBindings/PackageInfo.ts | 9 + sdk/lib/osBindings/PackageInfoShort.ts | 3 + sdk/lib/osBindings/PackageVersionInfo.ts | 25 + sdk/lib/osBindings/RegistryAsset.ts | 9 +- sdk/lib/osBindings/RemoveVersionParams.ts | 3 +- ...1SignatureInfo.ts => RequestCommitment.ts} | 10 +- sdk/lib/osBindings/ServerInfo.ts | 3 +- sdk/lib/osBindings/SetMainStatus.ts | 4 +- sdk/lib/osBindings/SetMainStatusStatus.ts | 3 + sdk/lib/osBindings/SignAssetParams.ts | 7 +- sdk/lib/osBindings/Signature.ts | 4 - sdk/lib/osBindings/SignatureInfo.ts | 7 - sdk/lib/osBindings/SignerInfo.ts | 4 +- sdk/lib/osBindings/Version.ts | 3 + sdk/lib/osBindings/VersionSignerParams.ts | 4 +- sdk/lib/osBindings/index.ts | 24 +- .../show/additional/additional.component.html | 11 - .../ui/src/app/services/api/api.fixures.ts | 3 - 113 files changed, 3265 insertions(+), 1436 deletions(-) create mode 100644 core/startos/src/registry/device_info.rs create mode 100644 core/startos/src/registry/package/add.rs create mode 100644 core/startos/src/registry/package/get.rs create mode 100644 core/startos/src/registry/package/index.rs create mode 100644 core/startos/src/registry/package/mod.rs delete mode 100644 core/startos/src/registry/signer.rs create mode 100644 core/startos/src/registry/signer/commitment/blake3.rs create mode 100644 core/startos/src/registry/signer/commitment/merkle_archive.rs create mode 100644 core/startos/src/registry/signer/commitment/mod.rs create mode 100644 core/startos/src/registry/signer/commitment/request.rs create mode 100644 core/startos/src/registry/signer/mod.rs create mode 100644 core/startos/src/registry/signer/sign/ed25519.rs create mode 100644 core/startos/src/registry/signer/sign/mod.rs rename core/startos/src/s9pk/{v1 => }/git_hash.rs (52%) rename sdk/lib/osBindings/{SignerKey.ts => AnySignature.ts} (55%) create mode 100644 sdk/lib/osBindings/AnySigningKey.ts create mode 100644 sdk/lib/osBindings/AnyVerifyingKey.ts create mode 100644 sdk/lib/osBindings/Blake3Commitment.ts create mode 100644 sdk/lib/osBindings/Category.ts create mode 100644 sdk/lib/osBindings/GetPackageParams.ts create mode 100644 sdk/lib/osBindings/GetPackageResponse.ts create mode 100644 sdk/lib/osBindings/GetPackageResponseFull.ts rename sdk/lib/osBindings/{Pem.ts => Guid.ts} (80%) rename sdk/lib/osBindings/{Blake3Ed25519Signature.ts => MerkleArchiveCommitment.ts} (52%) create mode 100644 sdk/lib/osBindings/PackageDetailLevel.ts create mode 100644 sdk/lib/osBindings/PackageIndex.ts create mode 100644 sdk/lib/osBindings/PackageInfo.ts create mode 100644 sdk/lib/osBindings/PackageInfoShort.ts create mode 100644 sdk/lib/osBindings/PackageVersionInfo.ts rename sdk/lib/osBindings/{Blake3Ed2551SignatureInfo.ts => RequestCommitment.ts} (51%) create mode 100644 sdk/lib/osBindings/SetMainStatusStatus.ts delete mode 100644 sdk/lib/osBindings/Signature.ts delete mode 100644 sdk/lib/osBindings/SignatureInfo.ts create mode 100644 sdk/lib/osBindings/Version.ts diff --git a/Makefile b/Makefile index 1dffd3629..ff5b9c4ad 100644 --- a/Makefile +++ b/Makefile @@ -140,7 +140,6 @@ install: $(ALL_TARGETS) $(call mkdir,$(DESTDIR)/usr/lib/startos/system-images) $(call cp,system-images/compat/docker-images/$(ARCH).tar,$(DESTDIR)/usr/lib/startos/system-images/compat.tar) $(call cp,system-images/utils/docker-images/$(ARCH).tar,$(DESTDIR)/usr/lib/startos/system-images/utils.tar) - $(call cp,system-images/binfmt/docker-images/$(ARCH).tar,$(DESTDIR)/usr/lib/startos/system-images/binfmt.tar) $(call cp,firmware/$(PLATFORM),$(DESTDIR)/usr/lib/startos/firmware) @@ -184,7 +183,7 @@ container-runtime/node_modules: container-runtime/package.json container-runtime npm --prefix container-runtime ci touch container-runtime/node_modules -sdk/lib/osBindings: $(shell core/startos/bindings) +sdk/lib/osBindings: core/startos/bindings mkdir -p sdk/lib/osBindings ls core/startos/bindings/*.ts | sed 's/core\/startos\/bindings\/\([^.]*\)\.ts/export { \1 } from ".\/\1";/g' > core/startos/bindings/index.ts npm --prefix sdk exec -- prettier --config ./sdk/package.json -w ./core/startos/bindings/*.ts diff --git a/core/Cargo.lock b/core/Cargo.lock index 920390d65..4ae02e98e 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -1153,10 +1153,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", + "der_derive", "pem-rfc7468", "zeroize", ] +[[package]] +name = "der_derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fe87ce4529967e0ba1dcf8450bab64d97dfd5010a6256187ffe2e43e6f0e049" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] + [[package]] name = "deranged" version = "0.3.11" @@ -4603,6 +4615,7 @@ dependencies = [ "cookie 0.18.1", "cookie_store", "current_platform", + "der", "digest 0.10.7", "divrem", "ed25519 2.2.3", diff --git a/core/helpers/src/script_dir.rs b/core/helpers/src/script_dir.rs index d90051899..5cedd419f 100644 --- a/core/helpers/src/script_dir.rs +++ b/core/helpers/src/script_dir.rs @@ -1,10 +1,14 @@ use std::path::{Path, PathBuf}; -use models::{PackageId, Version}; +use models::{PackageId, VersionString}; pub const PKG_SCRIPT_DIR: &str = "package-data/scripts"; -pub fn script_dir>(datadir: P, pkg_id: &PackageId, version: &Version) -> PathBuf { +pub fn script_dir>( + datadir: P, + pkg_id: &PackageId, + version: &VersionString, +) -> PathBuf { datadir .as_ref() .join(&*PKG_SCRIPT_DIR) diff --git a/core/models/src/errors.rs b/core/models/src/errors.rs index 340fd15ca..95416ec80 100644 --- a/core/models/src/errors.rs +++ b/core/models/src/errors.rs @@ -89,6 +89,7 @@ pub enum ErrorKind { Timeout = 71, Lxc = 72, Cancelled = 73, + Git = 74, } impl ErrorKind { pub fn as_str(&self) -> &'static str { @@ -167,6 +168,7 @@ impl ErrorKind { Timeout => "Timeout Error", Lxc => "LXC Error", Cancelled => "Cancelled", + Git => "Git Error", } } } diff --git a/core/models/src/id/image.rs b/core/models/src/id/image.rs index bbb0a601e..69a04f880 100644 --- a/core/models/src/id/image.rs +++ b/core/models/src/id/image.rs @@ -5,7 +5,7 @@ use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize}; use ts_rs::TS; -use crate::{Id, InvalidId, PackageId, Version}; +use crate::{Id, InvalidId, PackageId, VersionString}; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, TS)] #[ts(type = "string")] @@ -21,7 +21,7 @@ impl std::fmt::Display for ImageId { } } impl ImageId { - pub fn for_package(&self, pkg_id: &PackageId, pkg_version: Option<&Version>) -> String { + pub fn for_package(&self, pkg_id: &PackageId, pkg_version: Option<&VersionString>) -> String { format!( "start9/{}/{}:{}", pkg_id, diff --git a/core/models/src/version.rs b/core/models/src/version.rs index 012f362aa..48871e3a1 100644 --- a/core/models/src/version.rs +++ b/core/models/src/version.rs @@ -6,12 +6,12 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use ts_rs::TS; #[derive(Debug, Clone, TS)] -#[ts(type = "string")] -pub struct Version { +#[ts(type = "string", rename = "Version")] +pub struct VersionString { version: emver::Version, string: String, } -impl Version { +impl VersionString { pub fn as_str(&self) -> &str { self.string.as_str() } @@ -19,76 +19,76 @@ impl Version { self.version } } -impl std::fmt::Display for Version { +impl std::fmt::Display for VersionString { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.string) } } -impl std::str::FromStr for Version { +impl std::str::FromStr for VersionString { type Err = ::Err; fn from_str(s: &str) -> Result { - Ok(Version { + Ok(VersionString { string: s.to_owned(), version: s.parse()?, }) } } -impl From for Version { +impl From for VersionString { fn from(v: emver::Version) -> Self { - Version { + VersionString { string: v.to_string(), version: v, } } } -impl From for emver::Version { - fn from(v: Version) -> Self { +impl From for emver::Version { + fn from(v: VersionString) -> Self { v.version } } -impl Default for Version { +impl Default for VersionString { fn default() -> Self { Self::from(emver::Version::default()) } } -impl Deref for Version { +impl Deref for VersionString { type Target = emver::Version; fn deref(&self) -> &Self::Target { &self.version } } -impl AsRef for Version { +impl AsRef for VersionString { fn as_ref(&self) -> &emver::Version { &self.version } } -impl AsRef for Version { +impl AsRef for VersionString { fn as_ref(&self) -> &str { self.as_str() } } -impl PartialEq for Version { - fn eq(&self, other: &Version) -> bool { +impl PartialEq for VersionString { + fn eq(&self, other: &VersionString) -> bool { self.version.eq(&other.version) } } -impl Eq for Version {} -impl PartialOrd for Version { +impl Eq for VersionString {} +impl PartialOrd for VersionString { fn partial_cmp(&self, other: &Self) -> Option { self.version.partial_cmp(&other.version) } } -impl Ord for Version { +impl Ord for VersionString { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.version.cmp(&other.version) } } -impl Hash for Version { +impl Hash for VersionString { fn hash(&self, state: &mut H) { self.version.hash(state) } } -impl<'de> Deserialize<'de> for Version { +impl<'de> Deserialize<'de> for VersionString { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, @@ -98,7 +98,7 @@ impl<'de> Deserialize<'de> for Version { Ok(Self { string, version }) } } -impl Serialize for Version { +impl Serialize for VersionString { fn serialize(&self, serializer: S) -> Result where S: Serializer, diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index d88b51f96..bd064a167 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -73,6 +73,7 @@ console-subscriber = { version = "0.2", optional = true } cookie = "0.18.0" cookie_store = "0.20.0" current_platform = "0.2.0" +der = { version = "0.7.9", features = ["derive", "pem"] } digest = "0.10.7" divrem = "1.0.0" ed25519 = { version = "2.2.3", features = ["pkcs8", "pem", "alloc"] } diff --git a/core/startos/src/backup/target/mod.rs b/core/startos/src/backup/target/mod.rs index 4d00a2501..2c7a28d94 100644 --- a/core/startos/src/backup/target/mod.rs +++ b/core/startos/src/backup/target/mod.rs @@ -29,7 +29,7 @@ use crate::util::clap::FromStrParser; use crate::util::serde::{ deserialize_from_str, display_serializable, serialize_display, HandlerExtSerde, WithIoFormat, }; -use crate::util::Version; +use crate::util::VersionString; pub mod cifs; @@ -194,7 +194,7 @@ pub async fn list(ctx: RpcContext) -> Result>, pub package_backups: BTreeMap, } @@ -203,8 +203,8 @@ pub struct BackupInfo { #[serde(rename_all = "camelCase")] pub struct PackageBackupInfo { pub title: String, - pub version: Version, - pub os_version: Version, + pub version: VersionString, + pub os_version: VersionString, pub timestamp: DateTime, } diff --git a/core/startos/src/context/cli.rs b/core/startos/src/context/cli.rs index 166ab3fd7..014457b67 100644 --- a/core/startos/src/context/cli.rs +++ b/core/startos/src/context/cli.rs @@ -21,7 +21,7 @@ use crate::context::config::{local_config_path, ClientConfig}; use crate::context::{DiagnosticContext, InstallContext, RpcContext, SetupContext}; use crate::middleware::auth::LOCAL_AUTH_COOKIE_PATH; use crate::prelude::*; -use crate::rpc_continuations::RequestGuid; +use crate::rpc_continuations::Guid; #[derive(Debug)] pub struct CliContextSeed { @@ -164,7 +164,7 @@ impl CliContext { pub async fn ws_continuation( &self, - guid: RequestGuid, + guid: Guid, ) -> Result>, Error> { let mut url = self.base_url.clone(); let ws_scheme = match url.scheme() { @@ -194,7 +194,7 @@ impl CliContext { pub async fn rest_continuation( &self, - guid: RequestGuid, + guid: Guid, body: reqwest::Body, headers: reqwest::header::HeaderMap, ) -> Result { diff --git a/core/startos/src/db/model/public.rs b/core/startos/src/db/model/public.rs index fe85056f9..e5257f2a4 100644 --- a/core/startos/src/db/model/public.rs +++ b/core/startos/src/db/model/public.rs @@ -21,7 +21,7 @@ use crate::net::utils::{get_iface_ipv4_addr, get_iface_ipv6_addr}; use crate::prelude::*; use crate::progress::FullProgress; use crate::util::cpupower::Governor; -use crate::util::Version; +use crate::util::VersionString; use crate::version::{Current, VersionT}; use crate::{ARCH, PLATFORM}; @@ -109,8 +109,7 @@ pub struct ServerInfo { pub platform: InternedString, pub id: String, pub hostname: String, - #[ts(type = "string")] - pub version: Version, + pub version: VersionString, #[ts(type = "string | null")] pub last_backup: Option>, #[ts(type = "string")] @@ -136,7 +135,7 @@ pub struct ServerInfo { #[serde(default)] pub zram: bool, pub governor: Option, - pub smtp: Option + pub smtp: Option, } #[derive(Debug, Deserialize, Serialize, HasModel, TS)] diff --git a/core/startos/src/disk/util.rs b/core/startos/src/disk/util.rs index b0bc00a5d..a98c52418 100644 --- a/core/startos/src/disk/util.rs +++ b/core/startos/src/disk/util.rs @@ -20,7 +20,7 @@ use super::mount::guard::TmpMountGuard; use crate::disk::mount::guard::GenericMountGuard; use crate::disk::OsPartitionInfo; use crate::util::serde::IoFormat; -use crate::util::{Invoke, Version}; +use crate::util::{Invoke, VersionString}; use crate::{Error, ResultExt as _}; #[derive(Clone, Copy, Debug, Deserialize, Serialize)] @@ -56,7 +56,7 @@ pub struct PartitionInfo { #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct EmbassyOsRecoveryInfo { - pub version: Version, + pub version: VersionString, pub full: bool, pub password_hash: Option, pub wrapped_key: Option, diff --git a/core/startos/src/install/mod.rs b/core/startos/src/install/mod.rs index f5cff36d9..c4e4452b7 100644 --- a/core/startos/src/install/mod.rs +++ b/core/startos/src/install/mod.rs @@ -1,4 +1,5 @@ use std::path::PathBuf; +use std::sync::Arc; use std::time::Duration; use clap::builder::ValueParserFactory; @@ -22,7 +23,7 @@ use crate::context::{CliContext, RpcContext}; use crate::db::model::package::{ManifestPreference, PackageState, PackageStateMatchModelRef}; use crate::prelude::*; use crate::progress::{FullProgress, PhasedProgressBar}; -use crate::rpc_continuations::{RequestGuid, RpcContinuation}; +use crate::rpc_continuations::{Guid, RpcContinuation}; use crate::s9pk::manifest::PackageId; use crate::s9pk::merkle_archive::source::http::HttpSource; use crate::s9pk::S9pk; @@ -139,15 +140,18 @@ pub async fn install( let registry = registry.unwrap_or_else(|| crate::DEFAULT_MARKETPLACE.parse().unwrap()); let version_priority = version_priority.unwrap_or_default(); let s9pk = S9pk::deserialize( - &HttpSource::new( - ctx.client.clone(), - format!( - "{}/package/v0/{}.s9pk?spec={}&version-priority={}", - registry, id, version, version_priority, + &Arc::new( + HttpSource::new( + ctx.client.clone(), + format!( + "{}/package/v0/{}.s9pk?spec={}&version-priority={}", + registry, id, version, version_priority, + ) + .parse()?, ) - .parse()?, - ) - .await?, + .await?, + ), + None, // TODO true, ) .await?; @@ -170,8 +174,8 @@ pub async fn install( #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SideloadResponse { - pub upload: RequestGuid, - pub progress: RequestGuid, + pub upload: Guid, + pub progress: Guid, } #[instrument(skip_all)] @@ -179,7 +183,7 @@ pub async fn sideload(ctx: RpcContext) -> Result { let (upload, file) = upload(&ctx).await?; let (id_send, id_recv) = oneshot::channel(); let (err_send, err_recv) = oneshot::channel(); - let progress = RequestGuid::new(); + let progress = Guid::new(); let db = ctx.db.clone(); let mut sub = db .subscribe( @@ -256,7 +260,11 @@ pub async fn sideload(ctx: RpcContext) -> Result { .await; tokio::spawn(async move { if let Err(e) = async { - let s9pk = S9pk::deserialize(&file, true).await?; + let s9pk = S9pk::deserialize( + &file, None, // TODO + true, + ) + .await?; let _ = id_send.send(s9pk.as_manifest().id.clone()); ctx.services .install(ctx.clone(), s9pk, None::) diff --git a/core/startos/src/lib.rs b/core/startos/src/lib.rs index 3152fcab3..d60a2db24 100644 --- a/core/startos/src/lib.rs +++ b/core/startos/src/lib.rs @@ -1,6 +1,8 @@ pub const DEFAULT_MARKETPLACE: &str = "https://registry.start9.com"; // pub const COMMUNITY_MARKETPLACE: &str = "https://community-registry.start9.com"; -pub const BUFFER_SIZE: usize = 1024; +pub const CAP_1_KiB: usize = 1024; +pub const CAP_1_MiB: usize = CAP_1_KiB * CAP_1_KiB; +pub const CAP_10_MiB: usize = 10 * CAP_1_MiB; pub const HOST_IP: [u8; 4] = [172, 18, 0, 1]; pub const TARGET: &str = current_platform::CURRENT_PLATFORM; lazy_static::lazy_static! { diff --git a/core/startos/src/logs.rs b/core/startos/src/logs.rs index 340b04b8b..bb1198451 100644 --- a/core/startos/src/logs.rs +++ b/core/startos/src/logs.rs @@ -26,7 +26,7 @@ use crate::context::{CliContext, RpcContext}; use crate::error::ResultExt; use crate::lxc::ContainerId; use crate::prelude::*; -use crate::rpc_continuations::{RequestGuid, RpcContinuation, RpcContinuations}; +use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations}; use crate::util::serde::Reversible; use crate::util::Invoke; @@ -118,7 +118,7 @@ pub struct LogResponse { #[serde(rename_all = "camelCase")] pub struct LogFollowResponse { start_cursor: Option, - guid: RequestGuid, + guid: Guid, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] @@ -581,7 +581,7 @@ pub async fn follow_logs>( first_entry = Some(entry); } - let guid = RequestGuid::new(); + let guid = Guid::new(); ctx.as_ref() .add( guid.clone(), diff --git a/core/startos/src/lxc/mod.rs b/core/startos/src/lxc/mod.rs index 084364c4a..f64ecebe7 100644 --- a/core/startos/src/lxc/mod.rs +++ b/core/startos/src/lxc/mod.rs @@ -33,7 +33,7 @@ use crate::disk::mount::filesystem::{MountType, ReadWrite}; use crate::disk::mount::guard::{GenericMountGuard, MountGuard, TmpMountGuard}; use crate::disk::mount::util::unmount; use crate::prelude::*; -use crate::rpc_continuations::{RequestGuid, RpcContinuation}; +use crate::rpc_continuations::{Guid, RpcContinuation}; use crate::util::clap::FromStrParser; use crate::util::rpc_client::UnixRpcClient; use crate::util::{new_guid, Invoke}; @@ -433,7 +433,7 @@ pub struct ConnectParams { pub async fn connect_rpc( ctx: RpcContext, ConnectParams { guid }: ConnectParams, -) -> Result { +) -> Result { connect( &ctx, ctx.dev.lxc.lock().await.get(&guid).ok_or_else(|| { @@ -443,11 +443,11 @@ pub async fn connect_rpc( .await } -pub async fn connect(ctx: &RpcContext, container: &LxcContainer) -> Result { +pub async fn connect(ctx: &RpcContext, container: &LxcContainer) -> Result { use axum::extract::ws::Message; let rpc = container.connect_rpc(Some(Duration::from_secs(30))).await?; - let guid = RequestGuid::new(); + let guid = Guid::new(); ctx.rpc_continuations .add( guid.clone(), @@ -504,7 +504,7 @@ pub async fn connect(ctx: &RpcContext, container: &LxcContainer) -> Result Result<(), Error> { +pub async fn connect_cli(ctx: &CliContext, guid: Guid) -> Result<(), Error> { use futures::SinkExt; use tokio_tungstenite::tungstenite::Message; diff --git a/core/startos/src/net/static_server.rs b/core/startos/src/net/static_server.rs index 85737598b..fff1731ce 100644 --- a/core/startos/src/net/static_server.rs +++ b/core/startos/src/net/static_server.rs @@ -30,7 +30,7 @@ use crate::middleware::auth::{Auth, HasValidSession}; use crate::middleware::cors::Cors; use crate::middleware::db::SyncDb; use crate::middleware::diagnostic::DiagnosticMode; -use crate::rpc_continuations::RequestGuid; +use crate::rpc_continuations::Guid; use crate::{diagnostic_api, install_api, main_api, setup_api, Error, ErrorKind, ResultExt}; const NOT_FOUND: &[u8] = b"Not Found"; @@ -136,7 +136,7 @@ pub fn main_ui_server_router(ctx: RpcContext) -> Router { let ctx = ctx.clone(); move |x::Path(path): x::Path, ws: axum::extract::ws::WebSocketUpgrade| async move { - match RequestGuid::from(&path) { + match Guid::from(&path) { None => { tracing::debug!("No Guid Path"); bad_request() @@ -159,7 +159,7 @@ pub fn main_ui_server_router(ctx: RpcContext) -> Router { .path() .strip_prefix("/rest/rpc/") .unwrap_or_default(); - match RequestGuid::from(&path) { + match Guid::from(&path) { None => { tracing::debug!("No Guid Path"); bad_request() diff --git a/core/startos/src/registry/admin.rs b/core/startos/src/registry/admin.rs index b09fa05aa..cd795e5cd 100644 --- a/core/startos/src/registry/admin.rs +++ b/core/startos/src/registry/admin.rs @@ -10,10 +10,11 @@ use ts_rs::TS; use crate::context::CliContext; use crate::prelude::*; use crate::registry::context::RegistryContext; -use crate::registry::signer::{ContactInfo, SignerInfo, SignerKey}; +use crate::registry::signer::sign::AnyVerifyingKey; +use crate::registry::signer::{ContactInfo, SignerInfo}; use crate::registry::RegistryDatabase; -use crate::rpc_continuations::RequestGuid; -use crate::util::serde::{display_serializable, HandlerExtSerde, Pem, WithIoFormat}; +use crate::rpc_continuations::Guid; +use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat}; pub fn admin_api() -> ParentHandler { ParentHandler::new() @@ -48,8 +49,8 @@ fn signers_api() -> ParentHandler { .subcommand("add", from_fn_async(cli_add_signer).no_display()) } -impl Model> { - pub fn get_signer(&self, key: &SignerKey) -> Result { +impl Model> { + pub fn get_signer(&self, key: &AnyVerifyingKey) -> Result { self.as_entries()? .into_iter() .map(|(guid, s)| Ok::<_, Error>((guid, s.as_keys().de()?))) @@ -60,7 +61,7 @@ impl Model> { .ok_or_else(|| Error::new(eyre!("unknown signer"), ErrorKind::Authorization)) } - pub fn get_signer_info(&self, key: &SignerKey) -> Result<(RequestGuid, SignerInfo), Error> { + pub fn get_signer_info(&self, key: &AnyVerifyingKey) -> Result<(Guid, SignerInfo), Error> { self.as_entries()? .into_iter() .map(|(guid, s)| Ok::<_, Error>((guid, s.de()?))) @@ -88,17 +89,15 @@ impl Model> { ErrorKind::InvalidRequest, )); } - self.insert(&RequestGuid::new(), signer) + self.insert(&Guid::new(), signer) } } -pub async fn list_signers( - ctx: RegistryContext, -) -> Result, Error> { +pub async fn list_signers(ctx: RegistryContext) -> Result, Error> { ctx.db.peek().await.into_index().into_signers().de() } -pub fn display_signers(params: WithIoFormat, signers: BTreeMap) { +pub fn display_signers(params: WithIoFormat, signers: BTreeMap) { use prettytable::*; if let Some(format) = params.format { @@ -137,8 +136,8 @@ pub struct CliAddSignerParams { pub name: String, #[arg(long = "contact", short = 'c')] pub contact: Vec, - #[arg(long = "ed25519-key")] - pub ed25519_keys: Vec>, + #[arg(long = "key")] + pub keys: Vec, pub database: Option, } @@ -151,7 +150,7 @@ pub async fn cli_add_signer( CliAddSignerParams { name, contact, - ed25519_keys, + keys, database, }, .. @@ -160,7 +159,7 @@ pub async fn cli_add_signer( let signer = SignerInfo { name, contact, - keys: ed25519_keys.into_iter().map(SignerKey::Ed25519).collect(), + keys: keys.into_iter().collect(), }; if let Some(database) = database { TypedPatchDb::::load(PatchDb::open(database).await?) @@ -181,8 +180,7 @@ pub async fn cli_add_signer( #[serde(rename_all = "camelCase")] #[ts(export)] pub struct AddAdminParams { - #[ts(type = "string")] - pub signer: RequestGuid, + pub signer: Guid, } pub async fn add_admin( @@ -206,7 +204,7 @@ pub async fn add_admin( #[command(rename_all = "kebab-case")] #[serde(rename_all = "camelCase")] pub struct CliAddAdminParams { - pub signer: RequestGuid, + pub signer: Guid, pub database: Option, } @@ -242,7 +240,7 @@ pub async fn cli_add_admin( Ok(()) } -pub async fn list_admins(ctx: RegistryContext) -> Result, Error> { +pub async fn list_admins(ctx: RegistryContext) -> Result, Error> { let db = ctx.db.peek().await; let admins = db.as_admins().de()?; Ok(db diff --git a/core/startos/src/registry/asset.rs b/core/startos/src/registry/asset.rs index c9acced45..ea37b2309 100644 --- a/core/startos/src/registry/asset.rs +++ b/core/startos/src/registry/asset.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use reqwest::Client; use serde::{Deserialize, Serialize}; use tokio::io::AsyncWrite; @@ -5,32 +7,48 @@ use ts_rs::TS; use url::Url; use crate::prelude::*; -use crate::registry::signer::{AcceptSigners, FileValidator, SignatureInfo}; +use crate::registry::signer::commitment::{Commitment, Digestable}; +use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey}; +use crate::registry::signer::AcceptSigners; +use crate::s9pk::merkle_archive::source::http::HttpSource; -#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[derive(Debug, Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] -#[model = "Model"] #[ts(export)] -pub struct RegistryAsset { +pub struct RegistryAsset { #[ts(type = "string")] pub url: Url, - pub signature_info: SignatureInfo, + pub commitment: Commitment, + pub signatures: HashMap, } -impl AsRef for RegistryAsset { - fn as_ref(&self) -> &RegistryAsset { - self +impl RegistryAsset { + pub fn all_signers(&self) -> AcceptSigners { + AcceptSigners::All( + self.signatures + .keys() + .cloned() + .map(AcceptSigners::Signer) + .collect(), + ) } } -impl RegistryAsset { - pub fn validate(&self, accept: AcceptSigners) -> Result { - self.signature_info.validate(accept) +impl RegistryAsset { + pub fn validate(&self, context: &str, mut accept: AcceptSigners) -> Result<&Commitment, Error> { + for (signer, signature) in &self.signatures { + accept.process_signature(signer, &self.commitment, context, signature)?; + } + accept.try_accept()?; + Ok(&self.commitment) } +} +impl Commitment<&'a HttpSource>> RegistryAsset { pub async fn download( &self, client: Client, dst: &mut (impl AsyncWrite + Unpin + Send + ?Sized), - validator: &FileValidator, ) -> Result<(), Error> { - validator.download(self.url.clone(), client, dst).await + self.commitment + .copy_to(&HttpSource::new(client, self.url.clone()).await?, dst) + .await } } diff --git a/core/startos/src/registry/auth.rs b/core/startos/src/registry/auth.rs index 187750870..741090b4d 100644 --- a/core/startos/src/registry/auth.rs +++ b/core/startos/src/registry/auth.rs @@ -6,19 +6,23 @@ use axum::body::Body; use axum::extract::Request; use axum::response::Response; use chrono::Utc; -use http_body_util::BodyExt; +use http::HeaderValue; use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::{Middleware, RpcRequest, RpcResponse}; use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha512}; use tokio::io::AsyncWriteExt; use tokio::sync::Mutex; use ts_rs::TS; +use url::Url; use crate::prelude::*; use crate::registry::context::RegistryContext; -use crate::registry::signer::SignerKey; -use crate::util::serde::{Base64, Pem}; +use crate::registry::signer::commitment::request::RequestCommitment; +use crate::registry::signer::commitment::Commitment; +use crate::registry::signer::sign::{ + AnySignature, AnySigningKey, AnyVerifyingKey, SignatureScheme, +}; +use crate::util::serde::Base64; pub const AUTH_SIG_HEADER: &str = "X-StartOS-Registry-Auth-Sig"; @@ -34,7 +38,7 @@ pub struct Metadata { #[derive(Clone)] pub struct Auth { nonce_cache: Arc>>, // for replay protection - signer: Option>, + signer: Option>, } impl Auth { pub fn new() -> Self { @@ -68,41 +72,57 @@ pub struct RegistryAdminLogRecord { pub name: String, #[ts(type = "{ id: string | number | null; method: string; params: any }")] pub request: RpcRequest, - pub key: SignerKey, + pub key: AnyVerifyingKey, } #[derive(Serialize, Deserialize)] pub struct SignatureHeader { - pub timestamp: i64, - pub nonce: u64, #[serde(flatten)] - pub signer: SignerKey, - pub signature: Base64<[u8; 64]>, + pub commitment: RequestCommitment, + pub signer: AnyVerifyingKey, + pub signature: AnySignature, } impl SignatureHeader { - pub fn sign_ed25519( - key: &ed25519_dalek::SigningKey, - body: &[u8], - context: &str, - ) -> Result { + pub fn to_header(&self) -> HeaderValue { + let mut url: Url = "http://localhost".parse().unwrap(); + self.commitment.append_query(&mut url); + url.query_pairs_mut() + .append_pair("signer", &self.signer.to_string()); + url.query_pairs_mut() + .append_pair("signature", &self.signature.to_string()); + HeaderValue::from_str(url.query().unwrap_or_default()).unwrap() + } + pub fn from_header(header: &HeaderValue) -> Result { + let url: Url = format!( + "http://localhost/?{}", + header.to_str().with_kind(ErrorKind::Utf8)? + ) + .parse()?; + let query: BTreeMap<_, _> = url.query_pairs().collect(); + Ok(Self { + commitment: RequestCommitment::from_query(&url)?, + signer: query.get("signer").or_not_found("signer")?.parse()?, + signature: query.get("signature").or_not_found("signature")?.parse()?, + }) + } + pub fn sign(signer: &AnySigningKey, body: &[u8], context: &str) -> Result { let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_secs() as i64) .unwrap_or_else(|e| e.duration().as_secs() as i64 * -1); let nonce = rand::random(); - let signer = SignerKey::Ed25519(Pem(key.verifying_key())); - let mut hasher = Sha512::new(); - hasher.update(&i64::to_be_bytes(timestamp)); - hasher.update(&u64::to_be_bytes(nonce)); - hasher.update(body); - let signature = Base64( - key.sign_prehashed(hasher, Some(context.as_bytes()))? - .to_bytes(), - ); - Ok(Self { + let commitment = RequestCommitment { timestamp, nonce, - signer, + size: body.len() as u64, + blake3: Base64(*blake3::hash(body).as_bytes()), + }; + let signature = signer + .scheme() + .sign_commitment(&signer, &commitment, context)?; + Ok(Self { + commitment, + signer: signer.verifying_key(), signature, }) } @@ -120,43 +140,40 @@ impl Middleware for Auth { async { let request = request; let SignatureHeader { - timestamp, - nonce, + commitment, signer, signature, - } = serde_urlencoded::from_str( + } = SignatureHeader::from_header( request .headers() .get(AUTH_SIG_HEADER) .or_not_found("missing X-StartOS-Registry-Auth-Sig") - .with_kind(ErrorKind::InvalidRequest)? - .to_str() - .with_kind(ErrorKind::Utf8)?, - ) - .with_kind(ErrorKind::Deserialization)?; + .with_kind(ErrorKind::InvalidRequest)?, + )?; + + signer.scheme().verify_commitment( + &signer, + &commitment, + &ctx.hostname, + &signature, + )?; + let now = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_secs() as i64) .unwrap_or_else(|e| e.duration().as_secs() as i64 * -1); - if (now - timestamp).abs() > 30 { + if (now - commitment.timestamp).abs() > 30 { return Err(Error::new( eyre!("timestamp not within 30s of now"), ErrorKind::InvalidSignature, )); } - self.handle_nonce(nonce).await?; - let body = std::mem::replace(request.body_mut(), Body::empty()) - .collect() - .await - .with_kind(ErrorKind::Network)? - .to_bytes(); - let mut verifier = signer.verifier(); - verifier.update(&i64::to_be_bytes(timestamp)); - verifier.update(&u64::to_be_bytes(nonce)); - verifier.update(&body); + self.handle_nonce(commitment.nonce).await?; + + let mut body = Vec::with_capacity(commitment.size as usize); + commitment.copy_to(request, &mut body).await?; *request.body_mut() = Body::from(body); - verifier.verify(&*signature, &ctx.hostname)?; Ok(signer) } .await diff --git a/core/startos/src/registry/context.rs b/core/startos/src/registry/context.rs index dac833434..99d60307b 100644 --- a/core/startos/src/registry/context.rs +++ b/core/startos/src/registry/context.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use clap::Parser; use imbl_value::InternedString; use patch_db::PatchDb; +use reqwest::{Client, Proxy}; use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::{CallRemote, Context, Empty}; use serde::{Deserialize, Serialize}; @@ -17,9 +18,10 @@ use crate::context::config::{ContextConfig, CONFIG_PATH}; use crate::context::{CliContext, RpcContext}; use crate::prelude::*; use crate::registry::auth::{SignatureHeader, AUTH_SIG_HEADER}; +use crate::registry::device_info::{DeviceInfo, DEVICE_INFO_HEADER}; +use crate::registry::signer::sign::AnySigningKey; use crate::registry::RegistryDatabase; use crate::rpc_continuations::RpcContinuations; -use crate::version::VersionT; #[derive(Debug, Clone, Default, Deserialize, Serialize, Parser)] #[serde(rename_all = "kebab-case")] @@ -31,6 +33,8 @@ pub struct RegistryConfig { pub listen: Option, #[arg(short = 'h', long = "hostname")] pub hostname: InternedString, + #[arg(short = 'p', long = "proxy")] + pub tor_proxy: Option, #[arg(short = 'd', long = "datadir")] pub datadir: Option, } @@ -58,6 +62,7 @@ pub struct RegistryContextSeed { pub db: TypedPatchDb, pub datadir: PathBuf, pub rpc_continuations: RpcContinuations, + pub client: Client, pub shutdown: Sender<()>, } @@ -81,6 +86,11 @@ impl RegistryContext { || async { Ok(Default::default()) }, ) .await?; + let tor_proxy_url = config + .tor_proxy + .clone() + .map(Ok) + .unwrap_or_else(|| "socks5h://localhost:9050".parse())?; Ok(Self(Arc::new(RegistryContextSeed { hostname: config.hostname.clone(), listen: config @@ -89,6 +99,16 @@ impl RegistryContext { db, datadir, rpc_continuations: RpcContinuations::new(), + client: Client::builder() + .proxy(Proxy::custom(move |url| { + if url.host_str().map_or(false, |h| h.ends_with(".onion")) { + Some(tor_proxy_url.clone()) + } else { + None + } + })) + .build() + .with_kind(crate::ErrorKind::ParseUrl)?, shutdown, }))) } @@ -145,12 +165,11 @@ impl CallRemote for CliContext { .header(CONTENT_LENGTH, body.len()) .header( AUTH_SIG_HEADER, - serde_urlencoded::to_string(&SignatureHeader::sign_ed25519( - self.developer_key()?, + SignatureHeader::sign( + &AnySigningKey::Ed25519(self.developer_key()?.clone()), &body, &host, - )?) - .with_kind(ErrorKind::Serialization)?, + )?.to_header(), ) .body(body) .send() @@ -171,29 +190,6 @@ impl CallRemote for CliContext { } } -fn hardware_header(ctx: &RpcContext) -> String { - let mut url: Url = "http://localhost".parse().unwrap(); - url.query_pairs_mut() - .append_pair( - "os.version", - &crate::version::Current::new().semver().to_string(), - ) - .append_pair( - "os.compat", - &crate::version::Current::new().compat().to_string(), - ) - .append_pair("os.arch", &*crate::PLATFORM) - .append_pair("hardware.arch", &*crate::ARCH) - .append_pair("hardware.ram", &ctx.hardware.ram.to_string()); - - for hw in &ctx.hardware.devices { - url.query_pairs_mut() - .append_pair(&format!("hardware.device.{}", hw.class()), hw.product()); - } - - url.query().unwrap_or_default().to_string() -} - impl CallRemote for RpcContext { async fn call_remote( &self, @@ -221,7 +217,7 @@ impl CallRemote for RpcContext { .header(CONTENT_TYPE, "application/json") .header(ACCEPT, "application/json") .header(CONTENT_LENGTH, body.len()) - .header("X-StartOS-Hardware", &hardware_header(self)) + .header(DEVICE_INFO_HEADER, DeviceInfo::from(self).to_header_value()) .body(body) .send() .await?; diff --git a/core/startos/src/registry/device_info.rs b/core/startos/src/registry/device_info.rs new file mode 100644 index 000000000..7da5bd8b9 --- /dev/null +++ b/core/startos/src/registry/device_info.rs @@ -0,0 +1,199 @@ +use std::collections::BTreeMap; +use std::convert::identity; +use std::ops::Deref; + +use axum::extract::Request; +use axum::response::Response; +use emver::{Version, VersionRange}; +use http::HeaderValue; +use imbl_value::InternedString; +use rpc_toolkit::{Middleware, RpcRequest, RpcResponse}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; +use url::Url; + +use crate::context::RpcContext; +use crate::prelude::*; +use crate::registry::context::RegistryContext; +use crate::util::VersionString; +use crate::version::VersionT; + +pub const DEVICE_INFO_HEADER: &str = "X-StartOS-Device-Info"; + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct DeviceInfo { + pub os: OsInfo, + pub hardware: HardwareInfo, +} +impl From<&RpcContext> for DeviceInfo { + fn from(value: &RpcContext) -> Self { + Self { + os: OsInfo::from(value), + hardware: HardwareInfo::from(value), + } + } +} +impl DeviceInfo { + pub fn to_header_value(&self) -> HeaderValue { + let mut url: Url = "http://localhost".parse().unwrap(); + 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 (class, products) in &self.hardware.devices { + for product in products { + url.query_pairs_mut() + .append_pair(&format!("hardware.device.{}", class), product); + } + } + + HeaderValue::from_str(url.query().unwrap_or_default()).unwrap() + } + pub fn from_header_value(header: &HeaderValue) -> Result { + let url: Url = format!( + "http://localhost/?{}", + header.to_str().with_kind(ErrorKind::ParseUrl)? + ) + .parse()?; + let query: BTreeMap<_, _> = url.query_pairs().collect(); + Ok(Self { + os: OsInfo { + version: query + .get("os.version") + .or_not_found("os.version")? + .parse()?, + compat: query.get("os.compat").or_not_found("os.compat")?.parse()?, + platform: query + .get("os.platform") + .or_not_found("os.platform")? + .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)| { + k.strip_prefix("hardware.device.") + .map(|k| (k.into(), v.into_owned())) + }) + .fold(BTreeMap::new(), |mut acc, (k, v)| { + let mut devs = acc.remove(&k).unwrap_or_default(); + devs.push(v); + acc.insert(k, devs); + acc + }), + }, + }) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct OsInfo { + #[ts(as = "VersionString")] + pub version: Version, + #[ts(type = "string")] + pub compat: VersionRange, + #[ts(type = "string")] + pub platform: InternedString, +} +impl From<&RpcContext> for OsInfo { + fn from(_: &RpcContext) -> Self { + Self { + version: crate::version::Current::new().semver(), + compat: crate::version::Current::new().compat().clone(), + platform: InternedString::intern(&*crate::PLATFORM), + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct HardwareInfo { + #[ts(type = "string")] + pub arch: InternedString, + #[ts(type = "number")] + pub ram: u64, + #[ts(as = "BTreeMap::>")] + pub devices: BTreeMap>, +} + +impl From<&RpcContext> for HardwareInfo { + fn from(value: &RpcContext) -> Self { + Self { + arch: InternedString::intern(&**crate::ARCH), + ram: value.hardware.ram, + devices: value + .hardware + .devices + .iter() + .fold(BTreeMap::new(), |mut acc, dev| { + let mut devs = acc.remove(dev.class()).unwrap_or_default(); + devs.push(dev.product().to_owned()); + acc.insert(dev.class().into(), devs); + acc + }), + } + } +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Metadata { + #[serde(default)] + get_device_info: bool, +} + +#[derive(Clone)] +pub struct DeviceInfoMiddleware { + device_info: Option, +} +impl DeviceInfoMiddleware { + pub fn new() -> Self { + Self { device_info: None } + } +} + +impl Middleware for DeviceInfoMiddleware { + type Metadata = Metadata; + async fn process_http_request( + &mut self, + _: &RegistryContext, + request: &mut Request, + ) -> Result<(), Response> { + self.device_info = request.headers_mut().remove(DEVICE_INFO_HEADER); + Ok(()) + } + async fn process_rpc_request( + &mut self, + _: &RegistryContext, + metadata: Self::Metadata, + request: &mut RpcRequest, + ) -> Result<(), RpcResponse> { + async move { + if metadata.get_device_info { + if let Some(device_info) = &self.device_info { + request.params["__device_info"] = + to_value(&DeviceInfo::from_header_value(device_info)?)?; + } + } + + Ok::<_, Error>(()) + } + .await + .map_err(|e| RpcResponse::from_result(Err(e))) + } +} diff --git a/core/startos/src/registry/mod.rs b/core/startos/src/registry/mod.rs index c063e5823..656edf337 100644 --- a/core/startos/src/registry/mod.rs +++ b/core/startos/src/registry/mod.rs @@ -3,20 +3,23 @@ use std::net::SocketAddr; use axum::Router; use futures::future::ready; +use models::DataUrl; use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler, Server}; use serde::{Deserialize, Serialize}; use ts_rs::TS; -use crate::context::{CliContext}; +use crate::context::CliContext; use crate::middleware::cors::Cors; use crate::net::static_server::{bad_request, not_found, server_error}; use crate::net::web_server::WebServer; use crate::prelude::*; use crate::registry::auth::Auth; -use crate::registry::context::{RegistryContext}; +use crate::registry::context::RegistryContext; +use crate::registry::device_info::DeviceInfoMiddleware; use crate::registry::os::index::OsIndex; +use crate::registry::package::index::PackageIndex; use crate::registry::signer::SignerInfo; -use crate::rpc_continuations::RequestGuid; +use crate::rpc_continuations::Guid; use crate::util::serde::HandlerExtSerde; pub mod admin; @@ -24,26 +27,29 @@ pub mod asset; pub mod auth; pub mod context; pub mod db; +pub mod device_info; pub mod os; +pub mod package; pub mod signer; #[derive(Debug, Default, Deserialize, Serialize, HasModel)] #[serde(rename_all = "camelCase")] #[model = "Model"] pub struct RegistryDatabase { - pub admins: BTreeSet, + pub admins: BTreeSet, pub index: FullIndex, } +impl RegistryDatabase {} #[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] #[serde(rename_all = "camelCase")] #[model = "Model"] #[ts(export)] pub struct FullIndex { - // pub package: PackageIndex, + pub icon: Option>, + pub package: PackageIndex, pub os: OsIndex, - #[ts(as = "BTreeMap::")] - pub signers: BTreeMap, + pub signers: BTreeMap, } pub async fn get_full_index(ctx: RegistryContext) -> Result { @@ -59,6 +65,7 @@ pub fn registry_api() -> ParentHandler { .with_call_remote::(), ) .subcommand("os", os::os_api::()) + .subcommand("package", package::package_api::()) .subcommand("admin", admin::admin_api::()) .subcommand("db", db::db_api::()) } @@ -72,7 +79,8 @@ pub fn registry_server_router(ctx: RegistryContext) -> Router { post( Server::new(move || ready(Ok(ctx.clone())), registry_api()) .middleware(Cors::new()) - .middleware(Auth::new()), + .middleware(Auth::new()) + .middleware(DeviceInfoMiddleware::new()), ) }) .route( @@ -81,7 +89,7 @@ pub fn registry_server_router(ctx: RegistryContext) -> Router { let ctx = ctx.clone(); move |x::Path(path): x::Path, ws: axum::extract::ws::WebSocketUpgrade| async move { - match RequestGuid::from(&path) { + match Guid::from(&path) { None => { tracing::debug!("No Guid Path"); bad_request() @@ -104,7 +112,7 @@ pub fn registry_server_router(ctx: RegistryContext) -> Router { .path() .strip_prefix("/rest/rpc/") .unwrap_or_default(); - match RequestGuid::from(&path) { + match Guid::from(&path) { None => { tracing::debug!("No Guid Path"); bad_request() diff --git a/core/startos/src/registry/os/asset/add.rs b/core/startos/src/registry/os/asset/add.rs index d2c20e711..6ca495547 100644 --- a/core/startos/src/registry/os/asset/add.rs +++ b/core/startos/src/registry/os/asset/add.rs @@ -1,17 +1,13 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; use std::panic::UnwindSafe; use std::path::PathBuf; -use std::time::Duration; -use axum::response::Response; use clap::Parser; -use futures::{FutureExt, TryStreamExt}; use helpers::NonDetachingJoinHandle; use imbl_value::InternedString; use itertools::Itertools; use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha512}; use ts_rs::TS; use url::Url; @@ -22,10 +18,15 @@ use crate::registry::asset::RegistryAsset; use crate::registry::context::RegistryContext; use crate::registry::os::index::OsVersionInfo; use crate::registry::os::SIG_CONTEXT; -use crate::registry::signer::{Blake3Ed25519Signature, Signature, SignatureInfo, SignerKey}; -use crate::rpc_continuations::{RequestGuid, RpcContinuation}; +use crate::registry::signer::commitment::blake3::Blake3Commitment; +use crate::registry::signer::sign::ed25519::Ed25519; +use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey, SignatureScheme}; +use crate::s9pk::merkle_archive::hash::VerifyingWriter; +use crate::s9pk::merkle_archive::source::http::HttpSource; +use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::ArchiveSource; -use crate::util::{Apply, Version}; +use crate::util::serde::Base64; +use crate::util::VersionString; pub fn add_api() -> ParentHandler { ParentHandler::new() @@ -53,39 +54,37 @@ pub fn add_api() -> ParentHandler { #[serde(rename_all = "camelCase")] #[ts(export)] pub struct AddAssetParams { - #[ts(type = "string")] - pub url: Url, - pub signature: Signature, - #[ts(type = "string")] - pub version: Version, + pub version: VersionString, #[ts(type = "string")] pub platform: InternedString, - #[serde(default)] - pub upload: bool, + #[ts(type = "string")] + pub url: Url, #[serde(rename = "__auth_signer")] - pub signer: SignerKey, + #[ts(skip)] + pub signer: AnyVerifyingKey, + pub signature: AnySignature, + pub commitment: Blake3Commitment, } async fn add_asset( ctx: RegistryContext, AddAssetParams { - url, - signature, version, platform, - upload, + url, signer, + signature, + commitment, }: AddAssetParams, - accessor: impl FnOnce(&mut Model) -> &mut Model> + accessor: impl FnOnce( + &mut Model, + ) -> &mut Model>> + UnwindSafe + Send, -) -> Result, Error> { - ensure_code!( - signature.signer() == signer, - ErrorKind::InvalidSignature, - "asset signature does not match request signer" - ); - +) -> Result<(), Error> { + signer + .scheme() + .verify_commitment(&signer, &commitment, SIG_CONTEXT, &signature)?; ctx.db .mutate(|db| { let signer_guid = db.as_index().as_signers().get_signer(&signer)?; @@ -95,7 +94,7 @@ async fn add_asset( .as_versions() .as_idx(&version) .or_not_found(&version)? - .as_signers() + .as_authorized() .de()? .contains(&signer_guid) { @@ -109,11 +108,21 @@ async fn add_asset( .upsert(&platform, || { Ok(RegistryAsset { url, - signature_info: SignatureInfo::new(SIG_CONTEXT), + commitment: commitment.clone(), + signatures: HashMap::new(), }) })? - .as_signature_info_mut() - .mutate(|s| s.add_sig(&signature))?; + .mutate(|s| { + if s.commitment != commitment { + Err(Error::new( + eyre!("commitment does not match"), + ErrorKind::InvalidSignature, + )) + } else { + s.signatures.insert(signer, signature); + Ok(()) + } + })?; Ok(()) } else { Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization)) @@ -121,80 +130,18 @@ async fn add_asset( }) .await?; - let guid = if upload { - let guid = RequestGuid::new(); - let auth_guid = guid.clone(); - let signer = signature.signer(); - let hostname = ctx.hostname.clone(); - ctx.rpc_continuations - .add( - guid.clone(), - RpcContinuation::rest( - Box::new(|req| { - async move { - Ok( - if async move { - let auth_sig = base64::decode( - req.headers().get("X-StartOS-Registry-Auth-Sig")?, - ) - .ok()?; - signer - .verify_message( - auth_guid.as_ref().as_bytes(), - &auth_sig, - &hostname, - ) - .ok()?; - - Some(()) - } - .await - .is_some() - { - Response::builder() - .status(200) - .body(axum::body::Body::empty()) - .with_kind(ErrorKind::Network)? - } else { - Response::builder() - .status(401) - .body(axum::body::Body::empty()) - .with_kind(ErrorKind::Network)? - }, - ) - } - .boxed() - }), - Duration::from_secs(30), - ), - ) - .await; - Some(guid) - } else { - None - }; - - Ok(guid) + Ok(()) } -pub async fn add_iso( - ctx: RegistryContext, - params: AddAssetParams, -) -> Result, Error> { +pub async fn add_iso(ctx: RegistryContext, params: AddAssetParams) -> Result<(), Error> { add_asset(ctx, params, |m| m.as_iso_mut()).await } -pub async fn add_img( - ctx: RegistryContext, - params: AddAssetParams, -) -> Result, Error> { +pub async fn add_img(ctx: RegistryContext, params: AddAssetParams) -> Result<(), Error> { add_asset(ctx, params, |m| m.as_img_mut()).await } -pub async fn add_squashfs( - ctx: RegistryContext, - params: AddAssetParams, -) -> Result, Error> { +pub async fn add_squashfs(ctx: RegistryContext, params: AddAssetParams) -> Result<(), Error> { add_asset(ctx, params, |m| m.as_squashfs_mut()).await } @@ -205,11 +152,9 @@ pub struct CliAddAssetParams { #[arg(short = 'p', long = "platform")] pub platform: InternedString, #[arg(short = 'v', long = "version")] - pub version: Version, + pub version: VersionString, pub file: PathBuf, pub url: Url, - #[arg(short = 'u', long = "upload")] - pub upload: bool, } pub async fn cli_add_asset( @@ -223,7 +168,6 @@ pub async fn cli_add_asset( version, file: path, url, - upload, }, .. }: HandlerArgs, @@ -240,21 +184,18 @@ pub async fn cli_add_asset( } }; - let file = tokio::fs::File::open(&path).await?.into(); + let file = MultiCursorFile::from(tokio::fs::File::open(&path).await?); let mut progress = FullProgressTracker::new(); let progress_handle = progress.handle(); let mut sign_phase = progress_handle.add_phase(InternedString::intern("Signing File"), Some(10)); + let mut verify_phase = + progress_handle.add_phase(InternedString::intern("Verifying URL"), Some(100)); let mut index_phase = progress_handle.add_phase( InternedString::intern("Adding File to Registry Index"), Some(1), ); - let mut upload_phase = if upload { - Some(progress_handle.add_phase(InternedString::intern("Uploading File"), Some(100))) - } else { - None - }; let progress_task: NonDetachingJoinHandle<()> = tokio::spawn(async move { let mut bar = PhasedProgressBar::new(&format!("Adding {} to registry...", path.display())); @@ -270,70 +211,46 @@ pub async fn cli_add_asset( .into(); sign_phase.start(); - let blake3_sig = - Blake3Ed25519Signature::sign_file(ctx.developer_key()?, &file, SIG_CONTEXT).await?; - let size = blake3_sig.size; - let signature = Signature::Blake3Ed25519(blake3_sig); + let blake3 = file.blake3_mmap().await?; + let size = file + .size() + .await + .ok_or_else(|| Error::new(eyre!("failed to read file metadata"), ErrorKind::Filesystem))?; + let commitment = Blake3Commitment { + hash: Base64(*blake3.as_bytes()), + size, + }; + let signature = Ed25519.sign_commitment(ctx.developer_key()?, &commitment, SIG_CONTEXT)?; sign_phase.complete(); - index_phase.start(); - let add_res = from_value::>( - ctx.call_remote::( - &parent_method - .into_iter() - .chain(method) - .chain([ext]) - .join("."), - imbl_value::json!({ - "platform": platform, - "version": version, - "url": &url, - "signature": signature, - "upload": upload, - }), - ) - .await?, - )?; - index_phase.complete(); + verify_phase.start(); + let src = HttpSource::new(ctx.client.clone(), url.clone()).await?; + let mut writer = verify_phase.writer(VerifyingWriter::new( + tokio::io::sink(), + Some((blake3::Hash::from_bytes(*commitment.hash), commitment.size)), + )); + src.copy_all_to(&mut writer).await?; + let (verifier, mut verify_phase) = writer.into_inner(); + verifier.verify().await?; + verify_phase.complete(); - if let Some(guid) = add_res { - upload_phase.as_mut().map(|p| p.start()); - upload_phase.as_mut().map(|p| p.set_total(size)); - let reg_url = ctx.registry_url.as_ref().or_not_found("--registry")?; - ctx.client - .post(url) - .header("X-StartOS-Registry-Token", guid.as_ref()) - .header( - "X-StartOS-Registry-Auth-Sig", - base64::encode( - ctx.developer_key()? - .sign_prehashed( - Sha512::new_with_prefix(guid.as_ref().as_bytes()), - Some( - reg_url - .host() - .or_not_found("registry hostname")? - .to_string() - .as_bytes(), - ), - )? - .to_bytes(), - ), - ) - .body(reqwest::Body::wrap_stream( - tokio_util::io::ReaderStream::new(file.fetch(0, size).await?).inspect_ok( - move |b| { - upload_phase - .as_mut() - .map(|p| *p += b.len() as u64) - .apply(|_| ()) - }, - ), - )) - .send() - .await?; - // upload_phase.as_mut().map(|p| p.complete()); - } + index_phase.start(); + ctx.call_remote::( + &parent_method + .into_iter() + .chain(method) + .chain([ext]) + .join("."), + imbl_value::json!({ + "platform": platform, + "version": version, + "url": &url, + "signature": signature, + "commitment": commitment, + }), + ) + .await?; + index_phase.complete(); progress_handle.complete(); diff --git a/core/startos/src/registry/os/asset/get.rs b/core/startos/src/registry/os/asset/get.rs index 52e95bc98..e099a50cf 100644 --- a/core/startos/src/registry/os/asset/get.rs +++ b/core/startos/src/registry/os/asset/get.rs @@ -16,8 +16,11 @@ use crate::progress::{FullProgressTracker, PhasedProgressBar}; use crate::registry::asset::RegistryAsset; use crate::registry::context::RegistryContext; use crate::registry::os::index::OsVersionInfo; +use crate::registry::os::SIG_CONTEXT; +use crate::registry::signer::commitment::blake3::Blake3Commitment; +use crate::registry::signer::commitment::Commitment; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; -use crate::util::Version; +use crate::util::VersionString; pub fn get_api() -> ParentHandler { ParentHandler::new() @@ -33,8 +36,7 @@ pub fn get_api() -> ParentHandler { #[serde(rename_all = "camelCase")] #[ts(export)] pub struct GetOsAssetParams { - #[ts(type = "string")] - pub version: Version, + pub version: VersionString, #[ts(type = "string")] pub platform: InternedString, } @@ -42,10 +44,12 @@ pub struct GetOsAssetParams { async fn get_os_asset( ctx: RegistryContext, GetOsAssetParams { version, platform }: GetOsAssetParams, - accessor: impl FnOnce(&Model) -> &Model> + accessor: impl FnOnce( + &Model, + ) -> &Model>> + UnwindSafe + Send, -) -> Result { +) -> Result, Error> { accessor( ctx.db .peek() @@ -64,21 +68,21 @@ async fn get_os_asset( pub async fn get_iso( ctx: RegistryContext, params: GetOsAssetParams, -) -> Result { +) -> Result, Error> { get_os_asset(ctx, params, |info| info.as_iso()).await } pub async fn get_img( ctx: RegistryContext, params: GetOsAssetParams, -) -> Result { +) -> Result, Error> { get_os_asset(ctx, params, |info| info.as_img()).await } pub async fn get_squashfs( ctx: RegistryContext, params: GetOsAssetParams, -) -> Result { +) -> Result, Error> { get_os_asset(ctx, params, |info| info.as_squashfs()).await } @@ -86,7 +90,7 @@ pub async fn get_squashfs( #[command(rename_all = "kebab-case")] #[serde(rename_all = "camelCase")] pub struct CliGetOsAssetParams { - pub version: Version, + pub version: VersionString, pub platform: InternedString, #[arg(long = "download", short = 'd')] pub download: Option, @@ -112,8 +116,8 @@ async fn cli_get_os_asset( }, .. }: HandlerArgs, -) -> Result { - let res = from_value::( +) -> Result, Error> { + let res = from_value::>( ctx.call_remote::( &parent_method.into_iter().chain(method).join("."), json!({ @@ -124,7 +128,7 @@ async fn cli_get_os_asset( .await?, )?; - let validator = res.validate(res.signature_info.all_signers())?; + res.validate(SIG_CONTEXT, res.all_signers())?; if let Some(download) = download { let mut file = AtomicFile::new(&download, None::<&Path>) @@ -135,7 +139,7 @@ async fn cli_get_os_asset( let progress_handle = progress.handle(); let mut download_phase = progress_handle.add_phase(InternedString::intern("Downloading File"), Some(100)); - download_phase.set_total(validator.size()?); + download_phase.set_total(res.commitment.size); let reverify_phase = if reverify { Some(progress_handle.add_phase(InternedString::intern("Reverifying File"), Some(10))) } else { @@ -157,7 +161,7 @@ async fn cli_get_os_asset( download_phase.start(); let mut download_writer = download_phase.writer(&mut *file); - res.download(ctx.client.clone(), &mut download_writer, &validator) + res.download(ctx.client.clone(), &mut download_writer) .await?; let (_, mut download_phase) = download_writer.into_inner(); file.save().await.with_kind(ErrorKind::Filesystem)?; @@ -165,8 +169,8 @@ async fn cli_get_os_asset( if let Some(mut reverify_phase) = reverify_phase { reverify_phase.start(); - validator - .validate_file(&MultiCursorFile::from( + res.commitment + .check(&MultiCursorFile::from( tokio::fs::File::open(download).await?, )) .await?; diff --git a/core/startos/src/registry/os/asset/sign.rs b/core/startos/src/registry/os/asset/sign.rs index e19a82899..0cb657bef 100644 --- a/core/startos/src/registry/os/asset/sign.rs +++ b/core/startos/src/registry/os/asset/sign.rs @@ -17,25 +17,47 @@ use crate::registry::asset::RegistryAsset; use crate::registry::context::RegistryContext; use crate::registry::os::index::OsVersionInfo; use crate::registry::os::SIG_CONTEXT; -use crate::registry::signer::{Blake3Ed25519Signature, Signature}; -use crate::util::Version; +use crate::registry::signer::commitment::blake3::Blake3Commitment; +use crate::registry::signer::sign::ed25519::Ed25519; +use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey, SignatureScheme}; +use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; +use crate::s9pk::merkle_archive::source::ArchiveSource; +use crate::util::serde::Base64; +use crate::util::VersionString; pub fn sign_api() -> ParentHandler { ParentHandler::new() - .subcommand("iso", from_fn_async(sign_iso).no_cli()) - .subcommand("img", from_fn_async(sign_img).no_cli()) - .subcommand("squashfs", from_fn_async(sign_squashfs).no_cli()) + .subcommand( + "iso", + from_fn_async(sign_iso) + .with_metadata("getSigner", Value::Bool(true)) + .no_cli(), + ) + .subcommand( + "img", + from_fn_async(sign_img) + .with_metadata("getSigner", Value::Bool(true)) + .no_cli(), + ) + .subcommand( + "squashfs", + from_fn_async(sign_squashfs) + .with_metadata("getSigner", Value::Bool(true)) + .no_cli(), + ) } #[derive(Debug, Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export)] pub struct SignAssetParams { - #[ts(type = "string")] - version: Version, + version: VersionString, #[ts(type = "string")] platform: InternedString, - signature: Signature, + #[ts(skip)] + #[serde(rename = "__auth_signer")] + signer: AnyVerifyingKey, + signature: AnySignature, } async fn sign_asset( @@ -43,22 +65,25 @@ async fn sign_asset( SignAssetParams { version, platform, + signer, signature, }: SignAssetParams, - accessor: impl FnOnce(&mut Model) -> &mut Model> + accessor: impl FnOnce( + &mut Model, + ) -> &mut Model>> + UnwindSafe + Send, ) -> Result<(), Error> { ctx.db .mutate(|db| { - let guid = db.as_index().as_signers().get_signer(&signature.signer())?; + let guid = db.as_index().as_signers().get_signer(&signer)?; if !db .as_index() .as_os() .as_versions() .as_idx(&version) .or_not_found(&version)? - .as_signers() + .as_authorized() .de()? .contains(&guid) { @@ -77,8 +102,16 @@ async fn sign_asset( ) .as_idx_mut(&platform) .or_not_found(&platform)? - .as_signature_info_mut() - .mutate(|s| s.add_sig(&signature))?; + .mutate(|s| { + signer.scheme().verify_commitment( + &signer, + &s.commitment, + SIG_CONTEXT, + &signature, + )?; + s.signatures.insert(signer, signature); + Ok(()) + })?; Ok(()) }) @@ -104,7 +137,7 @@ pub struct CliSignAssetParams { #[arg(short = 'p', long = "platform")] pub platform: InternedString, #[arg(short = 'v', long = "version")] - pub version: Version, + pub version: VersionString, pub file: PathBuf, } @@ -134,7 +167,7 @@ pub async fn cli_sign_asset( } }; - let file = tokio::fs::File::open(&path).await?.into(); + let file = MultiCursorFile::from(tokio::fs::File::open(&path).await?); let mut progress = FullProgressTracker::new(); let progress_handle = progress.handle(); @@ -159,9 +192,16 @@ pub async fn cli_sign_asset( .into(); sign_phase.start(); - let blake3_sig = - Blake3Ed25519Signature::sign_file(ctx.developer_key()?, &file, SIG_CONTEXT).await?; - let signature = Signature::Blake3Ed25519(blake3_sig); + let blake3 = file.blake3_mmap().await?; + let size = file + .size() + .await + .ok_or_else(|| Error::new(eyre!("failed to read file metadata"), ErrorKind::Filesystem))?; + let commitment = Blake3Commitment { + hash: Base64(*blake3.as_bytes()), + size, + }; + let signature = Ed25519.sign_commitment(ctx.developer_key()?, &commitment, SIG_CONTEXT)?; sign_phase.complete(); index_phase.start(); diff --git a/core/startos/src/registry/os/index.rs b/core/startos/src/registry/os/index.rs index 186a7e95e..3ee75bc6a 100644 --- a/core/startos/src/registry/os/index.rs +++ b/core/startos/src/registry/os/index.rs @@ -8,16 +8,16 @@ use ts_rs::TS; use crate::prelude::*; use crate::registry::asset::RegistryAsset; use crate::registry::context::RegistryContext; -use crate::rpc_continuations::RequestGuid; -use crate::util::Version; +use crate::registry::signer::commitment::blake3::Blake3Commitment; +use crate::rpc_continuations::Guid; +use crate::util::VersionString; #[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] #[serde(rename_all = "camelCase")] #[model = "Model"] #[ts(export)] pub struct OsIndex { - #[ts(as = "BTreeMap::")] - pub versions: BTreeMap, + pub versions: BTreeMap, } #[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] @@ -29,14 +29,13 @@ pub struct OsVersionInfo { pub release_notes: String, #[ts(type = "string")] pub source_version: VersionRange, - #[ts(type = "string[]")] - pub signers: BTreeSet, - #[ts(as = "BTreeMap::")] - pub iso: BTreeMap, // platform (i.e. x86_64-nonfree) -> asset - #[ts(as = "BTreeMap::")] - pub squashfs: BTreeMap, // platform (i.e. x86_64-nonfree) -> asset - #[ts(as = "BTreeMap::")] - pub img: BTreeMap, // platform (i.e. raspberrypi) -> asset + pub authorized: BTreeSet, + #[ts(as = "BTreeMap::>")] + pub iso: BTreeMap>, // platform (i.e. x86_64-nonfree) -> asset + #[ts(as = "BTreeMap::>")] + pub squashfs: BTreeMap>, // platform (i.e. x86_64-nonfree) -> asset + #[ts(as = "BTreeMap::>")] + pub img: BTreeMap>, // platform (i.e. raspberrypi) -> asset } pub async fn get_os_index(ctx: RegistryContext) -> Result { diff --git a/core/startos/src/registry/os/version/mod.rs b/core/startos/src/registry/os/version/mod.rs index 5af407c5c..5bf926e5e 100644 --- a/core/startos/src/registry/os/version/mod.rs +++ b/core/startos/src/registry/os/version/mod.rs @@ -11,9 +11,9 @@ use crate::context::CliContext; use crate::prelude::*; use crate::registry::context::RegistryContext; use crate::registry::os::index::OsVersionInfo; -use crate::registry::signer::SignerKey; +use crate::registry::signer::sign::AnyVerifyingKey; use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat}; -use crate::util::Version; +use crate::util::VersionString; pub mod signer; @@ -51,8 +51,7 @@ pub fn version_api() -> ParentHandler { #[serde(rename_all = "camelCase")] #[ts(export)] pub struct AddVersionParams { - #[ts(type = "string")] - pub version: Version, + pub version: VersionString, pub headline: String, pub release_notes: String, #[ts(type = "string")] @@ -60,7 +59,7 @@ pub struct AddVersionParams { #[arg(skip)] #[ts(skip)] #[serde(rename = "__auth_signer")] - pub signer: Option, + pub signer: Option, } pub async fn add_version( @@ -86,7 +85,7 @@ pub async fn add_version( i.headline = headline; i.release_notes = release_notes; i.source_version = source_version; - i.signers.extend(signer); + i.authorized.extend(signer); Ok(()) }) }) @@ -98,8 +97,7 @@ pub async fn add_version( #[serde(rename_all = "camelCase")] #[ts(export)] pub struct RemoveVersionParams { - #[ts(type = "string")] - pub version: Version, + pub version: VersionString, } pub async fn remove_version( @@ -124,7 +122,7 @@ pub async fn remove_version( pub struct GetVersionParams { #[ts(type = "string | null")] #[arg(long = "src")] - pub source: Option, + pub source: Option, #[ts(type = "string | null")] #[arg(long = "target")] pub target: Option, @@ -133,7 +131,7 @@ pub struct GetVersionParams { pub async fn get_version( ctx: RegistryContext, GetVersionParams { source, target }: GetVersionParams, -) -> Result, Error> { +) -> Result, Error> { let target = target.unwrap_or(VersionRange::Any); ctx.db .peek() @@ -153,7 +151,10 @@ pub async fn get_version( .collect() } -pub fn display_version_info(params: WithIoFormat, info: BTreeMap) { +pub fn display_version_info( + params: WithIoFormat, + info: BTreeMap, +) { use prettytable::*; if let Some(format) = params.format { diff --git a/core/startos/src/registry/os/version/signer.rs b/core/startos/src/registry/os/version/signer.rs index 01173ad18..bb15860aa 100644 --- a/core/startos/src/registry/os/version/signer.rs +++ b/core/startos/src/registry/os/version/signer.rs @@ -10,9 +10,9 @@ use crate::prelude::*; use crate::registry::admin::display_signers; use crate::registry::context::RegistryContext; use crate::registry::signer::SignerInfo; -use crate::rpc_continuations::RequestGuid; +use crate::rpc_continuations::Guid; use crate::util::serde::HandlerExtSerde; -use crate::util::Version; +use crate::util::VersionString; pub fn signer_api() -> ParentHandler { ParentHandler::new() @@ -44,10 +44,8 @@ pub fn signer_api() -> ParentHandler { #[serde(rename_all = "camelCase")] #[ts(export)] pub struct VersionSignerParams { - #[ts(type = "string")] - pub version: Version, - #[ts(type = "string")] - pub signer: RequestGuid, + pub version: VersionString, + pub signer: Guid, } pub async fn add_version_signer( @@ -67,7 +65,7 @@ pub async fn add_version_signer( .as_versions_mut() .as_idx_mut(&version) .or_not_found(&version)? - .as_signers_mut() + .as_authorized_mut() .mutate(|s| Ok(s.insert(signer)))?; Ok(()) @@ -87,7 +85,7 @@ pub async fn remove_version_signer( .as_versions_mut() .as_idx_mut(&version) .or_not_found(&version)? - .as_signers_mut() + .as_authorized_mut() .mutate(|s| Ok(s.remove(&signer)))? { return Err(Error::new( @@ -106,21 +104,20 @@ pub async fn remove_version_signer( #[serde(rename_all = "camelCase")] #[ts(export)] pub struct ListVersionSignersParams { - #[ts(type = "string")] - pub version: Version, + pub version: VersionString, } pub async fn list_version_signers( ctx: RegistryContext, ListVersionSignersParams { version }: ListVersionSignersParams, -) -> Result, Error> { +) -> Result, Error> { let db = ctx.db.peek().await; db.as_index() .as_os() .as_versions() .as_idx(&version) .or_not_found(&version)? - .as_signers() + .as_authorized() .de()? .into_iter() .filter_map(|guid| { diff --git a/core/startos/src/registry/package/add.rs b/core/startos/src/registry/package/add.rs new file mode 100644 index 000000000..6a1050b99 --- /dev/null +++ b/core/startos/src/registry/package/add.rs @@ -0,0 +1,170 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use clap::Parser; +use helpers::NonDetachingJoinHandle; +use imbl_value::InternedString; +use itertools::Itertools; +use rpc_toolkit::HandlerArgs; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; +use url::Url; + +use crate::context::CliContext; +use crate::prelude::*; +use crate::progress::{FullProgressTracker, PhasedProgressBar}; +use crate::registry::context::RegistryContext; +use crate::registry::package::index::PackageVersionInfo; +use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; +use crate::registry::signer::sign::ed25519::Ed25519; +use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey, SignatureScheme}; +use crate::s9pk::merkle_archive::source::http::HttpSource; +use crate::s9pk::v2::SIG_CONTEXT; +use crate::s9pk::S9pk; +use crate::util::io::TrackingIO; + +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct AddPackageParams { + #[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_package( + ctx: RegistryContext, + AddPackageParams { + url, + 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 s9pk = S9pk::deserialize( + &Arc::new(HttpSource::new(ctx.client.clone(), url.clone()).await?), + Some(&commitment), + false, + ) + .await?; + + let manifest = s9pk.as_manifest(); + + let mut info = PackageVersionInfo::from_s9pk(&s9pk, url).await?; + if !info.s9pk.signatures.contains_key(&uploader) { + info.s9pk.signatures.insert(uploader.clone(), signature); + } + + 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()? + .contains(&uploader_guid) + { + let package = db + .as_index_mut() + .as_package_mut() + .as_packages_mut() + .upsert(&manifest.id, || Ok(Default::default()))?; + package.as_versions_mut().insert(&manifest.version, &info)?; + + Ok(()) + } else { + Err(Error::new(eyre!("UNAUTHORIZED"), ErrorKind::Authorization)) + } + }) + .await +} + +#[derive(Debug, Deserialize, Serialize, Parser)] +#[command(rename_all = "kebab-case")] +#[serde(rename_all = "camelCase")] +pub struct CliAddPackageParams { + pub file: PathBuf, + pub url: Url, +} + +pub async fn cli_add_package( + HandlerArgs { + context: ctx, + parent_method, + method, + params: CliAddPackageParams { file, url }, + .. + }: HandlerArgs, +) -> Result<(), Error> { + let s9pk = S9pk::open(&file, None, false).await?; + + let mut progress = FullProgressTracker::new(); + let progress_handle = progress.handle(); + let mut sign_phase = progress_handle.add_phase(InternedString::intern("Signing File"), Some(1)); + let mut verify_phase = + progress_handle.add_phase(InternedString::intern("Verifying URL"), Some(100)); + let mut index_phase = progress_handle.add_phase( + InternedString::intern("Adding File to Registry Index"), + Some(1), + ); + + let progress_task: NonDetachingJoinHandle<()> = tokio::spawn(async move { + let mut bar = PhasedProgressBar::new(&format!("Adding {} to registry...", file.display())); + loop { + let snap = progress.snapshot(); + bar.update(&snap); + if snap.overall.is_complete() { + break; + } + progress.changed().await + } + }) + .into(); + + sign_phase.start(); + let commitment = s9pk.as_archive().commitment().await?; + let signature = Ed25519.sign_commitment(ctx.developer_key()?, &commitment, SIG_CONTEXT)?; + sign_phase.complete(); + + verify_phase.start(); + let mut src = S9pk::deserialize( + &Arc::new(HttpSource::new(ctx.client.clone(), url.clone()).await?), + Some(&commitment), + false, + ) + .await?; + src.serialize(&mut TrackingIO::new(0, tokio::io::sink()), true) + .await?; + verify_phase.complete(); + + index_phase.start(); + ctx.call_remote::( + &parent_method.into_iter().chain(method).join("."), + imbl_value::json!({ + "url": &url, + "signature": signature, + "commitment": commitment, + }), + ) + .await?; + index_phase.complete(); + + progress_handle.complete(); + + progress_task.await.with_kind(ErrorKind::Unknown)?; + + Ok(()) +} diff --git a/core/startos/src/registry/package/get.rs b/core/startos/src/registry/package/get.rs new file mode 100644 index 000000000..835192361 --- /dev/null +++ b/core/startos/src/registry/package/get.rs @@ -0,0 +1,387 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use clap::{Parser, ValueEnum}; +use emver::{Version, VersionRange}; +use imbl_value::InternedString; +use itertools::Itertools; +use models::PackageId; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::prelude::*; +use crate::registry::context::RegistryContext; +use crate::registry::device_info::DeviceInfo; +use crate::registry::package::index::{PackageIndex, PackageVersionInfo}; +use crate::util::serde::{display_serializable, WithIoFormat}; +use crate::util::VersionString; + +#[derive( + Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS, ValueEnum, +)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub enum PackageDetailLevel { + Short, + Full, +} +impl Default for PackageDetailLevel { + fn default() -> Self { + Self::Short + } +} + +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct PackageInfoShort { + pub release_notes: String, +} + +#[derive(Debug, Deserialize, Serialize, TS, Parser)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "kebab-case")] +#[ts(export)] +pub struct GetPackageParams { + pub id: Option, + #[ts(type = "string | null")] + pub version: Option, + #[ts(type = "string | null")] + pub source_version: Option, + #[ts(skip)] + #[arg(skip)] + #[serde(rename = "__device_info")] + pub device_info: Option, + pub other_versions: Option, +} + +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct GetPackageResponse { + #[ts(type = "string[]")] + pub categories: BTreeSet, + pub best: BTreeMap, + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub other_versions: Option>, +} +impl GetPackageResponse { + pub fn tables(&self) -> Vec { + use prettytable::*; + + let mut res = Vec::with_capacity(self.best.len()); + + for (version, info) in &self.best { + let mut table = info.table(version); + + let lesser_versions: BTreeMap<_, _> = self + .other_versions + .as_ref() + .into_iter() + .flatten() + .filter(|(v, _)| ***v < **version) + .collect(); + + if !lesser_versions.is_empty() { + table.add_row(row![bc => "OLDER VERSIONS"]); + table.add_row(row![bc => "VERSION", "RELEASE NOTES"]); + for (version, info) in lesser_versions { + table.add_row(row![AsRef::::as_ref(version), &info.release_notes]); + } + } + + res.push(table); + } + + res + } +} + +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct GetPackageResponseFull { + #[ts(type = "string[]")] + pub categories: BTreeSet, + pub best: BTreeMap, + pub other_versions: BTreeMap, +} +impl GetPackageResponseFull { + pub fn tables(&self) -> Vec { + let mut res = Vec::with_capacity(self.best.len()); + + let all: BTreeMap<_, _> = self.best.iter().chain(self.other_versions.iter()).collect(); + + for (version, info) in all { + res.push(info.table(version)); + } + + res + } +} + +pub type GetPackagesResponse = BTreeMap; +pub type GetPackagesResponseFull = BTreeMap; + +fn get_matching_models<'a>( + db: &'a Model, + GetPackageParams { + id, + version, + source_version, + device_info, + .. + }: &GetPackageParams, +) -> Result)>, Error> { + if let Some(id) = id { + if let Some(pkg) = db.as_packages().as_idx(id) { + vec![(id.clone(), pkg)] + } else { + vec![] + } + } else { + db.as_packages().as_entries()? + } + .iter() + .map(|(k, v)| { + Ok(v.as_versions() + .as_entries()? + .into_iter() + .map(|(v, info)| { + Ok::<_, Error>( + if version + .as_ref() + .map_or(true, |version| v.satisfies(version)) + && source_version.as_ref().map_or(Ok(true), |source_version| { + Ok::<_, Error>( + source_version.satisfies( + &info + .as_source_version() + .de()? + .unwrap_or(VersionRange::any()), + ), + ) + })? + && device_info + .as_ref() + .map_or(Ok(true), |device_info| info.works_for_device(device_info))? + { + Some((k.clone(), Version::from(v), info)) + } else { + None + }, + ) + }) + .flatten_ok()) + }) + .flatten_ok() + .map(|res| res.and_then(|a| a)) + .collect() +} + +pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Result { + use patch_db::ModelExt; + + let peek = ctx.db.peek().await; + let mut best: BTreeMap>> = + Default::default(); + let mut other: BTreeMap>> = + Default::default(); + for (id, version, info) in get_matching_models(&peek.as_index().as_package(), ¶ms)? { + let mut package_best = best.remove(&id).unwrap_or_default(); + let mut package_other = other.remove(&id).unwrap_or_default(); + for worse_version in package_best + .keys() + .filter(|k| ***k < version) + .cloned() + .collect_vec() + { + if let Some(info) = package_best.remove(&worse_version) { + package_other.insert(worse_version, info); + } + } + if package_best.keys().all(|k| !(**k > version)) { + package_best.insert(version.into(), info); + } + best.insert(id.clone(), package_best); + if params.other_versions.is_some() { + other.insert(id.clone(), package_other); + } + } + if let Some(id) = params.id { + let categories = peek + .as_index() + .as_package() + .as_packages() + .as_idx(&id) + .map(|p| p.as_categories().de()) + .transpose()? + .unwrap_or_default(); + let best = best + .remove(&id) + .unwrap_or_default() + .into_iter() + .map(|(k, v)| v.de().map(|v| (k, v))) + .try_collect()?; + let other = other.remove(&id).unwrap_or_default(); + match params.other_versions { + None => to_value(&GetPackageResponse { + categories, + best, + other_versions: None, + }), + Some(PackageDetailLevel::Short) => to_value(&GetPackageResponse { + categories, + best, + other_versions: Some( + other + .into_iter() + .map(|(k, v)| from_value(v.as_value().clone()).map(|v| (k, v))) + .try_collect()?, + ), + }), + Some(PackageDetailLevel::Full) => to_value(&GetPackageResponseFull { + categories, + best, + other_versions: other + .into_iter() + .map(|(k, v)| v.de().map(|v| (k, v))) + .try_collect()?, + }), + } + } else { + match params.other_versions { + None => to_value( + &best + .into_iter() + .map(|(id, best)| { + let categories = peek + .as_index() + .as_package() + .as_packages() + .as_idx(&id) + .map(|p| p.as_categories().de()) + .transpose()? + .unwrap_or_default(); + Ok::<_, Error>(( + id, + GetPackageResponse { + categories, + best: best + .into_iter() + .map(|(k, v)| v.de().map(|v| (k, v))) + .try_collect()?, + other_versions: None, + }, + )) + }) + .try_collect::<_, GetPackagesResponse, _>()?, + ), + Some(PackageDetailLevel::Short) => to_value( + &best + .into_iter() + .map(|(id, best)| { + let categories = peek + .as_index() + .as_package() + .as_packages() + .as_idx(&id) + .map(|p| p.as_categories().de()) + .transpose()? + .unwrap_or_default(); + let other = other.remove(&id).unwrap_or_default(); + Ok::<_, Error>(( + id, + GetPackageResponse { + categories, + best: best + .into_iter() + .map(|(k, v)| v.de().map(|v| (k, v))) + .try_collect()?, + other_versions: Some( + other + .into_iter() + .map(|(k, v)| { + from_value(v.as_value().clone()).map(|v| (k, v)) + }) + .try_collect()?, + ), + }, + )) + }) + .try_collect::<_, GetPackagesResponse, _>()?, + ), + Some(PackageDetailLevel::Full) => to_value( + &best + .into_iter() + .map(|(id, best)| { + let categories = peek + .as_index() + .as_package() + .as_packages() + .as_idx(&id) + .map(|p| p.as_categories().de()) + .transpose()? + .unwrap_or_default(); + let other = other.remove(&id).unwrap_or_default(); + Ok::<_, Error>(( + id, + GetPackageResponseFull { + categories, + best: best + .into_iter() + .map(|(k, v)| v.de().map(|v| (k, v))) + .try_collect()?, + other_versions: other + .into_iter() + .map(|(k, v)| v.de().map(|v| (k, v))) + .try_collect()?, + }, + )) + }) + .try_collect::<_, GetPackagesResponseFull, _>()?, + ), + } + } +} + +pub fn display_package_info( + params: WithIoFormat, + info: Value, +) -> Result<(), Error> { + if let Some(format) = params.format { + display_serializable(format, info); + return Ok(()); + } + + if let Some(_) = params.rest.id { + if params.rest.other_versions == Some(PackageDetailLevel::Full) { + for table in from_value::(info)?.tables() { + table.print_tty(false)?; + println!(); + } + } else { + for table in from_value::(info)?.tables() { + table.print_tty(false)?; + println!(); + } + } + } else { + if params.rest.other_versions == Some(PackageDetailLevel::Full) { + for (_, package) in from_value::(info)? { + for table in package.tables() { + table.print_tty(false)?; + println!(); + } + } + } else { + for (_, package) in from_value::(info)? { + for table in package.tables() { + table.print_tty(false)?; + println!(); + } + } + } + } + Ok(()) +} diff --git a/core/startos/src/registry/package/index.rs b/core/startos/src/registry/package/index.rs new file mode 100644 index 000000000..0e6969fa5 --- /dev/null +++ b/core/startos/src/registry/package/index.rs @@ -0,0 +1,163 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use emver::{Version, VersionRange}; +use imbl_value::InternedString; +use models::{DataUrl, PackageId, VersionString}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; +use url::Url; + +use crate::prelude::*; +use crate::registry::asset::RegistryAsset; +use crate::registry::context::RegistryContext; +use crate::registry::device_info::DeviceInfo; +use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; +use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey}; +use crate::rpc_continuations::Guid; +use crate::s9pk::git_hash::GitHash; +use crate::s9pk::manifest::{Description, HardwareRequirements}; +use crate::s9pk::merkle_archive::source::FileSource; +use crate::s9pk::S9pk; + +#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct PackageIndex { + #[ts(as = "BTreeMap::")] + pub categories: BTreeMap, + pub packages: BTreeMap, +} + +#[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct PackageInfo { + pub authorized: BTreeSet, + pub versions: BTreeMap, + #[ts(type = "string[]")] + pub categories: BTreeSet, +} + +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct Category { + pub name: String, + pub description: Description, +} + +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct PackageVersionInfo { + pub title: String, + pub icon: DataUrl<'static>, + pub description: Description, + pub release_notes: String, + #[ts(type = "string")] + pub git_hash: GitHash, + #[ts(type = "string")] + pub license: InternedString, + #[ts(type = "string")] + pub wrapper_repo: Url, + #[ts(type = "string")] + pub upstream_repo: Url, + #[ts(type = "string")] + pub support_site: Url, + #[ts(type = "string")] + pub marketing_site: Url, + pub os_version: VersionString, + pub hardware_requirements: HardwareRequirements, + #[ts(type = "string | null")] + pub source_version: Option, + pub s9pk: RegistryAsset, +} +impl PackageVersionInfo { + pub async fn from_s9pk(s9pk: &S9pk, url: Url) -> Result { + let manifest = s9pk.as_manifest(); + Ok(Self { + title: manifest.title.clone(), + icon: s9pk.icon_data_url().await?, + description: manifest.description.clone(), + release_notes: manifest.release_notes.clone(), + git_hash: manifest.git_hash.clone().or_not_found("git hash")?, + license: manifest.license.clone(), + wrapper_repo: manifest.wrapper_repo.clone(), + upstream_repo: manifest.upstream_repo.clone(), + support_site: manifest.support_site.clone(), + marketing_site: manifest.marketing_site.clone(), + os_version: manifest.os_version.clone(), + hardware_requirements: manifest.hardware_requirements.clone(), + source_version: None, // TODO + s9pk: RegistryAsset { + url, + commitment: s9pk.as_archive().commitment().await?, + signatures: [( + AnyVerifyingKey::Ed25519(s9pk.as_archive().signer()), + AnySignature::Ed25519(s9pk.as_archive().signature().await?), + )] + .into_iter() + .collect(), + }, + }) + } + pub fn table(&self, version: &VersionString) -> prettytable::Table { + use prettytable::*; + + let mut table = Table::new(); + + table.add_row(row![bc => &self.title]); + table.add_row(row![br -> "VERSION", AsRef::::as_ref(version)]); + table.add_row(row![br -> "RELEASE NOTES", &self.release_notes]); + table.add_row(row![br -> "ABOUT", &self.description.short]); + table.add_row(row![br -> "DESCRIPTION", &self.description.long]); + table.add_row(row![br -> "GIT HASH", AsRef::::as_ref(&self.git_hash)]); + table.add_row(row![br -> "LICENSE", &self.license]); + table.add_row(row![br -> "PACKAGE REPO", &self.wrapper_repo.to_string()]); + table.add_row(row![br -> "SERVICE REPO", &self.upstream_repo.to_string()]); + table.add_row(row![br -> "WEBSITE", &self.marketing_site.to_string()]); + table.add_row(row![br -> "SUPPORT", &self.support_site.to_string()]); + + table + } +} +impl Model { + pub fn works_for_device(&self, device_info: &DeviceInfo) -> Result { + if !self.as_os_version().de()?.satisfies(&device_info.os.compat) { + return Ok(false); + } + let hw = self.as_hardware_requirements().de()?; + if let Some(arch) = hw.arch { + if !arch.contains(&device_info.hardware.arch) { + return Ok(false); + } + } + if let Some(ram) = hw.ram { + if device_info.hardware.ram < ram { + return Ok(false); + } + } + for (class, regex) in hw.device { + if !device_info + .hardware + .devices + .get(&*class) + .unwrap_or(&Vec::new()) + .iter() + .any(|product| regex.as_ref().is_match(product)) + { + return Ok(false); + } + } + + Ok(true) + } +} + +pub async fn get_package_index(ctx: RegistryContext) -> Result { + ctx.db.peek().await.into_index().into_package().de() +} diff --git a/core/startos/src/registry/package/mod.rs b/core/startos/src/registry/package/mod.rs new file mode 100644 index 000000000..ac09afbb1 --- /dev/null +++ b/core/startos/src/registry/package/mod.rs @@ -0,0 +1,29 @@ +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; + +use crate::context::CliContext; +use crate::prelude::*; +use crate::util::serde::HandlerExtSerde; + +pub mod add; +pub mod get; +pub mod index; + +pub fn package_api() -> ParentHandler { + ParentHandler::new() + .subcommand( + "index", + from_fn_async(index::get_package_index) + .with_display_serializable() + .with_call_remote::(), + ) + .subcommand("add", from_fn_async(add::add_package).no_cli()) + .subcommand("add", from_fn_async(add::cli_add_package).no_display()) + .subcommand( + "get", + from_fn_async(get::get_package) + .with_display_serializable() + .with_custom_display_fn(|handle, result| { + get::display_package_info(handle.params, result) + }), + ) +} diff --git a/core/startos/src/registry/signer.rs b/core/startos/src/registry/signer.rs deleted file mode 100644 index bf5374d75..000000000 --- a/core/startos/src/registry/signer.rs +++ /dev/null @@ -1,477 +0,0 @@ -use std::collections::{HashMap, HashSet}; -use std::path::Path; -use std::str::FromStr; - -use clap::builder::ValueParserFactory; -use imbl_value::InternedString; -use reqwest::Client; -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha512}; -use tokio::io::AsyncWrite; -use ts_rs::TS; -use url::Url; - -use crate::prelude::*; -use crate::s9pk::merkle_archive::source::http::HttpSource; -use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; -use crate::s9pk::merkle_archive::source::{ArchiveSource, FileSource}; -use crate::util::clap::FromStrParser; -use crate::util::serde::{Base64, Pem}; - -#[derive(Debug, Deserialize, Serialize, HasModel, TS)] -#[serde(rename_all = "camelCase")] -#[model = "Model"] -#[ts(export)] -pub struct SignerInfo { - pub name: String, - pub contact: Vec, - pub keys: HashSet, -} - -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -#[serde(tag = "alg", content = "pubkey")] -pub enum SignerKey { - Ed25519(Pem), -} -impl SignerKey { - pub fn verifier(&self) -> Verifier { - match self { - Self::Ed25519(k) => Verifier::Ed25519(*k, Sha512::new()), - } - } - pub fn verify_message( - &self, - message: &[u8], - signature: &[u8], - context: &str, - ) -> Result<(), Error> { - let mut v = self.verifier(); - v.update(message); - v.verify(signature, context) - } -} -impl std::fmt::Display for SignerKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Ed25519(k) => write!(f, "{k}"), - } - } -} - -pub enum Verifier { - Ed25519(Pem, Sha512), -} -impl Verifier { - pub fn update(&mut self, data: &[u8]) { - match self { - Self::Ed25519(_, h) => h.update(data), - } - } - pub fn verify(self, signature: &[u8], context: &str) -> Result<(), Error> { - match self { - Self::Ed25519(k, h) => k.verify_prehashed_strict( - h, - Some(context.as_bytes()), - &ed25519_dalek::Signature::from_slice(signature)?, - )?, - } - Ok(()) - } -} - -#[derive(Clone, Debug, Deserialize, Serialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -// TODO: better types -pub enum ContactInfo { - Email(String), - Matrix(String), - Website(#[ts(type = "string")] Url), -} -impl std::fmt::Display for ContactInfo { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Email(e) => write!(f, "mailto:{e}"), - Self::Matrix(m) => write!(f, "https://matrix.to/#/{m}"), - Self::Website(w) => write!(f, "{w}"), - } - } -} -impl FromStr for ContactInfo { - type Err = Error; - fn from_str(s: &str) -> Result { - Ok(if let Some(s) = s.strip_prefix("mailto:") { - Self::Email(s.to_owned()) - } else if let Some(s) = s.strip_prefix("https://matrix.to/#/") { - Self::Matrix(s.to_owned()) - } else { - Self::Website(s.parse()?) - }) - } -} -impl ValueParserFactory for ContactInfo { - type Parser = FromStrParser; - fn value_parser() -> Self::Parser { - Self::Parser::new() - } -} - -#[derive(Debug, Deserialize, Serialize, HasModel, TS)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -#[ts(export)] -pub struct SignatureInfo { - #[ts(type = "string")] - pub context: InternedString, - pub blake3_ed255i9: Option, -} -impl SignatureInfo { - pub fn new(context: &str) -> Self { - Self { - context: context.into(), - blake3_ed255i9: None, - } - } - pub fn validate(&self, accept: AcceptSigners) -> Result { - FileValidator::from_signatures(self.signatures(), accept, &self.context) - } - pub fn all_signers(&self) -> AcceptSigners { - AcceptSigners::All( - self.signatures() - .map(|s| AcceptSigners::Signer(s.signer())) - .collect(), - ) - .flatten() - } - pub fn signatures(&self) -> impl Iterator + '_ { - self.blake3_ed255i9.iter().flat_map(|info| { - info.signatures - .iter() - .map(|(k, s)| (k.clone(), *s)) - .map(|(pubkey, signature)| { - Signature::Blake3Ed25519(Blake3Ed25519Signature { - hash: info.hash, - size: info.size, - pubkey, - signature, - }) - }) - }) - } - pub fn add_sig(&mut self, signature: &Signature) -> Result<(), Error> { - signature.validate(&self.context)?; - match signature { - Signature::Blake3Ed25519(s) => { - if self - .blake3_ed255i9 - .as_ref() - .map_or(true, |info| info.hash == s.hash) - { - let new = if let Some(mut info) = self.blake3_ed255i9.take() { - info.signatures.insert(s.pubkey, s.signature); - info - } else { - s.info() - }; - self.blake3_ed255i9 = Some(new); - Ok(()) - } else { - Err(Error::new( - eyre!("hash sum mismatch"), - ErrorKind::InvalidSignature, - )) - } - } - } - } -} - -#[derive(Clone, Debug, Deserialize, Serialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -pub enum AcceptSigners { - #[serde(skip)] - Accepted(Signature), - Signer(SignerKey), - Any(Vec), - All(Vec), -} -impl AcceptSigners { - const fn null() -> Self { - Self::Any(Vec::new()) - } - pub fn flatten(self) -> Self { - match self { - Self::Any(mut s) | Self::All(mut s) if s.len() == 1 => s.swap_remove(0).flatten(), - s => s, - } - } - pub fn accepted(&self) -> bool { - match self { - Self::Accepted(_) => true, - Self::All(s) => s.iter().all(|s| s.accepted()), - _ => false, - } - } - pub fn try_accept( - self, - context: &str, - ) -> Box> + Send + Sync + '_> { - match self { - Self::Accepted(s) => Box::new(std::iter::once(s).map(|s| { - s.validate(context)?; - Ok(s) - })), - Self::All(s) => Box::new(s.into_iter().flat_map(|s| s.try_accept(context))), - _ => Box::new(std::iter::once(Err(Error::new( - eyre!("signer(s) not accepted"), - ErrorKind::InvalidSignature, - )))), - } - } - pub fn process_signature(&mut self, sig: &Signature) { - let new = match std::mem::replace(self, Self::null()) { - Self::Accepted(s) => Self::Accepted(s), - Self::Signer(s) => { - if s == sig.signer() { - Self::Accepted(sig.clone()) - } else { - Self::Signer(s) - } - } - Self::All(mut s) => { - s.iter_mut().for_each(|s| s.process_signature(sig)); - - Self::All(s) - } - Self::Any(mut s) => { - if let Some(s) = s - .iter_mut() - .map(|s| { - s.process_signature(sig); - s - }) - .filter(|s| s.accepted()) - .next() - { - std::mem::replace(s, Self::null()) - } else { - Self::Any(s) - } - } - }; - *self = new; - } -} - -#[must_use] -pub struct FileValidator { - blake3: Option, - size: Option, -} -impl FileValidator { - fn add_blake3(&mut self, hash: [u8; 32], size: u64) -> Result<(), Error> { - if let Some(h) = self.blake3 { - ensure_code!(h == hash, ErrorKind::InvalidSignature, "hash sum mismatch"); - } - self.blake3 = Some(blake3::Hash::from_bytes(hash)); - if let Some(s) = self.size { - ensure_code!(s == size, ErrorKind::InvalidSignature, "file size mismatch"); - } - self.size = Some(size); - Ok(()) - } - pub fn blake3(&self) -> Result { - if let Some(hash) = self.blake3 { - Ok(hash) - } else { - Err(Error::new( - eyre!("no BLAKE3 signatures found"), - ErrorKind::InvalidSignature, - )) - } - } - pub fn size(&self) -> Result { - if let Some(size) = self.size { - Ok(size) - } else { - Err(Error::new( - eyre!("no signatures found"), - ErrorKind::InvalidSignature, - )) - } - } - pub fn from_signatures( - signatures: impl IntoIterator, - mut accept: AcceptSigners, - context: &str, - ) -> Result { - let mut res = Self { - blake3: None, - size: None, - }; - for signature in signatures { - accept.process_signature(&signature); - } - for signature in accept.try_accept(context) { - match signature? { - Signature::Blake3Ed25519(s) => res.add_blake3(*s.hash, s.size)?, - } - } - - Ok(res) - } - pub async fn download( - &self, - url: Url, - client: Client, - dst: &mut (impl AsyncWrite + Unpin + Send + ?Sized), - ) -> Result<(), Error> { - let src = HttpSource::new(client, url).await?; - let (Some(hash), Some(size)) = (self.blake3, self.size) else { - return Err(Error::new( - eyre!("no BLAKE3 signatures found"), - ErrorKind::InvalidSignature, - )); - }; - src.section(0, size) - .copy_verify(dst, Some((hash, size))) - .await?; - - Ok(()) - } - pub async fn validate_file(&self, file: &MultiCursorFile) -> Result<(), Error> { - ensure_code!( - file.size().await == Some(self.size()?), - ErrorKind::InvalidSignature, - "file size mismatch" - ); - ensure_code!( - file.blake3_mmap().await? == self.blake3()?, - ErrorKind::InvalidSignature, - "hash sum mismatch" - ); - Ok(()) - } -} - -#[derive(Debug, Deserialize, Serialize, HasModel, TS)] -#[serde(rename_all = "camelCase")] -#[model = "Model"] -#[ts(export)] -pub struct Blake3Ed2551SignatureInfo { - pub hash: Base64<[u8; 32]>, - pub size: u64, - pub signatures: HashMap, Base64<[u8; 64]>>, -} -impl Blake3Ed2551SignatureInfo { - pub fn validate(&self, context: &str) -> Result>, Error> { - self.signatures - .iter() - .map(|(k, s)| { - let sig = Blake3Ed25519Signature { - hash: self.hash, - size: self.size, - pubkey: k.clone(), - signature: *s, - }; - sig.validate(context)?; - Ok(sig.pubkey) - }) - .collect() - } -} - -#[derive(Clone, Debug, Deserialize, Serialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -pub enum Signature { - Blake3Ed25519(Blake3Ed25519Signature), -} -impl Signature { - pub fn validate(&self, context: &str) -> Result<(), Error> { - match self { - Self::Blake3Ed25519(a) => a.validate(context), - } - } - pub fn signer(&self) -> SignerKey { - match self { - Self::Blake3Ed25519(s) => SignerKey::Ed25519(s.pubkey.clone()), - } - } -} - -#[derive(Clone, Debug, Deserialize, Serialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -pub struct Blake3Ed25519Signature { - pub hash: Base64<[u8; 32]>, - pub size: u64, - pub pubkey: Pem, - // ed25519-sig(sha512(blake3(file) + len_u64_be(file))) - pub signature: Base64<[u8; 64]>, -} -impl Blake3Ed25519Signature { - pub async fn sign_file( - key: &ed25519_dalek::SigningKey, - file: &MultiCursorFile, - context: &str, - ) -> Result { - let size = file - .size() - .await - .ok_or_else(|| Error::new(eyre!("failed to get file size"), ErrorKind::Filesystem))?; - let hash = file.blake3_mmap().await?; - let signature = key.sign_prehashed( - Sha512::new_with_prefix(hash.as_bytes()).chain_update(u64::to_be_bytes(size)), - Some(context.as_bytes()), - )?; - Ok(Self { - hash: Base64(*hash.as_bytes()), - size, - pubkey: Pem::new(key.verifying_key()), - signature: Base64(signature.to_bytes()), - }) - } - - pub fn validate(&self, context: &str) -> Result<(), Error> { - let sig = ed25519_dalek::Signature::from_bytes(&*self.signature); - self.pubkey.verify_prehashed_strict( - Sha512::new_with_prefix(*self.hash).chain_update(u64::to_be_bytes(self.size)), - Some(context.as_bytes()), - &sig, - )?; - Ok(()) - } - - pub async fn check_file(&self, file: &MultiCursorFile) -> Result<(), Error> { - let size = file - .size() - .await - .ok_or_else(|| Error::new(eyre!("failed to get file size"), ErrorKind::Filesystem))?; - if self.size != size { - return Err(Error::new( - eyre!("incorrect file size: expected {} got {}", self.size, size), - ErrorKind::InvalidSignature, - )); - } - let hash = file.blake3_mmap().await?; - if &*self.hash != hash.as_bytes() { - return Err(Error::new( - eyre!("hash sum mismatch"), - ErrorKind::InvalidSignature, - )); - } - Ok(()) - } - - pub fn info(&self) -> Blake3Ed2551SignatureInfo { - Blake3Ed2551SignatureInfo { - hash: self.hash, - size: self.size, - signatures: [(self.pubkey, self.signature)].into_iter().collect(), - } - } -} diff --git a/core/startos/src/registry/signer/commitment/blake3.rs b/core/startos/src/registry/signer/commitment/blake3.rs new file mode 100644 index 000000000..d99e68c16 --- /dev/null +++ b/core/startos/src/registry/signer/commitment/blake3.rs @@ -0,0 +1,50 @@ +use blake3::Hash; +use digest::Update; +use serde::{Deserialize, Serialize}; +use tokio::io::AsyncWrite; +use ts_rs::TS; + +use crate::prelude::*; +use crate::registry::signer::commitment::{Commitment, Digestable}; +use crate::s9pk::merkle_archive::hash::VerifyingWriter; +use crate::s9pk::merkle_archive::source::ArchiveSource; +use crate::util::io::{ParallelBlake3Writer, TrackingIO}; +use crate::util::serde::Base64; +use crate::CAP_10_MiB; + +#[derive(Clone, Debug, Deserialize, Serialize, HasModel, PartialEq, Eq, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct Blake3Commitment { + pub hash: Base64<[u8; 32]>, + #[ts(type = "number")] + pub size: u64, +} +impl Digestable for Blake3Commitment { + fn update(&self, digest: &mut D) { + digest.update(&*self.hash); + digest.update(&u64::to_be_bytes(self.size)); + } +} +impl<'a, Resource: ArchiveSource> Commitment<&'a Resource> for Blake3Commitment { + async fn create(resource: &'a Resource) -> Result { + let mut hasher = TrackingIO::new(0, ParallelBlake3Writer::new(CAP_10_MiB)); + resource.copy_all_to(&mut hasher).await?; + Ok(Self { + size: hasher.position(), + hash: Base64(*hasher.into_inner().finalize().await?.as_bytes()), + }) + } + async fn copy_to( + &self, + resource: &'a Resource, + writer: W, + ) -> Result<(), Error> { + let mut hasher = + VerifyingWriter::new(writer, Some((Hash::from_bytes(*self.hash), self.size))); + resource.copy_to(0, self.size, &mut hasher).await?; + hasher.verify().await?; + Ok(()) + } +} diff --git a/core/startos/src/registry/signer/commitment/merkle_archive.rs b/core/startos/src/registry/signer/commitment/merkle_archive.rs new file mode 100644 index 000000000..0b61734b4 --- /dev/null +++ b/core/startos/src/registry/signer/commitment/merkle_archive.rs @@ -0,0 +1,98 @@ +use digest::Update; +use serde::{Deserialize, Serialize}; +use tokio::io::AsyncWrite; +use ts_rs::TS; + +use crate::prelude::*; +use crate::registry::signer::commitment::{Commitment, Digestable}; +use crate::s9pk::merkle_archive::source::FileSource; +use crate::s9pk::merkle_archive::MerkleArchive; +use crate::s9pk::S9pk; +use crate::util::io::TrackingIO; +use crate::util::serde::Base64; + +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct MerkleArchiveCommitment { + pub root_sighash: Base64<[u8; 32]>, + #[ts(type = "number")] + pub root_maxsize: u64, +} +impl Digestable for MerkleArchiveCommitment { + fn update(&self, digest: &mut D) { + digest.update(&*self.root_sighash); + digest.update(&u64::to_be_bytes(self.root_maxsize)); + } +} +impl<'a, S: FileSource + Clone> Commitment<&'a MerkleArchive> for MerkleArchiveCommitment { + async fn create(resource: &'a MerkleArchive) -> Result { + resource.commitment().await + } + async fn check(&self, resource: &'a MerkleArchive) -> Result<(), Error> { + let MerkleArchiveCommitment { + root_sighash, + root_maxsize, + } = resource.commitment().await?; + if root_sighash != self.root_sighash { + return Err(Error::new( + eyre!("merkle root mismatch"), + ErrorKind::InvalidSignature, + )); + } + if root_maxsize > self.root_maxsize { + return Err(Error::new( + eyre!("merkle root directory max size too large"), + ErrorKind::InvalidSignature, + )); + } + Ok(()) + } + async fn copy_to( + &self, + resource: &'a MerkleArchive, + writer: W, + ) -> Result<(), Error> { + self.check(resource).await?; + resource + .serialize(&mut TrackingIO::new(0, writer), true) + .await + } +} + +impl<'a, S: FileSource + Clone> Commitment<&'a S9pk> for MerkleArchiveCommitment { + async fn create(resource: &'a S9pk) -> Result { + resource.as_archive().commitment().await + } + async fn check(&self, resource: &'a S9pk) -> Result<(), Error> { + let MerkleArchiveCommitment { + root_sighash, + root_maxsize, + } = resource.as_archive().commitment().await?; + if root_sighash != self.root_sighash { + return Err(Error::new( + eyre!("merkle root mismatch"), + ErrorKind::InvalidSignature, + )); + } + if root_maxsize > self.root_maxsize { + return Err(Error::new( + eyre!("merkle root directory max size too large"), + ErrorKind::InvalidSignature, + )); + } + Ok(()) + } + async fn copy_to( + &self, + resource: &'a S9pk, + writer: W, + ) -> Result<(), Error> { + self.check(resource).await?; + resource + .clone() + .serialize(&mut TrackingIO::new(0, writer), true) + .await + } +} diff --git a/core/startos/src/registry/signer/commitment/mod.rs b/core/startos/src/registry/signer/commitment/mod.rs new file mode 100644 index 000000000..b85e02a4e --- /dev/null +++ b/core/startos/src/registry/signer/commitment/mod.rs @@ -0,0 +1,25 @@ +use digest::Update; +use futures::Future; +use tokio::io::AsyncWrite; + +use crate::prelude::*; + +pub mod blake3; +pub mod merkle_archive; +pub mod request; + +pub trait Digestable { + fn update(&self, digest: &mut D); +} + +pub trait Commitment: Sized + Digestable { + fn create(resource: Resource) -> impl Future> + Send; + fn copy_to( + &self, + resource: Resource, + writer: W, + ) -> impl Future> + Send; + fn check(&self, resource: Resource) -> impl Future> + Send { + self.copy_to(resource, tokio::io::sink()) + } +} diff --git a/core/startos/src/registry/signer/commitment/request.rs b/core/startos/src/registry/signer/commitment/request.rs new file mode 100644 index 000000000..62d59163f --- /dev/null +++ b/core/startos/src/registry/signer/commitment/request.rs @@ -0,0 +1,102 @@ +use std::time::{SystemTime, UNIX_EPOCH}; +use std::collections::BTreeMap; + +use axum::body::Body; +use axum::extract::Request; +use digest::Update; +use futures::TryStreamExt; +use serde::{Deserialize, Serialize}; +use tokio::io::AsyncWrite; +use tokio_util::io::StreamReader; +use ts_rs::TS; +use url::Url; + +use crate::prelude::*; +use crate::registry::signer::commitment::{Commitment, Digestable}; +use crate::s9pk::merkle_archive::hash::VerifyingWriter; +use crate::util::serde::Base64; + +#[derive(Clone, Debug, Deserialize, Serialize, HasModel, PartialEq, Eq, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct RequestCommitment { + #[ts(type = "number")] + pub timestamp: i64, + #[ts(type = "number")] + pub nonce: u64, + #[ts(type = "number")] + pub size: u64, + pub blake3: Base64<[u8; 32]>, +} +impl RequestCommitment { + pub fn append_query(&self, url: &mut Url) { + url.query_pairs_mut() + .append_pair("timestamp", &self.timestamp.to_string()) + .append_pair("nonce", &self.nonce.to_string()) + .append_pair("size", &self.size.to_string()) + .append_pair("blake3", &self.blake3.to_string()); + } + pub fn from_query(url: &Url) -> Result { + let query: BTreeMap<_, _> = url.query_pairs().collect(); + Ok(Self { + timestamp: query.get("timestamp").or_not_found("timestamp")?.parse()?, + nonce: query.get("nonce").or_not_found("nonce")?.parse()?, + size: query.get("size").or_not_found("size")?.parse()?, + blake3: query.get("blake3").or_not_found("blake3")?.parse()?, + }) + } +} +impl Digestable for RequestCommitment { + fn update(&self, digest: &mut D) { + digest.update(&i64::to_be_bytes(self.timestamp)); + digest.update(&u64::to_be_bytes(self.nonce)); + digest.update(&u64::to_be_bytes(self.size)); + digest.update(&*self.blake3); + } +} +impl<'a> Commitment<&'a mut Request> for RequestCommitment { + async fn create(resource: &'a mut Request) -> Result { + use http_body_util::BodyExt; + + let body = std::mem::replace(resource.body_mut(), Body::empty()) + .collect() + .await + .with_kind(ErrorKind::Network)? + .to_bytes(); + let res = Self { + timestamp: SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or_else(|e| e.duration().as_secs() as i64 * -1), + nonce: rand::random(), + size: body.len() as u64, + blake3: Base64(*blake3::hash(&*body).as_bytes()), + }; + *resource.body_mut() = Body::from(body); + Ok(res) + } + async fn copy_to( + &self, + resource: &'a mut Request, + writer: W, + ) -> Result<(), Error> { + use tokio::io::AsyncReadExt; + + let mut body = StreamReader::new( + std::mem::replace(resource.body_mut(), Body::empty()) + .into_data_stream() + .map_err(std::io::Error::other), + ) + .take(self.size); + + let mut writer = VerifyingWriter::new( + writer, + Some((blake3::Hash::from_bytes(*self.blake3), self.size)), + ); + tokio::io::copy(&mut body, &mut writer).await?; + writer.verify().await?; + + Ok(()) + } +} diff --git a/core/startos/src/registry/signer/mod.rs b/core/startos/src/registry/signer/mod.rs new file mode 100644 index 000000000..99b23b88e --- /dev/null +++ b/core/startos/src/registry/signer/mod.rs @@ -0,0 +1,154 @@ +use std::collections::HashSet; +use std::str::FromStr; + +use clap::builder::ValueParserFactory; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; +use url::Url; + +use crate::prelude::*; +use crate::registry::signer::commitment::Digestable; +use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey, SignatureScheme}; +use crate::util::clap::FromStrParser; + +pub mod commitment; +pub mod sign; + +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct SignerInfo { + pub name: String, + pub contact: Vec, + pub keys: HashSet, +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +// TODO: better types +pub enum ContactInfo { + Email(String), + Matrix(String), + Website(#[ts(type = "string")] Url), +} +impl std::fmt::Display for ContactInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Email(e) => write!(f, "mailto:{e}"), + Self::Matrix(m) => write!(f, "https://matrix.to/#/{m}"), + Self::Website(w) => write!(f, "{w}"), + } + } +} +impl FromStr for ContactInfo { + type Err = Error; + fn from_str(s: &str) -> Result { + Ok(if let Some(s) = s.strip_prefix("mailto:") { + Self::Email(s.to_owned()) + } else if let Some(s) = s.strip_prefix("https://matrix.to/#/") { + Self::Matrix(s.to_owned()) + } else { + Self::Website(s.parse()?) + }) + } +} +impl ValueParserFactory for ContactInfo { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + Self::Parser::new() + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub enum AcceptSigners { + #[serde(skip)] + Accepted, + Signer(AnyVerifyingKey), + Any(Vec), + All(Vec), +} +impl AcceptSigners { + const fn null() -> Self { + Self::Any(Vec::new()) + } + pub fn flatten(self) -> Self { + match self { + Self::Any(mut s) | Self::All(mut s) if s.len() == 1 => s.swap_remove(0).flatten(), + s => s, + } + } + pub fn accepted(&self) -> bool { + match self { + Self::Accepted => true, + _ => false, + } + } + pub fn try_accept(self) -> Result<(), Error> { + if self.accepted() { + Ok(()) + } else { + Err(Error::new( + eyre!("signer(s) not accepted"), + ErrorKind::InvalidSignature, + )) + } + } + pub fn process_signature( + &mut self, + signer: &AnyVerifyingKey, + commitment: &impl Digestable, + context: &str, + signature: &AnySignature, + ) -> Result<(), Error> { + let mut res = Ok(()); + let new = match std::mem::replace(self, Self::null()) { + Self::Accepted => Self::Accepted, + Self::Signer(s) => { + if &s == signer { + res = signer + .scheme() + .verify_commitment(signer, commitment, context, signature); + Self::Accepted + } else { + Self::Signer(s) + } + } + Self::All(mut s) => { + res = s + .iter_mut() + .map(|s| s.process_signature(signer, commitment, context, signature)) + .collect(); + if s.iter().all(|s| s.accepted()) { + Self::Accepted + } else { + Self::All(s) + } + } + Self::Any(mut s) => { + match s + .iter_mut() + .map(|s| { + s.process_signature(signer, commitment, context, signature)?; + Ok(s) + }) + .filter_ok(|s| s.accepted()) + .next() + { + Some(Ok(s)) => std::mem::replace(s, Self::null()), + Some(Err(e)) => { + res = Err(e); + Self::Any(s) + } + None => Self::Any(s), + } + } + }; + *self = new; + res + } +} diff --git a/core/startos/src/registry/signer/sign/ed25519.rs b/core/startos/src/registry/signer/sign/ed25519.rs new file mode 100644 index 000000000..3ec4c136e --- /dev/null +++ b/core/startos/src/registry/signer/sign/ed25519.rs @@ -0,0 +1,34 @@ +use ed25519_dalek::{Signature, SigningKey, VerifyingKey}; +use sha2::Sha512; + +use crate::prelude::*; +use crate::registry::signer::sign::SignatureScheme; + +pub struct Ed25519; +impl SignatureScheme for Ed25519 { + type SigningKey = SigningKey; + type VerifyingKey = VerifyingKey; + type Signature = Signature; + type Digest = Sha512; + fn new_digest(&self) -> Self::Digest { + ::new() + } + fn sign( + &self, + key: &Self::SigningKey, + digest: Self::Digest, + context: &str, + ) -> Result { + Ok(key.sign_prehashed(digest, Some(context.as_bytes()))?) + } + fn verify( + &self, + key: &Self::VerifyingKey, + digest: Self::Digest, + context: &str, + signature: &Self::Signature, + ) -> Result<(), Error> { + key.verify_prehashed_strict(digest, Some(context.as_bytes()), signature)?; + Ok(()) + } +} diff --git a/core/startos/src/registry/signer/sign/mod.rs b/core/startos/src/registry/signer/sign/mod.rs new file mode 100644 index 000000000..50576a198 --- /dev/null +++ b/core/startos/src/registry/signer/sign/mod.rs @@ -0,0 +1,348 @@ +use std::fmt::Display; +use std::str::FromStr; + +use ::ed25519::pkcs8::BitStringRef; +use clap::builder::ValueParserFactory; +use der::referenced::OwnedToRef; +use der::{Decode, Encode}; +use pkcs8::der::AnyRef; +use pkcs8::{PrivateKeyInfo, SubjectPublicKeyInfo}; +use serde::{Deserialize, Serialize}; +use sha2::Sha512; +use ts_rs::TS; + +use crate::prelude::*; +use crate::registry::signer::commitment::Digestable; +use crate::registry::signer::sign::ed25519::Ed25519; +use crate::util::clap::FromStrParser; +use crate::util::serde::{deserialize_from_str, serialize_display}; + +pub mod ed25519; + +pub trait SignatureScheme { + type SigningKey; + type VerifyingKey; + type Signature; + type Digest: digest::Update; + fn new_digest(&self) -> Self::Digest; + fn sign( + &self, + key: &Self::SigningKey, + digest: Self::Digest, + context: &str, + ) -> Result; + fn sign_commitment( + &self, + key: &Self::SigningKey, + commitment: &C, + context: &str, + ) -> Result { + let mut digest = self.new_digest(); + commitment.update(&mut digest); + self.sign(key, digest, context) + } + fn verify( + &self, + key: &Self::VerifyingKey, + digest: Self::Digest, + context: &str, + signature: &Self::Signature, + ) -> Result<(), Error>; + fn verify_commitment( + &self, + key: &Self::VerifyingKey, + commitment: &C, + context: &str, + signature: &Self::Signature, + ) -> Result<(), Error> { + let mut digest = self.new_digest(); + commitment.update(&mut digest); + self.verify(key, digest, context, signature) + } +} + +pub enum AnyScheme { + Ed25519(Ed25519), +} +impl From for AnyScheme { + fn from(value: Ed25519) -> Self { + Self::Ed25519(value) + } +} +impl SignatureScheme for AnyScheme { + type SigningKey = AnySigningKey; + type VerifyingKey = AnyVerifyingKey; + type Signature = AnySignature; + type Digest = AnyDigest; + fn new_digest(&self) -> Self::Digest { + match self { + Self::Ed25519(s) => AnyDigest::Sha512(s.new_digest()), + } + } + fn sign( + &self, + key: &Self::SigningKey, + digest: Self::Digest, + context: &str, + ) -> Result { + match (self, key, digest) { + (Self::Ed25519(s), AnySigningKey::Ed25519(key), AnyDigest::Sha512(digest)) => { + Ok(AnySignature::Ed25519(s.sign(key, digest, context)?)) + } + _ => Err(Error::new( + eyre!("mismatched signature algorithm"), + ErrorKind::InvalidSignature, + )), + } + } + fn verify( + &self, + key: &Self::VerifyingKey, + digest: Self::Digest, + context: &str, + signature: &Self::Signature, + ) -> Result<(), Error> { + match (self, key, digest, signature) { + ( + Self::Ed25519(s), + AnyVerifyingKey::Ed25519(key), + AnyDigest::Sha512(digest), + AnySignature::Ed25519(signature), + ) => s.verify(key, digest, context, signature), + _ => Err(Error::new( + eyre!("mismatched signature algorithm"), + ErrorKind::InvalidSignature, + )), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, TS)] +#[ts(export, type = "string")] +pub enum AnySigningKey { + Ed25519(::SigningKey), +} +impl AnySigningKey { + pub fn scheme(&self) -> AnyScheme { + match self { + Self::Ed25519(_) => AnyScheme::Ed25519(Ed25519), + } + } + pub fn verifying_key(&self) -> AnyVerifyingKey { + match self { + Self::Ed25519(k) => AnyVerifyingKey::Ed25519(k.into()), + } + } +} +impl<'a> TryFrom> for AnySigningKey { + type Error = pkcs8::Error; + fn try_from(value: PrivateKeyInfo<'a>) -> Result { + if value.algorithm == ed25519_dalek::pkcs8::ALGORITHM_ID { + Ok(Self::Ed25519(ed25519_dalek::SigningKey::try_from(value)?)) + } else { + Err(pkcs8::spki::Error::OidUnknown { + oid: value.algorithm.oid, + } + .into()) + } + } +} +impl pkcs8::EncodePrivateKey for AnySigningKey { + fn to_pkcs8_der(&self) -> pkcs8::Result { + match self { + Self::Ed25519(s) => s.to_pkcs8_der(), + } + } +} +impl FromStr for AnySigningKey { + type Err = Error; + fn from_str(s: &str) -> Result { + use pkcs8::DecodePrivateKey; + Self::from_pkcs8_pem(s).with_kind(ErrorKind::Deserialization) + } +} +impl Display for AnySigningKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use pkcs8::EncodePrivateKey; + f.write_str( + &self + .to_pkcs8_pem(pkcs8::LineEnding::LF) + .map_err(|_| std::fmt::Error)?, + ) + } +} +impl<'de> Deserialize<'de> for AnySigningKey { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserialize_from_str(deserializer) + } +} +impl Serialize for AnySigningKey { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serialize_display(self, serializer) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, TS)] +#[ts(export, type = "string")] +pub enum AnyVerifyingKey { + Ed25519(::VerifyingKey), +} +impl AnyVerifyingKey { + pub fn scheme(&self) -> AnyScheme { + match self { + Self::Ed25519(_) => AnyScheme::Ed25519(Ed25519), + } + } +} +impl<'a> TryFrom, BitStringRef<'a>>> for AnyVerifyingKey { + type Error = pkcs8::spki::Error; + fn try_from( + value: SubjectPublicKeyInfo, BitStringRef<'a>>, + ) -> Result { + if value.algorithm == ed25519_dalek::pkcs8::ALGORITHM_ID { + Ok(Self::Ed25519(ed25519_dalek::VerifyingKey::try_from(value)?)) + } else { + Err(pkcs8::spki::Error::OidUnknown { + oid: value.algorithm.oid, + }) + } + } +} +impl pkcs8::EncodePublicKey for AnyVerifyingKey { + fn to_public_key_der(&self) -> pkcs8::spki::Result { + match self { + Self::Ed25519(s) => s.to_public_key_der(), + } + } +} +impl FromStr for AnyVerifyingKey { + type Err = Error; + fn from_str(s: &str) -> Result { + use pkcs8::DecodePublicKey; + Self::from_public_key_pem(s).with_kind(ErrorKind::Deserialization) + } +} +impl Display for AnyVerifyingKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use pkcs8::EncodePublicKey; + f.write_str( + &self + .to_public_key_pem(pkcs8::LineEnding::LF) + .map_err(|_| std::fmt::Error)?, + ) + } +} +impl<'de> Deserialize<'de> for AnyVerifyingKey { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserialize_from_str(deserializer) + } +} +impl Serialize for AnyVerifyingKey { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serialize_display(self, serializer) + } +} +impl ValueParserFactory for AnyVerifyingKey { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + Self::Parser::new() + } +} + +#[derive(Clone, Debug)] +pub enum AnyDigest { + Sha512(Sha512), +} +impl digest::Update for AnyDigest { + fn update(&mut self, data: &[u8]) { + match self { + Self::Sha512(d) => digest::Update::update(d, data), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, TS)] +#[ts(export, type = "string")] +pub enum AnySignature { + Ed25519(::Signature), +} +impl FromStr for AnySignature { + type Err = Error; + fn from_str(s: &str) -> Result { + use der::DecodePem; + + #[derive(der::Sequence)] + struct AnySignatureDer { + alg: pkcs8::spki::AlgorithmIdentifierOwned, + sig: der::asn1::OctetString, + } + impl der::pem::PemLabel for AnySignatureDer { + const PEM_LABEL: &'static str = "SIGNATURE"; + } + + let der = AnySignatureDer::from_pem(s.as_bytes()).with_kind(ErrorKind::Deserialization)?; + if der.alg.oid == ed25519_dalek::pkcs8::ALGORITHM_ID.oid + && der.alg.parameters.owned_to_ref() == ed25519_dalek::pkcs8::ALGORITHM_ID.parameters + { + Ok(Self::Ed25519( + ed25519_dalek::Signature::from_slice(der.sig.as_bytes()) + .with_kind(ErrorKind::Deserialization)?, + )) + } else { + Err(pkcs8::spki::Error::OidUnknown { oid: der.alg.oid }) + .with_kind(ErrorKind::Deserialization) + } + } +} +impl Display for AnySignature { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use der::EncodePem; + + #[derive(der::Sequence)] + struct AnySignatureDer<'a> { + alg: pkcs8::AlgorithmIdentifierRef<'a>, + sig: der::asn1::OctetString, + } + impl<'a> der::pem::PemLabel for AnySignatureDer<'a> { + const PEM_LABEL: &'static str = "SIGNATURE"; + } + f.write_str( + &match self { + Self::Ed25519(s) => AnySignatureDer { + alg: ed25519_dalek::pkcs8::ALGORITHM_ID, + sig: der::asn1::OctetString::new(s.to_bytes()).map_err(|_| std::fmt::Error)?, + }, + } + .to_pem(der::pem::LineEnding::LF) + .map_err(|_| std::fmt::Error)?, + ) + } +} +impl<'de> Deserialize<'de> for AnySignature { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserialize_from_str(deserializer) + } +} +impl Serialize for AnySignature { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serialize_display(self, serializer) + } +} diff --git a/core/startos/src/rpc_continuations.rs b/core/startos/src/rpc_continuations.rs index 04b88ea8e..ce8bf43fd 100644 --- a/core/startos/src/rpc_continuations.rs +++ b/core/startos/src/rpc_continuations.rs @@ -10,20 +10,24 @@ use futures::future::BoxFuture; use helpers::TimedResource; use imbl_value::InternedString; use tokio::sync::Mutex; +use ts_rs::TS; #[allow(unused_imports)] use crate::prelude::*; use crate::util::clap::FromStrParser; use crate::util::new_guid; -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)] -pub struct RequestGuid(InternedString); -impl RequestGuid { +#[derive( + Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, TS, +)] +#[ts(type = "string")] +pub struct Guid(InternedString); +impl Guid { pub fn new() -> Self { Self(new_guid()) } - pub fn from(r: &str) -> Option { + pub fn from(r: &str) -> Option { if r.len() != 32 { return None; } @@ -32,21 +36,21 @@ impl RequestGuid { return None; } } - Some(RequestGuid(InternedString::intern(r))) + Some(Guid(InternedString::intern(r))) } } -impl AsRef for RequestGuid { +impl AsRef for Guid { fn as_ref(&self) -> &str { self.0.as_ref() } } -impl FromStr for RequestGuid { +impl FromStr for Guid { type Err = Error; fn from_str(s: &str) -> Result { Self::from(s).ok_or_else(|| Error::new(eyre!("invalid guid"), ErrorKind::Deserialization)) } } -impl ValueParserFactory for RequestGuid { +impl ValueParserFactory for Guid { type Parser = FromStrParser; fn value_parser() -> Self::Parser { Self::Parser::new() @@ -55,13 +59,10 @@ impl ValueParserFactory for RequestGuid { #[test] fn parse_guid() { - println!( - "{:?}", - RequestGuid::from(&format!("{}", RequestGuid::new())) - ) + println!("{:?}", Guid::from(&format!("{}", Guid::new()))) } -impl std::fmt::Display for RequestGuid { +impl std::fmt::Display for Guid { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } @@ -91,7 +92,7 @@ impl RpcContinuation { } } -pub struct RpcContinuations(Mutex>); +pub struct RpcContinuations(Mutex>); impl RpcContinuations { pub fn new() -> Self { RpcContinuations(Mutex::new(BTreeMap::new())) @@ -112,12 +113,12 @@ impl RpcContinuations { } #[instrument(skip_all)] - pub async fn add(&self, guid: RequestGuid, handler: RpcContinuation) { + pub async fn add(&self, guid: Guid, handler: RpcContinuation) { self.clean().await; self.0.lock().await.insert(guid, handler); } - pub async fn get_ws_handler(&self, guid: &RequestGuid) -> Option { + pub async fn get_ws_handler(&self, guid: &Guid) -> Option { let mut continuations = self.0.lock().await; if !matches!(continuations.get(guid), Some(RpcContinuation::WebSocket(_))) { return None; @@ -128,8 +129,8 @@ impl RpcContinuations { x.get().await } - pub async fn get_rest_handler(&self, guid: &RequestGuid) -> Option { - let mut continuations: tokio::sync::MutexGuard<'_, BTreeMap> = + pub async fn get_rest_handler(&self, guid: &Guid) -> Option { + let mut continuations: tokio::sync::MutexGuard<'_, BTreeMap> = self.0.lock().await; if !matches!(continuations.get(guid), Some(RpcContinuation::Rest(_))) { return None; diff --git a/core/startos/src/s9pk/v1/git_hash.rs b/core/startos/src/s9pk/git_hash.rs similarity index 52% rename from core/startos/src/s9pk/v1/git_hash.rs rename to core/startos/src/s9pk/git_hash.rs index b2990a111..02f83bf4a 100644 --- a/core/startos/src/s9pk/v1/git_hash.rs +++ b/core/startos/src/s9pk/git_hash.rs @@ -1,24 +1,35 @@ use std::path::Path; -use crate::Error; +use tokio::process::Command; + +use crate::prelude::*; +use crate::util::Invoke; #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct GitHash(String); impl GitHash { pub async fn from_path(path: impl AsRef) -> Result { - let hash = tokio::process::Command::new("git") - .args(["describe", "--always", "--abbrev=40", "--dirty=-modified"]) - .current_dir(path) - .output() - .await?; - if !hash.status.success() { - return Err(Error::new( - color_eyre::eyre::eyre!("Could not get hash: {}", String::from_utf8(hash.stderr)?), - crate::ErrorKind::Filesystem, - )); + let mut hash = String::from_utf8( + Command::new("git") + .arg("rev-parse") + .arg("HEAD") + .current_dir(&path) + .invoke(ErrorKind::Git) + .await?, + )?; + if Command::new("git") + .arg("diff-index") + .arg("--quiet") + .arg("HEAD") + .arg("--") + .invoke(ErrorKind::Git) + .await + .is_err() + { + hash += "-modified"; } - Ok(GitHash(String::from_utf8(hash.stdout)?)) + Ok(GitHash(hash)) } } diff --git a/core/startos/src/s9pk/merkle_archive/directory_contents.rs b/core/startos/src/s9pk/merkle_archive/directory_contents.rs index ebc46a60b..77c3582d9 100644 --- a/core/startos/src/s9pk/merkle_archive/directory_contents.rs +++ b/core/startos/src/s9pk/merkle_archive/directory_contents.rs @@ -12,11 +12,12 @@ use itertools::Itertools; use tokio::io::AsyncRead; use crate::prelude::*; -use crate::s9pk::merkle_archive::sink::{Sink, TrackingWriter}; +use crate::s9pk::merkle_archive::sink::Sink; use crate::s9pk::merkle_archive::source::{ArchiveSource, DynFileSource, FileSource, Section}; use crate::s9pk::merkle_archive::write_queue::WriteQueue; use crate::s9pk::merkle_archive::{varint, Entry, EntryContents}; -use crate::util::io::ParallelBlake3Writer; +use crate::util::io::{ParallelBlake3Writer, TrackingIO}; +use crate::CAP_10_MiB; #[derive(Clone)] pub struct DirectoryContents { @@ -151,7 +152,7 @@ impl DirectoryContents { Ok(()) } } -impl DirectoryContents> { +impl DirectoryContents> { #[instrument(skip_all)] pub fn deserialize<'a>( source: &'a S, @@ -181,7 +182,7 @@ impl DirectoryContents> { let mut entries = OrdMap::new(); for _ in 0..len { let name = varint::deserialize_varstring(&mut toc_reader).await?; - let entry = Entry::deserialize(source, &mut toc_reader).await?; + let entry = Entry::deserialize(source.clone(), &mut toc_reader).await?; entries.insert(name.into(), entry); } @@ -202,7 +203,7 @@ impl DirectoryContents> { .boxed() } } -impl DirectoryContents { +impl DirectoryContents { pub fn filter(&mut self, filter: impl Fn(&Path) -> bool) -> Result<(), Error> { for k in self.keys().cloned().collect::>() { let path = Path::new(&*k); @@ -239,8 +240,7 @@ impl DirectoryContents { #[instrument(skip_all)] pub fn sighash<'a>(&'a self) -> BoxFuture<'a, Result> { async move { - let mut hasher = - TrackingWriter::new(0, ParallelBlake3Writer::new(super::hash::BUFFER_CAPACITY)); + let mut hasher = TrackingIO::new(0, ParallelBlake3Writer::new(CAP_10_MiB)); let mut sig_contents = OrdMap::new(); for (name, entry) in &**self { sig_contents.insert(name.clone(), entry.to_missing().await?); @@ -280,6 +280,7 @@ impl DirectoryContents { Ok(()) } + pub fn into_dyn(self) -> DirectoryContents { DirectoryContents { contents: self diff --git a/core/startos/src/s9pk/merkle_archive/file_contents.rs b/core/startos/src/s9pk/merkle_archive/file_contents.rs index a2d19ed88..c34193e31 100644 --- a/core/startos/src/s9pk/merkle_archive/file_contents.rs +++ b/core/startos/src/s9pk/merkle_archive/file_contents.rs @@ -2,9 +2,10 @@ use blake3::Hash; use tokio::io::AsyncRead; use crate::prelude::*; -use crate::s9pk::merkle_archive::sink::{Sink, TrackingWriter}; +use crate::s9pk::merkle_archive::sink::Sink; use crate::s9pk::merkle_archive::source::{ArchiveSource, DynFileSource, FileSource, Section}; -use crate::util::io::ParallelBlake3Writer; +use crate::util::io::{ParallelBlake3Writer, TrackingIO}; +use crate::CAP_10_MiB; #[derive(Debug, Clone)] pub struct FileContents(S); @@ -19,7 +20,7 @@ impl FileContents { impl FileContents> { #[instrument(skip_all)] pub async fn deserialize( - source: &S, + source: S, header: &mut (impl AsyncRead + Unpin + Send), size: u64, ) -> Result { @@ -34,8 +35,7 @@ impl FileContents> { } impl FileContents { pub async fn hash(&self) -> Result<(Hash, u64), Error> { - let mut hasher = - TrackingWriter::new(0, ParallelBlake3Writer::new(super::hash::BUFFER_CAPACITY)); + let mut hasher = TrackingIO::new(0, ParallelBlake3Writer::new(CAP_10_MiB)); self.serialize_body(&mut hasher, None).await?; let size = hasher.position(); let hash = hasher.into_inner().finalize().await?; diff --git a/core/startos/src/s9pk/merkle_archive/hash.rs b/core/startos/src/s9pk/merkle_archive/hash.rs index fc635c435..c7ad470e4 100644 --- a/core/startos/src/s9pk/merkle_archive/hash.rs +++ b/core/startos/src/s9pk/merkle_archive/hash.rs @@ -6,8 +6,7 @@ use tokio_util::either::Either; use crate::prelude::*; use crate::util::io::{ParallelBlake3Writer, TeeWriter}; - -pub const BUFFER_CAPACITY: usize = 10 * 1024 * 1024; // 10MiB +use crate::CAP_10_MiB; #[pin_project::pin_project] pub struct VerifyingWriter { @@ -21,8 +20,8 @@ impl VerifyingWriter { writer: if verify.is_some() { Either::Left(TeeWriter::new( w, - ParallelBlake3Writer::new(BUFFER_CAPACITY), - BUFFER_CAPACITY, + ParallelBlake3Writer::new(CAP_10_MiB), + CAP_10_MiB, )) } else { Either::Right(w) diff --git a/core/startos/src/s9pk/merkle_archive/mod.rs b/core/startos/src/s9pk/merkle_archive/mod.rs index 3863ceeaf..1c0d6b786 100644 --- a/core/startos/src/s9pk/merkle_archive/mod.rs +++ b/core/startos/src/s9pk/merkle_archive/mod.rs @@ -7,11 +7,16 @@ use sha2::{Digest, Sha512}; use tokio::io::AsyncRead; use crate::prelude::*; +use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; +use crate::registry::signer::sign::ed25519::Ed25519; +use crate::registry::signer::sign::SignatureScheme; use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; use crate::s9pk::merkle_archive::file_contents::FileContents; use crate::s9pk::merkle_archive::sink::Sink; use crate::s9pk::merkle_archive::source::{ArchiveSource, DynFileSource, FileSource, Section}; use crate::s9pk::merkle_archive::write_queue::WriteQueue; +use crate::util::serde::Base64; +use crate::CAP_1_MiB; pub mod directory_contents; pub mod file_contents; @@ -70,12 +75,13 @@ impl MerkleArchive { self.contents.sort_by(sort_by) } } -impl MerkleArchive> { +impl MerkleArchive> { #[instrument(skip_all)] pub async fn deserialize( source: &S, context: &str, header: &mut (impl AsyncRead + Unpin + Send), + commitment: Option<&MerkleArchiveCommitment>, ) -> Result { use tokio::io::AsyncReadExt; @@ -101,6 +107,32 @@ impl MerkleArchive> { &signature, )?; + if let Some(MerkleArchiveCommitment { + root_sighash, + root_maxsize, + }) = commitment + { + if sighash.as_bytes() != &**root_sighash { + return Err(Error::new( + eyre!("merkle root mismatch"), + ErrorKind::InvalidSignature, + )); + } + if max_size > *root_maxsize { + return Err(Error::new( + eyre!("merkle root directory max size too large"), + ErrorKind::InvalidSignature, + )); + } + } else { + if max_size > CAP_1_MiB as u64 { + return Err(Error::new( + eyre!("merkle root directory max size over 1MiB, cancelling download in case of DOS attack"), + ErrorKind::InvalidSignature, + )); + } + } + let contents = DirectoryContents::deserialize(source, header, (sighash, max_size)).await?; Ok(Self { @@ -109,38 +141,50 @@ impl MerkleArchive> { }) } } -impl MerkleArchive { +impl MerkleArchive { pub async fn update_hashes(&mut self, only_missing: bool) -> Result<(), Error> { self.contents.update_hashes(only_missing).await } pub fn filter(&mut self, filter: impl Fn(&Path) -> bool) -> Result<(), Error> { self.contents.filter(filter) } + pub async fn commitment(&self) -> Result { + let root_maxsize = match self.signer { + Signer::Signed(_, _, s, _) => s, + _ => self.contents.toc_size(), + }; + let root_sighash = self.contents.sighash().await?; + Ok(MerkleArchiveCommitment { + root_sighash: Base64(*root_sighash.as_bytes()), + root_maxsize, + }) + } + pub async fn signature(&self) -> Result { + match &self.signer { + Signer::Signed(_, s, _, _) => Ok(*s), + Signer::Signer(k, context) => { + Ed25519.sign_commitment(k, &self.commitment().await?, context) + } + } + } #[instrument(skip_all)] pub async fn serialize(&self, w: &mut W, verify: bool) -> Result<(), Error> { use tokio::io::AsyncWriteExt; - let sighash = self.contents.sighash().await?; - let size = self.contents.toc_size(); + let commitment = self.commitment().await?; - let (pubkey, signature, max_size) = match &self.signer { - Signer::Signed(pubkey, signature, max_size, _) => (*pubkey, *signature, *max_size), - Signer::Signer(s, context) => ( - s.into(), - ed25519_dalek::SigningKey::sign_prehashed( - s, - Sha512::new_with_prefix(sighash.as_bytes()) - .chain_update(&u64::to_be_bytes(size)), - Some(context.as_bytes()), - )?, - size, - ), + let (pubkey, signature) = match &self.signer { + Signer::Signed(pubkey, signature, _, _) => (*pubkey, *signature), + Signer::Signer(s, context) => { + (s.into(), Ed25519.sign_commitment(s, &commitment, context)?) + } }; w.write_all(pubkey.as_bytes()).await?; w.write_all(&signature.to_bytes()).await?; - w.write_all(sighash.as_bytes()).await?; - w.write_all(&u64::to_be_bytes(max_size)).await?; + w.write_all(&*commitment.root_sighash).await?; + w.write_all(&u64::to_be_bytes(commitment.root_maxsize)) + .await?; let mut next_pos = w.current_position().await?; next_pos += DirectoryContents::::header_size(); self.contents.serialize_header(next_pos, w).await?; @@ -216,11 +260,10 @@ impl Entry { + self.contents.header_size() } } -impl Entry {} -impl Entry> { +impl Entry> { #[instrument(skip_all)] pub async fn deserialize( - source: &S, + source: S, header: &mut (impl AsyncRead + Unpin + Send), ) -> Result { use tokio::io::AsyncReadExt; @@ -241,24 +284,19 @@ impl Entry> { }) } } -impl Entry { +impl Entry { pub fn filter(&mut self, filter: impl Fn(&Path) -> bool) -> Result<(), Error> { if let EntryContents::Directory(d) = &mut self.contents { d.filter(filter)?; } Ok(()) } - pub async fn read_file_to_vec(&self) -> Result, Error> { - match self.as_contents() { - EntryContents::File(f) => Ok(f.to_vec(self.hash).await?), - EntryContents::Directory(_) => Err(Error::new( - eyre!("expected file, found directory"), - ErrorKind::ParseS9pk, - )), - EntryContents::Missing => { - Err(Error::new(eyre!("entry is missing"), ErrorKind::ParseS9pk)) - } + pub async fn update_hash(&mut self, only_missing: bool) -> Result<(), Error> { + if let EntryContents::Directory(d) = &mut self.contents { + d.update_hashes(only_missing).await?; } + self.hash = Some(self.contents.hash().await?); + Ok(()) } pub async fn to_missing(&self) -> Result { let hash = if let Some(hash) = self.hash { @@ -271,13 +309,6 @@ impl Entry { contents: EntryContents::Missing, }) } - pub async fn update_hash(&mut self, only_missing: bool) -> Result<(), Error> { - if let EntryContents::Directory(d) = &mut self.contents { - d.update_hashes(only_missing).await?; - } - self.hash = Some(self.contents.hash().await?); - Ok(()) - } #[instrument(skip_all)] pub async fn serialize_header( &self, @@ -302,6 +333,20 @@ impl Entry { } } } +impl Entry { + pub async fn read_file_to_vec(&self) -> Result, Error> { + match self.as_contents() { + EntryContents::File(f) => Ok(f.to_vec(self.hash).await?), + EntryContents::Directory(_) => Err(Error::new( + eyre!("expected file, found directory"), + ErrorKind::ParseS9pk, + )), + EntryContents::Missing => { + Err(Error::new(eyre!("entry is missing"), ErrorKind::ParseS9pk)) + } + } + } +} #[derive(Debug, Clone)] pub enum EntryContents { @@ -329,10 +374,10 @@ impl EntryContents { matches!(self, &EntryContents::Directory(_)) } } -impl EntryContents> { +impl EntryContents> { #[instrument(skip_all)] pub async fn deserialize( - source: &S, + source: S, header: &mut (impl AsyncRead + Unpin + Send), (hash, size): (Hash, u64), ) -> Result { @@ -346,7 +391,7 @@ impl EntryContents> { FileContents::deserialize(source, header, size).await?, )), 2 => Ok(Self::Directory( - DirectoryContents::deserialize(source, header, (hash, size)).await?, + DirectoryContents::deserialize(&source, header, (hash, size)).await?, )), id => Err(Error::new( eyre!("Unknown type id {id} found in MerkleArchive"), @@ -355,7 +400,7 @@ impl EntryContents> { } } } -impl EntryContents { +impl EntryContents { pub async fn hash(&self) -> Result<(Hash, u64), Error> { match self { Self::Missing => Err(Error::new( @@ -366,6 +411,15 @@ impl EntryContents { Self::Directory(d) => Ok((d.sighash().await?, d.toc_size())), } } + pub fn into_dyn(self) -> EntryContents { + match self { + Self::Missing => EntryContents::Missing, + Self::File(f) => EntryContents::File(f.into_dyn()), + Self::Directory(d) => EntryContents::Directory(d.into_dyn()), + } + } +} +impl EntryContents { #[instrument(skip_all)] pub async fn serialize_header( &self, @@ -381,11 +435,4 @@ impl EntryContents { Self::Directory(d) => Some(d.serialize_header(position, w).await?), }) } - pub fn into_dyn(self) -> EntryContents { - match self { - Self::Missing => EntryContents::Missing, - Self::File(f) => EntryContents::File(f.into_dyn()), - Self::Directory(d) => EntryContents::Directory(d.into_dyn()), - } - } } diff --git a/core/startos/src/s9pk/merkle_archive/sink.rs b/core/startos/src/s9pk/merkle_archive/sink.rs index ec1431b8e..5357eb2d6 100644 --- a/core/startos/src/s9pk/merkle_archive/sink.rs +++ b/core/startos/src/s9pk/merkle_archive/sink.rs @@ -1,6 +1,7 @@ use tokio::io::{AsyncSeek, AsyncWrite}; use crate::prelude::*; +use crate::util::io::TrackingIO; #[async_trait::async_trait] pub trait Sink: AsyncWrite + Unpin + Send { @@ -17,57 +18,8 @@ impl Sink for S { } #[async_trait::async_trait] -impl Sink for TrackingWriter { +impl Sink for TrackingIO { async fn current_position(&mut self) -> Result { - Ok(self.position) - } -} - -#[pin_project::pin_project] -pub struct TrackingWriter { - position: u64, - #[pin] - writer: W, -} -impl TrackingWriter { - pub fn new(start: u64, w: W) -> Self { - Self { - position: start, - writer: w, - } - } - pub fn position(&self) -> u64 { - self.position - } - pub fn into_inner(self) -> W { - self.writer - } -} -impl AsyncWrite for TrackingWriter { - fn poll_write( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - buf: &[u8], - ) -> std::task::Poll> { - let this = self.project(); - match this.writer.poll_write(cx, buf) { - std::task::Poll::Ready(Ok(written)) => { - *this.position += written as u64; - std::task::Poll::Ready(Ok(written)) - } - a => a, - } - } - fn poll_flush( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - self.project().writer.poll_flush(cx) - } - fn poll_shutdown( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - self.project().writer.poll_shutdown(cx) + Ok(self.position()) } } diff --git a/core/startos/src/s9pk/merkle_archive/source/http.rs b/core/startos/src/s9pk/merkle_archive/source/http.rs index 738c480ed..fda9d32ed 100644 --- a/core/startos/src/s9pk/merkle_archive/source/http.rs +++ b/core/startos/src/s9pk/merkle_archive/source/http.rs @@ -1,23 +1,25 @@ +use std::collections::BTreeSet; +use std::pin::Pin; +use std::sync::{Arc, Mutex}; +use std::task::Poll; + use bytes::Bytes; -use futures::stream::BoxStream; -use futures::{StreamExt, TryStreamExt}; +use futures::{Stream, StreamExt, TryStreamExt}; use reqwest::header::{ACCEPT_RANGES, CONTENT_LENGTH, RANGE}; use reqwest::{Client, Url}; -use tokio::io::{AsyncRead, AsyncReadExt, Take}; +use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf, Take}; use tokio_util::io::StreamReader; use crate::prelude::*; use crate::s9pk::merkle_archive::source::ArchiveSource; +use crate::util::io::TrackingIO; +use crate::util::Apply; -#[derive(Clone)] pub struct HttpSource { url: Url, client: Client, size: Option, - range_support: Result< - (), - (), // Arc>> - >, + range_support: Result<(), Arc>>>>, } impl HttpSource { pub async fn new(client: Client, url: Url) -> Result { @@ -32,7 +34,8 @@ impl HttpSource { .headers() .get(ACCEPT_RANGES) .and_then(|s| s.to_str().ok()) - == Some("bytes"); + == Some("bytes") + && false; let size = head .headers() .get(CONTENT_LENGTH) @@ -45,53 +48,141 @@ impl HttpSource { range_support: if range_support { Ok(()) } else { - Err(()) // Err(Arc::new(Mutex::new(None))) + Err(Arc::new(Mutex::new(BTreeSet::new()))) }, }) } } impl ArchiveSource for HttpSource { - type Reader = Take; + type Reader = HttpReader; async fn size(&self) -> Option { self.size } + async fn fetch_all(&self) -> Result { + Ok(StreamReader::new( + self.client + .get(self.url.clone()) + .send() + .await + .with_kind(ErrorKind::Network)? + .error_for_status() + .with_kind(ErrorKind::Network)? + .bytes_stream() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) + .apply(boxed), + )) + } async fn fetch(&self, position: u64, size: u64) -> Result { - match self.range_support { - Ok(_) => Ok(HttpReader::Range(StreamReader::new(if size > 0 { - self.client - .get(self.url.clone()) - .header(RANGE, format!("bytes={}-{}", position, position + size - 1)) - .send() - .await - .with_kind(ErrorKind::Network)? - .error_for_status() - .with_kind(ErrorKind::Network)? - .bytes_stream() - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) - .boxed() - } else { - futures::stream::empty().boxed() - })) - .take(size)), - _ => todo!(), + match &self.range_support { + Ok(_) => Ok(HttpReader::Range( + StreamReader::new(if size > 0 { + self.client + .get(self.url.clone()) + .header(RANGE, format!("bytes={}-{}", position, position + size - 1)) + .send() + .await + .with_kind(ErrorKind::Network)? + .error_for_status() + .with_kind(ErrorKind::Network)? + .bytes_stream() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) + .apply(boxed) + } else { + futures::stream::empty().apply(boxed) + }) + .take(size), + )), + Err(pool) => { + fn get_reader_for( + pool: &Arc>>>, + position: u64, + ) -> Option> { + let mut lock = pool.lock().unwrap(); + let pos = lock.range(..position).last()?.position(); + lock.take(&pos) + } + let reader = get_reader_for(pool, position); + let mut reader = if let Some(reader) = reader { + reader + } else { + TrackingIO::new( + 0, + StreamReader::new( + self.client + .get(self.url.clone()) + .send() + .await + .with_kind(ErrorKind::Network)? + .error_for_status() + .with_kind(ErrorKind::Network)? + .bytes_stream() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) + .apply(boxed), + ), + ) + }; + if reader.position() < position { + let to_skip = position - reader.position(); + tokio::io::copy(&mut (&mut reader).take(to_skip), &mut tokio::io::sink()) + .await?; + } + Ok(HttpReader::Rangeless { + pool: pool.clone(), + reader: Some(reader.take(size)), + }) + } } } } -#[pin_project::pin_project(project = HttpReaderProj)] +type BoxStream<'a, T> = Pin + Send + Sync + 'a>>; +fn boxed<'a, T>(stream: impl Stream + Send + Sync + 'a) -> BoxStream<'a, T> { + Box::pin(stream) +} +type HttpBodyReader = StreamReader>, Bytes>; + +#[pin_project::pin_project(project = HttpReaderProj, PinnedDrop)] pub enum HttpReader { - Range(#[pin] StreamReader>, Bytes>), - // Rangeless(#[pin] RangelessReader), + Range(#[pin] Take), + Rangeless { + pool: Arc>>>, + #[pin] + reader: Option>>, + }, } impl AsyncRead for HttpReader { fn poll_read( - self: std::pin::Pin<&mut Self>, + self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, - buf: &mut tokio::io::ReadBuf<'_>, - ) -> std::task::Poll> { + buf: &mut ReadBuf<'_>, + ) -> Poll> { match self.project() { HttpReaderProj::Range(r) => r.poll_read(cx, buf), - // HttpReaderProj::Rangeless(r) => r.poll_read(cx, buf), + HttpReaderProj::Rangeless { mut reader, .. } => { + let mut finished = false; + if let Some(reader) = reader.as_mut().as_pin_mut() { + let start = buf.filled().len(); + futures::ready!(reader.poll_read(cx, buf)?); + finished = start == buf.filled().len(); + } + if finished { + reader.take(); + } + Poll::Ready(Ok(())) + } + } + } +} +#[pin_project::pinned_drop] +impl PinnedDrop for HttpReader { + fn drop(self: Pin<&mut Self>) { + match self.project() { + HttpReaderProj::Range(_) => (), + HttpReaderProj::Rangeless { pool, mut reader } => { + if let Some(reader) = reader.take() { + pool.lock().unwrap().insert(reader.into_inner()); + } + } } } } diff --git a/core/startos/src/s9pk/merkle_archive/source/mod.rs b/core/startos/src/s9pk/merkle_archive/source/mod.rs index 6e3b1e584..f6922d109 100644 --- a/core/startos/src/s9pk/merkle_archive/source/mod.rs +++ b/core/startos/src/s9pk/merkle_archive/source/mod.rs @@ -1,3 +1,4 @@ +use std::ops::Deref; use std::path::PathBuf; use std::sync::Arc; @@ -13,7 +14,7 @@ use crate::s9pk::merkle_archive::hash::VerifyingWriter; pub mod http; pub mod multi_cursor_file; -pub trait FileSource: Clone + Send + Sync + Sized + 'static { +pub trait FileSource: Send + Sync + Sized + 'static { type Reader: AsyncRead + Unpin + Send; fn size(&self) -> impl Future> + Send; fn reader(&self) -> impl Future> + Send; @@ -61,6 +62,29 @@ pub trait FileSource: Clone + Send + Sync + Sized + 'static { } } +impl FileSource for Arc { + type Reader = T::Reader; + async fn size(&self) -> Result { + self.deref().size().await + } + async fn reader(&self) -> Result { + self.deref().reader().await + } + async fn copy(&self, w: &mut W) -> Result<(), Error> { + self.deref().copy(w).await + } + async fn copy_verify( + &self, + w: &mut W, + verify: Option<(Hash, u64)>, + ) -> Result<(), Error> { + self.deref().copy_verify(w, verify).await + } + async fn to_vec(&self, verify: Option<(Hash, u64)>) -> Result, Error> { + self.deref().to_vec(verify).await + } +} + #[derive(Clone)] pub struct DynFileSource(Arc); impl DynFileSource { @@ -155,16 +179,28 @@ impl FileSource for Arc<[u8]> { } } -pub trait ArchiveSource: Clone + Send + Sync + Sized + 'static { +pub trait ArchiveSource: Send + Sync + Sized + 'static { type Reader: AsyncRead + Unpin + Send; fn size(&self) -> impl Future> + Send { async { None } } + fn fetch_all( + &self, + ) -> impl Future> + Send; fn fetch( &self, position: u64, size: u64, ) -> impl Future> + Send; + fn copy_all_to( + &self, + w: &mut W, + ) -> impl Future> + Send { + async move { + tokio::io::copy(&mut self.fetch_all().await?, w).await?; + Ok(()) + } + } fn copy_to( &self, position: u64, @@ -176,17 +212,47 @@ pub trait ArchiveSource: Clone + Send + Sync + Sized + 'static { Ok(()) } } - fn section(&self, position: u64, size: u64) -> Section { + fn section(self, position: u64, size: u64) -> Section { Section { - source: self.clone(), + source: self, position, size, } } } +impl ArchiveSource for Arc { + type Reader = T::Reader; + async fn size(&self) -> Option { + self.deref().size().await + } + async fn fetch_all(&self) -> Result { + self.deref().fetch_all().await + } + async fn fetch(&self, position: u64, size: u64) -> Result { + self.deref().fetch(position, size).await + } + async fn copy_all_to( + &self, + w: &mut W, + ) -> Result<(), Error> { + self.deref().copy_all_to(w).await + } + async fn copy_to( + &self, + position: u64, + size: u64, + w: &mut W, + ) -> Result<(), Error> { + self.deref().copy_to(position, size, w).await + } +} + impl ArchiveSource for Arc<[u8]> { type Reader = tokio::io::Take>; + async fn fetch_all(&self) -> Result { + Ok(std::io::Cursor::new(self.clone())) + } async fn fetch(&self, position: u64, size: u64) -> Result { use tokio::io::AsyncReadExt; diff --git a/core/startos/src/s9pk/merkle_archive/source/multi_cursor_file.rs b/core/startos/src/s9pk/merkle_archive/source/multi_cursor_file.rs index 00188559a..9cc162f0e 100644 --- a/core/startos/src/s9pk/merkle_archive/source/multi_cursor_file.rs +++ b/core/startos/src/s9pk/merkle_archive/source/multi_cursor_file.rs @@ -1,18 +1,35 @@ use std::io::SeekFrom; use std::os::fd::{AsRawFd, RawFd}; use std::path::{Path, PathBuf}; +use std::pin::Pin; use std::sync::Arc; +use std::task::Poll; use tokio::fs::File; -use tokio::io::{AsyncRead, AsyncReadExt}; +use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf, Take}; use tokio::sync::{Mutex, OwnedMutexGuard}; use crate::disk::mount::filesystem::loop_dev::LoopDev; use crate::prelude::*; use crate::s9pk::merkle_archive::source::{ArchiveSource, Section}; -fn path_from_fd(fd: RawFd) -> PathBuf { - Path::new("/proc/self/fd").join(fd.to_string()) +fn path_from_fd(fd: RawFd) -> Result { + #[cfg(target_os = "linux")] + let path = Path::new("/proc/self/fd").join(fd.to_string()); + #[cfg(target_os = "macos")] // here be dragons + let path = unsafe { + let mut buf = [0u8; libc::PATH_MAX as usize]; + if libc::fcntl(fd, libc::F_GETPATH, buf.as_mut_ptr().cast::()) == -1 { + return Err(std::io::Error::last_os_error().into()); + } + Path::new( + &*std::ffi::CStr::from_bytes_until_nul(&buf) + .with_kind(ErrorKind::Utf8)? + .to_string_lossy(), + ) + .to_owned() + }; + Ok(path) } #[derive(Clone)] @@ -21,18 +38,26 @@ pub struct MultiCursorFile { file: Arc>, } impl MultiCursorFile { - fn path(&self) -> PathBuf { + fn path(&self) -> Result { path_from_fd(self.fd) } pub async fn open(fd: &impl AsRawFd) -> Result { - let fd = fd.as_raw_fd(); - Ok(Self { - fd, - file: Arc::new(Mutex::new(File::open(path_from_fd(fd)).await?)), - }) + let f = File::open(path_from_fd(fd.as_raw_fd())?).await?; + Ok(Self::from(f)) + } + pub async fn cursor(&self) -> Result { + Ok(FileCursor( + if let Ok(file) = self.file.clone().try_lock_owned() { + file + } else { + Arc::new(Mutex::new(File::open(self.path()?).await?)) + .try_lock_owned() + .expect("freshly created") + }, + )) } pub async fn blake3_mmap(&self) -> Result { - let path = self.path(); + let path = self.path()?; tokio::task::spawn_blocking(move || { let mut hasher = blake3::Hasher::new(); hasher.update_mmap_rayon(path)?; @@ -52,76 +77,44 @@ impl From for MultiCursorFile { } #[pin_project::pin_project] -pub struct FileSectionReader { - #[pin] - file: OwnedMutexGuard, - remaining: u64, -} -impl AsyncRead for FileSectionReader { +pub struct FileCursor(#[pin] OwnedMutexGuard); +impl AsyncRead for FileCursor { fn poll_read( - self: std::pin::Pin<&mut Self>, + self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, - buf: &mut tokio::io::ReadBuf<'_>, - ) -> std::task::Poll> { + buf: &mut ReadBuf<'_>, + ) -> Poll> { let this = self.project(); - if *this.remaining == 0 { - return std::task::Poll::Ready(Ok(())); - } - let before = buf.filled().len() as u64; - let res = std::pin::Pin::new(&mut (&mut **this.file.get_mut()).take(*this.remaining)) - .poll_read(cx, buf); - *this.remaining = this - .remaining - .saturating_sub(buf.filled().len() as u64 - before); - res + Pin::new(&mut (&mut **this.0.get_mut())).poll_read(cx, buf) } } impl ArchiveSource for MultiCursorFile { - type Reader = FileSectionReader; + type Reader = Take; async fn size(&self) -> Option { - tokio::fs::metadata(self.path()).await.ok().map(|m| m.len()) + tokio::fs::metadata(self.path().ok()?) + .await + .ok() + .map(|m| m.len()) + } + async fn fetch_all(&self) -> Result { + use tokio::io::AsyncSeekExt; + + let mut file = self.cursor().await?; + file.0.seek(SeekFrom::Start(0)).await?; + Ok(file) } async fn fetch(&self, position: u64, size: u64) -> Result { use tokio::io::AsyncSeekExt; - let mut file = if let Ok(file) = self.file.clone().try_lock_owned() { - file - } else { - #[cfg(target_os = "linux")] - let file = File::open(self.path()).await?; - #[cfg(target_os = "macos")] // here be dragons - let file = unsafe { - let mut buf = [0u8; libc::PATH_MAX as usize]; - if libc::fcntl( - self.fd, - libc::F_GETPATH, - buf.as_mut_ptr().cast::(), - ) == -1 - { - return Err(std::io::Error::last_os_error().into()); - } - File::open( - &*std::ffi::CStr::from_bytes_until_nul(&buf) - .with_kind(ErrorKind::Utf8)? - .to_string_lossy(), - ) - .await? - }; - Arc::new(Mutex::new(file)) - .try_lock_owned() - .expect("freshly created") - }; - file.seek(SeekFrom::Start(position)).await?; - Ok(Self::Reader { - file, - remaining: size, - }) + let mut file = self.cursor().await?; + file.0.seek(SeekFrom::Start(position)).await?; + Ok(file.take(size)) } } impl From<&Section> for LoopDev { fn from(value: &Section) -> Self { - LoopDev::new(value.source.path(), value.position, value.size) + LoopDev::new(value.source.path().unwrap(), value.position, value.size) } } diff --git a/core/startos/src/s9pk/merkle_archive/test.rs b/core/startos/src/s9pk/merkle_archive/test.rs index e902aafd9..861f3b04c 100644 --- a/core/startos/src/s9pk/merkle_archive/test.rs +++ b/core/startos/src/s9pk/merkle_archive/test.rs @@ -8,9 +8,9 @@ use ed25519_dalek::SigningKey; use crate::prelude::*; use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; use crate::s9pk::merkle_archive::file_contents::FileContents; -use crate::s9pk::merkle_archive::sink::TrackingWriter; use crate::s9pk::merkle_archive::source::FileSource; use crate::s9pk::merkle_archive::{Entry, EntryContents, MerkleArchive}; +use crate::util::io::TrackingIO; /// Creates a MerkleArchive (a1) with the provided files at the provided paths. NOTE: later files can overwrite previous files/directories at the same path /// Tests: @@ -60,10 +60,15 @@ fn test(files: Vec<(PathBuf, String)>) -> Result<(), Error> { .block_on(async move { a1.update_hashes(true).await?; let mut s1 = Vec::new(); - a1.serialize(&mut TrackingWriter::new(0, &mut s1), true) - .await?; + a1.serialize(&mut TrackingIO::new(0, &mut s1), true).await?; let s1: Arc<[u8]> = s1.into(); - let a2 = MerkleArchive::deserialize(&s1, "test", &mut Cursor::new(s1.clone())).await?; + let a2 = MerkleArchive::deserialize( + &s1, + "test", + &mut Cursor::new(s1.clone()), + Some(&a1.commitment().await?), + ) + .await?; for (path, content) in check_set { match a2 @@ -88,8 +93,7 @@ fn test(files: Vec<(PathBuf, String)>) -> Result<(), Error> { } let mut s2 = Vec::new(); - a2.serialize(&mut TrackingWriter::new(0, &mut s2), true) - .await?; + a2.serialize(&mut TrackingIO::new(0, &mut s2), true).await?; let s2: Arc<[u8]> = s2.into(); ensure_code!(s1 == s2, ErrorKind::Pack, "s1 does not match s2"); diff --git a/core/startos/src/s9pk/merkle_archive/write_queue.rs b/core/startos/src/s9pk/merkle_archive/write_queue.rs index 9496d5e83..4e1bb3a73 100644 --- a/core/startos/src/s9pk/merkle_archive/write_queue.rs +++ b/core/startos/src/s9pk/merkle_archive/write_queue.rs @@ -30,6 +30,8 @@ impl<'a, S: FileSource> WriteQueue<'a, S> { self.queue.push_back(entry); Ok(res) } +} +impl<'a, S: FileSource + Clone> WriteQueue<'a, S> { pub async fn serialize(&mut self, w: &mut W, verify: bool) -> Result<(), Error> { loop { let Some(next) = self.queue.pop_front() else { diff --git a/core/startos/src/s9pk/mod.rs b/core/startos/src/s9pk/mod.rs index 83924293a..fcf9379a0 100644 --- a/core/startos/src/s9pk/mod.rs +++ b/core/startos/src/s9pk/mod.rs @@ -1,3 +1,4 @@ +pub mod git_hash; pub mod merkle_archive; pub mod rpc; pub mod v1; diff --git a/core/startos/src/s9pk/rpc.rs b/core/startos/src/s9pk/rpc.rs index ca149c3e3..89cfc9b5a 100644 --- a/core/startos/src/s9pk/rpc.rs +++ b/core/startos/src/s9pk/rpc.rs @@ -172,14 +172,14 @@ async fn add_image( .join(&arch) .join(&id) .with_extension("env"), - Entry::file(DynFileSource::new(Arc::from(Vec::from(env)))), + Entry::file(DynFileSource::new(Arc::<[u8]>::from(Vec::from(env)))), )?; archive.contents_mut().insert_path( Path::new("images") .join(&arch) .join(&id) .with_extension("json"), - Entry::file(DynFileSource::new(Arc::from( + Entry::file(DynFileSource::new(Arc::<[u8]>::from( serde_json::to_vec(&serde_json::json!({ "workdir": workdir })) diff --git a/core/startos/src/s9pk/v1/manifest.rs b/core/startos/src/s9pk/v1/manifest.rs index 264843766..845d8f773 100644 --- a/core/startos/src/s9pk/v1/manifest.rs +++ b/core/startos/src/s9pk/v1/manifest.rs @@ -3,35 +3,38 @@ use std::path::{Path, PathBuf}; use emver::VersionRange; use imbl_value::InOMap; +use indexmap::IndexMap; pub use models::PackageId; -use models::VolumeId; +use models::{ActionId, HealthCheckId, ImageId, VolumeId}; use serde::{Deserialize, Serialize}; use url::Url; -use super::git_hash::GitHash; use crate::prelude::*; +use crate::s9pk::git_hash::GitHash; use crate::s9pk::manifest::{Alerts, Description, HardwareRequirements}; -use crate::util::Version; +use crate::util::serde::{Duration, IoFormat}; +use crate::util::VersionString; use crate::version::{Current, VersionT}; -fn current_version() -> Version { +fn current_version() -> VersionString { Current::new().semver().into() } -#[derive(Clone, Debug, Deserialize, Serialize, HasModel)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] -#[model = "Model"] pub struct Manifest { #[serde(default = "current_version")] - pub eos_version: Version, + pub eos_version: VersionString, pub id: PackageId, #[serde(default)] pub git_hash: Option, + pub title: String, + pub version: VersionString, + pub description: Description, #[serde(default)] pub assets: Assets, - pub title: String, - pub version: Version, - pub description: Description, + #[serde(default)] + pub build: Option>, pub release_notes: String, pub license: String, // type of license pub wrapper_repo: Url, @@ -41,10 +44,23 @@ pub struct Manifest { pub donation_url: Option, #[serde(default)] pub alerts: Alerts, + pub main: PackageProcedure, + pub health_checks: HealthChecks, + pub config: Option, + pub properties: Option, pub volumes: BTreeMap, + // #[serde(default)] + // pub interfaces: Interfaces, + // #[serde(default)] + pub backup: BackupActions, + #[serde(default)] + pub migrations: Migrations, + #[serde(default)] + pub actions: BTreeMap, + // #[serde(default)] + // pub permissions: Permissions, #[serde(default)] pub dependencies: BTreeMap, - pub config: Option>, #[serde(default)] pub replaces: Vec, @@ -53,6 +69,123 @@ pub struct Manifest { pub hardware_requirements: HardwareRequirements, } +impl Manifest { + pub fn package_procedures(&self) -> impl Iterator { + use std::iter::once; + let main = once(&self.main); + let cfg_get = self.config.as_ref().map(|a| &a.get).into_iter(); + let cfg_set = self.config.as_ref().map(|a| &a.set).into_iter(); + let props = self.properties.iter(); + let backups = vec![&self.backup.create, &self.backup.restore].into_iter(); + let migrations = self + .migrations + .to + .values() + .chain(self.migrations.from.values()); + let actions = self.actions.values().map(|a| &a.implementation); + main.chain(cfg_get) + .chain(cfg_set) + .chain(props) + .chain(backups) + .chain(migrations) + .chain(actions) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, HasModel)] +#[serde(rename_all = "kebab-case")] +#[serde(tag = "type")] +#[model = "Model"] +pub enum PackageProcedure { + Docker(DockerProcedure), + Script(Value), +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct DockerProcedure { + pub image: ImageId, + #[serde(default)] + pub system: bool, + pub entrypoint: String, + #[serde(default)] + pub args: Vec, + #[serde(default)] + pub inject: bool, + #[serde(default)] + pub mounts: BTreeMap, + #[serde(default)] + pub io_format: Option, + #[serde(default)] + pub sigterm_timeout: Option, + #[serde(default)] + pub shm_size_mb: Option, // TODO: use postfix sizing? like 1k vs 1m vs 1g + #[serde(default)] + pub gpu_acceleration: bool, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct HealthChecks(pub BTreeMap); + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct HealthCheck { + pub name: String, + pub success_message: Option, + #[serde(flatten)] + implementation: PackageProcedure, + pub timeout: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ConfigActions { + pub get: PackageProcedure, + pub set: PackageProcedure, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct BackupActions { + pub create: PackageProcedure, + pub restore: PackageProcedure, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct Migrations { + pub from: IndexMap, + pub to: IndexMap, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct Action { + pub name: String, + pub description: String, + #[serde(default)] + pub warning: Option, + pub implementation: PackageProcedure, + // pub allowed_statuses: Vec, + // #[serde(default)] + // pub input_spec: ConfigSpec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct DepInfo { + pub version: VersionRange, + pub requirement: DependencyRequirement, + pub description: Option, + #[serde(default)] + pub config: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub struct DependencyConfig { + check: PackageProcedure, + auto_configure: PackageProcedure, +} + #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] #[serde(tag = "type")] @@ -67,15 +200,6 @@ impl DependencyRequirement { } } -#[derive(Clone, Debug, Deserialize, Serialize, HasModel)] -#[serde(rename_all = "kebab-case")] -#[model = "Model"] -pub struct DepInfo { - pub version: VersionRange, - pub requirement: DependencyRequirement, - pub description: Option, -} - #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct Assets { diff --git a/core/startos/src/s9pk/v1/mod.rs b/core/startos/src/s9pk/v1/mod.rs index 173921a75..9910d0adb 100644 --- a/core/startos/src/s9pk/v1/mod.rs +++ b/core/startos/src/s9pk/v1/mod.rs @@ -6,7 +6,6 @@ use ts_rs::TS; pub mod builder; pub mod docker; -pub mod git_hash; pub mod header; pub mod manifest; pub mod reader; diff --git a/core/startos/src/s9pk/v1/reader.rs b/core/startos/src/s9pk/v1/reader.rs index 4288ac0b0..7437fdaa9 100644 --- a/core/startos/src/s9pk/v1/reader.rs +++ b/core/startos/src/s9pk/v1/reader.rs @@ -20,7 +20,7 @@ use super::header::{FileSection, Header, TableOfContents}; use super::SIG_CONTEXT; use crate::prelude::*; use crate::s9pk::v1::docker::DockerReader; -use crate::util::Version; +use crate::util::VersionString; #[pin_project::pin_project] #[derive(Debug)] @@ -83,11 +83,11 @@ impl<'a, R: AsyncSeek + Unpin> AsyncSeek for ReadHandle<'a, R> { pub struct ImageTag { pub package_id: PackageId, pub image_id: ImageId, - pub version: Version, + pub version: VersionString, } impl ImageTag { #[instrument(skip_all)] - pub fn validate(&self, id: &PackageId, version: &Version) -> Result<(), Error> { + pub fn validate(&self, id: &PackageId, version: &VersionString) -> Result<(), Error> { if id != &self.package_id { return Err(Error::new( eyre!( diff --git a/core/startos/src/s9pk/v2/compat.rs b/core/startos/src/s9pk/v2/compat.rs index 109ea43b4..5d4ad2f44 100644 --- a/core/startos/src/s9pk/v2/compat.rs +++ b/core/startos/src/s9pk/v2/compat.rs @@ -1,9 +1,10 @@ -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use std::io::Cursor; use std::path::{Path, PathBuf}; use std::sync::Arc; use itertools::Itertools; +use models::ImageId; use tokio::fs::File; use tokio::io::{AsyncRead, AsyncSeek, AsyncWriteExt}; use tokio::process::Command; @@ -16,7 +17,7 @@ use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::{FileSource, Section}; use crate::s9pk::merkle_archive::{Entry, MerkleArchive}; use crate::s9pk::rpc::SKIP_ENV; -use crate::s9pk::v1::manifest::Manifest as ManifestV1; +use crate::s9pk::v1::manifest::{Manifest as ManifestV1, PackageProcedure}; use crate::s9pk::v1::reader::S9pkReader; use crate::s9pk::v2::{S9pk, SIG_CONTEXT}; use crate::util::io::TmpDir; @@ -72,6 +73,17 @@ impl S9pk> { let manifest = from_value::(manifest_raw.clone())?; let mut new_manifest = Manifest::from(manifest.clone()); + let images: BTreeMap = manifest + .package_procedures() + .filter_map(|p| { + if let PackageProcedure::Docker(p) = p { + Some((p.image.clone(), p.system)) + } else { + None + } + }) + .collect(); + // LICENSE.md let license: Arc<[u8]> = reader.license().await?.to_vec().await?.into(); archive.insert_path( @@ -109,61 +121,14 @@ impl S9pk> { .input(Some(&mut reader.docker_images(&arch).await?)) .invoke(ErrorKind::Docker) .await?; - #[derive(serde::Deserialize)] - #[serde(rename_all = "PascalCase")] - struct DockerImagesOut { - repository: Option, - tag: Option, - #[serde(default)] - names: Vec, - } - for image in { - #[cfg(feature = "docker")] - let images = std::str::from_utf8( - &Command::new(CONTAINER_TOOL) - .arg("images") - .arg("--format=json") - .invoke(ErrorKind::Docker) - .await?, - )? - .lines() - .map(|l| serde_json::from_str::(l)) - .collect::, _>>() - .with_kind(ErrorKind::Deserialization)? - .into_iter(); - #[cfg(not(feature = "docker"))] - let images = serde_json::from_slice::>( - &Command::new(CONTAINER_TOOL) - .arg("images") - .arg("--format=json") - .invoke(ErrorKind::Docker) - .await?, - ) - .with_kind(ErrorKind::Deserialization)? - .into_iter(); - images - } - .flat_map(|i| { - if let (Some(repository), Some(tag)) = (i.repository, i.tag) { - vec![format!("{repository}:{tag}")] + for (image, system) in &images { + new_manifest.images.insert(image.clone()); + let sqfs_path = images_dir.join(image).with_extension("squashfs"); + let image_name = if *system { + format!("start9/{}:latest", image) } else { - i.names - .into_iter() - .filter_map(|i| i.strip_prefix("docker.io/").map(|s| s.to_owned())) - .collect() - } - }) - .filter_map(|i| { - i.strip_suffix(&format!(":{}", manifest.version)) - .map(|s| s.to_owned()) - }) - .filter_map(|i| { - i.strip_prefix(&format!("start9/{}/", manifest.id)) - .map(|s| s.to_owned()) - }) { - new_manifest.images.insert(image.parse()?); - let sqfs_path = images_dir.join(&image).with_extension("squashfs"); - let image_name = format!("start9/{}/{}:{}", manifest.id, image, manifest.version); + format!("start9/{}/{}:{}", manifest.id, image, manifest.version) + }; let id = String::from_utf8( Command::new(CONTAINER_TOOL) .arg("create") @@ -323,6 +288,7 @@ impl S9pk> { Ok(S9pk::deserialize( &MultiCursorFile::from(File::open(destination.as_ref()).await?), + None, false, ) .await?) @@ -337,8 +303,7 @@ impl From for Manifest { title: value.title, version: value.version, release_notes: value.release_notes, - license: value.license, - replaces: value.replaces, + license: value.license.into(), wrapper_repo: value.wrapper_repo, upstream_repo: value.upstream_repo, support_site: value.support_site.unwrap_or_else(|| default_url.clone()), diff --git a/core/startos/src/s9pk/v2/manifest.rs b/core/startos/src/s9pk/v2/manifest.rs index ee2358b13..b5ada621b 100644 --- a/core/startos/src/s9pk/v2/manifest.rs +++ b/core/startos/src/s9pk/v2/manifest.rs @@ -2,6 +2,7 @@ use std::collections::{BTreeMap, BTreeSet}; use color_eyre::eyre::eyre; use helpers::const_true; +use imbl_value::InternedString; pub use models::PackageId; use models::{ImageId, VolumeId}; use serde::{Deserialize, Serialize}; @@ -10,12 +11,12 @@ use url::Url; use crate::dependencies::Dependencies; use crate::prelude::*; -use crate::s9pk::v1::git_hash::GitHash; +use crate::s9pk::git_hash::GitHash; use crate::util::serde::Regex; -use crate::util::Version; +use crate::util::VersionString; use crate::version::{Current, VersionT}; -fn current_version() -> Version { +fn current_version() -> VersionString { Current::new().semver().into() } @@ -26,12 +27,10 @@ fn current_version() -> Version { pub struct Manifest { pub id: PackageId, pub title: String, - #[ts(type = "string")] - pub version: Version, + pub version: VersionString, pub release_notes: String, - pub license: String, // type of license - #[serde(default)] - pub replaces: Vec, + #[ts(type = "string")] + pub license: InternedString, // type of license #[ts(type = "string")] pub wrapper_repo: Url, #[ts(type = "string")] @@ -56,8 +55,7 @@ pub struct Manifest { #[ts(type = "string | null")] pub git_hash: Option, #[serde(default = "current_version")] - #[ts(type = "string")] - pub os_version: Version, + pub os_version: VersionString, #[serde(default = "const_true")] pub has_config: bool, } @@ -68,9 +66,11 @@ pub struct Manifest { pub struct HardwareRequirements { #[serde(default)] #[ts(type = "{ [key: string]: string }")] - device: BTreeMap, - ram: Option, - pub arch: Option>, + pub device: BTreeMap, + #[ts(type = "number | null")] + pub ram: Option, + #[ts(type = "string[] | null")] + pub arch: Option>, } #[derive(Clone, Debug, Deserialize, Serialize, TS)] diff --git a/core/startos/src/s9pk/v2/mod.rs b/core/startos/src/s9pk/v2/mod.rs index 9452b7a72..e206c8553 100644 --- a/core/startos/src/s9pk/v2/mod.rs +++ b/core/startos/src/s9pk/v2/mod.rs @@ -7,6 +7,7 @@ use models::{mime, DataUrl, PackageId}; use tokio::fs::File; use crate::prelude::*; +use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; use crate::s9pk::manifest::Manifest; use crate::s9pk::merkle_archive::file_contents::FileContents; use crate::s9pk::merkle_archive::sink::Sink; @@ -96,7 +97,7 @@ impl S9pk { } } -impl S9pk { +impl S9pk { pub async fn new(archive: MerkleArchive, size: Option) -> Result { let manifest = extract_manifest(&archive).await?; Ok(Self { @@ -173,9 +174,13 @@ impl S9pk { } } -impl S9pk> { +impl S9pk> { #[instrument(skip_all)] - pub async fn deserialize(source: &S, apply_filter: bool) -> Result { + pub async fn deserialize( + source: &S, + commitment: Option<&MerkleArchiveCommitment>, + apply_filter: bool, + ) -> Result { use tokio::io::AsyncReadExt; let mut header = source @@ -193,7 +198,8 @@ impl S9pk> { "Invalid Magic or Unexpected Version" ); - let mut archive = MerkleArchive::deserialize(source, SIG_CONTEXT, &mut header).await?; + let mut archive = + MerkleArchive::deserialize(source, SIG_CONTEXT, &mut header, commitment).await?; if apply_filter { archive.filter(filter)?; @@ -211,7 +217,7 @@ impl S9pk> { } impl S9pk { pub async fn from_file(file: File, apply_filter: bool) -> Result { - Self::deserialize(&MultiCursorFile::from(file), apply_filter).await + Self::deserialize(&MultiCursorFile::from(file), None, apply_filter).await } pub async fn open( path: impl AsRef, diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index ec8566133..e6ccdd131 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -22,7 +22,7 @@ use crate::install::PKG_ARCHIVE_DIR; use crate::lxc::ContainerId; use crate::prelude::*; use crate::progress::{NamedProgress, Progress}; -use crate::rpc_continuations::RequestGuid; +use crate::rpc_continuations::Guid; use crate::s9pk::S9pk; use crate::service::service_map::InstallProgressHandles; use crate::service::transition::TransitionKind; @@ -254,7 +254,7 @@ impl Service { pub async fn install( ctx: RpcContext, s9pk: S9pk, - src_version: Option, + src_version: Option, progress: Option, ) -> Result { let manifest = s9pk.as_manifest().clone(); @@ -339,7 +339,7 @@ impl Service { Ok(()) } - pub async fn uninstall(self, target_version: Option) -> Result<(), Error> { + pub async fn uninstall(self, target_version: Option) -> Result<(), Error> { self.seed .persistent_container .execute(ProcedureName::Uninit, to_value(&target_version)?, None) // TODO timeout @@ -513,7 +513,7 @@ pub struct ConnectParams { pub async fn connect_rpc( ctx: RpcContext, ConnectParams { id }: ConnectParams, -) -> Result { +) -> Result { let id_ref = &id; crate::lxc::connect( &ctx, diff --git a/core/startos/src/service/service_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs index 44d17da30..31c5a8026 100644 --- a/core/startos/src/service/service_effect_handler.rs +++ b/core/startos/src/service/service_effect_handler.rs @@ -1336,12 +1336,15 @@ async fn set_dependencies( }; let (icon, title) = match async { let remote_s9pk = S9pk::deserialize( - &HttpSource::new( - ctx.ctx.client.clone(), - registry_url - .join(&format!("package/v2/{}.s9pk?spec={}", dep_id, version_spec))?, - ) - .await?, + &Arc::new( + HttpSource::new( + ctx.ctx.client.clone(), + registry_url + .join(&format!("package/v2/{}.s9pk?spec={}", dep_id, version_spec))?, + ) + .await?, + ), + None, // TODO true, ) .await?; diff --git a/core/startos/src/service/service_map.rs b/core/startos/src/service/service_map.rs index 02b0538ec..ba9188f32 100644 --- a/core/startos/src/service/service_map.rs +++ b/core/startos/src/service/service_map.rs @@ -89,7 +89,7 @@ impl ServiceMap { } #[instrument(skip_all)] - pub async fn install( + pub async fn install( &self, ctx: RpcContext, mut s9pk: S9pk, diff --git a/core/startos/src/update/mod.rs b/core/startos/src/update/mod.rs index 5bcf8445d..be908e776 100644 --- a/core/startos/src/update/mod.rs +++ b/core/startos/src/update/mod.rs @@ -26,8 +26,10 @@ use crate::progress::{ use crate::registry::asset::RegistryAsset; use crate::registry::context::{RegistryContext, RegistryUrlParams}; use crate::registry::os::index::OsVersionInfo; -use crate::registry::signer::FileValidator; -use crate::rpc_continuations::{RequestGuid, RpcContinuation}; +use crate::registry::os::SIG_CONTEXT; +use crate::registry::signer::commitment::blake3::Blake3Commitment; +use crate::registry::signer::commitment::Commitment; +use crate::rpc_continuations::{Guid, RpcContinuation}; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::sound::{ CIRCLE_OF_5THS_SHORT, UPDATE_FAILED_1, UPDATE_FAILED_2, UPDATE_FAILED_3, UPDATE_FAILED_4, @@ -54,7 +56,7 @@ pub struct UpdateSystemRes { #[ts(type = "string | null")] target: Option, #[ts(type = "string | null")] - progress: Option, + progress: Option, } /// An user/ daemon would call this to update the system to the latest version and do the updates available, @@ -83,7 +85,7 @@ pub async fn update_system( let target = maybe_do_update(ctx.clone(), registry, target.unwrap_or(VersionRange::Any)).await?; let progress = if progress && target.is_some() { - let guid = RequestGuid::new(); + let guid = Guid::new(); ctx.clone() .rpc_continuations .add( @@ -246,12 +248,12 @@ async fn maybe_do_update( )); } - let validator = asset.validate(asset.signature_info.all_signers())?; + asset.validate(SIG_CONTEXT, asset.all_signers())?; let mut progress = FullProgressTracker::new(); let progress_handle = progress.handle(); let mut download_phase = progress_handle.add_phase("Downloading File".into(), Some(100)); - download_phase.set_total(validator.size()?); + download_phase.set_total(asset.commitment.size); let reverify_phase = progress_handle.add_phase("Reverifying File".into(), Some(10)); let sync_boot_phase = progress_handle.add_phase("Syncing Boot Files".into(), Some(1)); let finalize_phase = progress_handle.add_phase("Finalizing Update".into(), Some(1)); @@ -300,7 +302,6 @@ async fn maybe_do_update( tokio::spawn(async move { let res = do_update( ctx.clone(), - validator, asset, UpdateProgressHandles { progress_handle, @@ -382,8 +383,7 @@ struct UpdateProgressHandles { #[instrument(skip_all)] async fn do_update( ctx: RpcContext, - validator: FileValidator, - asset: RegistryAsset, + asset: RegistryAsset, UpdateProgressHandles { progress_handle, mut download_phase, @@ -394,21 +394,23 @@ async fn do_update( ) -> Result<(), Error> { download_phase.start(); let path = Path::new("/media/startos/images") - .join(hex::encode(&validator.blake3()?.as_bytes()[..16])) + .join(hex::encode(&asset.commitment.hash[..16])) .with_extension("rootfs"); let mut dst = AtomicFile::new(&path, None::<&Path>) .await .with_kind(ErrorKind::Filesystem)?; let mut download_writer = download_phase.writer(&mut *dst); asset - .download(ctx.client.clone(), &mut download_writer, &validator) + .download(ctx.client.clone(), &mut download_writer) .await?; let (_, mut download_phase) = download_writer.into_inner(); + dst.sync_all().await?; download_phase.complete(); reverify_phase.start(); - validator - .validate_file(&MultiCursorFile::open(&*dst).await?) + asset + .commitment + .check(&MultiCursorFile::open(&*dst).await?) .await?; dst.save().await.with_kind(ErrorKind::Filesystem)?; reverify_phase.complete(); diff --git a/core/startos/src/upload.rs b/core/startos/src/upload.rs index 65494b1e4..f28d81799 100644 --- a/core/startos/src/upload.rs +++ b/core/startos/src/upload.rs @@ -9,18 +9,18 @@ use futures::{FutureExt, StreamExt}; use http::header::CONTENT_LENGTH; use http::StatusCode; use tokio::fs::File; -use tokio::io::{AsyncWrite, AsyncWriteExt}; +use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; use tokio::sync::watch; use crate::context::RpcContext; use crate::prelude::*; -use crate::rpc_continuations::{RequestGuid, RpcContinuation}; +use crate::rpc_continuations::{Guid, RpcContinuation}; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::ArchiveSource; use crate::util::io::TmpDir; -pub async fn upload(ctx: &RpcContext) -> Result<(RequestGuid, UploadingFile), Error> { - let guid = RequestGuid::new(); +pub async fn upload(ctx: &RpcContext) -> Result<(Guid, UploadingFile), Error> { + let guid = Guid::new(); let (mut handle, file) = UploadingFile::new().await?; ctx.rpc_continuations .add( @@ -120,22 +120,44 @@ impl Progress { .and_then(|a| a.expected_size) } async fn ready_for(watch: &mut watch::Receiver, size: u64) -> Result<(), Error> { - if let Some(e) = watch - .wait_for(|progress| progress.error.is_some() || progress.written >= size) + match &*watch + .wait_for(|progress| { + progress.error.is_some() + || progress.written >= size + || progress.expected_size.map_or(false, |e| e < size) + }) .await .map_err(|_| { Error::new( eyre!("failed to determine upload progress"), ErrorKind::Network, ) - })? - .error - .as_ref() - .map(|e| e.clone_output()) - { - Err(e) - } else { - Ok(()) + })? { + Progress { error: Some(e), .. } => Err(e.clone_output()), + Progress { + expected_size: Some(e), + .. + } if *e < size => Err(Error::new( + eyre!("file size is less than requested"), + ErrorKind::Network, + )), + _ => Ok(()), + } + } + async fn ready(watch: &mut watch::Receiver) -> Result<(), Error> { + match &*watch + .wait_for(|progress| { + progress.error.is_some() || Some(progress.written) == progress.expected_size + }) + .await + .map_err(|_| { + Error::new( + eyre!("failed to determine upload progress"), + ErrorKind::Network, + ) + })? { + Progress { error: Some(e), .. } => Err(e.clone_output()), + _ => Ok(()), } } fn complete(&mut self) -> bool { @@ -156,13 +178,25 @@ impl Progress { )); true } - Self { error, .. } if error.is_none() => { + Self { + error, + expected_size: Some(_), + .. + } if error.is_none() => { *error = Some(Error::new( eyre!("Connection closed or timed out before full file received"), ErrorKind::Network, )); true } + Self { + expected_size, + written, + .. + } if expected_size.is_none() => { + *expected_size = Some(*written); + true + } _ => false, } } @@ -204,6 +238,10 @@ impl ArchiveSource for UploadingFile { async fn size(&self) -> Option { Progress::expected_size(&mut self.progress.clone()).await } + async fn fetch_all(&self) -> Result { + Progress::ready(&mut self.progress.clone()).await?; + self.file.fetch_all().await + } async fn fetch(&self, position: u64, size: u64) -> Result { Progress::ready_for(&mut self.progress.clone(), position + size).await?; self.file.fetch(position, size).await diff --git a/core/startos/src/util/io.rs b/core/startos/src/util/io.rs index 75ab2c708..16bafd8f6 100644 --- a/core/startos/src/util/io.rs +++ b/core/startos/src/util/io.rs @@ -1,4 +1,4 @@ -use std::collections::VecDeque; +use std::collections::{BTreeSet, VecDeque}; use std::future::Future; use std::io::Cursor; use std::os::unix::prelude::MetadataExt; @@ -19,7 +19,7 @@ use tokio::io::{ duplex, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, DuplexStream, ReadBuf, WriteHalf, }; use tokio::net::TcpStream; -use tokio::sync::Notify; +use tokio::sync::{Notify, OwnedMutexGuard}; use tokio::time::{Instant, Sleep}; use crate::prelude::*; @@ -804,7 +804,7 @@ pub struct TeeWriter { #[pin] writer2: W2, } -impl TeeWriter { +impl TeeWriter { pub fn new(writer1: W1, writer2: W2, capacity: usize) -> Self { Self { capacity, @@ -815,7 +815,6 @@ impl TeeWriter { } } } - impl TeeWriter { pub async fn into_inner(mut self) -> Result<(W1, W2), Error> { self.flush().await?; @@ -1007,3 +1006,114 @@ impl AsyncWrite for ParallelBlake3Writer { Poll::Pending } } + +#[pin_project::pin_project] +pub struct TrackingIO { + position: u64, + #[pin] + io: T, +} +impl TrackingIO { + pub fn new(start: u64, io: T) -> Self { + Self { + position: start, + io, + } + } + pub fn position(&self) -> u64 { + self.position + } + pub fn into_inner(self) -> T { + self.io + } +} +impl AsyncWrite for TrackingIO { + fn poll_write( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + let this = self.project(); + let written = futures::ready!(this.io.poll_write(cx, buf)?); + *this.position += written as u64; + Poll::Ready(Ok(written)) + } + fn poll_flush( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.project().io.poll_flush(cx) + } + fn poll_shutdown( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.project().io.poll_shutdown(cx) + } +} +impl AsyncRead for TrackingIO { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let this = self.project(); + let start = buf.filled().len(); + futures::ready!(this.io.poll_read(cx, buf)?); + *this.position += (buf.filled().len() - start) as u64; + Poll::Ready(Ok(())) + } +} +impl std::cmp::PartialEq for TrackingIO { + fn eq(&self, other: &Self) -> bool { + self.position.eq(&other.position) + } +} +impl std::cmp::Eq for TrackingIO {} +impl std::cmp::PartialOrd for TrackingIO { + fn partial_cmp(&self, other: &Self) -> Option { + self.position.partial_cmp(&other.position) + } +} +impl std::cmp::Ord for TrackingIO { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.position.cmp(&other.position) + } +} +impl std::borrow::Borrow for TrackingIO { + fn borrow(&self) -> &u64 { + &self.position + } +} + +pub struct MutexIO(OwnedMutexGuard); +impl AsyncRead for MutexIO { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + Pin::new(&mut *self.get_mut().0).poll_read(cx, buf) + } +} +impl AsyncWrite for MutexIO { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> Poll> { + Pin::new(&mut *self.get_mut().0).poll_write(cx, buf) + } + fn poll_flush( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + Pin::new(&mut *self.get_mut().0).poll_flush(cx) + } + fn poll_shutdown( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + Pin::new(&mut *self.get_mut().0).poll_shutdown(cx) + } +} diff --git a/core/startos/src/util/lshw.rs b/core/startos/src/util/lshw.rs index 2a658a8c8..eb5293d89 100644 --- a/core/startos/src/util/lshw.rs +++ b/core/startos/src/util/lshw.rs @@ -1,12 +1,13 @@ use models::{Error, ResultExt}; use serde::{Deserialize, Serialize}; use tokio::process::Command; +use ts_rs::TS; use crate::util::Invoke; const KNOWN_CLASSES: &[&str] = &["processor", "display"]; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, TS)] #[serde(tag = "class")] #[serde(rename_all = "camelCase")] pub enum LshwDevice { @@ -28,12 +29,12 @@ impl LshwDevice { } } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, TS)] pub struct LshwProcessor { pub product: String, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize, TS)] pub struct LshwDisplay { pub product: String, } diff --git a/core/startos/src/util/mod.rs b/core/startos/src/util/mod.rs index ec64c6f97..aa17b6d7a 100644 --- a/core/startos/src/util/mod.rs +++ b/core/startos/src/util/mod.rs @@ -15,7 +15,7 @@ use helpers::canonicalize; pub use helpers::NonDetachingJoinHandle; use imbl_value::InternedString; use lazy_static::lazy_static; -pub use models::Version; +pub use models::VersionString; use pin_project::pin_project; use sha2::Digest; use tokio::fs::File; diff --git a/core/startos/src/util/rpc.rs b/core/startos/src/util/rpc.rs index 7d2356877..e80aac6cb 100644 --- a/core/startos/src/util/rpc.rs +++ b/core/startos/src/util/rpc.rs @@ -1,3 +1,4 @@ +use std::path::Path; use clap::Parser; use rpc_toolkit::{from_fn_async, Context, ParentHandler}; @@ -9,9 +10,11 @@ use crate::context::CliContext; use crate::prelude::*; use crate::s9pk::merkle_archive::source::http::HttpSource; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; -use crate::s9pk::merkle_archive::source::{ArchiveSource, DynFileSource, FileSource}; +use crate::s9pk::merkle_archive::source::ArchiveSource; use crate::util::io::ParallelBlake3Writer; use crate::util::serde::Base16; +use crate::util::Apply; +use crate::CAP_10_MiB; pub fn util() -> ParentHandler { ParentHandler::new().subcommand("b3sum", from_fn_async(b3sum)) @@ -28,26 +31,29 @@ pub async fn b3sum( ctx: CliContext, B3sumParams { file, allow_mmap }: B3sumParams, ) -> Result, Error> { - let source = if let Ok(url) = file.parse::() { + async fn b3sum_source(source: S) -> Result, Error> { + let mut hasher = ParallelBlake3Writer::new(CAP_10_MiB); + source.copy_all_to(&mut hasher).await?; + hasher.finalize().await.map(|h| *h.as_bytes()).map(Base16) + } + async fn b3sum_file( + path: impl AsRef, + allow_mmap: bool, + ) -> Result, Error> { + let file = MultiCursorFile::from(File::open(path).await?); + if allow_mmap { + return file.blake3_mmap().await.map(|h| *h.as_bytes()).map(Base16); + } + b3sum_source(file).await + } + if let Ok(url) = file.parse::() { if url.scheme() == "file" { - let file = MultiCursorFile::from(File::open(url.path()).await?); - if allow_mmap { - return file.blake3_mmap().await.map(|h| *h.as_bytes()).map(Base16); - } - DynFileSource::new(file.section( - 0, - file.size().await.ok_or_else(|| { - Error::new(eyre!("failed to get file size"), ErrorKind::Filesystem) - })?, - )) + b3sum_file(url.path(), allow_mmap).await } else if url.scheme() == "http" || url.scheme() == "https" { - let file = HttpSource::new(ctx.client.clone(), url).await?; - DynFileSource::new(file.section( - 0, - file.size().await.ok_or_else(|| { - Error::new(eyre!("failed to get file size"), ErrorKind::Filesystem) - })?, - )) + HttpSource::new(ctx.client.clone(), url) + .await? + .apply(b3sum_source) + .await } else { return Err(Error::new( eyre!("unknown scheme: {}", url.scheme()), @@ -55,18 +61,6 @@ pub async fn b3sum( )); } } else { - let file = MultiCursorFile::from(File::open(file).await?); - if allow_mmap { - return file.blake3_mmap().await.map(|h| *h.as_bytes()).map(Base16); - } - DynFileSource::new(file.section( - 0, - file.size().await.ok_or_else(|| { - Error::new(eyre!("failed to get file size"), ErrorKind::Filesystem) - })?, - )) - }; - let mut hasher = ParallelBlake3Writer::new(crate::s9pk::merkle_archive::hash::BUFFER_CAPACITY); - source.copy(&mut hasher).await?; - hasher.finalize().await.map(|h| *h.as_bytes()).map(Base16) + b3sum_file(file, allow_mmap).await + } } diff --git a/core/startos/src/util/serde.rs b/core/startos/src/util/serde.rs index 9696f9c91..382ef2814 100644 --- a/core/startos/src/util/serde.rs +++ b/core/startos/src/util/serde.rs @@ -22,6 +22,7 @@ use ts_rs::TS; use super::IntoDoubleEndedIterator; use crate::prelude::*; +use crate::util::Apply; use crate::util::clap::FromStrParser; pub fn deserialize_from_str< @@ -999,6 +1000,11 @@ impl> std::fmt::Display for Base16 { #[derive(TS)] #[ts(type = "string", concrete(T = Vec))] pub struct Base32(pub T); +impl> std::fmt::Display for Base32 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + base32::encode(base32::Alphabet::RFC4648 { padding: true }, self.0.as_ref()).fmt(f) + } +} impl<'de, T: TryFrom>> Deserialize<'de> for Base32 { fn deserialize(deserializer: D) -> Result where @@ -1022,32 +1028,35 @@ impl> Serialize for Base32 { where S: Serializer, { - serializer.serialize_str(&base32::encode( - base32::Alphabet::RFC4648 { padding: true }, - self.0.as_ref(), - )) - } -} -impl> std::fmt::Display for Base32 { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - base32::encode(base32::Alphabet::RFC4648 { padding: true }, self.0.as_ref()).fmt(f) + serialize_display(self, serializer) } } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, TS)] #[ts(type = "string", concrete(T = Vec))] pub struct Base64(pub T); +impl> std::fmt::Display for Base64 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&base64::encode(self.0.as_ref())) + } +} +impl>> FromStr for Base64 +{ + type Err = Error; + fn from_str(s: &str) -> Result { + base64::decode(&s) + .with_kind(ErrorKind::Deserialization)? + .apply(TryFrom::try_from) + .map(Self) + .map_err(|_| Error::new(eyre!("failed to create from buffer"), ErrorKind::Deserialization)) + } +} impl<'de, T: TryFrom>> Deserialize<'de> for Base64 { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { - let s = String::deserialize(deserializer)?; - base64::decode(&s) - .map_err(serde::de::Error::custom)? - .try_into() - .map_err(|_| serde::de::Error::custom("invalid length")) - .map(Self) + deserialize_from_str(deserializer) } } impl> Serialize for Base64 { @@ -1055,7 +1064,7 @@ impl> Serialize for Base64 { where S: Serializer, { - serializer.serialize_str(&base64::encode(self.0.as_ref())) + serialize_display(self, serializer) } } impl Deref for Base64 { diff --git a/core/startos/src/version/mod.rs b/core/startos/src/version/mod.rs index 1acce7896..7212b8801 100644 --- a/core/startos/src/version/mod.rs +++ b/core/startos/src/version/mod.rs @@ -28,7 +28,7 @@ enum Version { } impl Version { - fn from_util_version(version: crate::util::Version) -> Self { + fn from_util_version(version: crate::util::VersionString) -> Self { serde_json::to_value(version.clone()) .and_then(serde_json::from_value) .unwrap_or_else(|_e| { @@ -161,7 +161,7 @@ where T: VersionT, { fn deserialize>(deserializer: D) -> Result { - let v = crate::util::Version::deserialize(deserializer)?; + let v = crate::util::VersionString::deserialize(deserializer)?; let version = T::new(); if *v < version.semver() { Ok(Self(version, v.into_version())) @@ -186,7 +186,7 @@ where T: VersionT, { fn deserialize>(deserializer: D) -> Result { - let v = crate::util::Version::deserialize(deserializer)?; + let v = crate::util::VersionString::deserialize(deserializer)?; let version = T::new(); if *v == version.semver() { Ok(Wrapper(version)) diff --git a/core/startos/src/volume.rs b/core/startos/src/volume.rs index 32935bc7a..e801da79b 100644 --- a/core/startos/src/volume.rs +++ b/core/startos/src/volume.rs @@ -6,7 +6,7 @@ use models::{HostId, PackageId}; use crate::net::PACKAGE_CERT_PATH; use crate::prelude::*; -use crate::util::Version; +use crate::util::VersionString; pub const PKG_VOLUME_DIR: &str = "package-data/volumes"; pub const BACKUP_DIR: &str = "/media/startos/backups"; @@ -20,7 +20,7 @@ pub fn data_dir>(datadir: P, pkg_id: &PackageId, volume_id: &Volu .join(volume_id) } -pub fn asset_dir>(datadir: P, pkg_id: &PackageId, version: &Version) -> PathBuf { +pub fn asset_dir>(datadir: P, pkg_id: &PackageId, version: &VersionString) -> PathBuf { datadir .as_ref() .join(PKG_VOLUME_DIR) diff --git a/sdk/lib/osBindings/AcceptSigners.ts b/sdk/lib/osBindings/AcceptSigners.ts index 55d3997fe..1ef417623 100644 --- a/sdk/lib/osBindings/AcceptSigners.ts +++ b/sdk/lib/osBindings/AcceptSigners.ts @@ -1,7 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { SignerKey } from "./SignerKey" +import type { AnyVerifyingKey } from "./AnyVerifyingKey" export type AcceptSigners = - | { signer: SignerKey } + | { signer: AnyVerifyingKey } | { any: Array } | { all: Array } diff --git a/sdk/lib/osBindings/AddAdminParams.ts b/sdk/lib/osBindings/AddAdminParams.ts index a900f793b..9da08b54b 100644 --- a/sdk/lib/osBindings/AddAdminParams.ts +++ b/sdk/lib/osBindings/AddAdminParams.ts @@ -1,3 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Guid } from "./Guid" -export type AddAdminParams = { signer: string } +export type AddAdminParams = { signer: Guid } diff --git a/sdk/lib/osBindings/AddAssetParams.ts b/sdk/lib/osBindings/AddAssetParams.ts index 6a4d3fdf7..ce6128cf7 100644 --- a/sdk/lib/osBindings/AddAssetParams.ts +++ b/sdk/lib/osBindings/AddAssetParams.ts @@ -1,12 +1,13 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Signature } from "./Signature" -import type { SignerKey } from "./SignerKey" +import type { AnySignature } from "./AnySignature" +import type { Blake3Commitment } from "./Blake3Commitment" +import type { Version } from "./Version" export type AddAssetParams = { - url: string - signature: Signature - version: string + version: Version platform: string upload: boolean - __auth_signer: SignerKey + url: string + signature: AnySignature + commitment: Blake3Commitment } diff --git a/sdk/lib/osBindings/AddVersionParams.ts b/sdk/lib/osBindings/AddVersionParams.ts index 9fc281a6f..4ecbb7dcc 100644 --- a/sdk/lib/osBindings/AddVersionParams.ts +++ b/sdk/lib/osBindings/AddVersionParams.ts @@ -1,7 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Version } from "./Version" export type AddVersionParams = { - version: string + version: Version headline: string releaseNotes: string sourceVersion: string diff --git a/sdk/lib/osBindings/SignerKey.ts b/sdk/lib/osBindings/AnySignature.ts similarity index 55% rename from sdk/lib/osBindings/SignerKey.ts rename to sdk/lib/osBindings/AnySignature.ts index bd5c7c043..32b68b911 100644 --- a/sdk/lib/osBindings/SignerKey.ts +++ b/sdk/lib/osBindings/AnySignature.ts @@ -1,4 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Pem } from "./Pem" -export type SignerKey = { alg: "ed25519"; pubkey: Pem } +export type AnySignature = string diff --git a/sdk/lib/osBindings/AnySigningKey.ts b/sdk/lib/osBindings/AnySigningKey.ts new file mode 100644 index 000000000..4933251e8 --- /dev/null +++ b/sdk/lib/osBindings/AnySigningKey.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AnySigningKey = string diff --git a/sdk/lib/osBindings/AnyVerifyingKey.ts b/sdk/lib/osBindings/AnyVerifyingKey.ts new file mode 100644 index 000000000..58a45aaa7 --- /dev/null +++ b/sdk/lib/osBindings/AnyVerifyingKey.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AnyVerifyingKey = string diff --git a/sdk/lib/osBindings/Blake3Commitment.ts b/sdk/lib/osBindings/Blake3Commitment.ts new file mode 100644 index 000000000..690559122 --- /dev/null +++ b/sdk/lib/osBindings/Blake3Commitment.ts @@ -0,0 +1,4 @@ +// 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" + +export type Blake3Commitment = { hash: Base64; size: number } diff --git a/sdk/lib/osBindings/Category.ts b/sdk/lib/osBindings/Category.ts new file mode 100644 index 000000000..6e0815675 --- /dev/null +++ b/sdk/lib/osBindings/Category.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Description } from "./Description" + +export type Category = { name: string; description: Description } diff --git a/sdk/lib/osBindings/FullIndex.ts b/sdk/lib/osBindings/FullIndex.ts index 8da15b4c0..4d9914015 100644 --- a/sdk/lib/osBindings/FullIndex.ts +++ b/sdk/lib/osBindings/FullIndex.ts @@ -1,5 +1,13 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DataUrl } from "./DataUrl" +import type { Guid } from "./Guid" import type { OsIndex } from "./OsIndex" +import type { PackageIndex } from "./PackageIndex" import type { SignerInfo } from "./SignerInfo" -export type FullIndex = { os: OsIndex; signers: { [key: string]: SignerInfo } } +export type FullIndex = { + icon: DataUrl | null + package: PackageIndex + os: OsIndex + signers: { [key: Guid]: SignerInfo } +} diff --git a/sdk/lib/osBindings/GetOsAssetParams.ts b/sdk/lib/osBindings/GetOsAssetParams.ts index 100f711c7..9872d0b59 100644 --- a/sdk/lib/osBindings/GetOsAssetParams.ts +++ b/sdk/lib/osBindings/GetOsAssetParams.ts @@ -1,3 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Version } from "./Version" -export type GetOsAssetParams = { version: string; platform: string } +export type GetOsAssetParams = { version: Version; platform: string } diff --git a/sdk/lib/osBindings/GetPackageParams.ts b/sdk/lib/osBindings/GetPackageParams.ts new file mode 100644 index 000000000..6dfecc9b2 --- /dev/null +++ b/sdk/lib/osBindings/GetPackageParams.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PackageDetailLevel } from "./PackageDetailLevel" +import type { PackageId } from "./PackageId" + +export type GetPackageParams = { + id: PackageId | null + version: string | null + sourceVersion: string | null + otherVersions: PackageDetailLevel | null +} diff --git a/sdk/lib/osBindings/GetPackageResponse.ts b/sdk/lib/osBindings/GetPackageResponse.ts new file mode 100644 index 000000000..5bf24bfc0 --- /dev/null +++ b/sdk/lib/osBindings/GetPackageResponse.ts @@ -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 { PackageInfoShort } from "./PackageInfoShort" +import type { PackageVersionInfo } from "./PackageVersionInfo" +import type { Version } from "./Version" + +export type GetPackageResponse = { + best: { [key: Version]: PackageVersionInfo } + otherVersions?: { [key: Version]: PackageInfoShort } +} diff --git a/sdk/lib/osBindings/GetPackageResponseFull.ts b/sdk/lib/osBindings/GetPackageResponseFull.ts new file mode 100644 index 000000000..579924291 --- /dev/null +++ b/sdk/lib/osBindings/GetPackageResponseFull.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PackageVersionInfo } from "./PackageVersionInfo" +import type { Version } from "./Version" + +export type GetPackageResponseFull = { + best: { [key: Version]: PackageVersionInfo } + otherVersions: { [key: Version]: PackageVersionInfo } +} diff --git a/sdk/lib/osBindings/Pem.ts b/sdk/lib/osBindings/Guid.ts similarity index 80% rename from sdk/lib/osBindings/Pem.ts rename to sdk/lib/osBindings/Guid.ts index 1ec1cd375..28dc4d92d 100644 --- a/sdk/lib/osBindings/Pem.ts +++ b/sdk/lib/osBindings/Guid.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Pem = string +export type Guid = string diff --git a/sdk/lib/osBindings/HardwareRequirements.ts b/sdk/lib/osBindings/HardwareRequirements.ts index bbe7c3ef5..4964bc66f 100644 --- a/sdk/lib/osBindings/HardwareRequirements.ts +++ b/sdk/lib/osBindings/HardwareRequirements.ts @@ -2,6 +2,6 @@ export type HardwareRequirements = { device: { [key: string]: string } - ram: bigint | null + ram: number | null arch: Array | null } diff --git a/sdk/lib/osBindings/ListVersionSignersParams.ts b/sdk/lib/osBindings/ListVersionSignersParams.ts index baf516bf2..d066fbeb4 100644 --- a/sdk/lib/osBindings/ListVersionSignersParams.ts +++ b/sdk/lib/osBindings/ListVersionSignersParams.ts @@ -1,3 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Version } from "./Version" -export type ListVersionSignersParams = { version: string } +export type ListVersionSignersParams = { version: Version } diff --git a/sdk/lib/osBindings/Manifest.ts b/sdk/lib/osBindings/Manifest.ts index 14f2224ef..7f06be25e 100644 --- a/sdk/lib/osBindings/Manifest.ts +++ b/sdk/lib/osBindings/Manifest.ts @@ -5,15 +5,15 @@ import type { Description } from "./Description" import type { HardwareRequirements } from "./HardwareRequirements" import type { ImageId } from "./ImageId" import type { PackageId } from "./PackageId" +import type { Version } from "./Version" import type { VolumeId } from "./VolumeId" export type Manifest = { id: PackageId title: string - version: string + version: Version releaseNotes: string license: string - replaces: Array wrapperRepo: string upstreamRepo: string supportSite: string @@ -27,6 +27,6 @@ export type Manifest = { dependencies: Dependencies hardwareRequirements: HardwareRequirements gitHash: string | null - osVersion: string + osVersion: Version hasConfig: boolean } diff --git a/sdk/lib/osBindings/Blake3Ed25519Signature.ts b/sdk/lib/osBindings/MerkleArchiveCommitment.ts similarity index 52% rename from sdk/lib/osBindings/Blake3Ed25519Signature.ts rename to sdk/lib/osBindings/MerkleArchiveCommitment.ts index 8d28cb9ec..5dcaa0aa3 100644 --- a/sdk/lib/osBindings/Blake3Ed25519Signature.ts +++ b/sdk/lib/osBindings/MerkleArchiveCommitment.ts @@ -1,10 +1,7 @@ // 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 { Pem } from "./Pem" -export type Blake3Ed25519Signature = { - hash: Base64 - size: bigint - pubkey: Pem - signature: Base64 +export type MerkleArchiveCommitment = { + rootSighash: Base64 + rootMaxsize: number } diff --git a/sdk/lib/osBindings/OsIndex.ts b/sdk/lib/osBindings/OsIndex.ts index 590e9f577..9fb795402 100644 --- a/sdk/lib/osBindings/OsIndex.ts +++ b/sdk/lib/osBindings/OsIndex.ts @@ -1,4 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { OsVersionInfo } from "./OsVersionInfo" +import type { Version } from "./Version" -export type OsIndex = { versions: { [key: string]: OsVersionInfo } } +export type OsIndex = { versions: { [key: Version]: OsVersionInfo } } diff --git a/sdk/lib/osBindings/OsVersionInfo.ts b/sdk/lib/osBindings/OsVersionInfo.ts index fc40ef842..7cd0fde9e 100644 --- a/sdk/lib/osBindings/OsVersionInfo.ts +++ b/sdk/lib/osBindings/OsVersionInfo.ts @@ -1,12 +1,14 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Blake3Commitment } from "./Blake3Commitment" +import type { Guid } from "./Guid" import type { RegistryAsset } from "./RegistryAsset" export type OsVersionInfo = { headline: string releaseNotes: string sourceVersion: string - signers: string[] - iso: { [key: string]: RegistryAsset } - squashfs: { [key: string]: RegistryAsset } - img: { [key: string]: RegistryAsset } + signers: Array + iso: { [key: string]: RegistryAsset } + squashfs: { [key: string]: RegistryAsset } + img: { [key: string]: RegistryAsset } } diff --git a/sdk/lib/osBindings/PackageDetailLevel.ts b/sdk/lib/osBindings/PackageDetailLevel.ts new file mode 100644 index 000000000..b5e1ae42b --- /dev/null +++ b/sdk/lib/osBindings/PackageDetailLevel.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PackageDetailLevel = "short" | "full" diff --git a/sdk/lib/osBindings/PackageIndex.ts b/sdk/lib/osBindings/PackageIndex.ts new file mode 100644 index 000000000..5e8c94945 --- /dev/null +++ b/sdk/lib/osBindings/PackageIndex.ts @@ -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 { Category } from "./Category" +import type { PackageId } from "./PackageId" +import type { PackageInfo } from "./PackageInfo" + +export type PackageIndex = { + categories: { [key: string]: Category } + packages: { [key: PackageId]: PackageInfo } +} diff --git a/sdk/lib/osBindings/PackageInfo.ts b/sdk/lib/osBindings/PackageInfo.ts new file mode 100644 index 000000000..af340424f --- /dev/null +++ b/sdk/lib/osBindings/PackageInfo.ts @@ -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 { Guid } from "./Guid" +import type { PackageVersionInfo } from "./PackageVersionInfo" +import type { Version } from "./Version" + +export type PackageInfo = { + signers: Array + versions: { [key: Version]: PackageVersionInfo } +} diff --git a/sdk/lib/osBindings/PackageInfoShort.ts b/sdk/lib/osBindings/PackageInfoShort.ts new file mode 100644 index 000000000..22c7fbea4 --- /dev/null +++ b/sdk/lib/osBindings/PackageInfoShort.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PackageInfoShort = { releaseNotes: string } diff --git a/sdk/lib/osBindings/PackageVersionInfo.ts b/sdk/lib/osBindings/PackageVersionInfo.ts new file mode 100644 index 000000000..da82540cd --- /dev/null +++ b/sdk/lib/osBindings/PackageVersionInfo.ts @@ -0,0 +1,25 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DataUrl } from "./DataUrl" +import type { Description } from "./Description" +import type { HardwareRequirements } from "./HardwareRequirements" +import type { MerkleArchiveCommitment } from "./MerkleArchiveCommitment" +import type { RegistryAsset } from "./RegistryAsset" +import type { Version } from "./Version" + +export type PackageVersionInfo = { + title: string + icon: DataUrl + description: Description + releaseNotes: string + gitHash: string + license: string + wrapperRepo: string + upstreamRepo: string + supportSite: string + marketingSite: string + categories: string[] + osVersion: Version + hardwareRequirements: HardwareRequirements + sourceVersion: string | null + s9pk: RegistryAsset +} diff --git a/sdk/lib/osBindings/RegistryAsset.ts b/sdk/lib/osBindings/RegistryAsset.ts index cb878c66a..3eb13e8a4 100644 --- a/sdk/lib/osBindings/RegistryAsset.ts +++ b/sdk/lib/osBindings/RegistryAsset.ts @@ -1,4 +1,9 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { SignatureInfo } from "./SignatureInfo" +import type { AnySignature } from "./AnySignature" +import type { AnyVerifyingKey } from "./AnyVerifyingKey" -export type RegistryAsset = { url: string; signatureInfo: SignatureInfo } +export type RegistryAsset = { + url: string + commitment: Commitment + signatures: { [key: AnyVerifyingKey]: AnySignature } +} diff --git a/sdk/lib/osBindings/RemoveVersionParams.ts b/sdk/lib/osBindings/RemoveVersionParams.ts index 2c974de56..d00a6ee9e 100644 --- a/sdk/lib/osBindings/RemoveVersionParams.ts +++ b/sdk/lib/osBindings/RemoveVersionParams.ts @@ -1,3 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Version } from "./Version" -export type RemoveVersionParams = { version: string } +export type RemoveVersionParams = { version: Version } diff --git a/sdk/lib/osBindings/Blake3Ed2551SignatureInfo.ts b/sdk/lib/osBindings/RequestCommitment.ts similarity index 51% rename from sdk/lib/osBindings/Blake3Ed2551SignatureInfo.ts rename to sdk/lib/osBindings/RequestCommitment.ts index efe18aca7..89df04e4a 100644 --- a/sdk/lib/osBindings/Blake3Ed2551SignatureInfo.ts +++ b/sdk/lib/osBindings/RequestCommitment.ts @@ -1,9 +1,9 @@ // 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 { Pem } from "./Pem" -export type Blake3Ed2551SignatureInfo = { - hash: Base64 - size: bigint - signatures: { [key: Pem]: Base64 } +export type RequestCommitment = { + timestamp: number + nonce: number + size: number + blake3: Base64 } diff --git a/sdk/lib/osBindings/ServerInfo.ts b/sdk/lib/osBindings/ServerInfo.ts index 935e3a99f..92b8b9b8f 100644 --- a/sdk/lib/osBindings/ServerInfo.ts +++ b/sdk/lib/osBindings/ServerInfo.ts @@ -2,6 +2,7 @@ import type { Governor } from "./Governor" import type { IpInfo } from "./IpInfo" import type { ServerStatus } from "./ServerStatus" +import type { Version } from "./Version" import type { WifiInfo } from "./WifiInfo" export type ServerInfo = { @@ -9,7 +10,7 @@ export type ServerInfo = { platform: string id: string hostname: string - version: string + version: Version lastBackup: string | null eosVersionCompat: string lanAddress: string diff --git a/sdk/lib/osBindings/SetMainStatus.ts b/sdk/lib/osBindings/SetMainStatus.ts index aa4aff0b2..6dcca73e9 100644 --- a/sdk/lib/osBindings/SetMainStatus.ts +++ b/sdk/lib/osBindings/SetMainStatus.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Status } from "./Status" +import type { SetMainStatusStatus } from "./SetMainStatusStatus" -export type SetMainStatus = { status: "running" | "stopped" | "starting" } +export type SetMainStatus = { status: SetMainStatusStatus } diff --git a/sdk/lib/osBindings/SetMainStatusStatus.ts b/sdk/lib/osBindings/SetMainStatusStatus.ts new file mode 100644 index 000000000..6db32e7bf --- /dev/null +++ b/sdk/lib/osBindings/SetMainStatusStatus.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SetMainStatusStatus = "running" | "stopped" | "starting" diff --git a/sdk/lib/osBindings/SignAssetParams.ts b/sdk/lib/osBindings/SignAssetParams.ts index 3b3612c47..d55a061a7 100644 --- a/sdk/lib/osBindings/SignAssetParams.ts +++ b/sdk/lib/osBindings/SignAssetParams.ts @@ -1,8 +1,9 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Signature } from "./Signature" +import type { AnySignature } from "./AnySignature" +import type { Version } from "./Version" export type SignAssetParams = { - version: string + version: Version platform: string - signature: Signature + signature: AnySignature } diff --git a/sdk/lib/osBindings/Signature.ts b/sdk/lib/osBindings/Signature.ts deleted file mode 100644 index a27ada0e7..000000000 --- a/sdk/lib/osBindings/Signature.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Blake3Ed25519Signature } from "./Blake3Ed25519Signature" - -export type Signature = { blake3Ed25519: Blake3Ed25519Signature } diff --git a/sdk/lib/osBindings/SignatureInfo.ts b/sdk/lib/osBindings/SignatureInfo.ts deleted file mode 100644 index 41a093742..000000000 --- a/sdk/lib/osBindings/SignatureInfo.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Blake3Ed2551SignatureInfo } from "./Blake3Ed2551SignatureInfo" - -export type SignatureInfo = { - context: string - "blake-3-ed-25-5i-9": Blake3Ed2551SignatureInfo | null -} diff --git a/sdk/lib/osBindings/SignerInfo.ts b/sdk/lib/osBindings/SignerInfo.ts index 06293cbab..7e7aa2588 100644 --- a/sdk/lib/osBindings/SignerInfo.ts +++ b/sdk/lib/osBindings/SignerInfo.ts @@ -1,9 +1,9 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AnyVerifyingKey } from "./AnyVerifyingKey" import type { ContactInfo } from "./ContactInfo" -import type { SignerKey } from "./SignerKey" export type SignerInfo = { name: string contact: Array - keys: Array + keys: Array } diff --git a/sdk/lib/osBindings/Version.ts b/sdk/lib/osBindings/Version.ts new file mode 100644 index 000000000..b49b6e887 --- /dev/null +++ b/sdk/lib/osBindings/Version.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Version = string diff --git a/sdk/lib/osBindings/VersionSignerParams.ts b/sdk/lib/osBindings/VersionSignerParams.ts index 70acdaa2e..102eecefd 100644 --- a/sdk/lib/osBindings/VersionSignerParams.ts +++ b/sdk/lib/osBindings/VersionSignerParams.ts @@ -1,3 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Guid } from "./Guid" +import type { Version } from "./Version" -export type VersionSignerParams = { version: string; signer: string } +export type VersionSignerParams = { version: Version; signer: Guid } diff --git a/sdk/lib/osBindings/index.ts b/sdk/lib/osBindings/index.ts index 4fcb9a617..16fc56a74 100644 --- a/sdk/lib/osBindings/index.ts +++ b/sdk/lib/osBindings/index.ts @@ -11,14 +11,17 @@ export { Algorithm } from "./Algorithm" export { AllowedStatuses } from "./AllowedStatuses" export { AllPackageData } from "./AllPackageData" export { AlpnInfo } from "./AlpnInfo" +export { AnySignature } from "./AnySignature" +export { AnySigningKey } from "./AnySigningKey" +export { AnyVerifyingKey } from "./AnyVerifyingKey" export { BackupProgress } from "./BackupProgress" export { Base64 } from "./Base64" export { BindInfo } from "./BindInfo" export { BindOptions } from "./BindOptions" export { BindParams } from "./BindParams" -export { Blake3Ed25519Signature } from "./Blake3Ed25519Signature" -export { Blake3Ed2551SignatureInfo } from "./Blake3Ed2551SignatureInfo" +export { Blake3Commitment } from "./Blake3Commitment" export { Callback } from "./Callback" +export { Category } from "./Category" export { CheckDependenciesParam } from "./CheckDependenciesParam" export { CheckDependenciesResult } from "./CheckDependenciesResult" export { ChrootParams } from "./ChrootParams" @@ -48,6 +51,9 @@ export { FullProgress } from "./FullProgress" export { GetHostInfoParamsKind } from "./GetHostInfoParamsKind" export { GetHostInfoParams } from "./GetHostInfoParams" export { GetOsAssetParams } from "./GetOsAssetParams" +export { GetPackageParams } from "./GetPackageParams" +export { GetPackageResponseFull } from "./GetPackageResponseFull" +export { GetPackageResponse } from "./GetPackageResponse" export { GetPrimaryUrlParams } from "./GetPrimaryUrlParams" export { GetServiceInterfaceParams } from "./GetServiceInterfaceParams" export { GetServicePortForwardParams } from "./GetServicePortForwardParams" @@ -57,6 +63,7 @@ export { GetStoreParams } from "./GetStoreParams" export { GetSystemSmtpParams } from "./GetSystemSmtpParams" export { GetVersionParams } from "./GetVersionParams" export { Governor } from "./Governor" +export { Guid } from "./Guid" export { HardwareRequirements } from "./HardwareRequirements" export { HealthCheckId } from "./HealthCheckId" export { HealthCheckResult } from "./HealthCheckResult" @@ -75,24 +82,30 @@ export { ListVersionSignersParams } from "./ListVersionSignersParams" export { MainStatus } from "./MainStatus" export { Manifest } from "./Manifest" export { MaybeUtf8String } from "./MaybeUtf8String" +export { MerkleArchiveCommitment } from "./MerkleArchiveCommitment" export { MountParams } from "./MountParams" export { MountTarget } from "./MountTarget" export { NamedProgress } from "./NamedProgress" export { OsIndex } from "./OsIndex" export { OsVersionInfo } from "./OsVersionInfo" export { PackageDataEntry } from "./PackageDataEntry" +export { PackageDetailLevel } from "./PackageDetailLevel" export { PackageId } from "./PackageId" +export { PackageIndex } from "./PackageIndex" +export { PackageInfoShort } from "./PackageInfoShort" +export { PackageInfo } from "./PackageInfo" export { PackageState } from "./PackageState" +export { PackageVersionInfo } from "./PackageVersionInfo" export { ParamsMaybePackageId } from "./ParamsMaybePackageId" export { ParamsPackageId } from "./ParamsPackageId" export { PasswordType } from "./PasswordType" -export { Pem } from "./Pem" export { Progress } from "./Progress" export { Public } from "./Public" export { RegistryAsset } from "./RegistryAsset" export { RemoveActionParams } from "./RemoveActionParams" export { RemoveAddressParams } from "./RemoveAddressParams" export { RemoveVersionParams } from "./RemoveVersionParams" +export { RequestCommitment } from "./RequestCommitment" export { ReverseProxyBind } from "./ReverseProxyBind" export { ReverseProxyDestination } from "./ReverseProxyDestination" export { ReverseProxyHttp } from "./ReverseProxyHttp" @@ -111,16 +124,15 @@ export { Session } from "./Session" export { SetConfigured } from "./SetConfigured" export { SetDependenciesParams } from "./SetDependenciesParams" export { SetHealth } from "./SetHealth" +export { SetMainStatusStatus } from "./SetMainStatusStatus" export { SetMainStatus } from "./SetMainStatus" export { SetStoreParams } from "./SetStoreParams" export { SetSystemSmtpParams } from "./SetSystemSmtpParams" export { SignAssetParams } from "./SignAssetParams" -export { SignatureInfo } from "./SignatureInfo" -export { Signature } from "./Signature" export { SignerInfo } from "./SignerInfo" -export { SignerKey } from "./SignerKey" export { Status } from "./Status" export { UpdatingState } from "./UpdatingState" export { VersionSignerParams } from "./VersionSignerParams" +export { Version } from "./Version" export { VolumeId } from "./VolumeId" export { WifiInfo } from "./WifiInfo" diff --git a/web/projects/marketplace/src/pages/show/additional/additional.component.html b/web/projects/marketplace/src/pages/show/additional/additional.component.html index d362991e3..a75daa3db 100644 --- a/web/projects/marketplace/src/pages/show/additional/additional.component.html +++ b/web/projects/marketplace/src/pages/show/additional/additional.component.html @@ -1,14 +1,3 @@ - -
- Intended to replace -
    -
  • - {{ app }} -
  • -
-
-
- Additional Info diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index 59ef6dd1e..2b0b96d23 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -51,7 +51,6 @@ export module Mock { short: 'A Bitcoin full node by Bitcoin Core.', long: 'Bitcoin is a decentralized consensus protocol and settlement network.', }, - replaces: ['banks', 'governments'], releaseNotes: 'Taproot, Schnorr, and more.', license: 'MIT', wrapperRepo: 'https://github.com/start9labs/bitcoind-wrapper', @@ -89,7 +88,6 @@ export module Mock { short: 'A bolt spec compliant client.', long: 'More info about LND. More info about LND. More info about LND.', }, - replaces: ['banks', 'governments'], releaseNotes: 'Dual funded channels!', license: 'MIT', wrapperRepo: 'https://github.com/start9labs/lnd-wrapper', @@ -158,7 +156,6 @@ export module Mock { optional: false, }, }, - replaces: [], hasConfig: false, images: ['main'], assets: [], From 2568bfde5e94bac25e3e8353daeb6943bdc4bca9 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 31 May 2024 13:46:58 -0600 Subject: [PATCH 025/125] create skeleton --- Makefile | 2 +- core/build-analyticsd.sh | 42 ++++++ core/{build-reg.sh => build-registrybox.sh} | 0 core/{build-prod.sh => build-startos-bins.sh} | 0 core/startos/Cargo.toml | 7 +- core/startos/src/analytics/context.rs | 121 ++++++++++++++++++ core/startos/src/analytics/mod.rs | 78 +++++++++++ core/startos/src/bins/analyticsd.rs | 86 +++++++++++++ core/startos/src/bins/mod.rs | 4 + core/startos/src/context/config.rs | 3 + core/startos/src/context/rpc.rs | 5 + core/startos/src/lib.rs | 9 +- core/startos/src/lxc/dev.rs | 112 ++++++++++++++++ core/startos/src/lxc/mod.rs | 117 +---------------- core/startos/src/os_install/mod.rs | 4 + core/startos/src/registry/context.rs | 21 ++- sdk/lib/osBindings/AddAssetParams.ts | 1 - sdk/lib/osBindings/AddPackageParams.ts | 9 ++ sdk/lib/osBindings/GetPackageResponse.ts | 1 + sdk/lib/osBindings/GetPackageResponseFull.ts | 1 + sdk/lib/osBindings/HardwareRequirements.ts | 2 +- sdk/lib/osBindings/OsVersionInfo.ts | 2 +- sdk/lib/osBindings/PackageInfo.ts | 3 +- sdk/lib/osBindings/PackageVersionInfo.ts | 1 - sdk/lib/osBindings/index.ts | 1 + 25 files changed, 508 insertions(+), 124 deletions(-) create mode 100755 core/build-analyticsd.sh rename core/{build-reg.sh => build-registrybox.sh} (100%) rename core/{build-prod.sh => build-startos-bins.sh} (100%) create mode 100644 core/startos/src/analytics/context.rs create mode 100644 core/startos/src/analytics/mod.rs create mode 100644 core/startos/src/bins/analyticsd.rs create mode 100644 core/startos/src/lxc/dev.rs create mode 100644 sdk/lib/osBindings/AddPackageParams.ts diff --git a/Makefile b/Makefile index ff5b9c4ad..7b488d2d2 100644 --- a/Makefile +++ b/Makefile @@ -225,7 +225,7 @@ system-images/binfmt/docker-images/$(ARCH).tar: $(BINFMT_SRC) cd system-images/binfmt && make docker-images/$(ARCH).tar && touch docker-images/$(ARCH).tar $(BINS): $(CORE_SRC) $(ENVIRONMENT_FILE) - cd core && ARCH=$(ARCH) ./build-prod.sh + cd core && ARCH=$(ARCH) ./build-startos-bins.sh touch $(BINS) web/node_modules/.package-lock.json: web/package.json sdk/dist diff --git a/core/build-analyticsd.sh b/core/build-analyticsd.sh new file mode 100755 index 000000000..4f8578da1 --- /dev/null +++ b/core/build-analyticsd.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +cd "$(dirname "${BASH_SOURCE[0]}")" + +set -e +shopt -s expand_aliases + +if [ -z "$ARCH" ]; then + ARCH=$(uname -m) +fi + +USE_TTY= +if tty -s; then + USE_TTY="-it" +fi + +cd .. +FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')" +RUSTFLAGS="" + +if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then + RUSTFLAGS="--cfg tokio_unstable" +fi + +alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl' + +set +e +fail= +echo "FEATURES=\"$FEATURES\"" +echo "RUSTFLAGS=\"$RUSTFLAGS\"" +if ! rust-musl-builder sh -c "(cd core && cargo build --release --no-default-features --features analyticsd,$FEATURES --locked --bin analyticsd --target=$ARCH-unknown-linux-musl)"; then + fail=true +fi +set -e +cd core + +sudo chown -R $USER target +sudo chown -R $USER ~/.cargo + +if [ -n "$fail" ]; then + exit 1 +fi diff --git a/core/build-reg.sh b/core/build-registrybox.sh similarity index 100% rename from core/build-reg.sh rename to core/build-registrybox.sh diff --git a/core/build-prod.sh b/core/build-startos-bins.sh similarity index 100% rename from core/build-prod.sh rename to core/build-startos-bins.sh diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index bd064a167..fa2bac69d 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -37,12 +37,17 @@ path = "src/main.rs" name = "registrybox" path = "src/main.rs" +[[bin]] +name = "analyticsd" +path = "src/main.rs" + [features] cli = [] container-runtime = [] daemon = [] registry = [] -default = ["cli", "daemon", "registry"] +analyticsd = [] +default = ["cli", "daemon"] dev = [] unstable = ["console-subscriber", "tokio/tracing"] docker = [] diff --git a/core/startos/src/analytics/context.rs b/core/startos/src/analytics/context.rs new file mode 100644 index 000000000..8ece8479b --- /dev/null +++ b/core/startos/src/analytics/context.rs @@ -0,0 +1,121 @@ +use std::net::{Ipv4Addr, SocketAddr}; +use std::ops::Deref; +use std::path::PathBuf; +use std::sync::Arc; + +use clap::Parser; +use reqwest::{Client, Proxy}; +use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::{call_remote_http, CallRemote, Context, Empty}; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; +use tokio::sync::broadcast::Sender; +use tracing::instrument; +use url::Url; + +use crate::context::config::{ContextConfig, CONFIG_PATH}; +use crate::context::RpcContext; +use crate::prelude::*; +use crate::rpc_continuations::RpcContinuations; + +#[derive(Debug, Clone, Default, Deserialize, Serialize, Parser)] +#[serde(rename_all = "kebab-case")] +#[command(rename_all = "kebab-case")] +pub struct AnalyticsConfig { + #[arg(short = 'c', long = "config")] + pub config: Option, + #[arg(short = 'l', long = "listen")] + pub listen: Option, + #[arg(short = 'p', long = "proxy")] + pub tor_proxy: Option, + #[arg(short = 'd', long = "dbconnect")] + pub dbconnect: Option, +} +impl ContextConfig for AnalyticsConfig { + fn next(&mut self) -> Option { + self.config.take() + } + fn merge_with(&mut self, other: Self) { + self.listen = self.listen.take().or(other.listen); + self.tor_proxy = self.tor_proxy.take().or(other.tor_proxy); + self.dbconnect = self.dbconnect.take().or(other.dbconnect); + } +} + +impl AnalyticsConfig { + pub fn load(mut self) -> Result { + let path = self.next(); + self.load_path_rec(path)?; + self.load_path_rec(Some(CONFIG_PATH))?; + Ok(self) + } +} + +pub struct AnalyticsContextSeed { + pub listen: SocketAddr, + pub db: PgPool, + pub rpc_continuations: RpcContinuations, + pub client: Client, + pub shutdown: Sender<()>, +} + +#[derive(Clone)] +pub struct AnalyticsContext(Arc); +impl AnalyticsContext { + #[instrument(skip_all)] + pub async fn init(config: &AnalyticsConfig) -> Result { + let (shutdown, _) = tokio::sync::broadcast::channel(1); + let dbconnect = config + .dbconnect + .clone() + .unwrap_or_else(|| "postgres://localhost/analytics".parse().unwrap()) + .to_owned(); + let db = PgPool::connect(dbconnect.as_str()).await?; + let tor_proxy_url = config + .tor_proxy + .clone() + .map(Ok) + .unwrap_or_else(|| "socks5h://localhost:9050".parse())?; + Ok(Self(Arc::new(AnalyticsContextSeed { + listen: config + .listen + .unwrap_or(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 5959)), + db, + rpc_continuations: RpcContinuations::new(), + client: Client::builder() + .proxy(Proxy::custom(move |url| { + if url.host_str().map_or(false, |h| h.ends_with(".onion")) { + Some(tor_proxy_url.clone()) + } else { + None + } + })) + .build() + .with_kind(crate::ErrorKind::ParseUrl)?, + shutdown, + }))) + } +} +impl AsRef for AnalyticsContext { + fn as_ref(&self) -> &RpcContinuations { + &self.rpc_continuations + } +} + +impl Context for AnalyticsContext {} +impl Deref for AnalyticsContext { + type Target = AnalyticsContextSeed; + fn deref(&self) -> &Self::Target { + &*self.0 + } +} + +impl CallRemote for RpcContext { + async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { + if let Some(analytics_url) = self.analytics_url.clone() { + call_remote_http(&self.client, analytics_url, method, params).await + } else { + Ok(Value::Null) + } + } +} diff --git a/core/startos/src/analytics/mod.rs b/core/startos/src/analytics/mod.rs new file mode 100644 index 000000000..52ca37948 --- /dev/null +++ b/core/startos/src/analytics/mod.rs @@ -0,0 +1,78 @@ +use std::net::SocketAddr; + +use axum::Router; +use futures::future::ready; +use rpc_toolkit::{Context, ParentHandler, Server}; + +use crate::analytics::context::AnalyticsContext; +use crate::middleware::cors::Cors; +use crate::net::static_server::{bad_request, not_found, server_error}; +use crate::net::web_server::WebServer; +use crate::rpc_continuations::Guid; + +pub mod context; + +pub fn analytics_api() -> ParentHandler { + ParentHandler::new() // TODO: FullMetal +} + +pub fn analytics_server_router(ctx: AnalyticsContext) -> Router { + use axum::extract as x; + use axum::routing::{any, get, post}; + Router::new() + .route("/rpc/*path", { + let ctx = ctx.clone(); + post( + Server::new(move || ready(Ok(ctx.clone())), analytics_api()) + .middleware(Cors::new()) + ) + }) + .route( + "/ws/rpc/*path", + get({ + let ctx = ctx.clone(); + move |x::Path(path): x::Path, + ws: axum::extract::ws::WebSocketUpgrade| async move { + match Guid::from(&path) { + None => { + tracing::debug!("No Guid Path"); + bad_request() + } + Some(guid) => match ctx.rpc_continuations.get_ws_handler(&guid).await { + Some(cont) => ws.on_upgrade(cont), + _ => not_found(), + }, + } + } + }), + ) + .route( + "/rest/rpc/*path", + any({ + let ctx = ctx.clone(); + move |request: x::Request| async move { + let path = request + .uri() + .path() + .strip_prefix("/rest/rpc/") + .unwrap_or_default(); + match Guid::from(&path) { + None => { + tracing::debug!("No Guid Path"); + bad_request() + } + Some(guid) => match ctx.rpc_continuations.get_rest_handler(&guid).await { + None => not_found(), + Some(cont) => cont(request).await.unwrap_or_else(server_error), + }, + } + } + }), + ) +} + +impl WebServer { + pub fn analytics(bind: SocketAddr, ctx: AnalyticsContext) -> Self { + Self::new(bind, analytics_server_router(ctx)) + } +} diff --git a/core/startos/src/bins/analyticsd.rs b/core/startos/src/bins/analyticsd.rs new file mode 100644 index 000000000..e37a7dae1 --- /dev/null +++ b/core/startos/src/bins/analyticsd.rs @@ -0,0 +1,86 @@ +use std::ffi::OsString; + +use clap::Parser; +use futures::FutureExt; +use tokio::signal::unix::signal; +use tracing::instrument; + +use crate::analytics::context::{AnalyticsConfig, AnalyticsContext}; +use crate::net::web_server::WebServer; +use crate::prelude::*; +use crate::util::logger::EmbassyLogger; + +#[instrument(skip_all)] +async fn inner_main(config: &AnalyticsConfig) -> Result<(), Error> { + let server = async { + let ctx = AnalyticsContext::init(config).await?; + let server = WebServer::analytics(ctx.listen, ctx.clone()); + + let mut shutdown_recv = ctx.shutdown.subscribe(); + + let sig_handler_ctx = ctx; + let sig_handler = tokio::spawn(async move { + use tokio::signal::unix::SignalKind; + futures::future::select_all( + [ + SignalKind::interrupt(), + SignalKind::quit(), + SignalKind::terminate(), + ] + .iter() + .map(|s| { + async move { + signal(*s) + .unwrap_or_else(|_| panic!("register {:?} handler", s)) + .recv() + .await + } + .boxed() + }), + ) + .await; + sig_handler_ctx + .shutdown + .send(()) + .map_err(|_| ()) + .expect("send shutdown signal"); + }); + + shutdown_recv + .recv() + .await + .with_kind(crate::ErrorKind::Unknown)?; + + sig_handler.abort(); + + Ok::<_, Error>(server) + } + .await?; + server.shutdown().await; + + Ok(()) +} + +pub fn main(args: impl IntoIterator) { + EmbassyLogger::init(); + + let config = AnalyticsConfig::parse_from(args).load().unwrap(); + + let res = { + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("failed to initialize runtime"); + rt.block_on(inner_main(&config)) + }; + + match res { + Ok(()) => (), + Err(e) => { + eprintln!("{}", e.source); + tracing::debug!("{:?}", e.source); + drop(e.source); + std::process::exit(e.kind as i32) + } + } +} diff --git a/core/startos/src/bins/mod.rs b/core/startos/src/bins/mod.rs index 4a4670a5b..70f87aa99 100644 --- a/core/startos/src/bins/mod.rs +++ b/core/startos/src/bins/mod.rs @@ -2,6 +2,8 @@ use std::collections::VecDeque; use std::ffi::OsString; use std::path::Path; +#[cfg(feature = "analytics")] +pub mod analytics; #[cfg(feature = "container-runtime")] pub mod container_cli; pub mod deprecated; @@ -24,6 +26,8 @@ fn select_executable(name: &str) -> Option)> { "startd" => Some(startd::main), #[cfg(feature = "registry")] "registry" => Some(registry::main), + #[cfg(feature = "analyticsd")] + "analyticsd" => Some(analyticsd::main), "embassy-cli" => Some(|_| deprecated::renamed("embassy-cli", "start-cli")), "embassy-sdk" => Some(|_| deprecated::renamed("embassy-sdk", "start-sdk")), "embassyd" => Some(|_| deprecated::renamed("embassyd", "startd")), diff --git a/core/startos/src/context/config.rs b/core/startos/src/context/config.rs index bc2da00e2..886361777 100644 --- a/core/startos/src/context/config.rs +++ b/core/startos/src/context/config.rs @@ -113,6 +113,8 @@ pub struct ServerConfig { pub datadir: Option, #[arg(long = "disable-encryption")] pub disable_encryption: Option, + #[arg(short = 'a', long = "analytics-url")] + pub analytics_url: Option, } impl ContextConfig for ServerConfig { fn next(&mut self) -> Option { @@ -131,6 +133,7 @@ impl ContextConfig for ServerConfig { .or(other.revision_cache_size); self.datadir = self.datadir.take().or(other.datadir); self.disable_encryption = self.disable_encryption.take().or(other.disable_encryption); + self.analytics_url = self.analytics_url.take().or(other.analytics_url); } } diff --git a/core/startos/src/context/rpc.rs b/core/startos/src/context/rpc.rs index cf2d28085..1042def74 100644 --- a/core/startos/src/context/rpc.rs +++ b/core/startos/src/context/rpc.rs @@ -13,6 +13,7 @@ use rpc_toolkit::{CallRemote, Context, Empty}; use tokio::sync::{broadcast, oneshot, Mutex, RwLock}; use tokio::time::Instant; use tracing::instrument; +use url::Url; use super::setup::CURRENT_SECRET; use crate::account::AccountInfo; @@ -55,7 +56,9 @@ pub struct RpcContextSeed { pub client: Client, pub hardware: Hardware, pub start_time: Instant, + #[cfg(feature = "dev")] pub dev: Dev, + pub analytics_url: Option, } pub struct Dev { @@ -184,9 +187,11 @@ impl RpcContext { .with_kind(crate::ErrorKind::ParseUrl)?, hardware: Hardware { devices, ram }, start_time: Instant::now(), + #[cfg(feature = "dev")] dev: Dev { lxc: Mutex::new(BTreeMap::new()), }, + analytics_url: config.analytics_url.clone(), }); let res = Self(seed.clone()); diff --git a/core/startos/src/lib.rs b/core/startos/src/lib.rs index d60a2db24..96210114f 100644 --- a/core/startos/src/lib.rs +++ b/core/startos/src/lib.rs @@ -24,6 +24,7 @@ lazy_static::lazy_static! { pub mod account; pub mod action; +pub mod analytics; pub mod auth; pub mod backup; pub mod bins; @@ -95,7 +96,7 @@ pub fn echo(_: C, EchoParams { message }: EchoParams) -> Result() -> ParentHandler { - ParentHandler::new() + let api = ParentHandler::new() .subcommand::("git-info", from_fn(version::git_info)) .subcommand( "echo", @@ -120,9 +121,11 @@ pub fn main_api() -> ParentHandler { ) .no_cli(), ) - .subcommand("lxc", lxc::lxc::()) .subcommand("s9pk", s9pk::rpc::s9pk()) - .subcommand("util", util::rpc::util::()) + .subcommand("util", util::rpc::util::()); + #[cfg(feature = "dev")] + let api = api.subcommand("lxc", lxc::dev::lxc::()); + api } pub fn server() -> ParentHandler { diff --git a/core/startos/src/lxc/dev.rs b/core/startos/src/lxc/dev.rs new file mode 100644 index 000000000..61dd8e598 --- /dev/null +++ b/core/startos/src/lxc/dev.rs @@ -0,0 +1,112 @@ +use std::ops::Deref; + +use clap::Parser; +use rpc_toolkit::{ + from_fn_async, CallRemoteHandler, Context, Empty, HandlerArgs, HandlerExt, HandlerFor, + ParentHandler, +}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use crate::context::{CliContext, RpcContext}; +use crate::lxc::{ContainerId, LxcConfig}; +use crate::prelude::*; +use crate::rpc_continuations::Guid; + +pub fn lxc() -> ParentHandler { + ParentHandler::new() + .subcommand( + "create", + from_fn_async(create).with_call_remote::(), + ) + .subcommand( + "list", + from_fn_async(list) + .with_custom_display_fn(|_, res| { + use prettytable::*; + let mut table = table!([bc => "GUID"]); + for guid in res { + table.add_row(row![&*guid]); + } + table.printstd(); + Ok(()) + }) + .with_call_remote::(), + ) + .subcommand( + "remove", + from_fn_async(remove) + .no_display() + .with_call_remote::(), + ) + .subcommand("connect", from_fn_async(connect_rpc).no_cli()) + .subcommand("connect", from_fn_async(connect_rpc_cli).no_display()) +} + +pub async fn create(ctx: RpcContext) -> Result { + let container = ctx.lxc_manager.create(None, LxcConfig::default()).await?; + let guid = container.guid.deref().clone(); + ctx.dev.lxc.lock().await.insert(guid.clone(), container); + Ok(guid) +} + +pub async fn list(ctx: RpcContext) -> Result, Error> { + Ok(ctx.dev.lxc.lock().await.keys().cloned().collect()) +} + +#[derive(Deserialize, Serialize, Parser, TS)] +pub struct RemoveParams { + #[ts(type = "string")] + pub guid: ContainerId, +} + +pub async fn remove(ctx: RpcContext, RemoveParams { guid }: RemoveParams) -> Result<(), Error> { + if let Some(container) = ctx.dev.lxc.lock().await.remove(&guid) { + container.exit().await?; + } + Ok(()) +} + +#[derive(Deserialize, Serialize, Parser, TS)] +pub struct ConnectParams { + #[ts(type = "string")] + pub guid: ContainerId, +} + +pub async fn connect_rpc( + ctx: RpcContext, + ConnectParams { guid }: ConnectParams, +) -> Result { + super::connect( + &ctx, + ctx.dev.lxc.lock().await.get(&guid).ok_or_else(|| { + Error::new(eyre!("No container with guid: {guid}"), ErrorKind::NotFound) + })?, + ) + .await +} + +pub async fn connect_rpc_cli( + HandlerArgs { + context, + parent_method, + method, + params, + inherited_params, + raw_params, + }: HandlerArgs, +) -> Result<(), Error> { + let ctx = context.clone(); + let guid = CallRemoteHandler::::new(from_fn_async(connect_rpc)) + .handle_async(HandlerArgs { + context, + parent_method, + method, + params: rpc_toolkit::util::Flat(params, Empty {}), + inherited_params, + raw_params, + }) + .await?; + + super::connect_cli(&ctx, guid).await +} diff --git a/core/startos/src/lxc/mod.rs b/core/startos/src/lxc/mod.rs index f64ecebe7..997c17c8a 100644 --- a/core/startos/src/lxc/mod.rs +++ b/core/startos/src/lxc/mod.rs @@ -1,20 +1,15 @@ use std::collections::BTreeSet; use std::net::Ipv4Addr; -use std::ops::Deref; use std::path::Path; use std::sync::{Arc, Weak}; use std::time::Duration; use clap::builder::ValueParserFactory; -use clap::Parser; use futures::{AsyncWriteExt, FutureExt, StreamExt}; use imbl_value::{InOMap, InternedString}; use models::InvalidId; -use rpc_toolkit::yajrc::{RpcError, RpcResponse}; -use rpc_toolkit::{ - from_fn_async, CallRemoteHandler, Context, Empty, GenericRpcMethod, HandlerArgs, HandlerExt, - HandlerFor, ParentHandler, RpcRequest, -}; +use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::{GenericRpcMethod, RpcRequest, RpcResponse}; use rustyline_async::{ReadlineEvent, SharedWriter}; use serde::{Deserialize, Serialize}; use tokio::fs::File; @@ -38,6 +33,9 @@ use crate::util::clap::FromStrParser; use crate::util::rpc_client::UnixRpcClient; use crate::util::{new_guid, Invoke}; +#[cfg(feature = "dev")] +pub mod dev; + const LXC_CONTAINER_DIR: &str = "/var/lib/lxc"; const RPC_DIR: &str = "media/startos/rpc"; // must not be absolute path pub const CONTAINER_RPC_SERVER_SOCKET: &str = "service.sock"; // must not be absolute path @@ -369,80 +367,6 @@ impl Drop for LxcContainer { #[derive(Default, Serialize)] pub struct LxcConfig {} - -pub fn lxc() -> ParentHandler { - ParentHandler::new() - .subcommand( - "create", - from_fn_async(create).with_call_remote::(), - ) - .subcommand( - "list", - from_fn_async(list) - .with_custom_display_fn(|_, res| { - use prettytable::*; - let mut table = table!([bc => "GUID"]); - for guid in res { - table.add_row(row![&*guid]); - } - table.printstd(); - Ok(()) - }) - .with_call_remote::(), - ) - .subcommand( - "remove", - from_fn_async(remove) - .no_display() - .with_call_remote::(), - ) - .subcommand("connect", from_fn_async(connect_rpc).no_cli()) - .subcommand("connect", from_fn_async(connect_rpc_cli).no_display()) -} - -pub async fn create(ctx: RpcContext) -> Result { - let container = ctx.lxc_manager.create(None, LxcConfig::default()).await?; - let guid = container.guid.deref().clone(); - ctx.dev.lxc.lock().await.insert(guid.clone(), container); - Ok(guid) -} - -pub async fn list(ctx: RpcContext) -> Result, Error> { - Ok(ctx.dev.lxc.lock().await.keys().cloned().collect()) -} - -#[derive(Deserialize, Serialize, Parser, TS)] -pub struct RemoveParams { - #[ts(type = "string")] - pub guid: ContainerId, -} - -pub async fn remove(ctx: RpcContext, RemoveParams { guid }: RemoveParams) -> Result<(), Error> { - if let Some(container) = ctx.dev.lxc.lock().await.remove(&guid) { - container.exit().await?; - } - Ok(()) -} - -#[derive(Deserialize, Serialize, Parser, TS)] -pub struct ConnectParams { - #[ts(type = "string")] - pub guid: ContainerId, -} - -pub async fn connect_rpc( - ctx: RpcContext, - ConnectParams { guid }: ConnectParams, -) -> Result { - connect( - &ctx, - ctx.dev.lxc.lock().await.get(&guid).ok_or_else(|| { - Error::new(eyre!("No container with guid: {guid}"), ErrorKind::NotFound) - })?, - ) - .await -} - pub async fn connect(ctx: &RpcContext, container: &LxcContainer) -> Result { use axum::extract::ws::Message; @@ -473,10 +397,8 @@ pub async fn connect(ctx: &RpcContext, container: &LxcContainer) -> Result { id, result }, - ) - .with_kind(ErrorKind::Serialization)?, + serde_json::to_string(&RpcResponse { id, result }) + .with_kind(ErrorKind::Serialization)?, )) .await .with_kind(ErrorKind::Network)?; @@ -612,28 +534,3 @@ pub async fn connect_cli(ctx: &CliContext, guid: Guid) -> Result<(), Error> { Ok(()) } - -pub async fn connect_rpc_cli( - HandlerArgs { - context, - parent_method, - method, - params, - inherited_params, - raw_params, - }: HandlerArgs, -) -> Result<(), Error> { - let ctx = context.clone(); - let guid = CallRemoteHandler::::new(from_fn_async(connect_rpc)) - .handle_async(HandlerArgs { - context, - parent_method, - method, - params: rpc_toolkit::util::Flat(params, Empty {}), - inherited_params, - raw_params, - }) - .await?; - - connect_cli(&ctx, guid).await -} diff --git a/core/startos/src/os_install/mod.rs b/core/startos/src/os_install/mod.rs index c3931b236..b1d9ef1c4 100644 --- a/core/startos/src/os_install/mod.rs +++ b/core/startos/src/os_install/mod.rs @@ -281,6 +281,10 @@ pub async fn execute( IoFormat::Yaml.to_vec(&ServerConfig { os_partitions: Some(part_info.clone()), ethernet_interface: Some(eth_iface), + #[cfg(feature = "dev")] + analytics_url: None, + #[cfg(not(feature = "dev"))] + analytics_url: Some("https://analytics.start9.com".parse()?), // TODO: FullMetal ..Default::default() })?, ) diff --git a/core/startos/src/registry/context.rs b/core/startos/src/registry/context.rs index 99d60307b..0461aa924 100644 --- a/core/startos/src/registry/context.rs +++ b/core/startos/src/registry/context.rs @@ -32,8 +32,8 @@ pub struct RegistryConfig { #[arg(short = 'l', long = "listen")] pub listen: Option, #[arg(short = 'h', long = "hostname")] - pub hostname: InternedString, - #[arg(short = 'p', long = "proxy")] + pub hostname: Option, + #[arg(short = 'p', long = "tor-proxy")] pub tor_proxy: Option, #[arg(short = 'd', long = "datadir")] pub datadir: Option, @@ -43,6 +43,9 @@ impl ContextConfig for RegistryConfig { self.config.take() } fn merge_with(&mut self, other: Self) { + self.listen = self.listen.take().or(other.listen); + self.hostname = self.hostname.take().or(other.hostname); + self.tor_proxy = self.tor_proxy.take().or(other.tor_proxy); self.datadir = self.datadir.take().or(other.datadir); } } @@ -92,7 +95,16 @@ impl RegistryContext { .map(Ok) .unwrap_or_else(|| "socks5h://localhost:9050".parse())?; Ok(Self(Arc::new(RegistryContextSeed { - hostname: config.hostname.clone(), + hostname: config + .hostname + .as_ref() + .ok_or_else(|| { + Error::new( + eyre!("missing required configuration: hostname"), + ErrorKind::NotFound, + ) + })? + .clone(), listen: config .listen .unwrap_or(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 5959)), @@ -169,7 +181,8 @@ impl CallRemote for CliContext { &AnySigningKey::Ed25519(self.developer_key()?.clone()), &body, &host, - )?.to_header(), + )? + .to_header(), ) .body(body) .send() diff --git a/sdk/lib/osBindings/AddAssetParams.ts b/sdk/lib/osBindings/AddAssetParams.ts index ce6128cf7..ffd7db675 100644 --- a/sdk/lib/osBindings/AddAssetParams.ts +++ b/sdk/lib/osBindings/AddAssetParams.ts @@ -6,7 +6,6 @@ import type { Version } from "./Version" export type AddAssetParams = { version: Version platform: string - upload: boolean url: string signature: AnySignature commitment: Blake3Commitment diff --git a/sdk/lib/osBindings/AddPackageParams.ts b/sdk/lib/osBindings/AddPackageParams.ts new file mode 100644 index 000000000..4395b9b8a --- /dev/null +++ b/sdk/lib/osBindings/AddPackageParams.ts @@ -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 AddPackageParams = { + url: string + commitment: MerkleArchiveCommitment + signature: AnySignature +} diff --git a/sdk/lib/osBindings/GetPackageResponse.ts b/sdk/lib/osBindings/GetPackageResponse.ts index 5bf24bfc0..3e1dd4e9d 100644 --- a/sdk/lib/osBindings/GetPackageResponse.ts +++ b/sdk/lib/osBindings/GetPackageResponse.ts @@ -4,6 +4,7 @@ import type { PackageVersionInfo } from "./PackageVersionInfo" import type { Version } from "./Version" export type GetPackageResponse = { + categories: string[] best: { [key: Version]: PackageVersionInfo } otherVersions?: { [key: Version]: PackageInfoShort } } diff --git a/sdk/lib/osBindings/GetPackageResponseFull.ts b/sdk/lib/osBindings/GetPackageResponseFull.ts index 579924291..e375dd489 100644 --- a/sdk/lib/osBindings/GetPackageResponseFull.ts +++ b/sdk/lib/osBindings/GetPackageResponseFull.ts @@ -3,6 +3,7 @@ import type { PackageVersionInfo } from "./PackageVersionInfo" import type { Version } from "./Version" export type GetPackageResponseFull = { + categories: string[] best: { [key: Version]: PackageVersionInfo } otherVersions: { [key: Version]: PackageVersionInfo } } diff --git a/sdk/lib/osBindings/HardwareRequirements.ts b/sdk/lib/osBindings/HardwareRequirements.ts index 4964bc66f..0e1da1f36 100644 --- a/sdk/lib/osBindings/HardwareRequirements.ts +++ b/sdk/lib/osBindings/HardwareRequirements.ts @@ -3,5 +3,5 @@ export type HardwareRequirements = { device: { [key: string]: string } ram: number | null - arch: Array | null + arch: string[] | null } diff --git a/sdk/lib/osBindings/OsVersionInfo.ts b/sdk/lib/osBindings/OsVersionInfo.ts index 7cd0fde9e..a88115350 100644 --- a/sdk/lib/osBindings/OsVersionInfo.ts +++ b/sdk/lib/osBindings/OsVersionInfo.ts @@ -7,7 +7,7 @@ export type OsVersionInfo = { headline: string releaseNotes: string sourceVersion: string - signers: Array + authorized: Array iso: { [key: string]: RegistryAsset } squashfs: { [key: string]: RegistryAsset } img: { [key: string]: RegistryAsset } diff --git a/sdk/lib/osBindings/PackageInfo.ts b/sdk/lib/osBindings/PackageInfo.ts index af340424f..6d07cd43e 100644 --- a/sdk/lib/osBindings/PackageInfo.ts +++ b/sdk/lib/osBindings/PackageInfo.ts @@ -4,6 +4,7 @@ import type { PackageVersionInfo } from "./PackageVersionInfo" import type { Version } from "./Version" export type PackageInfo = { - signers: Array + authorized: Array versions: { [key: Version]: PackageVersionInfo } + categories: string[] } diff --git a/sdk/lib/osBindings/PackageVersionInfo.ts b/sdk/lib/osBindings/PackageVersionInfo.ts index da82540cd..bdded46bd 100644 --- a/sdk/lib/osBindings/PackageVersionInfo.ts +++ b/sdk/lib/osBindings/PackageVersionInfo.ts @@ -17,7 +17,6 @@ export type PackageVersionInfo = { upstreamRepo: string supportSite: string marketingSite: string - categories: string[] osVersion: Version hardwareRequirements: HardwareRequirements sourceVersion: string | null diff --git a/sdk/lib/osBindings/index.ts b/sdk/lib/osBindings/index.ts index 16fc56a74..3059c6087 100644 --- a/sdk/lib/osBindings/index.ts +++ b/sdk/lib/osBindings/index.ts @@ -3,6 +3,7 @@ export { ActionId } from "./ActionId" export { ActionMetadata } from "./ActionMetadata" export { AddAdminParams } from "./AddAdminParams" export { AddAssetParams } from "./AddAssetParams" +export { AddPackageParams } from "./AddPackageParams" export { AddressInfo } from "./AddressInfo" export { AddSslOptions } from "./AddSslOptions" export { AddVersionParams } from "./AddVersionParams" From 8f7072d7e96e4898d5b7b2fe694ee33d0cdf2e42 Mon Sep 17 00:00:00 2001 From: Shadowy Super Coder Date: Tue, 4 Jun 2024 09:21:55 -0600 Subject: [PATCH 026/125] metrics wip --- core/startos/src/analytics/mod.rs | 69 ++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/core/startos/src/analytics/mod.rs b/core/startos/src/analytics/mod.rs index 52ca37948..6903ecfe1 100644 --- a/core/startos/src/analytics/mod.rs +++ b/core/startos/src/analytics/mod.rs @@ -1,19 +1,28 @@ use std::net::SocketAddr; use axum::Router; +use chrono::Utc; use futures::future::ready; -use rpc_toolkit::{Context, ParentHandler, Server}; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler, Server}; +use sqlx::query; +use ts_rs::TS; use crate::analytics::context::AnalyticsContext; use crate::middleware::cors::Cors; use crate::net::static_server::{bad_request, not_found, server_error}; use crate::net::web_server::WebServer; use crate::rpc_continuations::Guid; +use crate::Error; pub mod context; pub fn analytics_api() -> ParentHandler { - ParentHandler::new() // TODO: FullMetal + ParentHandler::new() + .subcommand("recordMetrics", from_fn_async(record_metrics).no_cli()) + .subcommand( + "recordUserActivity", + from_fn_async(record_user_activity).no_cli(), + ) } pub fn analytics_server_router(ctx: AnalyticsContext) -> Router { @@ -76,3 +85,59 @@ impl WebServer { Self::new(bind, analytics_server_router(ctx)) } } + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +struct MetricsParams { + version: char, + pkg_id: char, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +struct ActivityParams { + server_id: String, + os_version: String, + arch: String, +} + +async fn record_metrics( + ctx: AnalyticsContext, + MetricsParams { + version, + pkg_id, + }: MetricsParams, +) -> Result<(), Error> { + let pool = ctx.db; + let created_at = Utc::now().to_rfc3339(); + query!( + "INSERT INTO metric (created_at, version, pkg_id) VALUES ($1, $2, $3)", + created_at, + version, + pkg_id + ) + .execute(pool) + .await?; + Ok(()) +} + +async fn record_user_activity( + analytics_ctx: AnalyticsContext, + ActivityParams { server_id, os_version, arch }: ActivityParams, +) -> Result<(), Error> { + let pool = analytics_ctx.db; + let created_at = Utc::now().to_rfc3339(); + + query!("INSERT INTO user_activity (created_at, server_id, os_version, arch) VALUES ($1, $2, $3, $4)", + created_at, + server_id, + os_vers, + arch + ) + .execute(pool) + .await?; + + Ok(()) +} From fa347fd49d92a7373f0080243127fc1e87ba8c87 Mon Sep 17 00:00:00 2001 From: Shadowy Super Coder Date: Tue, 4 Jun 2024 11:53:30 -0600 Subject: [PATCH 027/125] remove record_metrics fn --- core/startos/src/analytics/mod.rs | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/core/startos/src/analytics/mod.rs b/core/startos/src/analytics/mod.rs index 6903ecfe1..551082d6b 100644 --- a/core/startos/src/analytics/mod.rs +++ b/core/startos/src/analytics/mod.rs @@ -18,7 +18,6 @@ pub mod context; pub fn analytics_api() -> ParentHandler { ParentHandler::new() - .subcommand("recordMetrics", from_fn_async(record_metrics).no_cli()) .subcommand( "recordUserActivity", from_fn_async(record_user_activity).no_cli(), @@ -86,14 +85,6 @@ impl WebServer { } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct MetricsParams { - version: char, - pkg_id: char, -} - #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] @@ -103,26 +94,6 @@ struct ActivityParams { arch: String, } -async fn record_metrics( - ctx: AnalyticsContext, - MetricsParams { - version, - pkg_id, - }: MetricsParams, -) -> Result<(), Error> { - let pool = ctx.db; - let created_at = Utc::now().to_rfc3339(); - query!( - "INSERT INTO metric (created_at, version, pkg_id) VALUES ($1, $2, $3)", - created_at, - version, - pkg_id - ) - .execute(pool) - .await?; - Ok(()) -} - async fn record_user_activity( analytics_ctx: AnalyticsContext, ActivityParams { server_id, os_version, arch }: ActivityParams, From 94875299920e8a54c5b0f45fbbd4adade2bb7a07 Mon Sep 17 00:00:00 2001 From: Shadowy Super Coder Date: Tue, 4 Jun 2024 11:54:49 -0600 Subject: [PATCH 028/125] remove os version from activity --- core/startos/src/analytics/mod.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/core/startos/src/analytics/mod.rs b/core/startos/src/analytics/mod.rs index 551082d6b..1fa08dd3f 100644 --- a/core/startos/src/analytics/mod.rs +++ b/core/startos/src/analytics/mod.rs @@ -90,21 +90,19 @@ impl WebServer { #[serde(rename_all = "camelCase")] struct ActivityParams { server_id: String, - os_version: String, arch: String, } async fn record_user_activity( - analytics_ctx: AnalyticsContext, - ActivityParams { server_id, os_version, arch }: ActivityParams, + ctx: AnalyticsContext, + ActivityParams { server_id, arch }: ActivityParams, ) -> Result<(), Error> { - let pool = analytics_ctx.db; + let pool = ctx.db; let created_at = Utc::now().to_rfc3339(); query!("INSERT INTO user_activity (created_at, server_id, os_version, arch) VALUES ($1, $2, $3, $4)", created_at, server_id, - os_vers, arch ) .execute(pool) From 2c12af5af8668cb82c2ffc85eb6a5bbc6e455d4c Mon Sep 17 00:00:00 2001 From: Jade <2364004+Blu-J@users.noreply.github.com> Date: Thu, 6 Jun 2024 15:39:54 -0600 Subject: [PATCH 029/125] Feature/network (#2622) * Feature: Add in the clear bindings * wip: Working on network * fix: Make it so the config gives the url * chore: Remove the repeated types * chore: Add in the todo's here * chore: UPdate and remove some poorly name var * chore: Remove the clear-bindings impl * chore: Remove the wrapper * handle HostnameInfo for Host bindings Co-authored-by: Jade * ?? * chore: Make the install work * Fix: Url's not being created * chore: Fix the local onion in url * include port in hostname * Chore of adding a comment just to modify. --------- Co-authored-by: Aiden McClelland Co-authored-by: Jade --- container-runtime/package-lock.json | 16 +- container-runtime/package.json | 2 +- .../src/Adapters/HostSystemStartOs.ts | 10 +- .../Systems/SystemForEmbassy/MainLoop.ts | 2 - .../Systems/SystemForEmbassy/index.ts | 140 +++++- .../Systems/SystemForEmbassy/matchManifest.ts | 1 + .../SystemForEmbassy/oldEmbassyTypes.ts | 1 + core/models/src/id/mod.rs | 2 +- core/startos/src/db/model/package.rs | 8 +- core/startos/src/disk/mod.rs | 4 +- core/startos/src/net/dhcp.rs | 2 +- core/startos/src/net/host/binding.rs | 65 ++- core/startos/src/net/host/mod.rs | 19 +- core/startos/src/net/net_controller.rs | 264 ++++++---- core/startos/src/net/service_interface.rs | 39 +- core/startos/src/net/vhost.rs | 2 +- core/startos/src/registry/auth.rs | 1 - core/startos/src/service/mod.rs | 7 +- .../src/service/persistent_container.rs | 2 +- .../src/service/service_effect_handler.rs | 208 +++----- sdk/lib/StartSdk.ts | 10 +- sdk/lib/interfaces/Host.ts | 86 ++-- sdk/lib/interfaces/Origin.ts | 19 +- sdk/lib/osBindings/AddAssetParams.ts | 1 - sdk/lib/osBindings/AddPackageParams.ts | 9 + sdk/lib/osBindings/AddSslOptions.ts | 3 +- sdk/lib/osBindings/AddressInfo.ts | 5 +- sdk/lib/osBindings/BindInfo.ts | 3 +- sdk/lib/osBindings/BindOptions.ts | 1 - sdk/lib/osBindings/BindParams.ts | 1 - .../ExportServiceInterfaceParams.ts | 4 - sdk/lib/osBindings/ExportedHostInfo.ts | 10 - sdk/lib/osBindings/ExportedHostnameInfo.ts | 12 - sdk/lib/osBindings/GetHostInfoParams.ts | 5 +- sdk/lib/osBindings/GetHostInfoParamsKind.ts | 3 - sdk/lib/osBindings/GetPackageResponse.ts | 1 + sdk/lib/osBindings/GetPackageResponseFull.ts | 1 + sdk/lib/osBindings/GetPrimaryUrlParams.ts | 5 +- .../osBindings/GetServiceInterfaceParams.ts | 3 +- sdk/lib/osBindings/HardwareRequirements.ts | 2 +- sdk/lib/osBindings/Host.ts | 6 +- sdk/lib/osBindings/HostnameInfo.ts | 12 + sdk/lib/osBindings/{HostInfo.ts => Hosts.ts} | 2 +- .../{ExportedIpHostname.ts => IpHostname.ts} | 2 +- ...{ReverseProxyDestination.ts => LanInfo.ts} | 7 +- ...ortedOnionHostname.ts => OnionHostname.ts} | 2 +- sdk/lib/osBindings/OsVersionInfo.ts | 2 +- sdk/lib/osBindings/PackageDataEntry.ts | 8 +- sdk/lib/osBindings/PackageInfo.ts | 3 +- sdk/lib/osBindings/PackageVersionInfo.ts | 1 - sdk/lib/osBindings/ReverseProxyBind.ts | 3 - sdk/lib/osBindings/ReverseProxyHttp.ts | 3 - sdk/lib/osBindings/ReverseProxyParams.ts | 10 - .../ServiceInterfaceWithHostInfo.ts | 17 - sdk/lib/osBindings/index.ts | 17 +- sdk/lib/test/host.test.ts | 1 + sdk/lib/test/startosTypeValidation.test.ts | 2 - sdk/lib/types.ts | 97 +--- sdk/lib/util/Hostname.ts | 25 + sdk/lib/util/getServiceInterface.ts | 178 +++---- sdk/lib/util/getServiceInterfaces.ts | 40 +- sdk/lib/util/index.ts | 2 + .../app-interfaces/app-interfaces.page.ts | 57 +-- .../ui/src/app/services/api/api.fixures.ts | 450 +----------------- .../ui/src/app/services/api/mock-patch.ts | 352 +------------- .../ui/src/app/services/config.service.ts | 31 +- .../src/app/services/ui-launcher.service.ts | 5 +- 67 files changed, 798 insertions(+), 1516 deletions(-) create mode 100644 sdk/lib/osBindings/AddPackageParams.ts delete mode 100644 sdk/lib/osBindings/ExportedHostInfo.ts delete mode 100644 sdk/lib/osBindings/ExportedHostnameInfo.ts delete mode 100644 sdk/lib/osBindings/GetHostInfoParamsKind.ts create mode 100644 sdk/lib/osBindings/HostnameInfo.ts rename sdk/lib/osBindings/{HostInfo.ts => Hosts.ts} (79%) rename sdk/lib/osBindings/{ExportedIpHostname.ts => IpHostname.ts} (94%) rename sdk/lib/osBindings/{ReverseProxyDestination.ts => LanInfo.ts} (55%) rename sdk/lib/osBindings/{ExportedOnionHostname.ts => OnionHostname.ts} (82%) delete mode 100644 sdk/lib/osBindings/ReverseProxyBind.ts delete mode 100644 sdk/lib/osBindings/ReverseProxyHttp.ts delete mode 100644 sdk/lib/osBindings/ReverseProxyParams.ts delete mode 100644 sdk/lib/osBindings/ServiceInterfaceWithHostInfo.ts create mode 100644 sdk/lib/util/Hostname.ts diff --git a/container-runtime/package-lock.json b/container-runtime/package-lock.json index dd8ba7855..838dcb769 100644 --- a/container-runtime/package-lock.json +++ b/container-runtime/package-lock.json @@ -14,7 +14,7 @@ "filebrowser": "^1.0.0", "isomorphic-fetch": "^3.0.0", "node-fetch": "^3.1.0", - "ts-matches": "^5.4.1", + "ts-matches": "^5.5.1", "tslib": "^2.5.3", "typescript": "^5.1.3", "yaml": "^2.3.1" @@ -29,7 +29,7 @@ }, "../sdk/dist": { "name": "@start9labs/start-sdk", - "version": "0.4.0-rev0.lib0.rc8.beta10", + "version": "0.3.6-alpha1", "license": "MIT", "dependencies": { "isomorphic-fetch": "^3.0.0", @@ -2428,9 +2428,9 @@ } }, "node_modules/ts-matches": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-5.4.1.tgz", - "integrity": "sha512-kXrY75F0s0WD15N2bWKDScKlKgwnusN6dTRzGs1N7LlxQRnazrsBISC1HL4sy2adsyk65Zbx3Ui3IGN8leAFOQ==" + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-5.5.1.tgz", + "integrity": "sha512-UFYaKgfqlg9FROK7bdpYqFwG1CJvP4kOJdjXuWoqxo9jCmANoDw1GxkSCpJgoTeIiSTaTH5Qr1klSspb8c+ydg==" }, "node_modules/tslib": { "version": "2.6.2", @@ -4195,9 +4195,9 @@ } }, "ts-matches": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-5.4.1.tgz", - "integrity": "sha512-kXrY75F0s0WD15N2bWKDScKlKgwnusN6dTRzGs1N7LlxQRnazrsBISC1HL4sy2adsyk65Zbx3Ui3IGN8leAFOQ==" + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-5.5.1.tgz", + "integrity": "sha512-UFYaKgfqlg9FROK7bdpYqFwG1CJvP4kOJdjXuWoqxo9jCmANoDw1GxkSCpJgoTeIiSTaTH5Qr1klSspb8c+ydg==" }, "tslib": { "version": "2.6.2", diff --git a/container-runtime/package.json b/container-runtime/package.json index f5e34a618..e2c56afff 100644 --- a/container-runtime/package.json +++ b/container-runtime/package.json @@ -22,7 +22,7 @@ "filebrowser": "^1.0.0", "isomorphic-fetch": "^3.0.0", "node-fetch": "^3.1.0", - "ts-matches": "^5.4.1", + "ts-matches": "^5.5.1", "tslib": "^2.5.3", "typescript": "^5.1.3", "yaml": "^2.3.1" diff --git a/container-runtime/src/Adapters/HostSystemStartOs.ts b/container-runtime/src/Adapters/HostSystemStartOs.ts index b745799e1..ba2076f52 100644 --- a/container-runtime/src/Adapters/HostSystemStartOs.ts +++ b/container-runtime/src/Adapters/HostSystemStartOs.ts @@ -96,7 +96,10 @@ export class HostSystemStartOs implements Effects { } bind(...[options]: Parameters) { - return this.rpcRound("bind", options) as ReturnType + return this.rpcRound("bind", { + ...options, + stack: new Error().stack, + }) as ReturnType } clearBindings(...[]: Parameters) { return this.rpcRound("clearBindings", null) as ReturnType< @@ -228,11 +231,6 @@ export class HostSystemStartOs implements Effects { restart(...[]: Parameters) { return this.rpcRound("restart", null) } - reverseProxy(...[options]: Parameters) { - return this.rpcRound("reverseProxy", options) as ReturnType< - T.Effects["reverseProxy"] - > - } running(...[packageId]: Parameters) { return this.rpcRound("running", { packageId }) as ReturnType< T.Effects["running"] diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts index 917bfff83..08bf944ed 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts @@ -97,11 +97,9 @@ export class MainLoop { id: interfaceId, internalPort, preferredExternalPort: torConf?.external || internalPort, - scheme: "http", secure: null, addSsl: lanConf?.ssl ? { - scheme: "https", preferredExternalPort: lanConf.external, alpn: { specified: ["http/1.1"] }, } diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index ada893b2b..8e0cb28c5 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -31,6 +31,16 @@ import { HostSystemStartOs } from "../../HostSystemStartOs" import { JsonPath, unNestPath } from "../../../Models/JsonPath" import { RpcResult, matchRpcResult } from "../../RpcListener" import { CT } from "@start9labs/start-sdk" +import { + AddSslOptions, + BindOptions, +} from "@start9labs/start-sdk/cjs/lib/osBindings" +import { + BindOptionsByProtocol, + Host, + MultiHost, +} from "@start9labs/start-sdk/cjs/lib/interfaces/Host" +import { ServiceInterfaceBuilder } from "@start9labs/start-sdk/cjs/lib/interfaces/ServiceInterfaceBuilder" type Optional
= A | undefined | null function todo(): never { @@ -335,6 +345,85 @@ export class SystemForEmbassy implements System { await this.migration(effects, previousVersion, timeoutMs) await effects.setMainStatus({ status: "stopped" }) await this.exportActions(effects) + await this.exportNetwork(effects) + } + async exportNetwork(effects: HostSystemStartOs) { + for (const [id, interfaceValue] of Object.entries( + this.manifest.interfaces, + )) { + const host = new MultiHost({ effects, id }) + const internalPorts = new Set( + Object.values(interfaceValue["tor-config"]?.["port-mapping"] ?? {}) + .map(Number.parseInt) + .concat( + ...Object.values(interfaceValue["lan-config"] ?? {}).map( + (c) => c.internal, + ), + ) + .filter(Boolean), + ) + const bindings = Array.from(internalPorts).map< + [number, BindOptionsByProtocol] + >((port) => { + const lanPort = Object.entries(interfaceValue["lan-config"] ?? {}).find( + ([external, internal]) => internal.internal === port, + )?.[0] + const torPort = Object.entries( + interfaceValue["tor-config"]?.["port-mapping"] ?? {}, + ).find( + ([external, internal]) => Number.parseInt(internal) === port, + )?.[0] + let addSsl: AddSslOptions | null = null + if (lanPort) { + const lanPortNum = Number.parseInt(lanPort) + if (lanPortNum === 443) { + return [port, { protocol: "http", preferredExternalPort: 80 }] + } + addSsl = { + preferredExternalPort: lanPortNum, + alpn: { specified: [] }, + } + } + return [ + port, + { + secure: null, + preferredExternalPort: Number.parseInt( + torPort || lanPort || String(port), + ), + addSsl, + }, + ] + }) + + await Promise.all( + bindings.map(async ([internal, options]) => { + if (internal == null) { + return + } + if (options?.preferredExternalPort == null) { + return + } + const origin = await host.bindPort(internal, options) + await origin.export([ + new ServiceInterfaceBuilder({ + effects, + name: interfaceValue.name, + id: `${id}-${internal}`, + description: interfaceValue.description, + hasPrimary: false, + disabled: false, + type: "api", + masked: false, + path: "", + schemeOverride: null, + search: {}, + username: null, + }), + ]) + }), + ) + } } async exportActions(effects: HostSystemStartOs) { const manifest = this.manifest @@ -486,6 +575,7 @@ export class SystemForEmbassy implements System { const newConfig = structuredClone(newConfigWithoutPointers) await updateConfig( effects, + this.manifest, await this.getConfigUncleaned(effects, timeoutMs).then((x) => x.spec), newConfig, ) @@ -866,6 +956,7 @@ function cleanConfigFromPointers( async function updateConfig( effects: HostSystemStartOs, + manifest: Manifest, spec: unknown, mutConfigValue: unknown, ) { @@ -877,7 +968,12 @@ async function updateConfig( const newConfigValue = mutConfigValue[key] if (matchSpec.test(specValue)) { const updateObject = { spec: null } - await updateConfig(effects, { spec: specValue.spec }, updateObject) + await updateConfig( + effects, + manifest, + { spec: specValue.spec }, + updateObject, + ) mutConfigValue[key] = updateObject.spec } if ( @@ -899,20 +995,48 @@ async function updateConfig( if (matchPointerPackage.test(specValue)) { if (specValue.target === "tor-key") throw new Error("This service uses an unsupported target TorKey") + + const specInterface = specValue.interface + const serviceInterfaceId = extractServiceInterfaceId( + manifest, + specInterface, + ) const filled = await utils .getServiceInterface(effects, { packageId: specValue["package-id"], - id: specValue.interface, + id: serviceInterfaceId, }) .once() - .catch(() => null) - - mutConfigValue[key] = + .catch((x) => { + console.error("Could not get the service interface", x) + return null + }) + const catchFn = (fn: () => X) => { + try { + return fn() + } catch (e) { + return undefined + } + } + const url: string = filled === null ? "" - : specValue.target === "lan-address" - ? filled.addressInfo.localHostnames[0] - : filled.addressInfo.onionHostnames[0] + : catchFn(() => + utils.hostnameInfoToAddress( + specValue.target === "lan-address" + ? filled.addressInfo.localHostnames[0] || + filled.addressInfo.onionHostnames[0] + : filled.addressInfo.onionHostnames[0] || + filled.addressInfo.localHostnames[0], + ), + ) || "" + mutConfigValue[key] = url } } } +function extractServiceInterfaceId(manifest: Manifest, specInterface: string) { + let serviceInterfaceId + const lanConfig = manifest.interfaces[specInterface]?.["lan-config"] || {} + serviceInterfaceId = `${specInterface}-${Object.entries(lanConfig)[0]?.[1]?.internal}` + return serviceInterfaceId +} diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.ts index d4758669b..8ce6cabbc 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/matchManifest.ts @@ -72,6 +72,7 @@ export const matchManifest = object( object( { name: string, + description: string, "tor-config": object({ "port-mapping": dictionary([string, string]), }), diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/oldEmbassyTypes.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/oldEmbassyTypes.ts index 35b95e095..0d7521626 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/oldEmbassyTypes.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/oldEmbassyTypes.ts @@ -99,6 +99,7 @@ export type Effects = { /** Sandbox mode lets us read but not write */ is_sandboxed(): boolean + // Does a volume and path exist? exists(input: { volumeId: string; path: string }): Promise fetch( diff --git a/core/models/src/id/mod.rs b/core/models/src/id/mod.rs index 10882d4f1..85c9d8255 100644 --- a/core/models/src/id/mod.rs +++ b/core/models/src/id/mod.rs @@ -24,7 +24,7 @@ pub use service_interface::ServiceInterfaceId; pub use volume::VolumeId; lazy_static::lazy_static! { - static ref ID_REGEX: Regex = Regex::new("^[a-z]+(-[a-z]+)*$").unwrap(); + static ref ID_REGEX: Regex = Regex::new("^[a-z]+(-[a-z0-9]+)*$").unwrap(); pub static ref SYSTEM_ID: Id = Id(InternedString::intern("x_system")); } diff --git a/core/startos/src/db/model/package.rs b/core/startos/src/db/model/package.rs index 3fad0ad9f..8bb5a9517 100644 --- a/core/startos/src/db/model/package.rs +++ b/core/startos/src/db/model/package.rs @@ -10,8 +10,8 @@ use reqwest::Url; use serde::{Deserialize, Serialize}; use ts_rs::TS; -use crate::net::host::HostInfo; -use crate::net::service_interface::ServiceInterfaceWithHostInfo; +use crate::net::host::Hosts; +use crate::net::service_interface::ServiceInterface; use crate::prelude::*; use crate::progress::FullProgress; use crate::s9pk::manifest::Manifest; @@ -333,8 +333,8 @@ pub struct PackageDataEntry { pub last_backup: Option>, pub current_dependencies: CurrentDependencies, pub actions: BTreeMap, - pub service_interfaces: BTreeMap, - pub hosts: HostInfo, + pub service_interfaces: BTreeMap, + pub hosts: Hosts, #[ts(type = "string[]")] pub store_exposed_dependents: Vec, } diff --git a/core/startos/src/disk/mod.rs b/core/startos/src/disk/mod.rs index 68eb2b187..705a34f98 100644 --- a/core/startos/src/disk/mod.rs +++ b/core/startos/src/disk/mod.rs @@ -1,8 +1,6 @@ use std::path::{Path, PathBuf}; -use rpc_toolkit::{ - from_fn_async, CallRemoteHandler, Context, Empty, HandlerExt, ParentHandler, -}; +use rpc_toolkit::{from_fn_async, CallRemoteHandler, Context, Empty, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use crate::context::{CliContext, RpcContext}; diff --git a/core/startos/src/net/dhcp.rs b/core/startos/src/net/dhcp.rs index 104cda961..ffcb9774b 100644 --- a/core/startos/src/net/dhcp.rs +++ b/core/startos/src/net/dhcp.rs @@ -3,7 +3,7 @@ use std::net::IpAddr; use clap::Parser; use futures::TryStreamExt; -use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; +use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; use ts_rs::TS; diff --git a/core/startos/src/net/host/binding.rs b/core/startos/src/net/host/binding.rs index 8301821f5..76dd04059 100644 --- a/core/startos/src/net/host/binding.rs +++ b/core/startos/src/net/host/binding.rs @@ -1,4 +1,3 @@ -use imbl_value::InternedString; use serde::{Deserialize, Serialize}; use ts_rs::TS; @@ -11,17 +10,31 @@ use crate::prelude::*; #[ts(export)] pub struct BindInfo { pub options: BindOptions, - pub assigned_lan_port: Option, + pub lan: LanInfo, +} +#[derive(Clone, Copy, Debug, Deserialize, Serialize, TS, PartialEq, Eq, PartialOrd, Ord)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct LanInfo { + pub assigned_port: Option, + pub assigned_ssl_port: Option, } impl BindInfo { pub fn new(available_ports: &mut AvailablePorts, options: BindOptions) -> Result { - let mut assigned_lan_port = None; - if options.add_ssl.is_some() || options.secure.is_some() { - assigned_lan_port = Some(available_ports.alloc()?); + let mut assigned_port = None; + let mut assigned_ssl_port = None; + if options.secure.is_some() { + assigned_port = Some(available_ports.alloc()?); + } + if options.add_ssl.is_some() { + assigned_ssl_port = Some(available_ports.alloc()?); } Ok(Self { options, - assigned_lan_port, + lan: LanInfo { + assigned_port, + assigned_ssl_port, + }, }) } pub fn update( @@ -29,29 +42,38 @@ impl BindInfo { available_ports: &mut AvailablePorts, options: BindOptions, ) -> Result { - let Self { - mut assigned_lan_port, - .. - } = self; - if options.add_ssl.is_some() || options.secure.is_some() { - assigned_lan_port = if let Some(port) = assigned_lan_port.take() { + let Self { mut lan, .. } = self; + if options + .secure + .map_or(false, |s| !(s.ssl && options.add_ssl.is_some())) + // doesn't make sense to have 2 listening ports, both with ssl + { + lan.assigned_port = if let Some(port) = lan.assigned_port.take() { Some(port) } else { Some(available_ports.alloc()?) }; } else { - if let Some(port) = assigned_lan_port.take() { + if let Some(port) = lan.assigned_port.take() { available_ports.free([port]); } } - Ok(Self { - options, - assigned_lan_port, - }) + if options.add_ssl.is_some() { + lan.assigned_ssl_port = if let Some(port) = lan.assigned_ssl_port.take() { + Some(port) + } else { + Some(available_ports.alloc()?) + }; + } else { + if let Some(port) = lan.assigned_ssl_port.take() { + available_ports.free([port]); + } + } + Ok(Self { options, lan }) } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] pub struct Security { @@ -62,8 +84,6 @@ pub struct Security { #[serde(rename_all = "camelCase")] #[ts(export)] pub struct BindOptions { - #[ts(type = "string | null")] - pub scheme: Option, pub preferred_external_port: u16, pub add_ssl: Option, pub secure: Option, @@ -73,11 +93,8 @@ pub struct BindOptions { #[serde(rename_all = "camelCase")] #[ts(export)] pub struct AddSslOptions { - #[ts(type = "string | null")] - pub scheme: Option, pub preferred_external_port: u16, // #[serde(default)] // pub add_x_forwarded_headers: bool, // TODO - #[serde(default)] - pub alpn: AlpnInfo, + pub alpn: Option, } diff --git a/core/startos/src/net/host/mod.rs b/core/startos/src/net/host/mod.rs index 2d8599ba9..6cbb2dfd5 100644 --- a/core/startos/src/net/host/mod.rs +++ b/core/startos/src/net/host/mod.rs @@ -3,13 +3,13 @@ use std::collections::{BTreeMap, BTreeSet}; use imbl_value::InternedString; use models::{HostId, PackageId}; use serde::{Deserialize, Serialize}; -use torut::onion::{OnionAddressV3, TorSecretKeyV3}; use ts_rs::TS; use crate::db::model::DatabaseModel; use crate::net::forward::AvailablePorts; use crate::net::host::address::HostAddress; use crate::net::host::binding::{BindInfo, BindOptions}; +use crate::net::service_interface::HostnameInfo; use crate::prelude::*; pub mod address; @@ -23,7 +23,8 @@ pub struct Host { pub kind: HostKind, pub bindings: BTreeMap, pub addresses: BTreeSet, - pub primary: Option, + /// COMPUTED: NetService::update + pub hostname_info: BTreeMap>, // internal port -> Hostnames } impl AsRef for Host { fn as_ref(&self) -> &Host { @@ -36,7 +37,7 @@ impl Host { kind, bindings: BTreeMap::new(), addresses: BTreeSet::new(), - primary: None, + hostname_info: BTreeMap::new(), } } } @@ -53,9 +54,9 @@ pub enum HostKind { #[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] #[model = "Model"] #[ts(export)] -pub struct HostInfo(BTreeMap); +pub struct Hosts(pub BTreeMap); -impl Map for HostInfo { +impl Map for Hosts { type Key = HostId; type Value = Host; fn key_str(key: &Self::Key) -> Result, Error> { @@ -75,7 +76,7 @@ pub fn host_for<'a>( fn host_info<'a>( db: &'a mut DatabaseModel, package_id: &PackageId, - ) -> Result<&'a mut Model, Error> { + ) -> Result<&'a mut Model, Error> { Ok::<_, Error>( db.as_public_mut() .as_package_data_mut() @@ -129,9 +130,3 @@ impl Model { }) } } - -impl HostInfo { - pub fn get_host_primary(&self, host_id: &HostId) -> Option { - self.0.get(&host_id).and_then(|h| h.primary.clone()) - } -} diff --git a/core/startos/src/net/net_controller.rs b/core/startos/src/net/net_controller.rs index 205cc4e36..69c5c7940 100644 --- a/core/startos/src/net/net_controller.rs +++ b/core/startos/src/net/net_controller.rs @@ -4,7 +4,6 @@ use std::sync::{Arc, Weak}; use color_eyre::eyre::eyre; use imbl::OrdMap; -use lazy_format::lazy_format; use models::{HostId, OptionExt, PackageId}; use torut::onion::{OnionAddressV3, TorSecretKeyV3}; use tracing::instrument; @@ -15,8 +14,9 @@ use crate::hostname::Hostname; use crate::net::dns::DnsController; use crate::net::forward::LanPortForwardController; use crate::net::host::address::HostAddress; -use crate::net::host::binding::{AddSslOptions, BindOptions}; +use crate::net::host::binding::{AddSslOptions, BindOptions, LanInfo}; use crate::net::host::{host_for, Host, HostKind}; +use crate::net::service_interface::{HostnameInfo, IpHostname, OnionHostname}; use crate::net::tor::TorController; use crate::net::vhost::{AlpnInfo, VHostController}; use crate::prelude::*; @@ -164,7 +164,7 @@ impl NetController { #[derive(Default, Debug)] struct HostBinds { - lan: BTreeMap, Arc<()>)>, + lan: BTreeMap, Vec>)>, tor: BTreeMap, Vec>)>, } @@ -209,105 +209,173 @@ impl NetService { .await?; self.update(id, host).await } + pub async fn clear_bindings(&mut self) -> Result<(), Error> { + // TODO BLUJ + Ok(()) + } async fn update(&mut self, id: HostId, host: Host) -> Result<(), Error> { - dbg!(&host); - dbg!(&self.binds); let ctrl = self.net_controller()?; + let mut hostname_info = BTreeMap::new(); let binds = { if !self.binds.contains_key(&id) { self.binds.insert(id.clone(), Default::default()); } self.binds.get_mut(&id).unwrap() }; - if true - // TODO: if should listen lan - { - for (port, bind) in &host.bindings { - let old_lan_bind = binds.lan.remove(port); - let old_lan_port = old_lan_bind.as_ref().map(|(external, _, _)| *external); - let lan_bind = old_lan_bind.filter(|(external, ssl, _)| { - ssl == &bind.options.add_ssl - && bind.assigned_lan_port.as_ref() == Some(external) - }); // only keep existing binding if relevant details match - if let Some(external) = bind.assigned_lan_port { - let new_lan_bind = if let Some(b) = lan_bind { - b - } else { - if let Some(ssl) = &bind.options.add_ssl { - let rc = ctrl - .vhost + let peek = ctrl.db.peek().await; + + // LAN + let server_info = peek.as_public().as_server_info(); + let ip_info = server_info.as_ip_info().de()?; + let hostname = server_info.as_hostname().de()?; + for (port, bind) in &host.bindings { + let old_lan_bind = binds.lan.remove(port); + let old_lan_port = old_lan_bind.as_ref().map(|(external, _, _)| *external); + let lan_bind = old_lan_bind + .filter(|(external, ssl, _)| ssl == &bind.options.add_ssl && bind.lan == *external); // only keep existing binding if relevant details match + if bind.lan.assigned_port.is_some() || bind.lan.assigned_ssl_port.is_some() { + let new_lan_bind = if let Some(b) = lan_bind { + b + } else { + let mut rcs = Vec::with_capacity(2); + if let Some(ssl) = &bind.options.add_ssl { + let external = bind + .lan + .assigned_ssl_port + .or_not_found("assigned ssl port")?; + rcs.push( + ctrl.vhost .add( None, external, (self.ip, *port).into(), - if bind.options.secure.as_ref().map_or(false, |s| s.ssl) { - Ok(()) + if let Some(alpn) = ssl.alpn.clone() { + Err(alpn) } else { - Err(ssl.alpn.clone()) + if bind.options.secure.as_ref().map_or(false, |s| s.ssl) { + Ok(()) + } else { + Err(AlpnInfo::Reflect) + } }, ) - .await?; - (*port, Some(ssl.clone()), rc) + .await?, + ); + } + if let Some(security) = bind.options.secure { + if bind.options.add_ssl.is_some() && security.ssl { + // doesn't make sense to have 2 listening ports, both with ssl } else { - let rc = ctrl.forward.add(external, (self.ip, *port).into()).await?; - (*port, None, rc) + let external = + bind.lan.assigned_port.or_not_found("assigned lan port")?; + rcs.push(ctrl.forward.add(external, (self.ip, *port).into()).await?); } - }; - binds.lan.insert(*port, new_lan_bind); + } + (bind.lan, bind.options.add_ssl.clone(), rcs) + }; + let mut bind_hostname_info: Vec = + hostname_info.remove(port).unwrap_or_default(); + for (interface, ip_info) in &ip_info { + bind_hostname_info.push(HostnameInfo::Ip { + network_interface_id: interface.clone(), + public: false, + hostname: IpHostname::Local { + value: format!("{hostname}.local"), + port: new_lan_bind.0.assigned_port, + ssl_port: new_lan_bind.0.assigned_ssl_port, + }, + }); + if let Some(ipv4) = ip_info.ipv4 { + bind_hostname_info.push(HostnameInfo::Ip { + network_interface_id: interface.clone(), + public: false, + hostname: IpHostname::Ipv4 { + value: ipv4, + port: new_lan_bind.0.assigned_port, + ssl_port: new_lan_bind.0.assigned_ssl_port, + }, + }); + } + if let Some(ipv6) = ip_info.ipv6 { + bind_hostname_info.push(HostnameInfo::Ip { + network_interface_id: interface.clone(), + public: false, + hostname: IpHostname::Ipv6 { + value: ipv6, + port: new_lan_bind.0.assigned_port, + ssl_port: new_lan_bind.0.assigned_ssl_port, + }, + }); + } } - if let Some(external) = old_lan_port { + hostname_info.insert(*port, bind_hostname_info); + binds.lan.insert(*port, new_lan_bind); + } + if let Some(lan) = old_lan_port { + if let Some(external) = lan.assigned_ssl_port { ctrl.vhost.gc(None, external).await?; + } + if let Some(external) = lan.assigned_port { ctrl.forward.gc(external).await?; } } - let mut removed = BTreeSet::new(); - let mut removed_ssl = BTreeSet::new(); - binds.lan.retain(|internal, (external, ssl, _)| { - if host.bindings.contains_key(internal) { - true - } else { - if ssl.is_some() { - removed_ssl.insert(*external); - } else { - removed.insert(*external); - } - false - } - }); - for external in removed { - ctrl.forward.gc(external).await?; + } + let mut removed = BTreeSet::new(); + binds.lan.retain(|internal, (external, _, _)| { + if host.bindings.contains_key(internal) { + true + } else { + removed.insert(*external); + + false } - for external in removed_ssl { + }); + for lan in removed { + if let Some(external) = lan.assigned_ssl_port { ctrl.vhost.gc(None, external).await?; } + if let Some(external) = lan.assigned_port { + ctrl.forward.gc(external).await?; + } } - let tor_binds: OrdMap = host - .bindings - .iter() - .flat_map(|(internal, info)| { - let non_ssl = ( - info.options.preferred_external_port, - SocketAddr::from((self.ip, *internal)), + + struct TorHostnamePorts { + non_ssl: Option, + ssl: Option, + } + let mut tor_hostname_ports = BTreeMap::::new(); + let mut tor_binds = OrdMap::::new(); + for (internal, info) in &host.bindings { + tor_binds.insert( + info.options.preferred_external_port, + SocketAddr::from((self.ip, *internal)), + ); + if let (Some(ssl), Some(ssl_internal)) = + (&info.options.add_ssl, info.lan.assigned_ssl_port) + { + tor_binds.insert( + ssl.preferred_external_port, + SocketAddr::from(([127, 0, 0, 1], ssl_internal)), ); - if let (Some(ssl), Some(ssl_internal)) = - (&info.options.add_ssl, info.assigned_lan_port) - { - itertools::Either::Left( - [ - ( - ssl.preferred_external_port, - SocketAddr::from(([127, 0, 0, 1], ssl_internal)), - ), - non_ssl, - ] - .into_iter(), - ) - } else { - itertools::Either::Right([non_ssl].into_iter()) - } - }) - .collect(); + tor_hostname_ports.insert( + *internal, + TorHostnamePorts { + non_ssl: Some(info.options.preferred_external_port) + .filter(|p| *p != ssl.preferred_external_port), + ssl: Some(ssl.preferred_external_port), + }, + ); + } else { + tor_hostname_ports.insert( + *internal, + TorHostnamePorts { + non_ssl: Some(info.options.preferred_external_port), + ssl: None, + }, + ); + } + } let mut keep_tor_addrs = BTreeSet::new(); for addr in match host.kind { HostKind::Multi => { @@ -324,13 +392,10 @@ impl NetService { let new_tor_bind = if let Some(tor_bind) = tor_bind { tor_bind } else { - let key = ctrl - .db - .peek() - .await - .into_private() - .into_key_store() - .into_onion() + let key = peek + .as_private() + .as_key_store() + .as_onion() .get_key(address)?; let rcs = ctrl .tor @@ -338,6 +403,18 @@ impl NetService { .await?; (tor_binds.clone(), rcs) }; + for (internal, ports) in &tor_hostname_ports { + let mut bind_hostname_info = + hostname_info.remove(internal).unwrap_or_default(); + bind_hostname_info.push(HostnameInfo::Onion { + hostname: OnionHostname { + value: address.to_string(), + port: ports.non_ssl, + ssl_port: ports.ssl, + }, + }); + hostname_info.insert(*internal, bind_hostname_info); + } binds.tor.insert(address.clone(), new_tor_bind); } } @@ -347,6 +424,14 @@ impl NetService { ctrl.tor.gc(Some(addr.clone()), None).await?; } } + self.net_controller()? + .db + .mutate(|db| { + host_for(db, &self.id, &id, host.kind)? + .as_hostname_info_mut() + .ser(&hostname_info) + }) + .await?; Ok(()) } @@ -355,12 +440,13 @@ impl NetService { let mut errors = ErrorCollection::new(); if let Some(ctrl) = Weak::upgrade(&self.controller) { for (_, binds) in std::mem::take(&mut self.binds) { - for (_, (external, ssl, rc)) in binds.lan { + for (_, (lan, _, rc)) in binds.lan { drop(rc); - if ssl.is_some() { - errors.handle(ctrl.vhost.gc(None, external).await); - } else { - errors.handle(ctrl.forward.gc(external).await); + if let Some(external) = lan.assigned_ssl_port { + ctrl.vhost.gc(None, external).await?; + } + if let Some(external) = lan.assigned_port { + ctrl.forward.gc(external).await?; } } for (addr, (_, rcs)) in binds.tor { @@ -384,12 +470,12 @@ impl NetService { self.ip } - pub fn get_ext_port(&self, host_id: HostId, internal_port: u16) -> Result { + pub fn get_ext_port(&self, host_id: HostId, internal_port: u16) -> Result { let host_id_binds = self.binds.get_key_value(&host_id); match host_id_binds { Some((_, binds)) => { - if let Some(ext_port_info) = binds.lan.get(&internal_port) { - Ok(ext_port_info.0) + if let Some((lan, _, _)) = binds.lan.get(&internal_port) { + Ok(*lan) } else { Err(Error::new( eyre!( diff --git a/core/startos/src/net/service_interface.rs b/core/startos/src/net/service_interface.rs index 9a4659cfd..e905be545 100644 --- a/core/startos/src/net/service_interface.rs +++ b/core/startos/src/net/service_interface.rs @@ -1,51 +1,32 @@ use std::net::{Ipv4Addr, Ipv6Addr}; +use imbl_value::InternedString; use models::{HostId, ServiceInterfaceId}; use serde::{Deserialize, Serialize}; use ts_rs::TS; -use crate::net::host::binding::BindOptions; -use crate::net::host::HostKind; -use crate::prelude::*; - -#[derive(Clone, Debug, Deserialize, Serialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -pub struct ServiceInterfaceWithHostInfo { - #[serde(flatten)] - pub service_interface: ServiceInterface, - pub host_info: ExportedHostInfo, -} - -#[derive(Clone, Debug, Deserialize, Serialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -pub struct ExportedHostInfo { - pub id: HostId, - pub kind: HostKind, - pub hostnames: Vec, -} +use crate::net::host::address::HostAddress; #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] #[serde(rename_all_fields = "camelCase")] #[serde(tag = "kind")] -pub enum ExportedHostnameInfo { +pub enum HostnameInfo { Ip { network_interface_id: String, public: bool, - hostname: ExportedIpHostname, + hostname: IpHostname, }, Onion { - hostname: ExportedOnionHostname, + hostname: OnionHostname, }, } #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] -pub struct ExportedOnionHostname { +pub struct OnionHostname { pub value: String, pub port: Option, pub ssl_port: Option, @@ -56,7 +37,7 @@ pub struct ExportedOnionHostname { #[serde(rename_all = "camelCase")] #[serde(rename_all_fields = "camelCase")] #[serde(tag = "kind")] -pub enum ExportedIpHostname { +pub enum IpHostname { Ipv4 { value: Ipv4Addr, port: Option, @@ -110,6 +91,10 @@ pub enum ServiceInterfaceType { pub struct AddressInfo { pub username: Option, pub host_id: HostId, - pub bind_options: BindOptions, + pub internal_port: u16, + #[ts(type = "string | null")] + pub scheme: Option, + #[ts(type = "string | null")] + pub ssl_scheme: Option, pub suffix: String, } diff --git a/core/startos/src/net/vhost.rs b/core/startos/src/net/vhost.rs index 8dc1e6da3..b4f5715ae 100644 --- a/core/startos/src/net/vhost.rs +++ b/core/startos/src/net/vhost.rs @@ -205,7 +205,7 @@ impl VHostServer { .into_entries()? .into_iter() .flat_map(|(_, ips)| [ - ips.as_ipv4().de().map(|ip| ip.map(IpAddr::V4)), + ips.as_ipv4().de().map(|ip| ip.map(IpAddr::V4)), ips.as_ipv6().de().map(|ip| ip.map(IpAddr::V6)) ]) .filter_map(|a| a.transpose()) diff --git a/core/startos/src/registry/auth.rs b/core/startos/src/registry/auth.rs index 741090b4d..27655c4a8 100644 --- a/core/startos/src/registry/auth.rs +++ b/core/startos/src/registry/auth.rs @@ -138,7 +138,6 @@ impl Middleware for Auth { if request.headers().contains_key(AUTH_SIG_HEADER) { self.signer = Some( async { - let request = request; let SignatureHeader { commitment, signer, diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index e6ccdd131..da641468b 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -10,7 +10,8 @@ use persistent_container::PersistentContainer; use rpc_toolkit::{from_fn_async, CallRemoteHandler, Empty, HandlerArgs, HandlerFor}; use serde::{Deserialize, Serialize}; use start_stop::StartStop; -use tokio::{fs::File, sync::Notify}; +use tokio::fs::File; +use tokio::sync::Notify; use ts_rs::TS; use crate::context::{CliContext, RpcContext}; @@ -308,7 +309,7 @@ impl Service { .send(transition::restore::Restore { path: backup_source.path().to_path_buf(), }) - .await?; + .await??; Ok(service) } @@ -370,7 +371,7 @@ impl Service { .send(transition::backup::Backup { path: guard.path().to_path_buf(), }) - .await?; + .await??; Ok(()) } diff --git a/core/startos/src/service/persistent_container.rs b/core/startos/src/service/persistent_container.rs index 06dcdc28e..03203633a 100644 --- a/core/startos/src/service/persistent_container.rs +++ b/core/startos/src/service/persistent_container.rs @@ -10,7 +10,7 @@ use imbl_value::InternedString; use models::{ProcedureName, VolumeId}; use rpc_toolkit::{Empty, Server, ShutdownHandle}; use serde::de::DeserializeOwned; -use tokio::fs::{ File}; +use tokio::fs::File; use tokio::process::Command; use tokio::sync::{oneshot, watch, Mutex, OnceCell}; use tracing::instrument; diff --git a/core/startos/src/service/service_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs index 31c5a8026..aedf4c194 100644 --- a/core/startos/src/service/service_effect_handler.rs +++ b/core/startos/src/service/service_effect_handler.rs @@ -9,7 +9,6 @@ use std::sync::{Arc, Weak}; use clap::builder::ValueParserFactory; use clap::Parser; use emver::VersionRange; -use imbl::OrdMap; use imbl_value::{json, InternedString}; use itertools::Itertools; use models::{ @@ -30,12 +29,9 @@ use crate::disk::mount::filesystem::idmapped::IdMapped; use crate::disk::mount::filesystem::loop_dev::LoopDev; use crate::disk::mount::filesystem::overlayfs::OverlayGuard; use crate::net::host::address::HostAddress; -use crate::net::host::binding::BindOptions; -use crate::net::host::{self, HostKind}; -use crate::net::service_interface::{ - AddressInfo, ExportedHostInfo, ExportedHostnameInfo, ServiceInterface, ServiceInterfaceType, - ServiceInterfaceWithHostInfo, -}; +use crate::net::host::binding::{BindOptions, LanInfo}; +use crate::net::host::{Host, HostKind}; +use crate::net::service_interface::{AddressInfo, ServiceInterface, ServiceInterfaceType}; use crate::prelude::*; use crate::s9pk::merkle_archive::source::http::HttpSource; use crate::s9pk::rpc::SKIP_ENV; @@ -193,7 +189,6 @@ pub fn service_effect_handler() -> ParentHandler { .subcommand("removeAddress", from_fn_async(remove_address).no_cli()) .subcommand("exportAction", from_fn_async(export_action).no_cli()) .subcommand("removeAction", from_fn_async(remove_action).no_cli()) - .subcommand("reverseProxy", from_fn_async(reverse_proxy).no_cli()) .subcommand("mount", from_fn_async(mount).no_cli()) // TODO Callbacks @@ -233,8 +228,6 @@ struct ExportServiceInterfaceParams { masked: bool, address_info: AddressInfo, r#type: ServiceInterfaceType, - host_kind: HostKind, - hostnames: Vec, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] #[ts(export)] @@ -242,9 +235,8 @@ struct ExportServiceInterfaceParams { struct GetPrimaryUrlParams { #[ts(type = "string | null")] package_id: Option, - service_interface_id: String, + service_interface_id: ServiceInterfaceId, callback: Callback, - host_id: HostId, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] #[ts(export)] @@ -276,37 +268,7 @@ struct RemoveActionParams { #[ts(type = "string")] id: ActionId, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct ReverseProxyBind { - ip: Option, - port: u32, - ssl: bool, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct ReverseProxyDestination { - ip: Option, - port: u32, - ssl: bool, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct ReverseProxyHttp { - #[ts(type = "null | {[key: string]: string}")] - headers: Option>, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct ReverseProxyParams { - bind: ReverseProxyBind, - dst: ReverseProxyDestination, - http: ReverseProxyHttp, -} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] @@ -367,7 +329,7 @@ async fn get_container_ip(context: EffectContext, _: Empty) -> Result Result { +) -> Result { let internal_port = data.internal_port as u16; let context = context.deref()?; @@ -404,13 +366,10 @@ async fn export_service_interface( masked, address_info, r#type, - host_kind, - hostnames, }: ExportServiceInterfaceParams, ) -> Result<(), Error> { let context = context.deref()?; let package_id = context.id.clone(); - let host_id = address_info.host_id.clone(); let service_interface = ServiceInterface { id: id.clone(), @@ -422,15 +381,7 @@ async fn export_service_interface( address_info, interface_type: r#type, }; - let host_info = ExportedHostInfo { - id: host_id, - kind: host_kind, - hostnames, - }; - let svc_interface_with_host_info = ServiceInterfaceWithHostInfo { - service_interface, - host_info, - }; + let svc_interface_with_host_info = service_interface; context .ctx @@ -449,35 +400,26 @@ async fn export_service_interface( } async fn get_primary_url( context: EffectContext, - data: GetPrimaryUrlParams, -) -> Result { + GetPrimaryUrlParams { + package_id, + service_interface_id, + callback, + }: GetPrimaryUrlParams, +) -> Result, Error> { let context = context.deref()?; - let package_id = context.id.clone(); + let package_id = package_id.unwrap_or_else(|| context.id.clone()); - let db_model = context.ctx.db.peek().await; - - let pkg_data_model = db_model - .as_public() - .as_package_data() - .as_idx(&package_id) - .or_not_found(&package_id)?; - - let host = pkg_data_model.de()?.hosts.get_host_primary(&data.host_id); - - match host { - Some(host_address) => Ok(host_address), - None => Err(Error::new( - eyre!("Primary Url not found for {}", data.host_id), - crate::ErrorKind::NotFound, - )), - } + Ok(None) // TODO } async fn list_service_interfaces( context: EffectContext, - data: ListServiceInterfacesParams, -) -> Result, Error> { + ListServiceInterfacesParams { + package_id, + callback, + }: ListServiceInterfacesParams, +) -> Result, Error> { let context = context.deref()?; - let package_id = context.id.clone(); + let package_id = package_id.unwrap_or_else(|| context.id.clone()); context .ctx @@ -553,10 +495,8 @@ async fn remove_action(context: EffectContext, data: RemoveActionParams) -> Resu .await?; Ok(()) } -async fn reverse_proxy(context: EffectContext, data: ReverseProxyParams) -> Result { - todo!() -} async fn mount(context: EffectContext, data: MountParams) -> Result { + // TODO todo!() } @@ -564,49 +504,42 @@ async fn mount(context: EffectContext, data: MountParams) -> Result, - service_interface_id: String, + host_id: HostId, #[ts(type = "string | null")] package_id: Option, callback: Callback, } async fn get_host_info( ctx: EffectContext, - GetHostInfoParams { .. }: GetHostInfoParams, -) -> Result { + GetHostInfoParams { + callback, + package_id, + host_id, + }: GetHostInfoParams, +) -> Result { let ctx = ctx.deref()?; - Ok(json!({ - "id": "fakeId1", - "kind": "multi", - "hostnames": [{ - "kind": "ip", - "networkInterfaceId": "fakeNetworkInterfaceId1", - "public": true, - "hostname":{ - "kind": "domain", - "domain": format!("{}", ctx.id), - "subdomain": (), - "port": (), - "sslPort": () - } - } + let db = ctx.ctx.db.peek().await; + let package_id = package_id.unwrap_or_else(|| ctx.id.clone()); - ] - })) + db.as_public() + .as_package_data() + .as_idx(&package_id) + .or_not_found(&package_id)? + .as_hosts() + .as_idx(&host_id) + .or_not_found(&host_id)? + .de() } -async fn clear_bindings(context: EffectContext, _: Empty) -> Result { - todo!() +async fn clear_bindings(context: EffectContext, _: Empty) -> Result<(), Error> { + let ctx = context.deref()?; + let mut svc = ctx.persistent_container.net_service.lock().await; + svc.clear_bindings().await?; + Ok(()) } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] @@ -619,15 +552,13 @@ struct BindParams { #[serde(flatten)] options: BindOptions, } -async fn bind( - context: EffectContext, - BindParams { +async fn bind(context: EffectContext, bind_params: Value) -> Result<(), Error> { + let BindParams { kind, id, internal_port, options, - }: BindParams, -) -> Result<(), Error> { + } = from_value(bind_params)?; let ctx = context.deref()?; let mut svc = ctx.persistent_container.net_service.lock().await; svc.bind(kind, id, internal_port, options).await @@ -639,39 +570,32 @@ async fn bind( struct GetServiceInterfaceParams { #[ts(type = "string | null")] package_id: Option, - service_interface_id: String, + service_interface_id: ServiceInterfaceId, callback: Callback, } + async fn get_service_interface( - _: EffectContext, + ctx: EffectContext, GetServiceInterfaceParams { callback, package_id, service_interface_id, }: GetServiceInterfaceParams, -) -> Result { - // TODO @Dr_Bonez - Ok(json!({ - "id": service_interface_id, - "name": service_interface_id, - "description": "This is a fake", - "hasPrimary": true, - "disabled": false, - "masked": false, - "addressInfo": json!({ - "username": Value::Null, - "hostId": "HostId?", - "options": json!({ - "scheme": Value::Null, - "preferredExternalPort": 80, - "addSsl":Value::Null, - "secure": false, - "ssl": false - }), - "suffix": "http" - }), - "type": "api" - })) +) -> Result { + let ctx = ctx.deref()?; + let package_id = package_id.unwrap_or_else(|| ctx.id.clone()); + let db = ctx.ctx.db.peek().await; + + let interface = db + .as_public() + .as_package_data() + .as_idx(&package_id) + .or_not_found(&package_id)? + .as_service_interfaces() + .as_idx(&service_interface_id) + .or_not_found(&service_interface_id)? + .de()?; + Ok(interface) } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] @@ -764,6 +688,7 @@ async fn get_ssl_certificate( host_id, }: GetSslCertificateParams, ) -> Result { + // TODO let fake = include_str!("./fake.cert.pem"); Ok(json!([fake, fake, fake])) } @@ -785,6 +710,7 @@ async fn get_ssl_key( algorithm, }: GetSslKeyParams, ) -> Result { + // TODO let fake = include_str!("./fake.cert.key"); Ok(json!(fake)) } diff --git a/sdk/lib/StartSdk.ts b/sdk/lib/StartSdk.ts index 381914c13..f5959a084 100644 --- a/sdk/lib/StartSdk.ts +++ b/sdk/lib/StartSdk.ts @@ -59,7 +59,7 @@ import { } from "./interfaces/setupInterfaces" import { successFailure } from "./trigger/successFailure" import { HealthReceipt } from "./health/HealthReceipt" -import { MultiHost, Scheme, SingleHost, StaticHost } from "./interfaces/Host" +import { MultiHost, Scheme } from "./interfaces/Host" import { ServiceInterfaceBuilder } from "./interfaces/ServiceInterfaceBuilder" import { GetSystemSmtp } from "./util/GetSystemSmtp" import nullIfEmpty from "./util/nullIfEmpty" @@ -178,10 +178,10 @@ export class StartSdk { }, host: { - static: (effects: Effects, id: string) => - new StaticHost({ id, effects }), - single: (effects: Effects, id: string) => - new SingleHost({ id, effects }), + // static: (effects: Effects, id: string) => + // new StaticHost({ id, effects }), + // single: (effects: Effects, id: string) => + // new SingleHost({ id, effects }), multi: (effects: Effects, id: string) => new MultiHost({ id, effects }), }, nullIfEmpty, diff --git a/sdk/lib/interfaces/Host.ts b/sdk/lib/interfaces/Host.ts index 2f1353cc1..96a76628a 100644 --- a/sdk/lib/interfaces/Host.ts +++ b/sdk/lib/interfaces/Host.ts @@ -1,14 +1,14 @@ -import { object, string } from "ts-matches" +import { number, object, string } from "ts-matches" import { Effects } from "../types" import { Origin } from "./Origin" -import { AddSslOptions } from ".././osBindings" +import { AddSslOptions, BindParams } from ".././osBindings" import { Security } from ".././osBindings" import { BindOptions } from ".././osBindings" import { AlpnInfo } from ".././osBindings" export { AddSslOptions, Security, BindOptions } -const knownProtocols = { +export const knownProtocols = { http: { secure: null, defaultPort: 80, @@ -69,19 +69,17 @@ type NotProtocolsWithSslVariants = Exclude< type BindOptionsByKnownProtocol = | { protocol: ProtocolsWithSslVariants - preferredExternalPort?: number - scheme?: Scheme + preferredExternalPort: number addSsl?: Partial } | { protocol: NotProtocolsWithSslVariants - preferredExternalPort?: number - scheme?: Scheme + preferredExternalPort: number addSsl?: AddSslOptions } -type BindOptionsByProtocol = BindOptionsByKnownProtocol | BindOptions +export type BindOptionsByProtocol = BindOptionsByKnownProtocol | BindOptions -export type HostKind = "static" | "single" | "multi" +export type HostKind = BindParams["kind"] const hasStringProtocol = object({ protocol: string, @@ -110,66 +108,62 @@ export class Host { private async bindPortForUnknown( internalPort: number, options: { - scheme: Scheme preferredExternalPort: number addSsl: AddSslOptions | null secure: { ssl: boolean } | null }, ) { - await this.options.effects.bind({ + const binderOptions = { kind: this.options.kind, id: this.options.id, - internalPort: internalPort, + internalPort, ...options, - }) + } + await this.options.effects.bind(binderOptions) - return new Origin(this, options) + return new Origin(this, internalPort, null, null) } private async bindPortForKnown( options: BindOptionsByKnownProtocol, internalPort: number, ) { - const scheme = - options.scheme === undefined ? options.protocol : options.scheme const protoInfo = knownProtocols[options.protocol] const preferredExternalPort = options.preferredExternalPort || knownProtocols[options.protocol].defaultPort - const addSsl = this.getAddSsl(options, protoInfo) + const sslProto = this.getSslProto(options, protoInfo) + const addSsl = + sslProto && "alpn" in protoInfo + ? { + // addXForwardedHeaders: null, + preferredExternalPort: knownProtocols[sslProto].defaultPort, + scheme: sslProto, + alpn: protoInfo.alpn, + ...("addSsl" in options ? options.addSsl : null), + } + : null const secure: Security | null = !protoInfo.secure ? null : { ssl: false } - const newOptions = { - scheme, - preferredExternalPort, - addSsl, - secure, - } - await this.options.effects.bind({ kind: this.options.kind, id: this.options.id, internalPort, - ...newOptions, + preferredExternalPort, + addSsl, + secure, }) - return new Origin(this, newOptions) + return new Origin(this, internalPort, options.protocol, sslProto) } - private getAddSsl( + private getSslProto( options: BindOptionsByKnownProtocol, protoInfo: KnownProtocols[keyof KnownProtocols], - ): AddSslOptions | null { + ) { if (inObject("noAddSsl", options) && options.noAddSsl) return null - if ("withSsl" in protoInfo && protoInfo.withSsl) - return { - // addXForwardedHeaders: null, - preferredExternalPort: knownProtocols[protoInfo.withSsl].defaultPort, - scheme: protoInfo.withSsl, - alpn: protoInfo.alpn, - ...("addSsl" in options ? options.addSsl : null), - } + if ("withSsl" in protoInfo && protoInfo.withSsl) return protoInfo.withSsl return null } } @@ -181,17 +175,17 @@ function inObject( return key in obj } -export class StaticHost extends Host { - constructor(options: { effects: Effects; id: string }) { - super({ ...options, kind: "static" }) - } -} +// export class StaticHost extends Host { +// constructor(options: { effects: Effects; id: string }) { +// super({ ...options, kind: "static" }) +// } +// } -export class SingleHost extends Host { - constructor(options: { effects: Effects; id: string }) { - super({ ...options, kind: "single" }) - } -} +// export class SingleHost extends Host { +// constructor(options: { effects: Effects; id: string }) { +// super({ ...options, kind: "single" }) +// } +// } export class MultiHost extends Host { constructor(options: { effects: Effects; id: string }) { diff --git a/sdk/lib/interfaces/Origin.ts b/sdk/lib/interfaces/Origin.ts index aaadbea50..52afe1ed3 100644 --- a/sdk/lib/interfaces/Origin.ts +++ b/sdk/lib/interfaces/Origin.ts @@ -6,7 +6,9 @@ import { ServiceInterfaceBuilder } from "./ServiceInterfaceBuilder" export class Origin { constructor( readonly host: T, - readonly options: BindOptions, + readonly internalPort: number, + readonly scheme: string | null, + readonly sslScheme: string | null, ) {} build({ username, path, search, schemeOverride }: BuildOptions): AddressInfo { @@ -20,18 +22,9 @@ export class Origin { return { hostId: this.host.options.id, - bindOptions: { - ...this.options, - scheme: schemeOverride ? schemeOverride.noSsl : this.options.scheme, - addSsl: this.options.addSsl - ? { - ...this.options.addSsl, - scheme: schemeOverride - ? schemeOverride.ssl - : this.options.addSsl.scheme, - } - : null, - }, + internalPort: this.internalPort, + scheme: schemeOverride ? schemeOverride.noSsl : this.scheme, + sslScheme: schemeOverride ? schemeOverride.ssl : this.sslScheme, suffix: `${path}${qp}`, username, } diff --git a/sdk/lib/osBindings/AddAssetParams.ts b/sdk/lib/osBindings/AddAssetParams.ts index ce6128cf7..ffd7db675 100644 --- a/sdk/lib/osBindings/AddAssetParams.ts +++ b/sdk/lib/osBindings/AddAssetParams.ts @@ -6,7 +6,6 @@ import type { Version } from "./Version" export type AddAssetParams = { version: Version platform: string - upload: boolean url: string signature: AnySignature commitment: Blake3Commitment diff --git a/sdk/lib/osBindings/AddPackageParams.ts b/sdk/lib/osBindings/AddPackageParams.ts new file mode 100644 index 000000000..4395b9b8a --- /dev/null +++ b/sdk/lib/osBindings/AddPackageParams.ts @@ -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 AddPackageParams = { + url: string + commitment: MerkleArchiveCommitment + signature: AnySignature +} diff --git a/sdk/lib/osBindings/AddSslOptions.ts b/sdk/lib/osBindings/AddSslOptions.ts index daa9aee0e..35071aff3 100644 --- a/sdk/lib/osBindings/AddSslOptions.ts +++ b/sdk/lib/osBindings/AddSslOptions.ts @@ -2,7 +2,6 @@ import type { AlpnInfo } from "./AlpnInfo" export type AddSslOptions = { - scheme: string | null preferredExternalPort: number - alpn: AlpnInfo + alpn: AlpnInfo | null } diff --git a/sdk/lib/osBindings/AddressInfo.ts b/sdk/lib/osBindings/AddressInfo.ts index 818b570bb..c7a1c1af1 100644 --- a/sdk/lib/osBindings/AddressInfo.ts +++ b/sdk/lib/osBindings/AddressInfo.ts @@ -1,10 +1,11 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { BindOptions } from "./BindOptions" import type { HostId } from "./HostId" export type AddressInfo = { username: string | null hostId: HostId - bindOptions: BindOptions + internalPort: number + scheme: string | null + sslScheme: string | null suffix: string } diff --git a/sdk/lib/osBindings/BindInfo.ts b/sdk/lib/osBindings/BindInfo.ts index d7b37a70f..221b1c37c 100644 --- a/sdk/lib/osBindings/BindInfo.ts +++ b/sdk/lib/osBindings/BindInfo.ts @@ -1,4 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { BindOptions } from "./BindOptions" +import type { LanInfo } from "./LanInfo" -export type BindInfo = { options: BindOptions; assignedLanPort: number | null } +export type BindInfo = { options: BindOptions; lan: LanInfo } diff --git a/sdk/lib/osBindings/BindOptions.ts b/sdk/lib/osBindings/BindOptions.ts index a462a8cfc..49d9ecbf2 100644 --- a/sdk/lib/osBindings/BindOptions.ts +++ b/sdk/lib/osBindings/BindOptions.ts @@ -3,7 +3,6 @@ import type { AddSslOptions } from "./AddSslOptions" import type { Security } from "./Security" export type BindOptions = { - scheme: string | null preferredExternalPort: number addSsl: AddSslOptions | null secure: Security | null diff --git a/sdk/lib/osBindings/BindParams.ts b/sdk/lib/osBindings/BindParams.ts index 544f65a40..fcc450476 100644 --- a/sdk/lib/osBindings/BindParams.ts +++ b/sdk/lib/osBindings/BindParams.ts @@ -8,7 +8,6 @@ export type BindParams = { kind: HostKind id: HostId internalPort: number - scheme: string | null preferredExternalPort: number addSsl: AddSslOptions | null secure: Security | null diff --git a/sdk/lib/osBindings/ExportServiceInterfaceParams.ts b/sdk/lib/osBindings/ExportServiceInterfaceParams.ts index 847a6090a..b93e83f7c 100644 --- a/sdk/lib/osBindings/ExportServiceInterfaceParams.ts +++ b/sdk/lib/osBindings/ExportServiceInterfaceParams.ts @@ -1,7 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AddressInfo } from "./AddressInfo" -import type { ExportedHostnameInfo } from "./ExportedHostnameInfo" -import type { HostKind } from "./HostKind" import type { ServiceInterfaceId } from "./ServiceInterfaceId" import type { ServiceInterfaceType } from "./ServiceInterfaceType" @@ -14,6 +12,4 @@ export type ExportServiceInterfaceParams = { masked: boolean addressInfo: AddressInfo type: ServiceInterfaceType - hostKind: HostKind - hostnames: Array } diff --git a/sdk/lib/osBindings/ExportedHostInfo.ts b/sdk/lib/osBindings/ExportedHostInfo.ts deleted file mode 100644 index 5e8a25ad6..000000000 --- a/sdk/lib/osBindings/ExportedHostInfo.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExportedHostnameInfo } from "./ExportedHostnameInfo" -import type { HostId } from "./HostId" -import type { HostKind } from "./HostKind" - -export type ExportedHostInfo = { - id: HostId - kind: HostKind - hostnames: Array -} diff --git a/sdk/lib/osBindings/ExportedHostnameInfo.ts b/sdk/lib/osBindings/ExportedHostnameInfo.ts deleted file mode 100644 index 42f849529..000000000 --- a/sdk/lib/osBindings/ExportedHostnameInfo.ts +++ /dev/null @@ -1,12 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ExportedIpHostname } from "./ExportedIpHostname" -import type { ExportedOnionHostname } from "./ExportedOnionHostname" - -export type ExportedHostnameInfo = - | { - kind: "ip" - networkInterfaceId: string - public: boolean - hostname: ExportedIpHostname - } - | { kind: "onion"; hostname: ExportedOnionHostname } diff --git a/sdk/lib/osBindings/GetHostInfoParams.ts b/sdk/lib/osBindings/GetHostInfoParams.ts index 1ffb95de0..120b4cfe1 100644 --- a/sdk/lib/osBindings/GetHostInfoParams.ts +++ b/sdk/lib/osBindings/GetHostInfoParams.ts @@ -1,10 +1,9 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Callback } from "./Callback" -import type { GetHostInfoParamsKind } from "./GetHostInfoParamsKind" +import type { HostId } from "./HostId" export type GetHostInfoParams = { - kind: GetHostInfoParamsKind | null - serviceInterfaceId: string + hostId: HostId packageId: string | null callback: Callback } diff --git a/sdk/lib/osBindings/GetHostInfoParamsKind.ts b/sdk/lib/osBindings/GetHostInfoParamsKind.ts deleted file mode 100644 index 482eb7177..000000000 --- a/sdk/lib/osBindings/GetHostInfoParamsKind.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type GetHostInfoParamsKind = "multi" diff --git a/sdk/lib/osBindings/GetPackageResponse.ts b/sdk/lib/osBindings/GetPackageResponse.ts index 5bf24bfc0..3e1dd4e9d 100644 --- a/sdk/lib/osBindings/GetPackageResponse.ts +++ b/sdk/lib/osBindings/GetPackageResponse.ts @@ -4,6 +4,7 @@ import type { PackageVersionInfo } from "./PackageVersionInfo" import type { Version } from "./Version" export type GetPackageResponse = { + categories: string[] best: { [key: Version]: PackageVersionInfo } otherVersions?: { [key: Version]: PackageInfoShort } } diff --git a/sdk/lib/osBindings/GetPackageResponseFull.ts b/sdk/lib/osBindings/GetPackageResponseFull.ts index 579924291..e375dd489 100644 --- a/sdk/lib/osBindings/GetPackageResponseFull.ts +++ b/sdk/lib/osBindings/GetPackageResponseFull.ts @@ -3,6 +3,7 @@ import type { PackageVersionInfo } from "./PackageVersionInfo" import type { Version } from "./Version" export type GetPackageResponseFull = { + categories: string[] best: { [key: Version]: PackageVersionInfo } otherVersions: { [key: Version]: PackageVersionInfo } } diff --git a/sdk/lib/osBindings/GetPrimaryUrlParams.ts b/sdk/lib/osBindings/GetPrimaryUrlParams.ts index 1a68ecc7b..dbafa4152 100644 --- a/sdk/lib/osBindings/GetPrimaryUrlParams.ts +++ b/sdk/lib/osBindings/GetPrimaryUrlParams.ts @@ -1,10 +1,9 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Callback } from "./Callback" -import type { HostId } from "./HostId" +import type { ServiceInterfaceId } from "./ServiceInterfaceId" export type GetPrimaryUrlParams = { packageId: string | null - serviceInterfaceId: string + serviceInterfaceId: ServiceInterfaceId callback: Callback - hostId: HostId } diff --git a/sdk/lib/osBindings/GetServiceInterfaceParams.ts b/sdk/lib/osBindings/GetServiceInterfaceParams.ts index ee3a0c03d..0a8bdfcb2 100644 --- a/sdk/lib/osBindings/GetServiceInterfaceParams.ts +++ b/sdk/lib/osBindings/GetServiceInterfaceParams.ts @@ -1,8 +1,9 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Callback } from "./Callback" +import type { ServiceInterfaceId } from "./ServiceInterfaceId" export type GetServiceInterfaceParams = { packageId: string | null - serviceInterfaceId: string + serviceInterfaceId: ServiceInterfaceId callback: Callback } diff --git a/sdk/lib/osBindings/HardwareRequirements.ts b/sdk/lib/osBindings/HardwareRequirements.ts index 4964bc66f..0e1da1f36 100644 --- a/sdk/lib/osBindings/HardwareRequirements.ts +++ b/sdk/lib/osBindings/HardwareRequirements.ts @@ -3,5 +3,5 @@ export type HardwareRequirements = { device: { [key: string]: string } ram: number | null - arch: Array | null + arch: string[] | null } diff --git a/sdk/lib/osBindings/Host.ts b/sdk/lib/osBindings/Host.ts index a476c0adc..7d8cf3a90 100644 --- a/sdk/lib/osBindings/Host.ts +++ b/sdk/lib/osBindings/Host.ts @@ -2,10 +2,14 @@ import type { BindInfo } from "./BindInfo" import type { HostAddress } from "./HostAddress" import type { HostKind } from "./HostKind" +import type { HostnameInfo } from "./HostnameInfo" export type Host = { kind: HostKind bindings: { [key: number]: BindInfo } addresses: Array - primary: HostAddress | null + /** + * COMPUTED: NetService::update + */ + hostnameInfo: { [key: number]: Array } } diff --git a/sdk/lib/osBindings/HostnameInfo.ts b/sdk/lib/osBindings/HostnameInfo.ts new file mode 100644 index 000000000..ef8bafac0 --- /dev/null +++ b/sdk/lib/osBindings/HostnameInfo.ts @@ -0,0 +1,12 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { IpHostname } from "./IpHostname" +import type { OnionHostname } from "./OnionHostname" + +export type HostnameInfo = + | { + kind: "ip" + networkInterfaceId: string + public: boolean + hostname: IpHostname + } + | { kind: "onion"; hostname: OnionHostname } diff --git a/sdk/lib/osBindings/HostInfo.ts b/sdk/lib/osBindings/Hosts.ts similarity index 79% rename from sdk/lib/osBindings/HostInfo.ts rename to sdk/lib/osBindings/Hosts.ts index d39b56ebe..c7aa84996 100644 --- a/sdk/lib/osBindings/HostInfo.ts +++ b/sdk/lib/osBindings/Hosts.ts @@ -2,4 +2,4 @@ import type { Host } from "./Host" import type { HostId } from "./HostId" -export type HostInfo = { [key: HostId]: Host } +export type Hosts = { [key: HostId]: Host } diff --git a/sdk/lib/osBindings/ExportedIpHostname.ts b/sdk/lib/osBindings/IpHostname.ts similarity index 94% rename from sdk/lib/osBindings/ExportedIpHostname.ts rename to sdk/lib/osBindings/IpHostname.ts index ea06fe05b..4a6b5e87c 100644 --- a/sdk/lib/osBindings/ExportedIpHostname.ts +++ b/sdk/lib/osBindings/IpHostname.ts @@ -1,6 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type ExportedIpHostname = +export type IpHostname = | { kind: "ipv4"; value: string; port: number | null; sslPort: number | null } | { kind: "ipv6"; value: string; port: number | null; sslPort: number | null } | { diff --git a/sdk/lib/osBindings/ReverseProxyDestination.ts b/sdk/lib/osBindings/LanInfo.ts similarity index 55% rename from sdk/lib/osBindings/ReverseProxyDestination.ts rename to sdk/lib/osBindings/LanInfo.ts index 88b5dd650..59b8a5519 100644 --- a/sdk/lib/osBindings/ReverseProxyDestination.ts +++ b/sdk/lib/osBindings/LanInfo.ts @@ -1,7 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type ReverseProxyDestination = { - ip: string | null - port: number - ssl: boolean +export type LanInfo = { + assignedPort: number | null + assignedSslPort: number | null } diff --git a/sdk/lib/osBindings/ExportedOnionHostname.ts b/sdk/lib/osBindings/OnionHostname.ts similarity index 82% rename from sdk/lib/osBindings/ExportedOnionHostname.ts rename to sdk/lib/osBindings/OnionHostname.ts index 2c7d67cec..0bea8245e 100644 --- a/sdk/lib/osBindings/ExportedOnionHostname.ts +++ b/sdk/lib/osBindings/OnionHostname.ts @@ -1,6 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type ExportedOnionHostname = { +export type OnionHostname = { value: string port: number | null sslPort: number | null diff --git a/sdk/lib/osBindings/OsVersionInfo.ts b/sdk/lib/osBindings/OsVersionInfo.ts index 7cd0fde9e..a88115350 100644 --- a/sdk/lib/osBindings/OsVersionInfo.ts +++ b/sdk/lib/osBindings/OsVersionInfo.ts @@ -7,7 +7,7 @@ export type OsVersionInfo = { headline: string releaseNotes: string sourceVersion: string - signers: Array + authorized: Array iso: { [key: string]: RegistryAsset } squashfs: { [key: string]: RegistryAsset } img: { [key: string]: RegistryAsset } diff --git a/sdk/lib/osBindings/PackageDataEntry.ts b/sdk/lib/osBindings/PackageDataEntry.ts index f88c6d7be..ef805741b 100644 --- a/sdk/lib/osBindings/PackageDataEntry.ts +++ b/sdk/lib/osBindings/PackageDataEntry.ts @@ -3,10 +3,10 @@ import type { ActionId } from "./ActionId" import type { ActionMetadata } from "./ActionMetadata" import type { CurrentDependencies } from "./CurrentDependencies" import type { DataUrl } from "./DataUrl" -import type { HostInfo } from "./HostInfo" +import type { Hosts } from "./Hosts" import type { PackageState } from "./PackageState" +import type { ServiceInterface } from "./ServiceInterface" import type { ServiceInterfaceId } from "./ServiceInterfaceId" -import type { ServiceInterfaceWithHostInfo } from "./ServiceInterfaceWithHostInfo" import type { Status } from "./Status" export type PackageDataEntry = { @@ -18,7 +18,7 @@ export type PackageDataEntry = { lastBackup: string | null currentDependencies: CurrentDependencies actions: { [key: ActionId]: ActionMetadata } - serviceInterfaces: { [key: ServiceInterfaceId]: ServiceInterfaceWithHostInfo } - hosts: HostInfo + serviceInterfaces: { [key: ServiceInterfaceId]: ServiceInterface } + hosts: Hosts storeExposedDependents: string[] } diff --git a/sdk/lib/osBindings/PackageInfo.ts b/sdk/lib/osBindings/PackageInfo.ts index af340424f..6d07cd43e 100644 --- a/sdk/lib/osBindings/PackageInfo.ts +++ b/sdk/lib/osBindings/PackageInfo.ts @@ -4,6 +4,7 @@ import type { PackageVersionInfo } from "./PackageVersionInfo" import type { Version } from "./Version" export type PackageInfo = { - signers: Array + authorized: Array versions: { [key: Version]: PackageVersionInfo } + categories: string[] } diff --git a/sdk/lib/osBindings/PackageVersionInfo.ts b/sdk/lib/osBindings/PackageVersionInfo.ts index da82540cd..bdded46bd 100644 --- a/sdk/lib/osBindings/PackageVersionInfo.ts +++ b/sdk/lib/osBindings/PackageVersionInfo.ts @@ -17,7 +17,6 @@ export type PackageVersionInfo = { upstreamRepo: string supportSite: string marketingSite: string - categories: string[] osVersion: Version hardwareRequirements: HardwareRequirements sourceVersion: string | null diff --git a/sdk/lib/osBindings/ReverseProxyBind.ts b/sdk/lib/osBindings/ReverseProxyBind.ts deleted file mode 100644 index bd07b9489..000000000 --- a/sdk/lib/osBindings/ReverseProxyBind.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ReverseProxyBind = { ip: string | null; port: number; ssl: boolean } diff --git a/sdk/lib/osBindings/ReverseProxyHttp.ts b/sdk/lib/osBindings/ReverseProxyHttp.ts deleted file mode 100644 index ba49e81dc..000000000 --- a/sdk/lib/osBindings/ReverseProxyHttp.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ReverseProxyHttp = { headers: null | { [key: string]: string } } diff --git a/sdk/lib/osBindings/ReverseProxyParams.ts b/sdk/lib/osBindings/ReverseProxyParams.ts deleted file mode 100644 index 00062cbdf..000000000 --- a/sdk/lib/osBindings/ReverseProxyParams.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ReverseProxyBind } from "./ReverseProxyBind" -import type { ReverseProxyDestination } from "./ReverseProxyDestination" -import type { ReverseProxyHttp } from "./ReverseProxyHttp" - -export type ReverseProxyParams = { - bind: ReverseProxyBind - dst: ReverseProxyDestination - http: ReverseProxyHttp -} diff --git a/sdk/lib/osBindings/ServiceInterfaceWithHostInfo.ts b/sdk/lib/osBindings/ServiceInterfaceWithHostInfo.ts deleted file mode 100644 index 979e6b500..000000000 --- a/sdk/lib/osBindings/ServiceInterfaceWithHostInfo.ts +++ /dev/null @@ -1,17 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AddressInfo } from "./AddressInfo" -import type { ExportedHostInfo } from "./ExportedHostInfo" -import type { ServiceInterfaceId } from "./ServiceInterfaceId" -import type { ServiceInterfaceType } from "./ServiceInterfaceType" - -export type ServiceInterfaceWithHostInfo = { - hostInfo: ExportedHostInfo - id: ServiceInterfaceId - name: string - description: string - hasPrimary: boolean - disabled: boolean - masked: boolean - addressInfo: AddressInfo - type: ServiceInterfaceType -} diff --git a/sdk/lib/osBindings/index.ts b/sdk/lib/osBindings/index.ts index 16fc56a74..efd7a5efb 100644 --- a/sdk/lib/osBindings/index.ts +++ b/sdk/lib/osBindings/index.ts @@ -3,6 +3,7 @@ export { ActionId } from "./ActionId" export { ActionMetadata } from "./ActionMetadata" export { AddAdminParams } from "./AddAdminParams" export { AddAssetParams } from "./AddAssetParams" +export { AddPackageParams } from "./AddPackageParams" export { AddressInfo } from "./AddressInfo" export { AddSslOptions } from "./AddSslOptions" export { AddVersionParams } from "./AddVersionParams" @@ -40,15 +41,10 @@ export { Duration } from "./Duration" export { EncryptedWire } from "./EncryptedWire" export { ExecuteAction } from "./ExecuteAction" export { ExportActionParams } from "./ExportActionParams" -export { ExportedHostInfo } from "./ExportedHostInfo" -export { ExportedHostnameInfo } from "./ExportedHostnameInfo" -export { ExportedIpHostname } from "./ExportedIpHostname" -export { ExportedOnionHostname } from "./ExportedOnionHostname" export { ExportServiceInterfaceParams } from "./ExportServiceInterfaceParams" export { ExposeForDependentsParams } from "./ExposeForDependentsParams" export { FullIndex } from "./FullIndex" export { FullProgress } from "./FullProgress" -export { GetHostInfoParamsKind } from "./GetHostInfoParamsKind" export { GetHostInfoParams } from "./GetHostInfoParams" export { GetOsAssetParams } from "./GetOsAssetParams" export { GetPackageParams } from "./GetPackageParams" @@ -69,14 +65,17 @@ export { HealthCheckId } from "./HealthCheckId" export { HealthCheckResult } from "./HealthCheckResult" export { HostAddress } from "./HostAddress" export { HostId } from "./HostId" -export { HostInfo } from "./HostInfo" export { HostKind } from "./HostKind" +export { HostnameInfo } from "./HostnameInfo" +export { Hosts } from "./Hosts" export { Host } from "./Host" export { ImageId } from "./ImageId" export { InstalledState } from "./InstalledState" export { InstallingInfo } from "./InstallingInfo" export { InstallingState } from "./InstallingState" +export { IpHostname } from "./IpHostname" export { IpInfo } from "./IpInfo" +export { LanInfo } from "./LanInfo" export { ListServiceInterfacesParams } from "./ListServiceInterfacesParams" export { ListVersionSignersParams } from "./ListVersionSignersParams" export { MainStatus } from "./MainStatus" @@ -86,6 +85,7 @@ export { MerkleArchiveCommitment } from "./MerkleArchiveCommitment" export { MountParams } from "./MountParams" export { MountTarget } from "./MountTarget" export { NamedProgress } from "./NamedProgress" +export { OnionHostname } from "./OnionHostname" export { OsIndex } from "./OsIndex" export { OsVersionInfo } from "./OsVersionInfo" export { PackageDataEntry } from "./PackageDataEntry" @@ -106,10 +106,6 @@ export { RemoveActionParams } from "./RemoveActionParams" export { RemoveAddressParams } from "./RemoveAddressParams" export { RemoveVersionParams } from "./RemoveVersionParams" export { RequestCommitment } from "./RequestCommitment" -export { ReverseProxyBind } from "./ReverseProxyBind" -export { ReverseProxyDestination } from "./ReverseProxyDestination" -export { ReverseProxyHttp } from "./ReverseProxyHttp" -export { ReverseProxyParams } from "./ReverseProxyParams" export { Security } from "./Security" export { ServerInfo } from "./ServerInfo" export { ServerSpecs } from "./ServerSpecs" @@ -117,7 +113,6 @@ export { ServerStatus } from "./ServerStatus" export { ServiceInterfaceId } from "./ServiceInterfaceId" export { ServiceInterface } from "./ServiceInterface" export { ServiceInterfaceType } from "./ServiceInterfaceType" -export { ServiceInterfaceWithHostInfo } from "./ServiceInterfaceWithHostInfo" export { SessionList } from "./SessionList" export { Sessions } from "./Sessions" export { Session } from "./Session" diff --git a/sdk/lib/test/host.test.ts b/sdk/lib/test/host.test.ts index 82372f61b..a8ae317ed 100644 --- a/sdk/lib/test/host.test.ts +++ b/sdk/lib/test/host.test.ts @@ -8,6 +8,7 @@ describe("host", () => { const foo = sdk.host.multi(effects, "foo") const fooOrigin = await foo.bindPort(80, { protocol: "http" as const, + preferredExternalPort: 80, }) const fooInterface = new ServiceInterfaceBuilder({ effects, diff --git a/sdk/lib/test/startosTypeValidation.test.ts b/sdk/lib/test/startosTypeValidation.test.ts index 4f6d50f53..79ec62106 100644 --- a/sdk/lib/test/startosTypeValidation.test.ts +++ b/sdk/lib/test/startosTypeValidation.test.ts @@ -25,7 +25,6 @@ import { ListServiceInterfacesParams } from ".././osBindings" import { RemoveAddressParams } from ".././osBindings" import { ExportActionParams } from ".././osBindings" import { RemoveActionParams } from ".././osBindings" -import { ReverseProxyParams } from ".././osBindings" import { MountParams } from ".././osBindings" function typeEquality(_a: ExpectedType) {} describe("startosTypeValidation ", () => { @@ -66,7 +65,6 @@ describe("startosTypeValidation ", () => { removeAddress: {} as RemoveAddressParams, exportAction: {} as ExportActionParams, removeAction: {} as RemoveActionParams, - reverseProxy: {} as ReverseProxyParams, mount: {} as MountParams, checkDependencies: {} as CheckDependenciesParam, getDependencies: undefined, diff --git a/sdk/lib/types.ts b/sdk/lib/types.ts index 7db42e5a9..2da92cd56 100644 --- a/sdk/lib/types.ts +++ b/sdk/lib/types.ts @@ -5,6 +5,12 @@ import { SetHealth, HealthCheckResult, SetMainStatus, + ServiceInterface, + Host, + ExportServiceInterfaceParams, + GetPrimaryUrlParams, + LanInfo, + BindParams, } from "./osBindings" import { MainEffects, ServiceInterfaceType, Signals } from "./StartSdk" @@ -12,7 +18,7 @@ import { InputSpec } from "./config/configTypes" import { DependenciesReceipt } from "./config/setupConfig" import { BindOptions, Scheme } from "./interfaces/Host" import { Daemons } from "./mainFn/Daemons" -import { PathBuilder, StorePath } from "./store/PathBuilder" +import { StorePath } from "./store/PathBuilder" import { ExposedStorePaths } from "./store/setupExposeStore" import { UrlString } from "./util/getServiceInterface" export * from "./osBindings" @@ -184,14 +190,6 @@ export declare const hostName: unique symbol // asdflkjadsf.onion | 1.2.3.4 export type Hostname = string & { [hostName]: never } -/** ${scheme}://${username}@${host}:${externalPort}${suffix} */ -export type AddressInfo = { - username: string | null - hostId: string - bindOptions: BindOptions - suffix: string -} - export type HostnameInfoIp = { kind: "ip" networkInterfaceId: string @@ -219,44 +217,9 @@ export type HostnameInfoOnion = { export type HostnameInfo = HostnameInfoIp | HostnameInfoOnion -export type SingleHost = { - id: string - kind: "single" | "static" - hostname: HostnameInfo | null -} - -export type MultiHost = { - id: string - kind: "multi" - hostnames: HostnameInfo[] -} - -export type HostInfo = SingleHost | MultiHost - export type ServiceInterfaceId = string -export type ServiceInterface = { - id: ServiceInterfaceId - /** The title of this field to be displayed */ - name: string - /** Human readable description, used as tooltip usually */ - description: string - /** Whether or not one address must be the primary address */ - hasPrimary: boolean - /** Disabled interfaces do not serve, but they retain their metadata and addresses */ - disabled: boolean - /** Whether or not to mask the URIs for this interface. Useful if the URIs contain sensitive information, such as a password, macaroon, or API key */ - masked: boolean - /** URI Information */ - addressInfo: AddressInfo - /** The network interface could be several types, something like ui, p2p, or network */ - type: ServiceInterfaceType -} - -export type ServiceInterfaceWithHostInfo = ServiceInterface & { - hostInfo: HostInfo -} - +export { ServiceInterface } export type ExposeServicePaths = { /** The path to the value in the Store. [JsonPath](https://jsonpath.com/) */ paths: ExposedStorePaths @@ -326,13 +289,7 @@ export type Effects = { /** Removes all network bindings */ clearBindings(): Promise /** Creates a host connected to the specified port with the provided options */ - bind( - options: { - kind: "static" | "single" | "multi" - id: string - internalPort: number - } & BindOptions, - ): Promise + bind(options: BindParams): Promise /** Retrieves the current hostname(s) associated with a host id */ // getHostInfo(options: { // kind: "static" | "single" @@ -341,11 +298,10 @@ export type Effects = { // callback: () => void // }): Promise getHostInfo(options: { - kind: "multi" | null - serviceInterfaceId: string + hostId: string packageId: string | null callback: () => void - }): Promise + }): Promise // /** // * Run rsync between two volumes. This is used to backup data between volumes. @@ -395,14 +351,14 @@ export type Effects = { getServicePortForward(options: { internalPort: number packageId: string | null - }): Promise + }): Promise /** Removes all network interfaces */ clearServiceInterfaces(): Promise /** When we want to create a link in the front end interfaces, and example is * exposing a url to view a web service */ - exportServiceInterface(options: ServiceInterface): Promise + exportServiceInterface(options: ExportServiceInterfaceParams): Promise exposeForDependents(options: { paths: string[] }): Promise @@ -422,11 +378,7 @@ export type Effects = { * The user sets the primary url for a interface * @param options */ - getPrimaryUrl(options: { - packageId: PackageId | null - serviceInterfaceId: ServiceInterfaceId - callback: () => void - }): Promise + getPrimaryUrl(options: GetPrimaryUrlParams): Promise /** * There are times that we want to see the addresses that where exported @@ -437,7 +389,7 @@ export type Effects = { listServiceInterfaces(options: { packageId: PackageId | null callback: () => void - }): Promise + }): Promise> /** *Remove an address that was exported. Used problably during main or during setConfig. @@ -501,25 +453,6 @@ export type Effects = { /** Exists could be useful during the runtime to know if some service is running, option dep */ running(options: { packageId: PackageId }): Promise - /** Instead of creating proxies with nginx, we have a utility to create and maintain a proxy in the lifetime of this running. */ - reverseProxy(options: { - bind: { - /** Optional, default is 0.0.0.0 */ - ip: string | null - port: number - ssl: boolean - } - dst: { - /** Optional: default is 127.0.0.1 */ - ip: string | null // optional, default 127.0.0.1 - port: number - ssl: boolean - } - http: { - // optional, will do TCP layer proxy only if not present - headers: Record | null - } | null - }): Promise<{ stop(): Promise }> restart(): void shutdown(): void diff --git a/sdk/lib/util/Hostname.ts b/sdk/lib/util/Hostname.ts new file mode 100644 index 000000000..ee68abe4f --- /dev/null +++ b/sdk/lib/util/Hostname.ts @@ -0,0 +1,25 @@ +import { HostnameInfo } from "../types" + +export function hostnameInfoToAddress(hostInfo: HostnameInfo): string { + if (hostInfo.kind === "onion") { + return `${hostInfo.hostname.value}` + } + if (hostInfo.kind !== "ip") { + throw Error("Expecting that the kind is ip.") + } + const hostname = hostInfo.hostname + if (hostname.kind === "domain") { + return `${hostname.subdomain ? `${hostname.subdomain}.` : ""}${hostname.domain}` + } + const port = hostname.sslPort || hostname.port + const portString = port ? `:${port}` : "" + if ("ipv4" === hostname.kind || "ipv6" === hostname.kind) { + return `${hostname.value}${portString}` + } + if ("local" === hostname.kind) { + return `${hostname.value}${portString}` + } + throw Error( + "Expecting to have a valid hostname kind." + JSON.stringify(hostname), + ) +} diff --git a/sdk/lib/util/getServiceInterface.ts b/sdk/lib/util/getServiceInterface.ts index 3b7af1c41..737cbbc16 100644 --- a/sdk/lib/util/getServiceInterface.ts +++ b/sdk/lib/util/getServiceInterface.ts @@ -1,10 +1,15 @@ import { ServiceInterfaceType } from "../StartSdk" +import { knownProtocols } from "../interfaces/Host" import { AddressInfo, Effects, - HostInfo, + Host, + HostAddress, Hostname, HostnameInfo, + HostnameInfoIp, + HostnameInfoOnion, + IpInfo, } from "../types" export type UrlString = string @@ -20,13 +25,13 @@ export const getHostname = (url: string): Hostname | null => { } export type Filled = { - hostnames: Hostname[] - onionHostnames: Hostname[] - localHostnames: Hostname[] - ipHostnames: Hostname[] - ipv4Hostnames: Hostname[] - ipv6Hostnames: Hostname[] - nonIpHostnames: Hostname[] + hostnames: HostnameInfo[] + onionHostnames: HostnameInfo[] + localHostnames: HostnameInfo[] + ipHostnames: HostnameInfo[] + ipv4Hostnames: HostnameInfo[] + ipv6Hostnames: HostnameInfo[] + nonIpHostnames: HostnameInfo[] urls: UrlString[] onionUrls: UrlString[] @@ -50,7 +55,7 @@ export type ServiceInterfaceFilled = { /** Whether or not to mask the URIs for this interface. Useful if the URIs contain sensitive information, such as a password, macaroon, or API key */ masked: boolean /** Information about the host for this binding */ - hostInfo: HostInfo + host: Host /** URI information */ addressInfo: FilledAddressInfo /** Indicates if we are a ui/p2p/api for the kind of interface that this is representing */ @@ -69,110 +74,103 @@ const negate = (a: A) => !fn(a) const unique = (values: A[]) => Array.from(new Set(values)) -function stringifyHostname(info: HostnameInfo): Hostname { - let base: string - if ("kind" in info.hostname && info.hostname.kind === "domain") { - base = info.hostname.subdomain - ? `${info.hostname.subdomain}.${info.hostname.domain}` - : info.hostname.domain - } else { - base = info.hostname.value +export const addressHostToUrl = ( + { scheme, sslScheme, username, suffix }: AddressInfo, + host: HostnameInfo, +): UrlString[] => { + const res = [] + const fmt = (scheme: string | null, host: HostnameInfo, port: number) => { + const includePort = + scheme && + scheme in knownProtocols && + port === knownProtocols[scheme as keyof typeof knownProtocols].defaultPort + let hostname + if (host.kind === "onion") { + hostname = host.hostname.value + } else if (host.kind === "ip") { + if (host.hostname.kind === "domain") { + hostname = `${host.hostname.subdomain ? `${host.hostname.subdomain}.` : ""}${host.hostname.domain}` + } else { + hostname = host.hostname.value + } + } + return `${scheme ? `${scheme}://` : ""}${ + username ? `${username}@` : "" + }${hostname}${includePort ? `:${port}` : ""}${suffix}` } - if (info.hostname.port && info.hostname.sslPort) { - return `${base}:${info.hostname.port}` as Hostname - } else if (info.hostname.sslPort) { - return `${base}:${info.hostname.sslPort}` as Hostname - } else if (info.hostname.port) { - return `${base}:${info.hostname.port}` as Hostname + if (host.hostname.sslPort !== null) { + res.push(fmt(sslScheme, host, host.hostname.sslPort)) } - return base as Hostname -} -const addressHostToUrl = ( - { bindOptions, username, suffix }: AddressInfo, - host: Hostname, -): UrlString => { - const scheme = host.endsWith(".onion") - ? bindOptions.scheme - : bindOptions.addSsl - ? bindOptions.addSsl.scheme - : bindOptions.scheme // TODO: encode whether hostname transport is "secure"? - return `${scheme ? `${scheme}//` : ""}${ - username ? `${username}@` : "" - }${host}${suffix}` + if (host.hostname.port !== null) { + res.push(fmt(scheme, host, host.hostname.port)) + } + + return res } + export const filledAddress = ( - hostInfo: HostInfo, + host: Host, addressInfo: AddressInfo, ): FilledAddressInfo => { const toUrl = addressHostToUrl.bind(null, addressInfo) - const hostnameInfo = - hostInfo.kind == "multi" - ? hostInfo.hostnames - : hostInfo.hostname - ? [hostInfo.hostname] - : [] + const hostnames = host.hostnameInfo[addressInfo.internalPort] + return { ...addressInfo, - hostnames: hostnameInfo.flatMap((h) => stringifyHostname(h)), + hostnames, get onionHostnames() { - return hostnameInfo - .filter((h) => h.kind === "onion") - .map((h) => stringifyHostname(h)) + return hostnames.filter((h) => h.kind === "onion") }, get localHostnames() { - return hostnameInfo - .filter((h) => h.kind === "ip" && h.hostname.kind === "local") - .map((h) => stringifyHostname(h)) + return hostnames.filter( + (h) => h.kind === "ip" && h.hostname.kind === "local", + ) }, get ipHostnames() { - return hostnameInfo - .filter( - (h) => - h.kind === "ip" && - (h.hostname.kind === "ipv4" || h.hostname.kind === "ipv6"), - ) - .map((h) => stringifyHostname(h)) + return hostnames.filter( + (h) => + h.kind === "ip" && + (h.hostname.kind === "ipv4" || h.hostname.kind === "ipv6"), + ) }, get ipv4Hostnames() { - return hostnameInfo - .filter((h) => h.kind === "ip" && h.hostname.kind === "ipv4") - .map((h) => stringifyHostname(h)) + return hostnames.filter( + (h) => h.kind === "ip" && h.hostname.kind === "ipv4", + ) }, get ipv6Hostnames() { - return hostnameInfo - .filter((h) => h.kind === "ip" && h.hostname.kind === "ipv6") - .map((h) => stringifyHostname(h)) + return hostnames.filter( + (h) => h.kind === "ip" && h.hostname.kind === "ipv6", + ) }, get nonIpHostnames() { - return hostnameInfo - .filter( - (h) => - h.kind === "ip" && - h.hostname.kind !== "ipv4" && - h.hostname.kind !== "ipv6", - ) - .map((h) => stringifyHostname(h)) + return hostnames.filter( + (h) => + h.kind === "ip" && + h.hostname.kind !== "ipv4" && + h.hostname.kind !== "ipv6", + ) }, get urls() { - return this.hostnames.map(toUrl) + return this.hostnames.flatMap(toUrl) }, get onionUrls() { - return this.onionHostnames.map(toUrl) + return this.onionHostnames.flatMap(toUrl) }, get localUrls() { - return this.localHostnames.map(toUrl) + return this.localHostnames.flatMap(toUrl) }, get ipUrls() { - return this.ipHostnames.map(toUrl) + return this.ipHostnames.flatMap(toUrl) }, get ipv4Urls() { - return this.ipv4Hostnames.map(toUrl) + return this.ipv4Hostnames.flatMap(toUrl) }, get ipv6Urls() { - return this.ipv6Hostnames.map(toUrl) + return this.ipv6Hostnames.flatMap(toUrl) }, get nonIpUrls() { - return this.nonIpHostnames.map(toUrl) + return this.nonIpHostnames.flatMap(toUrl) }, } } @@ -193,23 +191,25 @@ const makeInterfaceFilled = async ({ packageId, callback, }) - const hostInfo = await effects.getHostInfo({ - packageId, - kind: null, - serviceInterfaceId: serviceInterfaceValue.id, - callback, - }) - const primaryUrl = await effects.getPrimaryUrl({ - serviceInterfaceId: id, + const hostId = serviceInterfaceValue.addressInfo.hostId + const host = await effects.getHostInfo({ packageId, + hostId, callback, }) + const primaryUrl = await effects + .getPrimaryUrl({ + serviceInterfaceId: id, + packageId, + callback, + }) + .catch((e) => null) const interfaceFilled: ServiceInterfaceFilled = { ...serviceInterfaceValue, primaryUrl: primaryUrl, - hostInfo, - addressInfo: filledAddress(hostInfo, serviceInterfaceValue.addressInfo), + host, + addressInfo: filledAddress(host, serviceInterfaceValue.addressInfo), get primaryHostname() { if (primaryUrl == null) return null return getHostname(primaryUrl) diff --git a/sdk/lib/util/getServiceInterfaces.ts b/sdk/lib/util/getServiceInterfaces.ts index a7106568b..c4cdc6b59 100644 --- a/sdk/lib/util/getServiceInterfaces.ts +++ b/sdk/lib/util/getServiceInterfaces.ts @@ -18,41 +18,27 @@ const makeManyInterfaceFilled = async ({ packageId, callback, }) - const hostIdsRecord = Object.fromEntries( - await Promise.all( - Array.from(new Set(serviceInterfaceValues.map((x) => x.id))).map( - async (id) => - [ - id, - await effects.getHostInfo({ - kind: null, - packageId, - serviceInterfaceId: id, - callback, - }), - ] as const, - ), - ), - ) const serviceInterfacesFilled: ServiceInterfaceFilled[] = await Promise.all( - serviceInterfaceValues.map(async (serviceInterfaceValue) => { - const hostInfo = await effects.getHostInfo({ - kind: null, - packageId, - serviceInterfaceId: serviceInterfaceValue.id, - callback, - }) - const primaryUrl = await effects.getPrimaryUrl({ - serviceInterfaceId: serviceInterfaceValue.id, + Object.values(serviceInterfaceValues).map(async (serviceInterfaceValue) => { + const hostId = serviceInterfaceValue.addressInfo.hostId + const host = await effects.getHostInfo({ packageId, + hostId, callback, }) + const primaryUrl = await effects + .getPrimaryUrl({ + serviceInterfaceId: serviceInterfaceValue.id, + packageId, + callback, + }) + .catch(() => null) return { ...serviceInterfaceValue, primaryUrl: primaryUrl, - hostInfo, - addressInfo: filledAddress(hostInfo, serviceInterfaceValue.addressInfo), + host, + addressInfo: filledAddress(host, serviceInterfaceValue.addressInfo), get primaryHostname() { if (primaryUrl == null) return null return getHostname(primaryUrl) diff --git a/sdk/lib/util/index.ts b/sdk/lib/util/index.ts index bd144f35a..9b19aeb38 100644 --- a/sdk/lib/util/index.ts +++ b/sdk/lib/util/index.ts @@ -10,6 +10,8 @@ import "./once" export { GetServiceInterface, getServiceInterface } from "./getServiceInterface" export { getServiceInterfaces } from "./getServiceInterfaces" +export { addressHostToUrl } from "./getServiceInterface" +export { hostnameInfoToAddress } from "./Hostname" // prettier-ignore export type FlattenIntersection = T extends ArrayLike ? T : diff --git a/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts index 8baa0bfde..f3fe7a0cc 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.ts @@ -8,6 +8,7 @@ import { PatchDB } from 'patch-db-client' import { QRComponent } from 'src/app/components/qr/qr.component' import { map } from 'rxjs' import { T } from '@start9labs/start-sdk' +import { addressHostToUrl } from '@start9labs/start-sdk/cjs/lib/util/getServiceInterface' type MappedInterface = T.ServiceInterface & { addresses: MappedAddress[] @@ -33,10 +34,14 @@ export class AppInterfacesPage { .sort(iface => iface.name.toLowerCase() > iface.name.toLowerCase() ? -1 : 1, ) - .map(iface => ({ - ...iface, - addresses: getAddresses(iface), - })) + .map(iface => { + // TODO @Matt + const host = {} as any + return { + ...iface, + addresses: getAddresses(iface, host), + } + }) return { ui: sorted.filter(val => val.type === 'ui'), @@ -99,66 +104,40 @@ export class AppInterfacesItemComponent { } function getAddresses( - serviceInterface: T.ServiceInterfaceWithHostInfo, + serviceInterface: T.ServiceInterface, + host: T.Host, ): MappedAddress[] { - const host = serviceInterface.hostInfo const addressInfo = serviceInterface.addressInfo const username = addressInfo.username ? addressInfo.username + '@' : '' const suffix = addressInfo.suffix || '' - const hostnames = host.kind === 'multi' ? host.hostnames : [] // TODO: non-multi + const hostnames = + host.kind === 'multi' ? host.hostnameInfo[addressInfo.internalPort] : [] // TODO: non-multi /* host.hostname ? [host.hostname] : [] */ - const addresses: MappedAddress[] = [] - - hostnames.forEach(h => { + return hostnames.flatMap(h => { let name = '' - let hostname = '' if (h.kind === 'onion') { name = 'Tor' - hostname = h.hostname.value } else { const hostnameKind = h.hostname.kind if (hostnameKind === 'domain') { name = 'Domain' - hostname = `${h.hostname.subdomain}.${h.hostname.domain}` } else { name = hostnameKind === 'local' ? 'Local' : `${h.networkInterfaceId} (${hostnameKind})` - hostname = h.hostname.value } } - if (h.hostname.sslPort) { - const port = h.hostname.sslPort === 443 ? '' : `:${h.hostname.sslPort}` - const scheme = addressInfo.bindOptions.addSsl?.scheme - ? `${addressInfo.bindOptions.addSsl.scheme}://` - : '' - - addresses.push({ - name: name === 'Tor' ? 'Tor (HTTPS)' : name, - url: `${scheme}${username}${hostname}${port}${suffix}`, - }) - } - - if (h.hostname.port) { - const port = h.hostname.port === 80 ? '' : `:${h.hostname.port}` - const scheme = addressInfo.bindOptions.scheme - ? `${addressInfo.bindOptions.scheme}://` - : '' - - addresses.push({ - name: name === 'Tor' ? 'Tor (HTTP)' : name, - url: `${scheme}${username}${hostname}${port}${suffix}`, - }) - } + return addressHostToUrl(addressInfo, h).map(url => ({ + name, + url, + })) }) - - return addresses } diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index 2b0b96d23..11d56ff15 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -1422,66 +1422,11 @@ export module Mock { addressInfo: { username: null, hostId: 'abcdefg', - bindOptions: { - scheme: 'http', - preferredExternalPort: 80, - addSsl: { - // addXForwardedHeaders: false, - preferredExternalPort: 443, - scheme: 'https', - alpn: { specified: ['http/1.1', 'h2'] }, - }, - secure: null, - }, + internalPort: 80, + scheme: 'http', + sslScheme: 'https', suffix: '', }, - hostInfo: { - id: 'abcdefg', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: null, - sslPort: 1234, - }, - }, - { - kind: 'onion', - hostname: { - value: 'bitcoin-ui-address.onion', - port: 80, - sslPort: 443, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: null, - sslPort: 1234, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: null, - sslPort: 1234, - }, - }, - ], - }, }, rpc: { id: 'rpc', @@ -1495,66 +1440,11 @@ export module Mock { addressInfo: { username: null, hostId: 'bcdefgh', - bindOptions: { - scheme: 'http', - preferredExternalPort: 80, - addSsl: { - // addXForwardedHeaders: false, - preferredExternalPort: 443, - scheme: 'https', - alpn: { specified: ['http/1.1'] }, - }, - secure: null, - }, + internalPort: 8332, + scheme: 'http', + sslScheme: 'https', suffix: '', }, - hostInfo: { - id: 'bcdefgh', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: null, - sslPort: 2345, - }, - }, - { - kind: 'onion', - hostname: { - value: 'bitcoin-rpc-address.onion', - port: 80, - sslPort: 443, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: null, - sslPort: 2345, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: null, - sslPort: 2345, - }, - }, - ], - }, }, p2p: { id: 'p2p', @@ -1568,63 +1458,11 @@ export module Mock { addressInfo: { username: null, hostId: 'cdefghi', - bindOptions: { - scheme: 'bitcoin', - preferredExternalPort: 8333, - addSsl: null, - secure: { - ssl: false, - }, - }, + internalPort: 8333, + scheme: 'bitcoin', + sslScheme: null, suffix: '', }, - hostInfo: { - id: 'cdefghi', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: 3456, - sslPort: null, - }, - }, - { - kind: 'onion', - hostname: { - value: 'bitcoin-p2p-address.onion', - port: 8333, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: 3456, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: 3456, - sslPort: null, - }, - }, - ], - }, }, }, currentDependencies: {}, @@ -1660,101 +1498,11 @@ export module Mock { addressInfo: { username: null, hostId: 'hijklmnop', - bindOptions: { - scheme: 'http', - preferredExternalPort: 80, - addSsl: { - // addXForwardedHeaders: false, - preferredExternalPort: 443, - scheme: 'https', - alpn: { specified: ['http/1.1', 'h2'] }, - }, - secure: { - ssl: true, - }, - }, + internalPort: 80, + scheme: 'http', + sslScheme: 'https', suffix: '', }, - hostInfo: { - id: 'hijklmnop', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: null, - sslPort: 4567, - }, - }, - { - kind: 'onion', - hostname: { - value: 'proxy-ui-address.onion', - port: 80, - sslPort: 443, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: null, - sslPort: 4567, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: null, - sslPort: 4567, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'wlan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: null, - sslPort: 4567, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'wlan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.7', - port: null, - sslPort: 4567, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'wlan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: null, - sslPort: 4567, - }, - }, - ], - }, }, }, currentDependencies: { @@ -1801,63 +1549,11 @@ export module Mock { addressInfo: { username: null, hostId: 'qrstuv', - bindOptions: { - scheme: 'grpc', - preferredExternalPort: 10009, - addSsl: null, - secure: { - ssl: true, - }, - }, + internalPort: 10009, + scheme: null, + sslScheme: 'grpc', suffix: '', }, - hostInfo: { - id: 'qrstuv', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: 5678, - sslPort: null, - }, - }, - { - kind: 'onion', - hostname: { - value: 'lnd-grpc-address.onion', - port: 10009, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: 5678, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: 5678, - sslPort: null, - }, - }, - ], - }, }, lndconnect: { id: 'lndconnect', @@ -1871,63 +1567,11 @@ export module Mock { addressInfo: { username: null, hostId: 'qrstuv', - bindOptions: { - scheme: 'lndconnect', - preferredExternalPort: 10009, - addSsl: null, - secure: { - ssl: true, - }, - }, + internalPort: 10009, + scheme: null, + sslScheme: 'lndconnect', suffix: 'cert=askjdfbjadnaskjnd&macaroon=ksjbdfnhjasbndjksand', }, - hostInfo: { - id: 'qrstuv', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: 5678, - sslPort: null, - }, - }, - { - kind: 'onion', - hostname: { - value: 'lnd-grpc-address.onion', - port: 10009, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: 5678, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: 5678, - sslPort: null, - }, - }, - ], - }, }, p2p: { id: 'p2p', @@ -1941,63 +1585,11 @@ export module Mock { addressInfo: { username: null, hostId: 'rstuvw', - bindOptions: { - scheme: null, - preferredExternalPort: 9735, - addSsl: null, - secure: { - ssl: true, - }, - }, + internalPort: 9735, + scheme: 'lightning', + sslScheme: null, suffix: '', }, - hostInfo: { - id: 'rstuvw', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: 6789, - sslPort: null, - }, - }, - { - kind: 'onion', - hostname: { - value: 'lnd-p2p-address.onion', - port: 9735, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: 6789, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: 6789, - sslPort: null, - }, - }, - ], - }, }, }, currentDependencies: { diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index a4ed61278..ee96f9fe2 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -141,66 +141,11 @@ export const mockPatchData: DataModel = { addressInfo: { username: null, hostId: 'abcdefg', - bindOptions: { - scheme: 'http', - preferredExternalPort: 80, - addSsl: { - // addXForwardedHeaders: false, - preferredExternalPort: 443, - scheme: 'https', - alpn: { specified: ['http/1.1', 'h2'] }, - }, - secure: null, - }, + internalPort: 80, + scheme: 'http', + sslScheme: 'https', suffix: '', }, - hostInfo: { - id: 'abcdefg', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: null, - sslPort: 1234, - }, - }, - { - kind: 'onion', - hostname: { - value: 'bitcoin-ui-address.onion', - port: 80, - sslPort: 443, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: null, - sslPort: 1234, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: null, - sslPort: 1234, - }, - }, - ], - }, }, rpc: { id: 'rpc', @@ -214,66 +159,11 @@ export const mockPatchData: DataModel = { addressInfo: { username: null, hostId: 'bcdefgh', - bindOptions: { - scheme: 'http', - preferredExternalPort: 80, - addSsl: { - // addXForwardedHeaders: false, - preferredExternalPort: 443, - scheme: 'https', - alpn: { specified: ['http/1.1'] }, - }, - secure: null, - }, + internalPort: 8332, + scheme: 'http', + sslScheme: 'https', suffix: '', }, - hostInfo: { - id: 'bcdefgh', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: null, - sslPort: 2345, - }, - }, - { - kind: 'onion', - hostname: { - value: 'bitcoin-rpc-address.onion', - port: 80, - sslPort: 443, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: null, - sslPort: 2345, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: null, - sslPort: 2345, - }, - }, - ], - }, }, p2p: { id: 'p2p', @@ -287,63 +177,11 @@ export const mockPatchData: DataModel = { addressInfo: { username: null, hostId: 'cdefghi', - bindOptions: { - scheme: 'bitcoin', - preferredExternalPort: 8333, - addSsl: null, - secure: { - ssl: false, - }, - }, + internalPort: 8333, + scheme: 'bitcoin', + sslScheme: null, suffix: '', }, - hostInfo: { - id: 'cdefghi', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: 3456, - sslPort: null, - }, - }, - { - kind: 'onion', - hostname: { - value: 'bitcoin-p2p-address.onion', - port: 8333, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: 3456, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: 3456, - sslPort: null, - }, - }, - ], - }, }, }, currentDependencies: {}, @@ -382,63 +220,11 @@ export const mockPatchData: DataModel = { addressInfo: { username: null, hostId: 'qrstuv', - bindOptions: { - scheme: 'grpc', - preferredExternalPort: 10009, - addSsl: null, - secure: { - ssl: true, - }, - }, + internalPort: 10009, + scheme: null, + sslScheme: 'grpc', suffix: '', }, - hostInfo: { - id: 'qrstuv', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: 5678, - sslPort: null, - }, - }, - { - kind: 'onion', - hostname: { - value: 'lnd-grpc-address.onion', - port: 10009, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: 5678, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: 5678, - sslPort: null, - }, - }, - ], - }, }, lndconnect: { id: 'lndconnect', @@ -452,63 +238,11 @@ export const mockPatchData: DataModel = { addressInfo: { username: null, hostId: 'qrstuv', - bindOptions: { - scheme: 'lndconnect', - preferredExternalPort: 10009, - addSsl: null, - secure: { - ssl: true, - }, - }, + internalPort: 10009, + scheme: null, + sslScheme: 'lndconnect', suffix: 'cert=askjdfbjadnaskjnd&macaroon=ksjbdfnhjasbndjksand', }, - hostInfo: { - id: 'qrstuv', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: 5678, - sslPort: null, - }, - }, - { - kind: 'onion', - hostname: { - value: 'lnd-grpc-address.onion', - port: 10009, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: 5678, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: 5678, - sslPort: null, - }, - }, - ], - }, }, p2p: { id: 'p2p', @@ -522,61 +256,11 @@ export const mockPatchData: DataModel = { addressInfo: { username: null, hostId: 'rstuvw', - bindOptions: { - scheme: null, - preferredExternalPort: 9735, - addSsl: null, - secure: { ssl: true }, - }, + internalPort: 8333, + scheme: 'bitcoin', + sslScheme: null, suffix: '', }, - hostInfo: { - id: 'rstuvw', - kind: 'multi', - hostnames: [ - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'local', - value: 'adjective-noun.local', - port: 6789, - sslPort: null, - }, - }, - { - kind: 'onion', - hostname: { - value: 'lnd-p2p-address.onion', - port: 9735, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv4', - value: '192.168.1.5', - port: 6789, - sslPort: null, - }, - }, - { - kind: 'ip', - networkInterfaceId: 'elan0', - public: false, - hostname: { - kind: 'ipv6', - value: '[2001:db8:85a3:8d3:1319:8a2e:370:7348]', - port: 6789, - sslPort: null, - }, - }, - ], - }, }, }, currentDependencies: { diff --git a/web/projects/ui/src/app/services/config.service.ts b/web/projects/ui/src/app/services/config.service.ts index 3eca4f48d..42116bdd8 100644 --- a/web/projects/ui/src/app/services/config.service.ts +++ b/web/projects/ui/src/app/services/config.service.ts @@ -57,12 +57,14 @@ export class ConfigService { } /** ${scheme}://${username}@${host}:${externalPort}${suffix} */ - launchableAddress(interfaces: PackageDataEntry['serviceInterfaces']): string { + launchableAddress( + interfaces: PackageDataEntry['serviceInterfaces'], + host: T.Host, + ): string { const ui = Object.values(interfaces).find(i => i.type === 'ui') if (!ui) return '' - const host = ui.hostInfo const addressInfo = ui.addressInfo const scheme = this.isHttps() ? 'https' : 'http' const username = addressInfo.username ? addressInfo.username + '@' : '' @@ -70,20 +72,25 @@ export class ConfigService { const url = new URL(`${scheme}://${username}placeholder${suffix}`) if (host.kind === 'multi') { - const onionHostname = host.hostnames.find(h => h.kind === 'onion') - ?.hostname as T.ExportedOnionHostname + const onionHostname = host.addresses.find(h => h.kind === 'onion') + ?.address as T.OnionHostname | undefined + + if (!onionHostname) + throw new Error('Expecting that there is an onion hostname') if (this.isTor() && onionHostname) { url.hostname = onionHostname.value - } else { - const ipHostname = host.hostnames.find(h => h.kind === 'ip') - ?.hostname as T.ExportedIpHostname - - if (!ipHostname) return '' - - url.hostname = this.hostname - url.port = String(ipHostname.sslPort || ipHostname.port) } + // TODO Handle single + // else { + // const ipHostname = host.addresses.find(h => h.kind === 'ip') + // ?.hostname as T.ExportedIpHostname + + // if (!ipHostname) return '' + + // url.hostname = this.hostname + // url.port = String(ipHostname.sslPort || ipHostname.port) + // } } else { throw new Error('unimplemented') // const hostname = {} as T.ExportedHostnameInfo // host.hostname diff --git a/web/projects/ui/src/app/services/ui-launcher.service.ts b/web/projects/ui/src/app/services/ui-launcher.service.ts index badbc7a3e..9c36036f3 100644 --- a/web/projects/ui/src/app/services/ui-launcher.service.ts +++ b/web/projects/ui/src/app/services/ui-launcher.service.ts @@ -2,6 +2,7 @@ import { Inject, Injectable } from '@angular/core' import { WINDOW } from '@ng-web-apis/common' import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { ConfigService } from './config.service' +import { T } from '@start9labs/start-sdk' @Injectable({ providedIn: 'root', @@ -13,8 +14,10 @@ export class UiLauncherService { ) {} launch(interfaces: PackageDataEntry['serviceInterfaces']): void { + // TODO @Matt + const host = {} as any this.windowRef.open( - this.config.launchableAddress(interfaces), + this.config.launchableAddress(interfaces, host), '_blank', 'noreferrer', ) From 4d6cb091cca261e17f5f84ea81c190968ecbadd0 Mon Sep 17 00:00:00 2001 From: Jade <2364004+Blu-J@users.noreply.github.com> Date: Fri, 7 Jun 2024 12:17:45 -0600 Subject: [PATCH 030/125] Feature/disk usage (#2637) * feat: Add disk usage * Fixed: let the set config work with nesting. * chore: Changes * chore: Add default route * fix: Tor only config * chore --- .../Systems/SystemForEmbassy/index.ts | 29 +++++++-- .../SystemForEmbassy/oldEmbassyTypes.ts | 4 ++ .../SystemForEmbassy/polyfillEffects.ts | 64 +++++++++++++++++-- .../src/Models/DockerProcedure.ts | 4 +- container-runtime/src/Models/Duration.ts | 26 +++++++- 5 files changed, 113 insertions(+), 14 deletions(-) diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index 8e0cb28c5..0bdb0d769 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -2,7 +2,7 @@ import { types as T, utils, EmVer } from "@start9labs/start-sdk" import * as fs from "fs/promises" import { PolyfillEffects } from "./polyfillEffects" -import { Duration, duration } from "../../../Models/Duration" +import { Duration, duration, fromDuration } from "../../../Models/Duration" import { System } from "../../../Interfaces/System" import { matchManifest, Manifest, Procedure } from "./matchManifest" import * as childProcess from "node:child_process" @@ -478,10 +478,13 @@ export class SystemForEmbassy implements System { delete this.currentRunning if (currentRunning) { await currentRunning.clean({ - timeout: this.manifest.main["sigterm-timeout"], + timeout: fromDuration(this.manifest.main["sigterm-timeout"]), }) } - const durationValue = duration(this.manifest.main["sigterm-timeout"], "s") + const durationValue = duration( + fromDuration(this.manifest.main["sigterm-timeout"]), + "s", + ) return durationValue } private async createBackup( @@ -967,7 +970,7 @@ async function updateConfig( const newConfigValue = mutConfigValue[key] if (matchSpec.test(specValue)) { - const updateObject = { spec: null } + const updateObject = { spec: newConfigValue } await updateConfig( effects, manifest, @@ -1001,6 +1004,10 @@ async function updateConfig( manifest, specInterface, ) + if (!serviceInterfaceId) { + mutConfigValue[key] = "" + return + } const filled = await utils .getServiceInterface(effects, { packageId: specValue["package-id"], @@ -1035,8 +1042,16 @@ async function updateConfig( } } function extractServiceInterfaceId(manifest: Manifest, specInterface: string) { - let serviceInterfaceId - const lanConfig = manifest.interfaces[specInterface]?.["lan-config"] || {} - serviceInterfaceId = `${specInterface}-${Object.entries(lanConfig)[0]?.[1]?.internal}` + const internalPort = + Object.entries( + manifest.interfaces[specInterface]?.["lan-config"] || {}, + )[0]?.[1]?.internal || + Object.entries( + manifest.interfaces[specInterface]?.["tor-config"]?.["port-mapping"] || + {}, + )?.[0]?.[1] + + if (!internalPort) return null + const serviceInterfaceId = `${specInterface}-${internalPort}` return serviceInterfaceId } diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/oldEmbassyTypes.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/oldEmbassyTypes.ts index 0d7521626..73d130c9a 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/oldEmbassyTypes.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/oldEmbassyTypes.ts @@ -120,6 +120,10 @@ export type Effects = { /// Returns the body as a json json(): Promise }> + diskUsage(options?: { + volumeId: string + path: string + }): Promise<{ used: number; total: number }> runRsync(options: { srcVolume: string diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts index 12c0a58d5..3a8d5f624 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts @@ -8,7 +8,8 @@ import { HostSystemStartOs } from "../../HostSystemStartOs" import "isomorphic-fetch" import { Manifest } from "./matchManifest" import { DockerProcedureContainer } from "./DockerProcedureContainer" - +import * as cp from "child_process" +export const execFile = promisify(cp.execFile) export class PolyfillEffects implements oet.Effects { constructor( readonly effects: HostSystemStartOs, @@ -104,7 +105,9 @@ export class PolyfillEffects implements oet.Effects { stderr: x.stderr.toString(), stdout: x.stdout.toString(), })) - .then((x) => (!!x.stderr ? { error: x.stderr } : { result: x.stdout })) + .then((x: any) => + !!x.stderr ? { error: x.stderr } : { result: x.stdout }, + ) } runDaemon(input: { command: string; args?: string[] | undefined }): { wait(): Promise> @@ -163,7 +166,7 @@ export class PolyfillEffects implements oet.Effects { stderr: x.stderr.toString(), stdout: x.stdout.toString(), })) - .then((x) => { + .then((x: any) => { if (!!x.stderr) { throw new Error(x.stderr) } @@ -198,7 +201,7 @@ export class PolyfillEffects implements oet.Effects { stderr: x.stderr.toString(), stdout: x.stdout.toString(), })) - .then((x) => { + .then((x: any) => { if (!!x.stderr) { throw new Error(x.stderr) } @@ -352,7 +355,7 @@ export class PolyfillEffects implements oet.Effects { return String(pid) } const waitPromise = new Promise((resolve, reject) => { - spawned.on("exit", (code) => { + spawned.on("exit", (code: any) => { if (code === 0) { resolve(null) } else { @@ -364,4 +367,55 @@ export class PolyfillEffects implements oet.Effects { const progress = () => Promise.resolve(percentage) return { id, wait, progress } } + async diskUsage( + options?: { volumeId: string; path: string } | undefined, + ): Promise<{ used: number; total: number }> { + const output = await execFile("df", ["--block-size=1", "-P", "/"]) + .then((x: any) => ({ + stderr: x.stderr.toString(), + stdout: x.stdout.toString(), + })) + .then((x: any) => { + if (!!x.stderr) { + throw new Error(x.stderr) + } + return parseDfOutput(x.stdout) + }) + if (!!options) { + const used = await execFile("du", [ + "-s", + "--block-size=1", + "-P", + new Volume(options.volumeId, options.path).path, + ]) + .then((x: any) => ({ + stderr: x.stderr.toString(), + stdout: x.stdout.toString(), + })) + .then((x: any) => { + if (!!x.stderr) { + throw new Error(x.stderr) + } + return Number.parseInt(x.stdout.split(/\s+/)[0]) + }) + return { + ...output, + used, + } + } + return output + } +} + +function parseDfOutput(output: string): { used: number; total: number } { + const lines = output + .split("\n") + .filter((x) => x.length) + .map((x) => x.split(/\s+/)) + const index = lines.splice(0, 1)[0].map((x) => x.toLowerCase()) + const usedIndex = index.indexOf("used") + const availableIndex = index.indexOf("available") + const used = lines.map((x) => Number.parseInt(x[usedIndex]))[0] || 0 + const total = lines.map((x) => Number.parseInt(x[availableIndex]))[0] || 0 + return { used, total } } diff --git a/container-runtime/src/Models/DockerProcedure.ts b/container-runtime/src/Models/DockerProcedure.ts index 91ae73b5f..20c8145ab 100644 --- a/container-runtime/src/Models/DockerProcedure.ts +++ b/container-runtime/src/Models/DockerProcedure.ts @@ -8,7 +8,9 @@ import { literals, number, Parser, + some, } from "ts-matches" +import { matchDuration } from "./Duration" const VolumeId = string const Path = string @@ -31,7 +33,7 @@ export const matchDockerProcedure = object( "toml", "toml-pretty", ), - "sigterm-timeout": number, + "sigterm-timeout": some(number, matchDuration), inject: boolean, }, ["io-format", "sigterm-timeout", "system", "args", "inject", "mounts"], diff --git a/container-runtime/src/Models/Duration.ts b/container-runtime/src/Models/Duration.ts index 75154c782..5f61c362a 100644 --- a/container-runtime/src/Models/Duration.ts +++ b/container-runtime/src/Models/Duration.ts @@ -1,6 +1,30 @@ -export type TimeUnit = "d" | "h" | "s" | "ms" +import { string } from "ts-matches" + +export type TimeUnit = "d" | "h" | "s" | "ms" | "m" | "µs" | "ns" export type Duration = `${number}${TimeUnit}` +const durationRegex = /^([0-9]*(\.[0-9]+)?)(ns|µs|ms|s|m|d)$/ + +export const matchDuration = string.refine(isDuration) +export function isDuration(value: string): value is Duration { + return durationRegex.test(value) +} + export function duration(timeValue: number, timeUnit: TimeUnit = "s") { return `${timeValue > 0 ? timeValue : 0}${timeUnit}` as Duration } +const unitsToSeconds: Record = { + ns: 1e-9, + µs: 1e-6, + ms: 0.001, + s: 1, + m: 60, + h: 3600, + d: 86400, +} + +export function fromDuration(duration: Duration | number): number { + if (typeof duration === "number") return duration + const [, num, , unit] = duration.match(durationRegex) || [] + return Number(num) * unitsToSeconds[unit] +} From 4afd3c2322ab30ab877c491f08b1c6a4cd23eb51 Mon Sep 17 00:00:00 2001 From: Shadowy Super Coder Date: Mon, 10 Jun 2024 18:56:39 -0600 Subject: [PATCH 031/125] move MAU tracking back to registry --- core/startos/src/analytics/mod.rs | 36 +-------------------- core/startos/src/registry/context.rs | 12 +++++++ core/startos/src/registry/os/version/mod.rs | 26 ++++++++++++++- 3 files changed, 38 insertions(+), 36 deletions(-) diff --git a/core/startos/src/analytics/mod.rs b/core/startos/src/analytics/mod.rs index 1fa08dd3f..7c84ff386 100644 --- a/core/startos/src/analytics/mod.rs +++ b/core/startos/src/analytics/mod.rs @@ -1,27 +1,19 @@ use std::net::SocketAddr; use axum::Router; -use chrono::Utc; use futures::future::ready; -use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler, Server}; -use sqlx::query; -use ts_rs::TS; +use rpc_toolkit::{Context, ParentHandler, Server}; use crate::analytics::context::AnalyticsContext; use crate::middleware::cors::Cors; use crate::net::static_server::{bad_request, not_found, server_error}; use crate::net::web_server::WebServer; use crate::rpc_continuations::Guid; -use crate::Error; pub mod context; pub fn analytics_api() -> ParentHandler { ParentHandler::new() - .subcommand( - "recordUserActivity", - from_fn_async(record_user_activity).no_cli(), - ) } pub fn analytics_server_router(ctx: AnalyticsContext) -> Router { @@ -84,29 +76,3 @@ impl WebServer { Self::new(bind, analytics_server_router(ctx)) } } - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct ActivityParams { - server_id: String, - arch: String, -} - -async fn record_user_activity( - ctx: AnalyticsContext, - ActivityParams { server_id, arch }: ActivityParams, -) -> Result<(), Error> { - let pool = ctx.db; - let created_at = Utc::now().to_rfc3339(); - - query!("INSERT INTO user_activity (created_at, server_id, os_version, arch) VALUES ($1, $2, $3, $4)", - created_at, - server_id, - arch - ) - .execute(pool) - .await?; - - Ok(()) -} diff --git a/core/startos/src/registry/context.rs b/core/startos/src/registry/context.rs index 0461aa924..64a157073 100644 --- a/core/startos/src/registry/context.rs +++ b/core/startos/src/registry/context.rs @@ -10,6 +10,7 @@ use reqwest::{Client, Proxy}; use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::{CallRemote, Context, Empty}; use serde::{Deserialize, Serialize}; +use sqlx::PgPool; use tokio::sync::broadcast::Sender; use tracing::instrument; use url::Url; @@ -37,6 +38,8 @@ pub struct RegistryConfig { pub tor_proxy: Option, #[arg(short = 'd', long = "datadir")] pub datadir: Option, + #[arg(short = 'u', long = "pg-connection-url")] + pub pg_connection_url: Option, } impl ContextConfig for RegistryConfig { fn next(&mut self) -> Option { @@ -67,6 +70,7 @@ pub struct RegistryContextSeed { pub rpc_continuations: RpcContinuations, pub client: Client, pub shutdown: Sender<()>, + pub pool: Option, } #[derive(Clone)] @@ -94,6 +98,13 @@ impl RegistryContext { .clone() .map(Ok) .unwrap_or_else(|| "socks5h://localhost:9050".parse())?; + let pool: Option = match &config.pg_connection_url { + Some(url) => match PgPool::connect(url.as_str()).await { + Ok(pool) => Some(pool), + Err(_) => None, + }, + None => None, + }; Ok(Self(Arc::new(RegistryContextSeed { hostname: config .hostname @@ -122,6 +133,7 @@ impl RegistryContext { .build() .with_kind(crate::ErrorKind::ParseUrl)?, shutdown, + pool, }))) } } diff --git a/core/startos/src/registry/os/version/mod.rs b/core/startos/src/registry/os/version/mod.rs index 5bf926e5e..cb36f078a 100644 --- a/core/startos/src/registry/os/version/mod.rs +++ b/core/startos/src/registry/os/version/mod.rs @@ -1,10 +1,12 @@ use std::collections::BTreeMap; +use chrono::Utc; use clap::Parser; use emver::VersionRange; use itertools::Itertools; use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; +use sqlx::query; use ts_rs::TS; use crate::context::CliContext; @@ -126,12 +128,34 @@ pub struct GetVersionParams { #[ts(type = "string | null")] #[arg(long = "target")] pub target: Option, + #[ts(type = "string | null")] + #[arg(long = "id")] + server_id: Option, + #[ts(type = "string | null")] + #[arg(long = "arch")] + arch: Option, } pub async fn get_version( ctx: RegistryContext, - GetVersionParams { source, target }: GetVersionParams, + GetVersionParams { + source, + target, + server_id, + arch, + }: GetVersionParams, ) -> Result, Error> { + if let (Some(pool), Some(server_id), Some(arch)) = (ctx.pool, server_id, arch) { + let created_at = Utc::now().to_rfc3339(); + + query!("INSERT INTO user_activity (created_at, server_id, os_version, arch) VALUES ($1, $2, $3, $4)", + created_at, + server_id, + arch + ) + .execute(pool) + .await?; + } let target = target.unwrap_or(VersionRange::Any); ctx.db .peek() From 5aefb707fa3d882098cfad2685ddf31344dd3371 Mon Sep 17 00:00:00 2001 From: Jade <2364004+Blu-J@users.noreply.github.com> Date: Mon, 10 Jun 2024 22:38:12 -0600 Subject: [PATCH 032/125] feat: Add the merge to the file. (#2643) * feat: Add the merge to the file. * chore: Fix the early escape --- sdk/lib/util/fileHelper.ts | 7 +++++++ sdk/package-lock.json | 17 +++++++++++++++-- sdk/package.json | 6 ++++-- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/sdk/lib/util/fileHelper.ts b/sdk/lib/util/fileHelper.ts index 56706f95a..07cfb2c4a 100644 --- a/sdk/lib/util/fileHelper.ts +++ b/sdk/lib/util/fileHelper.ts @@ -1,6 +1,7 @@ import * as matches from "ts-matches" import * as YAML from "yaml" import * as TOML from "@iarna/toml" +import _ from "lodash" import * as T from "../types" import * as fs from "fs" @@ -82,6 +83,12 @@ export class FileHelper { ), ) } + + async merge(data: A, effects: T.Effects) { + const fileData = (await this.read(effects).catch(() => ({}))) || {} + const mergeData = _.merge({}, fileData, data) + return await this.write(mergeData, effects) + } /** * Create a File Helper for an arbitrary file type. * diff --git a/sdk/package-lock.json b/sdk/package-lock.json index f0d61ea4d..0c457540f 100644 --- a/sdk/package-lock.json +++ b/sdk/package-lock.json @@ -1,20 +1,22 @@ { "name": "@start9labs/start-sdk", - "version": "0.4.0-rev0.lib0.rc8.beta10", + "version": "0.3.6-alpha1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@start9labs/start-sdk", - "version": "0.4.0-rev0.lib0.rc8.beta10", + "version": "0.3.6-alpha1", "license": "MIT", "dependencies": { "isomorphic-fetch": "^3.0.0", + "lodash": "4.*.*", "ts-matches": "^5.4.1" }, "devDependencies": { "@iarna/toml": "^2.2.5", "@types/jest": "^29.4.0", + "@types/lodash": "^4.17.5", "jest": "^29.4.3", "prettier": "^3.2.5", "ts-jest": "^29.0.5", @@ -1136,6 +1138,12 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw==", + "dev": true + }, "node_modules/@types/node": { "version": "18.15.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.10.tgz", @@ -2841,6 +2849,11 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", diff --git a/sdk/package.json b/sdk/package.json index db387c036..53ee291ee 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -31,6 +31,7 @@ "homepage": "https://github.com/Start9Labs/start-sdk#readme", "dependencies": { "isomorphic-fetch": "^3.0.0", + "lodash": "4.*.*", "ts-matches": "^5.4.1" }, "prettier": { @@ -41,13 +42,14 @@ }, "devDependencies": { "@iarna/toml": "^2.2.5", - "yaml": "^2.2.2", "@types/jest": "^29.4.0", + "@types/lodash": "^4.17.5", "jest": "^29.4.3", "prettier": "^3.2.5", "ts-jest": "^29.0.5", "ts-node": "^10.9.1", "tsx": "^4.7.1", - "typescript": "^5.0.4" + "typescript": "^5.0.4", + "yaml": "^2.2.2" } } From 3f380fa0da46c041db7313c9e006b1724b5af8ff Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Wed, 12 Jun 2024 11:46:59 -0600 Subject: [PATCH 033/125] feature: pack s9pk (#2642) * TODO: images * wip * pack s9pk images * include path in packsource error * debug info * add cmd as context to invoke * filehelper bugfix * fix file helper * fix exposeForDependents * misc fixes * force image removal * fix filtering * fix deadlock * fix api * chore: Up the version of the package.json * always allow concurrency within same call stack * Update core/startos/src/s9pk/merkle_archive/expected.rs Co-authored-by: Jade <2364004+Blu-J@users.noreply.github.com> --------- Co-authored-by: J H Co-authored-by: Jade <2364004+Blu-J@users.noreply.github.com> --- Makefile | 4 + container-runtime/deb-install.sh | 2 +- container-runtime/package-lock.json | 1797 +++++------------ container-runtime/package.json | 1 + .../src/Adapters/HostSystemStartOs.ts | 26 +- container-runtime/src/Adapters/RpcListener.ts | 3 + .../Systems/SystemForEmbassy/index.ts | 7 +- .../src/Adapters/Systems/SystemForStartOs.ts | 139 +- .../src/Adapters/Systems/index.ts | 22 +- container-runtime/src/Interfaces/System.ts | 1 + core/Cargo.lock | 172 +- core/startos/Cargo.toml | 4 +- core/startos/src/action.rs | 2 + core/startos/src/auth.rs | 1 + core/startos/src/backup/restore.rs | 1 - core/startos/src/config/mod.rs | 5 +- core/startos/src/context/config.rs | 21 +- core/startos/src/context/rpc.rs | 6 + core/startos/src/control.rs | 7 +- core/startos/src/dependencies.rs | 13 +- core/startos/src/disk/mount/backup.rs | 1 - .../src/disk/mount/filesystem/overlayfs.rs | 23 +- core/startos/src/disk/mount/guard.rs | 19 +- core/startos/src/init.rs | 2 +- core/startos/src/install/mod.rs | 2 - core/startos/src/lib.rs | 6 +- core/startos/src/lxc/mod.rs | 20 +- core/startos/src/os_install/gpt.rs | 2 +- core/startos/src/os_install/mod.rs | 2 +- core/startos/src/registry/device_info.rs | 2 +- core/startos/src/registry/package/add.rs | 4 +- core/startos/src/rpc_continuations.rs | 5 + .../s9pk/merkle_archive/directory_contents.rs | 5 +- .../src/s9pk/merkle_archive/expected.rs | 103 + core/startos/src/s9pk/merkle_archive/mod.rs | 4 + .../src/s9pk/merkle_archive/source/mod.rs | 5 + .../source/multi_cursor_file.rs | 2 +- core/startos/src/s9pk/rpc.rs | 130 +- core/startos/src/s9pk/v2/compat.rs | 67 +- core/startos/src/s9pk/v2/manifest.rs | 85 +- core/startos/src/s9pk/v2/mod.rs | 76 +- core/startos/src/s9pk/v2/pack.rs | 536 +++++ core/startos/src/service/action.rs | 27 +- core/startos/src/service/config.rs | 15 +- core/startos/src/service/control.rs | 13 +- core/startos/src/service/dependencies.rs | 67 +- core/startos/src/service/mod.rs | 179 +- .../src/service/persistent_container.rs | 97 +- core/startos/src/service/properties.rs | 2 + core/startos/src/service/rpc.rs | 10 +- .../src/service/service_effect_handler.rs | 326 +-- core/startos/src/service/service_map.rs | 38 +- core/startos/src/service/transition/backup.rs | 10 +- .../startos/src/service/transition/restart.rs | 9 +- .../startos/src/service/transition/restore.rs | 10 +- core/startos/src/util/actor/concurrent.rs | 39 +- core/startos/src/util/actor/mod.rs | 12 +- core/startos/src/util/actor/simple.rs | 7 +- core/startos/src/util/io.rs | 10 +- core/startos/src/util/mod.rs | 248 ++- sdk/lib/StartSdk.ts | 9 +- sdk/lib/health/HealthCheck.ts | 3 +- sdk/lib/interfaces/Host.ts | 4 +- sdk/lib/mainFn/CommandController.ts | 4 +- sdk/lib/mainFn/Daemon.ts | 4 +- sdk/lib/mainFn/Daemons.ts | 21 +- sdk/lib/manifest/ManifestTypes.ts | 6 +- sdk/lib/manifest/setupManifest.ts | 9 +- .../osBindings/CreateOverlayedImageParams.ts | 3 +- .../osBindings/DestroyOverlayedImageParams.ts | 3 +- sdk/lib/osBindings/ImageConfig.ts | 8 + sdk/lib/osBindings/ImageMetadata.ts | 3 + sdk/lib/osBindings/ImageSource.ts | 6 + sdk/lib/osBindings/LoginParams.ts | 4 + sdk/lib/osBindings/Manifest.ts | 3 +- sdk/lib/osBindings/index.ts | 4 + sdk/lib/test/configBuilder.test.ts | 2 +- sdk/lib/test/output.sdk.ts | 2 +- sdk/lib/types.ts | 24 +- sdk/lib/util/Overlay.ts | 16 +- sdk/lib/util/fileHelper.ts | 30 +- sdk/package-lock.json | 2 +- sdk/package.json | 12 +- .../ui/src/app/services/api/api.fixures.ts | 24 +- 84 files changed, 2552 insertions(+), 2108 deletions(-) create mode 100644 core/startos/src/s9pk/merkle_archive/expected.rs create mode 100644 core/startos/src/s9pk/v2/pack.rs create mode 100644 sdk/lib/osBindings/ImageConfig.ts create mode 100644 sdk/lib/osBindings/ImageMetadata.ts create mode 100644 sdk/lib/osBindings/ImageSource.ts create mode 100644 sdk/lib/osBindings/LoginParams.ts diff --git a/Makefile b/Makefile index ff5b9c4ad..34064e799 100644 --- a/Makefile +++ b/Makefile @@ -160,6 +160,10 @@ wormhole-deb: results/$(BASENAME).deb @echo "Paste the following command into the shell of your start-os server:" @wormhole send results/$(BASENAME).deb 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo /usr/lib/startos/scripts/chroot-and-upgrade '"'"'cd $$(mktemp -d) && wormhole receive --accept-file %s && apt-get install -y --reinstall ./$(BASENAME).deb'"'"'\n", $$3 }' +wormhole-cli: core/target/$(ARCH)-unknown-linux-musl/release/start-cli + @echo "Paste the following command into the shell of your start-os server:" + @wormhole send results/$(BASENAME).deb 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo /usr/lib/startos/scripts/chroot-and-upgrade '"'"'cd $$(mktemp -d) && wormhole receive --accept-file %s && apt-get install -y --reinstall ./$(BASENAME).deb'"'"'\n", $$3 }' + update: $(ALL_TARGETS) @if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi $(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create') diff --git a/container-runtime/deb-install.sh b/container-runtime/deb-install.sh index b439c6308..697bfd10e 100644 --- a/container-runtime/deb-install.sh +++ b/container-runtime/deb-install.sh @@ -6,7 +6,7 @@ mkdir -p /run/systemd/resolve echo "nameserver 8.8.8.8" > /run/systemd/resolve/stub-resolv.conf apt-get update -apt-get install -y curl rsync +apt-get install -y curl rsync qemu-user-static curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash source ~/.bashrc diff --git a/container-runtime/package-lock.json b/container-runtime/package-lock.json index 838dcb769..9b211c077 100644 --- a/container-runtime/package-lock.json +++ b/container-runtime/package-lock.json @@ -13,6 +13,7 @@ "esbuild-plugin-resolve": "^2.0.0", "filebrowser": "^1.0.0", "isomorphic-fetch": "^3.0.0", + "lodash": "^4.17.21", "node-fetch": "^3.1.0", "ts-matches": "^5.5.1", "tslib": "^2.5.3", @@ -33,11 +34,13 @@ "license": "MIT", "dependencies": { "isomorphic-fetch": "^3.0.0", + "lodash": "^4.17.21", "ts-matches": "^5.4.1" }, "devDependencies": { "@iarna/toml": "^2.2.5", "@types/jest": "^29.4.0", + "@types/lodash": "^4.17.5", "jest": "^29.4.3", "prettier": "^3.2.5", "ts-jest": "^29.0.5", @@ -49,14 +52,12 @@ }, "node_modules/@iarna/toml": { "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", - "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==" + "license": "ISC" }, "node_modules/@mole-inc/bin-wrapper": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@mole-inc/bin-wrapper/-/bin-wrapper-8.0.1.tgz", - "integrity": "sha512-sTGoeZnjI8N4KS+sW2AN95gDBErhAguvkw/tWdCjeM8bvxpz5lqrnd0vOJABA1A+Ic3zED7PYoLP/RANLgVotA==", "dev": true, + "license": "MIT", "dependencies": { "bin-check": "^4.1.0", "bin-version-check": "^5.0.0", @@ -73,9 +74,8 @@ }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -86,18 +86,16 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -108,9 +106,8 @@ }, "node_modules/@sindresorhus/is": { "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -124,9 +121,8 @@ }, "node_modules/@swc/cli": { "version": "0.1.65", - "resolved": "https://registry.npmjs.org/@swc/cli/-/cli-0.1.65.tgz", - "integrity": "sha512-4NcgsvJVHhA7trDnMmkGLLvWMHu2kSy+qHx6QwRhhJhdiYdNUrhdp+ERxen73sYtaeEOYeLJcWrQ60nzKi6rpg==", "dev": true, + "license": "MIT", "dependencies": { "@mole-inc/bin-wrapper": "^8.0.1", "commander": "^7.1.0", @@ -155,14 +151,13 @@ } }, "node_modules/@swc/core": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.4.1.tgz", - "integrity": "sha512-3y+Y8js+e7BbM16iND+6Rcs3jdiL28q3iVtYsCviYSSpP2uUVKkp5sJnCY4pg8AaVvyN7CGQHO7gLEZQ5ByozQ==", + "version": "1.5.28", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@swc/counter": "^0.1.2", - "@swc/types": "^0.1.5" + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.8" }, "engines": { "node": ">=10" @@ -172,19 +167,19 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.4.1", - "@swc/core-darwin-x64": "1.4.1", - "@swc/core-linux-arm-gnueabihf": "1.4.1", - "@swc/core-linux-arm64-gnu": "1.4.1", - "@swc/core-linux-arm64-musl": "1.4.1", - "@swc/core-linux-x64-gnu": "1.4.1", - "@swc/core-linux-x64-musl": "1.4.1", - "@swc/core-win32-arm64-msvc": "1.4.1", - "@swc/core-win32-ia32-msvc": "1.4.1", - "@swc/core-win32-x64-msvc": "1.4.1" + "@swc/core-darwin-arm64": "1.5.28", + "@swc/core-darwin-x64": "1.5.28", + "@swc/core-linux-arm-gnueabihf": "1.5.28", + "@swc/core-linux-arm64-gnu": "1.5.28", + "@swc/core-linux-arm64-musl": "1.5.28", + "@swc/core-linux-x64-gnu": "1.5.28", + "@swc/core-linux-x64-musl": "1.5.28", + "@swc/core-win32-arm64-msvc": "1.5.28", + "@swc/core-win32-ia32-msvc": "1.5.28", + "@swc/core-win32-x64-msvc": "1.5.28" }, "peerDependencies": { - "@swc/helpers": "^0.5.0" + "@swc/helpers": "*" }, "peerDependenciesMeta": { "@swc/helpers": { @@ -192,94 +187,13 @@ } } }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.4.1.tgz", - "integrity": "sha512-ePyfx0348UbR4DOAW24TedeJbafnzha8liXFGuQ4bdXtEVXhLfPngprrxKrAddCuv42F9aTxydlF6+adD3FBhA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-darwin-x64": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.4.1.tgz", - "integrity": "sha512-eLf4JSe6VkCMdDowjM8XNC5rO+BrgfbluEzAVtKR8L2HacNYukieumN7EzpYCi0uF1BYwu1ku6tLyG2r0VcGxA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.4.1.tgz", - "integrity": "sha512-K8VtTLWMw+rkN/jDC9o/Q9SMmzdiHwYo2CfgkwVT29NsGccwmNhCQx6XoYiPKyKGIFKt4tdQnJHKUFzxUqQVtQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.4.1.tgz", - "integrity": "sha512-0e8p4g0Bfkt8lkiWgcdiENH3RzkcqKtpRXIVNGOmVc0OBkvc2tpm2WTx/eoCnes2HpTT4CTtR3Zljj4knQ4Fvw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.4.1.tgz", - "integrity": "sha512-b/vWGQo2n7lZVUnSQ7NBq3Qrj85GrAPPiRbpqaIGwOytiFSk8VULFihbEUwDe0rXgY4LDm8z8wkgADZcLnmdUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.4.1.tgz", - "integrity": "sha512-AFMQlvkKEdNi1Vk2GFTxxJzbICttBsOQaXa98kFTeWTnFFIyiIj2w7Sk8XRTEJ/AjF8ia8JPKb1zddBWr9+bEQ==", + "version": "1.5.28", "cpu": [ "x64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -289,13 +203,12 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.4.1.tgz", - "integrity": "sha512-QX2MxIECX1gfvUVZY+jk528/oFkS9MAl76e3ZRvG2KC/aKlCQL0KSzcTSm13mOxkDKS30EaGRDRQWNukGpMeRg==", + "version": "1.5.28", "cpu": [ "x64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -304,71 +217,23 @@ "node": ">=10" } }, - "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.4.1.tgz", - "integrity": "sha512-OklkJYXXI/tntD2zaY8i3iZldpyDw5q+NAP3k9OlQ7wXXf37djRsHLV0NW4+ZNHBjE9xp2RsXJ0jlOJhfgGoFA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.4.1.tgz", - "integrity": "sha512-MBuc3/QfKX9FnLOU7iGN+6yHRTQaPQ9WskiC8s8JFiKQ+7I2p25tay2RplR9dIEEGgVAu6L7auv96LbNTh+FaA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.4.1.tgz", - "integrity": "sha512-lu4h4wFBb/bOK6N2MuZwg7TrEpwYXgpQf5R7ObNSXL65BwZ9BG8XRzD+dLJmALu8l5N08rP/TrpoKRoGT4WSxw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, "node_modules/@swc/counter": { "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/@swc/types": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", - "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", - "dev": true + "version": "0.1.8", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", "dev": true, + "license": "MIT", "dependencies": { "defer-to-connect": "^2.0.0" }, @@ -378,15 +243,13 @@ }, "node_modules/@tokenizer/token": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/cacheable-request": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", "dev": true, + "license": "MIT", "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", @@ -396,41 +259,36 @@ }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/keyv": { "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/node": { - "version": "20.11.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", - "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", + "version": "20.14.2", "dev": true, + "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@types/responselike": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/accepts": { "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -441,8 +299,6 @@ }, "node_modules/arch": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", - "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", "dev": true, "funding": [ { @@ -457,24 +313,22 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/array-flatten": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + "license": "MIT" }, "node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/bin-check": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bin-check/-/bin-check-4.1.0.tgz", - "integrity": "sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==", "dev": true, + "license": "MIT", "dependencies": { "execa": "^0.7.0", "executable": "^4.1.0" @@ -485,9 +339,8 @@ }, "node_modules/bin-version": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/bin-version/-/bin-version-6.0.0.tgz", - "integrity": "sha512-nk5wEsP4RiKjG+vF+uG8lFsEn4d7Y6FVDamzzftSunXOoOcOOkzcWdKVlGgFFwlUQCj63SgnUkLLGF8v7lufhw==", "dev": true, + "license": "MIT", "dependencies": { "execa": "^5.0.0", "find-versions": "^5.0.0" @@ -501,9 +354,8 @@ }, "node_modules/bin-version-check": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bin-version-check/-/bin-version-check-5.1.0.tgz", - "integrity": "sha512-bYsvMqJ8yNGILLz1KP9zKLzQ6YpljV3ln1gqhuLkUtyfGi3qXKGuK2p+U4NAvjVFzDFiBBtOpCOSFNuYYEGZ5g==", "dev": true, + "license": "MIT", "dependencies": { "bin-version": "^6.0.0", "semver": "^7.5.3", @@ -518,9 +370,8 @@ }, "node_modules/bin-version/node_modules/cross-spawn": { "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -532,9 +383,8 @@ }, "node_modules/bin-version/node_modules/execa": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -555,9 +405,8 @@ }, "node_modules/bin-version/node_modules/get-stream": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -567,9 +416,8 @@ }, "node_modules/bin-version/node_modules/is-stream": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -579,9 +427,8 @@ }, "node_modules/bin-version/node_modules/npm-run-path": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.0.0" }, @@ -591,18 +438,16 @@ }, "node_modules/bin-version/node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/bin-version/node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -612,18 +457,16 @@ }, "node_modules/bin-version/node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/bin-version/node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -635,12 +478,11 @@ } }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "license": "MIT", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -648,7 +490,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -659,20 +501,18 @@ }, "node_modules/brace-expansion": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -680,26 +520,23 @@ }, "node_modules/bytes": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/cacheable-lookup": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.6.0" } }, "node_modules/cacheable-request": { "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", "dev": true, + "license": "MIT", "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", @@ -715,9 +552,8 @@ }, "node_modules/cacheable-request/node_modules/get-stream": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, + "license": "MIT", "dependencies": { "pump": "^3.0.0" }, @@ -730,8 +566,7 @@ }, "node_modules/call-bind": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -748,9 +583,8 @@ }, "node_modules/clone-response": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", "dev": true, + "license": "MIT", "dependencies": { "mimic-response": "^1.0.0" }, @@ -760,17 +594,15 @@ }, "node_modules/commander": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10" } }, "node_modules/content-disposition": { "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" }, @@ -780,30 +612,26 @@ }, "node_modules/content-type": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie-signature": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + "license": "MIT" }, "node_modules/cross-spawn": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", "dev": true, + "license": "MIT", "dependencies": { "lru-cache": "^4.0.1", "shebang-command": "^1.2.0", @@ -812,25 +640,22 @@ }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", "engines": { "node": ">= 12" } }, "node_modules/debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/decompress-response": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, + "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" }, @@ -843,9 +668,8 @@ }, "node_modules/decompress-response/node_modules/mimic-response": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -855,17 +679,15 @@ }, "node_modules/defer-to-connect": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/define-data-property": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -880,16 +702,14 @@ }, "node_modules/depd": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/destroy": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -897,30 +717,26 @@ }, "node_modules/ee-first": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + "license": "MIT" }, "node_modules/encodeurl": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/end-of-stream": { "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, + "license": "MIT", "dependencies": { "once": "^1.4.0" } }, "node_modules/es-define-property": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.4" }, @@ -930,27 +746,23 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/esbuild-plugin-resolve": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/esbuild-plugin-resolve/-/esbuild-plugin-resolve-2.0.0.tgz", - "integrity": "sha512-eJy9B8yDW5X/J48eWtR1uVmv+DKfHvYYnrrcqQoe/nUkVHVOTZlJnSevkYyGOz6hI90t036Y5QIPDrGzmppxfg==" + "license": "MIT" }, "node_modules/escape-html": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -960,17 +772,15 @@ }, "node_modules/etag": { "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/execa": { "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==", "dev": true, + "license": "MIT", "dependencies": { "cross-spawn": "^5.0.1", "get-stream": "^3.0.0", @@ -986,9 +796,8 @@ }, "node_modules/executable": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", - "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", "dev": true, + "license": "MIT", "dependencies": { "pify": "^2.2.0" }, @@ -997,16 +806,15 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -1039,9 +847,8 @@ }, "node_modules/ext-list": { "version": "2.2.2", - "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", - "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", "dev": true, + "license": "MIT", "dependencies": { "mime-db": "^1.28.0" }, @@ -1051,9 +858,8 @@ }, "node_modules/ext-name": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", - "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", "dev": true, + "license": "MIT", "dependencies": { "ext-list": "^2.0.0", "sort-keys-length": "^1.0.0" @@ -1064,9 +870,8 @@ }, "node_modules/fast-glob": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -1080,17 +885,14 @@ }, "node_modules/fastq": { "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, "node_modules/fetch-blob": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", "funding": [ { "type": "github", @@ -1101,6 +903,7 @@ "url": "https://paypal.me/jimmywarting" } ], + "license": "MIT", "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" @@ -1111,9 +914,8 @@ }, "node_modules/file-type": { "version": "17.1.6", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-17.1.6.tgz", - "integrity": "sha512-hlDw5Ev+9e883s0pwUsuuYNu4tD7GgpUnOvykjv1Gya0ZIjuKumthDRua90VUn6/nlRKAjcxLUnHNTIUWwWIiw==", "dev": true, + "license": "MIT", "dependencies": { "readable-web-to-node-stream": "^3.0.2", "strtok3": "^7.0.0-alpha.9", @@ -1128,8 +930,7 @@ }, "node_modules/filebrowser": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/filebrowser/-/filebrowser-1.0.0.tgz", - "integrity": "sha512-RRONYpCDzbmWPhBX43T4dE+ptqLznJ7lKfbMaZLChB2i2ZIdFXoqT9qZTi70Dpq6fnJHuvcdeiRqMIPZKhVgTQ==", + "license": "ISC", "dependencies": { "commander": "^2.9.0", "content-disposition": "^0.5.1", @@ -1138,14 +939,12 @@ }, "node_modules/filebrowser/node_modules/commander": { "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + "license": "MIT" }, "node_modules/filename-reserved-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz", - "integrity": "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -1155,9 +954,8 @@ }, "node_modules/filenamify": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-5.1.1.tgz", - "integrity": "sha512-M45CbrJLGACfrPOkrTp3j2EcO9OBkKUYME0eiqOCa7i2poaklU0jhlIaMlr8ijLorT0uLAzrn3qXOp5684CkfA==", "dev": true, + "license": "MIT", "dependencies": { "filename-reserved-regex": "^3.0.0", "strip-outer": "^2.0.0", @@ -1171,10 +969,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1184,8 +981,7 @@ }, "node_modules/finalhandler": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "license": "MIT", "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", @@ -1201,9 +997,8 @@ }, "node_modules/find-versions": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-5.1.0.tgz", - "integrity": "sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg==", "dev": true, + "license": "MIT", "dependencies": { "semver-regex": "^4.0.5" }, @@ -1216,8 +1011,7 @@ }, "node_modules/formdata-polyfill": { "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", "dependencies": { "fetch-blob": "^3.1.2" }, @@ -1227,32 +1021,28 @@ }, "node_modules/forwarded": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/fresh": { "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/get-intrinsic": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", @@ -1269,18 +1059,16 @@ }, "node_modules/get-stream": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -1290,8 +1078,7 @@ }, "node_modules/gopd": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "license": "MIT", "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -1301,9 +1088,8 @@ }, "node_modules/got": { "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", "dev": true, + "license": "MIT", "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", @@ -1326,8 +1112,7 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" }, @@ -1336,9 +1121,8 @@ } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -1348,8 +1132,7 @@ }, "node_modules/has-symbols": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -1358,9 +1141,8 @@ } }, "node_modules/hasown": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", - "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "version": "2.0.2", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -1370,14 +1152,12 @@ }, "node_modules/http-cache-semantics": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "dev": true + "dev": true, + "license": "BSD-2-Clause" }, "node_modules/http-errors": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", @@ -1391,9 +1171,8 @@ }, "node_modules/http2-wrapper": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", "dev": true, + "license": "MIT", "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" @@ -1404,17 +1183,15 @@ }, "node_modules/human-signals": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=10.17.0" } }, "node_modules/iconv-lite": { "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -1424,8 +1201,6 @@ }, "node_modules/ieee754": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "dev": true, "funding": [ { @@ -1440,35 +1215,32 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "BSD-3-Clause" }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "license": "ISC" }, "node_modules/ipaddr.js": { "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", "engines": { "node": ">= 0.10" } }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -1478,41 +1250,36 @@ }, "node_modules/is-number": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } }, "node_modules/is-plain-obj": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/is-stream": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/isomorphic-fetch": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", - "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "license": "MIT", "dependencies": { "node-fetch": "^2.6.1", "whatwg-fetch": "^3.4.1" @@ -1520,8 +1287,7 @@ }, "node_modules/isomorphic-fetch/node_modules/node-fetch": { "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -1539,33 +1305,34 @@ }, "node_modules/json-buffer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/keyv": { "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lowercase-keys": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/lru-cache": { "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", "dev": true, + "license": "ISC", "dependencies": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" @@ -1573,47 +1340,41 @@ }, "node_modules/media-typer": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/merge-descriptors": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "license": "MIT" }, "node_modules/merge-stream": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/methods": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.7", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -1622,8 +1383,7 @@ }, "node_modules/mime": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", "bin": { "mime": "cli.js" }, @@ -1633,16 +1393,14 @@ }, "node_modules/mime-db": { "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -1652,27 +1410,24 @@ }, "node_modules/mimic-fn": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/mimic-response": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.4", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -1685,21 +1440,17 @@ }, "node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "license": "MIT" }, "node_modules/negotiator": { "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/node-domexception": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", "funding": [ { "type": "github", @@ -1710,14 +1461,14 @@ "url": "https://paypal.me/jimmywarting" } ], + "license": "MIT", "engines": { "node": ">=10.5.0" } }, "node_modules/node-fetch": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", @@ -1733,9 +1484,8 @@ }, "node_modules/normalize-url": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -1745,9 +1495,8 @@ }, "node_modules/npm-run-path": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^2.0.0" }, @@ -1757,16 +1506,14 @@ }, "node_modules/object-inspect": { "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/on-finished": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, @@ -1776,18 +1523,16 @@ }, "node_modules/once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, + "license": "ISC", "dependencies": { "wrappy": "1" } }, "node_modules/onetime": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, + "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" }, @@ -1800,9 +1545,8 @@ }, "node_modules/os-filter-obj": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/os-filter-obj/-/os-filter-obj-2.0.0.tgz", - "integrity": "sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg==", "dev": true, + "license": "MIT", "dependencies": { "arch": "^2.1.0" }, @@ -1812,49 +1556,43 @@ }, "node_modules/p-cancelable": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/p-finally": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/parseurl": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/path-key": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/path-to-regexp": { "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "license": "MIT" }, "node_modules/peek-readable": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", - "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.16" }, @@ -1865,9 +1603,8 @@ }, "node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -1877,18 +1614,16 @@ }, "node_modules/pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "version": "3.3.2", "dev": true, + "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, @@ -1901,8 +1636,7 @@ }, "node_modules/proxy-addr": { "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -1913,15 +1647,13 @@ }, "node_modules/pseudomap": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/pump": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, + "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -1929,8 +1661,7 @@ }, "node_modules/qs": { "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.0.4" }, @@ -1943,120 +1674,6 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readable-web-to-node-stream": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", - "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", - "dev": true, - "dependencies": { - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true - }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "dev": true, - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { @@ -2072,14 +1689,95 @@ "url": "https://feross.org/support" } ], - "dependencies": { - "queue-microtask": "^1.2.2" + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "node_modules/range-parser": { + "version": "1.2.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "dev": true, "funding": [ { "type": "github", @@ -2093,21 +1791,38 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" }, "node_modules/safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "license": "MIT" }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.2", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -2117,9 +1832,8 @@ }, "node_modules/semver-regex": { "version": "4.0.5", - "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-4.0.5.tgz", - "integrity": "sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -2129,9 +1843,8 @@ }, "node_modules/semver-truncate": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/semver-truncate/-/semver-truncate-3.0.0.tgz", - "integrity": "sha512-LJWA9kSvMolR51oDE6PN3kALBNaUdkxzAGcexw8gjMA8xr5zUqK0JiR3CgARSqanYF3Z1YHvsErb1KDgh+v7Rg==", "dev": true, + "license": "MIT", "dependencies": { "semver": "^7.3.5" }, @@ -2142,28 +1855,9 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/send": { "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -2185,13 +1879,11 @@ }, "node_modules/send/node_modules/ms": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "license": "MIT" }, "node_modules/serve-static": { "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "license": "MIT", "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", @@ -2203,16 +1895,15 @@ } }, "node_modules/set-function-length": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", - "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "version": "1.2.2", + "license": "MIT", "dependencies": { - "define-data-property": "^1.1.2", + "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.3", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -2220,14 +1911,12 @@ }, "node_modules/setprototypeof": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + "license": "ISC" }, "node_modules/shebang-command": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^1.0.0" }, @@ -2237,19 +1926,17 @@ }, "node_modules/shebang-regex": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/side-channel": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz", - "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==", + "version": "1.0.6", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bind": "^1.0.7", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.4", "object-inspect": "^1.13.1" @@ -2263,24 +1950,21 @@ }, "node_modules/signal-exit": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/sort-keys": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", - "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", "dev": true, + "license": "MIT", "dependencies": { "is-plain-obj": "^1.0.0" }, @@ -2290,9 +1974,8 @@ }, "node_modules/sort-keys-length": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", - "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==", "dev": true, + "license": "MIT", "dependencies": { "sort-keys": "^1.0.0" }, @@ -2302,53 +1985,47 @@ }, "node_modules/source-map": { "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">= 8" } }, "node_modules/statuses": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/string_decoder": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } }, "node_modules/strip-eof": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/strip-final-newline": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/strip-outer": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-2.0.0.tgz", - "integrity": "sha512-A21Xsm1XzUkK0qK1ZrytDUvqsQWict2Cykhvi0fBQntGG5JSprESasEyV1EZ/4CiR5WB5KjzLTrP/bO37B0wPg==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -2358,9 +2035,8 @@ }, "node_modules/strtok3": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", - "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==", "dev": true, + "license": "MIT", "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^5.0.0" @@ -2375,9 +2051,8 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -2387,17 +2062,15 @@ }, "node_modules/toidentifier": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", "engines": { "node": ">=0.6" } }, "node_modules/token-types": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", - "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==", "dev": true, + "license": "MIT", "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" @@ -2412,14 +2085,12 @@ }, "node_modules/tr46": { "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + "license": "MIT" }, "node_modules/trim-repeated": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-2.0.0.tgz", - "integrity": "sha512-QUHBFTJGdOwmp0tbOG505xAgOp/YliZP/6UgafFXYZ26WT1bvQmSMJUvkeVSASuJJHbqsFbynTvkd5W8RBTipg==", "dev": true, + "license": "MIT", "dependencies": { "escape-string-regexp": "^5.0.0" }, @@ -2429,18 +2100,15 @@ }, "node_modules/ts-matches": { "version": "5.5.1", - "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-5.5.1.tgz", - "integrity": "sha512-UFYaKgfqlg9FROK7bdpYqFwG1CJvP4kOJdjXuWoqxo9jCmANoDw1GxkSCpJgoTeIiSTaTH5Qr1klSspb8c+ydg==" + "license": "MIT" }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.6.3", + "license": "0BSD" }, "node_modules/type-is": { "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -2450,10 +2118,9 @@ } }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.4.5", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2464,62 +2131,53 @@ }, "node_modules/undici-types": { "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/unpipe": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/utils-merge": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", "engines": { "node": ">= 0.4.0" } }, "node_modules/vary": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/web-streams-polyfill": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.2.tgz", - "integrity": "sha512-3pRGuxRF5gpuZc0W+EpwQRmCD7gRqcDOMt688KmdlDAgAyaB1XlN0zq2njfDNm44XVdIouE7pZ6GzbdyH47uIQ==", + "version": "3.3.3", + "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/webidl-conversions": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + "license": "BSD-2-Clause" }, "node_modules/whatwg-fetch": { "version": "3.6.20", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==" + "license": "MIT" }, "node_modules/whatwg-url": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -2527,9 +2185,8 @@ }, "node_modules/which": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -2539,20 +2196,20 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/yallist": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "version": "2.4.5", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, "engines": { "node": ">= 14" } @@ -2560,14 +2217,10 @@ }, "dependencies": { "@iarna/toml": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", - "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==" + "version": "2.2.5" }, "@mole-inc/bin-wrapper": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@mole-inc/bin-wrapper/-/bin-wrapper-8.0.1.tgz", - "integrity": "sha512-sTGoeZnjI8N4KS+sW2AN95gDBErhAguvkw/tWdCjeM8bvxpz5lqrnd0vOJABA1A+Ic3zED7PYoLP/RANLgVotA==", "dev": true, "requires": { "bin-check": "^4.1.0", @@ -2582,8 +2235,6 @@ }, "@nodelib/fs.scandir": { "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "requires": { "@nodelib/fs.stat": "2.0.5", @@ -2592,14 +2243,10 @@ }, "@nodelib/fs.stat": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true }, "@nodelib/fs.walk": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "requires": { "@nodelib/fs.scandir": "2.1.5", @@ -2608,8 +2255,6 @@ }, "@sindresorhus/is": { "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", "dev": true }, "@start9labs/start-sdk": { @@ -2617,8 +2262,10 @@ "requires": { "@iarna/toml": "^2.2.5", "@types/jest": "^29.4.0", + "@types/lodash": "^4.17.5", "isomorphic-fetch": "^3.0.0", "jest": "^29.4.3", + "lodash": "^4.17.21", "prettier": "^3.2.5", "ts-jest": "^29.0.5", "ts-matches": "^5.4.1", @@ -2630,8 +2277,6 @@ }, "@swc/cli": { "version": "0.1.65", - "resolved": "https://registry.npmjs.org/@swc/cli/-/cli-0.1.65.tgz", - "integrity": "sha512-4NcgsvJVHhA7trDnMmkGLLvWMHu2kSy+qHx6QwRhhJhdiYdNUrhdp+ERxen73sYtaeEOYeLJcWrQ60nzKi6rpg==", "dev": true, "requires": { "@mole-inc/bin-wrapper": "^8.0.1", @@ -2644,111 +2289,46 @@ } }, "@swc/core": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.4.1.tgz", - "integrity": "sha512-3y+Y8js+e7BbM16iND+6Rcs3jdiL28q3iVtYsCviYSSpP2uUVKkp5sJnCY4pg8AaVvyN7CGQHO7gLEZQ5ByozQ==", + "version": "1.5.28", "dev": true, "requires": { - "@swc/core-darwin-arm64": "1.4.1", - "@swc/core-darwin-x64": "1.4.1", - "@swc/core-linux-arm-gnueabihf": "1.4.1", - "@swc/core-linux-arm64-gnu": "1.4.1", - "@swc/core-linux-arm64-musl": "1.4.1", - "@swc/core-linux-x64-gnu": "1.4.1", - "@swc/core-linux-x64-musl": "1.4.1", - "@swc/core-win32-arm64-msvc": "1.4.1", - "@swc/core-win32-ia32-msvc": "1.4.1", - "@swc/core-win32-x64-msvc": "1.4.1", - "@swc/counter": "^0.1.2", - "@swc/types": "^0.1.5" + "@swc/core-darwin-arm64": "1.5.28", + "@swc/core-darwin-x64": "1.5.28", + "@swc/core-linux-arm-gnueabihf": "1.5.28", + "@swc/core-linux-arm64-gnu": "1.5.28", + "@swc/core-linux-arm64-musl": "1.5.28", + "@swc/core-linux-x64-gnu": "1.5.28", + "@swc/core-linux-x64-musl": "1.5.28", + "@swc/core-win32-arm64-msvc": "1.5.28", + "@swc/core-win32-ia32-msvc": "1.5.28", + "@swc/core-win32-x64-msvc": "1.5.28", + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.8" } }, - "@swc/core-darwin-arm64": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.4.1.tgz", - "integrity": "sha512-ePyfx0348UbR4DOAW24TedeJbafnzha8liXFGuQ4bdXtEVXhLfPngprrxKrAddCuv42F9aTxydlF6+adD3FBhA==", - "dev": true, - "optional": true - }, - "@swc/core-darwin-x64": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.4.1.tgz", - "integrity": "sha512-eLf4JSe6VkCMdDowjM8XNC5rO+BrgfbluEzAVtKR8L2HacNYukieumN7EzpYCi0uF1BYwu1ku6tLyG2r0VcGxA==", - "dev": true, - "optional": true - }, - "@swc/core-linux-arm-gnueabihf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.4.1.tgz", - "integrity": "sha512-K8VtTLWMw+rkN/jDC9o/Q9SMmzdiHwYo2CfgkwVT29NsGccwmNhCQx6XoYiPKyKGIFKt4tdQnJHKUFzxUqQVtQ==", - "dev": true, - "optional": true - }, - "@swc/core-linux-arm64-gnu": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.4.1.tgz", - "integrity": "sha512-0e8p4g0Bfkt8lkiWgcdiENH3RzkcqKtpRXIVNGOmVc0OBkvc2tpm2WTx/eoCnes2HpTT4CTtR3Zljj4knQ4Fvw==", - "dev": true, - "optional": true - }, - "@swc/core-linux-arm64-musl": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.4.1.tgz", - "integrity": "sha512-b/vWGQo2n7lZVUnSQ7NBq3Qrj85GrAPPiRbpqaIGwOytiFSk8VULFihbEUwDe0rXgY4LDm8z8wkgADZcLnmdUA==", - "dev": true, - "optional": true - }, "@swc/core-linux-x64-gnu": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.4.1.tgz", - "integrity": "sha512-AFMQlvkKEdNi1Vk2GFTxxJzbICttBsOQaXa98kFTeWTnFFIyiIj2w7Sk8XRTEJ/AjF8ia8JPKb1zddBWr9+bEQ==", + "version": "1.5.28", "dev": true, "optional": true }, "@swc/core-linux-x64-musl": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.4.1.tgz", - "integrity": "sha512-QX2MxIECX1gfvUVZY+jk528/oFkS9MAl76e3ZRvG2KC/aKlCQL0KSzcTSm13mOxkDKS30EaGRDRQWNukGpMeRg==", - "dev": true, - "optional": true - }, - "@swc/core-win32-arm64-msvc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.4.1.tgz", - "integrity": "sha512-OklkJYXXI/tntD2zaY8i3iZldpyDw5q+NAP3k9OlQ7wXXf37djRsHLV0NW4+ZNHBjE9xp2RsXJ0jlOJhfgGoFA==", - "dev": true, - "optional": true - }, - "@swc/core-win32-ia32-msvc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.4.1.tgz", - "integrity": "sha512-MBuc3/QfKX9FnLOU7iGN+6yHRTQaPQ9WskiC8s8JFiKQ+7I2p25tay2RplR9dIEEGgVAu6L7auv96LbNTh+FaA==", - "dev": true, - "optional": true - }, - "@swc/core-win32-x64-msvc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.4.1.tgz", - "integrity": "sha512-lu4h4wFBb/bOK6N2MuZwg7TrEpwYXgpQf5R7ObNSXL65BwZ9BG8XRzD+dLJmALu8l5N08rP/TrpoKRoGT4WSxw==", + "version": "1.5.28", "dev": true, "optional": true }, "@swc/counter": { "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", "dev": true }, "@swc/types": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", - "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", - "dev": true + "version": "0.1.8", + "dev": true, + "requires": { + "@swc/counter": "^0.1.3" + } }, "@szmarczak/http-timer": { "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", "dev": true, "requires": { "defer-to-connect": "^2.0.0" @@ -2756,14 +2336,10 @@ }, "@tokenizer/token": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", "dev": true }, "@types/cacheable-request": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", "dev": true, "requires": { "@types/http-cache-semantics": "*", @@ -2774,23 +2350,17 @@ }, "@types/http-cache-semantics": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", "dev": true }, "@types/keyv": { "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", "dev": true, "requires": { "@types/node": "*" } }, "@types/node": { - "version": "20.11.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", - "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", + "version": "20.14.2", "dev": true, "requires": { "undici-types": "~5.26.4" @@ -2798,8 +2368,6 @@ }, "@types/responselike": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", "dev": true, "requires": { "@types/node": "*" @@ -2807,8 +2375,6 @@ }, "accepts": { "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "requires": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -2816,25 +2382,17 @@ }, "arch": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", - "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", "dev": true }, "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + "version": "1.1.1" }, "balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, "bin-check": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bin-check/-/bin-check-4.1.0.tgz", - "integrity": "sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==", "dev": true, "requires": { "execa": "^0.7.0", @@ -2843,8 +2401,6 @@ }, "bin-version": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/bin-version/-/bin-version-6.0.0.tgz", - "integrity": "sha512-nk5wEsP4RiKjG+vF+uG8lFsEn4d7Y6FVDamzzftSunXOoOcOOkzcWdKVlGgFFwlUQCj63SgnUkLLGF8v7lufhw==", "dev": true, "requires": { "execa": "^5.0.0", @@ -2853,8 +2409,6 @@ "dependencies": { "cross-spawn": { "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "requires": { "path-key": "^3.1.0", @@ -2864,8 +2418,6 @@ }, "execa": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, "requires": { "cross-spawn": "^7.0.3", @@ -2881,20 +2433,14 @@ }, "get-stream": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true }, "is-stream": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true }, "npm-run-path": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, "requires": { "path-key": "^3.0.0" @@ -2902,14 +2448,10 @@ }, "path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, "shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "requires": { "shebang-regex": "^3.0.0" @@ -2917,14 +2459,10 @@ }, "shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, "which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "requires": { "isexe": "^2.0.0" @@ -2934,8 +2472,6 @@ }, "bin-version-check": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bin-version-check/-/bin-version-check-5.1.0.tgz", - "integrity": "sha512-bYsvMqJ8yNGILLz1KP9zKLzQ6YpljV3ln1gqhuLkUtyfGi3qXKGuK2p+U4NAvjVFzDFiBBtOpCOSFNuYYEGZ5g==", "dev": true, "requires": { "bin-version": "^6.0.0", @@ -2944,12 +2480,10 @@ } }, "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", "requires": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -2957,44 +2491,34 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "brace-expansion": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "requires": { "balanced-match": "^1.0.0" } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + "version": "3.1.2" }, "cacheable-lookup": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", "dev": true }, "cacheable-request": { "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", "dev": true, "requires": { "clone-response": "^1.0.2", @@ -3008,8 +2532,6 @@ "dependencies": { "get-stream": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, "requires": { "pump": "^3.0.0" @@ -3019,8 +2541,6 @@ }, "call-bind": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "requires": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -3031,8 +2551,6 @@ }, "clone-response": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", "dev": true, "requires": { "mimic-response": "^1.0.0" @@ -3040,37 +2558,25 @@ }, "commander": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "dev": true }, "content-disposition": { "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "requires": { "safe-buffer": "5.2.1" } }, "content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + "version": "1.0.5" }, "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + "version": "0.6.0" }, "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + "version": "1.0.6" }, "cross-spawn": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", "dev": true, "requires": { "lru-cache": "^4.0.1", @@ -3079,22 +2585,16 @@ } }, "data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==" + "version": "4.0.1" }, "debug": { "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "requires": { "ms": "2.0.0" } }, "decompress-response": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, "requires": { "mimic-response": "^3.1.0" @@ -3102,22 +2602,16 @@ "dependencies": { "mimic-response": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true } } }, "defer-to-connect": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", "dev": true }, "define-data-property": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "requires": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -3125,29 +2619,19 @@ } }, "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + "version": "2.0.0" }, "destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + "version": "1.2.0" }, "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + "version": "1.1.1" }, "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + "version": "1.0.2" }, "end-of-stream": { "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, "requires": { "once": "^1.4.0" @@ -3155,42 +2639,28 @@ }, "es-define-property": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", "requires": { "get-intrinsic": "^1.2.4" } }, "es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + "version": "1.3.0" }, "esbuild-plugin-resolve": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/esbuild-plugin-resolve/-/esbuild-plugin-resolve-2.0.0.tgz", - "integrity": "sha512-eJy9B8yDW5X/J48eWtR1uVmv+DKfHvYYnrrcqQoe/nUkVHVOTZlJnSevkYyGOz6hI90t036Y5QIPDrGzmppxfg==" + "version": "2.0.0" }, "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + "version": "1.0.3" }, "escape-string-regexp": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "dev": true }, "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + "version": "1.8.1" }, "execa": { "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==", "dev": true, "requires": { "cross-spawn": "^5.0.1", @@ -3204,24 +2674,20 @@ }, "executable": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", - "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", "dev": true, "requires": { "pify": "^2.2.0" } }, "express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -3251,8 +2717,6 @@ }, "ext-list": { "version": "2.2.2", - "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", - "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", "dev": true, "requires": { "mime-db": "^1.28.0" @@ -3260,8 +2724,6 @@ }, "ext-name": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", - "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", "dev": true, "requires": { "ext-list": "^2.0.0", @@ -3270,8 +2732,6 @@ }, "fast-glob": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", @@ -3283,8 +2743,6 @@ }, "fastq": { "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, "requires": { "reusify": "^1.0.4" @@ -3292,8 +2750,6 @@ }, "fetch-blob": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", "requires": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" @@ -3301,8 +2757,6 @@ }, "file-type": { "version": "17.1.6", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-17.1.6.tgz", - "integrity": "sha512-hlDw5Ev+9e883s0pwUsuuYNu4tD7GgpUnOvykjv1Gya0ZIjuKumthDRua90VUn6/nlRKAjcxLUnHNTIUWwWIiw==", "dev": true, "requires": { "readable-web-to-node-stream": "^3.0.2", @@ -3312,8 +2766,6 @@ }, "filebrowser": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/filebrowser/-/filebrowser-1.0.0.tgz", - "integrity": "sha512-RRONYpCDzbmWPhBX43T4dE+ptqLznJ7lKfbMaZLChB2i2ZIdFXoqT9qZTi70Dpq6fnJHuvcdeiRqMIPZKhVgTQ==", "requires": { "commander": "^2.9.0", "content-disposition": "^0.5.1", @@ -3321,22 +2773,16 @@ }, "dependencies": { "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + "version": "2.20.3" } } }, "filename-reserved-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz", - "integrity": "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==", "dev": true }, "filenamify": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-5.1.1.tgz", - "integrity": "sha512-M45CbrJLGACfrPOkrTp3j2EcO9OBkKUYME0eiqOCa7i2poaklU0jhlIaMlr8ijLorT0uLAzrn3qXOp5684CkfA==", "dev": true, "requires": { "filename-reserved-regex": "^3.0.0", @@ -3345,9 +2791,7 @@ } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", "dev": true, "requires": { "to-regex-range": "^5.0.1" @@ -3355,8 +2799,6 @@ }, "finalhandler": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", "requires": { "debug": "2.6.9", "encodeurl": "~1.0.2", @@ -3369,8 +2811,6 @@ }, "find-versions": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-5.1.0.tgz", - "integrity": "sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg==", "dev": true, "requires": { "semver-regex": "^4.0.5" @@ -3378,31 +2818,21 @@ }, "formdata-polyfill": { "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", "requires": { "fetch-blob": "^3.1.2" } }, "forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + "version": "0.2.0" }, "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + "version": "0.5.2" }, "function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + "version": "1.1.2" }, "get-intrinsic": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "requires": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", @@ -3413,14 +2843,10 @@ }, "get-stream": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", "dev": true }, "glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "requires": { "is-glob": "^4.0.1" @@ -3428,16 +2854,12 @@ }, "gopd": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", "requires": { "get-intrinsic": "^1.1.3" } }, "got": { "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", "dev": true, "requires": { "@sindresorhus/is": "^4.0.0", @@ -3455,40 +2877,28 @@ }, "has-property-descriptors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "requires": { "es-define-property": "^1.0.0" } }, "has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" + "version": "1.0.3" }, "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + "version": "1.0.3" }, "hasown": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", - "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "version": "2.0.2", "requires": { "function-bind": "^1.1.2" } }, "http-cache-semantics": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, "http-errors": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "requires": { "depd": "2.0.0", "inherits": "2.0.4", @@ -3499,8 +2909,6 @@ }, "http2-wrapper": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", "dev": true, "requires": { "quick-lru": "^5.1.1", @@ -3509,44 +2917,30 @@ }, "human-signals": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, "iconv-lite": { "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "requires": { "safer-buffer": ">= 2.1.2 < 3" } }, "ieee754": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "dev": true }, "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "version": "2.0.4" }, "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + "version": "1.9.1" }, "is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true }, "is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "requires": { "is-extglob": "^2.1.1" @@ -3554,32 +2948,22 @@ }, "is-number": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, "is-plain-obj": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "dev": true }, "is-stream": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", "dev": true }, "isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, "isomorphic-fetch": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", - "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", "requires": { "node-fetch": "^2.6.1", "whatwg-fetch": "^3.4.1" @@ -3587,8 +2971,6 @@ "dependencies": { "node-fetch": { "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "requires": { "whatwg-url": "^5.0.0" } @@ -3597,29 +2979,26 @@ }, "json-buffer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, "keyv": { "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "requires": { "json-buffer": "3.0.1" } }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "lowercase-keys": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", "dev": true }, "lru-cache": { "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", "dev": true, "requires": { "pseudomap": "^1.0.2", @@ -3627,100 +3006,68 @@ } }, "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + "version": "0.3.0" }, "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.1" }, "merge-stream": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, "merge2": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true }, "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + "version": "1.1.2" }, "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.7", "dev": true, "requires": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + "version": "1.6.0" }, "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + "version": "1.52.0" }, "mime-types": { "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "requires": { "mime-db": "1.52.0" } }, "mimic-fn": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, "mimic-response": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", "dev": true }, "minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.4", "dev": true, "requires": { "brace-expansion": "^2.0.1" } }, "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "version": "2.0.0" }, "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + "version": "0.6.3" }, "node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" + "version": "1.0.0" }, "node-fetch": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "requires": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", @@ -3729,36 +3076,26 @@ }, "normalize-url": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", "dev": true }, "npm-run-path": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", "dev": true, "requires": { "path-key": "^2.0.0" } }, "object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" + "version": "1.13.1" }, "on-finished": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "requires": { "ee-first": "1.1.1" } }, "once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "requires": { "wrappy": "1" @@ -3766,8 +3103,6 @@ }, "onetime": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, "requires": { "mimic-fn": "^2.1.0" @@ -3775,8 +3110,6 @@ }, "os-filter-obj": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/os-filter-obj/-/os-filter-obj-2.0.0.tgz", - "integrity": "sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg==", "dev": true, "requires": { "arch": "^2.1.0" @@ -3784,60 +3117,40 @@ }, "p-cancelable": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", "dev": true }, "p-finally": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", "dev": true }, "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + "version": "1.3.3" }, "path-key": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", "dev": true }, "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.7" }, "peek-readable": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", - "integrity": "sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A==", "dev": true }, "picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, "pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true }, "prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "version": "3.3.2", "dev": true }, "proxy-addr": { "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "requires": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -3845,14 +3158,10 @@ }, "pseudomap": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", "dev": true }, "pump": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, "requires": { "end-of-stream": "^1.1.0", @@ -3861,33 +3170,23 @@ }, "qs": { "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "requires": { "side-channel": "^1.0.4" } }, "queue-microtask": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, "quick-lru": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "dev": true }, "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + "version": "1.2.1" }, "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", "requires": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -3897,8 +3196,6 @@ }, "readable-stream": { "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "requires": { "inherits": "^2.0.3", @@ -3908,8 +3205,6 @@ }, "readable-web-to-node-stream": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", - "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", "dev": true, "requires": { "readable-stream": "^3.6.0" @@ -3917,14 +3212,10 @@ }, "resolve-alpn": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", "dev": true }, "responselike": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", "dev": true, "requires": { "lowercase-keys": "^2.0.0" @@ -3932,65 +3223,31 @@ }, "reusify": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true }, "run-parallel": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "requires": { "queue-microtask": "^1.2.2" } }, "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + "version": "5.2.1" }, "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "version": "2.1.2" }, "semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } + "version": "7.6.2", + "dev": true }, "semver-regex": { "version": "4.0.5", - "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-4.0.5.tgz", - "integrity": "sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==", "dev": true }, "semver-truncate": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/semver-truncate/-/semver-truncate-3.0.0.tgz", - "integrity": "sha512-LJWA9kSvMolR51oDE6PN3kALBNaUdkxzAGcexw8gjMA8xr5zUqK0JiR3CgARSqanYF3Z1YHvsErb1KDgh+v7Rg==", "dev": true, "requires": { "semver": "^7.3.5" @@ -3998,8 +3255,6 @@ }, "send": { "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", "requires": { "debug": "2.6.9", "depd": "2.0.0", @@ -4017,16 +3272,12 @@ }, "dependencies": { "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "version": "2.1.3" } } }, "serve-static": { "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", "requires": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", @@ -4035,27 +3286,21 @@ } }, "set-function-length": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", - "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "version": "1.2.2", "requires": { - "define-data-property": "^1.1.2", + "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.3", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" } }, "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + "version": "1.2.0" }, "shebang-command": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", "dev": true, "requires": { "shebang-regex": "^1.0.0" @@ -4063,16 +3308,12 @@ }, "shebang-regex": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", "dev": true }, "side-channel": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz", - "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==", + "version": "1.0.6", "requires": { - "call-bind": "^1.0.6", + "call-bind": "^1.0.7", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.4", "object-inspect": "^1.13.1" @@ -4080,20 +3321,14 @@ }, "signal-exit": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, "slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, "sort-keys": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", - "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", "dev": true, "requires": { "is-plain-obj": "^1.0.0" @@ -4101,8 +3336,6 @@ }, "sort-keys-length": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", - "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==", "dev": true, "requires": { "sort-keys": "^1.0.0" @@ -4110,19 +3343,13 @@ }, "source-map": { "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true }, "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + "version": "2.0.1" }, "string_decoder": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, "requires": { "safe-buffer": "~5.2.0" @@ -4130,26 +3357,18 @@ }, "strip-eof": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", "dev": true }, "strip-final-newline": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true }, "strip-outer": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-2.0.0.tgz", - "integrity": "sha512-A21Xsm1XzUkK0qK1ZrytDUvqsQWict2Cykhvi0fBQntGG5JSprESasEyV1EZ/4CiR5WB5KjzLTrP/bO37B0wPg==", "dev": true }, "strtok3": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.0.0.tgz", - "integrity": "sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ==", "dev": true, "requires": { "@tokenizer/token": "^0.3.0", @@ -4158,22 +3377,16 @@ }, "to-regex-range": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "requires": { "is-number": "^7.0.0" } }, "toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + "version": "1.0.1" }, "token-types": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", - "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==", "dev": true, "requires": { "@tokenizer/token": "^0.3.0", @@ -4181,90 +3394,60 @@ } }, "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + "version": "0.0.3" }, "trim-repeated": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-2.0.0.tgz", - "integrity": "sha512-QUHBFTJGdOwmp0tbOG505xAgOp/YliZP/6UgafFXYZ26WT1bvQmSMJUvkeVSASuJJHbqsFbynTvkd5W8RBTipg==", "dev": true, "requires": { "escape-string-regexp": "^5.0.0" } }, "ts-matches": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-5.5.1.tgz", - "integrity": "sha512-UFYaKgfqlg9FROK7bdpYqFwG1CJvP4kOJdjXuWoqxo9jCmANoDw1GxkSCpJgoTeIiSTaTH5Qr1klSspb8c+ydg==" + "version": "5.5.1" }, "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.6.3" }, "type-is": { "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "requires": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.4.5", "dev": true }, "undici-types": { "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + "version": "1.0.0" }, "util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + "version": "1.0.1" }, "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + "version": "1.1.2" }, "web-streams-polyfill": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.2.tgz", - "integrity": "sha512-3pRGuxRF5gpuZc0W+EpwQRmCD7gRqcDOMt688KmdlDAgAyaB1XlN0zq2njfDNm44XVdIouE7pZ6GzbdyH47uIQ==" + "version": "3.3.3" }, "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + "version": "3.0.1" }, "whatwg-fetch": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==" + "version": "3.6.20" }, "whatwg-url": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "requires": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -4272,8 +3455,6 @@ }, "which": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, "requires": { "isexe": "^2.0.0" @@ -4281,20 +3462,14 @@ }, "wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, "yallist": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", "dev": true }, "yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==" + "version": "2.4.5" } } } diff --git a/container-runtime/package.json b/container-runtime/package.json index e2c56afff..357c606fc 100644 --- a/container-runtime/package.json +++ b/container-runtime/package.json @@ -21,6 +21,7 @@ "esbuild-plugin-resolve": "^2.0.0", "filebrowser": "^1.0.0", "isomorphic-fetch": "^3.0.0", + "lodash": "^4.17.21", "node-fetch": "^3.1.0", "ts-matches": "^5.5.1", "tslib": "^2.5.3", diff --git a/container-runtime/src/Adapters/HostSystemStartOs.ts b/container-runtime/src/Adapters/HostSystemStartOs.ts index ba2076f52..93905825a 100644 --- a/container-runtime/src/Adapters/HostSystemStartOs.ts +++ b/container-runtime/src/Adapters/HostSystemStartOs.ts @@ -32,6 +32,8 @@ type RpcError = typeof matchRpcError._TYPE const SOCKET_PATH = "/media/startos/rpc/host.sock" const MAIN = "/main" as const export class HostSystemStartOs implements Effects { + procedureId: string | null = null + static of(callbackHolder: CallbackHolder) { return new HostSystemStartOs(callbackHolder) } @@ -40,7 +42,7 @@ export class HostSystemStartOs implements Effects { id = 0 rpcRound( method: K, - params: unknown, + params: Record, ) { const id = this.id++ const client = net.createConnection({ path: SOCKET_PATH }, () => { @@ -48,7 +50,7 @@ export class HostSystemStartOs implements Effects { JSON.stringify({ id, method, - params, + params: { ...params, procedureId: this.procedureId }, }) + "\n", ) }) @@ -102,14 +104,14 @@ export class HostSystemStartOs implements Effects { }) as ReturnType } clearBindings(...[]: Parameters) { - return this.rpcRound("clearBindings", null) as ReturnType< + return this.rpcRound("clearBindings", {}) as ReturnType< T.Effects["clearBindings"] > } clearServiceInterfaces( ...[]: Parameters ) { - return this.rpcRound("clearServiceInterfaces", null) as ReturnType< + return this.rpcRound("clearServiceInterfaces", {}) as ReturnType< T.Effects["clearServiceInterfaces"] > } @@ -145,18 +147,20 @@ export class HostSystemStartOs implements Effects { T.Effects["exportServiceInterface"] > } - exposeForDependents(...[options]: any) { - return this.rpcRound("exposeForDependents", null) as ReturnType< + exposeForDependents( + ...[options]: Parameters + ) { + return this.rpcRound("exposeForDependents", options) as ReturnType< T.Effects["exposeForDependents"] > } getConfigured(...[]: Parameters) { - return this.rpcRound("getConfigured", null) as ReturnType< + return this.rpcRound("getConfigured", {}) as ReturnType< T.Effects["getConfigured"] > } getContainerIp(...[]: Parameters) { - return this.rpcRound("getContainerIp", null) as ReturnType< + return this.rpcRound("getContainerIp", {}) as ReturnType< T.Effects["getContainerIp"] > } @@ -229,7 +233,7 @@ export class HostSystemStartOs implements Effects { > } restart(...[]: Parameters) { - return this.rpcRound("restart", null) + return this.rpcRound("restart", {}) as ReturnType } running(...[packageId]: Parameters) { return this.rpcRound("running", { packageId }) as ReturnType< @@ -262,7 +266,7 @@ export class HostSystemStartOs implements Effects { > } getDependencies(): ReturnType { - return this.rpcRound("getDependencies", null) as ReturnType< + return this.rpcRound("getDependencies", {}) as ReturnType< T.Effects["getDependencies"] > } @@ -279,7 +283,7 @@ export class HostSystemStartOs implements Effects { } shutdown(...[]: Parameters) { - return this.rpcRound("shutdown", null) + return this.rpcRound("shutdown", {}) as ReturnType } stopped(...[packageId]: Parameters) { return this.rpcRound("stopped", { packageId }) as ReturnType< diff --git a/container-runtime/src/Adapters/RpcListener.ts b/container-runtime/src/Adapters/RpcListener.ts index faff253fe..5391d943e 100644 --- a/container-runtime/src/Adapters/RpcListener.ts +++ b/container-runtime/src/Adapters/RpcListener.ts @@ -58,6 +58,7 @@ const runType = object({ method: literal("execute"), params: object( { + id: string, procedure: string, input: any, timeout: number, @@ -70,6 +71,7 @@ const sandboxRunType = object({ method: literal("sandbox"), params: object( { + id: string, procedure: string, input: any, timeout: number, @@ -195,6 +197,7 @@ export class RpcListener { const procedure = jsonPath.unsafeCast(params.procedure) return system .execute(this.effects, { + id: params.id, procedure, input: params.input, timeout: params.timeout, diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index 0bdb0d769..731b38903 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -49,7 +49,7 @@ function todo(): never { const execFile = promisify(childProcess.execFile) const MANIFEST_LOCATION = "/usr/lib/startos/package/embassyManifest.json" -const EMBASSY_JS_LOCATION = "/usr/lib/startos/package/embassy.js" +export const EMBASSY_JS_LOCATION = "/usr/lib/startos/package/embassy.js" const EMBASSY_POINTER_PATH_PREFIX = "/embassyConfig" const matchSetResult = object( @@ -199,11 +199,14 @@ export class SystemForEmbassy implements System { async execute( effects: HostSystemStartOs, options: { + id: string procedure: JsonPath input: unknown timeout?: number | undefined }, ): Promise { + effects = Object.create(effects) + effects.procedureId = options.id return this._execute(effects, options) .then((x) => matches(x) @@ -724,7 +727,7 @@ export class SystemForEmbassy implements System { private async properties( effects: HostSystemStartOs, timeoutMs: number | null, - ): Promise> { + ): Promise> { // TODO BLU-J set the properties ever so often const setConfigValue = this.manifest.properties if (!setConfigValue) throw new Error("There is no properties") diff --git a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts index 7549bf0f2..bf27f222d 100644 --- a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts +++ b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts @@ -1,20 +1,23 @@ import { ExecuteResult, System } from "../../Interfaces/System" import { unNestPath } from "../../Models/JsonPath" -import { string } from "ts-matches" +import matches, { any, number, object, string, tuple } from "ts-matches" import { HostSystemStartOs } from "../HostSystemStartOs" import { Effects } from "../../Models/Effects" -import { RpcResult } from "../RpcListener" +import { RpcResult, matchRpcResult } from "../RpcListener" import { duration } from "../../Models/Duration" -const LOCATION = "/usr/lib/startos/package/startos" +import { T } from "@start9labs/start-sdk" +import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk" +export const STARTOS_JS_LOCATION = "/usr/lib/startos/package/index.js" export class SystemForStartOs implements System { private onTerm: (() => Promise) | undefined static of() { - return new SystemForStartOs() + return new SystemForStartOs(require(STARTOS_JS_LOCATION)) } - constructor() {} + constructor(readonly abi: T.ABI) {} async execute( effects: HostSystemStartOs, options: { + id: string procedure: | "/init" | "/uninit" @@ -33,7 +36,61 @@ export class SystemForStartOs implements System { timeout?: number | undefined }, ): Promise { - return { result: await this._execute(effects, options) } + effects = Object.create(effects) + effects.procedureId = options.id + return this._execute(effects, options) + .then((x) => + matches(x) + .when( + object({ + result: any, + }), + (x) => x, + ) + .when( + object({ + error: string, + }), + (x) => ({ + error: { + code: 0, + message: x.error, + }, + }), + ) + .when( + object({ + "error-code": tuple(number, string), + }), + ({ "error-code": [code, message] }) => ({ + error: { + code, + message, + }, + }), + ) + .defaultTo({ result: x }), + ) + .catch((error: unknown) => { + if (error instanceof Error) + return { + error: { + code: 0, + message: error.name, + data: { + details: error.message, + debug: `${error?.cause ?? "[noCause]"}:${error?.stack ?? "[noStack]"}`, + }, + }, + } + if (matchRpcResult.test(error)) return error + return { + error: { + code: 0, + message: String(error), + }, + } + }) } async _execute( effects: Effects, @@ -58,26 +115,27 @@ export class SystemForStartOs implements System { ): Promise { switch (options.procedure) { case "/init": { - const path = `${LOCATION}/procedures/init` - const procedure: any = await import(path).catch(() => require(path)) - const previousVersion = string.optional().unsafeCast(options) - return procedure.init({ effects, previousVersion }) + const previousVersion = + string.optional().unsafeCast(options.input) || null + return this.abi.init({ effects, previousVersion }) } case "/uninit": { - const path = `${LOCATION}/procedures/init` - const procedure: any = await import(path).catch(() => require(path)) - const nextVersion = string.optional().unsafeCast(options) - return procedure.uninit({ effects, nextVersion }) + const nextVersion = string.optional().unsafeCast(options.input) || null + return this.abi.uninit({ effects, nextVersion }) } case "/main/start": { - const path = `${LOCATION}/procedures/main` - const procedure: any = await import(path).catch(() => require(path)) const started = async (onTerm: () => Promise) => { await effects.setMainStatus({ status: "running" }) if (this.onTerm) await this.onTerm() this.onTerm = onTerm } - return procedure.main({ effects, started }) + const daemons = await ( + await this.abi.main({ + effects: { ...effects, _type: "main" }, + started, + }) + ).build() + this.onTerm = daemons.term } case "/main/stop": { await effects.setMainStatus({ status: "stopped" }) @@ -86,67 +144,50 @@ export class SystemForStartOs implements System { return duration(30, "s") } case "/config/set": { - const path = `${LOCATION}/procedures/config` - const procedure: any = await import(path).catch(() => require(path)) - const input = options.input - return procedure.setConfig({ effects, input }) + const input = options.input as any // TODO + return this.abi.setConfig({ effects, input }) } case "/config/get": { - const path = `${LOCATION}/procedures/config` - const procedure: any = await import(path).catch(() => require(path)) - return procedure.getConfig({ effects }) + return this.abi.getConfig({ effects }) } case "/backup/create": case "/backup/restore": throw new Error("this should be called with the init/unit") case "/actions/metadata": { - const path = `${LOCATION}/procedures/actions` - const procedure: any = await import(path).catch(() => require(path)) - return procedure.actionsMetadata({ effects }) + return this.abi.actionsMetadata({ effects }) } default: const procedures = unNestPath(options.procedure) const id = procedures[2] switch (true) { case procedures[1] === "actions" && procedures[3] === "get": { - const path = `${LOCATION}/procedures/actions` - const action: any = (await import(path).catch(() => require(path))) - .actions[id] + const action = (await this.abi.actions({ effects }))[id] if (!action) throw new Error(`Action ${id} not found`) - return action.get({ effects }) + return action.getConfig({ effects }) } case procedures[1] === "actions" && procedures[3] === "run": { - const path = `${LOCATION}/procedures/actions` - const action: any = (await import(path).catch(() => require(path))) - .actions[id] + const action = (await this.abi.actions({ effects }))[id] if (!action) throw new Error(`Action ${id} not found`) - const input = options.input - return action.run({ effects, input }) + return action.run({ effects, input: options.input as any }) // TODO } case procedures[1] === "dependencies" && procedures[3] === "query": { - const path = `${LOCATION}/procedures/dependencies` - const dependencyConfig: any = ( - await import(path).catch(() => require(path)) - ).dependencyConfig[id] + const dependencyConfig = this.abi.dependencyConfig[id] if (!dependencyConfig) throw new Error(`dependencyConfig ${id} not found`) const localConfig = options.input - return dependencyConfig.query({ effects, localConfig }) + return dependencyConfig.query({ effects }) } case procedures[1] === "dependencies" && procedures[3] === "update": { - const path = `${LOCATION}/procedures/dependencies` - const dependencyConfig: any = ( - await import(path).catch(() => require(path)) - ).dependencyConfig[id] + const dependencyConfig = this.abi.dependencyConfig[id] if (!dependencyConfig) throw new Error(`dependencyConfig ${id} not found`) - return dependencyConfig.update(options.input) + return dependencyConfig.update(options.input as any) // TODO } } } - throw new Error("Method not implemented.") + throw new Error(`Method ${options.procedure} not implemented.`) } - exit(effects: Effects): Promise { - throw new Error("Method not implemented.") + async exit(effects: Effects): Promise { + return void null } } diff --git a/container-runtime/src/Adapters/Systems/index.ts b/container-runtime/src/Adapters/Systems/index.ts index eadc67318..a44ad533e 100644 --- a/container-runtime/src/Adapters/Systems/index.ts +++ b/container-runtime/src/Adapters/Systems/index.ts @@ -1,6 +1,22 @@ +import * as fs from "node:fs/promises" import { System } from "../../Interfaces/System" -import { SystemForEmbassy } from "./SystemForEmbassy" -import { SystemForStartOs } from "./SystemForStartOs" +import { EMBASSY_JS_LOCATION, SystemForEmbassy } from "./SystemForEmbassy" +import { STARTOS_JS_LOCATION, SystemForStartOs } from "./SystemForStartOs" export async function getSystem(): Promise { - return SystemForEmbassy.of() + if ( + await fs.access(STARTOS_JS_LOCATION).then( + () => true, + () => false, + ) + ) { + return SystemForStartOs.of() + } else if ( + await fs.access(EMBASSY_JS_LOCATION).then( + () => true, + () => false, + ) + ) { + return SystemForEmbassy.of() + } + throw new Error(`${STARTOS_JS_LOCATION} not found`) } diff --git a/container-runtime/src/Interfaces/System.ts b/container-runtime/src/Interfaces/System.ts index 86b2aa492..85ba0fb0f 100644 --- a/container-runtime/src/Interfaces/System.ts +++ b/container-runtime/src/Interfaces/System.ts @@ -14,6 +14,7 @@ export interface System { execute( effects: T.Effects, options: { + id: string procedure: JsonPath input: unknown timeout?: number diff --git a/core/Cargo.lock b/core/Cargo.lock index 4ae02e98e..cf10152c2 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -387,6 +387,22 @@ dependencies = [ "tower-service", ] +[[package]] +name = "backhand" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f2fc1bc7bb7fd449e02000cc1592cc63dcdcd61710f8b9efe32bab2d1784603" +dependencies = [ + "deku", + "flate2", + "rustc-hash", + "thiserror", + "tracing", + "xz2", + "zstd", + "zstd-safe", +] + [[package]] name = "backtrace" version = "0.3.71" @@ -588,6 +604,11 @@ name = "cc" version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd" +dependencies = [ + "jobserver", + "libc", + "once_cell", +] [[package]] name = "cfg-if" @@ -1058,12 +1079,6 @@ dependencies = [ "cipher 0.3.0", ] -[[package]] -name = "current_platform" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a74858bcfe44b22016cb49337d7b6f04618c58e5dbfdef61b06b8c434324a0bc" - [[package]] name = "curve25519-dalek" version = "3.2.0" @@ -1146,6 +1161,31 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +[[package]] +name = "deku" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709ade444d53896e60f6265660eb50480dd08b77bfc822e5dcc233b88b0b2fba" +dependencies = [ + "bitvec", + "deku_derive", + "no_std_io", + "rustversion", +] + +[[package]] +name = "deku_derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7534973f93f9de83203e41c8ddd32d230599fa73fa889f3deb1580ccd186913" +dependencies = [ + "darling", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.60", +] + [[package]] name = "der" version = "0.7.9" @@ -2405,6 +2445,15 @@ dependencies = [ "jaq-parse", ] +[[package]] +name = "jobserver" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +dependencies = [ + "libc", +] + [[package]] name = "josekit" version = "0.8.6" @@ -2561,6 +2610,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libyml" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e281a65eeba3d4503a2839252f86374528f9ceafe6fed97c1d3b52e1fb625c1" + [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -2583,6 +2638,17 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "matchers" version = "0.1.0" @@ -2788,6 +2854,15 @@ dependencies = [ "libc", ] +[[package]] +name = "no_std_io" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fa5f306a6f2c01b4fd172f29bb46195b1764061bf926c75e96ff55df3178208" +dependencies = [ + "memchr", +] + [[package]] name = "nom" version = "7.1.3" @@ -3800,6 +3875,12 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.0" @@ -3895,9 +3976,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "rusty-fork" @@ -3929,9 +4010,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -4015,9 +4096,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.200" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] @@ -4041,9 +4122,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.200" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", @@ -4052,9 +4133,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "indexmap 2.2.6", "itoa", @@ -4124,16 +4205,20 @@ dependencies = [ ] [[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" +name = "serde_yml" +version = "0.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +checksum = "78ce6afeda22f0b55dde2c34897bce76a629587348480384231205c14b59a01f" dependencies = [ "indexmap 2.2.6", "itoa", + "libyml", + "log", + "memchr", "ryu", "serde", - "unsafe-libyaml", + "serde_json", + "tempfile", ] [[package]] @@ -4600,6 +4685,7 @@ dependencies = [ "async-trait", "axum 0.7.5", "axum-server", + "backhand", "base32", "base64 0.21.7", "base64ct", @@ -4614,7 +4700,6 @@ dependencies = [ "console-subscriber", "cookie 0.18.1", "cookie_store", - "current_platform", "der", "digest 0.10.7", "divrem", @@ -4681,7 +4766,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "serde_with", - "serde_yaml", + "serde_yml", "sha2 0.10.8", "shell-words", "simple-logging", @@ -5537,12 +5622,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - [[package]] name = "untrusted" version = "0.9.0" @@ -5993,6 +6072,15 @@ dependencies = [ "rustix", ] +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + [[package]] name = "yajrc" version = "0.1.3" @@ -6056,3 +6144,31 @@ dependencies = [ "quote", "syn 2.0.60", ] + +[[package]] +name = "zstd" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.10+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index bd064a167..3ad4f9eeb 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -59,6 +59,7 @@ async-stream = "0.3.5" async-trait = "0.1.74" axum = { version = "0.7.3", features = ["ws"] } axum-server = "0.6.0" +backhand = "0.18.0" base32 = "0.4.0" base64 = "0.21.4" base64ct = "1.6.0" @@ -72,7 +73,6 @@ console = "0.15.7" console-subscriber = { version = "0.2", optional = true } cookie = "0.18.0" cookie_store = "0.20.0" -current_platform = "0.2.0" der = { version = "0.7.9", features = ["derive", "pem"] } digest = "0.10.7" divrem = "1.0.0" @@ -154,7 +154,7 @@ serde_json = "1.0" serde_toml = { package = "toml", version = "0.8.2" } serde_urlencoded = "0.7" serde_with = { version = "3.4.0", features = ["macros", "json"] } -serde_yaml = "0.9.25" +serde_yaml = { package = "serde_yml", version = "0.0.10" } sha2 = "0.10.2" shell-words = "1" simple-logging = "2.0.2" diff --git a/core/startos/src/action.rs b/core/startos/src/action.rs index 87ff317f8..e93af4a4d 100644 --- a/core/startos/src/action.rs +++ b/core/startos/src/action.rs @@ -8,6 +8,7 @@ use ts_rs::TS; use crate::config::Config; use crate::context::RpcContext; use crate::prelude::*; +use crate::rpc_continuations::Guid; use crate::util::serde::{display_serializable, StdinDeserializable, WithIoFormat}; #[derive(Debug, Serialize, Deserialize)] @@ -77,6 +78,7 @@ pub async fn action( .as_ref() .or_not_found(lazy_format!("Manager for {}", package_id))? .action( + Guid::new(), action_id, input.map(|c| to_value(&c)).transpose()?.unwrap_or_default(), ) diff --git a/core/startos/src/auth.rs b/core/startos/src/auth.rs index 4838f2ea2..d33320b78 100644 --- a/core/startos/src/auth.rs +++ b/core/startos/src/auth.rs @@ -178,6 +178,7 @@ pub fn check_password_against_db(db: &DatabaseModel, password: &str) -> Result<( #[derive(Deserialize, Serialize, Parser, TS)] #[serde(rename_all = "camelCase")] #[command(rename_all = "kebab-case")] +#[ts(export)] pub struct LoginParams { password: Option, #[ts(skip)] diff --git a/core/startos/src/backup/restore.rs b/core/startos/src/backup/restore.rs index 774b1f1cf..4753a4290 100644 --- a/core/startos/src/backup/restore.rs +++ b/core/startos/src/backup/restore.rs @@ -149,7 +149,6 @@ async fn restore_packages( S9pk::open( backup_dir.path().join(&id).with_extension("s9pk"), Some(&id), - true, ) .await?, Some(backup_dir), diff --git a/core/startos/src/config/mod.rs b/core/startos/src/config/mod.rs index e61517794..01309a16f 100644 --- a/core/startos/src/config/mod.rs +++ b/core/startos/src/config/mod.rs @@ -16,6 +16,7 @@ use ts_rs::TS; use crate::context::{CliContext, RpcContext}; use crate::prelude::*; +use crate::rpc_continuations::Guid; use crate::util::serde::{HandlerExtSerde, StdinDeserializable}; #[derive(Clone, Debug, Default, Serialize, Deserialize)] @@ -156,7 +157,7 @@ pub async fn get(ctx: RpcContext, _: Empty, id: PackageId) -> Result, - #[arg(long = "ethernet-interface")] + #[arg(long)] pub ethernet_interface: Option, #[arg(skip)] pub os_partitions: Option, - #[arg(long = "bind-rpc")] + #[arg(long)] pub bind_rpc: Option, - #[arg(long = "tor-control")] + #[arg(long)] pub tor_control: Option, - #[arg(long = "tor-socks")] + #[arg(long)] pub tor_socks: Option, - #[arg(long = "dns-bind")] + #[arg(long)] pub dns_bind: Option>, - #[arg(long = "revision-cache-size")] + #[arg(long)] pub revision_cache_size: Option, - #[arg(short = 'd', long = "datadir")] + #[arg(short, long)] pub datadir: Option, - #[arg(long = "disable-encryption")] + #[arg(long)] pub disable_encryption: Option, + #[arg(long)] + pub multi_arch_s9pks: Option, } impl ContextConfig for ServerConfig { fn next(&mut self) -> Option { @@ -131,6 +133,7 @@ impl ContextConfig for ServerConfig { .or(other.revision_cache_size); self.datadir = self.datadir.take().or(other.datadir); self.disable_encryption = self.disable_encryption.take().or(other.disable_encryption); + self.multi_arch_s9pks = self.multi_arch_s9pks.take().or(other.multi_arch_s9pks); } } diff --git a/core/startos/src/context/rpc.rs b/core/startos/src/context/rpc.rs index cf2d28085..a3e77a62c 100644 --- a/core/startos/src/context/rpc.rs +++ b/core/startos/src/context/rpc.rs @@ -43,6 +43,7 @@ pub struct RpcContextSeed { pub db: TypedPatchDb, pub account: RwLock, pub net_controller: Arc, + pub s9pk_arch: Option<&'static str>, pub services: ServiceMap, pub metrics_cache: RwLock>, pub shutdown: broadcast::Sender>, @@ -152,6 +153,11 @@ impl RpcContext { db, account: RwLock::new(account), net_controller, + s9pk_arch: if config.multi_arch_s9pks.unwrap_or(false) { + None + } else { + Some(crate::ARCH) + }, services, metrics_cache, shutdown, diff --git a/core/startos/src/control.rs b/core/startos/src/control.rs index d4a595a61..e831e07d6 100644 --- a/core/startos/src/control.rs +++ b/core/startos/src/control.rs @@ -7,6 +7,7 @@ use ts_rs::TS; use crate::context::RpcContext; use crate::prelude::*; +use crate::rpc_continuations::Guid; use crate::Error; #[derive(Deserialize, Serialize, Parser, TS)] @@ -23,7 +24,7 @@ pub async fn start(ctx: RpcContext, ControlParams { id }: ControlParams) -> Resu .await .as_ref() .or_not_found(lazy_format!("Manager for {id}"))? - .start() + .start(Guid::new()) .await?; Ok(()) @@ -36,7 +37,7 @@ pub async fn stop(ctx: RpcContext, ControlParams { id }: ControlParams) -> Resul .await .as_ref() .ok_or_else(|| Error::new(eyre!("Manager not found"), crate::ErrorKind::InvalidRequest))? - .stop() + .stop(Guid::new()) .await?; Ok(()) @@ -48,7 +49,7 @@ pub async fn restart(ctx: RpcContext, ControlParams { id }: ControlParams) -> Re .await .as_ref() .ok_or_else(|| Error::new(eyre!("Manager not found"), crate::ErrorKind::InvalidRequest))? - .restart() + .restart(Guid::new()) .await?; Ok(()) diff --git a/core/startos/src/dependencies.rs b/core/startos/src/dependencies.rs index a746a42c9..f6ccc53ad 100644 --- a/core/startos/src/dependencies.rs +++ b/core/startos/src/dependencies.rs @@ -13,6 +13,7 @@ use crate::config::{Config, ConfigSpec, ConfigureContext}; use crate::context::RpcContext; use crate::db::model::package::CurrentDependencies; use crate::prelude::*; +use crate::rpc_continuations::Guid; use crate::Error; pub fn dependency() -> ParentHandler { @@ -86,7 +87,7 @@ pub async fn configure_impl( ErrorKind::Unknown, ) })? - .configure(configure_context) + .configure(Guid::new(), configure_context) .await?; Ok(()) } @@ -103,14 +104,15 @@ pub async fn configure_logic( ctx: RpcContext, (dependent_id, dependency_id): (PackageId, PackageId), ) -> Result { + let procedure_id = Guid::new(); let dependency_guard = ctx.services.get(&dependency_id).await; let dependency = dependency_guard.as_ref().or_not_found(&dependency_id)?; let dependent_guard = ctx.services.get(&dependent_id).await; let dependent = dependent_guard.as_ref().or_not_found(&dependent_id)?; - let config_res = dependency.get_config().await?; + let config_res = dependency.get_config(procedure_id.clone()).await?; let diff = Value::Object( dependent - .dependency_config(dependency_id, config_res.config.clone()) + .dependency_config(procedure_id, dependency_id, config_res.config.clone()) .await? .unwrap_or_default(), ); @@ -129,6 +131,7 @@ pub async fn compute_dependency_config_errs( id: &PackageId, current_dependencies: &mut CurrentDependencies, ) -> Result<(), Error> { + let procedure_id = Guid::new(); let service_guard = ctx.services.get(id).await; let service = service_guard.as_ref().or_not_found(id)?; for (dep_id, dep_info) in current_dependencies.0.iter_mut() { @@ -137,10 +140,10 @@ pub async fn compute_dependency_config_errs( continue; }; - let dep_config = dependency.get_config().await?.config; + let dep_config = dependency.get_config(procedure_id.clone()).await?.config; dep_info.config_satisfied = service - .dependency_config(dep_id.clone(), dep_config) + .dependency_config(procedure_id.clone(), dep_id.clone(), dep_config) .await? .is_none(); } diff --git a/core/startos/src/disk/mount/backup.rs b/core/startos/src/disk/mount/backup.rs index 5dbd80db3..ad9f5090b 100644 --- a/core/startos/src/disk/mount/backup.rs +++ b/core/startos/src/disk/mount/backup.rs @@ -178,7 +178,6 @@ impl BackupMountGuard { Ok(()) } } -#[async_trait::async_trait] impl GenericMountGuard for BackupMountGuard { fn path(&self) -> &Path { if let Some(guard) = &self.encrypted_guard { diff --git a/core/startos/src/disk/mount/filesystem/overlayfs.rs b/core/startos/src/disk/mount/filesystem/overlayfs.rs index f96de7b11..5e40a21a1 100644 --- a/core/startos/src/disk/mount/filesystem/overlayfs.rs +++ b/core/startos/src/disk/mount/filesystem/overlayfs.rs @@ -6,8 +6,8 @@ use digest::generic_array::GenericArray; use digest::{Digest, OutputSizeUser}; use sha2::Sha256; -use crate::disk::mount::filesystem::{FileSystem, ReadOnly, ReadWrite}; -use crate::disk::mount::guard::{GenericMountGuard, MountGuard, TmpMountGuard}; +use crate::disk::mount::filesystem::{FileSystem, ReadWrite}; +use crate::disk::mount::guard::{GenericMountGuard, MountGuard}; use crate::prelude::*; use crate::util::io::TmpDir; @@ -94,17 +94,13 @@ impl< } #[derive(Debug)] -pub struct OverlayGuard { - lower: Option, +pub struct OverlayGuard { + lower: Option, upper: Option, inner_guard: MountGuard, } -impl OverlayGuard { - pub async fn mount( - base: &impl FileSystem, - mountpoint: impl AsRef, - ) -> Result { - let lower = TmpMountGuard::mount(base, ReadOnly).await?; +impl OverlayGuard { + pub async fn mount(lower: G, mountpoint: impl AsRef) -> Result { let upper = TmpDir::new().await?; let inner_guard = MountGuard::mount( &OverlayFs::new( @@ -140,16 +136,15 @@ impl OverlayGuard { } } } -#[async_trait::async_trait] -impl GenericMountGuard for OverlayGuard { +impl GenericMountGuard for OverlayGuard { fn path(&self) -> &Path { self.inner_guard.path() } - async fn unmount(mut self) -> Result<(), Error> { + async fn unmount(self) -> Result<(), Error> { self.unmount(false).await } } -impl Drop for OverlayGuard { +impl Drop for OverlayGuard { fn drop(&mut self) { let lower = self.lower.take(); let upper = self.upper.take(); diff --git a/core/startos/src/disk/mount/guard.rs b/core/startos/src/disk/mount/guard.rs index d08b04881..4686a10b8 100644 --- a/core/startos/src/disk/mount/guard.rs +++ b/core/startos/src/disk/mount/guard.rs @@ -2,6 +2,7 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::sync::{Arc, Weak}; +use futures::Future; use lazy_static::lazy_static; use models::ResultExt; use tokio::sync::Mutex; @@ -14,23 +15,20 @@ use crate::Error; pub const TMP_MOUNTPOINT: &'static str = "/media/startos/tmp"; -#[async_trait::async_trait] pub trait GenericMountGuard: std::fmt::Debug + Send + Sync + 'static { fn path(&self) -> &Path; - async fn unmount(mut self) -> Result<(), Error>; + fn unmount(self) -> impl Future> + Send; } -#[async_trait::async_trait] impl GenericMountGuard for Never { fn path(&self) -> &Path { match *self {} } - async fn unmount(mut self) -> Result<(), Error> { + async fn unmount(self) -> Result<(), Error> { match self {} } } -#[async_trait::async_trait] impl GenericMountGuard for Arc where T: GenericMountGuard, @@ -38,7 +36,7 @@ where fn path(&self) -> &Path { (&**self).path() } - async fn unmount(mut self) -> Result<(), Error> { + async fn unmount(self) -> Result<(), Error> { if let Ok(guard) = Arc::try_unwrap(self) { guard.unmount().await?; } @@ -102,12 +100,11 @@ impl Drop for MountGuard { } } } -#[async_trait::async_trait] impl GenericMountGuard for MountGuard { fn path(&self) -> &Path { &self.mountpoint } - async fn unmount(mut self) -> Result<(), Error> { + async fn unmount(self) -> Result<(), Error> { MountGuard::unmount(self, false).await } } @@ -165,12 +162,11 @@ impl TmpMountGuard { std::mem::replace(self, unmounted) } } -#[async_trait::async_trait] impl GenericMountGuard for TmpMountGuard { fn path(&self) -> &Path { self.guard.path() } - async fn unmount(mut self) -> Result<(), Error> { + async fn unmount(self) -> Result<(), Error> { self.guard.unmount().await } } @@ -187,12 +183,11 @@ impl SubPath { Self { guard, path } } } -#[async_trait::async_trait] impl GenericMountGuard for SubPath { fn path(&self) -> &Path { self.path.as_path() } - async fn unmount(mut self) -> Result<(), Error> { + async fn unmount(self) -> Result<(), Error> { self.guard.unmount().await } } diff --git a/core/startos/src/init.rs b/core/startos/src/init.rs index 694d4e3a3..97c674ac5 100644 --- a/core/startos/src/init.rs +++ b/core/startos/src/init.rs @@ -242,7 +242,7 @@ pub async fn init(cfg: &ServerConfig) -> Result { let should_rebuild = tokio::fs::metadata(SYSTEM_REBUILD_PATH).await.is_ok() || &*server_info.version < &emver::Version::new(0, 3, 2, 0) - || (*ARCH == "x86_64" && &*server_info.version < &emver::Version::new(0, 3, 4, 0)); + || (ARCH == "x86_64" && &*server_info.version < &emver::Version::new(0, 3, 4, 0)); let log_dir = cfg.datadir().join("main/logs"); if tokio::fs::metadata(&log_dir).await.is_err() { diff --git a/core/startos/src/install/mod.rs b/core/startos/src/install/mod.rs index c4e4452b7..bb7d1a02e 100644 --- a/core/startos/src/install/mod.rs +++ b/core/startos/src/install/mod.rs @@ -152,7 +152,6 @@ pub async fn install( .await?, ), None, // TODO - true, ) .await?; @@ -262,7 +261,6 @@ pub async fn sideload(ctx: RpcContext) -> Result { if let Err(e) = async { let s9pk = S9pk::deserialize( &file, None, // TODO - true, ) .await?; let _ = id_send.send(s9pk.as_manifest().id.clone()); diff --git a/core/startos/src/lib.rs b/core/startos/src/lib.rs index d60a2db24..0e125af98 100644 --- a/core/startos/src/lib.rs +++ b/core/startos/src/lib.rs @@ -4,12 +4,8 @@ pub const CAP_1_KiB: usize = 1024; pub const CAP_1_MiB: usize = CAP_1_KiB * CAP_1_KiB; pub const CAP_10_MiB: usize = 10 * CAP_1_MiB; pub const HOST_IP: [u8; 4] = [172, 18, 0, 1]; -pub const TARGET: &str = current_platform::CURRENT_PLATFORM; +pub use std::env::consts::ARCH; lazy_static::lazy_static! { - pub static ref ARCH: &'static str = { - let (arch, _) = TARGET.split_once("-").unwrap(); - arch - }; pub static ref PLATFORM: String = { if let Ok(platform) = std::fs::read_to_string("/usr/lib/startos/PLATFORM.txt") { platform diff --git a/core/startos/src/lxc/mod.rs b/core/startos/src/lxc/mod.rs index f64ecebe7..8d37120ba 100644 --- a/core/startos/src/lxc/mod.rs +++ b/core/startos/src/lxc/mod.rs @@ -29,7 +29,7 @@ use crate::disk::mount::filesystem::bind::Bind; use crate::disk::mount::filesystem::block_dev::BlockDev; use crate::disk::mount::filesystem::idmapped::IdMapped; use crate::disk::mount::filesystem::overlayfs::OverlayGuard; -use crate::disk::mount::filesystem::{MountType, ReadWrite}; +use crate::disk::mount::filesystem::{MountType, ReadOnly, ReadWrite}; use crate::disk::mount::guard::{GenericMountGuard, MountGuard, TmpMountGuard}; use crate::disk::mount::util::unmount; use crate::prelude::*; @@ -153,7 +153,7 @@ impl LxcManager { pub struct LxcContainer { manager: Weak, - rootfs: OverlayGuard, + rootfs: OverlayGuard, pub guid: Arc, rpc_bind: TmpMountGuard, log_mount: Option, @@ -184,12 +184,16 @@ impl LxcContainer { .invoke(ErrorKind::Filesystem) .await?; let rootfs = OverlayGuard::mount( - &IdMapped::new( - BlockDev::new("/usr/lib/startos/container-runtime/rootfs.squashfs"), - 0, - 100000, - 65536, - ), + TmpMountGuard::mount( + &IdMapped::new( + BlockDev::new("/usr/lib/startos/container-runtime/rootfs.squashfs"), + 0, + 100000, + 65536, + ), + ReadOnly, + ) + .await?, &rootfs_dir, ) .await?; diff --git a/core/startos/src/os_install/gpt.rs b/core/startos/src/os_install/gpt.rs index 4139b4cf2..01703083b 100644 --- a/core/startos/src/os_install/gpt.rs +++ b/core/startos/src/os_install/gpt.rs @@ -87,7 +87,7 @@ pub async fn partition(disk: &DiskInfo, overwrite: bool) -> Result gpt::partition_types::LINUX_ROOT_X64, "aarch64" => gpt::partition_types::LINUX_ROOT_ARM_64, _ => gpt::partition_types::LINUX_FS, diff --git a/core/startos/src/os_install/mod.rs b/core/startos/src/os_install/mod.rs index c3931b236..4e5c7ed15 100644 --- a/core/startos/src/os_install/mod.rs +++ b/core/startos/src/os_install/mod.rs @@ -366,7 +366,7 @@ pub async fn execute( if tokio::fs::metadata("/sys/firmware/efi").await.is_err() { install.arg("--target=i386-pc"); } else { - match *ARCH { + match ARCH { "x86_64" => install.arg("--target=x86_64-efi"), "aarch64" => install.arg("--target=arm64-efi"), _ => &mut install, diff --git a/core/startos/src/registry/device_info.rs b/core/startos/src/registry/device_info.rs index 7da5bd8b9..51d6ac46b 100644 --- a/core/startos/src/registry/device_info.rs +++ b/core/startos/src/registry/device_info.rs @@ -134,7 +134,7 @@ pub struct HardwareInfo { impl From<&RpcContext> for HardwareInfo { fn from(value: &RpcContext) -> Self { Self { - arch: InternedString::intern(&**crate::ARCH), + arch: InternedString::intern(crate::ARCH), ram: value.hardware.ram, devices: value .hardware diff --git a/core/startos/src/registry/package/add.rs b/core/startos/src/registry/package/add.rs index 6a1050b99..d28aeaaa4 100644 --- a/core/startos/src/registry/package/add.rs +++ b/core/startos/src/registry/package/add.rs @@ -53,7 +53,6 @@ pub async fn add_package( let s9pk = S9pk::deserialize( &Arc::new(HttpSource::new(ctx.client.clone(), url.clone()).await?), Some(&commitment), - false, ) .await?; @@ -109,7 +108,7 @@ pub async fn cli_add_package( .. }: HandlerArgs, ) -> Result<(), Error> { - let s9pk = S9pk::open(&file, None, false).await?; + let s9pk = S9pk::open(&file, None).await?; let mut progress = FullProgressTracker::new(); let progress_handle = progress.handle(); @@ -143,7 +142,6 @@ pub async fn cli_add_package( let mut src = S9pk::deserialize( &Arc::new(HttpSource::new(ctx.client.clone(), url.clone()).await?), Some(&commitment), - false, ) .await?; src.serialize(&mut TrackingIO::new(0, tokio::io::sink()), true) diff --git a/core/startos/src/rpc_continuations.rs b/core/startos/src/rpc_continuations.rs index ce8bf43fd..e6b823ef9 100644 --- a/core/startos/src/rpc_continuations.rs +++ b/core/startos/src/rpc_continuations.rs @@ -39,6 +39,11 @@ impl Guid { Some(Guid(InternedString::intern(r))) } } +impl Default for Guid { + fn default() -> Self { + Self::new() + } +} impl AsRef for Guid { fn as_ref(&self) -> &str { self.0.as_ref() diff --git a/core/startos/src/s9pk/merkle_archive/directory_contents.rs b/core/startos/src/s9pk/merkle_archive/directory_contents.rs index 77c3582d9..c5e5c4a7d 100644 --- a/core/startos/src/s9pk/merkle_archive/directory_contents.rs +++ b/core/startos/src/s9pk/merkle_archive/directory_contents.rs @@ -211,7 +211,10 @@ impl DirectoryContents { if !filter(path) { if v.hash.is_none() { return Err(Error::new( - eyre!("cannot filter out unhashed file, run `update_hashes` first"), + eyre!( + "cannot filter out unhashed file {}, run `update_hashes` first", + path.display() + ), ErrorKind::InvalidRequest, )); } diff --git a/core/startos/src/s9pk/merkle_archive/expected.rs b/core/startos/src/s9pk/merkle_archive/expected.rs new file mode 100644 index 000000000..a0f095b7a --- /dev/null +++ b/core/startos/src/s9pk/merkle_archive/expected.rs @@ -0,0 +1,103 @@ + +use std::ffi::OsStr; +use std::path::Path; + +use crate::prelude::*; +use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; +use crate::s9pk::merkle_archive::source::FileSource; +use crate::s9pk::merkle_archive::Entry; + +/// An object for tracking the files expected to be in an s9pk +pub struct Expected<'a, T> { + keep: DirectoryContents<()>, + dir: &'a DirectoryContents, +} +impl<'a, T> Expected<'a, T> { + pub fn new(dir: &'a DirectoryContents,) -> Self { + Self { + keep: DirectoryContents::new(), + dir + } + } +} +impl<'a, T: Clone> Expected<'a, T> { + pub fn check_file(&mut self, path: impl AsRef) -> Result<(), Error> { + if self + .dir + .get_path(path.as_ref()) + .and_then(|e| e.as_file()) + .is_some() + { + self.keep.insert_path(path, Entry::file(()))?; + Ok(()) + } else { + Err(Error::new( + eyre!("file {} missing from archive", path.as_ref().display()), + ErrorKind::ParseS9pk, + )) + } + } + pub fn check_stem( + &mut self, + path: impl AsRef, + mut valid_extension: impl FnMut(Option<&OsStr>) -> bool, + ) -> Result<(), Error> { + let (dir, stem) = if let Some(parent) = path.as_ref().parent().filter(|p| *p != Path::new("")) { + ( + self.dir + .get_path(parent) + .and_then(|e| e.as_directory()) + .ok_or_else(|| { + Error::new( + eyre!("directory {} missing from archive", parent.display()), + ErrorKind::ParseS9pk, + ) + })?, + path.as_ref().strip_prefix(parent).unwrap(), + ) + } else { + (self.dir, path.as_ref()) + }; + let name = dir + .with_stem(&stem.as_os_str().to_string_lossy()) + .filter(|(_, e)| e.as_file().is_some()) + .try_fold( + Err(Error::new( + eyre!( + "file {} with valid extension missing from archive", + path.as_ref().display() + ), + ErrorKind::ParseS9pk, + )), + |acc, (name, _)| + if valid_extension(Path::new(&*name).extension()) { + match acc { + Ok(_) => Err(Error::new( + eyre!( + "more than one file matching {} with valid extension in archive", + path.as_ref().display() + ), + ErrorKind::ParseS9pk, + )), + Err(_) => Ok(Ok(name)) + } + } else { + Ok(acc) + } + )??; + self.keep + .insert_path(path.as_ref().with_file_name(name), Entry::file(()))?; + Ok(()) + } + pub fn into_filter(self) -> Filter { + Filter(self.keep) + } +} + +pub struct Filter(DirectoryContents<()>); +impl Filter { + pub fn keep_checked(&self, dir: &mut DirectoryContents) -> Result<(), Error> { + dir.filter(|path| self.0.get_path(path).is_some()) + } +} + diff --git a/core/startos/src/s9pk/merkle_archive/mod.rs b/core/startos/src/s9pk/merkle_archive/mod.rs index 1c0d6b786..00ead65c5 100644 --- a/core/startos/src/s9pk/merkle_archive/mod.rs +++ b/core/startos/src/s9pk/merkle_archive/mod.rs @@ -19,6 +19,7 @@ use crate::util::serde::Base64; use crate::CAP_1_MiB; pub mod directory_contents; +pub mod expected; pub mod file_contents; pub mod hash; pub mod sink; @@ -217,6 +218,9 @@ impl Entry { pub fn file(source: S) -> Self { Self::new(EntryContents::File(FileContents::new(source))) } + pub fn directory(directory: DirectoryContents) -> Self { + Self::new(EntryContents::Directory(directory)) + } pub fn hash(&self) -> Option<(Hash, u64)> { self.hash } diff --git a/core/startos/src/s9pk/merkle_archive/source/mod.rs b/core/startos/src/s9pk/merkle_archive/source/mod.rs index f6922d109..0a00b18dd 100644 --- a/core/startos/src/s9pk/merkle_archive/source/mod.rs +++ b/core/startos/src/s9pk/merkle_archive/source/mod.rs @@ -280,3 +280,8 @@ impl FileSource for Section { self.source.copy_to(self.position, self.size, w).await } } + +pub type DynRead = Box; +pub fn into_dyn_read(r: R) -> DynRead { + Box::new(r) +} diff --git a/core/startos/src/s9pk/merkle_archive/source/multi_cursor_file.rs b/core/startos/src/s9pk/merkle_archive/source/multi_cursor_file.rs index 9cc162f0e..92eb40f9d 100644 --- a/core/startos/src/s9pk/merkle_archive/source/multi_cursor_file.rs +++ b/core/startos/src/s9pk/merkle_archive/source/multi_cursor_file.rs @@ -97,7 +97,7 @@ impl ArchiveSource for MultiCursorFile { .ok() .map(|m| m.len()) } - async fn fetch_all(&self) -> Result { + async fn fetch_all(&self) -> Result { use tokio::io::AsyncSeekExt; let mut file = self.cursor().await?; diff --git a/core/startos/src/s9pk/rpc.rs b/core/startos/src/s9pk/rpc.rs index 89cfc9b5a..fac9e6724 100644 --- a/core/startos/src/s9pk/rpc.rs +++ b/core/startos/src/s9pk/rpc.rs @@ -1,32 +1,26 @@ -use std::collections::BTreeSet; -use std::path::{Path, PathBuf}; -use std::sync::Arc; +use std::path::PathBuf; use clap::Parser; -use itertools::Itertools; use models::ImageId; use rpc_toolkit::{from_fn_async, Empty, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use tokio::fs::File; -use tokio::process::Command; use ts_rs::TS; use crate::context::CliContext; use crate::prelude::*; use crate::s9pk::manifest::Manifest; -use crate::s9pk::merkle_archive::source::DynFileSource; -use crate::s9pk::merkle_archive::Entry; -use crate::s9pk::v2::compat::CONTAINER_TOOL; +use crate::s9pk::v2::pack::ImageConfig; use crate::s9pk::v2::SIG_CONTEXT; use crate::s9pk::S9pk; use crate::util::io::TmpDir; use crate::util::serde::{apply_expr, HandlerExtSerde}; -use crate::util::Invoke; pub const SKIP_ENV: &[&str] = &["TERM", "container", "HOME", "HOSTNAME"]; pub fn s9pk() -> ParentHandler { ParentHandler::new() + .subcommand("pack", from_fn_async(super::v2::pack::pack).no_display()) .subcommand("edit", edit()) .subcommand("inspect", inspect()) } @@ -77,117 +71,21 @@ fn inspect() -> ParentHandler { #[derive(Deserialize, Serialize, Parser, TS)] struct AddImageParams { id: ImageId, - image: String, - arches: Option>, + #[command(flatten)] + config: ImageConfig, } async fn add_image( ctx: CliContext, - AddImageParams { id, image, arches }: AddImageParams, + AddImageParams { id, config }: AddImageParams, S9pkPath { s9pk: s9pk_path }: S9pkPath, ) -> Result<(), Error> { - let mut s9pk = S9pk::from_file(super::load(&ctx, &s9pk_path).await?, false) + let mut s9pk = S9pk::from_file(super::load(&ctx, &s9pk_path).await?) .await? .into_dyn(); - let arches: BTreeSet<_> = arches - .unwrap_or_else(|| vec!["x86_64".to_owned(), "aarch64".to_owned()]) - .into_iter() - .collect(); + s9pk.as_manifest_mut().images.insert(id, config); let tmpdir = TmpDir::new().await?; - for arch in arches { - let sqfs_path = tmpdir.join(format!("image.{arch}.squashfs")); - let docker_platform = if arch == "x86_64" { - "--platform=linux/amd64".to_owned() - } else if arch == "aarch64" { - "--platform=linux/arm64".to_owned() - } else { - format!("--platform=linux/{arch}") - }; - let env = String::from_utf8( - Command::new(CONTAINER_TOOL) - .arg("run") - .arg("--rm") - .arg(&docker_platform) - .arg("--entrypoint") - .arg("env") - .arg(&image) - .invoke(ErrorKind::Docker) - .await?, - )? - .lines() - .filter(|l| { - l.trim() - .split_once("=") - .map_or(false, |(v, _)| !SKIP_ENV.contains(&v)) - }) - .join("\n") - + "\n"; - let workdir = Path::new( - String::from_utf8( - Command::new(CONTAINER_TOOL) - .arg("run") - .arg(&docker_platform) - .arg("--rm") - .arg("--entrypoint") - .arg("pwd") - .arg(&image) - .invoke(ErrorKind::Docker) - .await?, - )? - .trim(), - ) - .to_owned(); - let container_id = String::from_utf8( - Command::new(CONTAINER_TOOL) - .arg("create") - .arg(&docker_platform) - .arg(&image) - .invoke(ErrorKind::Docker) - .await?, - )?; - Command::new("bash") - .arg("-c") - .arg(format!( - "{CONTAINER_TOOL} export {container_id} | mksquashfs - {sqfs} -tar", - container_id = container_id.trim(), - sqfs = sqfs_path.display() - )) - .invoke(ErrorKind::Docker) - .await?; - Command::new(CONTAINER_TOOL) - .arg("rm") - .arg(container_id.trim()) - .invoke(ErrorKind::Docker) - .await?; - let archive = s9pk.as_archive_mut(); - archive.set_signer(ctx.developer_key()?.clone(), SIG_CONTEXT); - archive.contents_mut().insert_path( - Path::new("images") - .join(&arch) - .join(&id) - .with_extension("squashfs"), - Entry::file(DynFileSource::new(sqfs_path)), - )?; - archive.contents_mut().insert_path( - Path::new("images") - .join(&arch) - .join(&id) - .with_extension("env"), - Entry::file(DynFileSource::new(Arc::<[u8]>::from(Vec::from(env)))), - )?; - archive.contents_mut().insert_path( - Path::new("images") - .join(&arch) - .join(&id) - .with_extension("json"), - Entry::file(DynFileSource::new(Arc::<[u8]>::from( - serde_json::to_vec(&serde_json::json!({ - "workdir": workdir - })) - .with_kind(ErrorKind::Serialization)?, - ))), - )?; - } - s9pk.as_manifest_mut().images.insert(id); + s9pk.load_images(&tmpdir).await?; + s9pk.validate_and_filter(None)?; let tmp_path = s9pk_path.with_extension("s9pk.tmp"); let mut tmp_file = File::create(&tmp_path).await?; s9pk.serialize(&mut tmp_file, true).await?; @@ -206,7 +104,7 @@ async fn edit_manifest( EditManifestParams { expression }: EditManifestParams, S9pkPath { s9pk: s9pk_path }: S9pkPath, ) -> Result { - let mut s9pk = S9pk::from_file(super::load(&ctx, &s9pk_path).await?, false).await?; + let mut s9pk = S9pk::from_file(super::load(&ctx, &s9pk_path).await?).await?; let old = serde_json::to_value(s9pk.as_manifest()).with_kind(ErrorKind::Serialization)?; *s9pk.as_manifest_mut() = serde_json::from_value(apply_expr(old.into(), &expression)?.into()) .with_kind(ErrorKind::Serialization)?; @@ -227,7 +125,7 @@ async fn file_tree( _: Empty, S9pkPath { s9pk }: S9pkPath, ) -> Result, Error> { - let s9pk = S9pk::from_file(super::load(&ctx, &s9pk).await?, false).await?; + let s9pk = S9pk::from_file(super::load(&ctx, &s9pk).await?).await?; Ok(s9pk.as_archive().contents().file_paths("")) } @@ -244,7 +142,7 @@ async fn cat( ) -> Result<(), Error> { use crate::s9pk::merkle_archive::source::FileSource; - let s9pk = S9pk::from_file(super::load(&ctx, &s9pk).await?, false).await?; + let s9pk = S9pk::from_file(super::load(&ctx, &s9pk).await?).await?; tokio::io::copy( &mut s9pk .as_archive() @@ -266,6 +164,6 @@ async fn inspect_manifest( _: Empty, S9pkPath { s9pk }: S9pkPath, ) -> Result { - let s9pk = S9pk::from_file(super::load(&ctx, &s9pk).await?, false).await?; + let s9pk = S9pk::from_file(super::load(&ctx, &s9pk).await?).await?; Ok(s9pk.as_manifest().clone()) } diff --git a/core/startos/src/s9pk/v2/compat.rs b/core/startos/src/s9pk/v2/compat.rs index 5d4ad2f44..835c86b87 100644 --- a/core/startos/src/s9pk/v2/compat.rs +++ b/core/startos/src/s9pk/v2/compat.rs @@ -1,6 +1,5 @@ -use std::collections::{BTreeMap, BTreeSet}; -use std::io::Cursor; -use std::path::{Path, PathBuf}; +use std::collections::BTreeMap; +use std::path::Path; use std::sync::Arc; use itertools::Itertools; @@ -14,49 +13,18 @@ use crate::prelude::*; use crate::s9pk::manifest::Manifest; use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; -use crate::s9pk::merkle_archive::source::{FileSource, Section}; +use crate::s9pk::merkle_archive::source::Section; use crate::s9pk::merkle_archive::{Entry, MerkleArchive}; use crate::s9pk::rpc::SKIP_ENV; use crate::s9pk::v1::manifest::{Manifest as ManifestV1, PackageProcedure}; use crate::s9pk::v1::reader::S9pkReader; +use crate::s9pk::v2::pack::{PackSource, CONTAINER_TOOL}; use crate::s9pk::v2::{S9pk, SIG_CONTEXT}; use crate::util::io::TmpDir; use crate::util::Invoke; pub const MAGIC_AND_VERSION: &[u8] = &[0x3b, 0x3b, 0x01]; -#[cfg(not(feature = "docker"))] -pub const CONTAINER_TOOL: &str = "podman"; - -#[cfg(feature = "docker")] -pub const CONTAINER_TOOL: &str = "docker"; - -type DynRead = Box; -fn into_dyn_read(r: R) -> DynRead { - Box::new(r) -} - -#[derive(Clone)] -enum CompatSource { - Buffered(Arc<[u8]>), - File(PathBuf), -} -impl FileSource for CompatSource { - type Reader = Box; - async fn size(&self) -> Result { - match self { - Self::Buffered(a) => Ok(a.len() as u64), - Self::File(f) => Ok(tokio::fs::metadata(f).await?.len()), - } - } - async fn reader(&self) -> Result { - match self { - Self::Buffered(a) => Ok(into_dyn_read(Cursor::new(a.clone()))), - Self::File(f) => Ok(into_dyn_read(File::open(f).await?)), - } - } -} - impl S9pk> { #[instrument(skip_all)] pub async fn from_v1( @@ -66,7 +34,7 @@ impl S9pk> { ) -> Result { let scratch_dir = TmpDir::new().await?; - let mut archive = DirectoryContents::::new(); + let mut archive = DirectoryContents::::new(); // manifest.json let manifest_raw = reader.manifest().await?; @@ -88,21 +56,21 @@ impl S9pk> { let license: Arc<[u8]> = reader.license().await?.to_vec().await?.into(); archive.insert_path( "LICENSE.md", - Entry::file(CompatSource::Buffered(license.into())), + Entry::file(PackSource::Buffered(license.into())), )?; // instructions.md let instructions: Arc<[u8]> = reader.instructions().await?.to_vec().await?.into(); archive.insert_path( "instructions.md", - Entry::file(CompatSource::Buffered(instructions.into())), + Entry::file(PackSource::Buffered(instructions.into())), )?; // icon.md let icon: Arc<[u8]> = reader.icon().await?.to_vec().await?.into(); archive.insert_path( format!("icon.{}", manifest.assets.icon_type()), - Entry::file(CompatSource::Buffered(icon.into())), + Entry::file(PackSource::Buffered(icon.into())), )?; // images @@ -122,7 +90,9 @@ impl S9pk> { .invoke(ErrorKind::Docker) .await?; for (image, system) in &images { - new_manifest.images.insert(image.clone()); + let mut image_config = new_manifest.images.remove(image).unwrap_or_default(); + image_config.arch.insert(arch.as_str().into()); + new_manifest.images.insert(image.clone(), image_config); let sqfs_path = images_dir.join(image).with_extension("squashfs"); let image_name = if *system { format!("start9/{}:latest", image) @@ -190,21 +160,21 @@ impl S9pk> { .join(&arch) .join(&image) .with_extension("squashfs"), - Entry::file(CompatSource::File(sqfs_path)), + Entry::file(PackSource::File(sqfs_path)), )?; archive.insert_path( Path::new("images") .join(&arch) .join(&image) .with_extension("env"), - Entry::file(CompatSource::Buffered(Vec::from(env).into())), + Entry::file(PackSource::Buffered(Vec::from(env).into())), )?; archive.insert_path( Path::new("images") .join(&arch) .join(&image) .with_extension("json"), - Entry::file(CompatSource::Buffered( + Entry::file(PackSource::Buffered( serde_json::to_vec(&serde_json::json!({ "workdir": workdir })) @@ -240,7 +210,7 @@ impl S9pk> { .await?; archive.insert_path( Path::new("assets").join(&asset_id), - Entry::file(CompatSource::File(sqfs_path)), + Entry::file(PackSource::File(sqfs_path)), )?; } @@ -267,12 +237,12 @@ impl S9pk> { .await?; archive.insert_path( Path::new("javascript.squashfs"), - Entry::file(CompatSource::File(sqfs_path)), + Entry::file(PackSource::File(sqfs_path)), )?; archive.insert_path( "manifest.json", - Entry::file(CompatSource::Buffered( + Entry::file(PackSource::Buffered( serde_json::to_vec::(&new_manifest) .with_kind(ErrorKind::Serialization)? .into(), @@ -289,7 +259,6 @@ impl S9pk> { Ok(S9pk::deserialize( &MultiCursorFile::from(File::open(destination.as_ref()).await?), None, - false, ) .await?) } @@ -310,7 +279,7 @@ impl From for Manifest { marketing_site: value.marketing_site.unwrap_or_else(|| default_url.clone()), donation_url: value.donation_url, description: value.description, - images: BTreeSet::new(), + images: BTreeMap::new(), assets: value .volumes .iter() diff --git a/core/startos/src/s9pk/v2/manifest.rs b/core/startos/src/s9pk/v2/manifest.rs index b5ada621b..77e48c126 100644 --- a/core/startos/src/s9pk/v2/manifest.rs +++ b/core/startos/src/s9pk/v2/manifest.rs @@ -1,10 +1,11 @@ use std::collections::{BTreeMap, BTreeSet}; +use std::path::Path; use color_eyre::eyre::eyre; use helpers::const_true; use imbl_value::InternedString; pub use models::PackageId; -use models::{ImageId, VolumeId}; +use models::{mime, ImageId, VolumeId}; use serde::{Deserialize, Serialize}; use ts_rs::TS; use url::Url; @@ -12,6 +13,9 @@ use url::Url; use crate::dependencies::Dependencies; use crate::prelude::*; 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::serde::Regex; use crate::util::VersionString; use crate::version::{Current, VersionT}; @@ -42,7 +46,7 @@ pub struct Manifest { #[ts(type = "string | null")] pub donation_url: Option, pub description: Description, - pub images: BTreeSet, + pub images: BTreeMap, pub assets: BTreeSet, // TODO: AssetsId pub volumes: BTreeSet, #[serde(default)] @@ -59,6 +63,83 @@ pub struct Manifest { #[serde(default = "const_true")] pub has_config: bool, } +impl Manifest { + pub fn validate_for<'a, T: Clone>( + &self, + arch: Option<&str>, + archive: &'a DirectoryContents, + ) -> Result { + let mut expected = Expected::new(archive); + expected.check_file("manifest.json")?; + expected.check_stem("icon", |ext| { + ext.and_then(|e| e.to_str()) + .and_then(mime) + .map_or(false, |mime| mime.starts_with("image/")) + })?; + expected.check_file("LICENSE.md")?; + expected.check_file("instructions.md")?; + expected.check_file("javascript.squashfs")?; + for assets in &self.assets { + expected.check_file(Path::new("assets").join(assets).with_extension("squashfs"))?; + } + for (image_id, config) in &self.images { + let mut check_arch = |arch: &str| { + let mut arch = arch; + if let Err(e) = expected.check_file( + Path::new("images") + .join(arch) + .join(image_id) + .with_extension("squashfs"), + ) { + if let Some(emulate_as) = &config.emulate_missing_as { + expected.check_file( + Path::new("images") + .join(arch) + .join(image_id) + .with_extension("squashfs"), + )?; + arch = &**emulate_as; + } else { + return Err(e); + } + } + expected.check_file( + Path::new("images") + .join(arch) + .join(image_id) + .with_extension("json"), + )?; + expected.check_file( + Path::new("images") + .join(arch) + .join(image_id) + .with_extension("env"), + )?; + Ok(()) + }; + if let Some(arch) = arch { + check_arch(arch)?; + } else if let Some(arches) = &self.hardware_requirements.arch { + for arch in arches { + check_arch(arch)?; + } + } else if let Some(arch) = config.emulate_missing_as.as_deref() { + if !config.arch.contains(arch) { + return Err(Error::new( + eyre!("`emulateMissingAs` must match an included `arch`"), + ErrorKind::ParseS9pk, + )); + } + for arch in &config.arch { + check_arch(&arch)?; + } + } else { + return Err(Error::new(eyre!("`emulateMissingAs` required for all images if no `arch` specified in `hardwareRequirements`"), ErrorKind::ParseS9pk)); + } + } + Ok(expected.into_filter()) + } +} #[derive(Clone, Debug, Default, Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] diff --git a/core/startos/src/s9pk/v2/mod.rs b/core/startos/src/s9pk/v2/mod.rs index e206c8553..a1183efa8 100644 --- a/core/startos/src/s9pk/v2/mod.rs +++ b/core/startos/src/s9pk/v2/mod.rs @@ -14,7 +14,8 @@ use crate::s9pk::merkle_archive::sink::Sink; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::{ArchiveSource, DynFileSource, FileSource, Section}; use crate::s9pk::merkle_archive::{Entry, MerkleArchive}; -use crate::ARCH; +use crate::s9pk::v2::pack::{ImageSource, PackSource}; +use crate::util::io::TmpDir; const MAGIC_AND_VERSION: &[u8] = &[0x3b, 0x3b, 0x02]; @@ -22,6 +23,7 @@ pub const SIG_CONTEXT: &str = "s9pk"; pub mod compat; pub mod manifest; +pub mod pack; /** / @@ -34,10 +36,14 @@ pub mod manifest; │ └── .squashfs (xN) └── images └── + ├── .json (xN) ├── .env (xN) └── .squashfs (xN) */ +// this sorts the s9pk to optimize such that the parts that are used first appear earlier in the s9pk +// this is useful for manipulating an s9pk while partially downloaded on a source that does not support +// random access fn priority(s: &str) -> Option { match s { "manifest.json" => Some(0), @@ -51,26 +57,6 @@ fn priority(s: &str) -> Option { } } -fn filter(p: &Path) -> bool { - match p.iter().count() { - 1 if p.file_name() == Some(OsStr::new("manifest.json")) => true, - 1 if p.file_stem() == Some(OsStr::new("icon")) => true, - 1 if p.file_name() == Some(OsStr::new("LICENSE.md")) => true, - 1 if p.file_name() == Some(OsStr::new("instructions.md")) => true, - 1 if p.file_name() == Some(OsStr::new("javascript.squashfs")) => true, - 1 if p.file_name() == Some(OsStr::new("assets")) => true, - 1 if p.file_name() == Some(OsStr::new("images")) => true, - 2 if p.parent() == Some(Path::new("assets")) => { - p.extension().map_or(false, |ext| ext == "squashfs") - } - 2 if p.parent() == Some(Path::new("images")) => p.file_name() == Some(OsStr::new(&*ARCH)), - 3 if p.parent() == Some(&*Path::new("images").join(&*ARCH)) => p - .extension() - .map_or(false, |ext| ext == "squashfs" || ext == "env"), - _ => false, - } -} - #[derive(Clone)] pub struct S9pk> { pub manifest: Manifest, @@ -108,6 +94,11 @@ impl S9pk { }) } + pub fn validate_and_filter(&mut self, arch: Option<&str>) -> Result<(), Error> { + let filter = self.manifest.validate_for(arch, self.archive.contents())?; + filter.keep_checked(self.archive.contents_mut()) + } + pub async fn icon(&self) -> Result<(InternedString, FileContents), Error> { let mut best_icon = None; for (path, icon) in self @@ -174,12 +165,37 @@ impl S9pk { } } +impl + FileSource + Clone> S9pk { + pub async fn load_images(&mut self, tmpdir: &TmpDir) -> Result<(), Error> { + let id = &self.manifest.id; + let version = &self.manifest.version; + for (image_id, image_config) in &mut self.manifest.images { + self.manifest_dirty = true; + for arch in &image_config.arch { + image_config + .source + .load( + tmpdir, + id, + version, + image_id, + arch, + self.archive.contents_mut(), + ) + .await?; + } + image_config.source = ImageSource::Packed; + } + + Ok(()) + } +} + impl S9pk> { #[instrument(skip_all)] pub async fn deserialize( source: &S, commitment: Option<&MerkleArchiveCommitment>, - apply_filter: bool, ) -> Result { use tokio::io::AsyncReadExt; @@ -201,10 +217,6 @@ impl S9pk> { let mut archive = MerkleArchive::deserialize(source, SIG_CONTEXT, &mut header, commitment).await?; - if apply_filter { - archive.filter(filter)?; - } - archive.sort_by(|a, b| match (priority(a), priority(b)) { (Some(a), Some(b)) => a.cmp(&b), (Some(_), None) => std::cmp::Ordering::Less, @@ -216,15 +228,11 @@ impl S9pk> { } } impl S9pk { - pub async fn from_file(file: File, apply_filter: bool) -> Result { - Self::deserialize(&MultiCursorFile::from(file), None, apply_filter).await + pub async fn from_file(file: File) -> Result { + Self::deserialize(&MultiCursorFile::from(file), None).await } - pub async fn open( - path: impl AsRef, - id: Option<&PackageId>, - apply_filter: bool, - ) -> Result { - let res = Self::from_file(tokio::fs::File::open(path).await?, apply_filter).await?; + pub async fn open(path: impl AsRef, id: Option<&PackageId>) -> Result { + let res = Self::from_file(tokio::fs::File::open(path).await?).await?; if let Some(id) = id { ensure_code!( &res.as_manifest().id == id, diff --git a/core/startos/src/s9pk/v2/pack.rs b/core/startos/src/s9pk/v2/pack.rs new file mode 100644 index 000000000..18fe0472b --- /dev/null +++ b/core/startos/src/s9pk/v2/pack.rs @@ -0,0 +1,536 @@ +use std::collections::BTreeSet; +use std::ffi::OsStr; +use std::io::Cursor; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use clap::Parser; +use futures::future::{ready, BoxFuture}; +use futures::{FutureExt, TryStreamExt}; +use imbl_value::InternedString; +use models::{ImageId, PackageId, VersionString}; +use serde::{Deserialize, Serialize}; +use tokio::fs::File; +use tokio::io::AsyncRead; +use tokio::process::Command; +use tokio::sync::OnceCell; +use tokio_stream::wrappers::ReadDirStream; +use ts_rs::TS; + +use crate::context::CliContext; +use crate::prelude::*; +use crate::rpc_continuations::Guid; +use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; +use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; +use crate::s9pk::merkle_archive::source::{ + into_dyn_read, ArchiveSource, DynFileSource, FileSource, +}; +use crate::s9pk::merkle_archive::{Entry, MerkleArchive}; +use crate::s9pk::v2::SIG_CONTEXT; +use crate::s9pk::S9pk; +use crate::util::io::TmpDir; +use crate::util::Invoke; + +#[cfg(not(feature = "docker"))] +pub const CONTAINER_TOOL: &str = "podman"; + +#[cfg(feature = "docker")] +pub const CONTAINER_TOOL: &str = "docker"; + +pub struct SqfsDir { + path: PathBuf, + tmpdir: Arc, + sqfs: OnceCell, +} +impl SqfsDir { + pub fn new(path: PathBuf, tmpdir: Arc) -> Self { + Self { + path, + tmpdir, + sqfs: OnceCell::new(), + } + } + async fn file(&self) -> Result<&MultiCursorFile, Error> { + self.sqfs + .get_or_try_init(|| async move { + let guid = Guid::new(); + let path = self.tmpdir.join(guid.as_ref()).with_extension("squashfs"); + let mut cmd = Command::new("mksquashfs"); + if self.path.extension().and_then(|s| s.to_str()) == Some("tar") { + cmd.arg("-tar"); + } + cmd.arg(&self.path) + .arg(&path) + .invoke(ErrorKind::Filesystem) + .await?; + Ok(MultiCursorFile::from( + File::open(&path) + .await + .with_ctx(|_| (ErrorKind::Filesystem, path.display()))?, + )) + }) + .await + } +} + +#[derive(Clone)] +pub enum PackSource { + Buffered(Arc<[u8]>), + File(PathBuf), + Squashfs(Arc), +} +impl FileSource for PackSource { + type Reader = Box; + async fn size(&self) -> Result { + match self { + Self::Buffered(a) => Ok(a.len() as u64), + Self::File(f) => Ok(tokio::fs::metadata(f) + .await + .with_ctx(|_| (ErrorKind::Filesystem, f.display()))? + .len()), + Self::Squashfs(dir) => dir + .file() + .await + .with_ctx(|_| (ErrorKind::Filesystem, dir.path.display()))? + .size() + .await + .or_not_found("file metadata"), + } + } + async fn reader(&self) -> Result { + match self { + Self::Buffered(a) => Ok(into_dyn_read(Cursor::new(a.clone()))), + Self::File(f) => Ok(into_dyn_read( + File::open(f) + .await + .with_ctx(|_| (ErrorKind::Filesystem, f.display()))?, + )), + Self::Squashfs(dir) => dir.file().await?.fetch_all().await.map(into_dyn_read), + } + } +} +impl From for DynFileSource { + fn from(value: PackSource) -> Self { + DynFileSource::new(value) + } +} + +#[derive(Deserialize, Serialize, Parser)] +pub struct PackParams { + pub path: Option, + #[arg(short = 'o', long = "output")] + pub output: Option, + #[arg(long = "javascript")] + pub javascript: Option, + #[arg(long = "icon")] + pub icon: Option, + #[arg(long = "license")] + pub license: Option, + #[arg(long = "instructions")] + pub instructions: Option, + #[arg(long = "assets")] + pub assets: Option, +} +impl PackParams { + fn path(&self) -> &Path { + self.path.as_deref().unwrap_or(Path::new(".")) + } + fn output(&self, id: &PackageId) -> PathBuf { + self.output + .as_ref() + .cloned() + .unwrap_or_else(|| self.path().join(id).with_extension("s9pk")) + } + fn javascript(&self) -> PathBuf { + self.javascript + .as_ref() + .cloned() + .unwrap_or_else(|| self.path().join("javascript")) + } + async fn icon(&self) -> Result { + if let Some(icon) = &self.icon { + Ok(icon.clone()) + } else { + ReadDirStream::new(tokio::fs::read_dir(self.path()).await?).try_filter(|x| ready(x.path().file_stem() == Some(OsStr::new("icon")))).map_err(Error::from).try_fold(Err(Error::new(eyre!("icon not found"), ErrorKind::NotFound)), |acc, x| async move { match acc { + Ok(_) => Err(Error::new(eyre!("multiple icons found in working directory, please specify which to use with `--icon`"), ErrorKind::InvalidRequest)), + Err(e) => Ok({ + let path = x.path(); + if path.file_stem().and_then(|s| s.to_str()) == Some("icon") { + Ok(path) + } else { + Err(e) + } + }) + }}).await? + } + } + fn license(&self) -> PathBuf { + self.license + .as_ref() + .cloned() + .unwrap_or_else(|| self.path().join("LICENSE.md")) + } + fn instructions(&self) -> PathBuf { + self.instructions + .as_ref() + .cloned() + .unwrap_or_else(|| self.path().join("instructions.md")) + } + fn assets(&self) -> PathBuf { + self.assets + .as_ref() + .cloned() + .unwrap_or_else(|| self.path().join("assets")) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct ImageConfig { + pub source: ImageSource, + #[ts(type = "string[]")] + pub arch: BTreeSet, + #[ts(type = "string | null")] + pub emulate_missing_as: Option, +} +impl Default for ImageConfig { + fn default() -> Self { + Self { + source: ImageSource::Packed, + arch: BTreeSet::new(), + emulate_missing_as: None, + } + } +} + +#[derive(Parser)] +struct CliImageConfig { + #[arg(long, conflicts_with("docker-tag"))] + docker_build: bool, + #[arg(long, requires("docker-build"))] + dockerfile: Option, + #[arg(long, requires("docker-build"))] + workdir: Option, + #[arg(long, conflicts_with_all(["dockerfile", "workdir"]))] + docker_tag: Option, + #[arg(long)] + arch: Vec, + #[arg(long)] + emulate_missing_as: Option, +} +impl TryFrom for ImageConfig { + type Error = clap::Error; + fn try_from(value: CliImageConfig) -> Result { + let res = Self { + source: if value.docker_build { + ImageSource::DockerBuild { + dockerfile: value.dockerfile, + workdir: value.workdir, + } + } else if let Some(tag) = value.docker_tag { + ImageSource::DockerTag(tag) + } else { + ImageSource::Packed + }, + arch: value.arch.into_iter().collect(), + emulate_missing_as: value.emulate_missing_as, + }; + res.emulate_missing_as + .as_ref() + .map(|a| { + if !res.arch.contains(a) { + Err(clap::Error::raw( + clap::error::ErrorKind::InvalidValue, + "`emulate-missing-as` must match one of the provided `arch`es", + )) + } else { + Ok(()) + } + }) + .transpose()?; + Ok(res) + } +} +impl clap::Args for ImageConfig { + fn augment_args(cmd: clap::Command) -> clap::Command { + CliImageConfig::augment_args(cmd) + } + fn augment_args_for_update(cmd: clap::Command) -> clap::Command { + CliImageConfig::augment_args_for_update(cmd) + } +} +impl clap::FromArgMatches for ImageConfig { + fn from_arg_matches(matches: &clap::ArgMatches) -> Result { + Self::try_from(CliImageConfig::from_arg_matches(matches)?) + } + fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> { + *self = Self::try_from(CliImageConfig::from_arg_matches(matches)?)?; + Ok(()) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub enum ImageSource { + Packed, + #[serde(rename_all = "camelCase")] + DockerBuild { + workdir: Option, + dockerfile: Option, + }, + DockerTag(String), +} +impl ImageSource { + #[instrument(skip_all)] + pub fn load<'a, S: From + FileSource + Clone>( + &'a self, + tmpdir: &'a TmpDir, + id: &'a PackageId, + version: &'a VersionString, + image_id: &'a ImageId, + arch: &'a str, + into: &'a mut DirectoryContents, + ) -> BoxFuture<'a, Result<(), Error>> { + #[derive(Deserialize)] + #[serde(rename_all = "PascalCase")] + struct DockerImageConfig { + env: Vec, + #[serde(default)] + working_dir: PathBuf, + #[serde(default)] + user: String, + } + async move { + match self { + ImageSource::Packed => Ok(()), + ImageSource::DockerBuild { + workdir, + dockerfile, + } => { + let workdir = workdir.as_deref().unwrap_or(Path::new(".")); + let dockerfile = dockerfile + .clone() + .unwrap_or_else(|| workdir.join("Dockerfile")); + let docker_platform = if arch == "x86_64" { + "--platform=linux/amd64".to_owned() + } else if arch == "aarch64" { + "--platform=linux/arm64".to_owned() + } else { + format!("--platform=linux/{arch}") + }; + // docker buildx build ${path} -o type=image,name=start9/${id} + let tag = format!("start9/{id}/{image_id}:{version}"); + Command::new(CONTAINER_TOOL) + .arg("build") + .arg(workdir) + .arg("-f") + .arg(dockerfile) + .arg("-t") + .arg(&tag) + .arg(&docker_platform) + .arg("-o") + .arg("type=image") + .capture(false) + .invoke(ErrorKind::Docker) + .await?; + ImageSource::DockerTag(tag.clone()) + .load(tmpdir, id, version, image_id, arch, into) + .await?; + Command::new(CONTAINER_TOOL) + .arg("rmi") + .arg("-f") + .arg(&tag) + .invoke(ErrorKind::Docker) + .await?; + Ok(()) + } + ImageSource::DockerTag(tag) => { + let docker_platform = if arch == "x86_64" { + "--platform=linux/amd64".to_owned() + } else if arch == "aarch64" { + "--platform=linux/arm64".to_owned() + } else { + format!("--platform=linux/{arch}") + }; + let mut inspect_cmd = Command::new(CONTAINER_TOOL); + inspect_cmd + .arg("image") + .arg("inspect") + .arg("--format") + .arg("{{json .Config}}") + .arg(&tag); + let inspect_res = match inspect_cmd.invoke(ErrorKind::Docker).await { + Ok(a) => a, + Err(e) + if { + let msg = e.source.to_string(); + #[cfg(feature = "docker")] + let matches = msg.contains("No such image:"); + #[cfg(not(feature = "docker"))] + let matches = msg.contains(": image not known"); + matches + } => + { + Command::new(CONTAINER_TOOL) + .arg("pull") + .arg(&docker_platform) + .arg(tag) + .capture(false) + .invoke(ErrorKind::Docker) + .await?; + inspect_cmd.invoke(ErrorKind::Docker).await? + } + Err(e) => return Err(e), + }; + let config = serde_json::from_slice::(&inspect_res) + .with_kind(ErrorKind::Deserialization)?; + let base_path = Path::new("images").join(arch).join(image_id); + into.insert_path( + base_path.with_extension("json"), + Entry::file( + PackSource::Buffered( + serde_json::to_vec(&ImageMetadata { + workdir: if config.working_dir == Path::new("") { + "/".into() + } else { + config.working_dir + }, + user: if config.user.is_empty() { + "root".into() + } else { + config.user.into() + }, + }) + .with_kind(ErrorKind::Serialization)? + .into(), + ) + .into(), + ), + )?; + into.insert_path( + base_path.with_extension("env"), + Entry::file( + PackSource::Buffered(config.env.join("\n").into_bytes().into()).into(), + ), + )?; + let dest = tmpdir.join(Guid::new().as_ref()).with_extension("squashfs"); + let container = String::from_utf8( + Command::new(CONTAINER_TOOL) + .arg("create") + .arg(&docker_platform) + .arg(&tag) + .invoke(ErrorKind::Docker) + .await?, + )?; + Command::new(CONTAINER_TOOL) + .arg("export") + .arg(container.trim()) + .pipe(Command::new("mksquashfs").arg("-").arg(&dest).arg("-tar")) + .capture(false) + .invoke(ErrorKind::Docker) + .await?; + Command::new(CONTAINER_TOOL) + .arg("rm") + .arg(container.trim()) + .invoke(ErrorKind::Docker) + .await?; + into.insert_path( + base_path.with_extension("squashfs"), + Entry::file(PackSource::File(dest).into()), + )?; + + Ok(()) + } + } + } + .boxed() + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct ImageMetadata { + pub workdir: PathBuf, + #[ts(type = "string")] + pub user: InternedString, +} + +#[instrument(skip_all)] +pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> { + let tmpdir = Arc::new(TmpDir::new().await?); + let mut files = DirectoryContents::::new(); + let js_dir = params.javascript(); + let manifest: Arc<[u8]> = Command::new("node") + .arg("-e") + .arg(format!( + "console.log(JSON.stringify(require('{}/index.js').manifest))", + js_dir.display() + )) + .invoke(ErrorKind::Javascript) + .await? + .into(); + files.insert( + "manifest.json".into(), + Entry::file(PackSource::Buffered(manifest.clone())), + ); + let icon = params.icon().await?; + let icon_ext = icon + .extension() + .or_not_found("icon file extension")? + .to_string_lossy(); + files.insert( + InternedString::from_display(&lazy_format!("icon.{}", icon_ext)), + Entry::file(PackSource::File(icon)), + ); + files.insert( + "LICENSE.md".into(), + Entry::file(PackSource::File(params.license())), + ); + files.insert( + "instructions.md".into(), + Entry::file(PackSource::File(params.instructions())), + ); + files.insert( + "javascript.squashfs".into(), + Entry::file(PackSource::Squashfs(Arc::new(SqfsDir::new( + js_dir, + tmpdir.clone(), + )))), + ); + + let mut s9pk = S9pk::new( + MerkleArchive::new(files, ctx.developer_key()?.clone(), SIG_CONTEXT), + None, + ) + .await?; + + let assets_dir = params.assets(); + for assets in s9pk.as_manifest().assets.clone() { + s9pk.as_archive_mut().contents_mut().insert_path( + Path::new("assets").join(&assets).with_extension("squashfs"), + Entry::file(PackSource::Squashfs(Arc::new(SqfsDir::new( + assets_dir.join(&assets), + tmpdir.clone(), + )))), + )?; + } + + s9pk.load_images(&*tmpdir).await?; + + s9pk.validate_and_filter(None)?; + + s9pk.serialize( + &mut File::create(params.output(&s9pk.as_manifest().id)).await?, + false, + ) + .await?; + + drop(s9pk); + + tmpdir.gc().await?; + + Ok(()) +} diff --git a/core/startos/src/service/action.rs b/core/startos/src/service/action.rs index 5f97685ae..6c5ac4eab 100644 --- a/core/startos/src/service/action.rs +++ b/core/startos/src/service/action.rs @@ -4,6 +4,7 @@ use models::{ActionId, ProcedureName}; use crate::action::ActionResult; use crate::prelude::*; +use crate::rpc_continuations::Guid; use crate::service::config::GetConfig; use crate::service::dependencies::DependencyConfig; use crate::service::{Service, ServiceActor}; @@ -23,13 +24,18 @@ impl Handler for ServiceActor { } async fn handle( &mut self, - Action { id, input }: Action, + id: Guid, + Action { + id: action_id, + input, + }: Action, _: &BackgroundJobQueue, ) -> Self::Response { let container = &self.0.persistent_container; container .execute::( - ProcedureName::RunAction(id), + id, + ProcedureName::RunAction(action_id), input, Some(Duration::from_secs(30)), ) @@ -39,7 +45,20 @@ impl Handler for ServiceActor { } impl Service { - pub async fn action(&self, id: ActionId, input: Value) -> Result { - self.actor.send(Action { id, input }).await? + pub async fn action( + &self, + id: Guid, + action_id: ActionId, + input: Value, + ) -> Result { + self.actor + .send( + id, + Action { + id: action_id, + input, + }, + ) + .await? } } diff --git a/core/startos/src/service/config.rs b/core/startos/src/service/config.rs index 0f166eedb..faa70fc41 100644 --- a/core/startos/src/service/config.rs +++ b/core/startos/src/service/config.rs @@ -5,6 +5,7 @@ use models::ProcedureName; use crate::config::action::ConfigRes; use crate::config::ConfigureContext; use crate::prelude::*; +use crate::rpc_continuations::Guid; use crate::service::dependencies::DependencyConfig; use crate::service::{Service, ServiceActor}; use crate::util::actor::background::BackgroundJobQueue; @@ -19,6 +20,7 @@ impl Handler for ServiceActor { } async fn handle( &mut self, + id: Guid, Configure(ConfigureContext { timeout, config }): Configure, _: &BackgroundJobQueue, ) -> Self::Response { @@ -26,7 +28,7 @@ impl Handler for ServiceActor { let package_id = &self.0.id; container - .execute::(ProcedureName::SetConfig, to_value(&config)?, timeout) + .execute::(id, ProcedureName::SetConfig, to_value(&config)?, timeout) .await .with_kind(ErrorKind::ConfigRulesViolation)?; self.0 @@ -52,10 +54,11 @@ impl Handler for ServiceActor { fn conflicts_with(_: &GetConfig) -> ConflictBuilder { ConflictBuilder::nothing().except::() } - async fn handle(&mut self, _: GetConfig, _: &BackgroundJobQueue) -> Self::Response { + async fn handle(&mut self, id: Guid, _: GetConfig, _: &BackgroundJobQueue) -> Self::Response { let container = &self.0.persistent_container; container .execute::( + id, ProcedureName::GetConfig, Value::Null, Some(Duration::from_secs(30)), // TODO timeout @@ -66,10 +69,10 @@ impl Handler for ServiceActor { } impl Service { - pub async fn configure(&self, ctx: ConfigureContext) -> Result<(), Error> { - self.actor.send(Configure(ctx)).await? + pub async fn configure(&self, id: Guid, ctx: ConfigureContext) -> Result<(), Error> { + self.actor.send(id, Configure(ctx)).await? } - pub async fn get_config(&self) -> Result { - self.actor.send(GetConfig).await? + pub async fn get_config(&self, id: Guid) -> Result { + self.actor.send(id, GetConfig).await? } } diff --git a/core/startos/src/service/control.rs b/core/startos/src/service/control.rs index 0443b80b1..7c4bdf815 100644 --- a/core/startos/src/service/control.rs +++ b/core/startos/src/service/control.rs @@ -1,4 +1,5 @@ use crate::prelude::*; +use crate::rpc_continuations::Guid; use crate::service::config::GetConfig; use crate::service::dependencies::DependencyConfig; use crate::service::start_stop::StartStop; @@ -15,7 +16,7 @@ impl Handler for ServiceActor { .except::() .except::() } - async fn handle(&mut self, _: Start, _: &BackgroundJobQueue) -> Self::Response { + async fn handle(&mut self, _: Guid, _: Start, _: &BackgroundJobQueue) -> Self::Response { self.0.persistent_container.state.send_modify(|x| { x.desired_state = StartStop::Start; }); @@ -23,8 +24,8 @@ impl Handler for ServiceActor { } } impl Service { - pub async fn start(&self) -> Result<(), Error> { - self.actor.send(Start).await + pub async fn start(&self, id: Guid) -> Result<(), Error> { + self.actor.send(id, Start).await } } @@ -36,7 +37,7 @@ impl Handler for ServiceActor { .except::() .except::() } - async fn handle(&mut self, _: Stop, _: &BackgroundJobQueue) -> Self::Response { + async fn handle(&mut self, _: Guid, _: Stop, _: &BackgroundJobQueue) -> Self::Response { let mut transition_state = None; self.0.persistent_container.state.send_modify(|x| { x.desired_state = StartStop::Stop; @@ -51,7 +52,7 @@ impl Handler for ServiceActor { } } impl Service { - pub async fn stop(&self) -> Result<(), Error> { - self.actor.send(Stop).await + pub async fn stop(&self, id: Guid) -> Result<(), Error> { + self.actor.send(id, Stop).await } } diff --git a/core/startos/src/service/dependencies.rs b/core/startos/src/service/dependencies.rs index e71b62917..60fd76ad6 100644 --- a/core/startos/src/service/dependencies.rs +++ b/core/startos/src/service/dependencies.rs @@ -4,35 +4,28 @@ use imbl_value::json; use models::{PackageId, ProcedureName}; use crate::prelude::*; -use crate::service::{Service, ServiceActor}; +use crate::rpc_continuations::Guid; +use crate::service::{Service, ServiceActor, ServiceActorSeed}; use crate::util::actor::background::BackgroundJobQueue; use crate::util::actor::{ConflictBuilder, Handler}; use crate::Config; -pub(super) struct DependencyConfig { - dependency_id: PackageId, - remote_config: Option, -} -impl Handler for ServiceActor { - type Response = Result, Error>; - fn conflicts_with(_: &DependencyConfig) -> ConflictBuilder { - ConflictBuilder::nothing() - } - async fn handle( - &mut self, - DependencyConfig { - dependency_id, - remote_config, - }: DependencyConfig, - _: &BackgroundJobQueue, - ) -> Self::Response { - let container = &self.0.persistent_container; +impl ServiceActorSeed { + async fn dependency_config( + &self, + id: Guid, + dependency_id: PackageId, + remote_config: Option, + ) -> Result, Error> { + let container = &self.persistent_container; container .sanboxed::>( + id.clone(), ProcedureName::UpdateDependency(dependency_id.clone()), json!({ "queryResults": container .execute::( + id, ProcedureName::QueryDependency(dependency_id), Value::Null, Some(Duration::from_secs(30)), @@ -49,17 +42,45 @@ impl Handler for ServiceActor { } } +pub(super) struct DependencyConfig { + dependency_id: PackageId, + remote_config: Option, +} +impl Handler for ServiceActor { + type Response = Result, Error>; + fn conflicts_with(_: &DependencyConfig) -> ConflictBuilder { + ConflictBuilder::nothing() + } + async fn handle( + &mut self, + id: Guid, + DependencyConfig { + dependency_id, + remote_config, + }: DependencyConfig, + _: &BackgroundJobQueue, + ) -> Self::Response { + self.0 + .dependency_config(id, dependency_id, remote_config) + .await + } +} + impl Service { pub async fn dependency_config( &self, + id: Guid, dependency_id: PackageId, remote_config: Option, ) -> Result, Error> { self.actor - .send(DependencyConfig { - dependency_id, - remote_config, - }) + .send( + id, + DependencyConfig { + dependency_id, + remote_config, + }, + ) .await? } } diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index da641468b..6588de836 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -1,4 +1,5 @@ -use std::sync::Arc; +use std::ops::Deref; +use std::sync::{Arc, Weak}; use std::time::Duration; use chrono::{DateTime, Utc}; @@ -68,13 +69,87 @@ pub enum LoadDisposition { Undo, } +pub struct ServiceRef(Arc); +impl ServiceRef { + pub fn weak(&self) -> Weak { + Arc::downgrade(&self.0) + } + pub async fn uninstall( + self, + target_version: Option, + ) -> Result<(), Error> { + self.seed + .persistent_container + .execute( + Guid::new(), + ProcedureName::Uninit, + to_value(&target_version)?, + None, + ) // TODO timeout + .await?; + let id = self.seed.persistent_container.s9pk.as_manifest().id.clone(); + let ctx = self.seed.ctx.clone(); + self.shutdown().await?; + if target_version.is_none() { + ctx.db + .mutate(|d| d.as_public_mut().as_package_data_mut().remove(&id)) + .await?; + } + Ok(()) + } + pub async fn shutdown(self) -> Result<(), Error> { + if let Some((hdl, shutdown)) = self.seed.persistent_container.rpc_server.send_replace(None) + { + self.seed + .persistent_container + .rpc_client + .request(rpc::Exit, Empty {}) + .await?; + shutdown.shutdown(); + hdl.await.with_kind(ErrorKind::Cancelled)?; + } + let service = Arc::try_unwrap(self.0).map_err(|_| { + Error::new( + eyre!("ServiceActor held somewhere after actor shutdown"), + ErrorKind::Unknown, + ) + })?; + service + .actor + .shutdown(crate::util::actor::PendingMessageStrategy::FinishAll { timeout: None }) // TODO timeout + .await; + Arc::try_unwrap(service.seed) + .map_err(|_| { + Error::new( + eyre!("ServiceActorSeed held somewhere after actor shutdown"), + ErrorKind::Unknown, + ) + })? + .persistent_container + .exit() + .await?; + Ok(()) + } +} +impl Deref for ServiceRef { + type Target = Service; + fn deref(&self) -> &Self::Target { + &*self.0 + } +} +impl From for ServiceRef { + fn from(value: Service) -> Self { + Self(Arc::new(value)) + } +} + pub struct Service { actor: ConcurrentActor, seed: Arc, } impl Service { #[instrument(skip_all)] - async fn new(ctx: RpcContext, s9pk: S9pk, start: StartStop) -> Result { + async fn new(ctx: RpcContext, s9pk: S9pk, start: StartStop) -> Result { let id = s9pk.as_manifest().id.clone(); let persistent_container = PersistentContainer::new( &ctx, s9pk, @@ -89,13 +164,17 @@ impl Service { ctx, synchronized: Arc::new(Notify::new()), }); - seed.persistent_container - .init(Arc::downgrade(&seed)) - .await?; - Ok(Self { + let service: ServiceRef = Self { actor: ConcurrentActor::new(ServiceActor(seed.clone())), seed, - }) + } + .into(); + service + .seed + .persistent_container + .init(service.weak()) + .await?; + Ok(service) } #[instrument(skip_all)] @@ -103,7 +182,7 @@ impl Service { ctx: &RpcContext, id: &PackageId, disposition: LoadDisposition, - ) -> Result, Error> { + ) -> Result, Error> { let handle_installed = { let ctx = ctx.clone(); move |s9pk: S9pk, i: Model| async move { @@ -137,7 +216,7 @@ impl Service { match entry.as_state_info().as_match() { PackageStateMatchModelRef::Installing(_) => { if disposition == LoadDisposition::Retry { - if let Ok(s9pk) = S9pk::open(s9pk_path, Some(id), true).await.map_err(|e| { + if let Ok(s9pk) = S9pk::open(s9pk_path, Some(id)).await.map_err(|e| { tracing::error!("Error opening s9pk for install: {e}"); tracing::debug!("{e:?}") }) { @@ -170,7 +249,7 @@ impl Service { && progress == &Progress::Complete(true) }) { - if let Ok(s9pk) = S9pk::open(&s9pk_path, Some(id), true).await.map_err(|e| { + if let Ok(s9pk) = S9pk::open(&s9pk_path, Some(id)).await.map_err(|e| { tracing::error!("Error opening s9pk for update: {e}"); tracing::debug!("{e:?}") }) { @@ -189,7 +268,7 @@ impl Service { } } } - let s9pk = S9pk::open(s9pk_path, Some(id), true).await?; + let s9pk = S9pk::open(s9pk_path, Some(id)).await?; ctx.db .mutate({ |db| { @@ -214,7 +293,7 @@ impl Service { handle_installed(s9pk, entry).await } PackageStateMatchModelRef::Removing(_) | PackageStateMatchModelRef::Restoring(_) => { - if let Ok(s9pk) = S9pk::open(s9pk_path, Some(id), true).await.map_err(|e| { + if let Ok(s9pk) = S9pk::open(s9pk_path, Some(id)).await.map_err(|e| { tracing::error!("Error opening s9pk for removal: {e}"); tracing::debug!("{e:?}") }) { @@ -225,7 +304,7 @@ impl Service { tracing::debug!("{e:?}") }) { - match service.uninstall(None).await { + match ServiceRef::from(service).uninstall(None).await { Err(e) => { tracing::error!("Error uninstalling service: {e}"); tracing::debug!("{e:?}") @@ -242,7 +321,7 @@ impl Service { Ok(None) } PackageStateMatchModelRef::Installed(_) => { - handle_installed(S9pk::open(s9pk_path, Some(id), true).await?, entry).await + handle_installed(S9pk::open(s9pk_path, Some(id)).await?, entry).await } PackageStateMatchModelRef::Error(e) => Err(Error::new( eyre!("Failed to parse PackageDataEntry, found {e:?}"), @@ -257,7 +336,7 @@ impl Service { s9pk: S9pk, src_version: Option, progress: Option, - ) -> Result { + ) -> Result { let manifest = s9pk.as_manifest().clone(); let developer_key = s9pk.as_archive().signer(); let icon = s9pk.icon_data_url().await?; @@ -265,7 +344,12 @@ impl Service { service .seed .persistent_container - .execute(ProcedureName::Init, to_value(&src_version)?, None) // TODO timeout + .execute( + Guid::new(), + ProcedureName::Init, + to_value(&src_version)?, + None, + ) // TODO timeout .await .with_kind(ErrorKind::MigrationFailed)?; // TODO: handle cancellation if let Some(mut progress) = progress { @@ -301,61 +385,21 @@ impl Service { s9pk: S9pk, backup_source: impl GenericMountGuard, progress: Option, - ) -> Result { + ) -> Result { let service = Service::install(ctx.clone(), s9pk, None, progress).await?; service .actor - .send(transition::restore::Restore { - path: backup_source.path().to_path_buf(), - }) + .send( + Guid::new(), + transition::restore::Restore { + path: backup_source.path().to_path_buf(), + }, + ) .await??; Ok(service) } - pub async fn shutdown(self) -> Result<(), Error> { - self.actor - .shutdown(crate::util::actor::PendingMessageStrategy::FinishAll { timeout: None }) // TODO timeout - .await; - if let Some((hdl, shutdown)) = self.seed.persistent_container.rpc_server.send_replace(None) - { - self.seed - .persistent_container - .rpc_client - .request(rpc::Exit, Empty {}) - .await?; - shutdown.shutdown(); - hdl.await.with_kind(ErrorKind::Cancelled)?; - } - Arc::try_unwrap(self.seed) - .map_err(|_| { - Error::new( - eyre!("ServiceActorSeed held somewhere after actor shutdown"), - ErrorKind::Unknown, - ) - })? - .persistent_container - .exit() - .await?; - Ok(()) - } - - pub async fn uninstall(self, target_version: Option) -> Result<(), Error> { - self.seed - .persistent_container - .execute(ProcedureName::Uninit, to_value(&target_version)?, None) // TODO timeout - .await?; - let id = self.seed.persistent_container.s9pk.as_manifest().id.clone(); - let ctx = self.seed.ctx.clone(); - self.shutdown().await?; - if target_version.is_none() { - ctx.db - .mutate(|d| d.as_public_mut().as_package_data_mut().remove(&id)) - .await?; - } - Ok(()) - } - #[instrument(skip_all)] pub async fn backup(&self, guard: impl GenericMountGuard) -> Result<(), Error> { let id = &self.seed.id; @@ -368,9 +412,12 @@ impl Service { .await?; drop(file); self.actor - .send(transition::backup::Backup { - path: guard.path().to_path_buf(), - }) + .send( + Guid::new(), + transition::backup::Backup { + path: guard.path().to_path_buf(), + }, + ) .await??; Ok(()) } diff --git a/core/startos/src/service/persistent_container.rs b/core/startos/src/service/persistent_container.rs index 03203633a..d9d08e5d3 100644 --- a/core/startos/src/service/persistent_container.rs +++ b/core/startos/src/service/persistent_container.rs @@ -6,8 +6,7 @@ use std::time::Duration; use futures::future::ready; use futures::{Future, FutureExt}; use helpers::NonDetachingJoinHandle; -use imbl_value::InternedString; -use models::{ProcedureName, VolumeId}; +use models::{ImageId, ProcedureName, VolumeId}; use rpc_toolkit::{Empty, Server, ShutdownHandle}; use serde::de::DeserializeOwned; use tokio::fs::File; @@ -24,14 +23,15 @@ use crate::disk::mount::filesystem::idmapped::IdMapped; use crate::disk::mount::filesystem::loop_dev::LoopDev; use crate::disk::mount::filesystem::overlayfs::OverlayGuard; use crate::disk::mount::filesystem::{MountType, ReadOnly}; -use crate::disk::mount::guard::MountGuard; +use crate::disk::mount::guard::{GenericMountGuard, MountGuard}; use crate::lxc::{LxcConfig, LxcContainer, HOST_RPC_SERVER_SOCKET}; use crate::net::net_controller::NetService; use crate::prelude::*; +use crate::rpc_continuations::Guid; use crate::s9pk::merkle_archive::source::FileSource; use crate::s9pk::S9pk; use crate::service::start_stop::StartStop; -use crate::service::{rpc, RunningStatus}; +use crate::service::{rpc, RunningStatus, Service}; use crate::util::rpc_client::UnixRpcClient; use crate::util::Invoke; use crate::volume::{asset_dir, data_dir}; @@ -89,7 +89,8 @@ pub struct PersistentContainer { js_mount: MountGuard, volumes: BTreeMap, assets: BTreeMap, - pub(super) overlays: Arc>>, + pub(super) images: BTreeMap>, + pub(super) overlays: Arc>>>>, pub(super) state: Arc>, pub(super) net_service: Mutex, destroyed: bool, @@ -178,14 +179,62 @@ impl PersistentContainer { .await?, ); } + + let mut images = BTreeMap::new(); let image_path = lxc_container.rootfs_dir().join("media/startos/images"); tokio::fs::create_dir_all(&image_path).await?; - for image in &s9pk.as_manifest().images { + for (image, config) in &s9pk.as_manifest().images { + let mut arch = ARCH; + let mut sqfs_path = Path::new("images") + .join(arch) + .join(image) + .with_extension("squashfs"); + if !s9pk + .as_archive() + .contents() + .get_path(&sqfs_path) + .and_then(|e| e.as_file()) + .is_some() + { + arch = if let Some(arch) = config.emulate_missing_as.as_deref() { + arch + } else { + continue; + }; + sqfs_path = Path::new("images") + .join(arch) + .join(image) + .with_extension("squashfs"); + } + let sqfs = s9pk + .as_archive() + .contents() + .get_path(&sqfs_path) + .and_then(|e| e.as_file()) + .or_not_found(sqfs_path.display())?; + let mountpoint = image_path.join(image); + tokio::fs::create_dir_all(&mountpoint).await?; + Command::new("chown") + .arg("100000:100000") + .arg(&mountpoint) + .invoke(ErrorKind::Filesystem) + .await?; + images.insert( + image.clone(), + Arc::new( + MountGuard::mount( + &IdMapped::new(LoopDev::from(&**sqfs), 0, 100000, 65536), + &mountpoint, + ReadOnly, + ) + .await?, + ), + ); let env_filename = Path::new(image.as_ref()).with_extension("env"); if let Some(env) = s9pk .as_archive() .contents() - .get_path(Path::new("images").join(*ARCH).join(&env_filename)) + .get_path(Path::new("images").join(arch).join(&env_filename)) .and_then(|e| e.as_file()) { env.copy(&mut File::create(image_path.join(&env_filename)).await?) @@ -195,7 +244,7 @@ impl PersistentContainer { if let Some(json) = s9pk .as_archive() .contents() - .get_path(Path::new("images").join(*ARCH).join(&json_filename)) + .get_path(Path::new("images").join(arch).join(&json_filename)) .and_then(|e| e.as_file()) { json.copy(&mut File::create(image_path.join(&json_filename)).await?) @@ -215,6 +264,7 @@ impl PersistentContainer { js_mount, volumes, assets, + images, overlays: Arc::new(Mutex::new(BTreeMap::new())), state: Arc::new(watch::channel(ServiceState::new(start)).0), net_service: Mutex::new(net_service), @@ -257,7 +307,7 @@ impl PersistentContainer { } #[instrument(skip_all)] - pub async fn init(&self, seed: Weak) -> Result<(), Error> { + pub async fn init(&self, seed: Weak) -> Result<(), Error> { let socket_server_context = EffectContext::new(seed); let server = Server::new( move || ready(Ok(socket_server_context.clone())), @@ -330,6 +380,7 @@ impl PersistentContainer { let js_mount = self.js_mount.take(); let volumes = std::mem::take(&mut self.volumes); let assets = std::mem::take(&mut self.assets); + let images = std::mem::take(&mut self.images); let overlays = self.overlays.clone(); let lxc_container = self.lxc_container.take(); self.destroyed = true; @@ -352,6 +403,9 @@ impl PersistentContainer { for (_, overlay) in std::mem::take(&mut *overlays.lock().await) { errs.handle(overlay.unmount(true).await); } + for (_, images) in images { + errs.handle(images.unmount().await); + } errs.handle(js_mount.unmount(true).await); if let Some(lxc_container) = lxc_container { errs.handle(lxc_container.exit().await); @@ -378,6 +432,7 @@ impl PersistentContainer { #[instrument(skip_all)] pub async fn start(&self) -> Result<(), Error> { self.execute( + Guid::new(), ProcedureName::StartMain, Value::Null, Some(Duration::from_secs(5)), // TODO @@ -389,7 +444,7 @@ impl PersistentContainer { #[instrument(skip_all)] pub async fn stop(&self) -> Result { let timeout: Option = self - .execute(ProcedureName::StopMain, Value::Null, None) + .execute(Guid::new(), ProcedureName::StopMain, Value::Null, None) .await?; Ok(timeout.map(|a| *a).unwrap_or(Duration::from_secs(30))) } @@ -397,6 +452,7 @@ impl PersistentContainer { #[instrument(skip_all)] pub async fn execute( &self, + id: Guid, name: ProcedureName, input: Value, timeout: Option, @@ -404,7 +460,7 @@ impl PersistentContainer { where O: DeserializeOwned, { - self._execute(name, input, timeout) + self._execute(id, name, input, timeout) .await .and_then(from_value) } @@ -412,6 +468,7 @@ impl PersistentContainer { #[instrument(skip_all)] pub async fn sanboxed( &self, + id: Guid, name: ProcedureName, input: Value, timeout: Option, @@ -419,7 +476,7 @@ impl PersistentContainer { where O: DeserializeOwned, { - self._sandboxed(name, input, timeout) + self._sandboxed(id, name, input, timeout) .await .and_then(from_value) } @@ -427,13 +484,15 @@ impl PersistentContainer { #[instrument(skip_all)] async fn _execute( &self, + id: Guid, name: ProcedureName, input: Value, timeout: Option, ) -> Result { - let fut = self - .rpc_client - .request(rpc::Execute, rpc::ExecuteParams::new(name, input, timeout)); + let fut = self.rpc_client.request( + rpc::Execute, + rpc::ExecuteParams::new(id, name, input, timeout), + ); Ok(if let Some(timeout) = timeout { tokio::time::timeout(timeout, fut) @@ -447,13 +506,15 @@ impl PersistentContainer { #[instrument(skip_all)] async fn _sandboxed( &self, + id: Guid, name: ProcedureName, input: Value, timeout: Option, ) -> Result { - let fut = self - .rpc_client - .request(rpc::Sandbox, rpc::ExecuteParams::new(name, input, timeout)); + let fut = self.rpc_client.request( + rpc::Sandbox, + rpc::ExecuteParams::new(id, name, input, timeout), + ); Ok(if let Some(timeout) = timeout { tokio::time::timeout(timeout, fut) diff --git a/core/startos/src/service/properties.rs b/core/startos/src/service/properties.rs index 3e795207a..3f5201f1d 100644 --- a/core/startos/src/service/properties.rs +++ b/core/startos/src/service/properties.rs @@ -3,6 +3,7 @@ use std::time::Duration; use models::ProcedureName; use crate::prelude::*; +use crate::rpc_continuations::Guid; use crate::service::Service; impl Service { @@ -11,6 +12,7 @@ impl Service { let container = &self.seed.persistent_container; container .execute::( + Guid::new(), ProcedureName::Properties, Value::Null, Some(Duration::from_secs(30)), diff --git a/core/startos/src/service/rpc.rs b/core/startos/src/service/rpc.rs index 65c8b98fe..eff44b2cf 100644 --- a/core/startos/src/service/rpc.rs +++ b/core/startos/src/service/rpc.rs @@ -7,6 +7,7 @@ use rpc_toolkit::Empty; use ts_rs::TS; use crate::prelude::*; +use crate::rpc_continuations::Guid; #[derive(Clone)] pub struct Init; @@ -46,14 +47,21 @@ impl serde::Serialize for Exit { #[derive(Clone, serde::Deserialize, serde::Serialize, TS)] pub struct ExecuteParams { + id: Guid, procedure: String, #[ts(type = "any")] input: Value, timeout: Option, } impl ExecuteParams { - pub fn new(procedure: ProcedureName, input: Value, timeout: Option) -> Self { + pub fn new( + id: Guid, + procedure: ProcedureName, + input: Value, + timeout: Option, + ) -> Self { Self { + id, procedure: procedure.js_function_name(), input, timeout: timeout.map(|d| d.as_millis()), diff --git a/core/startos/src/service/service_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs index aedf4c194..28a611854 100644 --- a/core/startos/src/service/service_effect_handler.rs +++ b/core/startos/src/service/service_effect_handler.rs @@ -7,9 +7,9 @@ use std::str::FromStr; use std::sync::{Arc, Weak}; use clap::builder::ValueParserFactory; -use clap::Parser; +use clap::{CommandFactory, FromArgMatches, Parser}; use emver::VersionRange; -use imbl_value::{json, InternedString}; +use imbl_value::json; use itertools::Itertools; use models::{ ActionId, DataUrl, HealthCheckId, HostId, ImageId, PackageId, ServiceInterfaceId, VolumeId, @@ -25,35 +25,34 @@ use crate::db::model::package::{ ActionMetadata, CurrentDependencies, CurrentDependencyInfo, CurrentDependencyKind, ManifestPreference, }; -use crate::disk::mount::filesystem::idmapped::IdMapped; -use crate::disk::mount::filesystem::loop_dev::LoopDev; use crate::disk::mount::filesystem::overlayfs::OverlayGuard; +use crate::echo; use crate::net::host::address::HostAddress; use crate::net::host::binding::{BindOptions, LanInfo}; use crate::net::host::{Host, HostKind}; use crate::net::service_interface::{AddressInfo, ServiceInterface, ServiceInterfaceType}; use crate::prelude::*; +use crate::rpc_continuations::Guid; use crate::s9pk::merkle_archive::source::http::HttpSource; use crate::s9pk::rpc::SKIP_ENV; use crate::s9pk::S9pk; use crate::service::cli::ContainerCliContext; -use crate::service::ServiceActorSeed; +use crate::service::Service; use crate::status::health_check::HealthCheckResult; use crate::status::MainStatus; use crate::util::clap::FromStrParser; -use crate::util::{new_guid, Invoke}; -use crate::{echo, ARCH}; +use crate::util::Invoke; #[derive(Clone)] -pub(super) struct EffectContext(Weak); +pub(super) struct EffectContext(Weak); impl EffectContext { - pub fn new(seed: Weak) -> Self { - Self(seed) + pub fn new(service: Weak) -> Self { + Self(service) } } impl Context for EffectContext {} impl EffectContext { - fn deref(&self) -> Result, Error> { + fn deref(&self) -> Result, Error> { if let Some(seed) = Weak::upgrade(&self.0) { Ok(seed) } else { @@ -66,11 +65,55 @@ impl EffectContext { } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -struct RpcData { - id: i64, - method: String, - params: Value, +#[serde(rename_all = "camelCase")] +pub struct WithProcedureId { + #[serde(default)] + procedure_id: Guid, + #[serde(flatten)] + rest: T, } +impl FromArgMatches for WithProcedureId { + fn from_arg_matches(matches: &clap::ArgMatches) -> Result { + let rest = T::from_arg_matches(matches)?; + Ok(Self { + procedure_id: matches.get_one("procedure-id").cloned().unwrap_or_default(), + rest, + }) + } + fn from_arg_matches_mut(matches: &mut clap::ArgMatches) -> Result { + let rest = T::from_arg_matches_mut(matches)?; + Ok(Self { + procedure_id: matches.get_one("procedure-id").cloned().unwrap_or_default(), + rest, + }) + } + fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> { + self.rest.update_from_arg_matches(matches)?; + self.procedure_id = matches.get_one("procedure-id").cloned().unwrap_or_default(); + Ok(()) + } + fn update_from_arg_matches_mut( + &mut self, + matches: &mut clap::ArgMatches, + ) -> Result<(), clap::Error> { + self.rest.update_from_arg_matches_mut(matches)?; + self.procedure_id = matches.get_one("procedure-id").cloned().unwrap_or_default(); + Ok(()) + } +} +impl CommandFactory for WithProcedureId { + fn command() -> clap::Command { + T::command_for_update().arg( + clap::Arg::new("procedure-id") + .action(clap::ArgAction::Set) + .value_parser(clap::value_parser!(Guid)), + ) + } + fn command_for_update() -> clap::Command { + Self::command() + } +} + pub fn service_effect_handler() -> ParentHandler { ParentHandler::new() .subcommand("gitInfo", from_fn(|_: C| crate::version::git_info())) @@ -290,6 +333,7 @@ struct MountParams { async fn set_system_smtp(context: EffectContext, data: SetSystemSmtpParams) -> Result<(), Error> { let context = context.deref()?; context + .seed .ctx .db .mutate(|db| { @@ -304,6 +348,7 @@ async fn get_system_smtp( ) -> Result { let context = context.deref()?; let res = context + .seed .ctx .db .peek() @@ -323,7 +368,7 @@ async fn get_system_smtp( } async fn get_container_ip(context: EffectContext, _: Empty) -> Result { let context = context.deref()?; - let net_service = context.persistent_container.net_service.lock().await; + let net_service = context.seed.persistent_container.net_service.lock().await; Ok(net_service.get_ip()) } async fn get_service_port_forward( @@ -333,14 +378,15 @@ async fn get_service_port_forward( let internal_port = data.internal_port as u16; let context = context.deref()?; - let net_service = context.persistent_container.net_service.lock().await; + let net_service = context.seed.persistent_container.net_service.lock().await; net_service.get_ext_port(data.host_id, internal_port) } async fn clear_network_interfaces(context: EffectContext, _: Empty) -> Result<(), Error> { let context = context.deref()?; - let package_id = context.id.clone(); + let package_id = context.seed.id.clone(); context + .seed .ctx .db .mutate(|db| { @@ -369,7 +415,7 @@ async fn export_service_interface( }: ExportServiceInterfaceParams, ) -> Result<(), Error> { let context = context.deref()?; - let package_id = context.id.clone(); + let package_id = context.seed.id.clone(); let service_interface = ServiceInterface { id: id.clone(), @@ -384,6 +430,7 @@ async fn export_service_interface( let svc_interface_with_host_info = service_interface; context + .seed .ctx .db .mutate(|db| { @@ -407,7 +454,7 @@ async fn get_primary_url( }: GetPrimaryUrlParams, ) -> Result, Error> { let context = context.deref()?; - let package_id = package_id.unwrap_or_else(|| context.id.clone()); + let package_id = package_id.unwrap_or_else(|| context.seed.id.clone()); Ok(None) // TODO } @@ -419,9 +466,10 @@ async fn list_service_interfaces( }: ListServiceInterfacesParams, ) -> Result, Error> { let context = context.deref()?; - let package_id = package_id.unwrap_or_else(|| context.id.clone()); + let package_id = package_id.unwrap_or_else(|| context.seed.id.clone()); context + .seed .ctx .db .peek() @@ -435,9 +483,10 @@ async fn list_service_interfaces( } async fn remove_address(context: EffectContext, data: RemoveAddressParams) -> Result<(), Error> { let context = context.deref()?; - let package_id = context.id.clone(); + let package_id = context.seed.id.clone(); context + .seed .ctx .db .mutate(|db| { @@ -454,8 +503,9 @@ async fn remove_address(context: EffectContext, data: RemoveAddressParams) -> Re } async fn export_action(context: EffectContext, data: ExportActionParams) -> Result<(), Error> { let context = context.deref()?; - let package_id = context.id.clone(); + let package_id = context.seed.id.clone(); context + .seed .ctx .db .mutate(|db| { @@ -477,8 +527,9 @@ async fn export_action(context: EffectContext, data: ExportActionParams) -> Resu } async fn remove_action(context: EffectContext, data: RemoveActionParams) -> Result<(), Error> { let context = context.deref()?; - let package_id = context.id.clone(); + let package_id = context.seed.id.clone(); context + .seed .ctx .db .mutate(|db| { @@ -514,16 +565,16 @@ struct GetHostInfoParams { callback: Callback, } async fn get_host_info( - ctx: EffectContext, + context: EffectContext, GetHostInfoParams { callback, package_id, host_id, }: GetHostInfoParams, ) -> Result { - let ctx = ctx.deref()?; - let db = ctx.ctx.db.peek().await; - let package_id = package_id.unwrap_or_else(|| ctx.id.clone()); + let context = context.deref()?; + let db = context.seed.ctx.db.peek().await; + let package_id = package_id.unwrap_or_else(|| context.seed.id.clone()); db.as_public() .as_package_data() @@ -536,8 +587,8 @@ async fn get_host_info( } async fn clear_bindings(context: EffectContext, _: Empty) -> Result<(), Error> { - let ctx = context.deref()?; - let mut svc = ctx.persistent_container.net_service.lock().await; + let context = context.deref()?; + let mut svc = context.seed.persistent_container.net_service.lock().await; svc.clear_bindings().await?; Ok(()) } @@ -559,8 +610,8 @@ async fn bind(context: EffectContext, bind_params: Value) -> Result<(), Error> { internal_port, options, } = from_value(bind_params)?; - let ctx = context.deref()?; - let mut svc = ctx.persistent_container.net_service.lock().await; + let context = context.deref()?; + let mut svc = context.seed.persistent_container.net_service.lock().await; svc.bind(kind, id, internal_port, options).await } @@ -575,16 +626,16 @@ struct GetServiceInterfaceParams { } async fn get_service_interface( - ctx: EffectContext, + context: EffectContext, GetServiceInterfaceParams { callback, package_id, service_interface_id, }: GetServiceInterfaceParams, ) -> Result { - let ctx = ctx.deref()?; - let package_id = package_id.unwrap_or_else(|| ctx.id.clone()); - let db = ctx.ctx.db.peek().await; + let context = context.deref()?; + let package_id = package_id.unwrap_or_else(|| context.seed.id.clone()); + let db = context.seed.ctx.db.peek().await; let interface = db .as_public() @@ -729,8 +780,8 @@ async fn get_store( GetStoreParams { package_id, path }: GetStoreParams, ) -> Result { let context = context.deref()?; - let peeked = context.ctx.db.peek().await; - let package_id = package_id.unwrap_or(context.id.clone()); + let peeked = context.seed.ctx.db.peek().await; + let package_id = package_id.unwrap_or(context.seed.id.clone()); let value = peeked .as_private() .as_package_stores() @@ -758,8 +809,9 @@ async fn set_store( SetStoreParams { value, path }: SetStoreParams, ) -> Result<(), Error> { let context = context.deref()?; - let package_id = context.id.clone(); + let package_id = context.seed.id.clone(); context + .seed .ctx .db .mutate(|db| { @@ -812,7 +864,7 @@ struct ParamsMaybePackageId { async fn exists(context: EffectContext, params: ParamsPackageId) -> Result { let context = context.deref()?; - let peeked = context.ctx.db.peek().await; + let peeked = context.seed.ctx.db.peek().await; let package = peeked .as_public() .as_package_data() @@ -834,31 +886,30 @@ struct ExecuteAction { } async fn execute_action( context: EffectContext, - ExecuteAction { - action_id, - input, - service_id, - }: ExecuteAction, + WithProcedureId { + procedure_id, + rest: + ExecuteAction { + service_id, + action_id, + input, + }, + }: WithProcedureId, ) -> Result { let context = context.deref()?; - let package_id = service_id.clone().unwrap_or_else(|| context.id.clone()); - let service = context.ctx.services.get(&package_id).await; - let service = service.as_ref().ok_or_else(|| { - Error::new( - eyre!("Could not find package {package_id}"), - ErrorKind::Unknown, - ) - })?; + let package_id = service_id + .clone() + .unwrap_or_else(|| context.seed.id.clone()); - Ok(json!(service.action(action_id, input).await?)) + Ok(json!(context.action(procedure_id, action_id, input).await?)) } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] struct FromService {} async fn get_configured(context: EffectContext, _: Empty) -> Result { let context = context.deref()?; - let peeked = context.ctx.db.peek().await; - let package_id = &context.id; + let peeked = context.seed.ctx.db.peek().await; + let package_id = &context.seed.id; let package = peeked .as_public() .as_package_data() @@ -872,8 +923,8 @@ async fn get_configured(context: EffectContext, _: Empty) -> Result Result { let context = context.deref()?; - let peeked = context.ctx.db.peek().await; - let package_id = params.package_id.unwrap_or_else(|| context.id.clone()); + let peeked = context.seed.ctx.db.peek().await; + let package_id = params.package_id.unwrap_or_else(|| context.seed.id.clone()); let package = peeked .as_public() .as_package_data() @@ -887,7 +938,7 @@ async fn stopped(context: EffectContext, params: ParamsMaybePackageId) -> Result async fn running(context: EffectContext, params: ParamsPackageId) -> Result { dbg!("Starting the running {params:?}"); let context = context.deref()?; - let peeked = context.ctx.db.peek().await; + let peeked = context.seed.ctx.db.peek().await; let package_id = params.package_id; let package = peeked .as_public() @@ -900,30 +951,24 @@ async fn running(context: EffectContext, params: ParamsPackageId) -> Result Result { +async fn restart( + context: EffectContext, + WithProcedureId { procedure_id, .. }: WithProcedureId, +) -> Result<(), Error> { let context = context.deref()?; - let service = context.ctx.services.get(&context.id).await; - let service = service.as_ref().ok_or_else(|| { - Error::new( - eyre!("Could not find package {}", context.id), - ErrorKind::Unknown, - ) - })?; - service.restart().await?; - Ok(json!(())) + dbg!("here"); + context.restart(procedure_id).await?; + dbg!("here"); + Ok(()) } -async fn shutdown(context: EffectContext, _: Empty) -> Result { +async fn shutdown( + context: EffectContext, + WithProcedureId { procedure_id, .. }: WithProcedureId, +) -> Result<(), Error> { let context = context.deref()?; - let service = context.ctx.services.get(&context.id).await; - let service = service.as_ref().ok_or_else(|| { - Error::new( - eyre!("Could not find package {}", context.id), - ErrorKind::Unknown, - ) - })?; - service.stop().await?; - Ok(json!(())) + context.stop(procedure_id).await?; + Ok(()) } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] @@ -935,8 +980,9 @@ struct SetConfigured { } async fn set_configured(context: EffectContext, params: SetConfigured) -> Result { let context = context.deref()?; - let package_id = &context.id; + let package_id = &context.seed.id; context + .seed .ctx .db .mutate(|db| { @@ -989,9 +1035,9 @@ async fn set_main_status(context: EffectContext, params: SetMainStatus) -> Resul dbg!(format!("Status for main will be is {params:?}")); let context = context.deref()?; match params.status { - SetMainStatusStatus::Running => context.started(), - SetMainStatusStatus::Stopped => context.stopped(), - SetMainStatusStatus::Starting => context.stopped(), + SetMainStatusStatus::Running => context.seed.started(), + SetMainStatusStatus::Stopped => context.seed.stopped(), + SetMainStatusStatus::Starting => context.seed.stopped(), } Ok(Value::Null) } @@ -1011,8 +1057,9 @@ async fn set_health( ) -> Result { let context = context.deref()?; - let package_id = &context.id; + let package_id = &context.seed.id; context + .seed .ctx .db .mutate(move |db| { @@ -1041,17 +1088,17 @@ async fn set_health( #[command(rename_all = "camelCase")] #[ts(export)] pub struct DestroyOverlayedImageParams { - #[ts(type = "string")] - guid: InternedString, + guid: Guid, } #[instrument(skip_all)] pub async fn destroy_overlayed_image( - ctx: EffectContext, + context: EffectContext, DestroyOverlayedImageParams { guid }: DestroyOverlayedImageParams, ) -> Result<(), Error> { - let ctx = ctx.deref()?; - if ctx + let context = context.deref()?; + if context + .seed .persistent_container .overlays .lock() @@ -1068,30 +1115,25 @@ pub async fn destroy_overlayed_image( #[command(rename_all = "camelCase")] #[ts(export)] pub struct CreateOverlayedImageParams { - #[ts(type = "string")] image_id: ImageId, } #[instrument(skip_all)] pub async fn create_overlayed_image( - ctx: EffectContext, + context: EffectContext, CreateOverlayedImageParams { image_id }: CreateOverlayedImageParams, -) -> Result<(PathBuf, InternedString), Error> { - let ctx = ctx.deref()?; - let path = Path::new("images") - .join(*ARCH) - .join(&image_id) - .with_extension("squashfs"); - if let Some(image) = ctx +) -> Result<(PathBuf, Guid), Error> { + let context = context.deref()?; + if let Some(image) = context + .seed .persistent_container - .s9pk - .as_archive() - .contents() - .get_path(&path) - .and_then(|e| e.as_file()) + .images + .get(&image_id) + .cloned() { - let guid = new_guid(); - let rootfs_dir = ctx + let guid = Guid::new(); + let rootfs_dir = context + .seed .persistent_container .lxc_container .get() @@ -1102,7 +1144,9 @@ pub async fn create_overlayed_image( ) })? .rootfs_dir(); - let mountpoint = rootfs_dir.join("media/startos/overlays").join(&*guid); + let mountpoint = rootfs_dir + .join("media/startos/overlays") + .join(guid.as_ref()); tokio::fs::create_dir_all(&mountpoint).await?; let container_mountpoint = Path::new("/").join( mountpoint @@ -1110,18 +1154,16 @@ pub async fn create_overlayed_image( .with_kind(ErrorKind::Incoherent)?, ); tracing::info!("Mounting overlay {guid} for {image_id}"); - let guard = OverlayGuard::mount( - &IdMapped::new(LoopDev::from(&**image), 0, 100000, 65536), - &mountpoint, - ) - .await?; + let guard = OverlayGuard::mount(image, &mountpoint).await?; Command::new("chown") .arg("100000:100000") .arg(&mountpoint) .invoke(ErrorKind::Filesystem) .await?; tracing::info!("Mounted overlay {guid} for {image_id}"); - ctx.persistent_container + context + .seed + .persistent_container .overlays .lock() .await @@ -1228,13 +1270,15 @@ struct SetDependenciesParams { } async fn set_dependencies( - ctx: EffectContext, - SetDependenciesParams { dependencies }: SetDependenciesParams, + context: EffectContext, + WithProcedureId { + procedure_id, + rest: SetDependenciesParams { dependencies }, + }: WithProcedureId, ) -> Result<(), Error> { - let ctx = ctx.deref()?; - let id = &ctx.id; - let service_guard = ctx.ctx.services.get(id).await; - let service = service_guard.as_ref().or_not_found(id)?; + let context = context.deref()?; + let id = &context.seed.id; + let mut deps = BTreeMap::new(); for dependency in dependencies { let (dep_id, kind, registry_url, version_spec) = match dependency { @@ -1264,14 +1308,13 @@ async fn set_dependencies( let remote_s9pk = S9pk::deserialize( &Arc::new( HttpSource::new( - ctx.ctx.client.clone(), + context.seed.ctx.client.clone(), registry_url .join(&format!("package/v2/{}.s9pk?spec={}", dep_id, version_spec))?, ) .await?, ), None, // TODO - true, ) .await?; @@ -1291,14 +1334,19 @@ async fn set_dependencies( ) } }; - let config_satisfied = if let Some(dep_service) = &*ctx.ctx.services.get(&dep_id).await { - service - .dependency_config(dep_id.clone(), dep_service.get_config().await?.config) - .await? - .is_none() - } else { - true - }; + let config_satisfied = + if let Some(dep_service) = &*context.seed.ctx.services.get(&dep_id).await { + context + .dependency_config( + procedure_id.clone(), + dep_id.clone(), + dep_service.get_config(procedure_id.clone()).await?.config, + ) + .await? + .is_none() + } else { + true + }; deps.insert( dep_id, CurrentDependencyInfo { @@ -1311,7 +1359,9 @@ async fn set_dependencies( }, ); } - ctx.ctx + context + .seed + .ctx .db .mutate(|db| { db.as_public_mut() @@ -1324,10 +1374,10 @@ async fn set_dependencies( .await } -async fn get_dependencies(ctx: EffectContext) -> Result, Error> { - let ctx = ctx.deref()?; - let id = &ctx.id; - let db = ctx.ctx.db.peek().await; +async fn get_dependencies(context: EffectContext) -> Result, Error> { + let context = context.deref()?; + let id = &context.seed.id; + let db = context.seed.ctx.db.peek().await; let data = db .as_public() .as_package_data() @@ -1384,16 +1434,16 @@ struct CheckDependenciesResult { } async fn check_dependencies( - ctx: EffectContext, + context: EffectContext, CheckDependenciesParam { package_ids }: CheckDependenciesParam, ) -> Result, Error> { - let ctx = ctx.deref()?; - let db = ctx.ctx.db.peek().await; + let context = context.deref()?; + let db = context.seed.ctx.db.peek().await; let current_dependencies = db .as_public() .as_package_data() - .as_idx(&ctx.id) - .or_not_found(&ctx.id)? + .as_idx(&context.seed.id) + .or_not_found(&context.seed.id)? .as_current_dependencies() .de()?; let package_ids: Vec<_> = package_ids diff --git a/core/startos/src/service/service_map.rs b/core/startos/src/service/service_map.rs index ba9188f32..1474ea35e 100644 --- a/core/startos/src/service/service_map.rs +++ b/core/startos/src/service/service_map.rs @@ -25,7 +25,7 @@ use crate::progress::{ use crate::s9pk::manifest::PackageId; use crate::s9pk::merkle_archive::source::FileSource; use crate::s9pk::S9pk; -use crate::service::{LoadDisposition, Service}; +use crate::service::{LoadDisposition, Service, ServiceRef}; use crate::status::{MainStatus, Status}; use crate::util::serde::Pem; @@ -39,23 +39,22 @@ pub struct InstallProgressHandles { /// This is the structure to contain all the services #[derive(Default)] -pub struct ServiceMap(Mutex>>>>); +pub struct ServiceMap(Mutex>>>>); impl ServiceMap { - async fn entry(&self, id: &PackageId) -> Arc>> { + async fn entry(&self, id: &PackageId) -> Arc>> { let mut lock = self.0.lock().await; - dbg!(lock.keys().collect::>()); lock.entry(id.clone()) .or_insert_with(|| Arc::new(RwLock::new(None))) .clone() } #[instrument(skip_all)] - pub async fn get(&self, id: &PackageId) -> OwnedRwLockReadGuard> { + pub async fn get(&self, id: &PackageId) -> OwnedRwLockReadGuard> { self.entry(id).await.read_owned().await } #[instrument(skip_all)] - pub async fn get_mut(&self, id: &PackageId) -> OwnedRwLockWriteGuard> { + pub async fn get_mut(&self, id: &PackageId) -> OwnedRwLockWriteGuard> { self.entry(id).await.write_owned().await } @@ -83,7 +82,7 @@ impl ServiceMap { shutdown_err = service.shutdown().await; } // TODO: retry on error? - *service = Service::load(ctx, id, disposition).await?; + *service = Service::load(ctx, id, disposition).await?.map(From::from); shutdown_err?; Ok(()) } @@ -95,6 +94,7 @@ impl ServiceMap { mut s9pk: S9pk, recovery_source: Option, ) -> Result { + s9pk.validate_and_filter(ctx.s9pk_arch)?; let manifest = s9pk.as_manifest().clone(); let id = manifest.id.clone(); let icon = s9pk.icon_data_url().await?; @@ -128,7 +128,7 @@ impl ServiceMap { ); let restoring = recovery_source.is_some(); - let mut reload_guard = ServiceReloadGuard::new(ctx.clone(), id.clone(), op_name); + let mut reload_guard = ServiceRefReloadGuard::new(ctx.clone(), id.clone(), op_name); reload_guard .handle(ctx.db.mutate({ @@ -231,7 +231,7 @@ impl ServiceMap { Ok(reload_guard .handle_last(async move { finalization_progress.start(); - let s9pk = S9pk::open(&installed_path, Some(&id), true).await?; + let s9pk = S9pk::open(&installed_path, Some(&id)).await?; let prev = if let Some(service) = service.take() { ensure_code!( recovery_source.is_none(), @@ -264,7 +264,8 @@ impl ServiceMap { progress_handle, }), ) - .await?, + .await? + .into(), ); } else { *service = Some( @@ -277,7 +278,8 @@ impl ServiceMap { progress_handle, }), ) - .await?, + .await? + .into(), ); } sync_progress_task.await.map_err(|_| { @@ -295,7 +297,7 @@ impl ServiceMap { pub async fn uninstall(&self, ctx: &RpcContext, id: &PackageId) -> Result<(), Error> { let mut guard = self.get_mut(id).await; if let Some(service) = guard.take() { - ServiceReloadGuard::new(ctx.clone(), id.clone(), "Uninstall") + ServiceRefReloadGuard::new(ctx.clone(), id.clone(), "Uninstall") .handle_last(async move { let res = service.uninstall(None).await; drop(guard); @@ -326,17 +328,17 @@ impl ServiceMap { } } -pub struct ServiceReloadGuard(Option); -impl Drop for ServiceReloadGuard { +pub struct ServiceRefReloadGuard(Option); +impl Drop for ServiceRefReloadGuard { fn drop(&mut self) { if let Some(info) = self.0.take() { tokio::spawn(info.reload(None)); } } } -impl ServiceReloadGuard { +impl ServiceRefReloadGuard { pub fn new(ctx: RpcContext, id: PackageId, operation: &'static str) -> Self { - Self(Some(ServiceReloadInfo { ctx, id, operation })) + Self(Some(ServiceRefReloadInfo { ctx, id, operation })) } pub async fn handle( @@ -365,12 +367,12 @@ impl ServiceReloadGuard { } } -struct ServiceReloadInfo { +struct ServiceRefReloadInfo { ctx: RpcContext, id: PackageId, operation: &'static str, } -impl ServiceReloadInfo { +impl ServiceRefReloadInfo { async fn reload(self, error: Option) -> Result<(), Error> { self.ctx .services diff --git a/core/startos/src/service/transition/backup.rs b/core/startos/src/service/transition/backup.rs index e6a5cb817..f7591f0d9 100644 --- a/core/startos/src/service/transition/backup.rs +++ b/core/startos/src/service/transition/backup.rs @@ -6,6 +6,7 @@ use models::ProcedureName; use super::TempDesiredRestore; use crate::disk::mount::filesystem::ReadWrite; use crate::prelude::*; +use crate::rpc_continuations::Guid; use crate::service::config::GetConfig; use crate::service::dependencies::DependencyConfig; use crate::service::transition::{TransitionKind, TransitionState}; @@ -24,7 +25,12 @@ impl Handler for ServiceActor { .except::() .except::() } - async fn handle(&mut self, backup: Backup, jobs: &BackgroundJobQueue) -> Self::Response { + async fn handle( + &mut self, + id: Guid, + backup: Backup, + jobs: &BackgroundJobQueue, + ) -> Self::Response { // So Need a handle to just a single field in the state let temp: TempDesiredRestore = TempDesiredRestore::new(&self.0.persistent_container.state); let mut current = self.0.persistent_container.state.subscribe(); @@ -45,7 +51,7 @@ impl Handler for ServiceActor { .mount_backup(path, ReadWrite) .await?; seed.persistent_container - .execute(ProcedureName::CreateBackup, Value::Null, None) + .execute(id, ProcedureName::CreateBackup, Value::Null, None) .await?; backup_guard.unmount(true).await?; diff --git a/core/startos/src/service/transition/restart.rs b/core/startos/src/service/transition/restart.rs index 07466def1..a39291621 100644 --- a/core/startos/src/service/transition/restart.rs +++ b/core/startos/src/service/transition/restart.rs @@ -2,6 +2,7 @@ use futures::FutureExt; use super::TempDesiredRestore; use crate::prelude::*; +use crate::rpc_continuations::Guid; use crate::service::config::GetConfig; use crate::service::dependencies::DependencyConfig; use crate::service::transition::{TransitionKind, TransitionState}; @@ -18,7 +19,8 @@ impl Handler for ServiceActor { .except::() .except::() } - async fn handle(&mut self, _: Restart, jobs: &BackgroundJobQueue) -> Self::Response { + async fn handle(&mut self, _: Guid, _: Restart, jobs: &BackgroundJobQueue) -> Self::Response { + dbg!("here"); // So Need a handle to just a single field in the state let temp = TempDesiredRestore::new(&self.0.persistent_container.state); let mut current = self.0.persistent_container.state.subscribe(); @@ -74,7 +76,8 @@ impl Handler for ServiceActor { } impl Service { #[instrument(skip_all)] - pub async fn restart(&self) -> Result<(), Error> { - self.actor.send(Restart).await + pub async fn restart(&self, id: Guid) -> Result<(), Error> { + dbg!("here"); + self.actor.send(id, Restart).await } } diff --git a/core/startos/src/service/transition/restore.rs b/core/startos/src/service/transition/restore.rs index b32ade4f5..1c4020ea4 100644 --- a/core/startos/src/service/transition/restore.rs +++ b/core/startos/src/service/transition/restore.rs @@ -5,6 +5,7 @@ use models::ProcedureName; use crate::disk::mount::filesystem::ReadOnly; use crate::prelude::*; +use crate::rpc_continuations::Guid; use crate::service::transition::{TransitionKind, TransitionState}; use crate::service::ServiceActor; use crate::util::actor::background::BackgroundJobQueue; @@ -19,7 +20,12 @@ impl Handler for ServiceActor { fn conflicts_with(_: &Restore) -> ConflictBuilder { ConflictBuilder::everything() } - async fn handle(&mut self, restore: Restore, jobs: &BackgroundJobQueue) -> Self::Response { + async fn handle( + &mut self, + id: Guid, + restore: Restore, + jobs: &BackgroundJobQueue, + ) -> Self::Response { // So Need a handle to just a single field in the state let path = restore.path.clone(); let seed = self.0.clone(); @@ -32,7 +38,7 @@ impl Handler for ServiceActor { .mount_backup(path, ReadOnly) .await?; seed.persistent_container - .execute(ProcedureName::RestoreBackup, Value::Null, None) + .execute(id, ProcedureName::RestoreBackup, Value::Null, None) .await?; backup_guard.unmount(true).await?; diff --git a/core/startos/src/util/actor/concurrent.rs b/core/startos/src/util/actor/concurrent.rs index e32470d80..7b26fc4c7 100644 --- a/core/startos/src/util/actor/concurrent.rs +++ b/core/startos/src/util/actor/concurrent.rs @@ -8,6 +8,7 @@ use helpers::NonDetachingJoinHandle; use tokio::sync::{mpsc, oneshot}; use crate::prelude::*; +use crate::rpc_continuations::Guid; use crate::util::actor::background::{BackgroundJobQueue, BackgroundJobRunner}; use crate::util::actor::{Actor, ConflictFn, Handler, PendingMessageStrategy, Request}; @@ -18,6 +19,7 @@ struct ConcurrentRunner { waiting: Vec>, recv: mpsc::UnboundedReceiver>, handlers: Vec<( + Guid, Arc>, oneshot::Sender>, BoxFuture<'static, Box>, @@ -41,16 +43,21 @@ impl Future for ConcurrentRunner { } }); if this.shutdown.is_some() { - while let std::task::Poll::Ready(Some((msg, reply))) = this.recv.poll_recv(cx) { - if this.handlers.iter().any(|(f, _, _)| f(&*msg)) { - this.waiting.push((msg, reply)); + while let std::task::Poll::Ready(Some((id, msg, reply))) = this.recv.poll_recv(cx) { + if this + .handlers + .iter() + .any(|(hid, f, _, _)| &id != hid && f(&*msg)) + { + this.waiting.push((id, msg, reply)); } else { let mut actor = this.actor.clone(); let queue = this.queue.clone(); this.handlers.push(( + id.clone(), msg.conflicts_with(), reply, - async move { msg.handle_with(&mut actor, &queue).await }.boxed(), + async move { msg.handle_with(id, &mut actor, &queue).await }.boxed(), )) } } @@ -62,29 +69,34 @@ impl Future for ConcurrentRunner { .handlers .iter_mut() .enumerate() - .filter_map(|(i, (_, _, f))| match f.poll_unpin(cx) { + .filter_map(|(i, (_, _, _, f))| match f.poll_unpin(cx) { std::task::Poll::Pending => None, std::task::Poll::Ready(res) => Some((i, res)), }) .collect::>(); for (idx, res) in complete.into_iter().rev() { #[allow(clippy::let_underscore_future)] - let (f, reply, _) = this.handlers.swap_remove(idx); + let (_, f, reply, _) = this.handlers.swap_remove(idx); let _ = reply.send(res); // TODO: replace with Vec::extract_if once stable if this.shutdown.is_some() { let mut i = 0; while i < this.waiting.len() { - if f(&*this.waiting[i].0) - && !this.handlers.iter().any(|(f, _, _)| f(&*this.waiting[i].0)) + if f(&*this.waiting[i].1) + && !this + .handlers + .iter() + .any(|(_, f, _, _)| f(&*this.waiting[i].1)) { - let (msg, reply) = this.waiting.remove(i); + let (id, msg, reply) = this.waiting.remove(i); let mut actor = this.actor.clone(); let queue = this.queue.clone(); this.handlers.push(( + id.clone(), msg.conflicts_with(), reply, - async move { msg.handle_with(&mut actor, &queue).await }.boxed(), + async move { msg.handle_with(id, &mut actor, &queue).await } + .boxed(), )); cont = true; } else { @@ -137,6 +149,7 @@ impl ConcurrentActor { /// Message is guaranteed to be queued immediately pub fn queue( &self, + id: Guid, message: M, ) -> impl Future> where @@ -150,7 +163,7 @@ impl ConcurrentActor { } let (reply_send, reply_recv) = oneshot::channel(); self.messenger - .send((Box::new(message), reply_send)) + .send((id, Box::new(message), reply_send)) .unwrap(); futures::future::Either::Right( reply_recv @@ -170,11 +183,11 @@ impl ConcurrentActor { ) } - pub async fn send(&self, message: M) -> Result + pub async fn send(&self, id: Guid, message: M) -> Result where A: Handler, { - self.queue(message).await + self.queue(id, message).await } pub async fn shutdown(self, strategy: PendingMessageStrategy) { diff --git a/core/startos/src/util/actor/mod.rs b/core/startos/src/util/actor/mod.rs index 5cef7c22e..d85e53757 100644 --- a/core/startos/src/util/actor/mod.rs +++ b/core/startos/src/util/actor/mod.rs @@ -9,6 +9,7 @@ use tokio::sync::oneshot; #[allow(unused_imports)] use crate::prelude::*; +use crate::rpc_continuations::Guid; use crate::util::actor::background::BackgroundJobQueue; pub mod background; @@ -28,6 +29,7 @@ pub trait Handler: Actor { } fn handle( &mut self, + id: Guid, msg: M, jobs: &BackgroundJobQueue, ) -> impl Future + Send; @@ -39,6 +41,7 @@ trait Message: Send + Any { fn conflicts_with(&self) -> Arc>; fn handle_with<'a>( self: Box, + id: Guid, actor: &'a mut A, jobs: &'a BackgroundJobQueue, ) -> BoxFuture<'a, Box>; @@ -52,10 +55,11 @@ where } fn handle_with<'a>( self: Box, + id: Guid, actor: &'a mut A, jobs: &'a BackgroundJobQueue, ) -> BoxFuture<'a, Box> { - async move { Box::new(actor.handle(*self, jobs).await) as Box }.boxed() + async move { Box::new(actor.handle(id, *self, jobs).await) as Box }.boxed() } } impl dyn Message { @@ -80,7 +84,11 @@ impl dyn Message { } } -type Request = (Box>, oneshot::Sender>); +type Request = ( + Guid, + Box>, + oneshot::Sender>, +); pub enum PendingMessageStrategy { CancelAll, diff --git a/core/startos/src/util/actor/simple.rs b/core/startos/src/util/actor/simple.rs index 6f880a57a..7f2ad388e 100644 --- a/core/startos/src/util/actor/simple.rs +++ b/core/startos/src/util/actor/simple.rs @@ -7,6 +7,7 @@ use tokio::sync::oneshot::error::TryRecvError; use tokio::sync::{mpsc, oneshot}; use crate::prelude::*; +use crate::rpc_continuations::Guid; use crate::util::actor::background::BackgroundJobQueue; use crate::util::actor::{Actor, Handler, PendingMessageStrategy, Request}; @@ -26,9 +27,9 @@ impl SimpleActor { tokio::select! { _ = &mut runner => (), msg = messenger_recv.recv() => match msg { - Some((msg, reply)) if shutdown_recv.try_recv() == Err(TryRecvError::Empty) => { + Some((id, msg, reply)) if shutdown_recv.try_recv() == Err(TryRecvError::Empty) => { tokio::select! { - res = msg.handle_with(&mut actor, &queue) => { let _ = reply.send(res); }, + res = msg.handle_with(id, &mut actor, &queue) => { let _ = reply.send(res); }, _ = &mut runner => (), } } @@ -60,7 +61,7 @@ impl SimpleActor { } let (reply_send, reply_recv) = oneshot::channel(); self.messenger - .send((Box::new(message), reply_send)) + .send((Guid::new(), Box::new(message), reply_send)) .unwrap(); futures::future::Either::Right( reply_recv diff --git a/core/startos/src/util/io.rs b/core/startos/src/util/io.rs index 16bafd8f6..f4476ee2b 100644 --- a/core/startos/src/util/io.rs +++ b/core/startos/src/util/io.rs @@ -681,8 +681,6 @@ impl AsyncWrite for TimeoutStream { } } -pub struct TmpFile {} - #[derive(Debug)] pub struct TmpDir { path: PathBuf, @@ -707,6 +705,14 @@ impl TmpDir { tokio::fs::remove_dir_all(&self.path).await?; Ok(()) } + + pub async fn gc(self: Arc) -> Result<(), Error> { + if let Ok(dir) = Arc::try_unwrap(self) { + dir.delete().await + } else { + Ok(()) + } + } } impl std::ops::Deref for TmpDir { type Target = Path; diff --git a/core/startos/src/util/mod.rs b/core/startos/src/util/mod.rs index aa17b6d7a..4346a0b1e 100644 --- a/core/startos/src/util/mod.rs +++ b/core/startos/src/util/mod.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, VecDeque}; use std::future::Future; use std::marker::PhantomData; use std::path::{Path, PathBuf}; @@ -11,6 +11,8 @@ use std::time::Duration; use async_trait::async_trait; use color_eyre::eyre::{self, eyre}; use fd_lock_rs::FdLock; +use futures::future::BoxFuture; +use futures::FutureExt; use helpers::canonicalize; pub use helpers::NonDetachingJoinHandle; use imbl_value::InternedString; @@ -19,7 +21,8 @@ pub use models::VersionString; use pin_project::pin_project; use sha2::Digest; use tokio::fs::File; -use tokio::sync::{Mutex, OwnedMutexGuard, RwLock}; +use tokio::io::{AsyncRead, AsyncReadExt, BufReader}; +use tokio::sync::{oneshot, Mutex, OwnedMutexGuard, RwLock}; use tracing::instrument; use crate::shutdown::Shutdown; @@ -62,11 +65,16 @@ pub trait Invoke<'a> { where Self: 'ext, 'ext: 'a; + fn pipe<'ext: 'a>( + &'ext mut self, + next: &'ext mut tokio::process::Command, + ) -> Self::Extended<'ext>; fn timeout<'ext: 'a>(&'ext mut self, timeout: Option) -> Self::Extended<'ext>; fn input<'ext: 'a, Input: tokio::io::AsyncRead + Unpin + Send>( &'ext mut self, input: Option<&'ext mut Input>, ) -> Self::Extended<'ext>; + fn capture<'ext: 'a>(&'ext mut self, capture: bool) -> Self::Extended<'ext>; fn invoke( &mut self, error_kind: crate::ErrorKind, @@ -76,7 +84,20 @@ pub trait Invoke<'a> { pub struct ExtendedCommand<'a> { cmd: &'a mut tokio::process::Command, timeout: Option, - input: Option<&'a mut (dyn tokio::io::AsyncRead + Unpin + Send)>, + input: Option<&'a mut (dyn AsyncRead + Unpin + Send)>, + pipe: VecDeque<&'a mut tokio::process::Command>, + capture: bool, +} +impl<'a> From<&'a mut tokio::process::Command> for ExtendedCommand<'a> { + fn from(value: &'a mut tokio::process::Command) -> Self { + ExtendedCommand { + cmd: value, + timeout: None, + input: None, + pipe: VecDeque::new(), + capture: true, + } + } } impl<'a> std::ops::Deref for ExtendedCommand<'a> { type Target = tokio::process::Command; @@ -95,35 +116,38 @@ impl<'a> Invoke<'a> for tokio::process::Command { where Self: 'ext, 'ext: 'a; - fn timeout<'ext: 'a>(&'ext mut self, timeout: Option) -> Self::Extended<'ext> { - ExtendedCommand { - cmd: self, - timeout, - input: None, - } + fn pipe<'ext: 'a>( + &'ext mut self, + next: &'ext mut tokio::process::Command, + ) -> Self::Extended<'ext> { + let mut cmd = ExtendedCommand::from(self); + cmd.pipe.push_back(next); + cmd } - fn input<'ext: 'a, Input: tokio::io::AsyncRead + Unpin + Send>( + fn timeout<'ext: 'a>(&'ext mut self, timeout: Option) -> Self::Extended<'ext> { + let mut cmd = ExtendedCommand::from(self); + cmd.timeout = timeout; + cmd + } + fn input<'ext: 'a, Input: AsyncRead + Unpin + Send>( &'ext mut self, input: Option<&'ext mut Input>, ) -> Self::Extended<'ext> { - ExtendedCommand { - cmd: self, - timeout: None, - input: if let Some(input) = input { - Some(&mut *input) - } else { - None - }, - } + let mut cmd = ExtendedCommand::from(self); + cmd.input = if let Some(input) = input { + Some(&mut *input) + } else { + None + }; + cmd + } + fn capture<'ext: 'a>(&'ext mut self, capture: bool) -> Self::Extended<'ext> { + let mut cmd = ExtendedCommand::from(self); + cmd.capture = capture; + cmd } async fn invoke(&mut self, error_kind: crate::ErrorKind) -> Result, Error> { - ExtendedCommand { - cmd: self, - timeout: None, - input: None, - } - .invoke(error_kind) - .await + ExtendedCommand::from(self).invoke(error_kind).await } } @@ -132,6 +156,13 @@ impl<'a> Invoke<'a> for ExtendedCommand<'a> { where Self: 'ext, 'ext: 'a; + fn pipe<'ext: 'a>( + &'ext mut self, + next: &'ext mut tokio::process::Command, + ) -> Self::Extended<'ext> { + self.pipe.push_back(next.kill_on_drop(true)); + self + } fn timeout<'ext: 'a>(&'ext mut self, timeout: Option) -> Self::Extended<'ext> { self.timeout = timeout; self @@ -147,39 +178,150 @@ impl<'a> Invoke<'a> for ExtendedCommand<'a> { }; self } + fn capture<'ext: 'a>(&'ext mut self, capture: bool) -> Self::Extended<'ext> { + self.capture = capture; + self + } + #[instrument(skip_all)] async fn invoke(&mut self, error_kind: crate::ErrorKind) -> Result, Error> { + let cmd_str = self + .cmd + .as_std() + .get_program() + .to_string_lossy() + .into_owned(); self.cmd.kill_on_drop(true); if self.input.is_some() { self.cmd.stdin(Stdio::piped()); } - self.cmd.stdout(Stdio::piped()); - self.cmd.stderr(Stdio::piped()); - let mut child = self.cmd.spawn().with_kind(error_kind)?; - if let (Some(mut stdin), Some(input)) = (child.stdin.take(), self.input.take()) { - use tokio::io::AsyncWriteExt; - tokio::io::copy(input, &mut stdin).await?; - stdin.flush().await?; - stdin.shutdown().await?; - drop(stdin); + if self.pipe.is_empty() { + if self.capture { + self.cmd.stdout(Stdio::piped()); + self.cmd.stderr(Stdio::piped()); + } + let mut child = self.cmd.spawn().with_ctx(|_| (error_kind, &cmd_str))?; + if let (Some(mut stdin), Some(input)) = (child.stdin.take(), self.input.take()) { + use tokio::io::AsyncWriteExt; + tokio::io::copy(input, &mut stdin).await?; + stdin.flush().await?; + stdin.shutdown().await?; + drop(stdin); + } + let res = match self.timeout { + None => child + .wait_with_output() + .await + .with_ctx(|_| (error_kind, &cmd_str))?, + Some(t) => tokio::time::timeout(t, child.wait_with_output()) + .await + .with_kind(ErrorKind::Timeout)? + .with_ctx(|_| (error_kind, &cmd_str))?, + }; + crate::ensure_code!( + res.status.success(), + error_kind, + "{}", + Some(&res.stderr) + .filter(|a| !a.is_empty()) + .or(Some(&res.stdout)) + .filter(|a| !a.is_empty()) + .and_then(|a| std::str::from_utf8(a).ok()) + .unwrap_or(&format!( + "{} exited with code {}", + self.cmd.as_std().get_program().to_string_lossy(), + res.status + )) + ); + Ok(res.stdout) + } else { + let mut futures = Vec::>>::new(); // todo: predict capacity + + let mut cmds = std::mem::take(&mut self.pipe); + cmds.push_front(&mut *self.cmd); + let len = cmds.len(); + + let timeout = self.timeout; + + let mut prev = self + .input + .take() + .map(|i| Box::new(i) as Box); + for (idx, cmd) in IntoIterator::into_iter(cmds).enumerate() { + let last = idx == len - 1; + if self.capture || !last { + cmd.stdout(Stdio::piped()); + } + if self.capture { + cmd.stderr(Stdio::piped()); + } + if prev.is_some() { + cmd.stdin(Stdio::piped()); + } + let mut child = cmd.spawn().with_kind(error_kind)?; + let input = std::mem::replace( + &mut prev, + child + .stdout + .take() + .map(|i| Box::new(BufReader::new(i)) as Box), + ); + futures.push( + async move { + if let (Some(mut stdin), Some(mut input)) = (child.stdin.take(), input) { + use tokio::io::AsyncWriteExt; + tokio::io::copy(&mut input, &mut stdin).await?; + stdin.flush().await?; + stdin.shutdown().await?; + drop(stdin); + } + let res = match timeout { + None => child.wait_with_output().await?, + Some(t) => tokio::time::timeout(t, child.wait_with_output()) + .await + .with_kind(ErrorKind::Timeout)??, + }; + crate::ensure_code!( + res.status.success(), + error_kind, + "{}", + Some(&res.stderr) + .filter(|a| !a.is_empty()) + .or(Some(&res.stdout)) + .filter(|a| !a.is_empty()) + .and_then(|a| std::str::from_utf8(a).ok()) + .unwrap_or(&format!( + "{} exited with code {}", + cmd.as_std().get_program().to_string_lossy(), + res.status + )) + ); + + Ok(()) + } + .boxed(), + ); + } + + let (send, recv) = oneshot::channel(); + futures.push( + async move { + if let Some(mut prev) = prev { + let mut res = Vec::new(); + prev.read_to_end(&mut res).await?; + send.send(res).unwrap(); + } else { + send.send(Vec::new()).unwrap(); + } + + Ok(()) + } + .boxed(), + ); + + futures::future::try_join_all(futures).await?; + + Ok(recv.await.unwrap()) } - let res = match self.timeout { - None => child.wait_with_output().await?, - Some(t) => tokio::time::timeout(t, child.wait_with_output()) - .await - .with_kind(ErrorKind::Timeout)??, - }; - crate::ensure_code!( - res.status.success(), - error_kind, - "{}", - Some(&res.stderr) - .filter(|a| !a.is_empty()) - .or(Some(&res.stdout)) - .filter(|a| !a.is_empty()) - .and_then(|a| std::str::from_utf8(a).ok()) - .unwrap_or(&format!("Unknown Error ({})", res.status)) - ); - Ok(res.stdout) } } diff --git a/sdk/lib/StartSdk.ts b/sdk/lib/StartSdk.ts index f5959a084..4208928c9 100644 --- a/sdk/lib/StartSdk.ts +++ b/sdk/lib/StartSdk.ts @@ -187,7 +187,10 @@ export class StartSdk { nullIfEmpty, runCommand: async ( effects: Effects, - image: { id: Manifest["images"][number]; sharedRun?: boolean }, + image: { + id: keyof Manifest["images"] & T.ImageId + sharedRun?: boolean + }, command: ValidIfNoStupidEscape | [string, ...string[]], options: CommandOptions & { mounts?: { path: string; options: MountOptions }[] @@ -396,7 +399,7 @@ export class StartSdk { setupProperties: ( fn: (options: { effects: Effects }) => Promise, - ): T.ExpectedExports.Properties => + ): T.ExpectedExports.properties => (options) => fn(options).then(nullifyProperties), setupUninstall: (fn: UninstallFn) => @@ -743,7 +746,7 @@ export class StartSdk { export async function runCommand( effects: Effects, - image: { id: Manifest["images"][number]; sharedRun?: boolean }, + image: { id: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean }, command: string | [string, ...string[]], options: CommandOptions & { mounts?: { path: string; options: MountOptions }[] diff --git a/sdk/lib/health/HealthCheck.ts b/sdk/lib/health/HealthCheck.ts index ca0e3ebb9..1ed8652bf 100644 --- a/sdk/lib/health/HealthCheck.ts +++ b/sdk/lib/health/HealthCheck.ts @@ -8,12 +8,13 @@ import { defaultTrigger } from "../trigger/defaultTrigger" import { once } from "../util/once" import { Overlay } from "../util/Overlay" import { object, unknown } from "ts-matches" +import { T } from ".." export type HealthCheckParams = { effects: Effects name: string image: { - id: Manifest["images"][number] + id: keyof Manifest["images"] & T.ImageId sharedRun?: boolean } trigger?: Trigger diff --git a/sdk/lib/interfaces/Host.ts b/sdk/lib/interfaces/Host.ts index 96a76628a..aa27a289c 100644 --- a/sdk/lib/interfaces/Host.ts +++ b/sdk/lib/interfaces/Host.ts @@ -69,12 +69,12 @@ type NotProtocolsWithSslVariants = Exclude< type BindOptionsByKnownProtocol = | { protocol: ProtocolsWithSslVariants - preferredExternalPort: number + preferredExternalPort?: number addSsl?: Partial } | { protocol: NotProtocolsWithSslVariants - preferredExternalPort: number + preferredExternalPort?: number addSsl?: AddSslOptions } export type BindOptionsByProtocol = BindOptionsByKnownProtocol | BindOptions diff --git a/sdk/lib/mainFn/CommandController.ts b/sdk/lib/mainFn/CommandController.ts index a68e1e426..264574f7c 100644 --- a/sdk/lib/mainFn/CommandController.ts +++ b/sdk/lib/mainFn/CommandController.ts @@ -1,6 +1,6 @@ import { NO_TIMEOUT, SIGKILL, SIGTERM } from "../StartSdk" import { SDKManifest } from "../manifest/ManifestTypes" -import { Effects, ValidIfNoStupidEscape } from "../types" +import { Effects, ImageId, ValidIfNoStupidEscape } from "../types" import { MountOptions, Overlay } from "../util/Overlay" import { splitCommand } from "../util/splitCommand" import { cpExecFile, cpExec } from "./Daemons" @@ -15,7 +15,7 @@ export class CommandController { return async ( effects: Effects, imageId: { - id: Manifest["images"][number] + id: keyof Manifest["images"] & ImageId sharedRun?: boolean }, command: ValidIfNoStupidEscape | [string, ...string[]], diff --git a/sdk/lib/mainFn/Daemon.ts b/sdk/lib/mainFn/Daemon.ts index 2ec438801..c48865f94 100644 --- a/sdk/lib/mainFn/Daemon.ts +++ b/sdk/lib/mainFn/Daemon.ts @@ -1,5 +1,5 @@ import { SDKManifest } from "../manifest/ManifestTypes" -import { Effects, ValidIfNoStupidEscape } from "../types" +import { Effects, ImageId, ValidIfNoStupidEscape } from "../types" import { MountOptions, Overlay } from "../util/Overlay" import { CommandController } from "./CommandController" @@ -18,7 +18,7 @@ export class Daemon { return async ( effects: Effects, imageId: { - id: Manifest["images"][number] + id: keyof Manifest["images"] & ImageId sharedRun?: boolean }, command: ValidIfNoStupidEscape | [string, ...string[]], diff --git a/sdk/lib/mainFn/Daemons.ts b/sdk/lib/mainFn/Daemons.ts index 8962061f5..d51d444a8 100644 --- a/sdk/lib/mainFn/Daemons.ts +++ b/sdk/lib/mainFn/Daemons.ts @@ -5,7 +5,12 @@ import { SDKManifest } from "../manifest/ManifestTypes" import { Trigger } from "../trigger" import { TriggerInput } from "../trigger/TriggerInput" import { defaultTrigger } from "../trigger/defaultTrigger" -import { DaemonReturned, Effects, ValidIfNoStupidEscape } from "../types" +import { + DaemonReturned, + Effects, + ImageId, + ValidIfNoStupidEscape, +} from "../types" import { Mounts } from "./Mounts" import { CommandOptions, MountOptions, Overlay } from "../util/Overlay" import { splitCommand } from "../util/splitCommand" @@ -34,8 +39,8 @@ type DaemonsParams< Id extends string, > = { command: ValidIfNoStupidEscape | [string, ...string[]] - image: { id: Manifest["images"][number]; sharedRun?: boolean } - mounts: { path: string; options: MountOptions }[] + image: { id: keyof Manifest["images"] & ImageId; sharedRun?: boolean } + mounts: Mounts env?: Record ready: Ready requires: Exclude[] @@ -116,12 +121,10 @@ export class Daemons { options: DaemonsParams, ) { const daemonIndex = this.daemons.length - const daemon = Daemon.of()( - this.effects, - options.image, - options.command, - options, - ) + const daemon = Daemon.of()(this.effects, options.image, options.command, { + ...options, + mounts: options.mounts.build(), + }) const healthDaemon = new HealthDaemon( daemon, daemonIndex, diff --git a/sdk/lib/manifest/ManifestTypes.ts b/sdk/lib/manifest/ManifestTypes.ts index ed8703bf2..c820930c8 100644 --- a/sdk/lib/manifest/ManifestTypes.ts +++ b/sdk/lib/manifest/ManifestTypes.ts @@ -1,5 +1,5 @@ import { ValidEmVer } from "../emverLite/mod" -import { ActionMetadata } from "../types" +import { ActionMetadata, ImageConfig, ImageId } from "../types" export interface Container { /** This should be pointing to a docker container name */ @@ -28,8 +28,6 @@ export type SDKManifest = { readonly releaseNotes: string /** The type of license for the project. Include the LICENSE in the root of the project directory. A license is required for a Start9 package.*/ readonly license: string // name of license - /** A list of normie (hosted, SaaS, custodial, etc) services this services intends to replace */ - readonly replaces: Readonly /** The Start9 wrapper repository URL for the package. This repo contains the manifest file (this), * any scripts necessary for configuration, backups, actions, or health checks (more below). This key * must exist. But could be embedded into the source repository @@ -52,7 +50,7 @@ export type SDKManifest = { } /** Defines the os images needed to run the container processes */ - readonly images: string[] + readonly images: Record /** This denotes readonly asset directories that should be available to mount to the container. * Assuming that there will be three files with names along the lines: * icon.* : the icon that will be this packages icon on the ui diff --git a/sdk/lib/manifest/setupManifest.ts b/sdk/lib/manifest/setupManifest.ts index 41c74baa0..8bd39a7aa 100644 --- a/sdk/lib/manifest/setupManifest.ts +++ b/sdk/lib/manifest/setupManifest.ts @@ -1,18 +1,19 @@ +import { ImageConfig, ImageId, VolumeId } from "../osBindings" import { SDKManifest, ManifestVersion } from "./ManifestTypes" export function setupManifest< Id extends string, Version extends ManifestVersion, Dependencies extends Record, - VolumesTypes extends string, - AssetTypes extends string, - ImagesTypes extends string, + VolumesTypes extends VolumeId, + AssetTypes extends VolumeId, + ImagesTypes extends ImageId, Manifest extends SDKManifest & { dependencies: Dependencies id: Id version: Version assets: AssetTypes[] - images: ImagesTypes[] + images: Record volumes: VolumesTypes[] }, >(manifest: Manifest): Manifest { diff --git a/sdk/lib/osBindings/CreateOverlayedImageParams.ts b/sdk/lib/osBindings/CreateOverlayedImageParams.ts index b8579ac7d..aad94f01f 100644 --- a/sdk/lib/osBindings/CreateOverlayedImageParams.ts +++ b/sdk/lib/osBindings/CreateOverlayedImageParams.ts @@ -1,3 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ImageId } from "./ImageId" -export type CreateOverlayedImageParams = { imageId: string } +export type CreateOverlayedImageParams = { imageId: ImageId } diff --git a/sdk/lib/osBindings/DestroyOverlayedImageParams.ts b/sdk/lib/osBindings/DestroyOverlayedImageParams.ts index 82fc1cc67..b5b7484a2 100644 --- a/sdk/lib/osBindings/DestroyOverlayedImageParams.ts +++ b/sdk/lib/osBindings/DestroyOverlayedImageParams.ts @@ -1,3 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Guid } from "./Guid" -export type DestroyOverlayedImageParams = { guid: string } +export type DestroyOverlayedImageParams = { guid: Guid } diff --git a/sdk/lib/osBindings/ImageConfig.ts b/sdk/lib/osBindings/ImageConfig.ts new file mode 100644 index 000000000..2b1033b83 --- /dev/null +++ b/sdk/lib/osBindings/ImageConfig.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ImageSource } from "./ImageSource" + +export type ImageConfig = { + source: ImageSource + arch: string[] + emulateMissingAs: string | null +} diff --git a/sdk/lib/osBindings/ImageMetadata.ts b/sdk/lib/osBindings/ImageMetadata.ts new file mode 100644 index 000000000..b50f7a084 --- /dev/null +++ b/sdk/lib/osBindings/ImageMetadata.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ImageMetadata = { workdir: string; user: string } diff --git a/sdk/lib/osBindings/ImageSource.ts b/sdk/lib/osBindings/ImageSource.ts new file mode 100644 index 000000000..a71684e46 --- /dev/null +++ b/sdk/lib/osBindings/ImageSource.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ImageSource = + | "packed" + | { dockerBuild: { workdir: string | null; dockerfile: string | null } } + | { dockerTag: string } diff --git a/sdk/lib/osBindings/LoginParams.ts b/sdk/lib/osBindings/LoginParams.ts new file mode 100644 index 000000000..272f7ed07 --- /dev/null +++ b/sdk/lib/osBindings/LoginParams.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PasswordType } from "./PasswordType" + +export type LoginParams = { password: PasswordType | null; metadata: any } diff --git a/sdk/lib/osBindings/Manifest.ts b/sdk/lib/osBindings/Manifest.ts index 7f06be25e..15b96bd13 100644 --- a/sdk/lib/osBindings/Manifest.ts +++ b/sdk/lib/osBindings/Manifest.ts @@ -3,6 +3,7 @@ import type { Alerts } from "./Alerts" import type { Dependencies } from "./Dependencies" import type { Description } from "./Description" import type { HardwareRequirements } from "./HardwareRequirements" +import type { ImageConfig } from "./ImageConfig" import type { ImageId } from "./ImageId" import type { PackageId } from "./PackageId" import type { Version } from "./Version" @@ -20,7 +21,7 @@ export type Manifest = { marketingSite: string donationUrl: string | null description: Description - images: Array + images: { [key: ImageId]: ImageConfig } assets: Array volumes: Array alerts: Alerts diff --git a/sdk/lib/osBindings/index.ts b/sdk/lib/osBindings/index.ts index efd7a5efb..06a4bed7e 100644 --- a/sdk/lib/osBindings/index.ts +++ b/sdk/lib/osBindings/index.ts @@ -69,7 +69,10 @@ export { HostKind } from "./HostKind" export { HostnameInfo } from "./HostnameInfo" export { Hosts } from "./Hosts" export { Host } from "./Host" +export { ImageConfig } from "./ImageConfig" export { ImageId } from "./ImageId" +export { ImageMetadata } from "./ImageMetadata" +export { ImageSource } from "./ImageSource" export { InstalledState } from "./InstalledState" export { InstallingInfo } from "./InstallingInfo" export { InstallingState } from "./InstallingState" @@ -78,6 +81,7 @@ export { IpInfo } from "./IpInfo" export { LanInfo } from "./LanInfo" export { ListServiceInterfacesParams } from "./ListServiceInterfacesParams" export { ListVersionSignersParams } from "./ListVersionSignersParams" +export { LoginParams } from "./LoginParams" export { MainStatus } from "./MainStatus" export { Manifest } from "./Manifest" export { MaybeUtf8String } from "./MaybeUtf8String" diff --git a/sdk/lib/test/configBuilder.test.ts b/sdk/lib/test/configBuilder.test.ts index 2df40b95c..9738475a6 100644 --- a/sdk/lib/test/configBuilder.test.ts +++ b/sdk/lib/test/configBuilder.test.ts @@ -400,7 +400,7 @@ describe("values", () => { long: "", }, containers: {}, - images: [], + images: {}, volumes: [], assets: [], alerts: { diff --git a/sdk/lib/test/output.sdk.ts b/sdk/lib/test/output.sdk.ts index a0bab1f6e..189491be5 100644 --- a/sdk/lib/test/output.sdk.ts +++ b/sdk/lib/test/output.sdk.ts @@ -21,7 +21,7 @@ export const sdk = StartSdk.of() long: "", }, containers: {}, - images: [], + images: {}, volumes: [], assets: [], alerts: { diff --git a/sdk/lib/types.ts b/sdk/lib/types.ts index 2da92cd56..1f1245adc 100644 --- a/sdk/lib/types.ts +++ b/sdk/lib/types.ts @@ -11,6 +11,7 @@ import { GetPrimaryUrlParams, LanInfo, BindParams, + Manifest, } from "./osBindings" import { MainEffects, ServiceInterfaceType, Signals } from "./StartSdk" @@ -110,9 +111,26 @@ export namespace ExpectedExports { */ export type dependencyConfig = Record - export type Properties = (options: { + export type properties = (options: { effects: Effects }) => Promise + + export type manifest = Manifest +} +export type ABI = { + setConfig: ExpectedExports.setConfig + getConfig: ExpectedExports.getConfig + createBackup: ExpectedExports.createBackup + restoreBackup: ExpectedExports.restoreBackup + actions: ExpectedExports.actions + actionsMetadata: ExpectedExports.actionsMetadata + main: ExpectedExports.main + afterShutdown: ExpectedExports.afterShutdown + init: ExpectedExports.init + uninit: ExpectedExports.uninit + dependencyConfig: ExpectedExports.dependencyConfig + properties: ExpectedExports.properties + manifest: ExpectedExports.manifest } export type TimeMs = number export type VersionString = string @@ -453,8 +471,8 @@ export type Effects = { /** Exists could be useful during the runtime to know if some service is running, option dep */ running(options: { packageId: PackageId }): Promise - restart(): void - shutdown(): void + restart(): Promise + shutdown(): Promise mount(options: { location: string diff --git a/sdk/lib/util/Overlay.ts b/sdk/lib/util/Overlay.ts index 794b78732..fbc854c57 100644 --- a/sdk/lib/util/Overlay.ts +++ b/sdk/lib/util/Overlay.ts @@ -8,16 +8,18 @@ const WORKDIR = (imageId: string) => `/media/startos/images/${imageId}/` export class Overlay { private constructor( readonly effects: T.Effects, - readonly imageId: string, + readonly imageId: T.ImageId, readonly rootfs: string, - readonly guid: string, + readonly guid: T.Guid, ) {} static async of( effects: T.Effects, - image: { id: string; sharedRun?: boolean }, + image: { id: T.ImageId; sharedRun?: boolean }, ) { - const { id: imageId, sharedRun } = image - const [rootfs, guid] = await effects.createOverlayedImage({ imageId }) + const { id, sharedRun } = image + const [rootfs, guid] = await effects.createOverlayedImage({ + imageId: id as string, + }) const shared = ["dev", "sys", "proc"] if (!!sharedRun) { @@ -33,7 +35,7 @@ export class Overlay { ]) } - return new Overlay(effects, imageId, rootfs, guid) + return new Overlay(effects, id, rootfs, guid) } async mount(options: MountOptions, path: string): Promise { @@ -97,7 +99,7 @@ export class Overlay { stdout: string | Buffer stderr: string | Buffer }> { - const imageMeta: any = await fs + const imageMeta: T.ImageMetadata = await fs .readFile(`/media/startos/images/${this.imageId}.json`, { encoding: "utf8", }) diff --git a/sdk/lib/util/fileHelper.ts b/sdk/lib/util/fileHelper.ts index 07cfb2c4a..54f1eca06 100644 --- a/sdk/lib/util/fileHelper.ts +++ b/sdk/lib/util/fileHelper.ts @@ -3,7 +3,7 @@ import * as YAML from "yaml" import * as TOML from "@iarna/toml" import _ from "lodash" import * as T from "../types" -import * as fs from "fs" +import * as fs from "node:fs/promises" const previousPath = /(.+?)\/([^/]*)$/ @@ -59,28 +59,24 @@ export class FileHelper { readonly readData: (stringValue: string) => A, ) {} async write(data: A, effects: T.Effects) { - if (previousPath.exec(this.path)) { - await new Promise((resolve, reject) => - fs.mkdir(this.path, (err: any) => (!err ? resolve(null) : reject(err))), - ) + const parent = previousPath.exec(this.path) + if (parent) { + await fs.mkdir(parent[1], { recursive: true }) } - await new Promise((resolve, reject) => - fs.writeFile(this.path, this.writeData(data), (err: any) => - !err ? resolve(null) : reject(err), - ), - ) + await fs.writeFile(this.path, this.writeData(data)) } async read(effects: T.Effects) { - if (!fs.existsSync(this.path)) { + if ( + !(await fs.access(this.path).then( + () => true, + () => false, + )) + ) { return null } return this.readData( - await new Promise((resolve, reject) => - fs.readFile(this.path, (err: any, data: any) => - !err ? resolve(data.toString("utf-8")) : reject(err), - ), - ), + await fs.readFile(this.path).then((data) => data.toString("utf-8")), ) } @@ -142,7 +138,7 @@ export class FileHelper { return new FileHelper( path, (inData) => { - return JSON.stringify(inData, null, 2) + return YAML.stringify(inData, null, 2) }, (inString) => { return shape.unsafeCast(YAML.parse(inString)) diff --git a/sdk/package-lock.json b/sdk/package-lock.json index 0c457540f..a50066b34 100644 --- a/sdk/package-lock.json +++ b/sdk/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "isomorphic-fetch": "^3.0.0", - "lodash": "4.*.*", + "lodash": "^4.17.21", "ts-matches": "^5.4.1" }, "devDependencies": { diff --git a/sdk/package.json b/sdk/package.json index 53ee291ee..d6c3a1ca5 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@start9labs/start-sdk", - "version": "0.3.6-alpha1", + "version": "0.3.6-alpha5", "description": "Software development kit to facilitate packaging services for StartOS", "main": "./cjs/lib/index.js", "types": "./cjs/lib/index.d.ts", @@ -31,8 +31,10 @@ "homepage": "https://github.com/Start9Labs/start-sdk#readme", "dependencies": { "isomorphic-fetch": "^3.0.0", - "lodash": "4.*.*", - "ts-matches": "^5.4.1" + "lodash": "^4.17.21", + "ts-matches": "^5.4.1", + "yaml": "^2.2.2", + "@iarna/toml": "^2.2.5" }, "prettier": { "trailingComma": "all", @@ -41,7 +43,6 @@ "singleQuote": false }, "devDependencies": { - "@iarna/toml": "^2.2.5", "@types/jest": "^29.4.0", "@types/lodash": "^4.17.5", "jest": "^29.4.3", @@ -49,7 +50,6 @@ "ts-jest": "^29.0.5", "ts-node": "^10.9.1", "tsx": "^4.7.1", - "typescript": "^5.0.4", - "yaml": "^2.2.2" + "typescript": "^5.0.4" } } diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index 11d56ff15..5642eece5 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -69,7 +69,13 @@ export module Mock { osVersion: '0.2.12', dependencies: {}, hasConfig: true, - images: ['main'], + images: { + main: { + source: 'packed', + arch: ['x86_64', 'aarch64'], + emulateMissingAs: 'aarch64', + }, + }, assets: [], volumes: ['main'], hardwareRequirements: { @@ -116,7 +122,13 @@ export module Mock { }, }, hasConfig: true, - images: ['main'], + images: { + main: { + source: 'packed', + arch: ['x86_64', 'aarch64'], + emulateMissingAs: 'aarch64', + }, + }, assets: [], volumes: ['main'], hardwareRequirements: { @@ -157,7 +169,13 @@ export module Mock { }, }, hasConfig: false, - images: ['main'], + images: { + main: { + source: 'packed', + arch: ['x86_64', 'aarch64'], + emulateMissingAs: 'aarch64', + }, + }, assets: [], volumes: ['main'], hardwareRequirements: { From bb514d621657333284c622a7bb66673328535a3c Mon Sep 17 00:00:00 2001 From: Jade <2364004+Blu-J@users.noreply.github.com> Date: Fri, 14 Jun 2024 14:16:12 -0600 Subject: [PATCH 034/125] Chore/refactoring effects (#2644) * fix mac build * wip * chore: Update the effects to get rid of bad pattern * chore: Some small changes --------- Co-authored-by: Aiden McClelland --- .../src/Adapters/HostSystemStartOs.ts | 519 ++++++------ container-runtime/src/Adapters/RpcListener.ts | 2 +- .../Systems/SystemForEmbassy/MainLoop.ts | 11 +- .../Systems/SystemForEmbassy/index.ts | 72 +- .../SystemForEmbassy/polyfillEffects.ts | 770 +++++++++--------- .../src/Adapters/Systems/SystemForStartOs.ts | 7 +- .../src/Interfaces/HostSystem.ts | 5 +- container-runtime/src/Interfaces/System.ts | 4 +- container-runtime/src/index.ts | 4 +- download-firmware.sh | 3 +- 10 files changed, 704 insertions(+), 693 deletions(-) diff --git a/container-runtime/src/Adapters/HostSystemStartOs.ts b/container-runtime/src/Adapters/HostSystemStartOs.ts index 93905825a..1996af0fd 100644 --- a/container-runtime/src/Adapters/HostSystemStartOs.ts +++ b/container-runtime/src/Adapters/HostSystemStartOs.ts @@ -31,274 +31,273 @@ type RpcError = typeof matchRpcError._TYPE const SOCKET_PATH = "/media/startos/rpc/host.sock" const MAIN = "/main" as const -export class HostSystemStartOs implements Effects { - procedureId: string | null = null - - static of(callbackHolder: CallbackHolder) { - return new HostSystemStartOs(callbackHolder) - } - - constructor(readonly callbackHolder: CallbackHolder) {} - id = 0 - rpcRound( - method: K, - params: Record, - ) { - const id = this.id++ - const client = net.createConnection({ path: SOCKET_PATH }, () => { - client.write( - JSON.stringify({ - id, - method, - params: { ...params, procedureId: this.procedureId }, - }) + "\n", - ) - }) - let bufs: Buffer[] = [] - return new Promise((resolve, reject) => { - client.on("data", (data) => { - try { - bufs.push(data) - if (data.reduce((acc, x) => acc || x == 10, false)) { - const res: unknown = JSON.parse( - Buffer.concat(bufs).toString().split("\n")[0], - ) - if (testRpcError(res)) { - let message = res.error.message - console.error({ method, params, hostSystemStartOs: true }) - if (string.test(res.error.data)) { - message += ": " + res.error.data - console.error(res.error.data) +let hostSystemId = 0 +export const hostSystemStartOs = + (callbackHolder: CallbackHolder) => + (procedureId: null | string): Effects => { + const rpcRound = ( + method: K, + params: Record, + ) => { + const id = hostSystemId++ + const client = net.createConnection({ path: SOCKET_PATH }, () => { + client.write( + JSON.stringify({ + id, + method, + params: { ...params, procedureId: procedureId }, + }) + "\n", + ) + }) + let bufs: Buffer[] = [] + return new Promise((resolve, reject) => { + client.on("data", (data) => { + try { + bufs.push(data) + if (data.reduce((acc, x) => acc || x == 10, false)) { + const res: unknown = JSON.parse( + Buffer.concat(bufs).toString().split("\n")[0], + ) + if (testRpcError(res)) { + let message = res.error.message + console.error({ method, params, hostSystemStartOs: true }) + if (string.test(res.error.data)) { + message += ": " + res.error.data + console.error(res.error.data) + } else { + if (res.error.data?.details) { + message += ": " + res.error.data.details + console.error(res.error.data.details) + } + if (res.error.data?.debug) { + message += "\n" + res.error.data.debug + console.error("Debug: " + res.error.data.debug) + } + } + reject(new Error(`${message}@${method}`)) + } else if (testRpcResult(res)) { + resolve(res.result) } else { - if (res.error.data?.details) { - message += ": " + res.error.data.details - console.error(res.error.data.details) - } - if (res.error.data?.debug) { - message += "\n" + res.error.data.debug - console.error("Debug: " + res.error.data.debug) - } + reject(new Error(`malformed response ${JSON.stringify(res)}`)) } - reject(new Error(`${message}@${method}`)) - } else if (testRpcResult(res)) { - resolve(res.result) - } else { - reject(new Error(`malformed response ${JSON.stringify(res)}`)) } + } catch (error) { + reject(error) } - } catch (error) { + client.end() + }) + client.on("error", (error) => { reject(error) - } - client.end() + }) }) - client.on("error", (error) => { - reject(error) - }) - }) - } - - bind(...[options]: Parameters) { - return this.rpcRound("bind", { - ...options, - stack: new Error().stack, - }) as ReturnType - } - clearBindings(...[]: Parameters) { - return this.rpcRound("clearBindings", {}) as ReturnType< - T.Effects["clearBindings"] - > - } - clearServiceInterfaces( - ...[]: Parameters - ) { - return this.rpcRound("clearServiceInterfaces", {}) as ReturnType< - T.Effects["clearServiceInterfaces"] - > - } - createOverlayedImage(options: { - imageId: string - }): Promise<[string, string]> { - return this.rpcRound("createOverlayedImage", options) as ReturnType< - T.Effects["createOverlayedImage"] - > - } - destroyOverlayedImage(options: { guid: string }): Promise { - return this.rpcRound("destroyOverlayedImage", options) as ReturnType< - T.Effects["destroyOverlayedImage"] - > - } - executeAction(...[options]: Parameters) { - return this.rpcRound("executeAction", options) as ReturnType< - T.Effects["executeAction"] - > - } - exists(...[packageId]: Parameters) { - return this.rpcRound("exists", packageId) as ReturnType - } - exportAction(...[options]: Parameters) { - return this.rpcRound("exportAction", options) as ReturnType< - T.Effects["exportAction"] - > - } - exportServiceInterface: Effects["exportServiceInterface"] = ( - ...[options]: Parameters - ) => { - return this.rpcRound("exportServiceInterface", options) as ReturnType< - T.Effects["exportServiceInterface"] - > - } - exposeForDependents( - ...[options]: Parameters - ) { - return this.rpcRound("exposeForDependents", options) as ReturnType< - T.Effects["exposeForDependents"] - > - } - getConfigured(...[]: Parameters) { - return this.rpcRound("getConfigured", {}) as ReturnType< - T.Effects["getConfigured"] - > - } - getContainerIp(...[]: Parameters) { - return this.rpcRound("getContainerIp", {}) as ReturnType< - T.Effects["getContainerIp"] - > - } - getHostInfo: Effects["getHostInfo"] = (...[allOptions]: any[]) => { - const options = { - ...allOptions, - callback: this.callbackHolder.addCallback(allOptions.callback), } - return this.rpcRound("getHostInfo", options) as ReturnType< - T.Effects["getHostInfo"] - > as any - } - getServiceInterface( - ...[options]: Parameters - ) { - return this.rpcRound("getServiceInterface", { - ...options, - callback: this.callbackHolder.addCallback(options.callback), - }) as ReturnType - } + const self: Effects = { + bind(...[options]: Parameters) { + return rpcRound("bind", { + ...options, + stack: new Error().stack, + }) as ReturnType + }, + clearBindings(...[]: Parameters) { + return rpcRound("clearBindings", {}) as ReturnType< + T.Effects["clearBindings"] + > + }, + clearServiceInterfaces( + ...[]: Parameters + ) { + return rpcRound("clearServiceInterfaces", {}) as ReturnType< + T.Effects["clearServiceInterfaces"] + > + }, + createOverlayedImage(options: { + imageId: string + }): Promise<[string, string]> { + return rpcRound("createOverlayedImage", options) as ReturnType< + T.Effects["createOverlayedImage"] + > + }, + destroyOverlayedImage(options: { guid: string }): Promise { + return rpcRound("destroyOverlayedImage", options) as ReturnType< + T.Effects["destroyOverlayedImage"] + > + }, + executeAction(...[options]: Parameters) { + return rpcRound("executeAction", options) as ReturnType< + T.Effects["executeAction"] + > + }, + exists(...[packageId]: Parameters) { + return rpcRound("exists", packageId) as ReturnType + }, + exportAction(...[options]: Parameters) { + return rpcRound("exportAction", options) as ReturnType< + T.Effects["exportAction"] + > + }, + exportServiceInterface: (( + ...[options]: Parameters + ) => { + return rpcRound("exportServiceInterface", options) as ReturnType< + T.Effects["exportServiceInterface"] + > + }) as Effects["exportServiceInterface"], + exposeForDependents( + ...[options]: Parameters + ) { + return rpcRound("exposeForDependents", options) as ReturnType< + T.Effects["exposeForDependents"] + > + }, + getConfigured(...[]: Parameters) { + return rpcRound("getConfigured", {}) as ReturnType< + T.Effects["getConfigured"] + > + }, + getContainerIp(...[]: Parameters) { + return rpcRound("getContainerIp", {}) as ReturnType< + T.Effects["getContainerIp"] + > + }, + getHostInfo: ((...[allOptions]: any[]) => { + const options = { + ...allOptions, + callback: callbackHolder.addCallback(allOptions.callback), + } + return rpcRound("getHostInfo", options) as ReturnType< + T.Effects["getHostInfo"] + > as any + }) as Effects["getHostInfo"], + getServiceInterface( + ...[options]: Parameters + ) { + return rpcRound("getServiceInterface", { + ...options, + callback: callbackHolder.addCallback(options.callback), + }) as ReturnType + }, - getPrimaryUrl(...[options]: Parameters) { - return this.rpcRound("getPrimaryUrl", { - ...options, - callback: this.callbackHolder.addCallback(options.callback), - }) as ReturnType - } - getServicePortForward( - ...[options]: Parameters - ) { - return this.rpcRound("getServicePortForward", options) as ReturnType< - T.Effects["getServicePortForward"] - > - } - getSslCertificate(options: Parameters[0]) { - return this.rpcRound("getSslCertificate", options) as ReturnType< - T.Effects["getSslCertificate"] - > - } - getSslKey(options: Parameters[0]) { - return this.rpcRound("getSslKey", options) as ReturnType< - T.Effects["getSslKey"] - > - } - getSystemSmtp(...[options]: Parameters) { - return this.rpcRound("getSystemSmtp", { - ...options, - callback: this.callbackHolder.addCallback(options.callback), - }) as ReturnType - } - listServiceInterfaces( - ...[options]: Parameters - ) { - return this.rpcRound("listServiceInterfaces", { - ...options, - callback: this.callbackHolder.addCallback(options.callback), - }) as ReturnType - } - mount(...[options]: Parameters) { - return this.rpcRound("mount", options) as ReturnType - } - removeAction(...[options]: Parameters) { - return this.rpcRound("removeAction", options) as ReturnType< - T.Effects["removeAction"] - > - } - removeAddress(...[options]: Parameters) { - return this.rpcRound("removeAddress", options) as ReturnType< - T.Effects["removeAddress"] - > - } - restart(...[]: Parameters) { - return this.rpcRound("restart", {}) as ReturnType - } - running(...[packageId]: Parameters) { - return this.rpcRound("running", { packageId }) as ReturnType< - T.Effects["running"] - > - } - // runRsync(...[options]: Parameters) { - // - // return this.rpcRound('executeAction', options) as ReturnType - // - // return this.rpcRound('executeAction', options) as ReturnType - // } - setConfigured(...[configured]: Parameters) { - return this.rpcRound("setConfigured", { configured }) as ReturnType< - T.Effects["setConfigured"] - > - } - setDependencies( - dependencies: Parameters[0], - ): ReturnType { - return this.rpcRound("setDependencies", dependencies) as ReturnType< - T.Effects["setDependencies"] - > - } - checkDependencies( - options: Parameters[0], - ): ReturnType { - return this.rpcRound("checkDependencies", options) as ReturnType< - T.Effects["checkDependencies"] - > - } - getDependencies(): ReturnType { - return this.rpcRound("getDependencies", {}) as ReturnType< - T.Effects["getDependencies"] - > - } - setHealth(...[options]: Parameters) { - return this.rpcRound("setHealth", options) as ReturnType< - T.Effects["setHealth"] - > - } + getPrimaryUrl(...[options]: Parameters) { + return rpcRound("getPrimaryUrl", { + ...options, + callback: callbackHolder.addCallback(options.callback), + }) as ReturnType + }, + getServicePortForward( + ...[options]: Parameters + ) { + return rpcRound("getServicePortForward", options) as ReturnType< + T.Effects["getServicePortForward"] + > + }, + getSslCertificate( + options: Parameters[0], + ) { + return rpcRound("getSslCertificate", options) as ReturnType< + T.Effects["getSslCertificate"] + > + }, + getSslKey(options: Parameters[0]) { + return rpcRound("getSslKey", options) as ReturnType< + T.Effects["getSslKey"] + > + }, + getSystemSmtp(...[options]: Parameters) { + return rpcRound("getSystemSmtp", { + ...options, + callback: callbackHolder.addCallback(options.callback), + }) as ReturnType + }, + listServiceInterfaces( + ...[options]: Parameters + ) { + return rpcRound("listServiceInterfaces", { + ...options, + callback: callbackHolder.addCallback(options.callback), + }) as ReturnType + }, + mount(...[options]: Parameters) { + return rpcRound("mount", options) as ReturnType + }, + removeAction(...[options]: Parameters) { + return rpcRound("removeAction", options) as ReturnType< + T.Effects["removeAction"] + > + }, + removeAddress(...[options]: Parameters) { + return rpcRound("removeAddress", options) as ReturnType< + T.Effects["removeAddress"] + > + }, + restart(...[]: Parameters) { + return rpcRound("restart", {}) as ReturnType + }, + running(...[packageId]: Parameters) { + return rpcRound("running", { packageId }) as ReturnType< + T.Effects["running"] + > + }, + // runRsync(...[options]: Parameters) { + // + // return rpcRound('executeAction', options) as ReturnType + // + // return rpcRound('executeAction', options) as ReturnType + // } + setConfigured(...[configured]: Parameters) { + return rpcRound("setConfigured", { configured }) as ReturnType< + T.Effects["setConfigured"] + > + }, + setDependencies( + dependencies: Parameters[0], + ): ReturnType { + return rpcRound("setDependencies", dependencies) as ReturnType< + T.Effects["setDependencies"] + > + }, + checkDependencies( + options: Parameters[0], + ): ReturnType { + return rpcRound("checkDependencies", options) as ReturnType< + T.Effects["checkDependencies"] + > + }, + getDependencies(): ReturnType { + return rpcRound("getDependencies", {}) as ReturnType< + T.Effects["getDependencies"] + > + }, + setHealth(...[options]: Parameters) { + return rpcRound("setHealth", options) as ReturnType< + T.Effects["setHealth"] + > + }, - setMainStatus(o: { status: "running" | "stopped" }): Promise { - return this.rpcRound("setMainStatus", o) as ReturnType< - T.Effects["setHealth"] - > - } + setMainStatus(o: { status: "running" | "stopped" }): Promise { + return rpcRound("setMainStatus", o) as ReturnType< + T.Effects["setHealth"] + > + }, - shutdown(...[]: Parameters) { - return this.rpcRound("shutdown", {}) as ReturnType + shutdown(...[]: Parameters) { + return rpcRound("shutdown", {}) as ReturnType + }, + stopped(...[packageId]: Parameters) { + return rpcRound("stopped", { packageId }) as ReturnType< + T.Effects["stopped"] + > + }, + store: { + get: async (options: any) => + rpcRound("getStore", { + ...options, + callback: callbackHolder.addCallback(options.callback), + }) as any, + set: async (options: any) => + rpcRound("setStore", options) as ReturnType< + T.Effects["store"]["set"] + >, + } as T.Effects["store"], + } + return self } - stopped(...[packageId]: Parameters) { - return this.rpcRound("stopped", { packageId }) as ReturnType< - T.Effects["stopped"] - > - } - store: T.Effects["store"] = { - get: async (options: any) => - this.rpcRound("getStore", { - ...options, - callback: this.callbackHolder.addCallback(options.callback), - }) as any, - set: async (options: any) => - this.rpcRound("setStore", options) as ReturnType< - T.Effects["store"]["set"] - >, - } -} diff --git a/container-runtime/src/Adapters/RpcListener.ts b/container-runtime/src/Adapters/RpcListener.ts index 5391d943e..04e9bc40f 100644 --- a/container-runtime/src/Adapters/RpcListener.ts +++ b/container-runtime/src/Adapters/RpcListener.ts @@ -247,7 +247,7 @@ export class RpcListener { })), ) .when(exitType, async ({ id }) => { - if (this._system) await this._system.exit(this.effects) + if (this._system) await this._system.exit(this.effects(null)) delete this._system delete this._effects diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts index 08bf944ed..975bb52ca 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts @@ -1,9 +1,10 @@ -import { PolyfillEffects } from "./polyfillEffects" +import { polyfillEffects } from "./polyfillEffects" import { DockerProcedureContainer } from "./DockerProcedureContainer" import { SystemForEmbassy } from "." -import { HostSystemStartOs } from "../../HostSystemStartOs" +import { hostSystemStartOs } from "../../HostSystemStartOs" import { Daemons, T, daemons } from "@start9labs/start-sdk" import { Daemon } from "@start9labs/start-sdk/cjs/lib/mainFn/Daemon" +import { Effects } from "../../../Models/Effects" const EMBASSY_HEALTH_INTERVAL = 15 * 1000 const EMBASSY_PROPERTIES_LOOP = 30 * 1000 @@ -27,7 +28,7 @@ export class MainLoop { | undefined constructor( readonly system: SystemForEmbassy, - readonly effects: HostSystemStartOs, + readonly effects: Effects, ) { this.healthLoops = this.constructHealthLoops() this.mainEvent = this.constructMainEvent() @@ -65,7 +66,7 @@ export class MainLoop { } } - private async setupInterfaces(effects: HostSystemStartOs) { + private async setupInterfaces(effects: T.Effects) { for (const interfaceId in this.system.manifest.interfaces) { const iface = this.system.manifest.interfaces[interfaceId] const internalPorts = new Set() @@ -211,7 +212,7 @@ export class MainLoop { } const result = await method( - new PolyfillEffects(effects, this.system.manifest), + polyfillEffects(effects, this.system.manifest), timeChanged, ) diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index 731b38903..127fb0e09 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -1,7 +1,7 @@ import { types as T, utils, EmVer } from "@start9labs/start-sdk" import * as fs from "fs/promises" -import { PolyfillEffects } from "./polyfillEffects" +import { polyfillEffects } from "./polyfillEffects" import { Duration, duration, fromDuration } from "../../../Models/Duration" import { System } from "../../../Interfaces/System" import { matchManifest, Manifest, Procedure } from "./matchManifest" @@ -27,7 +27,7 @@ import { Parser, array, } from "ts-matches" -import { HostSystemStartOs } from "../../HostSystemStartOs" +import { hostSystemStartOs } from "../../HostSystemStartOs" import { JsonPath, unNestPath } from "../../../Models/JsonPath" import { RpcResult, matchRpcResult } from "../../RpcListener" import { CT } from "@start9labs/start-sdk" @@ -41,6 +41,7 @@ import { MultiHost, } from "@start9labs/start-sdk/cjs/lib/interfaces/Host" import { ServiceInterfaceBuilder } from "@start9labs/start-sdk/cjs/lib/interfaces/ServiceInterfaceBuilder" +import { Effects } from "../../../Models/Effects" type Optional = A | undefined | null function todo(): never { @@ -197,7 +198,7 @@ export class SystemForEmbassy implements System { readonly moduleCode: Partial, ) {} async execute( - effects: HostSystemStartOs, + effectCreator: ReturnType, options: { id: string procedure: JsonPath @@ -205,8 +206,7 @@ export class SystemForEmbassy implements System { timeout?: number | undefined }, ): Promise { - effects = Object.create(effects) - effects.procedureId = options.id + const effects = effectCreator(options.id) return this._execute(effects, options) .then((x) => matches(x) @@ -261,12 +261,12 @@ export class SystemForEmbassy implements System { } }) } - async exit(effects: HostSystemStartOs): Promise { + async exit(): Promise { if (this.currentRunning) await this.currentRunning.clean() delete this.currentRunning } async _execute( - effects: HostSystemStartOs, + effects: Effects, options: { procedure: JsonPath input: unknown @@ -340,7 +340,7 @@ export class SystemForEmbassy implements System { throw new Error(`Could not find the path for ${options.procedure}`) } private async init( - effects: HostSystemStartOs, + effects: Effects, previousVersion: Optional, timeoutMs: number | null, ): Promise { @@ -350,7 +350,7 @@ export class SystemForEmbassy implements System { await this.exportActions(effects) await this.exportNetwork(effects) } - async exportNetwork(effects: HostSystemStartOs) { + async exportNetwork(effects: Effects) { for (const [id, interfaceValue] of Object.entries( this.manifest.interfaces, )) { @@ -428,7 +428,7 @@ export class SystemForEmbassy implements System { ) } } - async exportActions(effects: HostSystemStartOs) { + async exportActions(effects: Effects) { const manifest = this.manifest if (!manifest.actions) return for (const [actionId, action] of Object.entries(manifest.actions)) { @@ -457,7 +457,7 @@ export class SystemForEmbassy implements System { } } private async uninit( - effects: HostSystemStartOs, + effects: Effects, nextVersion: Optional, timeoutMs: number | null, ): Promise { @@ -465,7 +465,7 @@ export class SystemForEmbassy implements System { await effects.setMainStatus({ status: "stopped" }) } private async mainStart( - effects: HostSystemStartOs, + effects: Effects, timeoutMs: number | null, ): Promise { if (!!this.currentRunning) return @@ -473,7 +473,7 @@ export class SystemForEmbassy implements System { this.currentRunning = new MainLoop(this, effects) } private async mainStop( - effects: HostSystemStartOs, + effects: Effects, timeoutMs: number | null, ): Promise { const { currentRunning } = this @@ -491,7 +491,7 @@ export class SystemForEmbassy implements System { return durationValue } private async createBackup( - effects: HostSystemStartOs, + effects: Effects, timeoutMs: number | null, ): Promise { const backup = this.manifest.backup.create @@ -503,13 +503,11 @@ export class SystemForEmbassy implements System { await container.execFail([backup.entrypoint, ...backup.args], timeoutMs) } else { const moduleCode = await this.moduleCode - await moduleCode.createBackup?.( - new PolyfillEffects(effects, this.manifest), - ) + await moduleCode.createBackup?.(polyfillEffects(effects, this.manifest)) } } private async restoreBackup( - effects: HostSystemStartOs, + effects: Effects, timeoutMs: number | null, ): Promise { const restoreBackup = this.manifest.backup.restore @@ -528,19 +526,17 @@ export class SystemForEmbassy implements System { ) } else { const moduleCode = await this.moduleCode - await moduleCode.restoreBackup?.( - new PolyfillEffects(effects, this.manifest), - ) + await moduleCode.restoreBackup?.(polyfillEffects(effects, this.manifest)) } } private async getConfig( - effects: HostSystemStartOs, + effects: Effects, timeoutMs: number | null, ): Promise { return this.getConfigUncleaned(effects, timeoutMs).then(removePointers) } private async getConfigUncleaned( - effects: HostSystemStartOs, + effects: Effects, timeoutMs: number | null, ): Promise { const config = this.manifest.config?.get @@ -564,7 +560,7 @@ export class SystemForEmbassy implements System { const moduleCode = await this.moduleCode const method = moduleCode.getConfig if (!method) throw new Error("Expecting that the method getConfig exists") - return (await method(new PolyfillEffects(effects, this.manifest)).then( + return (await method(polyfillEffects(effects, this.manifest)).then( (x) => { if ("result" in x) return x.result if ("error" in x) throw new Error("Error getting config: " + x.error) @@ -574,7 +570,7 @@ export class SystemForEmbassy implements System { } } private async setConfig( - effects: HostSystemStartOs, + effects: Effects, newConfigWithoutPointers: unknown, timeoutMs: number | null, ): Promise { @@ -617,7 +613,7 @@ export class SystemForEmbassy implements System { const answer = matchSetResult.unsafeCast( await method( - new PolyfillEffects(effects, this.manifest), + polyfillEffects(effects, this.manifest), newConfig as U.Config, ).then((x): T.SetResult => { if ("result" in x) @@ -636,7 +632,7 @@ export class SystemForEmbassy implements System { } } private async setConfigSetConfig( - effects: HostSystemStartOs, + effects: Effects, dependsOn: { [x: string]: readonly string[] }, ) { await effects.setDependencies({ @@ -660,7 +656,7 @@ export class SystemForEmbassy implements System { } private async migration( - effects: HostSystemStartOs, + effects: Effects, fromVersion: string, timeoutMs: number | null, ): Promise { @@ -713,7 +709,7 @@ export class SystemForEmbassy implements System { if (!method) throw new Error("Expecting that the method migration exists") return (await method( - new PolyfillEffects(effects, this.manifest), + polyfillEffects(effects, this.manifest), fromVersion as string, ).then((x) => { if ("result" in x) return x.result @@ -725,7 +721,7 @@ export class SystemForEmbassy implements System { return { configured: true } } private async properties( - effects: HostSystemStartOs, + effects: Effects, timeoutMs: number | null, ): Promise> { // TODO BLU-J set the properties ever so often @@ -754,7 +750,7 @@ export class SystemForEmbassy implements System { if (!method) throw new Error("Expecting that the method properties exists") const properties = matchProperties.unsafeCast( - await method(new PolyfillEffects(effects, this.manifest)).then((x) => { + await method(polyfillEffects(effects, this.manifest)).then((x) => { if ("result" in x) return x.result if ("error" in x) throw new Error("Error getting config: " + x.error) throw new Error("Error getting config: " + x["error-code"][1]) @@ -765,7 +761,7 @@ export class SystemForEmbassy implements System { throw new Error(`Unknown type in the fetch properties: ${setConfigValue}`) } private async action( - effects: HostSystemStartOs, + effects: Effects, actionId: string, formData: unknown, timeoutMs: number | null, @@ -795,7 +791,7 @@ export class SystemForEmbassy implements System { const method = moduleCode.action?.[actionId] if (!method) throw new Error("Expecting that the method action exists") return (await method( - new PolyfillEffects(effects, this.manifest), + polyfillEffects(effects, this.manifest), formData as any, ).then((x) => { if ("result" in x) return x.result @@ -805,7 +801,7 @@ export class SystemForEmbassy implements System { } } private async dependenciesCheck( - effects: HostSystemStartOs, + effects: Effects, id: string, oldConfig: unknown, timeoutMs: number | null, @@ -838,7 +834,7 @@ export class SystemForEmbassy implements System { `Expecting that the method dependency check ${id} exists`, ) return (await method( - new PolyfillEffects(effects, this.manifest), + polyfillEffects(effects, this.manifest), oldConfig as any, ).then((x) => { if ("result" in x) return x.result @@ -850,7 +846,7 @@ export class SystemForEmbassy implements System { } } private async dependenciesAutoconfig( - effects: HostSystemStartOs, + effects: Effects, id: string, oldConfig: unknown, timeoutMs: number | null, @@ -863,7 +859,7 @@ export class SystemForEmbassy implements System { `Expecting that the method dependency autoConfigure ${id} exists`, ) return (await method( - new PolyfillEffects(effects, this.manifest), + polyfillEffects(effects, this.manifest), oldConfig as any, ).then((x) => { if ("result" in x) return x.result @@ -961,7 +957,7 @@ function cleanConfigFromPointers( } async function updateConfig( - effects: HostSystemStartOs, + effects: Effects, manifest: Manifest, spec: unknown, mutConfigValue: unknown, diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts index 3a8d5f624..2b7363cbf 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts @@ -4,390 +4,175 @@ import { Volume } from "../../../Models/Volume" import * as child_process from "child_process" import { promisify } from "util" import { daemons, startSdk, T } from "@start9labs/start-sdk" -import { HostSystemStartOs } from "../../HostSystemStartOs" import "isomorphic-fetch" import { Manifest } from "./matchManifest" import { DockerProcedureContainer } from "./DockerProcedureContainer" import * as cp from "child_process" +import { Effects } from "../../../Models/Effects" export const execFile = promisify(cp.execFile) -export class PolyfillEffects implements oet.Effects { - constructor( - readonly effects: HostSystemStartOs, - private manifest: Manifest, - ) {} - async writeFile(input: { - path: string - volumeId: string - toWrite: string - }): Promise { - await fs.writeFile( - new Volume(input.volumeId, input.path).path, - input.toWrite, - ) - } - async readFile(input: { volumeId: string; path: string }): Promise { - return ( - await fs.readFile(new Volume(input.volumeId, input.path).path) - ).toString() - } - async metadata(input: { - volumeId: string - path: string - }): Promise { - const stats = await fs.stat(new Volume(input.volumeId, input.path).path) - return { - fileType: stats.isFile() ? "file" : "directory", - gid: stats.gid, - uid: stats.uid, - mode: stats.mode, - isDir: stats.isDirectory(), - isFile: stats.isFile(), - isSymlink: stats.isSymbolicLink(), - len: stats.size, - readonly: (stats.mode & 0o200) > 0, - } - } - async createDir(input: { volumeId: string; path: string }): Promise { - const path = new Volume(input.volumeId, input.path).path - await fs.mkdir(path, { recursive: true }) - return path - } - async readDir(input: { volumeId: string; path: string }): Promise { - return fs.readdir(new Volume(input.volumeId, input.path).path) - } - async removeDir(input: { volumeId: string; path: string }): Promise { - const path = new Volume(input.volumeId, input.path).path - await fs.rmdir(new Volume(input.volumeId, input.path).path, { - recursive: true, - }) - return path - } - removeFile(input: { volumeId: string; path: string }): Promise { - return fs.rm(new Volume(input.volumeId, input.path).path) - } - async writeJsonFile(input: { - volumeId: string - path: string - toWrite: Record - }): Promise { - await fs.writeFile( - new Volume(input.volumeId, input.path).path, - JSON.stringify(input.toWrite), - ) - } - async readJsonFile(input: { - volumeId: string - path: string - }): Promise> { - return JSON.parse( - ( +export const polyfillEffects = ( + effects: Effects, + manifest: Manifest, +): oet.Effects => { + const self = { + effects, + manifest, + async writeFile(input: { + path: string + volumeId: string + toWrite: string + }): Promise { + await fs.writeFile( + new Volume(input.volumeId, input.path).path, + input.toWrite, + ) + }, + async readFile(input: { volumeId: string; path: string }): Promise { + return ( await fs.readFile(new Volume(input.volumeId, input.path).path) - ).toString(), - ) - } - runCommand({ - command, - args, - timeoutMillis, - }: { - command: string - args?: string[] | undefined - timeoutMillis?: number | undefined - }): Promise> { - return startSdk - .runCommand( - this.effects, - { id: this.manifest.main.image }, - [command, ...(args || [])], - {}, + ).toString() + }, + async metadata(input: { + volumeId: string + path: string + }): Promise { + const stats = await fs.stat(new Volume(input.volumeId, input.path).path) + return { + fileType: stats.isFile() ? "file" : "directory", + gid: stats.gid, + uid: stats.uid, + mode: stats.mode, + isDir: stats.isDirectory(), + isFile: stats.isFile(), + isSymlink: stats.isSymbolicLink(), + len: stats.size, + readonly: (stats.mode & 0o200) > 0, + } + }, + async createDir(input: { + volumeId: string + path: string + }): Promise { + const path = new Volume(input.volumeId, input.path).path + await fs.mkdir(path, { recursive: true }) + return path + }, + async readDir(input: { + volumeId: string + path: string + }): Promise { + return fs.readdir(new Volume(input.volumeId, input.path).path) + }, + async removeDir(input: { + volumeId: string + path: string + }): Promise { + const path = new Volume(input.volumeId, input.path).path + await fs.rmdir(new Volume(input.volumeId, input.path).path, { + recursive: true, + }) + return path + }, + removeFile(input: { volumeId: string; path: string }): Promise { + return fs.rm(new Volume(input.volumeId, input.path).path) + }, + async writeJsonFile(input: { + volumeId: string + path: string + toWrite: Record + }): Promise { + await fs.writeFile( + new Volume(input.volumeId, input.path).path, + JSON.stringify(input.toWrite), ) - .then((x: any) => ({ - stderr: x.stderr.toString(), - stdout: x.stdout.toString(), - })) - .then((x: any) => - !!x.stderr ? { error: x.stderr } : { result: x.stdout }, + }, + async readJsonFile(input: { + volumeId: string + path: string + }): Promise> { + return JSON.parse( + ( + await fs.readFile(new Volume(input.volumeId, input.path).path) + ).toString(), ) - } - runDaemon(input: { command: string; args?: string[] | undefined }): { - wait(): Promise> - term(): Promise - } { - const dockerProcedureContainer = DockerProcedureContainer.of( - this.effects, - this.manifest.main, - this.manifest.volumes, - ) - const daemon = dockerProcedureContainer.then((dockerProcedureContainer) => - daemons.runCommand()( - this.effects, - { id: this.manifest.main.image }, - [input.command, ...(input.args || [])], - { - overlay: dockerProcedureContainer.overlay, - }, - ), - ) - return { - wait: () => - daemon.then((daemon) => - daemon.wait().then(() => { - return { result: "" } - }), + }, + runCommand({ + command, + args, + timeoutMillis, + }: { + command: string + args?: string[] | undefined + timeoutMillis?: number | undefined + }): Promise> { + return startSdk + .runCommand( + effects, + { id: manifest.main.image }, + [command, ...(args || [])], + {}, + ) + .then((x: any) => ({ + stderr: x.stderr.toString(), + stdout: x.stdout.toString(), + })) + .then((x: any) => + !!x.stderr ? { error: x.stderr } : { result: x.stdout }, + ) + }, + runDaemon(input: { command: string; args?: string[] | undefined }): { + wait(): Promise> + term(): Promise + } { + const dockerProcedureContainer = DockerProcedureContainer.of( + effects, + manifest.main, + manifest.volumes, + ) + const daemon = dockerProcedureContainer.then((dockerProcedureContainer) => + daemons.runCommand()( + effects, + { id: manifest.main.image }, + [input.command, ...(input.args || [])], + { + overlay: dockerProcedureContainer.overlay, + }, ), - term: () => daemon.then((daemon) => daemon.term()), - } - } - async chown(input: { - volumeId: string - path: string - uid: string - }): Promise { - await startSdk - .runCommand( - this.effects, - { id: this.manifest.main.image }, - ["chown", "--recursive", input.uid, `/drive/${input.path}`], - { - mounts: [ - { - path: "/drive", - options: { - type: "volume", - id: input.volumeId, - subpath: null, - readonly: false, - }, - }, - ], - }, ) - .then((x: any) => ({ - stderr: x.stderr.toString(), - stdout: x.stdout.toString(), - })) - .then((x: any) => { - if (!!x.stderr) { - throw new Error(x.stderr) - } - }) - return null - } - async chmod(input: { - volumeId: string - path: string - mode: string - }): Promise { - await startSdk - .runCommand( - this.effects, - { id: this.manifest.main.image }, - ["chmod", "--recursive", input.mode, `/drive/${input.path}`], - { - mounts: [ - { - path: "/drive", - options: { - type: "volume", - id: input.volumeId, - subpath: null, - readonly: false, + return { + wait: () => + daemon.then((daemon) => + daemon.wait().then(() => { + return { result: "" } + }), + ), + term: () => daemon.then((daemon) => daemon.term()), + } + }, + async chown(input: { + volumeId: string + path: string + uid: string + }): Promise { + await startSdk + .runCommand( + effects, + { id: manifest.main.image }, + ["chown", "--recursive", input.uid, `/drive/${input.path}`], + { + mounts: [ + { + path: "/drive", + options: { + type: "volume", + id: input.volumeId, + subpath: null, + readonly: false, + }, }, - }, - ], - }, - ) - .then((x: any) => ({ - stderr: x.stderr.toString(), - stdout: x.stdout.toString(), - })) - .then((x: any) => { - if (!!x.stderr) { - throw new Error(x.stderr) - } - }) - return null - } - sleep(timeMs: number): Promise { - return new Promise((resolve) => setTimeout(resolve, timeMs)) - } - trace(whatToPrint: string): void { - console.trace(whatToPrint) - } - warn(whatToPrint: string): void { - console.warn(whatToPrint) - } - error(whatToPrint: string): void { - console.error(whatToPrint) - } - debug(whatToPrint: string): void { - console.debug(whatToPrint) - } - info(whatToPrint: string): void { - console.log(false) - } - is_sandboxed(): boolean { - return false - } - exists(input: { volumeId: string; path: string }): Promise { - return this.metadata(input) - .then(() => true) - .catch(() => false) - } - async fetch( - url: string, - options?: - | { - method?: - | "GET" - | "POST" - | "PUT" - | "DELETE" - | "HEAD" - | "PATCH" - | undefined - headers?: Record | undefined - body?: string | undefined - } - | undefined, - ): Promise<{ - method: string - ok: boolean - status: number - headers: Record - body?: string | null | undefined - text(): Promise - json(): Promise - }> { - const fetched = await fetch(url, options) - return { - method: fetched.type, - ok: fetched.ok, - status: fetched.status, - headers: Object.fromEntries(fetched.headers.entries()), - body: await fetched.text(), - text: () => fetched.text(), - json: () => fetched.json(), - } - } - - runRsync(rsyncOptions: { - srcVolume: string - dstVolume: string - srcPath: string - dstPath: string - options: oet.BackupOptions - }): { - id: () => Promise - wait: () => Promise - progress: () => Promise - } { - let secondRun: ReturnType | undefined - let firstRun = this._runRsync(rsyncOptions) - let waitValue = firstRun.wait().then((x) => { - secondRun = this._runRsync(rsyncOptions) - return secondRun.wait() - }) - const id = async () => { - return secondRun?.id?.() ?? firstRun.id() - } - const wait = () => waitValue - const progress = async () => { - const secondProgress = secondRun?.progress?.() - if (secondProgress) { - return (await secondProgress) / 2.0 + 0.5 - } - return (await firstRun.progress()) / 2.0 - } - return { id, wait, progress } - } - _runRsync(rsyncOptions: { - srcVolume: string - dstVolume: string - srcPath: string - dstPath: string - options: oet.BackupOptions - }): { - id: () => Promise - wait: () => Promise - progress: () => Promise - } { - const { srcVolume, dstVolume, srcPath, dstPath, options } = rsyncOptions - const command = "rsync" - const args: string[] = [] - if (options.delete) { - args.push("--delete") - } - if (options.force) { - args.push("--force") - } - if (options.ignoreExisting) { - args.push("--ignore-existing") - } - for (const exclude of options.exclude) { - args.push(`--exclude=${exclude}`) - } - args.push("-actAXH") - args.push("--info=progress2") - args.push("--no-inc-recursive") - args.push(new Volume(srcVolume, srcPath).path) - args.push(new Volume(dstVolume, dstPath).path) - const spawned = child_process.spawn(command, args, { detached: true }) - let percentage = 0.0 - spawned.stdout.on("data", (data: unknown) => { - const lines = String(data).replace("\r", "\n").split("\n") - for (const line of lines) { - const parsed = /$([0-9.]+)%/.exec(line)?.[1] - if (!parsed) continue - percentage = Number.parseFloat(parsed) - } - }) - - spawned.stderr.on("data", (data: unknown) => { - console.error(String(data)) - }) - - const id = async () => { - const pid = spawned.pid - if (pid === undefined) { - throw new Error("rsync process has no pid") - } - return String(pid) - } - const waitPromise = new Promise((resolve, reject) => { - spawned.on("exit", (code: any) => { - if (code === 0) { - resolve(null) - } else { - reject(new Error(`rsync exited with code ${code}`)) - } - }) - }) - const wait = () => waitPromise - const progress = () => Promise.resolve(percentage) - return { id, wait, progress } - } - async diskUsage( - options?: { volumeId: string; path: string } | undefined, - ): Promise<{ used: number; total: number }> { - const output = await execFile("df", ["--block-size=1", "-P", "/"]) - .then((x: any) => ({ - stderr: x.stderr.toString(), - stdout: x.stdout.toString(), - })) - .then((x: any) => { - if (!!x.stderr) { - throw new Error(x.stderr) - } - return parseDfOutput(x.stdout) - }) - if (!!options) { - const used = await execFile("du", [ - "-s", - "--block-size=1", - "-P", - new Volume(options.volumeId, options.path).path, - ]) + ], + }, + ) .then((x: any) => ({ stderr: x.stderr.toString(), stdout: x.stdout.toString(), @@ -396,15 +181,244 @@ export class PolyfillEffects implements oet.Effects { if (!!x.stderr) { throw new Error(x.stderr) } - return Number.parseInt(x.stdout.split(/\s+/)[0]) }) + return null + }, + async chmod(input: { + volumeId: string + path: string + mode: string + }): Promise { + await startSdk + .runCommand( + effects, + { id: manifest.main.image }, + ["chmod", "--recursive", input.mode, `/drive/${input.path}`], + { + mounts: [ + { + path: "/drive", + options: { + type: "volume", + id: input.volumeId, + subpath: null, + readonly: false, + }, + }, + ], + }, + ) + .then((x: any) => ({ + stderr: x.stderr.toString(), + stdout: x.stdout.toString(), + })) + .then((x: any) => { + if (!!x.stderr) { + throw new Error(x.stderr) + } + }) + return null + }, + sleep(timeMs: number): Promise { + return new Promise((resolve) => setTimeout(resolve, timeMs)) + }, + trace(whatToPrint: string): void { + console.trace(whatToPrint) + }, + warn(whatToPrint: string): void { + console.warn(whatToPrint) + }, + error(whatToPrint: string): void { + console.error(whatToPrint) + }, + debug(whatToPrint: string): void { + console.debug(whatToPrint) + }, + info(whatToPrint: string): void { + console.log(false) + }, + is_sandboxed(): boolean { + return false + }, + exists(input: { volumeId: string; path: string }): Promise { + return self + .metadata(input) + .then(() => true) + .catch(() => false) + }, + async fetch( + url: string, + options?: + | { + method?: + | "GET" + | "POST" + | "PUT" + | "DELETE" + | "HEAD" + | "PATCH" + | undefined + headers?: Record | undefined + body?: string | undefined + } + | undefined, + ): Promise<{ + method: string + ok: boolean + status: number + headers: Record + body?: string | null | undefined + text(): Promise + json(): Promise + }> { + const fetched = await fetch(url, options) return { - ...output, - used, + method: fetched.type, + ok: fetched.ok, + status: fetched.status, + headers: Object.fromEntries(fetched.headers.entries()), + body: await fetched.text(), + text: () => fetched.text(), + json: () => fetched.json(), } - } - return output + }, + + runRsync(rsyncOptions: { + srcVolume: string + dstVolume: string + srcPath: string + dstPath: string + options: oet.BackupOptions + }): { + id: () => Promise + wait: () => Promise + progress: () => Promise + } { + let secondRun: ReturnType | undefined + let firstRun = self._runRsync(rsyncOptions) + let waitValue = firstRun.wait().then((x) => { + secondRun = self._runRsync(rsyncOptions) + return secondRun.wait() + }) + const id = async () => { + return secondRun?.id?.() ?? firstRun.id() + } + const wait = () => waitValue + const progress = async () => { + const secondProgress = secondRun?.progress?.() + if (secondProgress) { + return (await secondProgress) / 2.0 + 0.5 + } + return (await firstRun.progress()) / 2.0 + } + return { id, wait, progress } + }, + _runRsync(rsyncOptions: { + srcVolume: string + dstVolume: string + srcPath: string + dstPath: string + options: oet.BackupOptions + }): { + id: () => Promise + wait: () => Promise + progress: () => Promise + } { + const { srcVolume, dstVolume, srcPath, dstPath, options } = rsyncOptions + const command = "rsync" + const args: string[] = [] + if (options.delete) { + args.push("--delete") + } + if (options.force) { + args.push("--force") + } + if (options.ignoreExisting) { + args.push("--ignore-existing") + } + for (const exclude of options.exclude) { + args.push(`--exclude=${exclude}`) + } + args.push("-actAXH") + args.push("--info=progress2") + args.push("--no-inc-recursive") + args.push(new Volume(srcVolume, srcPath).path) + args.push(new Volume(dstVolume, dstPath).path) + const spawned = child_process.spawn(command, args, { detached: true }) + let percentage = 0.0 + spawned.stdout.on("data", (data: unknown) => { + const lines = String(data).replace("\r", "\n").split("\n") + for (const line of lines) { + const parsed = /$([0-9.]+)%/.exec(line)?.[1] + if (!parsed) continue + percentage = Number.parseFloat(parsed) + } + }) + + spawned.stderr.on("data", (data: unknown) => { + console.error(String(data)) + }) + + const id = async () => { + const pid = spawned.pid + if (pid === undefined) { + throw new Error("rsync process has no pid") + } + return String(pid) + } + const waitPromise = new Promise((resolve, reject) => { + spawned.on("exit", (code: any) => { + if (code === 0) { + resolve(null) + } else { + reject(new Error(`rsync exited with code ${code}`)) + } + }) + }) + const wait = () => waitPromise + const progress = () => Promise.resolve(percentage) + return { id, wait, progress } + }, + async diskUsage( + options?: { volumeId: string; path: string } | undefined, + ): Promise<{ used: number; total: number }> { + const output = await execFile("df", ["--block-size=1", "-P", "/"]) + .then((x: any) => ({ + stderr: x.stderr.toString(), + stdout: x.stdout.toString(), + })) + .then((x: any) => { + if (!!x.stderr) { + throw new Error(x.stderr) + } + return parseDfOutput(x.stdout) + }) + if (!!options) { + const used = await execFile("du", [ + "-s", + "--block-size=1", + "-P", + new Volume(options.volumeId, options.path).path, + ]) + .then((x: any) => ({ + stderr: x.stderr.toString(), + stdout: x.stdout.toString(), + })) + .then((x: any) => { + if (!!x.stderr) { + throw new Error(x.stderr) + } + return Number.parseInt(x.stdout.split(/\s+/)[0]) + }) + return { + ...output, + used, + } + } + return output + }, } + return self } function parseDfOutput(output: string): { used: number; total: number } { diff --git a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts index bf27f222d..e10434032 100644 --- a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts +++ b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts @@ -1,7 +1,7 @@ import { ExecuteResult, System } from "../../Interfaces/System" import { unNestPath } from "../../Models/JsonPath" import matches, { any, number, object, string, tuple } from "ts-matches" -import { HostSystemStartOs } from "../HostSystemStartOs" +import { hostSystemStartOs } from "../HostSystemStartOs" import { Effects } from "../../Models/Effects" import { RpcResult, matchRpcResult } from "../RpcListener" import { duration } from "../../Models/Duration" @@ -15,7 +15,7 @@ export class SystemForStartOs implements System { } constructor(readonly abi: T.ABI) {} async execute( - effects: HostSystemStartOs, + effectCreator: ReturnType, options: { id: string procedure: @@ -36,8 +36,7 @@ export class SystemForStartOs implements System { timeout?: number | undefined }, ): Promise { - effects = Object.create(effects) - effects.procedureId = options.id + const effects = effectCreator(options.id) return this._execute(effects, options) .then((x) => matches(x) diff --git a/container-runtime/src/Interfaces/HostSystem.ts b/container-runtime/src/Interfaces/HostSystem.ts index 4e04bbcc8..4ba986e3b 100644 --- a/container-runtime/src/Interfaces/HostSystem.ts +++ b/container-runtime/src/Interfaces/HostSystem.ts @@ -2,6 +2,7 @@ import { types as T } from "@start9labs/start-sdk" import { CallbackHolder } from "../Models/CallbackHolder" import { Effects } from "../Models/Effects" - export type HostSystem = Effects -export type GetHostSystem = (callbackHolder: CallbackHolder) => HostSystem +export type GetHostSystem = ( + callbackHolder: CallbackHolder, +) => (procedureId: null | string) => Effects diff --git a/container-runtime/src/Interfaces/System.ts b/container-runtime/src/Interfaces/System.ts index 85ba0fb0f..986288411 100644 --- a/container-runtime/src/Interfaces/System.ts +++ b/container-runtime/src/Interfaces/System.ts @@ -1,7 +1,7 @@ import { types as T } from "@start9labs/start-sdk" import { JsonPath } from "../Models/JsonPath" -import { HostSystemStartOs } from "../Adapters/HostSystemStartOs" import { RpcResult } from "../Adapters/RpcListener" +import { hostSystemStartOs } from "../Adapters/HostSystemStartOs" export type ExecuteResult = | { ok: unknown } | { err: { code: number; message: string } } @@ -12,7 +12,7 @@ export interface System { // stop(effects: Effects, options: { timeout: number, signal?: number }): Promise execute( - effects: T.Effects, + effectCreator: ReturnType, options: { id: string procedure: JsonPath diff --git a/container-runtime/src/index.ts b/container-runtime/src/index.ts index d86111ecb..74be5b73a 100644 --- a/container-runtime/src/index.ts +++ b/container-runtime/src/index.ts @@ -1,12 +1,12 @@ import { RpcListener } from "./Adapters/RpcListener" import { SystemForEmbassy } from "./Adapters/Systems/SystemForEmbassy" -import { HostSystemStartOs } from "./Adapters/HostSystemStartOs" +import { hostSystemStartOs } from "./Adapters/HostSystemStartOs" import { AllGetDependencies } from "./Interfaces/AllGetDependencies" import { getSystem } from "./Adapters/Systems" const getDependencies: AllGetDependencies = { system: getSystem, - hostSystem: () => HostSystemStartOs.of, + hostSystem: () => hostSystemStartOs, } new RpcListener(getDependencies) diff --git a/download-firmware.sh b/download-firmware.sh index 2457b3062..be72e6a6d 100755 --- a/download-firmware.sh +++ b/download-firmware.sh @@ -16,7 +16,8 @@ mkdir -p ./firmware/$PLATFORM cd ./firmware/$PLATFORM -mapfile -t firmwares <<< "$(jq -c ".[] | select(.platform[] | contains(\"$PLATFORM\"))" ../../build/lib/firmware.json)" +firmwares=() +while IFS= read -r line; do firmwares+=("$line"); done < <(jq -c ".[] | select(.platform[] | contains(\"$PLATFORM\"))" ../../build/lib/firmware.json) for firmware in "${firmwares[@]}"; do if [ -n "$firmware" ]; then id=$(echo "$firmware" | jq --raw-output '.id') From e92d4ff1471ec5c3fea8edb7962b2427a1c1fb5e Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Mon, 17 Jun 2024 10:37:57 -0600 Subject: [PATCH 035/125] fix compat assets (#2645) * fix compat assets * return error on s9pk parse fail in sideload * return parse error over websocket --- core/startos/src/install/mod.rs | 40 ++++++++++++++++++++++-------- core/startos/src/s9pk/v2/compat.rs | 4 ++- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/core/startos/src/install/mod.rs b/core/startos/src/install/mod.rs index bb7d1a02e..707503615 100644 --- a/core/startos/src/install/mod.rs +++ b/core/startos/src/install/mod.rs @@ -198,12 +198,22 @@ pub async fn sideload(ctx: RpcContext) -> Result { use axum::extract::ws::Message; async move { if let Err(e) = async { - let id = id_recv.await.map_err(|_| { + let id = match id_recv.await.map_err(|_| { Error::new( eyre!("Could not get id to watch progress"), ErrorKind::Cancelled, ) - })?; + }).and_then(|a|a) { + Ok(a) => a, + Err(e) =>{ ws.send(Message::Text( + serde_json::to_string(&Err::<(), _>(RpcError::from(e.clone_output()))) + .with_kind(ErrorKind::Serialization)?, + )) + .await + .with_kind(ErrorKind::Network)?; + return Err(e); + } + }; tokio::select! { res = async { while let Some(_) = sub.recv().await { @@ -259,17 +269,25 @@ pub async fn sideload(ctx: RpcContext) -> Result { .await; tokio::spawn(async move { if let Err(e) = async { - let s9pk = S9pk::deserialize( + match S9pk::deserialize( &file, None, // TODO ) - .await?; - let _ = id_send.send(s9pk.as_manifest().id.clone()); - ctx.services - .install(ctx.clone(), s9pk, None::) - .await? - .await? - .await?; - file.delete().await + .await + { + Ok(s9pk) => { + let _ = id_send.send(Ok(s9pk.as_manifest().id.clone())); + ctx.services + .install(ctx.clone(), s9pk, None::) + .await? + .await? + .await?; + file.delete().await + } + Err(e) => { + let _ = id_send.send(Err(e.clone_output())); + return Err(e); + } + } } .await { diff --git a/core/startos/src/s9pk/v2/compat.rs b/core/startos/src/s9pk/v2/compat.rs index 835c86b87..ec5b586ea 100644 --- a/core/startos/src/s9pk/v2/compat.rs +++ b/core/startos/src/s9pk/v2/compat.rs @@ -209,7 +209,9 @@ impl S9pk> { .invoke(ErrorKind::Filesystem) .await?; archive.insert_path( - Path::new("assets").join(&asset_id), + Path::new("assets") + .join(&asset_id) + .with_extension("squashfs"), Entry::file(PackSource::File(sqfs_path)), )?; } From da3720c7a9e158d64a6d9825f883f972597e05ae Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Wed, 19 Jun 2024 13:51:44 -0600 Subject: [PATCH 036/125] Feat/combine uis (#2633) * wip * restructure backend for new ui structure * new patchdb bootstrap, single websocket api, local storage migration, more * update db websocket * init apis * update patch-db * setup progress * feat: implement state service, alert and routing Signed-off-by: waterplea * update setup wizard for new types * feat: add init page Signed-off-by: waterplea * chore: refactor message, patch-db source stream and connection service Signed-off-by: waterplea * fix method not found on state * fix backend bugs * fix compat assets * address comments * remove unneeded styling * cleaner progress * bugfixes * fix init logs * fix progress reporting * fix navigation by getting state after init * remove patch dependency from live api * fix caching * re-add patchDB to live api * fix metrics values * send close frame * add bootId and fix polling --------- Signed-off-by: waterplea Co-authored-by: Aiden McClelland Co-authored-by: waterplea --- Makefile | 7 +- core/Cargo.lock | 718 ++++++++++++------ core/startos/Cargo.toml | 4 +- core/startos/src/backup/restore.rs | 60 +- core/startos/src/bins/registry.rs | 3 +- core/startos/src/bins/start_init.rs | 211 +++-- core/startos/src/bins/startd.rs | 76 +- core/startos/src/context/cli.rs | 7 +- core/startos/src/context/diagnostic.rs | 2 +- core/startos/src/context/init.rs | 47 ++ core/startos/src/context/install.rs | 9 + core/startos/src/context/mod.rs | 2 + core/startos/src/context/rpc.rs | 114 ++- core/startos/src/context/setup.rs | 147 +++- core/startos/src/db/mod.rs | 244 ++---- core/startos/src/diagnostic.rs | 13 +- core/startos/src/disk/main.rs | 2 +- core/startos/src/firmware.rs | 83 +- core/startos/src/init.rs | 323 +++++++- core/startos/src/install/mod.rs | 30 +- core/startos/src/lib.rs | 87 ++- core/startos/src/lxc/mod.rs | 80 +- core/startos/src/middleware/auth.rs | 18 +- core/startos/src/middleware/diagnostic.rs | 42 - core/startos/src/middleware/mod.rs | 1 - core/startos/src/net/net_controller.rs | 53 +- core/startos/src/net/refresher.html | 11 + core/startos/src/net/static_server.rs | 359 ++++----- core/startos/src/net/web_server.rs | 103 ++- core/startos/src/progress.rs | 163 ++-- core/startos/src/registry/context.rs | 3 +- core/startos/src/registry/mod.rs | 6 +- core/startos/src/registry/os/asset/add.rs | 27 +- core/startos/src/registry/os/asset/get.rs | 26 +- core/startos/src/registry/os/asset/sign.rs | 27 +- core/startos/src/registry/package/add.rs | 29 +- .../src/registry/signer/commitment/request.rs | 2 +- core/startos/src/rpc_continuations.rs | 131 +++- .../source/multi_cursor_file.rs | 1 + core/startos/src/service/mod.rs | 2 +- core/startos/src/service/service_map.rs | 35 +- core/startos/src/setup.rs | 361 ++++----- core/startos/src/update/mod.rs | 103 ++- core/startos/src/upload.rs | 101 +-- core/startos/src/util/io.rs | 77 +- core/startos/src/util/mod.rs | 1 + core/startos/src/util/net.rs | 24 + core/startos/src/util/serde.rs | 12 +- core/startos/src/version/mod.rs | 34 +- core/startos/src/volume.rs | 6 +- patch-db | 2 +- sdk/lib/osBindings/AttachParams.ts | 7 + sdk/lib/osBindings/BackupTargetFS.ts | 7 + sdk/lib/osBindings/BlockDev.ts | 3 + sdk/lib/osBindings/Cifs.ts | 8 + sdk/lib/osBindings/InitProgressRes.ts | 5 + sdk/lib/osBindings/RecoverySource.ts | 6 + sdk/lib/osBindings/SetupExecuteParams.ts | 10 + sdk/lib/osBindings/SetupProgress.ts | 5 + sdk/lib/osBindings/SetupResult.ts | 7 + sdk/lib/osBindings/SetupStatusRes.ts | 7 + sdk/lib/osBindings/VerifyCifsParams.ts | 9 + sdk/lib/osBindings/index.ts | 11 + web/package-lock.json | 19 +- web/package.json | 3 +- .../src/app/app-routing.module.ts | 27 - .../diagnostic-ui/src/app/app.component.html | 5 - .../diagnostic-ui/src/app/app.component.scss | 8 - .../diagnostic-ui/src/app/app.component.ts | 10 - .../diagnostic-ui/src/app/app.module.ts | 43 -- .../src/app/pages/home/home-routing.module.ts | 16 - .../src/app/services/api/api.service.ts | 16 - .../src/app/services/api/live-api.service.ts | 68 -- .../src/app/services/api/mock-api.service.ts | 67 -- .../src/environments/environment.prod.ts | 3 - .../src/environments/environment.ts | 16 - web/projects/diagnostic-ui/src/index.html | 23 - web/projects/diagnostic-ui/src/main.ts | 12 - web/projects/diagnostic-ui/src/polyfills.ts | 64 -- web/projects/diagnostic-ui/src/styles.scss | 41 - web/projects/diagnostic-ui/src/zone-flags.ts | 6 - web/projects/diagnostic-ui/tsconfig.json | 9 - .../setup-wizard/src/app/app.component.ts | 2 +- .../src/app/pages/embassy/embassy.page.ts | 24 +- .../src/app/pages/loading/loading.module.ts | 4 +- .../src/app/pages/loading/loading.page.html | 54 +- .../src/app/pages/loading/loading.page.scss | 3 - .../src/app/pages/loading/loading.page.ts | 162 ++-- .../src/app/services/api/api.service.ts | 63 +- .../src/app/services/api/live-api.service.ts | 60 +- .../src/app/services/api/mock-api.service.ts | 186 ++++- .../src/app/services/state.service.ts | 6 +- web/projects/shared/src/types/api.ts | 1 + web/projects/ui/src/app/app-routing.module.ts | 27 +- web/projects/ui/src/app/app.component.html | 1 + web/projects/ui/src/app/app.component.scss | 6 +- web/projects/ui/src/app/app.component.ts | 20 +- web/projects/ui/src/app/app.module.ts | 2 + web/projects/ui/src/app/app.providers.ts | 7 +- .../ui/src/app/app/menu/menu.component.ts | 4 +- .../connection-bar.component.ts | 12 +- .../src/app/components/logs/logs.component.ts | 68 +- .../components/status/status.component.html | 4 +- .../app/components/status/status.component.ts | 4 +- .../app/modals/os-update/os-update.page.ts | 3 +- .../app-list-icon.component.html | 2 +- .../app-list-icon/app-list-icon.component.ts | 4 +- .../app-show-health-checks.component.html | 2 +- .../app-show-health-checks.component.ts | 4 +- .../app-show-status.component.html | 2 +- .../app-show-status.component.ts | 4 +- .../diagnostic-routing.module.ts | 21 + .../diagnostic-routes}/home/home.module.ts | 12 +- .../diagnostic-routes}/home/home.page.html | 6 - .../diagnostic-routes}/home/home.page.scss | 0 .../diagnostic-routes}/home/home.page.ts | 59 +- .../diagnostic-routes}/logs/logs.module.ts | 0 .../diagnostic-routes}/logs/logs.page.html | 0 .../diagnostic-routes}/logs/logs.page.scss | 0 .../diagnostic-routes}/logs/logs.page.ts | 4 +- .../ui/src/app/pages/init/init.module.ts | 24 + .../ui/src/app/pages/init/init.page.html | 18 + .../ui/src/app/pages/init/init.page.scss | 23 + .../ui/src/app/pages/init/init.page.ts | 11 + .../ui/src/app/pages/init/init.service.ts | 91 +++ .../src/app/pages/init/logs/logs.component.ts | 33 + .../ui/src/app/pages/init/logs/logs.module.ts | 20 + .../src/app/pages/init/logs/logs.service.ts | 49 ++ .../app/pages/init/logs/logs.template.html | 9 + .../login/ca-wizard/ca-wizard.component.ts | 2 +- .../server-metrics/server-metrics.page.html | 4 +- .../server-show/server-show.page.ts | 49 -- .../ui/src/app/services/api/api.fixures.ts | 21 +- .../ui/src/app/services/api/api.types.ts | 47 +- .../app/services/api/embassy-api.service.ts | 53 +- .../services/api/embassy-live-api.service.ts | 133 ++-- .../services/api/embassy-mock-api.service.ts | 246 ++++-- .../ui/src/app/services/auth.service.ts | 2 +- .../ui/src/app/services/connection.service.ts | 30 +- .../ui/src/app/services/eos.service.ts | 11 +- .../ui/src/app/services/network.service.ts | 22 + .../ui/src/app/services/patch-data.service.ts | 27 +- .../patch-db/local-storage-bootstrap.ts | 16 +- .../app/services/patch-db/patch-db.factory.ts | 56 +- .../src/app/services/patch-monitor.service.ts | 9 +- .../ui/src/app/services/state.service.ts | 136 ++++ .../ui/src/app/services/storage.service.ts | 21 +- 147 files changed, 3939 insertions(+), 2637 deletions(-) create mode 100644 core/startos/src/context/init.rs delete mode 100644 core/startos/src/middleware/diagnostic.rs create mode 100644 core/startos/src/net/refresher.html create mode 100644 core/startos/src/util/net.rs create mode 100644 sdk/lib/osBindings/AttachParams.ts create mode 100644 sdk/lib/osBindings/BackupTargetFS.ts create mode 100644 sdk/lib/osBindings/BlockDev.ts create mode 100644 sdk/lib/osBindings/Cifs.ts create mode 100644 sdk/lib/osBindings/InitProgressRes.ts create mode 100644 sdk/lib/osBindings/RecoverySource.ts create mode 100644 sdk/lib/osBindings/SetupExecuteParams.ts create mode 100644 sdk/lib/osBindings/SetupProgress.ts create mode 100644 sdk/lib/osBindings/SetupResult.ts create mode 100644 sdk/lib/osBindings/SetupStatusRes.ts create mode 100644 sdk/lib/osBindings/VerifyCifsParams.ts delete mode 100644 web/projects/diagnostic-ui/src/app/app-routing.module.ts delete mode 100644 web/projects/diagnostic-ui/src/app/app.component.html delete mode 100644 web/projects/diagnostic-ui/src/app/app.component.scss delete mode 100644 web/projects/diagnostic-ui/src/app/app.component.ts delete mode 100644 web/projects/diagnostic-ui/src/app/app.module.ts delete mode 100644 web/projects/diagnostic-ui/src/app/pages/home/home-routing.module.ts delete mode 100644 web/projects/diagnostic-ui/src/app/services/api/api.service.ts delete mode 100644 web/projects/diagnostic-ui/src/app/services/api/live-api.service.ts delete mode 100644 web/projects/diagnostic-ui/src/app/services/api/mock-api.service.ts delete mode 100644 web/projects/diagnostic-ui/src/environments/environment.prod.ts delete mode 100644 web/projects/diagnostic-ui/src/environments/environment.ts delete mode 100644 web/projects/diagnostic-ui/src/index.html delete mode 100644 web/projects/diagnostic-ui/src/main.ts delete mode 100644 web/projects/diagnostic-ui/src/polyfills.ts delete mode 100644 web/projects/diagnostic-ui/src/styles.scss delete mode 100644 web/projects/diagnostic-ui/src/zone-flags.ts delete mode 100644 web/projects/diagnostic-ui/tsconfig.json create mode 100644 web/projects/ui/src/app/pages/diagnostic-routes/diagnostic-routing.module.ts rename web/projects/{diagnostic-ui/src/app/pages => ui/src/app/pages/diagnostic-routes}/home/home.module.ts (55%) rename web/projects/{diagnostic-ui/src/app/pages => ui/src/app/pages/diagnostic-routes}/home/home.page.html (92%) rename web/projects/{diagnostic-ui/src/app/pages => ui/src/app/pages/diagnostic-routes}/home/home.page.scss (100%) rename web/projects/{diagnostic-ui/src/app/pages => ui/src/app/pages/diagnostic-routes}/home/home.page.ts (74%) rename web/projects/{diagnostic-ui/src/app/pages => ui/src/app/pages/diagnostic-routes}/logs/logs.module.ts (100%) rename web/projects/{diagnostic-ui/src/app/pages => ui/src/app/pages/diagnostic-routes}/logs/logs.page.html (100%) rename web/projects/{diagnostic-ui/src/app/pages => ui/src/app/pages/diagnostic-routes}/logs/logs.page.scss (100%) rename web/projects/{diagnostic-ui/src/app/pages => ui/src/app/pages/diagnostic-routes}/logs/logs.page.ts (93%) create mode 100644 web/projects/ui/src/app/pages/init/init.module.ts create mode 100644 web/projects/ui/src/app/pages/init/init.page.html create mode 100644 web/projects/ui/src/app/pages/init/init.page.scss create mode 100644 web/projects/ui/src/app/pages/init/init.page.ts create mode 100644 web/projects/ui/src/app/pages/init/init.service.ts create mode 100644 web/projects/ui/src/app/pages/init/logs/logs.component.ts create mode 100644 web/projects/ui/src/app/pages/init/logs/logs.module.ts create mode 100644 web/projects/ui/src/app/pages/init/logs/logs.service.ts create mode 100644 web/projects/ui/src/app/pages/init/logs/logs.template.html create mode 100644 web/projects/ui/src/app/services/network.service.ts create mode 100644 web/projects/ui/src/app/services/state.service.ts diff --git a/Makefile b/Makefile index 34064e799..4915101e3 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ PLATFORM := $(shell if [ -f ./PLATFORM.txt ]; then cat ./PLATFORM.txt; else echo ARCH := $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo aarch64; else echo $(PLATFORM) | sed 's/-nonfree$$//g'; fi) IMAGE_TYPE=$(shell if [ "$(PLATFORM)" = raspberrypi ]; then echo img; else echo iso; fi) BINS := core/target/$(ARCH)-unknown-linux-musl/release/startbox core/target/$(ARCH)-unknown-linux-musl/release/containerbox -WEB_UIS := web/dist/raw/ui web/dist/raw/setup-wizard web/dist/raw/diagnostic-ui web/dist/raw/install-wizard +WEB_UIS := web/dist/raw/ui web/dist/raw/setup-wizard web/dist/raw/install-wizard FIRMWARE_ROMS := ./firmware/$(PLATFORM) $(shell jq --raw-output '.[] | select(.platform[] | contains("$(PLATFORM)")) | "./firmware/$(PLATFORM)/" + .id + ".rom.gz"' build/lib/firmware.json) BUILD_SRC := $(shell git ls-files build) build/lib/depends build/lib/conflicts $(FIRMWARE_ROMS) DEBIAN_SRC := $(shell git ls-files debian/) @@ -20,7 +20,6 @@ CORE_SRC := $(shell git ls-files core) $(shell git ls-files --recurse-submodules WEB_SHARED_SRC := $(shell git ls-files web/projects/shared) $(shell ls -p web/ | grep -v / | sed 's/^/web\//g') web/node_modules/.package-lock.json web/config.json patch-db/client/dist web/patchdb-ui-seed.json WEB_UI_SRC := $(shell git ls-files web/projects/ui) WEB_SETUP_WIZARD_SRC := $(shell git ls-files web/projects/setup-wizard) -WEB_DIAGNOSTIC_UI_SRC := $(shell git ls-files web/projects/diagnostic-ui) WEB_INSTALL_WIZARD_SRC := $(shell git ls-files web/projects/install-wizard) PATCH_DB_CLIENT_SRC := $(shell git ls-files --recurse-submodules patch-db/client) GZIP_BIN := $(shell which pigz || which gzip) @@ -244,10 +243,6 @@ web/dist/raw/setup-wizard: $(WEB_SETUP_WIZARD_SRC) $(WEB_SHARED_SRC) npm --prefix web run build:setup touch web/dist/raw/setup-wizard -web/dist/raw/diagnostic-ui: $(WEB_DIAGNOSTIC_UI_SRC) $(WEB_SHARED_SRC) - npm --prefix web run build:dui - touch web/dist/raw/diagnostic-ui - web/dist/raw/install-wizard: $(WEB_INSTALL_WIZARD_SRC) $(WEB_SHARED_SRC) npm --prefix web run build:install-wiz touch web/dist/raw/install-wizard diff --git a/core/Cargo.lock b/core/Cargo.lock index cf10152c2..031ec41ab 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -42,7 +42,7 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.14", + "getrandom 0.2.15", "once_cell", "version_check", ] @@ -54,7 +54,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom 0.2.14", + "getrandom 0.2.15", "once_cell", "version_check", "zerocopy", @@ -137,9 +137,9 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ "windows-sys 0.52.0", ] @@ -156,9 +156,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.82" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arrayref" @@ -187,16 +187,16 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" dependencies = [ - "concurrent-queue", + "concurrent-queue 2.5.0", "event-listener", "futures-core", ] [[package]] name = "async-compression" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e9eabd7a98fe442131a17c316bd9349c43695e49e730c3c8e12cfb5f4da2693" +checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5" dependencies = [ "brotli", "flate2", @@ -225,7 +225,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -236,7 +236,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -248,6 +248,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atty" version = "0.2.14" @@ -278,7 +284,7 @@ dependencies = [ "futures-util", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.28", + "hyper 0.14.29", "itoa", "matchit", "memchr", @@ -418,6 +424,17 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "barrage" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be5951c75bdabb58753d140dd5802f12ff3a483cb2e16fb5276e111b94b19e87" +dependencies = [ + "concurrent-queue 1.2.4", + "event-listener", + "spin 0.9.8", +] + [[package]] name = "base16ct" version = "0.2.0" @@ -562,9 +579,9 @@ dependencies = [ [[package]] name = "brotli" -version = "5.0.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19483b140a7ac7174d34b5a581b406c64f84da5409d3e09cf4fff604f9270e67" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -573,9 +590,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "4.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6221fe77a248b9117d431ad93761222e1cf8ff282d9d1d5d9f53d6299a1cf76" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -600,10 +617,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] -name = "cc" -version = "1.0.96" +name = "cache-padded" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd" +checksum = "981520c98f422fcc584dc1a95c334e6953900b9106bc47a9839b81790009eb21" + +[[package]] +name = "cc" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" dependencies = [ "jobserver", "libc", @@ -688,9 +711,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.4" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" dependencies = [ "clap_builder", "clap_derive", @@ -698,9 +721,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" dependencies = [ "anstream", "anstyle", @@ -710,21 +733,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.4" +version = "4.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" [[package]] name = "color-eyre" @@ -759,6 +782,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "concurrent-queue" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c" +dependencies = [ + "cache-padded", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -945,18 +977,18 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] @@ -991,9 +1023,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crossterm" @@ -1117,14 +1149,14 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] name = "darling" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" dependencies = [ "darling_core", "darling_macro", @@ -1132,27 +1164,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim 0.10.0", - "syn 2.0.60", + "strsim 0.11.1", + "syn 2.0.66", ] [[package]] name = "darling_macro" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ "darling_core", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -1183,7 +1215,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -1206,7 +1238,7 @@ checksum = "5fe87ce4529967e0ba1dcf8450bab64d97dfd5010a6256187ffe2e43e6f0e049" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -1274,6 +1306,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "divrem" version = "1.0.0" @@ -1367,9 +1410,9 @@ dependencies = [ [[package]] name = "either" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" dependencies = [ "serde", ] @@ -1407,9 +1450,9 @@ dependencies = [ [[package]] name = "ena" -version = "0.14.2" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c533630cf40e9caa44bd91aadc88a75d75a4c3a12b4cfde353cbed41daa1e1f1" +checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" dependencies = [ "log", ] @@ -1444,7 +1487,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -1455,9 +1498,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1517,9 +1560,9 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38793c55593b33412e3ae40c2c9781ffaa6f438f6f8c10f24e71846fbd7ae01e" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "filetime" @@ -1533,12 +1576,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "finl_unicode" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" - [[package]] name = "fixedbitset" version = "0.4.2" @@ -1678,7 +1715,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -1735,9 +1772,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -1794,15 +1831,15 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" dependencies = [ + "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "futures-util", "http 1.1.0", "indexmap 2.2.6", "slab", @@ -2005,12 +2042,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", - "futures-core", + "futures-util", "http 1.1.0", "http-body 1.0.0", "pin-project-lite", @@ -2018,9 +2055,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "d0e7a4dd27b9476dc40cb050d3632d3bba3a70ddbff012285f7f8559a1e7e545" [[package]] name = "httpdate" @@ -2036,9 +2073,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.28" +version = "0.14.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" dependencies = [ "bytes", "futures-channel", @@ -2067,7 +2104,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.4", + "h2 0.4.5", "http 1.1.0", "http-body 1.0.0", "httparse", @@ -2085,7 +2122,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper 0.14.28", + "hyper 0.14.29", "pin-project-lite", "tokio", "tokio-io-timeout", @@ -2109,9 +2146,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" dependencies = [ "bytes", "futures-channel", @@ -2150,6 +2187,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "id-pool" version = "0.2.2" @@ -2187,12 +2342,14 @@ dependencies = [ [[package]] name = "idna" -version = "0.5.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "icu_normalizer", + "icu_properties", + "smallvec", + "utf8_iter", ] [[package]] @@ -2302,9 +2459,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", ] @@ -2535,7 +2692,7 @@ dependencies = [ "petgraph", "pico-args", "regex", - "regex-syntax 0.8.3", + "regex-syntax 0.8.4", "string_cache", "term", "tiny-keccak", @@ -2549,7 +2706,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" dependencies = [ - "regex-automata 0.4.6", + "regex-automata 0.4.7", ] [[package]] @@ -2579,9 +2736,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.154" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libm" @@ -2618,9 +2775,15 @@ checksum = "3e281a65eeba3d4503a2839252f86374528f9ceafe6fed97c1d3b52e1fb625c1" [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" [[package]] name = "lock_api" @@ -2734,9 +2897,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ "adler", ] @@ -2786,11 +2949,10 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" dependencies = [ - "lazy_static", "libc", "log", "openssl", @@ -2885,9 +3047,9 @@ dependencies = [ [[package]] name = "num" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3135b08af27d103b0a51f2ae0f8632117b7b185ccf931445affa8df530576a41" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ "num-bigint", "num-complex", @@ -2899,11 +3061,10 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -2927,9 +3088,9 @@ dependencies = [ [[package]] name = "num-complex" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", ] @@ -2962,11 +3123,10 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ - "autocfg", "num-bigint", "num-integer", "num-traits", @@ -3010,7 +3170,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -3076,7 +3236,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -3087,9 +3247,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.2.3+3.2.1" +version = "300.3.1+3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843" +checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91" dependencies = [ "cc", ] @@ -3159,9 +3319,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -3182,9 +3342,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "patch-db" @@ -3254,9 +3414,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", "indexmap 2.2.6", @@ -3294,7 +3454,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -3400,9 +3560,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] @@ -3421,7 +3581,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "rand_xorshift", - "regex-syntax 0.8.3", + "regex-syntax 0.8.4", "rusty-fork", "tempfile", "unarray", @@ -3440,9 +3600,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.12.4" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f5d036824e4761737860779c906171497f6d55681139d8312388f8fe398922" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" dependencies = [ "bytes", "prost-derive", @@ -3450,22 +3610,22 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.12.4" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19de2de2a00075bf566bee3bd4db014b11587e84184d3f7a791bc17f1a8e9e48" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] name = "prost-types" -version = "0.12.4" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3235c33eb02c1f1e212abdbe34c78b264b038fb58ca612664343271e36e55ffe" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" dependencies = [ "prost", ] @@ -3566,7 +3726,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.14", + "getrandom 0.2.15", ] [[package]] @@ -3655,21 +3815,21 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ - "getrandom 0.2.14", + "getrandom 0.2.15", "libredox", "thiserror", ] [[package]] name = "regex" -version = "1.10.4" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.6", - "regex-syntax 0.8.3", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", ] [[package]] @@ -3683,13 +3843,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.3", + "regex-syntax 0.8.4", ] [[package]] @@ -3700,9 +3860,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "reqwest" @@ -3717,7 +3877,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2 0.4.4", + "h2 0.4.5", "http 1.1.0", "http-body 1.0.0", "http-body-util", @@ -3781,7 +3941,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.14", + "getrandom 0.2.15", "libc", "spin 0.9.8", "untrusted", @@ -3802,7 +3962,7 @@ dependencies = [ [[package]] name = "rpc-toolkit" version = "0.2.3" -source = "git+https://github.com/Start9Labs/rpc-toolkit.git?branch=refactor/no-dyn-ctx#f5566840bb9af0743612fede4034f5a390cd1eee" +source = "git+https://github.com/Start9Labs/rpc-toolkit.git?branch=refactor/no-dyn-ctx#5a24903031e72ac75fd23889215361edc7b20842" dependencies = [ "async-stream", "async-trait", @@ -3871,9 +4031,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -3923,7 +4083,7 @@ dependencies = [ "log", "ring", "rustls-pki-types", - "rustls-webpki 0.102.3", + "rustls-webpki 0.102.4", "subtle", "zeroize", ] @@ -3949,9 +4109,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.5.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" @@ -3965,9 +4125,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.3" +version = "0.102.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" +checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" dependencies = [ "ring", "rustls-pki-types", @@ -4064,11 +4224,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "core-foundation", "core-foundation-sys", "libc", @@ -4077,9 +4237,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" dependencies = [ "core-foundation-sys", "libc", @@ -4087,9 +4247,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" dependencies = [ "serde", ] @@ -4128,7 +4288,7 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -4155,9 +4315,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde", ] @@ -4201,7 +4361,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -4396,11 +4556,10 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" +checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" dependencies = [ - "itertools 0.12.1", "nom", "unicode_categories", ] @@ -4629,7 +4788,7 @@ dependencies = [ "quote", "regex-syntax 0.6.29", "strsim 0.10.0", - "syn 2.0.60", + "syn 2.0.66", "unicode-width", ] @@ -4675,6 +4834,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "start-os" version = "0.3.5-rev.2" @@ -4686,6 +4851,7 @@ dependencies = [ "axum 0.7.5", "axum-server", "backhand", + "barrage", "base32", "base64 0.21.7", "base64ct", @@ -4783,8 +4949,9 @@ dependencies = [ "tokio-tar", "tokio-tungstenite", "tokio-util", - "toml 0.8.12", + "toml 0.8.14", "torut", + "tower-service", "tracing", "tracing-error", "tracing-futures", @@ -4827,13 +4994,13 @@ dependencies = [ [[package]] name = "stringprep" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" dependencies = [ - "finl_unicode", "unicode-bidi", "unicode-normalization", + "unicode-properties", ] [[package]] @@ -4867,9 +5034,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.60" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", @@ -4888,6 +5055,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -4917,9 +5095,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909" dependencies = [ "filetime", "libc", @@ -4970,22 +5148,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.59" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.59" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -5049,6 +5227,16 @@ dependencies = [ "crunchy", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -5066,9 +5254,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.37.0" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", @@ -5096,13 +5284,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -5180,16 +5368,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] @@ -5206,21 +5393,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.12" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.12", + "toml_edit 0.22.14", ] [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] @@ -5251,15 +5438,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.12" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.7", + "winnow 0.6.13", ] [[package]] @@ -5276,7 +5463,7 @@ dependencies = [ "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.28", + "hyper 0.14.29", "hyper-timeout", "percent-encoding", "pin-project", @@ -5360,7 +5547,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -5512,7 +5699,7 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", "termcolor", ] @@ -5553,7 +5740,7 @@ checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", ] [[package]] @@ -5598,6 +5785,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" + [[package]] name = "unicode-segmentation" version = "1.11.0" @@ -5606,9 +5799,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "unicode-xid" @@ -5630,12 +5823,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" dependencies = [ "form_urlencoded", - "idna 0.5.0", + "idna 1.0.0", "percent-encoding", "serde", ] @@ -5653,10 +5846,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] -name = "utf8parse" -version = "0.2.1" +name = "utf16_iter" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" @@ -5664,7 +5869,7 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ - "getrandom 0.2.14", + "getrandom 0.2.15", ] [[package]] @@ -5752,7 +5957,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", "wasm-bindgen-shared", ] @@ -5786,7 +5991,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6026,9 +6231,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.7" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14b9415ee827af173ebb3f15f9083df5a122eb93572ec28741fb153356ea2578" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" dependencies = [ "memchr", ] @@ -6043,6 +6248,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "wyz" version = "0.5.1" @@ -6106,30 +6323,75 @@ dependencies = [ ] [[package]] -name = "zerocopy" -version = "0.7.32" +name = "yoke" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", + "synstructure", ] [[package]] name = "zeroize" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" dependencies = [ "zeroize_derive", ] @@ -6142,7 +6404,29 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.66", +] + +[[package]] +name = "zerovec" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", ] [[package]] diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index 3ad4f9eeb..a8707bf65 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -59,6 +59,7 @@ async-stream = "0.3.5" async-trait = "0.1.74" axum = { version = "0.7.3", features = ["ws"] } axum-server = "0.6.0" +barrage = "0.2.3" backhand = "0.18.0" base32 = "0.4.0" base64 = "0.21.4" @@ -102,7 +103,7 @@ id-pool = { version = "0.2.2", default-features = false, features = [ ] } imbl = "2.0.2" imbl-value = { git = "https://github.com/Start9Labs/imbl-value.git" } -include_dir = "0.7.3" +include_dir = { version = "0.7.3", features = ["metadata"] } indexmap = { version = "2.0.2", features = ["serde"] } indicatif = { version = "0.17.7", features = ["tokio"] } integer-encoding = { version = "4.0.0", features = ["tokio_async"] } @@ -178,6 +179,7 @@ tokio-util = { version = "0.7.9", features = ["io"] } torut = { git = "https://github.com/Start9Labs/torut.git", branch = "update/dependencies", features = [ "serialize", ] } +tower-service = "0.3.2" tracing = "0.1.39" tracing-error = "0.2.0" tracing-futures = "0.2.5" diff --git a/core/startos/src/backup/restore.rs b/core/startos/src/backup/restore.rs index 4753a4290..556f750ec 100644 --- a/core/startos/src/backup/restore.rs +++ b/core/startos/src/backup/restore.rs @@ -4,25 +4,25 @@ use std::sync::Arc; use clap::Parser; use futures::{stream, StreamExt}; use models::PackageId; -use openssl::x509::X509; use patch_db::json_ptr::ROOT; use serde::{Deserialize, Serialize}; -use torut::onion::OnionAddressV3; +use tokio::sync::Mutex; use tracing::instrument; use ts_rs::TS; use super::target::BackupTargetId; use crate::backup::os::OsBackup; +use crate::context::setup::SetupResult; use crate::context::{RpcContext, SetupContext}; use crate::db::model::Database; use crate::disk::mount::backup::BackupMountGuard; use crate::disk::mount::filesystem::ReadWrite; use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; -use crate::hostname::Hostname; -use crate::init::init; +use crate::init::{init, InitResult}; use crate::prelude::*; use crate::s9pk::S9pk; use crate::service::service_map::DownloadInstallFuture; +use crate::setup::SetupExecuteProgress; use crate::util::serde::IoFormat; #[derive(Deserialize, Serialize, Parser, TS)] @@ -67,14 +67,21 @@ pub async fn restore_packages_rpc( Ok(()) } -#[instrument(skip(ctx))] +#[instrument(skip_all)] pub async fn recover_full_embassy( - ctx: SetupContext, + ctx: &SetupContext, disk_guid: Arc, start_os_password: String, recovery_source: TmpMountGuard, recovery_password: Option, -) -> Result<(Arc, Hostname, OnionAddressV3, X509), Error> { + SetupExecuteProgress { + init_phases, + restore_phase, + rpc_ctx_phases, + }: SetupExecuteProgress, +) -> Result<(SetupResult, RpcContext), Error> { + let mut restore_phase = restore_phase.or_not_found("restore progress")?; + let backup_guard = BackupMountGuard::mount( recovery_source, recovery_password.as_deref().unwrap_or_default(), @@ -99,10 +106,17 @@ pub async fn recover_full_embassy( db.put(&ROOT, &Database::init(&os_backup.account)?).await?; drop(db); - init(&ctx.config).await?; + let InitResult { net_ctrl } = init(&ctx.config, init_phases).await?; - let rpc_ctx = RpcContext::init(&ctx.config, disk_guid.clone()).await?; + let rpc_ctx = RpcContext::init( + &ctx.config, + disk_guid.clone(), + Some(net_ctrl), + rpc_ctx_phases, + ) + .await?; + restore_phase.start(); let ids: Vec<_> = backup_guard .metadata .package_backups @@ -110,26 +124,26 @@ pub async fn recover_full_embassy( .cloned() .collect(); let tasks = restore_packages(&rpc_ctx, backup_guard, ids).await?; + restore_phase.set_total(tasks.len() as u64); + let restore_phase = Arc::new(Mutex::new(restore_phase)); stream::iter(tasks) - .for_each_concurrent(5, |(id, res)| async move { - match async { res.await?.await }.await { - Ok(_) => (), - Err(err) => { - tracing::error!("Error restoring package {}: {}", id, err); - tracing::debug!("{:?}", err); + .for_each_concurrent(5, |(id, res)| { + let restore_phase = restore_phase.clone(); + async move { + match async { res.await?.await }.await { + Ok(_) => (), + Err(err) => { + tracing::error!("Error restoring package {}: {}", id, err); + tracing::debug!("{:?}", err); + } } + *restore_phase.lock().await += 1; } }) .await; + restore_phase.lock().await.complete(); - rpc_ctx.shutdown().await?; - - Ok(( - disk_guid, - os_backup.account.hostname, - os_backup.account.tor_key.public().get_onion_address(), - os_backup.account.root_ca_cert, - )) + Ok(((&os_backup.account).try_into()?, rpc_ctx)) } #[instrument(skip(ctx, backup_guard))] diff --git a/core/startos/src/bins/registry.rs b/core/startos/src/bins/registry.rs index 3028d1766..132e0984a 100644 --- a/core/startos/src/bins/registry.rs +++ b/core/startos/src/bins/registry.rs @@ -14,7 +14,8 @@ use crate::util::logger::EmbassyLogger; async fn inner_main(config: &RegistryConfig) -> Result<(), Error> { let server = async { let ctx = RegistryContext::init(config).await?; - let server = WebServer::registry(ctx.listen, ctx.clone()); + let mut server = WebServer::new(ctx.listen); + server.serve_registry(ctx.clone()); let mut shutdown_recv = ctx.shutdown.subscribe(); diff --git a/core/startos/src/bins/start_init.rs b/core/startos/src/bins/start_init.rs index 8e60884f1..f4aa411b5 100644 --- a/core/startos/src/bins/start_init.rs +++ b/core/startos/src/bins/start_init.rs @@ -1,47 +1,56 @@ -use std::net::{Ipv6Addr, SocketAddr}; -use std::path::Path; use std::sync::Arc; -use std::time::Duration; -use helpers::NonDetachingJoinHandle; use tokio::process::Command; use tracing::instrument; use crate::context::config::ServerConfig; -use crate::context::{DiagnosticContext, InstallContext, SetupContext}; -use crate::disk::fsck::{RepairStrategy, RequiresReboot}; +use crate::context::rpc::InitRpcContextPhases; +use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext}; +use crate::disk::fsck::RepairStrategy; use crate::disk::main::DEFAULT_PASSWORD; use crate::disk::REPAIR_DISK_PATH; -use crate::firmware::update_firmware; -use crate::init::STANDBY_MODE_PATH; +use crate::firmware::{check_for_firmware_update, update_firmware}; +use crate::init::{InitPhases, InitResult, STANDBY_MODE_PATH}; use crate::net::web_server::WebServer; +use crate::prelude::*; +use crate::progress::FullProgressTracker; use crate::shutdown::Shutdown; -use crate::sound::{BEP, CHIME}; use crate::util::Invoke; -use crate::{Error, ErrorKind, ResultExt, PLATFORM}; +use crate::PLATFORM; #[instrument(skip_all)] -async fn setup_or_init(config: &ServerConfig) -> Result, Error> { - let song = NonDetachingJoinHandle::from(tokio::spawn(async { - loop { - BEP.play().await.unwrap(); - BEP.play().await.unwrap(); - tokio::time::sleep(Duration::from_secs(30)).await; - } - })); +async fn setup_or_init( + server: &mut WebServer, + config: &ServerConfig, +) -> Result, Error> { + if let Some(firmware) = check_for_firmware_update() + .await + .map_err(|e| { + tracing::warn!("Error checking for firmware update: {e}"); + tracing::debug!("{e:?}"); + }) + .ok() + .and_then(|a| a) + { + let init_ctx = InitContext::init(config).await?; + let handle = &init_ctx.progress; + let mut update_phase = handle.add_phase("Updating Firmware".into(), Some(10)); + let mut reboot_phase = handle.add_phase("Rebooting".into(), Some(1)); - match update_firmware().await { - Ok(RequiresReboot(true)) => { - return Ok(Some(Shutdown { - export_args: None, - restart: true, - })) - } - Err(e) => { + server.serve_init(init_ctx); + + update_phase.start(); + if let Err(e) = update_firmware(firmware).await { tracing::warn!("Error performing firmware update: {e}"); tracing::debug!("{e:?}"); + } else { + update_phase.complete(); + reboot_phase.start(); + return Ok(Err(Shutdown { + export_args: None, + restart: true, + })); } - _ => (), } Command::new("ln") @@ -84,14 +93,7 @@ async fn setup_or_init(config: &ServerConfig) -> Result, Error> let ctx = InstallContext::init().await?; - let server = WebServer::install( - SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), 80), - ctx.clone(), - )?; - - drop(song); - tokio::time::sleep(Duration::from_secs(1)).await; // let the record state that I hate this - CHIME.play().await?; + server.serve_install(ctx.clone()); ctx.shutdown .subscribe() @@ -99,33 +101,23 @@ async fn setup_or_init(config: &ServerConfig) -> Result, Error> .await .expect("context dropped"); - server.shutdown().await; + return Ok(Err(Shutdown { + export_args: None, + restart: true, + })); + } - Command::new("reboot") - .invoke(crate::ErrorKind::Unknown) - .await?; - } else if tokio::fs::metadata("/media/startos/config/disk.guid") + if tokio::fs::metadata("/media/startos/config/disk.guid") .await .is_err() { let ctx = SetupContext::init(config)?; - let server = WebServer::setup( - SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), 80), - ctx.clone(), - )?; - - drop(song); - tokio::time::sleep(Duration::from_secs(1)).await; // let the record state that I hate this - CHIME.play().await?; + server.serve_setup(ctx.clone()); let mut shutdown = ctx.shutdown.subscribe(); shutdown.recv().await.expect("context dropped"); - server.shutdown().await; - - drop(shutdown); - tokio::task::yield_now().await; if let Err(e) = Command::new("killall") .arg("firefox-esr") @@ -135,19 +127,40 @@ async fn setup_or_init(config: &ServerConfig) -> Result, Error> tracing::error!("Failed to kill kiosk: {}", e); tracing::debug!("{:?}", e); } + + Ok(Ok(match ctx.result.get() { + Some(Ok((_, rpc_ctx))) => (rpc_ctx.clone(), ctx.progress.clone()), + Some(Err(e)) => return Err(e.clone_output()), + None => { + return Err(Error::new( + eyre!("Setup mode exited before setup completed"), + ErrorKind::Unknown, + )) + } + })) } else { + let init_ctx = InitContext::init(config).await?; + let handle = init_ctx.progress.clone(); + + let mut disk_phase = handle.add_phase("Opening data drive".into(), Some(10)); + let init_phases = InitPhases::new(&handle); + let rpc_ctx_phases = InitRpcContextPhases::new(&handle); + + server.serve_init(init_ctx); + + disk_phase.start(); let guid_string = tokio::fs::read_to_string("/media/startos/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy .await?; - let guid = guid_string.trim(); + let disk_guid = Arc::new(String::from(guid_string.trim())); let requires_reboot = crate::disk::main::import( - guid, + &**disk_guid, config.datadir(), if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() { RepairStrategy::Aggressive } else { RepairStrategy::Preen }, - if guid.ends_with("_UNENC") { + if disk_guid.ends_with("_UNENC") { None } else { Some(DEFAULT_PASSWORD) @@ -159,40 +172,31 @@ async fn setup_or_init(config: &ServerConfig) -> Result, Error> .await .with_ctx(|_| (crate::ErrorKind::Filesystem, REPAIR_DISK_PATH))?; } - if requires_reboot.0 { - crate::disk::main::export(guid, config.datadir()).await?; - Command::new("reboot") - .invoke(crate::ErrorKind::Unknown) - .await?; - } + disk_phase.complete(); tracing::info!("Loaded Disk"); - crate::init::init(config).await?; - drop(song); - } - Ok(None) -} - -async fn run_script_if_exists>(path: P) { - let script = path.as_ref(); - if script.exists() { - match Command::new("/bin/bash").arg(script).spawn() { - Ok(mut c) => { - if let Err(e) = c.wait().await { - tracing::error!("Error Running {}: {}", script.display(), e); - tracing::debug!("{:?}", e); - } - } - Err(e) => { - tracing::error!("Error Running {}: {}", script.display(), e); - tracing::debug!("{:?}", e); - } + if requires_reboot.0 { + let mut reboot_phase = handle.add_phase("Rebooting".into(), Some(1)); + reboot_phase.start(); + return Ok(Err(Shutdown { + export_args: Some((disk_guid, config.datadir().to_owned())), + restart: true, + })); } + + let InitResult { net_ctrl } = crate::init::init(config, init_phases).await?; + + let rpc_ctx = RpcContext::init(config, disk_guid, Some(net_ctrl), rpc_ctx_phases).await?; + + Ok(Ok((rpc_ctx, handle))) } } #[instrument(skip_all)] -async fn inner_main(config: &ServerConfig) -> Result, Error> { +pub async fn main( + server: &mut WebServer, + config: &ServerConfig, +) -> Result, Error> { if &*PLATFORM == "raspberrypi" && tokio::fs::metadata(STANDBY_MODE_PATH).await.is_ok() { tokio::fs::remove_file(STANDBY_MODE_PATH).await?; Command::new("sync").invoke(ErrorKind::Filesystem).await?; @@ -200,16 +204,11 @@ async fn inner_main(config: &ServerConfig) -> Result, Error> { futures::future::pending::<()>().await; } - crate::sound::BEP.play().await?; - - run_script_if_exists("/media/startos/config/preinit.sh").await; - - let res = match setup_or_init(config).await { + let res = match setup_or_init(server, config).await { Err(e) => { async move { - tracing::error!("{}", e.source); - tracing::debug!("{}", e.source); - crate::sound::BEETHOVEN.play().await?; + tracing::error!("{e}"); + tracing::debug!("{e:?}"); let ctx = DiagnosticContext::init( config, @@ -229,44 +228,16 @@ async fn inner_main(config: &ServerConfig) -> Result, Error> { e, )?; - let server = WebServer::diagnostic( - SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), 80), - ctx.clone(), - )?; + server.serve_diagnostic(ctx.clone()); let shutdown = ctx.shutdown.subscribe().recv().await.unwrap(); - server.shutdown().await; - - Ok(shutdown) + Ok(Err(shutdown)) } .await } Ok(s) => Ok(s), }; - run_script_if_exists("/media/startos/config/postinit.sh").await; - res } - -pub fn main(config: &ServerConfig) { - let res = { - let rt = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .expect("failed to initialize runtime"); - rt.block_on(inner_main(config)) - }; - - match res { - Ok(Some(shutdown)) => shutdown.execute(), - Ok(None) => (), - Err(e) => { - eprintln!("{}", e.source); - tracing::debug!("{:?}", e.source); - drop(e.source); - std::process::exit(e.kind as i32) - } - } -} diff --git a/core/startos/src/bins/startd.rs b/core/startos/src/bins/startd.rs index f0bc428be..7576c41e9 100644 --- a/core/startos/src/bins/startd.rs +++ b/core/startos/src/bins/startd.rs @@ -1,6 +1,5 @@ use std::ffi::OsString; use std::net::{Ipv6Addr, SocketAddr}; -use std::path::Path; use std::sync::Arc; use clap::Parser; @@ -10,7 +9,8 @@ use tokio::signal::unix::signal; use tracing::instrument; use crate::context::config::ServerConfig; -use crate::context::{DiagnosticContext, RpcContext}; +use crate::context::rpc::InitRpcContextPhases; +use crate::context::{DiagnosticContext, InitContext, RpcContext}; use crate::net::web_server::WebServer; use crate::shutdown::Shutdown; use crate::system::launch_metrics_task; @@ -18,9 +18,31 @@ use crate::util::logger::EmbassyLogger; use crate::{Error, ErrorKind, ResultExt}; #[instrument(skip_all)] -async fn inner_main(config: &ServerConfig) -> Result, Error> { - let (rpc_ctx, server, shutdown) = async { - let rpc_ctx = RpcContext::init( +async fn inner_main( + server: &mut WebServer, + config: &ServerConfig, +) -> Result, Error> { + let rpc_ctx = if !tokio::fs::metadata("/run/startos/initialized") + .await + .is_ok() + { + let (ctx, handle) = match super::start_init::main(server, &config).await? { + Err(s) => return Ok(Some(s)), + Ok(ctx) => ctx, + }; + tokio::fs::write("/run/startos/initialized", "").await?; + + server.serve_main(ctx.clone()); + handle.complete(); + + ctx + } else { + let init_ctx = InitContext::init(config).await?; + let handle = init_ctx.progress.clone(); + let rpc_ctx_phases = InitRpcContextPhases::new(&handle); + server.serve_init(init_ctx); + + let ctx = RpcContext::init( config, Arc::new( tokio::fs::read_to_string("/media/startos/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy @@ -28,13 +50,19 @@ async fn inner_main(config: &ServerConfig) -> Result, Error> { .trim() .to_owned(), ), + None, + rpc_ctx_phases, ) .await?; + + server.serve_main(ctx.clone()); + handle.complete(); + + ctx + }; + + let (rpc_ctx, shutdown) = async { crate::hostname::sync_hostname(&rpc_ctx.account.read().await.hostname).await?; - let server = WebServer::main( - SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), 80), - rpc_ctx.clone(), - )?; let mut shutdown_recv = rpc_ctx.shutdown.subscribe(); @@ -74,8 +102,6 @@ async fn inner_main(config: &ServerConfig) -> Result, Error> { .await }); - crate::sound::CHIME.play().await?; - metrics_task .map_err(|e| { Error::new( @@ -93,10 +119,9 @@ async fn inner_main(config: &ServerConfig) -> Result, Error> { sig_handler.abort(); - Ok::<_, Error>((rpc_ctx, server, shutdown)) + Ok::<_, Error>((rpc_ctx, shutdown)) } .await?; - server.shutdown().await; rpc_ctx.shutdown().await?; tracing::info!("RPC Context is dropped"); @@ -109,24 +134,22 @@ pub fn main(args: impl IntoIterator) { let config = ServerConfig::parse_from(args).load().unwrap(); - if !Path::new("/run/embassy/initialized").exists() { - super::start_init::main(&config); - std::fs::write("/run/embassy/initialized", "").unwrap(); - } - let res = { let rt = tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .expect("failed to initialize runtime"); rt.block_on(async { - match inner_main(&config).await { - Ok(a) => Ok(a), + let mut server = WebServer::new(SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), 80)); + match inner_main(&mut server, &config).await { + Ok(a) => { + server.shutdown().await; + Ok(a) + } Err(e) => { async { - tracing::error!("{}", e.source); - tracing::debug!("{:?}", e.source); - crate::sound::BEETHOVEN.play().await?; + tracing::error!("{e}"); + tracing::debug!("{e:?}"); let ctx = DiagnosticContext::init( &config, if tokio::fs::metadata("/media/startos/config/disk.guid") @@ -145,10 +168,7 @@ pub fn main(args: impl IntoIterator) { e, )?; - let server = WebServer::diagnostic( - SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), 80), - ctx.clone(), - )?; + server.serve_diagnostic(ctx.clone()); let mut shutdown = ctx.shutdown.subscribe(); @@ -157,7 +177,7 @@ pub fn main(args: impl IntoIterator) { server.shutdown().await; - Ok::<_, Error>(shutdown) + Ok::<_, Error>(Some(shutdown)) } .await } diff --git a/core/startos/src/context/cli.rs b/core/startos/src/context/cli.rs index 014457b67..22084bbe1 100644 --- a/core/startos/src/context/cli.rs +++ b/core/startos/src/context/cli.rs @@ -18,7 +18,7 @@ use tracing::instrument; use super::setup::CURRENT_SECRET; use crate::context::config::{local_config_path, ClientConfig}; -use crate::context::{DiagnosticContext, InstallContext, RpcContext, SetupContext}; +use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext}; use crate::middleware::auth::LOCAL_AUTH_COOKIE_PATH; use crate::prelude::*; use crate::rpc_continuations::Guid; @@ -271,6 +271,11 @@ impl CallRemote for CliContext { call_remote_http(&self.client, self.rpc_url.clone(), method, params).await } } +impl CallRemote for CliContext { + async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { + call_remote_http(&self.client, self.rpc_url.clone(), method, params).await + } +} impl CallRemote for CliContext { async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { call_remote_http(&self.client, self.rpc_url.clone(), method, params).await diff --git a/core/startos/src/context/diagnostic.rs b/core/startos/src/context/diagnostic.rs index 10379dcf3..0bf67e172 100644 --- a/core/startos/src/context/diagnostic.rs +++ b/core/startos/src/context/diagnostic.rs @@ -14,7 +14,7 @@ use crate::Error; pub struct DiagnosticContextSeed { pub datadir: PathBuf, - pub shutdown: Sender>, + pub shutdown: Sender, pub error: Arc, pub disk_guid: Option>, pub rpc_continuations: RpcContinuations, diff --git a/core/startos/src/context/init.rs b/core/startos/src/context/init.rs new file mode 100644 index 000000000..f5f4a5430 --- /dev/null +++ b/core/startos/src/context/init.rs @@ -0,0 +1,47 @@ +use std::ops::Deref; +use std::sync::Arc; + +use rpc_toolkit::Context; +use tokio::sync::broadcast::Sender; +use tracing::instrument; + +use crate::context::config::ServerConfig; +use crate::progress::FullProgressTracker; +use crate::rpc_continuations::RpcContinuations; +use crate::Error; + +pub struct InitContextSeed { + pub config: ServerConfig, + pub progress: FullProgressTracker, + pub shutdown: Sender<()>, + pub rpc_continuations: RpcContinuations, +} + +#[derive(Clone)] +pub struct InitContext(Arc); +impl InitContext { + #[instrument(skip_all)] + pub async fn init(cfg: &ServerConfig) -> Result { + let (shutdown, _) = tokio::sync::broadcast::channel(1); + Ok(Self(Arc::new(InitContextSeed { + config: cfg.clone(), + progress: FullProgressTracker::new(), + shutdown, + rpc_continuations: RpcContinuations::new(), + }))) + } +} + +impl AsRef for InitContext { + fn as_ref(&self) -> &RpcContinuations { + &self.rpc_continuations + } +} + +impl Context for InitContext {} +impl Deref for InitContext { + type Target = InitContextSeed; + fn deref(&self) -> &Self::Target { + &*self.0 + } +} diff --git a/core/startos/src/context/install.rs b/core/startos/src/context/install.rs index d4717d2b0..c0c564b34 100644 --- a/core/startos/src/context/install.rs +++ b/core/startos/src/context/install.rs @@ -6,11 +6,13 @@ use tokio::sync::broadcast::Sender; use tracing::instrument; use crate::net::utils::find_eth_iface; +use crate::rpc_continuations::RpcContinuations; use crate::Error; pub struct InstallContextSeed { pub ethernet_interface: String, pub shutdown: Sender<()>, + pub rpc_continuations: RpcContinuations, } #[derive(Clone)] @@ -22,10 +24,17 @@ impl InstallContext { Ok(Self(Arc::new(InstallContextSeed { ethernet_interface: find_eth_iface().await?, shutdown, + rpc_continuations: RpcContinuations::new(), }))) } } +impl AsRef for InstallContext { + fn as_ref(&self) -> &RpcContinuations { + &self.rpc_continuations + } +} + impl Context for InstallContext {} impl Deref for InstallContext { type Target = InstallContextSeed; diff --git a/core/startos/src/context/mod.rs b/core/startos/src/context/mod.rs index 77f54f26c..efe261b0c 100644 --- a/core/startos/src/context/mod.rs +++ b/core/startos/src/context/mod.rs @@ -1,12 +1,14 @@ pub mod cli; pub mod config; pub mod diagnostic; +pub mod init; pub mod install; pub mod rpc; pub mod setup; pub use cli::CliContext; pub use diagnostic::DiagnosticContext; +pub use init::InitContext; pub use install::InstallContext; pub use rpc::RpcContext; pub use setup::SetupContext; diff --git a/core/startos/src/context/rpc.rs b/core/startos/src/context/rpc.rs index a3e77a62c..3dd3354aa 100644 --- a/core/startos/src/context/rpc.rs +++ b/core/startos/src/context/rpc.rs @@ -6,11 +6,12 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; +use imbl_value::InternedString; use josekit::jwk::Jwk; use reqwest::{Client, Proxy}; use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::{CallRemote, Context, Empty}; -use tokio::sync::{broadcast, oneshot, Mutex, RwLock}; +use tokio::sync::{broadcast, Mutex, RwLock}; use tokio::time::Instant; use tracing::instrument; @@ -22,12 +23,12 @@ use crate::dependencies::compute_dependency_config_errs; use crate::disk::OsPartitionInfo; use crate::init::check_time_is_synchronized; use crate::lxc::{ContainerId, LxcContainer, LxcManager}; -use crate::middleware::auth::HashSessionToken; -use crate::net::net_controller::NetController; +use crate::net::net_controller::{NetController, PreInitNetController}; use crate::net::utils::{find_eth_iface, find_wifi_iface}; use crate::net::wifi::WpaCli; use crate::prelude::*; -use crate::rpc_continuations::RpcContinuations; +use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle}; +use crate::rpc_continuations::{OpenAuthedContinuations, RpcContinuations}; use crate::service::ServiceMap; use crate::shutdown::Shutdown; use crate::system::get_mem_info; @@ -49,7 +50,7 @@ pub struct RpcContextSeed { pub shutdown: broadcast::Sender>, pub tor_socks: SocketAddr, pub lxc_manager: Arc, - pub open_authed_websockets: Mutex>>>, + pub open_authed_continuations: OpenAuthedContinuations, pub rpc_continuations: RpcContinuations, pub wifi_manager: Option>>, pub current_secret: Arc, @@ -68,45 +69,103 @@ pub struct Hardware { pub ram: u64, } +pub struct InitRpcContextPhases { + load_db: PhaseProgressTrackerHandle, + init_net_ctrl: PhaseProgressTrackerHandle, + read_device_info: PhaseProgressTrackerHandle, + cleanup_init: CleanupInitPhases, +} +impl InitRpcContextPhases { + pub fn new(handle: &FullProgressTracker) -> Self { + Self { + load_db: handle.add_phase("Loading database".into(), Some(5)), + init_net_ctrl: handle.add_phase("Initializing network".into(), Some(1)), + read_device_info: handle.add_phase("Reading device information".into(), Some(1)), + cleanup_init: CleanupInitPhases::new(handle), + } + } +} + +pub struct CleanupInitPhases { + init_services: PhaseProgressTrackerHandle, + check_dependencies: PhaseProgressTrackerHandle, +} +impl CleanupInitPhases { + pub fn new(handle: &FullProgressTracker) -> Self { + Self { + init_services: handle.add_phase("Initializing services".into(), Some(10)), + check_dependencies: handle.add_phase("Checking dependencies".into(), Some(1)), + } + } +} + #[derive(Clone)] pub struct RpcContext(Arc); impl RpcContext { #[instrument(skip_all)] - pub async fn init(config: &ServerConfig, disk_guid: Arc) -> Result { - tracing::info!("Loaded Config"); + pub async fn init( + config: &ServerConfig, + disk_guid: Arc, + net_ctrl: Option, + InitRpcContextPhases { + mut load_db, + mut init_net_ctrl, + mut read_device_info, + cleanup_init, + }: InitRpcContextPhases, + ) -> Result { let tor_proxy = config.tor_socks.unwrap_or(SocketAddr::V4(SocketAddrV4::new( Ipv4Addr::new(127, 0, 0, 1), 9050, ))); let (shutdown, _) = tokio::sync::broadcast::channel(1); - let db = TypedPatchDb::::load(config.db().await?).await?; + load_db.start(); + let db = if let Some(net_ctrl) = &net_ctrl { + net_ctrl.db.clone() + } else { + TypedPatchDb::::load(config.db().await?).await? + }; let peek = db.peek().await; let account = AccountInfo::load(&peek)?; + load_db.complete(); tracing::info!("Opened PatchDB"); + + init_net_ctrl.start(); let net_controller = Arc::new( NetController::init( - db.clone(), - config - .tor_control - .unwrap_or(SocketAddr::from(([127, 0, 0, 1], 9051))), - tor_proxy, + if let Some(net_ctrl) = net_ctrl { + net_ctrl + } else { + PreInitNetController::init( + db.clone(), + config + .tor_control + .unwrap_or(SocketAddr::from(([127, 0, 0, 1], 9051))), + tor_proxy, + &account.hostname, + account.tor_key.clone(), + ) + .await? + }, config .dns_bind .as_deref() .unwrap_or(&[SocketAddr::from(([127, 0, 0, 1], 53))]), - &account.hostname, - account.tor_key.clone(), ) .await?, ); + init_net_ctrl.complete(); tracing::info!("Initialized Net Controller"); + let services = ServiceMap::default(); let metrics_cache = RwLock::>::new(None); - tracing::info!("Initialized Notification Manager"); let tor_proxy_url = format!("socks5h://{tor_proxy}"); + + read_device_info.start(); let devices = lshw().await?; let ram = get_mem_info().await?.total.0 as u64 * 1024 * 1024; + read_device_info.complete(); if !db .peek() @@ -163,7 +222,7 @@ impl RpcContext { shutdown, tor_socks: tor_proxy, lxc_manager: Arc::new(LxcManager::new()), - open_authed_websockets: Mutex::new(BTreeMap::new()), + open_authed_continuations: OpenAuthedContinuations::new(), rpc_continuations: RpcContinuations::new(), wifi_manager: wifi_interface .clone() @@ -196,7 +255,7 @@ impl RpcContext { }); let res = Self(seed.clone()); - res.cleanup_and_initialize().await?; + res.cleanup_and_initialize(cleanup_init).await?; tracing::info!("Cleaned up transient states"); Ok(res) } @@ -210,11 +269,18 @@ impl RpcContext { Ok(()) } - #[instrument(skip(self))] - pub async fn cleanup_and_initialize(&self) -> Result<(), Error> { - self.services.init(&self).await?; + #[instrument(skip_all)] + pub async fn cleanup_and_initialize( + &self, + CleanupInitPhases { + init_services, + mut check_dependencies, + }: CleanupInitPhases, + ) -> Result<(), Error> { + self.services.init(&self, init_services).await?; tracing::info!("Initialized Package Managers"); + check_dependencies.start(); let mut updated_current_dependents = BTreeMap::new(); let peek = self.db.peek().await; for (package_id, package) in peek.as_public().as_package_data().as_entries()?.into_iter() { @@ -238,6 +304,7 @@ impl RpcContext { Ok(()) }) .await?; + check_dependencies.complete(); Ok(()) } @@ -274,6 +341,11 @@ impl AsRef for RpcContext { &self.rpc_continuations } } +impl AsRef> for RpcContext { + fn as_ref(&self) -> &OpenAuthedContinuations { + &self.open_authed_continuations + } +} impl Context for RpcContext {} impl Deref for RpcContext { type Target = RpcContextSeed; diff --git a/core/startos/src/context/setup.rs b/core/startos/src/context/setup.rs index 013dc060b..6041f49b9 100644 --- a/core/startos/src/context/setup.rs +++ b/core/startos/src/context/setup.rs @@ -1,23 +1,31 @@ use std::ops::Deref; use std::path::PathBuf; use std::sync::Arc; +use std::time::Duration; +use futures::{Future, StreamExt}; +use helpers::NonDetachingJoinHandle; use josekit::jwk::Jwk; use patch_db::PatchDb; -use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::Context; use serde::{Deserialize, Serialize}; use sqlx::postgres::PgConnectOptions; use sqlx::PgPool; use tokio::sync::broadcast::Sender; -use tokio::sync::RwLock; +use tokio::sync::OnceCell; use tracing::instrument; +use ts_rs::TS; +use crate::account::AccountInfo; use crate::context::config::ServerConfig; +use crate::context::RpcContext; use crate::disk::OsPartitionInfo; use crate::init::init_postgres; use crate::prelude::*; -use crate::setup::SetupStatus; +use crate::progress::FullProgressTracker; +use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations}; +use crate::setup::SetupProgress; +use crate::util::net::WebSocketExt; lazy_static::lazy_static! { pub static ref CURRENT_SECRET: Jwk = Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::P256).unwrap_or_else(|e| { @@ -27,30 +35,35 @@ lazy_static::lazy_static! { }); } -#[derive(Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] +#[ts(export)] pub struct SetupResult { pub tor_address: String, pub lan_address: String, pub root_ca: String, } +impl TryFrom<&AccountInfo> for SetupResult { + type Error = Error; + fn try_from(value: &AccountInfo) -> Result { + Ok(Self { + tor_address: format!("https://{}", value.tor_key.public().get_onion_address()), + lan_address: value.hostname.lan_address(), + root_ca: String::from_utf8(value.root_ca_cert.to_pem()?)?, + }) + } +} pub struct SetupContextSeed { pub config: ServerConfig, pub os_partitions: OsPartitionInfo, pub disable_encryption: bool, + pub progress: FullProgressTracker, + pub task: OnceCell>, + pub result: OnceCell>, pub shutdown: Sender<()>, pub datadir: PathBuf, - pub selected_v2_drive: RwLock>, - pub cached_product_key: RwLock>>, - pub setup_status: RwLock>>, - pub setup_result: RwLock, SetupResult)>>, -} - -impl AsRef for SetupContextSeed { - fn as_ref(&self) -> &Jwk { - &*CURRENT_SECRET - } + pub rpc_continuations: RpcContinuations, } #[derive(Clone)] @@ -69,12 +82,12 @@ impl SetupContext { ) })?, disable_encryption: config.disable_encryption.unwrap_or(false), + progress: FullProgressTracker::new(), + task: OnceCell::new(), + result: OnceCell::new(), shutdown, datadir, - selected_v2_drive: RwLock::new(None), - cached_product_key: RwLock::new(None), - setup_status: RwLock::new(None), - setup_result: RwLock::new(None), + rpc_continuations: RpcContinuations::new(), }))) } #[instrument(skip_all)] @@ -97,6 +110,104 @@ impl SetupContext { .with_kind(crate::ErrorKind::Database)?; Ok(secret_store) } + + pub fn run_setup(&self, f: F) -> Result<(), Error> + where + F: FnOnce() -> Fut + Send + 'static, + Fut: Future> + Send, + { + let local_ctx = self.clone(); + self.task + .set( + tokio::spawn(async move { + local_ctx + .result + .get_or_init(|| async { + match f().await { + Ok(res) => { + tracing::info!("Setup complete!"); + Ok(res) + } + Err(e) => { + tracing::error!("Setup failed: {e}"); + tracing::debug!("{e:?}"); + Err(e) + } + } + }) + .await; + local_ctx.progress.complete(); + }) + .into(), + ) + .map_err(|_| { + if self.result.initialized() { + Error::new(eyre!("Setup already complete"), ErrorKind::InvalidRequest) + } else { + Error::new( + eyre!("Setup already in progress"), + ErrorKind::InvalidRequest, + ) + } + })?; + Ok(()) + } + + pub async fn progress(&self) -> SetupProgress { + use axum::extract::ws; + + let guid = Guid::new(); + let progress_tracker = self.progress.clone(); + let progress = progress_tracker.snapshot(); + self.rpc_continuations + .add( + guid.clone(), + RpcContinuation::ws( + |mut ws| async move { + if let Err(e) = async { + let mut stream = + progress_tracker.stream(Some(Duration::from_millis(100))); + while let Some(progress) = stream.next().await { + ws.send(ws::Message::Text( + serde_json::to_string(&progress) + .with_kind(ErrorKind::Serialization)?, + )) + .await + .with_kind(ErrorKind::Network)?; + if progress.overall.is_complete() { + break; + } + } + + ws.normal_close("complete").await?; + + Ok::<_, Error>(()) + } + .await + { + tracing::error!("Error in setup progress websocket: {e}"); + tracing::debug!("{e:?}"); + } + }, + Duration::from_secs(30), + ), + ) + .await; + + SetupProgress { progress, guid } + } +} + +impl AsRef for SetupContext { + fn as_ref(&self) -> &Jwk { + &*CURRENT_SECRET + } +} + +impl AsRef for SetupContext { + fn as_ref(&self) -> &RpcContinuations { + &self.rpc_continuations + } } impl Context for SetupContext {} diff --git a/core/startos/src/db/mod.rs b/core/startos/src/db/mod.rs index 0bb8a23db..e59161e9b 100644 --- a/core/startos/src/db/mod.rs +++ b/core/startos/src/db/mod.rs @@ -3,175 +3,40 @@ pub mod prelude; use std::path::PathBuf; use std::sync::Arc; +use std::time::Duration; -use axum::extract::ws::{self, WebSocket}; -use axum::extract::WebSocketUpgrade; -use axum::response::Response; +use axum::extract::ws; use clap::Parser; -use futures::{FutureExt, StreamExt}; -use http::header::COOKIE; -use http::HeaderMap; +use imbl_value::InternedString; use itertools::Itertools; use patch_db::json_ptr::{JsonPointer, ROOT}; use patch_db::{Dump, Revision}; use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; -use serde_json::Value; -use tokio::sync::oneshot; use tracing::instrument; use ts_rs::TS; use crate::context::{CliContext, RpcContext}; -use crate::middleware::auth::{HasValidSession, HashSessionToken}; use crate::prelude::*; +use crate::rpc_continuations::{Guid, RpcContinuation}; +use crate::util::net::WebSocketExt; use crate::util::serde::{apply_expr, HandlerExtSerde}; lazy_static::lazy_static! { static ref PUBLIC: JsonPointer = "/public".parse().unwrap(); } -#[instrument(skip_all)] -async fn ws_handler( - ctx: RpcContext, - session: Option<(HasValidSession, HashSessionToken)>, - mut stream: WebSocket, -) -> Result<(), Error> { - let (dump, sub) = ctx.db.dump_and_sub(PUBLIC.clone()).await; - - if let Some((session, token)) = session { - let kill = subscribe_to_session_kill(&ctx, token).await; - send_dump(session.clone(), &mut stream, dump).await?; - - deal_with_messages(session, kill, sub, stream).await?; - } else { - stream - .send(ws::Message::Close(Some(ws::CloseFrame { - code: ws::close_code::ERROR, - reason: "UNAUTHORIZED".into(), - }))) - .await - .with_kind(ErrorKind::Network)?; - drop(stream); - } - - Ok(()) -} - -async fn subscribe_to_session_kill( - ctx: &RpcContext, - token: HashSessionToken, -) -> oneshot::Receiver<()> { - let (send, recv) = oneshot::channel(); - let mut guard = ctx.open_authed_websockets.lock().await; - if !guard.contains_key(&token) { - guard.insert(token, vec![send]); - } else { - guard.get_mut(&token).unwrap().push(send); - } - recv -} - -#[instrument(skip_all)] -async fn deal_with_messages( - _has_valid_authentication: HasValidSession, - mut kill: oneshot::Receiver<()>, - mut sub: patch_db::Subscriber, - mut stream: WebSocket, -) -> Result<(), Error> { - let mut timer = tokio::time::interval(tokio::time::Duration::from_secs(5)); - - loop { - futures::select! { - _ = (&mut kill).fuse() => { - tracing::info!("Closing WebSocket: Reason: Session Terminated"); - stream - .send(ws::Message::Close(Some(ws::CloseFrame { - code: ws::close_code::ERROR, - reason: "UNAUTHORIZED".into(), - }))).await - .with_kind(ErrorKind::Network)?; - drop(stream); - return Ok(()) - } - new_rev = sub.recv().fuse() => { - let rev = new_rev.expect("UNREACHABLE: patch-db is dropped"); - stream - .send(ws::Message::Text(serde_json::to_string(&rev).with_kind(ErrorKind::Serialization)?)) - .await - .with_kind(ErrorKind::Network)?; - } - message = stream.next().fuse() => { - let message = message.transpose().with_kind(ErrorKind::Network)?; - match message { - None => { - tracing::info!("Closing WebSocket: Stream Finished"); - return Ok(()) - } - _ => (), - } - } - // This is trying to give a health checks to the home to keep the ui alive. - _ = timer.tick().fuse() => { - stream - .send(ws::Message::Ping(vec![])) - .await - .with_kind(crate::ErrorKind::Network)?; - } - } - } -} - -async fn send_dump( - _has_valid_authentication: HasValidSession, - stream: &mut WebSocket, - dump: Dump, -) -> Result<(), Error> { - stream - .send(ws::Message::Text( - serde_json::to_string(&dump).with_kind(ErrorKind::Serialization)?, - )) - .await - .with_kind(ErrorKind::Network)?; - Ok(()) -} - -pub async fn subscribe( - ctx: RpcContext, - headers: HeaderMap, - ws: WebSocketUpgrade, -) -> Result { - let session = match async { - let token = HashSessionToken::from_header(headers.get(COOKIE))?; - let session = HasValidSession::from_header(headers.get(COOKIE), &ctx).await?; - Ok::<_, Error>((session, token)) - } - .await - { - Ok(a) => Some(a), - Err(e) => { - if e.kind != ErrorKind::Authorization { - tracing::error!("Error Authenticating Websocket: {}", e); - tracing::debug!("{:?}", e); - } - None - } - }; - Ok(ws.on_upgrade(|ws| async move { - match ws_handler(ctx, session, ws).await { - Ok(()) => (), - Err(e) => { - tracing::error!("WebSocket Closed: {}", e); - tracing::debug!("{:?}", e); - } - } - })) -} - pub fn db() -> ParentHandler { ParentHandler::new() .subcommand("dump", from_fn_async(cli_dump).with_display_serializable()) .subcommand("dump", from_fn_async(dump).no_cli()) + .subcommand( + "subscribe", + from_fn_async(subscribe) + .with_metadata("get_session", Value::Bool(true)) + .no_cli(), + ) .subcommand("put", put::()) .subcommand("apply", from_fn_async(cli_apply).no_display()) .subcommand("apply", from_fn_async(apply).no_cli()) @@ -215,7 +80,13 @@ async fn cli_dump( context .call_remote::( &method, - imbl_value::json!({ "includePrivate":include_private }), + imbl_value::json!({ + "pointer": if include_private { + AsRef::::as_ref(&ROOT) + } else { + AsRef::::as_ref(&*PUBLIC) + } + }), ) .await?, )? @@ -224,25 +95,76 @@ async fn cli_dump( Ok(dump) } -#[derive(Deserialize, Serialize, Parser, TS)] +#[derive(Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] -#[command(rename_all = "kebab-case")] pub struct DumpParams { - #[arg(long = "include-private", short = 'p')] - #[serde(default)] - #[ts(skip)] - include_private: bool, + #[ts(type = "string | null")] + pointer: Option, } -pub async fn dump( +pub async fn dump(ctx: RpcContext, DumpParams { pointer }: DumpParams) -> Result { + Ok(ctx.db.dump(pointer.as_ref().unwrap_or(&*PUBLIC)).await) +} + +#[derive(Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct SubscribeParams { + #[ts(type = "string | null")] + pointer: Option, + #[ts(skip)] + #[serde(rename = "__auth_session")] + session: InternedString, +} + +#[derive(Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct SubscribeRes { + #[ts(type = "{ id: number; value: unknown }")] + pub dump: Dump, + pub guid: Guid, +} + +pub async fn subscribe( ctx: RpcContext, - DumpParams { include_private }: DumpParams, -) -> Result { - Ok(if include_private { - ctx.db.dump(&ROOT).await - } else { - ctx.db.dump(&PUBLIC).await - }) + SubscribeParams { pointer, session }: SubscribeParams, +) -> Result { + let (dump, mut sub) = ctx + .db + .dump_and_sub(pointer.unwrap_or_else(|| PUBLIC.clone())) + .await; + let guid = Guid::new(); + ctx.rpc_continuations + .add( + guid.clone(), + RpcContinuation::ws_authed( + &ctx, + session, + |mut ws| async move { + if let Err(e) = async { + while let Some(rev) = sub.recv().await { + ws.send(ws::Message::Text( + serde_json::to_string(&rev).with_kind(ErrorKind::Serialization)?, + )) + .await + .with_kind(ErrorKind::Network)?; + } + + ws.normal_close("complete").await?; + + Ok::<_, Error>(()) + } + .await + { + tracing::error!("Error in db websocket: {e}"); + tracing::debug!("{e:?}"); + } + }, + Duration::from_secs(30), + ), + ) + .await; + + Ok(SubscribeRes { dump, guid }) } #[derive(Deserialize, Serialize, Parser)] diff --git a/core/startos/src/diagnostic.rs b/core/startos/src/diagnostic.rs index 485f2359f..5e99580e9 100644 --- a/core/startos/src/diagnostic.rs +++ b/core/startos/src/diagnostic.rs @@ -27,10 +27,6 @@ pub fn diagnostic() -> ParentHandler { "kernel-logs", from_fn_async(crate::logs::cli_logs::).no_display(), ) - .subcommand( - "exit", - from_fn(exit).no_display().with_call_remote::(), - ) .subcommand( "restart", from_fn(restart) @@ -51,20 +47,15 @@ pub fn error(ctx: DiagnosticContext) -> Result, Error> { Ok(ctx.error.clone()) } -pub fn exit(ctx: DiagnosticContext) -> Result<(), Error> { - ctx.shutdown.send(None).expect("receiver dropped"); - Ok(()) -} - pub fn restart(ctx: DiagnosticContext) -> Result<(), Error> { ctx.shutdown - .send(Some(Shutdown { + .send(Shutdown { export_args: ctx .disk_guid .clone() .map(|guid| (guid, ctx.datadir.clone())), restart: true, - })) + }) .expect("receiver dropped"); Ok(()) } diff --git a/core/startos/src/disk/main.rs b/core/startos/src/disk/main.rs index d414c247e..ee807a938 100644 --- a/core/startos/src/disk/main.rs +++ b/core/startos/src/disk/main.rs @@ -13,7 +13,7 @@ use crate::disk::mount::util::unmount; use crate::util::Invoke; use crate::{Error, ErrorKind, ResultExt}; -pub const PASSWORD_PATH: &'static str = "/run/embassy/password"; +pub const PASSWORD_PATH: &'static str = "/run/startos/password"; pub const DEFAULT_PASSWORD: &'static str = "password"; pub const MAIN_FS_SIZE: FsSize = FsSize::Gigabytes(8); diff --git a/core/startos/src/firmware.rs b/core/startos/src/firmware.rs index 20347bcff..a9d5ced79 100644 --- a/core/startos/src/firmware.rs +++ b/core/startos/src/firmware.rs @@ -9,6 +9,7 @@ use tokio::process::Command; use crate::disk::fsck::RequiresReboot; use crate::prelude::*; +use crate::progress::PhaseProgressTrackerHandle; use crate::util::Invoke; use crate::PLATFORM; @@ -49,12 +50,7 @@ pub fn display_firmware_update_result(result: RequiresReboot) { } } -/// We wanted to make sure during every init -/// that the firmware was the correct and updated for -/// systems like the Pure System that a new firmware -/// was released and the updates where pushed through the pure os. -// #[command(rename = "update-firmware", display(display_firmware_update_result))] -pub async fn update_firmware() -> Result { +pub async fn check_for_firmware_update() -> Result, Error> { let system_product_name = String::from_utf8( Command::new("dmidecode") .arg("-s") @@ -74,22 +70,21 @@ pub async fn update_firmware() -> Result { .trim() .to_owned(); if system_product_name.is_empty() || bios_version.is_empty() { - return Ok(RequiresReboot(false)); + return Ok(None); } - let firmware_dir = Path::new("/usr/lib/startos/firmware"); - for firmware in serde_json::from_str::>( &tokio::fs::read_to_string("/usr/lib/startos/firmware.json").await?, ) .with_kind(ErrorKind::Deserialization)? { - let id = firmware.id; let matches_product_name = firmware .system_product_name - .map_or(true, |spn| spn == system_product_name); + .as_ref() + .map_or(true, |spn| spn == &system_product_name); let matches_bios_version = firmware .bios_version + .as_ref() .map_or(Some(true), |bv| { let mut semver_str = bios_version.as_str(); if let Some(prefix) = &bv.semver_prefix { @@ -113,35 +108,45 @@ pub async fn update_firmware() -> Result { }) .unwrap_or(false); if firmware.platform.contains(&*PLATFORM) && matches_product_name && matches_bios_version { - let filename = format!("{id}.rom.gz"); - let firmware_path = firmware_dir.join(&filename); - Command::new("sha256sum") - .arg("-c") - .input(Some(&mut std::io::Cursor::new(format!( - "{} {}", - firmware.shasum, - firmware_path.display() - )))) - .invoke(ErrorKind::Filesystem) - .await?; - let mut rdr = if tokio::fs::metadata(&firmware_path).await.is_ok() { - GzipDecoder::new(BufReader::new(File::open(&firmware_path).await?)) - } else { - return Err(Error::new( - eyre!("Firmware {id}.rom.gz not found in {firmware_dir:?}"), - ErrorKind::NotFound, - )); - }; - Command::new("flashrom") - .arg("-p") - .arg("internal") - .arg("-w-") - .input(Some(&mut rdr)) - .invoke(ErrorKind::Firmware) - .await?; - return Ok(RequiresReboot(true)); + return Ok(Some(firmware)); } } - Ok(RequiresReboot(false)) + Ok(None) +} + +/// We wanted to make sure during every init +/// that the firmware was the correct and updated for +/// systems like the Pure System that a new firmware +/// was released and the updates where pushed through the pure os. +pub async fn update_firmware(firmware: Firmware) -> Result<(), Error> { + let id = &firmware.id; + let firmware_dir = Path::new("/usr/lib/startos/firmware"); + let filename = format!("{id}.rom.gz"); + let firmware_path = firmware_dir.join(&filename); + Command::new("sha256sum") + .arg("-c") + .input(Some(&mut std::io::Cursor::new(format!( + "{} {}", + firmware.shasum, + firmware_path.display() + )))) + .invoke(ErrorKind::Filesystem) + .await?; + let mut rdr = if tokio::fs::metadata(&firmware_path).await.is_ok() { + GzipDecoder::new(BufReader::new(File::open(&firmware_path).await?)) + } else { + return Err(Error::new( + eyre!("Firmware {id}.rom.gz not found in {firmware_dir:?}"), + ErrorKind::NotFound, + )); + }; + Command::new("flashrom") + .arg("-p") + .arg("internal") + .arg("-w-") + .input(Some(&mut rdr)) + .invoke(ErrorKind::Firmware) + .await?; + Ok(()) } diff --git a/core/startos/src/init.rs b/core/startos/src/init.rs index 97c674ac5..cdc444c32 100644 --- a/core/startos/src/init.rs +++ b/core/startos/src/init.rs @@ -1,25 +1,40 @@ use std::fs::Permissions; +use std::io::Cursor; +use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; use std::os::unix::fs::PermissionsExt; use std::path::Path; use std::time::{Duration, SystemTime}; +use axum::extract::ws::{self, CloseFrame}; use color_eyre::eyre::eyre; +use futures::{StreamExt, TryStreamExt}; +use itertools::Itertools; use models::ResultExt; use rand::random; +use rpc_toolkit::{from_fn_async, Context, Empty, HandlerArgs, HandlerExt, ParentHandler}; +use serde::{Deserialize, Serialize}; use tokio::process::Command; use tracing::instrument; +use ts_rs::TS; use crate::account::AccountInfo; use crate::context::config::ServerConfig; +use crate::context::{CliContext, InitContext}; use crate::db::model::public::ServerStatus; use crate::db::model::Database; use crate::disk::mount::util::unmount; use crate::middleware::auth::LOCAL_AUTH_COOKIE_PATH; +use crate::net::net_controller::PreInitNetController; use crate::prelude::*; +use crate::progress::{ + FullProgress, FullProgressTracker, PhaseProgressTrackerHandle, PhasedProgressBar, +}; +use crate::rpc_continuations::{Guid, RpcContinuation}; use crate::ssh::SSH_AUTHORIZED_KEYS_FILE; -use crate::util::cpupower::{get_available_governors, get_preferred_governor, set_governor}; -use crate::util::Invoke; -use crate::{Error, ARCH}; +use crate::util::io::IOHook; +use crate::util::net::WebSocketExt; +use crate::util::{cpupower, Invoke}; +use crate::Error; pub const SYSTEM_REBUILD_PATH: &str = "/media/startos/config/system-rebuild"; pub const STANDBY_MODE_PATH: &str = "/media/startos/config/standby"; @@ -180,14 +195,114 @@ pub async fn init_postgres(datadir: impl AsRef) -> Result<(), Error> { } pub struct InitResult { - pub db: TypedPatchDb, + pub net_ctrl: PreInitNetController, +} + +pub struct InitPhases { + preinit: Option, + local_auth: PhaseProgressTrackerHandle, + load_database: PhaseProgressTrackerHandle, + load_ssh_keys: PhaseProgressTrackerHandle, + start_net: PhaseProgressTrackerHandle, + mount_logs: PhaseProgressTrackerHandle, + load_ca_cert: PhaseProgressTrackerHandle, + load_wifi: PhaseProgressTrackerHandle, + init_tmp: PhaseProgressTrackerHandle, + set_governor: PhaseProgressTrackerHandle, + sync_clock: PhaseProgressTrackerHandle, + enable_zram: PhaseProgressTrackerHandle, + update_server_info: PhaseProgressTrackerHandle, + launch_service_network: PhaseProgressTrackerHandle, + run_migrations: PhaseProgressTrackerHandle, + validate_db: PhaseProgressTrackerHandle, + postinit: Option, +} +impl InitPhases { + pub fn new(handle: &FullProgressTracker) -> Self { + Self { + preinit: if Path::new("/media/startos/config/preinit.sh").exists() { + Some(handle.add_phase("Running preinit.sh".into(), Some(5))) + } else { + None + }, + local_auth: handle.add_phase("Enabling local authentication".into(), Some(1)), + load_database: handle.add_phase("Loading database".into(), Some(5)), + load_ssh_keys: handle.add_phase("Loading SSH Keys".into(), Some(1)), + start_net: handle.add_phase("Starting network controller".into(), Some(1)), + mount_logs: handle.add_phase("Switching logs to write to data drive".into(), Some(1)), + load_ca_cert: handle.add_phase("Loading CA certificate".into(), Some(1)), + load_wifi: handle.add_phase("Loading WiFi configuration".into(), Some(1)), + init_tmp: handle.add_phase("Initializing temporary files".into(), Some(1)), + set_governor: handle.add_phase("Setting CPU performance profile".into(), Some(1)), + sync_clock: handle.add_phase("Synchronizing system clock".into(), Some(10)), + enable_zram: handle.add_phase("Enabling ZRAM".into(), Some(1)), + update_server_info: handle.add_phase("Updating server info".into(), Some(1)), + launch_service_network: handle.add_phase("Launching service intranet".into(), Some(10)), + run_migrations: handle.add_phase("Running migrations".into(), Some(10)), + validate_db: handle.add_phase("Validating database".into(), Some(1)), + postinit: if Path::new("/media/startos/config/postinit.sh").exists() { + Some(handle.add_phase("Running postinit.sh".into(), Some(5))) + } else { + None + }, + } + } +} + +pub async fn run_script>(path: P, mut progress: PhaseProgressTrackerHandle) { + let script = path.as_ref(); + progress.start(); + if let Err(e) = async { + let script = tokio::fs::read_to_string(script).await?; + progress.set_total(script.as_bytes().iter().filter(|b| **b == b'\n').count() as u64); + let mut reader = IOHook::new(Cursor::new(script.as_bytes())); + reader.post_read(|buf| progress += buf.iter().filter(|b| **b == b'\n').count() as u64); + Command::new("/bin/bash") + .input(Some(&mut reader)) + .invoke(ErrorKind::Unknown) + .await?; + + Ok::<_, Error>(()) + } + .await + { + tracing::error!("Error Running {}: {}", script.display(), e); + tracing::debug!("{:?}", e); + } + progress.complete(); } #[instrument(skip_all)] -pub async fn init(cfg: &ServerConfig) -> Result { - tokio::fs::create_dir_all("/run/embassy") +pub async fn init( + cfg: &ServerConfig, + InitPhases { + preinit, + mut local_auth, + mut load_database, + mut load_ssh_keys, + mut start_net, + mut mount_logs, + mut load_ca_cert, + mut load_wifi, + mut init_tmp, + mut set_governor, + mut sync_clock, + mut enable_zram, + mut update_server_info, + mut launch_service_network, + run_migrations, + mut validate_db, + postinit, + }: InitPhases, +) -> Result { + if let Some(progress) = preinit { + run_script("/media/startos/config/preinit.sh", progress).await; + } + + local_auth.start(); + tokio::fs::create_dir_all("/run/startos") .await - .with_ctx(|_| (crate::ErrorKind::Filesystem, "mkdir -p /run/embassy"))?; + .with_ctx(|_| (crate::ErrorKind::Filesystem, "mkdir -p /run/startos"))?; if tokio::fs::metadata(LOCAL_AUTH_COOKIE_PATH).await.is_err() { tokio::fs::write( LOCAL_AUTH_COOKIE_PATH, @@ -207,43 +322,41 @@ pub async fn init(cfg: &ServerConfig) -> Result { .invoke(crate::ErrorKind::Filesystem) .await?; } + local_auth.complete(); + load_database.start(); let db = TypedPatchDb::::load_unchecked(cfg.db().await?); let peek = db.peek().await; + load_database.complete(); tracing::info!("Opened PatchDB"); + load_ssh_keys.start(); crate::ssh::sync_keys( &peek.as_private().as_ssh_pubkeys().de()?, SSH_AUTHORIZED_KEYS_FILE, ) .await?; + load_ssh_keys.complete(); tracing::info!("Synced SSH Keys"); let account = AccountInfo::load(&peek)?; - let mut server_info = peek.as_public().as_server_info().de()?; - - // write to ca cert store - tokio::fs::write( - "/usr/local/share/ca-certificates/startos-root-ca.crt", - account.root_ca_cert.to_pem()?, + start_net.start(); + let net_ctrl = PreInitNetController::init( + db.clone(), + cfg.tor_control + .unwrap_or(SocketAddr::from(([127, 0, 0, 1], 9051))), + cfg.tor_socks.unwrap_or(SocketAddr::V4(SocketAddrV4::new( + Ipv4Addr::new(127, 0, 0, 1), + 9050, + ))), + &account.hostname, + account.tor_key, ) .await?; - Command::new("update-ca-certificates") - .invoke(crate::ErrorKind::OpenSsl) - .await?; - - crate::net::wifi::synchronize_wpa_supplicant_conf( - &cfg.datadir().join("main"), - &mut server_info.wifi, - ) - .await?; - tracing::info!("Synchronized WiFi"); - - let should_rebuild = tokio::fs::metadata(SYSTEM_REBUILD_PATH).await.is_ok() - || &*server_info.version < &emver::Version::new(0, 3, 2, 0) - || (ARCH == "x86_64" && &*server_info.version < &emver::Version::new(0, 3, 4, 0)); + start_net.complete(); + mount_logs.start(); let log_dir = cfg.datadir().join("main/logs"); if tokio::fs::metadata(&log_dir).await.is_err() { tokio::fs::create_dir_all(&log_dir).await?; @@ -272,10 +385,35 @@ pub async fn init(cfg: &ServerConfig) -> Result { .arg("systemd-journald") .invoke(crate::ErrorKind::Journald) .await?; + mount_logs.complete(); tracing::info!("Mounted Logs"); + let mut server_info = peek.as_public().as_server_info().de()?; + + load_ca_cert.start(); + // write to ca cert store + tokio::fs::write( + "/usr/local/share/ca-certificates/startos-root-ca.crt", + account.root_ca_cert.to_pem()?, + ) + .await?; + Command::new("update-ca-certificates") + .invoke(crate::ErrorKind::OpenSsl) + .await?; + load_ca_cert.complete(); + + load_wifi.start(); + crate::net::wifi::synchronize_wpa_supplicant_conf( + &cfg.datadir().join("main"), + &mut server_info.wifi, + ) + .await?; + load_wifi.complete(); + tracing::info!("Synchronized WiFi"); + + init_tmp.start(); let tmp_dir = cfg.datadir().join("package-data/tmp"); - if should_rebuild && tokio::fs::metadata(&tmp_dir).await.is_ok() { + if tokio::fs::metadata(&tmp_dir).await.is_ok() { tokio::fs::remove_dir_all(&tmp_dir).await?; } if tokio::fs::metadata(&tmp_dir).await.is_err() { @@ -286,23 +424,30 @@ pub async fn init(cfg: &ServerConfig) -> Result { tokio::fs::remove_dir_all(&tmp_var).await?; } crate::disk::mount::util::bind(&tmp_var, "/var/tmp", false).await?; + init_tmp.complete(); + set_governor.start(); let governor = if let Some(governor) = &server_info.governor { - if get_available_governors().await?.contains(governor) { + if cpupower::get_available_governors() + .await? + .contains(governor) + { Some(governor) } else { tracing::warn!("CPU Governor \"{governor}\" Not Available"); None } } else { - get_preferred_governor().await? + cpupower::get_preferred_governor().await? }; if let Some(governor) = governor { tracing::info!("Setting CPU Governor to \"{governor}\""); - set_governor(governor).await?; + cpupower::set_governor(governor).await?; tracing::info!("Set CPU Governor"); } + set_governor.complete(); + sync_clock.start(); server_info.ntp_synced = false; let mut not_made_progress = 0u32; for _ in 0..1800 { @@ -329,10 +474,15 @@ pub async fn init(cfg: &ServerConfig) -> Result { } else { tracing::info!("Syncronized system clock"); } + sync_clock.complete(); + enable_zram.start(); if server_info.zram { crate::system::enable_zram().await? } + enable_zram.complete(); + + update_server_info.start(); server_info.ip_info = crate::net::dhcp::init_ips().await?; server_info.status_info = ServerStatus { updated: false, @@ -341,36 +491,129 @@ pub async fn init(cfg: &ServerConfig) -> Result { shutting_down: false, restarting: false, }; - db.mutate(|v| { v.as_public_mut().as_server_info_mut().ser(&server_info)?; Ok(()) }) .await?; + update_server_info.complete(); + launch_service_network.start(); Command::new("systemctl") .arg("start") .arg("lxc-net.service") .invoke(ErrorKind::Lxc) .await?; + launch_service_network.complete(); - crate::version::init(&db).await?; + crate::version::init(&db, run_migrations).await?; + validate_db.start(); db.mutate(|d| { let model = d.de()?; d.ser(&model) }) .await?; + validate_db.complete(); - if should_rebuild { - match tokio::fs::remove_file(SYSTEM_REBUILD_PATH).await { - Ok(()) => Ok(()), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), - Err(e) => Err(e), - }?; + if let Some(progress) = postinit { + run_script("/media/startos/config/postinit.sh", progress).await; } tracing::info!("System initialized."); - Ok(InitResult { db }) + Ok(InitResult { net_ctrl }) +} + +pub fn init_api() -> ParentHandler { + ParentHandler::new() + .subcommand("logs", crate::system::logs::()) + .subcommand( + "logs", + from_fn_async(crate::logs::cli_logs::).no_display(), + ) + .subcommand("kernel-logs", crate::system::kernel_logs::()) + .subcommand( + "kernel-logs", + from_fn_async(crate::logs::cli_logs::).no_display(), + ) + .subcommand("subscribe", from_fn_async(init_progress).no_cli()) + .subcommand("subscribe", from_fn_async(cli_init_progress).no_display()) +} + +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct InitProgressRes { + pub progress: FullProgress, + pub guid: Guid, +} + +pub async fn init_progress(ctx: InitContext) -> Result { + let progress_tracker = ctx.progress.clone(); + let progress = progress_tracker.snapshot(); + let guid = Guid::new(); + ctx.rpc_continuations + .add( + guid.clone(), + RpcContinuation::ws( + |mut ws| async move { + if let Err(e) = async { + let mut stream = progress_tracker.stream(Some(Duration::from_millis(100))); + while let Some(progress) = stream.next().await { + ws.send(ws::Message::Text( + serde_json::to_string(&progress) + .with_kind(ErrorKind::Serialization)?, + )) + .await + .with_kind(ErrorKind::Network)?; + if progress.overall.is_complete() { + break; + } + } + + ws.normal_close("complete").await?; + + Ok::<_, Error>(()) + } + .await + { + tracing::error!("error in init progress websocket: {e}"); + tracing::debug!("{e:?}"); + } + }, + Duration::from_secs(30), + ), + ) + .await; + Ok(InitProgressRes { progress, guid }) +} + +pub async fn cli_init_progress( + HandlerArgs { + context: ctx, + parent_method, + method, + raw_params, + .. + }: HandlerArgs, +) -> Result<(), Error> { + let res: InitProgressRes = from_value( + ctx.call_remote::( + &parent_method + .into_iter() + .chain(method.into_iter()) + .join("."), + raw_params, + ) + .await?, + )?; + let mut ws = ctx.ws_continuation(res.guid).await?; + let mut bar = PhasedProgressBar::new("Initializing..."); + while let Some(msg) = ws.try_next().await.with_kind(ErrorKind::Network)? { + if let tokio_tungstenite::tungstenite::Message::Text(msg) = msg { + bar.update(&serde_json::from_str(&msg).with_kind(ErrorKind::Deserialization)?); + } + } + Ok(()) } diff --git a/core/startos/src/install/mod.rs b/core/startos/src/install/mod.rs index 707503615..5d50da27d 100644 --- a/core/startos/src/install/mod.rs +++ b/core/startos/src/install/mod.rs @@ -6,7 +6,8 @@ use clap::builder::ValueParserFactory; use clap::{value_parser, CommandFactory, FromArgMatches, Parser}; use color_eyre::eyre::eyre; use emver::VersionRange; -use futures::{FutureExt, StreamExt}; +use futures::StreamExt; +use imbl_value::InternedString; use itertools::Itertools; use patch_db::json_ptr::JsonPointer; use reqwest::header::{HeaderMap, CONTENT_LENGTH}; @@ -29,6 +30,7 @@ use crate::s9pk::merkle_archive::source::http::HttpSource; use crate::s9pk::S9pk; use crate::upload::upload; use crate::util::clap::FromStrParser; +use crate::util::net::WebSocketExt; use crate::util::Never; pub const PKG_ARCHIVE_DIR: &str = "package-data/archive"; @@ -170,7 +172,15 @@ pub async fn install( Ok(()) } -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +pub struct SideloadParams { + #[ts(skip)] + #[serde(rename = "__auth_session")] + session: InternedString, +} + +#[derive(Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] pub struct SideloadResponse { pub upload: Guid, @@ -178,8 +188,11 @@ pub struct SideloadResponse { } #[instrument(skip_all)] -pub async fn sideload(ctx: RpcContext) -> Result { - let (upload, file) = upload(&ctx).await?; +pub async fn sideload( + ctx: RpcContext, + SideloadParams { session }: SideloadParams, +) -> Result { + let (upload, file) = upload(&ctx, session.clone()).await?; let (id_send, id_recv) = oneshot::channel(); let (err_send, err_recv) = oneshot::channel(); let progress = Guid::new(); @@ -193,8 +206,8 @@ pub async fn sideload(ctx: RpcContext) -> Result { .await; ctx.rpc_continuations.add( progress.clone(), - RpcContinuation::ws( - Box::new(|mut ws| { + RpcContinuation::ws_authed(&ctx, session, + |mut ws| { use axum::extract::ws::Message; async move { if let Err(e) = async { @@ -251,7 +264,7 @@ pub async fn sideload(ctx: RpcContext) -> Result { } } - ws.close().await.with_kind(ErrorKind::Network)?; + ws.normal_close("complete").await?; Ok::<_, Error>(()) } @@ -261,8 +274,7 @@ pub async fn sideload(ctx: RpcContext) -> Result { tracing::debug!("{e:?}"); } } - .boxed() - }), + }, Duration::from_secs(600), ), ) diff --git a/core/startos/src/lib.rs b/core/startos/src/lib.rs index 0e125af98..6e7cc8f8b 100644 --- a/core/startos/src/lib.rs +++ b/core/startos/src/lib.rs @@ -1,8 +1,5 @@ pub const DEFAULT_MARKETPLACE: &str = "https://registry.start9.com"; // pub const COMMUNITY_MARKETPLACE: &str = "https://community-registry.start9.com"; -pub const CAP_1_KiB: usize = 1024; -pub const CAP_1_MiB: usize = CAP_1_KiB * CAP_1_KiB; -pub const CAP_10_MiB: usize = 10 * CAP_1_MiB; pub const HOST_IP: [u8; 4] = [172, 18, 0, 1]; pub use std::env::consts::ARCH; lazy_static::lazy_static! { @@ -18,6 +15,15 @@ lazy_static::lazy_static! { }; } +mod cap { + #![allow(non_upper_case_globals)] + + pub const CAP_1_KiB: usize = 1024; + pub const CAP_1_MiB: usize = CAP_1_KiB * CAP_1_KiB; + pub const CAP_10_MiB: usize = 10 * CAP_1_MiB; +} +pub use cap::*; + pub mod account; pub mod action; pub mod auth; @@ -75,13 +81,17 @@ use rpc_toolkit::{ use serde::{Deserialize, Serialize}; use ts_rs::TS; -use crate::context::{CliContext, DiagnosticContext, InstallContext, RpcContext, SetupContext}; +use crate::context::{ + CliContext, DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext, +}; +use crate::disk::fsck::RequiresReboot; use crate::registry::context::{RegistryContext, RegistryUrlParams}; use crate::util::serde::HandlerExtSerde; #[derive(Deserialize, Serialize, Parser, TS)] #[serde(rename_all = "camelCase")] #[command(rename_all = "kebab-case")] +#[ts(export)] pub struct EchoParams { message: String, } @@ -90,6 +100,20 @@ pub fn echo(_: C, EchoParams { message }: EchoParams) -> Result) -> std::fmt::Result { + std::fmt::Debug::fmt(&self, f) + } +} + pub fn main_api() -> ParentHandler { ParentHandler::new() .subcommand::("git-info", from_fn(version::git_info)) @@ -99,6 +123,12 @@ pub fn main_api() -> ParentHandler { .with_metadata("authenticated", Value::Bool(false)) .with_call_remote::(), ) + .subcommand( + "state", + from_fn(|_: RpcContext| Ok::<_, Error>(ApiState::Running)) + .with_metadata("authenticated", Value::Bool(false)) + .with_call_remote::(), + ) .subcommand("server", server::()) .subcommand("package", package::()) .subcommand("net", net::net::()) @@ -179,11 +209,18 @@ pub fn server() -> ParentHandler { ) .subcommand( "update-firmware", - from_fn_async(|_: RpcContext| firmware::update_firmware()) - .with_custom_display_fn(|_handle, result| { - Ok(firmware::display_firmware_update_result(result)) - }) - .with_call_remote::(), + from_fn_async(|_: RpcContext| async { + if let Some(firmware) = firmware::check_for_firmware_update().await? { + firmware::update_firmware(firmware).await?; + Ok::<_, Error>(RequiresReboot(true)) + } else { + Ok(RequiresReboot(false)) + } + }) + .with_custom_display_fn(|_handle, result| { + Ok(firmware::display_firmware_update_result(result)) + }) + .with_call_remote::(), ) } @@ -204,7 +241,12 @@ pub fn package() -> ParentHandler { .with_metadata("sync_db", Value::Bool(true)) .no_cli(), ) - .subcommand("sideload", from_fn_async(install::sideload).no_cli()) + .subcommand( + "sideload", + from_fn_async(install::sideload) + .with_metadata("get_session", Value::Bool(true)) + .no_cli(), + ) .subcommand("install", from_fn_async(install::cli_install).no_display()) .subcommand( "uninstall", @@ -273,9 +315,34 @@ pub fn diagnostic_api() -> ParentHandler { "echo", from_fn(echo::).with_call_remote::(), ) + .subcommand( + "state", + from_fn(|_: DiagnosticContext| Ok::<_, Error>(ApiState::Error)) + .with_metadata("authenticated", Value::Bool(false)) + .with_call_remote::(), + ) .subcommand("diagnostic", diagnostic::diagnostic::()) } +pub fn init_api() -> ParentHandler { + ParentHandler::new() + .subcommand::( + "git-info", + from_fn(version::git_info).with_metadata("authenticated", Value::Bool(false)), + ) + .subcommand( + "echo", + from_fn(echo::).with_call_remote::(), + ) + .subcommand( + "state", + from_fn(|_: InitContext| Ok::<_, Error>(ApiState::Initializing)) + .with_metadata("authenticated", Value::Bool(false)) + .with_call_remote::(), + ) + .subcommand("init", init::init_api::()) +} + pub fn setup_api() -> ParentHandler { ParentHandler::new() .subcommand::( diff --git a/core/startos/src/lxc/mod.rs b/core/startos/src/lxc/mod.rs index 8d37120ba..1f1424338 100644 --- a/core/startos/src/lxc/mod.rs +++ b/core/startos/src/lxc/mod.rs @@ -7,7 +7,7 @@ use std::time::Duration; use clap::builder::ValueParserFactory; use clap::Parser; -use futures::{AsyncWriteExt, FutureExt, StreamExt}; +use futures::{AsyncWriteExt, StreamExt}; use imbl_value::{InOMap, InternedString}; use models::InvalidId; use rpc_toolkit::yajrc::{RpcError, RpcResponse}; @@ -456,51 +456,49 @@ pub async fn connect(ctx: &RpcContext, container: &LxcContainer) -> Result break, - Some(Ok(Message::Text(txt))) => { - let mut id = None; - let result = async { - let req: RpcRequest = serde_json::from_str(&txt) - .map_err(|e| RpcError { - data: Some(serde_json::Value::String( - e.to_string(), - )), - ..rpc_toolkit::yajrc::PARSE_ERROR - })?; - id = req.id; - rpc.request(req.method, req.params).await - } - .await; - ws.send(Message::Text( - serde_json::to_string( - &RpcResponse:: { id, result }, - ) - .with_kind(ErrorKind::Serialization)?, - )) - .await - .with_kind(ErrorKind::Network)?; - } - Some(Ok(_)) => (), - Some(Err(e)) => { - return Err(Error::new(e, ErrorKind::Network)); + |mut ws| async move { + if let Err(e) = async { + loop { + match ws.next().await { + None => break, + Some(Ok(Message::Text(txt))) => { + let mut id = None; + let result = async { + let req: RpcRequest = + serde_json::from_str(&txt).map_err(|e| RpcError { + data: Some(serde_json::Value::String( + e.to_string(), + )), + ..rpc_toolkit::yajrc::PARSE_ERROR + })?; + id = req.id; + rpc.request(req.method, req.params).await } + .await; + ws.send(Message::Text( + serde_json::to_string(&RpcResponse:: { + id, + result, + }) + .with_kind(ErrorKind::Serialization)?, + )) + .await + .with_kind(ErrorKind::Network)?; + } + Some(Ok(_)) => (), + Some(Err(e)) => { + return Err(Error::new(e, ErrorKind::Network)); } } - Ok::<_, Error>(()) - } - .await - { - tracing::error!("{e}"); - tracing::debug!("{e:?}"); } + Ok::<_, Error>(()) } - .boxed() - }), + .await + { + tracing::error!("{e}"); + tracing::debug!("{e:?}"); + } + }, Duration::from_secs(30), ), ) diff --git a/core/startos/src/middleware/auth.rs b/core/startos/src/middleware/auth.rs index 1852c4890..ecf88dba7 100644 --- a/core/startos/src/middleware/auth.rs +++ b/core/startos/src/middleware/auth.rs @@ -23,7 +23,7 @@ use tokio::sync::Mutex; use crate::context::RpcContext; use crate::prelude::*; -pub const LOCAL_AUTH_COOKIE_PATH: &str = "/run/embassy/rpc.authcookie"; +pub const LOCAL_AUTH_COOKIE_PATH: &str = "/run/startos/rpc.authcookie"; #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -48,19 +48,9 @@ impl HasLoggedOutSessions { .into_iter() .map(|s| s.as_logout_session_id()) .collect(); - ctx.open_authed_websockets - .lock() - .await - .retain(|session, sockets| { - if to_log_out.contains(session.hashed()) { - for socket in std::mem::take(sockets) { - let _ = socket.send(()); - } - false - } else { - true - } - }); + for sid in &to_log_out { + ctx.open_authed_continuations.kill(sid) + } ctx.db .mutate(|db| { let sessions = db.as_private_mut().as_sessions_mut(); diff --git a/core/startos/src/middleware/diagnostic.rs b/core/startos/src/middleware/diagnostic.rs deleted file mode 100644 index 2a7467e34..000000000 --- a/core/startos/src/middleware/diagnostic.rs +++ /dev/null @@ -1,42 +0,0 @@ -use rpc_toolkit::yajrc::RpcMethod; -use rpc_toolkit::{Empty, Middleware, RpcRequest, RpcResponse}; - -use crate::context::DiagnosticContext; -use crate::prelude::*; - -#[derive(Clone)] -pub struct DiagnosticMode { - method: Option, -} -impl DiagnosticMode { - pub fn new() -> Self { - Self { method: None } - } -} - -impl Middleware for DiagnosticMode { - type Metadata = Empty; - async fn process_rpc_request( - &mut self, - _: &DiagnosticContext, - _: Self::Metadata, - request: &mut RpcRequest, - ) -> Result<(), RpcResponse> { - self.method = Some(request.method.as_str().to_owned()); - Ok(()) - } - async fn process_rpc_response(&mut self, _: &DiagnosticContext, response: &mut RpcResponse) { - if let Err(e) = &mut response.result { - if e.code == -32601 { - *e = Error::new( - eyre!( - "{} is not available on the Diagnostic API", - self.method.as_ref().map(|s| s.as_str()).unwrap_or_default() - ), - crate::ErrorKind::DiagnosticMode, - ) - .into(); - } - } - } -} diff --git a/core/startos/src/middleware/mod.rs b/core/startos/src/middleware/mod.rs index 3af0cb5a4..3438dc3db 100644 --- a/core/startos/src/middleware/mod.rs +++ b/core/startos/src/middleware/mod.rs @@ -1,4 +1,3 @@ pub mod auth; pub mod cors; pub mod db; -pub mod diagnostic; diff --git a/core/startos/src/net/net_controller.rs b/core/startos/src/net/net_controller.rs index 69c5c7940..270c7ca09 100644 --- a/core/startos/src/net/net_controller.rs +++ b/core/startos/src/net/net_controller.rs @@ -23,22 +23,18 @@ use crate::prelude::*; use crate::util::serde::MaybeUtf8String; use crate::HOST_IP; -pub struct NetController { - db: TypedPatchDb, - pub(super) tor: TorController, - pub(super) vhost: VHostController, - pub(super) dns: DnsController, - pub(super) forward: LanPortForwardController, - pub(super) os_bindings: Vec>, +pub struct PreInitNetController { + pub db: TypedPatchDb, + tor: TorController, + vhost: VHostController, + os_bindings: Vec>, } - -impl NetController { +impl PreInitNetController { #[instrument(skip_all)] pub async fn init( db: TypedPatchDb, tor_control: SocketAddr, tor_socks: SocketAddr, - dns_bind: &[SocketAddr], hostname: &Hostname, os_tor_key: TorSecretKeyV3, ) -> Result { @@ -46,8 +42,6 @@ impl NetController { db: db.clone(), tor: TorController::new(tor_control, tor_socks), vhost: VHostController::new(db), - dns: DnsController::init(dns_bind).await?, - forward: LanPortForwardController::new(), os_bindings: Vec::new(), }; res.add_os_bindings(hostname, os_tor_key).await?; @@ -73,8 +67,6 @@ impl NetController { alpn.clone(), ) .await?; - self.os_bindings - .push(self.dns.add(None, HOST_IP.into()).await?); // LAN IP self.os_bindings.push( @@ -142,6 +134,39 @@ impl NetController { Ok(()) } +} + +pub struct NetController { + db: TypedPatchDb, + pub(super) tor: TorController, + pub(super) vhost: VHostController, + pub(super) dns: DnsController, + pub(super) forward: LanPortForwardController, + pub(super) os_bindings: Vec>, +} + +impl NetController { + pub async fn init( + PreInitNetController { + db, + tor, + vhost, + os_bindings, + }: PreInitNetController, + dns_bind: &[SocketAddr], + ) -> Result { + let mut res = Self { + db, + tor, + vhost, + dns: DnsController::init(dns_bind).await?, + forward: LanPortForwardController::new(), + os_bindings, + }; + res.os_bindings + .push(res.dns.add(None, HOST_IP.into()).await?); + Ok(res) + } #[instrument(skip_all)] pub async fn create_service( diff --git a/core/startos/src/net/refresher.html b/core/startos/src/net/refresher.html new file mode 100644 index 000000000..445c6b5be --- /dev/null +++ b/core/startos/src/net/refresher.html @@ -0,0 +1,11 @@ + + + StartOS: Loading... + + + + Loading... + + \ No newline at end of file diff --git a/core/startos/src/net/static_server.rs b/core/startos/src/net/static_server.rs index fff1731ce..7e8034d99 100644 --- a/core/startos/src/net/static_server.rs +++ b/core/startos/src/net/static_server.rs @@ -1,4 +1,3 @@ -use std::fs::Metadata; use std::future::Future; use std::path::{Path, PathBuf}; use std::time::UNIX_EPOCH; @@ -13,25 +12,26 @@ use digest::Digest; use futures::future::ready; use http::header::ACCEPT_ENCODING; use http::request::Parts as RequestParts; -use http::{HeaderMap, Method, StatusCode}; +use http::{Method, StatusCode}; +use imbl_value::InternedString; use include_dir::Dir; use new_mime_guess::MimeGuess; use openssl::hash::MessageDigest; use openssl::x509::X509; -use rpc_toolkit::Server; +use rpc_toolkit::{Context, HttpServer, Server}; use tokio::fs::File; use tokio::io::BufReader; use tokio_util::io::ReaderStream; -use crate::context::{DiagnosticContext, InstallContext, RpcContext, SetupContext}; -use crate::db::subscribe; +use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext}; use crate::hostname::Hostname; use crate::middleware::auth::{Auth, HasValidSession}; use crate::middleware::cors::Cors; use crate::middleware::db::SyncDb; -use crate::middleware::diagnostic::DiagnosticMode; -use crate::rpc_continuations::Guid; -use crate::{diagnostic_api, install_api, main_api, setup_api, Error, ErrorKind, ResultExt}; +use crate::rpc_continuations::{Guid, RpcContinuations}; +use crate::{ + diagnostic_api, init_api, install_api, main_api, setup_api, Error, ErrorKind, ResultExt, +}; const NOT_FOUND: &[u8] = b"Not Found"; const METHOD_NOT_ALLOWED: &[u8] = b"Method Not Allowed"; @@ -49,7 +49,6 @@ const PROXY_STRIP_HEADERS: &[&str] = &["cookie", "host", "origin", "referer", "u #[derive(Clone)] pub enum UiMode { Setup, - Diag, Install, Main, } @@ -58,128 +57,46 @@ impl UiMode { fn path(&self, path: &str) -> PathBuf { match self { Self::Setup => Path::new("setup-wizard").join(path), - Self::Diag => Path::new("diagnostic-ui").join(path), Self::Install => Path::new("install-wizard").join(path), Self::Main => Path::new("ui").join(path), } } } -pub fn setup_ui_file_router(ctx: SetupContext) -> Router { - Router::new() - .route_service( - "/rpc/*path", - post(Server::new(move || ready(Ok(ctx.clone())), setup_api()).middleware(Cors::new())), - ) - .fallback(any(|request: Request| async move { - alt_ui(request, UiMode::Setup) - .await - .unwrap_or_else(server_error) - })) -} - -pub fn diag_ui_file_router(ctx: DiagnosticContext) -> Router { +pub fn rpc_router>( + ctx: C, + server: HttpServer, +) -> Router { Router::new() + .route("/rpc/*path", post(server)) .route( - "/rpc/*path", - post( - Server::new(move || ready(Ok(ctx.clone())), diagnostic_api()) - .middleware(Cors::new()) - .middleware(DiagnosticMode::new()), - ), - ) - .fallback(any(|request: Request| async move { - alt_ui(request, UiMode::Diag) - .await - .unwrap_or_else(server_error) - })) -} - -pub fn install_ui_file_router(ctx: InstallContext) -> Router { - Router::new() - .route("/rpc/*path", { - let ctx = ctx.clone(); - post(Server::new(move || ready(Ok(ctx.clone())), install_api()).middleware(Cors::new())) - }) - .fallback(any(|request: Request| async move { - alt_ui(request, UiMode::Install) - .await - .unwrap_or_else(server_error) - })) -} - -pub fn main_ui_server_router(ctx: RpcContext) -> Router { - Router::new() - .route("/rpc/*path", { - let ctx = ctx.clone(); - post( - Server::new(move || ready(Ok(ctx.clone())), main_api::()) - .middleware(Cors::new()) - .middleware(Auth::new()) - .middleware(SyncDb::new()), - ) - }) - .route( - "/ws/db", - any({ - let ctx = ctx.clone(); - move |headers: HeaderMap, ws: x::WebSocketUpgrade| async move { - subscribe(ctx, headers, ws) - .await - .unwrap_or_else(server_error) - } - }), - ) - .route( - "/ws/rpc/*path", + "/ws/rpc/:guid", get({ let ctx = ctx.clone(); - move |x::Path(path): x::Path, + move |x::Path(guid): x::Path, ws: axum::extract::ws::WebSocketUpgrade| async move { - match Guid::from(&path) { - None => { - tracing::debug!("No Guid Path"); - bad_request() - } - Some(guid) => match ctx.rpc_continuations.get_ws_handler(&guid).await { - Some(cont) => ws.on_upgrade(cont), - _ => not_found(), - }, + match AsRef::::as_ref(&ctx).get_ws_handler(&guid).await { + Some(cont) => ws.on_upgrade(cont), + _ => not_found(), } } }), ) .route( - "/rest/rpc/*path", + "/rest/rpc/:guid", any({ let ctx = ctx.clone(); - move |request: x::Request| async move { - let path = request - .uri() - .path() - .strip_prefix("/rest/rpc/") - .unwrap_or_default(); - match Guid::from(&path) { - None => { - tracing::debug!("No Guid Path"); - bad_request() - } - Some(guid) => match ctx.rpc_continuations.get_rest_handler(&guid).await { - None => not_found(), - Some(cont) => cont(request).await.unwrap_or_else(server_error), - }, + move |x::Path(guid): x::Path, request: x::Request| async move { + match AsRef::::as_ref(&ctx).get_rest_handler(&guid).await { + None => not_found(), + Some(cont) => cont(request).await.unwrap_or_else(server_error), } } }), ) - .fallback(any(move |request: Request| async move { - main_start_os_ui(request, ctx) - .await - .unwrap_or_else(server_error) - })) } -async fn alt_ui(req: Request, ui_mode: UiMode) -> Result { +fn serve_ui(req: Request, ui_mode: UiMode) -> Result { let (request_parts, _body) = req.into_parts(); match &request_parts.method { &Method::GET => { @@ -196,9 +113,7 @@ async fn alt_ui(req: Request, ui_mode: UiMode) -> Result { .or_else(|| EMBEDDED_UIS.get_file(&*ui_mode.path("index.html"))); if let Some(file) = file { - FileData::from_embedded(&request_parts, file) - .into_response(&request_parts) - .await + FileData::from_embedded(&request_parts, file).into_response(&request_parts) } else { Ok(not_found()) } @@ -207,6 +122,75 @@ async fn alt_ui(req: Request, ui_mode: UiMode) -> Result { } } +pub fn setup_ui_router(ctx: SetupContext) -> Router { + rpc_router( + ctx.clone(), + Server::new(move || ready(Ok(ctx.clone())), setup_api()).middleware(Cors::new()), + ) + .fallback(any(|request: Request| async move { + serve_ui(request, UiMode::Setup).unwrap_or_else(server_error) + })) +} + +pub fn diagnostic_ui_router(ctx: DiagnosticContext) -> Router { + rpc_router( + ctx.clone(), + Server::new(move || ready(Ok(ctx.clone())), diagnostic_api()).middleware(Cors::new()), + ) + .fallback(any(|request: Request| async move { + serve_ui(request, UiMode::Main).unwrap_or_else(server_error) + })) +} + +pub fn install_ui_router(ctx: InstallContext) -> Router { + rpc_router( + ctx.clone(), + Server::new(move || ready(Ok(ctx.clone())), install_api()).middleware(Cors::new()), + ) + .fallback(any(|request: Request| async move { + serve_ui(request, UiMode::Install).unwrap_or_else(server_error) + })) +} + +pub fn init_ui_router(ctx: InitContext) -> Router { + rpc_router( + ctx.clone(), + Server::new(move || ready(Ok(ctx.clone())), init_api()).middleware(Cors::new()), + ) + .fallback(any(|request: Request| async move { + serve_ui(request, UiMode::Main).unwrap_or_else(server_error) + })) +} + +pub fn main_ui_router(ctx: RpcContext) -> Router { + rpc_router( + ctx.clone(), + Server::new(move || ready(Ok(ctx.clone())), main_api::()) + .middleware(Cors::new()) + .middleware(Auth::new()) + .middleware(SyncDb::new()), + ) + // TODO: cert + .fallback(any(|request: Request| async move { + serve_ui(request, UiMode::Main).unwrap_or_else(server_error) + })) +} + +pub fn refresher() -> Router { + Router::new().fallback(get(|request: Request| async move { + let res = include_bytes!("./refresher.html"); + FileData { + data: Body::from(&res[..]), + e_tag: None, + encoding: None, + len: Some(res.len() as u64), + mime: Some("text/html".into()), + } + .into_response(&request.into_parts().0) + .unwrap_or_else(server_error) + })) +} + async fn if_authorized< F: FnOnce() -> Fut, Fut: Future> + Send + Sync, @@ -223,89 +207,6 @@ async fn if_authorized< } } -async fn main_start_os_ui(req: Request, ctx: RpcContext) -> Result { - let (request_parts, _body) = req.into_parts(); - match ( - &request_parts.method, - request_parts - .uri - .path() - .strip_prefix('/') - .unwrap_or(request_parts.uri.path()) - .split_once('/'), - ) { - (&Method::GET, Some(("public", path))) => { - todo!("pull directly from s9pk") - } - (&Method::GET, Some(("proxy", target))) => { - if_authorized(&ctx, &request_parts, || async { - let target = urlencoding::decode(target)?; - let res = ctx - .client - .get(target.as_ref()) - .headers( - request_parts - .headers - .iter() - .filter(|(h, _)| { - !PROXY_STRIP_HEADERS - .iter() - .any(|bad| h.as_str().eq_ignore_ascii_case(bad)) - }) - .flat_map(|(h, v)| { - Some(( - reqwest::header::HeaderName::from_lowercase( - h.as_str().as_bytes(), - ) - .ok()?, - reqwest::header::HeaderValue::from_bytes(v.as_bytes()).ok()?, - )) - }) - .collect(), - ) - .send() - .await - .with_kind(crate::ErrorKind::Network)?; - let mut hres = Response::builder().status(res.status().as_u16()); - for (h, v) in res.headers().clone() { - if let Some(h) = h { - hres = hres.header(h.to_string(), v.as_bytes()); - } - } - hres.body(Body::from_stream(res.bytes_stream())) - .with_kind(crate::ErrorKind::Network) - }) - .await - } - (&Method::GET, Some(("eos", "local.crt"))) => { - let account = ctx.account.read().await; - cert_send(&account.root_ca_cert, &account.hostname) - } - (&Method::GET, _) => { - let uri_path = UiMode::Main.path( - request_parts - .uri - .path() - .strip_prefix('/') - .unwrap_or(request_parts.uri.path()), - ); - - let file = EMBEDDED_UIS - .get_file(&*uri_path) - .or_else(|| EMBEDDED_UIS.get_file(&*UiMode::Main.path("index.html"))); - - if let Some(file) = file { - FileData::from_embedded(&request_parts, file) - .into_response(&request_parts) - .await - } else { - Ok(not_found()) - } - } - _ => Ok(method_not_allowed()), - } -} - pub fn unauthorized(err: Error, path: &str) -> Response { tracing::warn!("unauthorized for {} @{:?}", err, path); tracing::debug!("{:?}", err); @@ -373,8 +274,8 @@ struct FileData { data: Body, len: Option, encoding: Option<&'static str>, - e_tag: String, - mime: Option, + e_tag: Option, + mime: Option, } impl FileData { fn from_embedded(req: &RequestParts, file: &'static include_dir::File<'static>) -> Self { @@ -407,10 +308,23 @@ impl FileData { len: Some(data.len() as u64), encoding, data: data.into(), - e_tag: e_tag(path, None), + e_tag: file.metadata().map(|metadata| { + e_tag( + path, + format!( + "{}", + metadata + .modified() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or_else(|e| e.duration().as_secs() as i64 * -1), + ) + .as_bytes(), + ) + }), mime: MimeGuess::from_path(path) .first() - .map(|m| m.essence_str().to_owned()), + .map(|m| m.essence_str().into()), } } @@ -434,7 +348,18 @@ impl FileData { .await .with_ctx(|_| (ErrorKind::Filesystem, path.display().to_string()))?; - let e_tag = e_tag(path, Some(&metadata)); + let e_tag = Some(e_tag( + path, + format!( + "{}", + metadata + .modified()? + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or_else(|e| e.duration().as_secs() as i64 * -1) + ) + .as_bytes(), + )); let (len, data) = if encoding == Some("gzip") { ( @@ -455,16 +380,18 @@ impl FileData { e_tag, mime: MimeGuess::from_path(path) .first() - .map(|m| m.essence_str().to_owned()), + .map(|m| m.essence_str().into()), }) } - async fn into_response(self, req: &RequestParts) -> Result { + fn into_response(self, req: &RequestParts) -> Result { let mut builder = Response::builder(); if let Some(mime) = self.mime { builder = builder.header(http::header::CONTENT_TYPE, &*mime); } - builder = builder.header(http::header::ETAG, &*self.e_tag); + if let Some(e_tag) = &self.e_tag { + builder = builder.header(http::header::ETAG, &**e_tag); + } builder = builder.header( http::header::CACHE_CONTROL, "public, max-age=21000000, immutable", @@ -481,11 +408,12 @@ impl FileData { builder = builder.header(http::header::CONNECTION, "keep-alive"); } - if req - .headers - .get("if-none-match") - .and_then(|h| h.to_str().ok()) - == Some(self.e_tag.as_ref()) + if self.e_tag.is_some() + && req + .headers + .get("if-none-match") + .and_then(|h| h.to_str().ok()) + == self.e_tag.as_deref() { builder = builder.status(StatusCode::NOT_MODIFIED); builder.body(Body::empty()) @@ -503,21 +431,14 @@ impl FileData { } } -fn e_tag(path: &Path, metadata: Option<&Metadata>) -> String { +lazy_static::lazy_static! { + static ref INSTANCE_NONCE: u64 = rand::random(); +} + +fn e_tag(path: &Path, modified: impl AsRef<[u8]>) -> String { let mut hasher = sha2::Sha256::new(); hasher.update(format!("{:?}", path).as_bytes()); - if let Some(modified) = metadata.and_then(|m| m.modified().ok()) { - hasher.update( - format!( - "{}", - modified - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs() - ) - .as_bytes(), - ); - } + hasher.update(modified.as_ref()); let res = hasher.finalize(); format!( "\"{}\"", diff --git a/core/startos/src/net/web_server.rs b/core/startos/src/net/web_server.rs index a89aae92f..a9cfdf046 100644 --- a/core/startos/src/net/web_server.rs +++ b/core/startos/src/net/web_server.rs @@ -1,23 +1,84 @@ +use std::convert::Infallible; use std::net::SocketAddr; +use std::task::Poll; use std::time::Duration; +use axum::extract::Request; use axum::Router; use axum_server::Handle; +use bytes::Bytes; +use futures::future::ready; +use futures::FutureExt; use helpers::NonDetachingJoinHandle; -use tokio::sync::oneshot; +use tokio::sync::{oneshot, watch}; -use crate::context::{DiagnosticContext, InstallContext, RpcContext, SetupContext}; +use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext}; use crate::net::static_server::{ - diag_ui_file_router, install_ui_file_router, main_ui_server_router, setup_ui_file_router, + diagnostic_ui_router, init_ui_router, install_ui_router, main_ui_router, refresher, + setup_ui_router, }; -use crate::Error; +use crate::prelude::*; + +#[derive(Clone)] +pub struct SwappableRouter(watch::Sender); +impl SwappableRouter { + pub fn new(router: Router) -> Self { + Self(watch::channel(router).0) + } + pub fn swap(&self, router: Router) { + let _ = self.0.send_replace(router); + } +} + +#[derive(Clone)] +pub struct SwappableRouterService(watch::Receiver); +impl tower_service::Service> for SwappableRouterService +where + B: axum::body::HttpBody + Send + 'static, + B::Error: Into, +{ + type Response = >>::Response; + type Error = >>::Error; + type Future = >>::Future; + #[inline] + fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> Poll> { + let mut changed = self.0.changed().boxed(); + if changed.poll_unpin(cx).is_ready() { + return Poll::Ready(Ok(())); + } + drop(changed); + tower_service::Service::>::poll_ready(&mut self.0.borrow().clone(), cx) + } + fn call(&mut self, req: Request) -> Self::Future { + self.0.borrow().clone().call(req) + } +} + +impl tower_service::Service for SwappableRouter { + type Response = SwappableRouterService; + type Error = Infallible; + type Future = futures::future::Ready>; + #[inline] + fn poll_ready( + &mut self, + _: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, _: T) -> Self::Future { + ready(Ok(SwappableRouterService(self.0.subscribe()))) + } +} pub struct WebServer { shutdown: oneshot::Sender<()>, + router: SwappableRouter, thread: NonDetachingJoinHandle<()>, } impl WebServer { - pub fn new(bind: SocketAddr, router: Router) -> Self { + pub fn new(bind: SocketAddr) -> Self { + let router = SwappableRouter::new(refresher()); + let thread_router = router.clone(); let (shutdown, shutdown_recv) = oneshot::channel(); let thread = NonDetachingJoinHandle::from(tokio::spawn(async move { let handle = Handle::new(); @@ -25,14 +86,18 @@ impl WebServer { server.http_builder().http1().preserve_header_case(true); server.http_builder().http1().title_case_headers(true); - if let (Err(e), _) = tokio::join!(server.serve(router.into_make_service()), async { + if let (Err(e), _) = tokio::join!(server.serve(thread_router), async { let _ = shutdown_recv.await; handle.graceful_shutdown(Some(Duration::from_secs(0))); }) { tracing::error!("Spawning hyper server error: {}", e); } })); - Self { shutdown, thread } + Self { + shutdown, + router, + thread, + } } pub async fn shutdown(self) { @@ -40,19 +105,27 @@ impl WebServer { self.thread.await.unwrap() } - pub fn main(bind: SocketAddr, ctx: RpcContext) -> Result { - Ok(Self::new(bind, main_ui_server_router(ctx))) + pub fn serve_router(&mut self, router: Router) { + self.router.swap(router) } - pub fn setup(bind: SocketAddr, ctx: SetupContext) -> Result { - Ok(Self::new(bind, setup_ui_file_router(ctx))) + pub fn serve_main(&mut self, ctx: RpcContext) { + self.serve_router(main_ui_router(ctx)) } - pub fn diagnostic(bind: SocketAddr, ctx: DiagnosticContext) -> Result { - Ok(Self::new(bind, diag_ui_file_router(ctx))) + pub fn serve_setup(&mut self, ctx: SetupContext) { + self.serve_router(setup_ui_router(ctx)) } - pub fn install(bind: SocketAddr, ctx: InstallContext) -> Result { - Ok(Self::new(bind, install_ui_file_router(ctx))) + pub fn serve_diagnostic(&mut self, ctx: DiagnosticContext) { + self.serve_router(diagnostic_ui_router(ctx)) + } + + pub fn serve_install(&mut self, ctx: InstallContext) { + self.serve_router(install_ui_router(ctx)) + } + + pub fn serve_init(&mut self, ctx: InitContext) { + self.serve_router(init_ui_router(ctx)) } } diff --git a/core/startos/src/progress.rs b/core/startos/src/progress.rs index 9a22405ca..f70a5adbc 100644 --- a/core/startos/src/progress.rs +++ b/core/startos/src/progress.rs @@ -1,14 +1,16 @@ use std::panic::UnwindSafe; -use std::sync::Arc; use std::time::Duration; -use futures::Future; +use futures::future::pending; +use futures::stream::BoxStream; +use futures::{Future, FutureExt, StreamExt, TryFutureExt}; +use helpers::NonDetachingJoinHandle; use imbl_value::{InOMap, InternedString}; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use itertools::Itertools; use serde::{Deserialize, Serialize}; use tokio::io::{AsyncSeek, AsyncWrite}; -use tokio::sync::{mpsc, watch}; +use tokio::sync::watch; use ts_rs::TS; use crate::db::model::{Database, DatabaseModel}; @@ -168,39 +170,23 @@ impl FullProgress { } } +#[derive(Clone)] pub struct FullProgressTracker { - overall: Arc>, - overall_recv: watch::Receiver, - phases: InOMap>, - new_phase: ( - mpsc::UnboundedSender<(InternedString, watch::Receiver)>, - mpsc::UnboundedReceiver<(InternedString, watch::Receiver)>, - ), + overall: watch::Sender, + phases: watch::Sender>>, } impl FullProgressTracker { pub fn new() -> Self { - let (overall, overall_recv) = watch::channel(Progress::new()); - Self { - overall: Arc::new(overall), - overall_recv, - phases: InOMap::new(), - new_phase: mpsc::unbounded_channel(), - } + let (overall, _) = watch::channel(Progress::new()); + let (phases, _) = watch::channel(InOMap::new()); + Self { overall, phases } } - fn fill_phases(&mut self) -> bool { - let mut changed = false; - while let Ok((name, phase)) = self.new_phase.1.try_recv() { - self.phases.insert(name, phase); - changed = true; - } - changed - } - pub fn snapshot(&mut self) -> FullProgress { - self.fill_phases(); + pub fn snapshot(&self) -> FullProgress { FullProgress { overall: *self.overall.borrow(), phases: self .phases + .borrow() .iter() .map(|(name, progress)| NamedProgress { name: name.clone(), @@ -209,28 +195,75 @@ impl FullProgressTracker { .collect(), } } - pub async fn changed(&mut self) { - if self.fill_phases() { - return; - } - let phases = self - .phases - .iter_mut() - .map(|(_, p)| Box::pin(p.changed())) - .collect_vec(); - tokio::select! { - _ = self.overall_recv.changed() => (), - _ = futures::future::select_all(phases) => (), - } - } - pub fn handle(&self) -> FullProgressTrackerHandle { - FullProgressTrackerHandle { - overall: self.overall.clone(), - new_phase: self.new_phase.0.clone(), + pub fn stream(&self, min_interval: Option) -> BoxStream<'static, FullProgress> { + struct StreamState { + overall: watch::Receiver, + phases_recv: watch::Receiver>>, + phases: InOMap>, } + let mut overall = self.overall.subscribe(); + overall.mark_changed(); // make sure stream starts with a value + let phases_recv = self.phases.subscribe(); + let phases = phases_recv.borrow().clone(); + let state = StreamState { + overall, + phases_recv, + phases, + }; + futures::stream::unfold( + state, + move |StreamState { + mut overall, + mut phases_recv, + mut phases, + }| async move { + let changed = phases + .iter_mut() + .map(|(_, p)| async move { p.changed().or_else(|_| pending()).await }.boxed()) + .chain([overall.changed().boxed()]) + .chain([phases_recv.changed().boxed()]) + .map(|fut| fut.map(|r| r.unwrap_or_default())) + .collect_vec(); + if let Some(min_interval) = min_interval { + tokio::join!( + tokio::time::sleep(min_interval), + futures::future::select_all(changed), + ); + } else { + futures::future::select_all(changed).await; + } + + for (name, phase) in &*phases_recv.borrow_and_update() { + if !phases.contains_key(name) { + phases.insert(name.clone(), phase.clone()); + } + } + + let o = *overall.borrow_and_update(); + + Some(( + FullProgress { + overall: o, + phases: phases + .iter_mut() + .map(|(name, progress)| NamedProgress { + name: name.clone(), + progress: *progress.borrow_and_update(), + }) + .collect(), + }, + StreamState { + overall, + phases_recv, + phases, + }, + )) + }, + ) + .boxed() } pub fn sync_to_db( - mut self, + &self, db: TypedPatchDb, deref: DerefFn, min_interval: Option, @@ -239,9 +272,9 @@ impl FullProgressTracker { DerefFn: Fn(&mut DatabaseModel) -> Option<&mut Model> + 'static, for<'a> &'a DerefFn: UnwindSafe + Send, { + let mut stream = self.stream(min_interval); async move { - loop { - let progress = self.snapshot(); + while let Some(progress) = stream.next().await { if db .mutate(|v| { if let Some(p) = deref(v) { @@ -255,25 +288,23 @@ impl FullProgressTracker { { break; } - tokio::join!(self.changed(), async { - if let Some(interval) = min_interval { - tokio::time::sleep(interval).await - } else { - futures::future::ready(()).await - } - }); } Ok(()) } } -} - -#[derive(Clone)] -pub struct FullProgressTrackerHandle { - overall: Arc>, - new_phase: mpsc::UnboundedSender<(InternedString, watch::Receiver)>, -} -impl FullProgressTrackerHandle { + pub fn progress_bar_task(&self, name: &str) -> NonDetachingJoinHandle<()> { + let mut stream = self.stream(None); + let mut bar = PhasedProgressBar::new(name); + tokio::spawn(async move { + while let Some(progress) = stream.next().await { + bar.update(&progress); + if progress.overall.is_complete() { + break; + } + } + }) + .into() + } pub fn add_phase( &self, name: InternedString, @@ -284,7 +315,9 @@ impl FullProgressTrackerHandle { .send_modify(|o| o.add_total(overall_contribution)); } let (send, recv) = watch::channel(Progress::new()); - let _ = self.new_phase.send((name, recv)); + self.phases.send_modify(|p| { + p.insert(name, recv); + }); PhaseProgressTrackerHandle { overall: self.overall.clone(), overall_contribution, @@ -298,7 +331,7 @@ impl FullProgressTrackerHandle { } pub struct PhaseProgressTrackerHandle { - overall: Arc>, + overall: watch::Sender, overall_contribution: Option, contributed: u64, progress: watch::Sender, diff --git a/core/startos/src/registry/context.rs b/core/startos/src/registry/context.rs index 99d60307b..e5beabca7 100644 --- a/core/startos/src/registry/context.rs +++ b/core/startos/src/registry/context.rs @@ -169,7 +169,8 @@ impl CallRemote for CliContext { &AnySigningKey::Ed25519(self.developer_key()?.clone()), &body, &host, - )?.to_header(), + )? + .to_header(), ) .body(body) .send() diff --git a/core/startos/src/registry/mod.rs b/core/startos/src/registry/mod.rs index 656edf337..9a53d6338 100644 --- a/core/startos/src/registry/mod.rs +++ b/core/startos/src/registry/mod.rs @@ -70,7 +70,7 @@ pub fn registry_api() -> ParentHandler { .subcommand("db", db::db_api::()) } -pub fn registry_server_router(ctx: RegistryContext) -> Router { +pub fn registry_router(ctx: RegistryContext) -> Router { use axum::extract as x; use axum::routing::{any, get, post}; Router::new() @@ -128,7 +128,7 @@ pub fn registry_server_router(ctx: RegistryContext) -> Router { } impl WebServer { - pub fn registry(bind: SocketAddr, ctx: RegistryContext) -> Self { - Self::new(bind, registry_server_router(ctx)) + pub fn serve_registry(&mut self, ctx: RegistryContext) { + self.serve_router(registry_router(ctx)) } } diff --git a/core/startos/src/registry/os/asset/add.rs b/core/startos/src/registry/os/asset/add.rs index 6ca495547..33f5ef90f 100644 --- a/core/startos/src/registry/os/asset/add.rs +++ b/core/startos/src/registry/os/asset/add.rs @@ -186,29 +186,16 @@ pub async fn cli_add_asset( let file = MultiCursorFile::from(tokio::fs::File::open(&path).await?); - let mut progress = FullProgressTracker::new(); - let progress_handle = progress.handle(); - let mut sign_phase = - progress_handle.add_phase(InternedString::intern("Signing File"), Some(10)); - let mut verify_phase = - progress_handle.add_phase(InternedString::intern("Verifying URL"), Some(100)); - let mut index_phase = progress_handle.add_phase( + let progress = FullProgressTracker::new(); + let mut sign_phase = progress.add_phase(InternedString::intern("Signing File"), Some(10)); + let mut verify_phase = progress.add_phase(InternedString::intern("Verifying URL"), Some(100)); + let mut index_phase = progress.add_phase( InternedString::intern("Adding File to Registry Index"), Some(1), ); - let progress_task: NonDetachingJoinHandle<()> = tokio::spawn(async move { - let mut bar = PhasedProgressBar::new(&format!("Adding {} to registry...", path.display())); - loop { - let snap = progress.snapshot(); - bar.update(&snap); - if snap.overall.is_complete() { - break; - } - progress.changed().await - } - }) - .into(); + let progress_task = + progress.progress_bar_task(&format!("Adding {} to registry...", path.display())); sign_phase.start(); let blake3 = file.blake3_mmap().await?; @@ -252,7 +239,7 @@ pub async fn cli_add_asset( .await?; index_phase.complete(); - progress_handle.complete(); + progress.complete(); progress_task.await.with_kind(ErrorKind::Unknown)?; diff --git a/core/startos/src/registry/os/asset/get.rs b/core/startos/src/registry/os/asset/get.rs index e099a50cf..29ff24da6 100644 --- a/core/startos/src/registry/os/asset/get.rs +++ b/core/startos/src/registry/os/asset/get.rs @@ -3,7 +3,7 @@ use std::panic::UnwindSafe; use std::path::{Path, PathBuf}; use clap::Parser; -use helpers::{AtomicFile, NonDetachingJoinHandle}; +use helpers::AtomicFile; use imbl_value::{json, InternedString}; use itertools::Itertools; use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; @@ -12,7 +12,7 @@ use ts_rs::TS; use crate::context::CliContext; use crate::prelude::*; -use crate::progress::{FullProgressTracker, PhasedProgressBar}; +use crate::progress::FullProgressTracker; use crate::registry::asset::RegistryAsset; use crate::registry::context::RegistryContext; use crate::registry::os::index::OsVersionInfo; @@ -135,29 +135,17 @@ async fn cli_get_os_asset( .await .with_kind(ErrorKind::Filesystem)?; - let mut progress = FullProgressTracker::new(); - let progress_handle = progress.handle(); + let progress = FullProgressTracker::new(); let mut download_phase = - progress_handle.add_phase(InternedString::intern("Downloading File"), Some(100)); + progress.add_phase(InternedString::intern("Downloading File"), Some(100)); download_phase.set_total(res.commitment.size); let reverify_phase = if reverify { - Some(progress_handle.add_phase(InternedString::intern("Reverifying File"), Some(10))) + Some(progress.add_phase(InternedString::intern("Reverifying File"), Some(10))) } else { None }; - let progress_task: NonDetachingJoinHandle<()> = tokio::spawn(async move { - let mut bar = PhasedProgressBar::new("Downloading..."); - loop { - let snap = progress.snapshot(); - bar.update(&snap); - if snap.overall.is_complete() { - break; - } - progress.changed().await - } - }) - .into(); + let progress_task = progress.progress_bar_task("Downloading..."); download_phase.start(); let mut download_writer = download_phase.writer(&mut *file); @@ -177,7 +165,7 @@ async fn cli_get_os_asset( reverify_phase.complete(); } - progress_handle.complete(); + progress.complete(); progress_task.await.with_kind(ErrorKind::Unknown)?; } diff --git a/core/startos/src/registry/os/asset/sign.rs b/core/startos/src/registry/os/asset/sign.rs index 0cb657bef..50c583593 100644 --- a/core/startos/src/registry/os/asset/sign.rs +++ b/core/startos/src/registry/os/asset/sign.rs @@ -3,7 +3,6 @@ use std::panic::UnwindSafe; use std::path::PathBuf; use clap::Parser; -use helpers::NonDetachingJoinHandle; use imbl_value::InternedString; use itertools::Itertools; use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; @@ -12,7 +11,7 @@ use ts_rs::TS; use crate::context::CliContext; use crate::prelude::*; -use crate::progress::{FullProgressTracker, PhasedProgressBar}; +use crate::progress::FullProgressTracker; use crate::registry::asset::RegistryAsset; use crate::registry::context::RegistryContext; use crate::registry::os::index::OsVersionInfo; @@ -169,27 +168,15 @@ pub async fn cli_sign_asset( let file = MultiCursorFile::from(tokio::fs::File::open(&path).await?); - let mut progress = FullProgressTracker::new(); - let progress_handle = progress.handle(); - let mut sign_phase = - progress_handle.add_phase(InternedString::intern("Signing File"), Some(10)); - let mut index_phase = progress_handle.add_phase( + let progress = FullProgressTracker::new(); + let mut sign_phase = progress.add_phase(InternedString::intern("Signing File"), Some(10)); + let mut index_phase = progress.add_phase( InternedString::intern("Adding Signature to Registry Index"), Some(1), ); - let progress_task: NonDetachingJoinHandle<()> = tokio::spawn(async move { - let mut bar = PhasedProgressBar::new(&format!("Adding {} to registry...", path.display())); - loop { - let snap = progress.snapshot(); - bar.update(&snap); - if snap.overall.is_complete() { - break; - } - progress.changed().await - } - }) - .into(); + let progress_task = + progress.progress_bar_task(&format!("Adding {} to registry...", path.display())); sign_phase.start(); let blake3 = file.blake3_mmap().await?; @@ -220,7 +207,7 @@ pub async fn cli_sign_asset( .await?; index_phase.complete(); - progress_handle.complete(); + progress.complete(); progress_task.await.with_kind(ErrorKind::Unknown)?; diff --git a/core/startos/src/registry/package/add.rs b/core/startos/src/registry/package/add.rs index d28aeaaa4..9bc772f78 100644 --- a/core/startos/src/registry/package/add.rs +++ b/core/startos/src/registry/package/add.rs @@ -2,7 +2,6 @@ use std::path::PathBuf; use std::sync::Arc; use clap::Parser; -use helpers::NonDetachingJoinHandle; use imbl_value::InternedString; use itertools::Itertools; use rpc_toolkit::HandlerArgs; @@ -12,7 +11,7 @@ use url::Url; use crate::context::CliContext; use crate::prelude::*; -use crate::progress::{FullProgressTracker, PhasedProgressBar}; +use crate::progress::FullProgressTracker; use crate::registry::context::RegistryContext; use crate::registry::package::index::PackageVersionInfo; use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; @@ -110,28 +109,16 @@ pub async fn cli_add_package( ) -> Result<(), Error> { let s9pk = S9pk::open(&file, None).await?; - let mut progress = FullProgressTracker::new(); - let progress_handle = progress.handle(); - let mut sign_phase = progress_handle.add_phase(InternedString::intern("Signing File"), Some(1)); - let mut verify_phase = - progress_handle.add_phase(InternedString::intern("Verifying URL"), Some(100)); - let mut index_phase = progress_handle.add_phase( + let progress = FullProgressTracker::new(); + let mut sign_phase = progress.add_phase(InternedString::intern("Signing File"), Some(1)); + let mut verify_phase = progress.add_phase(InternedString::intern("Verifying URL"), Some(100)); + let mut index_phase = progress.add_phase( InternedString::intern("Adding File to Registry Index"), Some(1), ); - let progress_task: NonDetachingJoinHandle<()> = tokio::spawn(async move { - let mut bar = PhasedProgressBar::new(&format!("Adding {} to registry...", file.display())); - loop { - let snap = progress.snapshot(); - bar.update(&snap); - if snap.overall.is_complete() { - break; - } - progress.changed().await - } - }) - .into(); + let progress_task = + progress.progress_bar_task(&format!("Adding {} to registry...", file.display())); sign_phase.start(); let commitment = s9pk.as_archive().commitment().await?; @@ -160,7 +147,7 @@ pub async fn cli_add_package( .await?; index_phase.complete(); - progress_handle.complete(); + progress.complete(); progress_task.await.with_kind(ErrorKind::Unknown)?; diff --git a/core/startos/src/registry/signer/commitment/request.rs b/core/startos/src/registry/signer/commitment/request.rs index 62d59163f..ce60b7f88 100644 --- a/core/startos/src/registry/signer/commitment/request.rs +++ b/core/startos/src/registry/signer/commitment/request.rs @@ -1,5 +1,5 @@ -use std::time::{SystemTime, UNIX_EPOCH}; use std::collections::BTreeMap; +use std::time::{SystemTime, UNIX_EPOCH}; use axum::body::Body; use axum::extract::Request; diff --git a/core/startos/src/rpc_continuations.rs b/core/startos/src/rpc_continuations.rs index e6b823ef9..043130b69 100644 --- a/core/startos/src/rpc_continuations.rs +++ b/core/startos/src/rpc_continuations.rs @@ -1,5 +1,8 @@ use std::collections::BTreeMap; +use std::pin::Pin; use std::str::FromStr; +use std::sync::Mutex as SyncMutex; +use std::task::{Context, Poll}; use std::time::Duration; use axum::extract::ws::WebSocket; @@ -7,9 +10,10 @@ use axum::extract::Request; use axum::response::Response; use clap::builder::ValueParserFactory; use futures::future::BoxFuture; +use futures::{Future, FutureExt}; use helpers::TimedResource; use imbl_value::InternedString; -use tokio::sync::Mutex; +use tokio::sync::{broadcast, Mutex as AsyncMutex}; use ts_rs::TS; #[allow(unused_imports)] @@ -73,21 +77,103 @@ impl std::fmt::Display for Guid { } } -pub type RestHandler = - Box BoxFuture<'static, Result> + Send>; +pub struct RestFuture { + kill: Option>, + fut: BoxFuture<'static, Result>, +} +impl Future for RestFuture { + type Output = Result; + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if self.kill.as_ref().map_or(false, |k| !k.is_empty()) { + Poll::Ready(Err(Error::new( + eyre!("session killed"), + ErrorKind::Authorization, + ))) + } else { + self.fut.poll_unpin(cx) + } + } +} +pub type RestHandler = Box RestFuture + Send>; -pub type WebSocketHandler = Box BoxFuture<'static, ()> + Send>; +pub struct WebSocketFuture { + kill: Option>, + fut: BoxFuture<'static, ()>, +} +impl Future for WebSocketFuture { + type Output = (); + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if self.kill.as_ref().map_or(false, |k| !k.is_empty()) { + Poll::Ready(()) + } else { + self.fut.poll_unpin(cx) + } + } +} +pub type WebSocketHandler = Box WebSocketFuture + Send>; pub enum RpcContinuation { Rest(TimedResource), WebSocket(TimedResource), } impl RpcContinuation { - pub fn rest(handler: RestHandler, timeout: Duration) -> Self { - RpcContinuation::Rest(TimedResource::new(handler, timeout)) + pub fn rest(handler: F, timeout: Duration) -> Self + where + F: FnOnce(Request) -> Fut + Send + 'static, + Fut: Future> + Send + 'static, + { + RpcContinuation::Rest(TimedResource::new( + Box::new(|req| RestFuture { + kill: None, + fut: handler(req).boxed(), + }), + timeout, + )) } - pub fn ws(handler: WebSocketHandler, timeout: Duration) -> Self { - RpcContinuation::WebSocket(TimedResource::new(handler, timeout)) + pub fn ws(handler: F, timeout: Duration) -> Self + where + F: FnOnce(WebSocket) -> Fut + Send + 'static, + Fut: Future + Send + 'static, + { + RpcContinuation::WebSocket(TimedResource::new( + Box::new(|ws| WebSocketFuture { + kill: None, + fut: handler(ws).boxed(), + }), + timeout, + )) + } + pub fn rest_authed(ctx: Ctx, session: T, handler: F, timeout: Duration) -> Self + where + Ctx: AsRef>, + T: Eq + Ord, + F: FnOnce(Request) -> Fut + Send + 'static, + Fut: Future> + Send + 'static, + { + let kill = Some(ctx.as_ref().subscribe_to_kill(session)); + RpcContinuation::Rest(TimedResource::new( + Box::new(|req| RestFuture { + kill, + fut: handler(req).boxed(), + }), + timeout, + )) + } + pub fn ws_authed(ctx: Ctx, session: T, handler: F, timeout: Duration) -> Self + where + Ctx: AsRef>, + T: Eq + Ord, + F: FnOnce(WebSocket) -> Fut + Send + 'static, + Fut: Future + Send + 'static, + { + let kill = Some(ctx.as_ref().subscribe_to_kill(session)); + RpcContinuation::WebSocket(TimedResource::new( + Box::new(|ws| WebSocketFuture { + kill, + fut: handler(ws).boxed(), + }), + timeout, + )) } pub fn is_timed_out(&self) -> bool { match self { @@ -97,10 +183,10 @@ impl RpcContinuation { } } -pub struct RpcContinuations(Mutex>); +pub struct RpcContinuations(AsyncMutex>); impl RpcContinuations { pub fn new() -> Self { - RpcContinuations(Mutex::new(BTreeMap::new())) + RpcContinuations(AsyncMutex::new(BTreeMap::new())) } #[instrument(skip_all)] @@ -146,3 +232,28 @@ impl RpcContinuations { x.get().await } } + +pub struct OpenAuthedContinuations(SyncMutex>>); +impl OpenAuthedContinuations +where + T: Eq + Ord, +{ + pub fn new() -> Self { + Self(SyncMutex::new(BTreeMap::new())) + } + pub fn kill(&self, session: &T) { + if let Some(channel) = self.0.lock().unwrap().remove(session) { + channel.send(()).ok(); + } + } + fn subscribe_to_kill(&self, session: T) -> broadcast::Receiver<()> { + let mut map = self.0.lock().unwrap(); + if let Some(send) = map.get(&session) { + send.subscribe() + } else { + let (send, recv) = broadcast::channel(1); + map.insert(session, send); + recv + } + } +} diff --git a/core/startos/src/s9pk/merkle_archive/source/multi_cursor_file.rs b/core/startos/src/s9pk/merkle_archive/source/multi_cursor_file.rs index 92eb40f9d..4b7d5736d 100644 --- a/core/startos/src/s9pk/merkle_archive/source/multi_cursor_file.rs +++ b/core/startos/src/s9pk/merkle_archive/source/multi_cursor_file.rs @@ -97,6 +97,7 @@ impl ArchiveSource for MultiCursorFile { .ok() .map(|m| m.len()) } + #[allow(refining_impl_trait)] async fn fetch_all(&self) -> Result { use tokio::io::AsyncSeekExt; diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index 6588de836..2eecc565e 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -354,7 +354,7 @@ impl Service { .with_kind(ErrorKind::MigrationFailed)?; // TODO: handle cancellation if let Some(mut progress) = progress { progress.finalization_progress.complete(); - progress.progress_handle.complete(); + progress.progress.complete(); tokio::task::yield_now().await; } ctx.db diff --git a/core/startos/src/service/service_map.rs b/core/startos/src/service/service_map.rs index 1474ea35e..f8874d21a 100644 --- a/core/startos/src/service/service_map.rs +++ b/core/startos/src/service/service_map.rs @@ -18,10 +18,7 @@ use crate::disk::mount::guard::GenericMountGuard; use crate::install::PKG_ARCHIVE_DIR; use crate::notifications::{notify, NotificationLevel}; use crate::prelude::*; -use crate::progress::{ - FullProgressTracker, FullProgressTrackerHandle, PhaseProgressTrackerHandle, - ProgressTrackerWriter, -}; +use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle, ProgressTrackerWriter}; use crate::s9pk::manifest::PackageId; use crate::s9pk::merkle_archive::source::FileSource; use crate::s9pk::S9pk; @@ -34,7 +31,7 @@ pub type InstallFuture = BoxFuture<'static, Result<(), Error>>; pub struct InstallProgressHandles { pub finalization_progress: PhaseProgressTrackerHandle, - pub progress_handle: FullProgressTrackerHandle, + pub progress: FullProgressTracker, } /// This is the structure to contain all the services @@ -59,13 +56,22 @@ impl ServiceMap { } #[instrument(skip_all)] - pub async fn init(&self, ctx: &RpcContext) -> Result<(), Error> { - for id in ctx.db.peek().await.as_public().as_package_data().keys()? { + pub async fn init( + &self, + ctx: &RpcContext, + mut progress: PhaseProgressTrackerHandle, + ) -> Result<(), Error> { + progress.start(); + let ids = ctx.db.peek().await.as_public().as_package_data().keys()?; + progress.set_total(ids.len() as u64); + for id in ids { if let Err(e) = self.load(ctx, &id, LoadDisposition::Retry).await { tracing::error!("Error loading installed package as service: {e}"); tracing::debug!("{e:?}"); } + progress += 1; } + progress.complete(); Ok(()) } @@ -112,17 +118,16 @@ impl ServiceMap { }; let size = s9pk.size(); - let mut progress = FullProgressTracker::new(); + let progress = FullProgressTracker::new(); let download_progress_contribution = size.unwrap_or(60); - let progress_handle = progress.handle(); - let mut download_progress = progress_handle.add_phase( + let mut download_progress = progress.add_phase( InternedString::intern("Download"), Some(download_progress_contribution), ); if let Some(size) = size { download_progress.set_total(size); } - let mut finalization_progress = progress_handle.add_phase( + let mut finalization_progress = progress.add_phase( InternedString::intern(op_name), Some(download_progress_contribution / 2), ); @@ -194,7 +199,7 @@ impl ServiceMap { let deref_id = id.clone(); let sync_progress_task = - NonDetachingJoinHandle::from(tokio::spawn(progress.sync_to_db( + NonDetachingJoinHandle::from(tokio::spawn(progress.clone().sync_to_db( ctx.db.clone(), move |v| { v.as_public_mut() @@ -248,7 +253,7 @@ impl ServiceMap { service .uninstall(Some(s9pk.as_manifest().version.clone())) .await?; - progress_handle.complete(); + progress.complete(); Some(version) } else { None @@ -261,7 +266,7 @@ impl ServiceMap { recovery_source, Some(InstallProgressHandles { finalization_progress, - progress_handle, + progress, }), ) .await? @@ -275,7 +280,7 @@ impl ServiceMap { prev, Some(InstallProgressHandles { finalization_progress, - progress_handle, + progress, }), ) .await? diff --git a/core/startos/src/setup.rs b/core/startos/src/setup.rs index a035e932f..2b701c01f 100644 --- a/core/startos/src/setup.rs +++ b/core/startos/src/setup.rs @@ -4,7 +4,6 @@ use std::time::Duration; use color_eyre::eyre::eyre; use josekit::jwk::Jwk; -use openssl::x509::X509; use patch_db::json_ptr::ROOT; use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; @@ -12,15 +11,15 @@ use serde::{Deserialize, Serialize}; use tokio::fs::File; use tokio::io::AsyncWriteExt; use tokio::try_join; -use torut::onion::OnionAddressV3; use tracing::instrument; use ts_rs::TS; use crate::account::AccountInfo; use crate::backup::restore::recover_full_embassy; use crate::backup::target::BackupTargetFS; +use crate::context::rpc::InitRpcContextPhases; use crate::context::setup::SetupResult; -use crate::context::SetupContext; +use crate::context::{RpcContext, SetupContext}; use crate::db::model::Database; use crate::disk::fsck::RepairStrategy; use crate::disk::main::DEFAULT_PASSWORD; @@ -29,10 +28,12 @@ use crate::disk::mount::filesystem::ReadWrite; use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; use crate::disk::util::{pvscan, recovery_info, DiskInfo, EmbassyOsRecoveryInfo}; use crate::disk::REPAIR_DISK_PATH; -use crate::hostname::Hostname; -use crate::init::{init, InitResult}; +use crate::init::{init, InitPhases, InitResult}; +use crate::net::net_controller::PreInitNetController; use crate::net::ssl::root_ca_start_time; use crate::prelude::*; +use crate::progress::{FullProgress, PhaseProgressTrackerHandle}; +use crate::rpc_continuations::Guid; use crate::util::crypto::EncryptedWire; use crate::util::io::{dir_copy, dir_size, Counter}; use crate::{Error, ErrorKind, ResultExt}; @@ -75,10 +76,12 @@ pub async fn list_disks(ctx: SetupContext) -> Result, Error> { async fn setup_init( ctx: &SetupContext, password: Option, -) -> Result<(Hostname, OnionAddressV3, X509), Error> { - let InitResult { db } = init(&ctx.config).await?; + init_phases: InitPhases, +) -> Result<(AccountInfo, PreInitNetController), Error> { + let InitResult { net_ctrl } = init(&ctx.config, init_phases).await?; - let account = db + let account = net_ctrl + .db .mutate(|m| { let mut account = AccountInfo::load(m)?; if let Some(password) = password { @@ -93,15 +96,12 @@ async fn setup_init( }) .await?; - Ok(( - account.hostname, - account.tor_key.public().get_onion_address(), - account.root_ca_cert, - )) + Ok((account, net_ctrl)) } #[derive(Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] +#[ts(export)] pub struct AttachParams { #[serde(rename = "startOsPassword")] password: Option, @@ -110,25 +110,20 @@ pub struct AttachParams { pub async fn attach( ctx: SetupContext, - AttachParams { password, guid }: AttachParams, -) -> Result<(), Error> { - let mut status = ctx.setup_status.write().await; - if status.is_some() { - return Err(Error::new( - eyre!("Setup already in progress"), - ErrorKind::InvalidRequest, - )); - } - *status = Some(Ok(SetupStatus { - bytes_transferred: 0, - total_bytes: None, - complete: false, - })); - drop(status); - tokio::task::spawn(async move { - if let Err(e) = async { + AttachParams { + password, + guid: disk_guid, + }: AttachParams, +) -> Result { + let setup_ctx = ctx.clone(); + ctx.run_setup(|| async move { + let progress = &setup_ctx.progress; + let mut disk_phase = progress.add_phase("Opening data drive".into(), Some(10)); + let init_phases = InitPhases::new(&progress); + let rpc_ctx_phases = InitRpcContextPhases::new(&progress); + let password: Option = match password { - Some(a) => match a.decrypt(&*ctx) { + Some(a) => match a.decrypt(&setup_ctx) { a @ Some(_) => a, None => { return Err(Error::new( @@ -139,15 +134,17 @@ pub async fn attach( }, None => None, }; + + disk_phase.start(); let requires_reboot = crate::disk::main::import( - &*guid, - &ctx.datadir, + &*disk_guid, + &setup_ctx.datadir, if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() { RepairStrategy::Aggressive } else { RepairStrategy::Preen }, - if guid.ends_with("_UNENC") { None } else { Some(DEFAULT_PASSWORD) }, + if disk_guid.ends_with("_UNENC") { None } else { Some(DEFAULT_PASSWORD) }, ) .await?; if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() { @@ -156,7 +153,7 @@ pub async fn attach( .with_ctx(|_| (ErrorKind::Filesystem, REPAIR_DISK_PATH))?; } if requires_reboot.0 { - crate::disk::main::export(&*guid, &ctx.datadir).await?; + crate::disk::main::export(&*disk_guid, &setup_ctx.datadir).await?; return Err(Error::new( eyre!( "Errors were corrected with your disk, but the server must be restarted in order to proceed" @@ -164,37 +161,48 @@ pub async fn attach( ErrorKind::DiskManagement, )); } - let (hostname, tor_addr, root_ca) = setup_init(&ctx, password).await?; - *ctx.setup_result.write().await = Some((guid, SetupResult { - tor_address: format!("https://{}", tor_addr), - lan_address: hostname.lan_address(), - root_ca: String::from_utf8(root_ca.to_pem()?)?, - })); - *ctx.setup_status.write().await = Some(Ok(SetupStatus { - bytes_transferred: 0, - total_bytes: None, - complete: true, - })); - Ok(()) - }.await { - tracing::error!("Error Setting Up Embassy: {}", e); - tracing::debug!("{:?}", e); - *ctx.setup_status.write().await = Some(Err(e.into())); - } - }); - Ok(()) + disk_phase.complete(); + + let (account, net_ctrl) = setup_init(&setup_ctx, password, init_phases).await?; + + let rpc_ctx = RpcContext::init(&setup_ctx.config, disk_guid, Some(net_ctrl), rpc_ctx_phases).await?; + + Ok(((&account).try_into()?, rpc_ctx)) + })?; + + Ok(ctx.progress().await) } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] -pub struct SetupStatus { - pub bytes_transferred: u64, - pub total_bytes: Option, - pub complete: bool, +#[ts(export)] +#[serde(tag = "status")] +pub enum SetupStatusRes { + Complete(SetupResult), + Running(SetupProgress), } -pub async fn status(ctx: SetupContext) -> Result, RpcError> { - ctx.setup_status.read().await.clone().transpose() +#[derive(Debug, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct SetupProgress { + pub progress: FullProgress, + pub guid: Guid, +} + +pub async fn status(ctx: SetupContext) -> Result, Error> { + if let Some(res) = ctx.result.get() { + match res { + Ok((res, _)) => Ok(Some(SetupStatusRes::Complete(res.clone()))), + Err(e) => Err(e.clone_output()), + } + } else { + if ctx.task.initialized() { + Ok(Some(SetupStatusRes::Running(ctx.progress().await))) + } else { + Ok(None) + } + } } /// We want to be able to get a secret, a shared private key with the frontend @@ -202,7 +210,7 @@ pub async fn status(ctx: SetupContext) -> Result, RpcError> /// without knowing the password over clearnet. We use the public key shared across the network /// since it is fine to share the public, and encrypt against the public. pub async fn get_pubkey(ctx: SetupContext) -> Result { - let secret = ctx.as_ref().clone(); + let secret = AsRef::::as_ref(&ctx).clone(); let pub_key = secret.to_public_key()?; Ok(pub_key) } @@ -213,6 +221,7 @@ pub fn cifs() -> ParentHandler { #[derive(Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] +#[ts(export)] pub struct VerifyCifsParams { hostname: String, path: PathBuf, @@ -230,7 +239,7 @@ pub async fn verify_cifs( password, }: VerifyCifsParams, ) -> Result { - let password: Option = password.map(|x| x.decrypt(&*ctx)).flatten(); + let password: Option = password.map(|x| x.decrypt(&ctx)).flatten(); let guard = TmpMountGuard::mount( &Cifs { hostname, @@ -256,7 +265,8 @@ pub enum RecoverySource { #[derive(Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] -pub struct ExecuteParams { +#[ts(export)] +pub struct SetupExecuteParams { start_os_logicalname: PathBuf, start_os_password: EncryptedWire, recovery_source: Option, @@ -266,104 +276,65 @@ pub struct ExecuteParams { // #[command(rpc_only)] pub async fn execute( ctx: SetupContext, - ExecuteParams { + SetupExecuteParams { start_os_logicalname, start_os_password, recovery_source, recovery_password, - }: ExecuteParams, -) -> Result<(), Error> { - let start_os_password = match start_os_password.decrypt(&*ctx) { + }: SetupExecuteParams, +) -> Result { + let start_os_password = match start_os_password.decrypt(&ctx) { Some(a) => a, None => { return Err(Error::new( - color_eyre::eyre::eyre!("Couldn't decode embassy-password"), + color_eyre::eyre::eyre!("Couldn't decode startOsPassword"), crate::ErrorKind::Unknown, )) } }; let recovery_password: Option = match recovery_password { - Some(a) => match a.decrypt(&*ctx) { + Some(a) => match a.decrypt(&ctx) { Some(a) => Some(a), None => { return Err(Error::new( - color_eyre::eyre::eyre!("Couldn't decode recovery-password"), + color_eyre::eyre::eyre!("Couldn't decode recoveryPassword"), crate::ErrorKind::Unknown, )) } }, None => None, }; - let mut status = ctx.setup_status.write().await; - if status.is_some() { - return Err(Error::new( - eyre!("Setup already in progress"), - ErrorKind::InvalidRequest, - )); - } - *status = Some(Ok(SetupStatus { - bytes_transferred: 0, - total_bytes: None, - complete: false, - })); - drop(status); - tokio::task::spawn({ - async move { - let ctx = ctx.clone(); - match execute_inner( - ctx.clone(), - start_os_logicalname, - start_os_password, - recovery_source, - recovery_password, - ) - .await - { - Ok((guid, hostname, tor_addr, root_ca)) => { - tracing::info!("Setup Complete!"); - *ctx.setup_result.write().await = Some(( - guid, - SetupResult { - tor_address: format!("https://{}", tor_addr), - lan_address: hostname.lan_address(), - root_ca: String::from_utf8( - root_ca.to_pem().expect("failed to serialize root ca"), - ) - .expect("invalid pem string"), - }, - )); - *ctx.setup_status.write().await = Some(Ok(SetupStatus { - bytes_transferred: 0, - total_bytes: None, - complete: true, - })); - } - Err(e) => { - tracing::error!("Error Setting Up Server: {}", e); - tracing::debug!("{:?}", e); - *ctx.setup_status.write().await = Some(Err(e.into())); - } - } - } - }); - Ok(()) + + let setup_ctx = ctx.clone(); + ctx.run_setup(|| { + execute_inner( + setup_ctx, + start_os_logicalname, + start_os_password, + recovery_source, + recovery_password, + ) + })?; + + Ok(ctx.progress().await) } #[instrument(skip_all)] // #[command(rpc_only)] pub async fn complete(ctx: SetupContext) -> Result { - let (guid, setup_result) = if let Some((guid, setup_result)) = &*ctx.setup_result.read().await { - (guid.clone(), setup_result.clone()) - } else { - return Err(Error::new( + match ctx.result.get() { + Some(Ok((res, ctx))) => { + let mut guid_file = File::create("/media/startos/config/disk.guid").await?; + guid_file.write_all(ctx.disk_guid.as_bytes()).await?; + guid_file.sync_all().await?; + Ok(res.clone()) + } + Some(Err(e)) => Err(e.clone_output()), + None => Err(Error::new( eyre!("setup.execute has not completed successfully"), crate::ErrorKind::InvalidRequest, - )); - }; - let mut guid_file = File::create("/media/startos/config/disk.guid").await?; - guid_file.write_all(guid.as_bytes()).await?; - guid_file.sync_all().await?; - Ok(setup_result) + )), + } } #[instrument(skip_all)] @@ -380,7 +351,22 @@ pub async fn execute_inner( start_os_password: String, recovery_source: Option, recovery_password: Option, -) -> Result<(Arc, Hostname, OnionAddressV3, X509), Error> { +) -> Result<(SetupResult, RpcContext), Error> { + let progress = &ctx.progress; + let mut disk_phase = progress.add_phase("Formatting data drive".into(), Some(10)); + let restore_phase = match &recovery_source { + Some(RecoverySource::Backup { .. }) => { + Some(progress.add_phase("Restoring backup".into(), Some(100))) + } + Some(RecoverySource::Migrate { .. }) => { + Some(progress.add_phase("Transferring data".into(), Some(100))) + } + None => None, + }; + let init_phases = InitPhases::new(&progress); + let rpc_ctx_phases = InitRpcContextPhases::new(&progress); + + disk_phase.start(); let encryption_password = if ctx.disable_encryption { None } else { @@ -402,41 +388,70 @@ pub async fn execute_inner( encryption_password, ) .await?; + disk_phase.complete(); - if let Some(RecoverySource::Backup { target }) = recovery_source { - recover(ctx, guid, start_os_password, target, recovery_password).await - } else if let Some(RecoverySource::Migrate { guid: old_guid }) = recovery_source { - migrate(ctx, guid, &old_guid, start_os_password).await - } else { - let (hostname, tor_addr, root_ca) = fresh_setup(&ctx, &start_os_password).await?; - Ok((guid, hostname, tor_addr, root_ca)) + let progress = SetupExecuteProgress { + init_phases, + restore_phase, + rpc_ctx_phases, + }; + + match recovery_source { + Some(RecoverySource::Backup { target }) => { + recover( + &ctx, + guid, + start_os_password, + target, + recovery_password, + progress, + ) + .await + } + Some(RecoverySource::Migrate { guid: old_guid }) => { + migrate(&ctx, guid, &old_guid, start_os_password, progress).await + } + None => fresh_setup(&ctx, guid, &start_os_password, progress).await, } } +pub struct SetupExecuteProgress { + pub init_phases: InitPhases, + pub restore_phase: Option, + pub rpc_ctx_phases: InitRpcContextPhases, +} + async fn fresh_setup( ctx: &SetupContext, + guid: Arc, start_os_password: &str, -) -> Result<(Hostname, OnionAddressV3, X509), Error> { + SetupExecuteProgress { + init_phases, + rpc_ctx_phases, + .. + }: SetupExecuteProgress, +) -> Result<(SetupResult, RpcContext), Error> { let account = AccountInfo::new(start_os_password, root_ca_start_time().await?)?; let db = ctx.db().await?; db.put(&ROOT, &Database::init(&account)?).await?; drop(db); - init(&ctx.config).await?; - Ok(( - account.hostname, - account.tor_key.public().get_onion_address(), - account.root_ca_cert, - )) + + let InitResult { net_ctrl } = init(&ctx.config, init_phases).await?; + + let rpc_ctx = RpcContext::init(&ctx.config, guid, Some(net_ctrl), rpc_ctx_phases).await?; + + Ok(((&account).try_into()?, rpc_ctx)) } #[instrument(skip_all)] async fn recover( - ctx: SetupContext, + ctx: &SetupContext, guid: Arc, start_os_password: String, recovery_source: BackupTargetFS, recovery_password: Option, -) -> Result<(Arc, Hostname, OnionAddressV3, X509), Error> { + progress: SetupExecuteProgress, +) -> Result<(SetupResult, RpcContext), Error> { let recovery_source = TmpMountGuard::mount(&recovery_source, ReadWrite).await?; recover_full_embassy( ctx, @@ -444,23 +459,26 @@ async fn recover( start_os_password, recovery_source, recovery_password, + progress, ) .await } #[instrument(skip_all)] async fn migrate( - ctx: SetupContext, + ctx: &SetupContext, guid: Arc, old_guid: &str, start_os_password: String, -) -> Result<(Arc, Hostname, OnionAddressV3, X509), Error> { - *ctx.setup_status.write().await = Some(Ok(SetupStatus { - bytes_transferred: 0, - total_bytes: None, - complete: false, - })); + SetupExecuteProgress { + init_phases, + restore_phase, + rpc_ctx_phases, + }: SetupExecuteProgress, +) -> Result<(SetupResult, RpcContext), Error> { + let mut restore_phase = restore_phase.or_not_found("restore progress")?; + restore_phase.start(); let _ = crate::disk::main::import( &old_guid, "/media/startos/migrate", @@ -500,20 +518,12 @@ async fn migrate( res = async { loop { tokio::time::sleep(Duration::from_secs(1)).await; - *ctx.setup_status.write().await = Some(Ok(SetupStatus { - bytes_transferred: 0, - total_bytes: Some(main_transfer_size.load() + package_data_transfer_size.load()), - complete: false, - })); + restore_phase.set_total(main_transfer_size.load() + package_data_transfer_size.load()); } } => res, }; - *ctx.setup_status.write().await = Some(Ok(SetupStatus { - bytes_transferred: 0, - total_bytes: Some(size), - complete: false, - })); + restore_phase.set_total(size); let main_transfer_progress = Counter::new(0, ordering); let package_data_transfer_progress = Counter::new(0, ordering); @@ -529,18 +539,17 @@ async fn migrate( res = async { loop { tokio::time::sleep(Duration::from_secs(1)).await; - *ctx.setup_status.write().await = Some(Ok(SetupStatus { - bytes_transferred: main_transfer_progress.load() + package_data_transfer_progress.load(), - total_bytes: Some(size), - complete: false, - })); + restore_phase.set_done(main_transfer_progress.load() + package_data_transfer_progress.load()); } } => res, } - let (hostname, tor_addr, root_ca) = setup_init(&ctx, Some(start_os_password)).await?; - crate::disk::main::export(&old_guid, "/media/startos/migrate").await?; + restore_phase.complete(); - Ok((guid, hostname, tor_addr, root_ca)) + let (account, net_ctrl) = setup_init(&ctx, Some(start_os_password), init_phases).await?; + + let rpc_ctx = RpcContext::init(&ctx.config, guid, Some(net_ctrl), rpc_ctx_phases).await?; + + Ok(((&account).try_into()?, rpc_ctx)) } diff --git a/core/startos/src/update/mod.rs b/core/startos/src/update/mod.rs index be908e776..dc164d2cd 100644 --- a/core/startos/src/update/mod.rs +++ b/core/startos/src/update/mod.rs @@ -20,9 +20,7 @@ use ts_rs::TS; use crate::context::{CliContext, RpcContext}; use crate::notifications::{notify, NotificationLevel}; use crate::prelude::*; -use crate::progress::{ - FullProgressTracker, FullProgressTrackerHandle, PhaseProgressTrackerHandle, PhasedProgressBar, -}; +use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle, PhasedProgressBar}; use crate::registry::asset::RegistryAsset; use crate::registry::context::{RegistryContext, RegistryUrlParams}; use crate::registry::os::index::OsVersionInfo; @@ -34,6 +32,7 @@ use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::sound::{ CIRCLE_OF_5THS_SHORT, UPDATE_FAILED_1, UPDATE_FAILED_2, UPDATE_FAILED_3, UPDATE_FAILED_4, }; +use crate::util::net::WebSocketExt; use crate::util::Invoke; use crate::PLATFORM; @@ -91,50 +90,47 @@ pub async fn update_system( .add( guid.clone(), RpcContinuation::ws( - Box::new(|mut ws| { - async move { - if let Err(e) = async { - let mut sub = ctx + |mut ws| async move { + if let Err(e) = async { + let mut sub = ctx + .db + .subscribe( + "/public/serverInfo/statusInfo/updateProgress" + .parse::() + .with_kind(ErrorKind::Database)?, + ) + .await; + while { + let progress = ctx .db - .subscribe( - "/public/serverInfo/statusInfo/updateProgress" - .parse::() - .with_kind(ErrorKind::Database)?, - ) - .await; - while { - let progress = ctx - .db - .peek() - .await - .into_public() - .into_server_info() - .into_status_info() - .into_update_progress() - .de()?; - ws.send(axum::extract::ws::Message::Text( - serde_json::to_string(&progress) - .with_kind(ErrorKind::Serialization)?, - )) + .peek() .await - .with_kind(ErrorKind::Network)?; - progress.is_some() - } { - sub.recv().await; - } - - ws.close().await.with_kind(ErrorKind::Network)?; - - Ok::<_, Error>(()) - } - .await - { - tracing::error!("Error returning progress of update: {e}"); - tracing::debug!("{e:?}") + .into_public() + .into_server_info() + .into_status_info() + .into_update_progress() + .de()?; + ws.send(axum::extract::ws::Message::Text( + serde_json::to_string(&progress) + .with_kind(ErrorKind::Serialization)?, + )) + .await + .with_kind(ErrorKind::Network)?; + progress.is_some() + } { + sub.recv().await; } + + ws.normal_close("complete").await?; + + Ok::<_, Error>(()) } - .boxed() - }), + .await + { + tracing::error!("Error returning progress of update: {e}"); + tracing::debug!("{e:?}") + } + }, Duration::from_secs(30), ), ) @@ -250,13 +246,12 @@ async fn maybe_do_update( asset.validate(SIG_CONTEXT, asset.all_signers())?; - let mut progress = FullProgressTracker::new(); - let progress_handle = progress.handle(); - let mut download_phase = progress_handle.add_phase("Downloading File".into(), Some(100)); + let progress = FullProgressTracker::new(); + let mut download_phase = progress.add_phase("Downloading File".into(), Some(100)); download_phase.set_total(asset.commitment.size); - let reverify_phase = progress_handle.add_phase("Reverifying File".into(), Some(10)); - let sync_boot_phase = progress_handle.add_phase("Syncing Boot Files".into(), Some(1)); - let finalize_phase = progress_handle.add_phase("Finalizing Update".into(), Some(1)); + let reverify_phase = progress.add_phase("Reverifying File".into(), Some(10)); + let sync_boot_phase = progress.add_phase("Syncing Boot Files".into(), Some(1)); + let finalize_phase = progress.add_phase("Finalizing Update".into(), Some(1)); let start_progress = progress.snapshot(); @@ -287,7 +282,7 @@ async fn maybe_do_update( )); } - let progress_task = NonDetachingJoinHandle::from(tokio::spawn(progress.sync_to_db( + let progress_task = NonDetachingJoinHandle::from(tokio::spawn(progress.clone().sync_to_db( ctx.db.clone(), |db| { db.as_public_mut() @@ -304,7 +299,7 @@ async fn maybe_do_update( ctx.clone(), asset, UpdateProgressHandles { - progress_handle, + progress, download_phase, reverify_phase, sync_boot_phase, @@ -373,7 +368,7 @@ async fn maybe_do_update( } struct UpdateProgressHandles { - progress_handle: FullProgressTrackerHandle, + progress: FullProgressTracker, download_phase: PhaseProgressTrackerHandle, reverify_phase: PhaseProgressTrackerHandle, sync_boot_phase: PhaseProgressTrackerHandle, @@ -385,7 +380,7 @@ async fn do_update( ctx: RpcContext, asset: RegistryAsset, UpdateProgressHandles { - progress_handle, + progress, mut download_phase, mut reverify_phase, mut sync_boot_phase, @@ -436,7 +431,7 @@ async fn do_update( .await?; finalize_phase.complete(); - progress_handle.complete(); + progress.complete(); Ok(()) } diff --git a/core/startos/src/upload.rs b/core/startos/src/upload.rs index f28d81799..b922ab9d2 100644 --- a/core/startos/src/upload.rs +++ b/core/startos/src/upload.rs @@ -5,9 +5,10 @@ use std::time::Duration; use axum::body::Body; use axum::response::Response; -use futures::{FutureExt, StreamExt}; +use futures::StreamExt; use http::header::CONTENT_LENGTH; use http::StatusCode; +use imbl_value::InternedString; use tokio::fs::File; use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; use tokio::sync::watch; @@ -19,68 +20,70 @@ use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::ArchiveSource; use crate::util::io::TmpDir; -pub async fn upload(ctx: &RpcContext) -> Result<(Guid, UploadingFile), Error> { +pub async fn upload( + ctx: &RpcContext, + session: InternedString, +) -> Result<(Guid, UploadingFile), Error> { let guid = Guid::new(); let (mut handle, file) = UploadingFile::new().await?; ctx.rpc_continuations .add( guid.clone(), - RpcContinuation::rest( - Box::new(|request| { - async move { - let headers = request.headers(); - let content_length = match headers.get(CONTENT_LENGTH).map(|a| a.to_str()) { - None => { - return Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Body::from("Content-Length is required")) - .with_kind(ErrorKind::Network) - } - Some(Err(_)) => { + RpcContinuation::rest_authed( + ctx, + session, + |request| async move { + let headers = request.headers(); + let content_length = match headers.get(CONTENT_LENGTH).map(|a| a.to_str()) { + None => { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from("Content-Length is required")) + .with_kind(ErrorKind::Network) + } + Some(Err(_)) => { + return Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from("Invalid Content-Length")) + .with_kind(ErrorKind::Network) + } + Some(Ok(a)) => match a.parse::() { + Err(_) => { return Response::builder() .status(StatusCode::BAD_REQUEST) .body(Body::from("Invalid Content-Length")) .with_kind(ErrorKind::Network) } - Some(Ok(a)) => match a.parse::() { - Err(_) => { - return Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Body::from("Invalid Content-Length")) - .with_kind(ErrorKind::Network) - } - Ok(a) => a, - }, - }; + Ok(a) => a, + }, + }; - handle - .progress - .send_modify(|p| p.expected_size = Some(content_length)); + handle + .progress + .send_modify(|p| p.expected_size = Some(content_length)); - let mut body = request.into_body().into_data_stream(); - while let Some(next) = body.next().await { - if let Err(e) = async { - handle - .write_all(&next.map_err(|e| { - std::io::Error::new(std::io::ErrorKind::Other, e) - })?) - .await?; - Ok(()) - } - .await - { - handle.progress.send_if_modified(|p| p.handle_error(&e)); - break; - } + let mut body = request.into_body().into_data_stream(); + while let Some(next) = body.next().await { + if let Err(e) = async { + handle + .write_all(&next.map_err(|e| { + std::io::Error::new(std::io::ErrorKind::Other, e) + })?) + .await?; + Ok(()) + } + .await + { + handle.progress.send_if_modified(|p| p.handle_error(&e)); + break; } - - Response::builder() - .status(StatusCode::NO_CONTENT) - .body(Body::empty()) - .with_kind(ErrorKind::Network) } - .boxed() - }), + + Response::builder() + .status(StatusCode::NO_CONTENT) + .body(Body::empty()) + .with_kind(ErrorKind::Network) + }, Duration::from_secs(30), ), ) diff --git a/core/startos/src/util/io.rs b/core/startos/src/util/io.rs index f4476ee2b..9a6bab64b 100644 --- a/core/startos/src/util/io.rs +++ b/core/startos/src/util/io.rs @@ -274,6 +274,81 @@ pub fn response_to_reader(response: reqwest::Response) -> impl AsyncRead + Unpin })) } +#[pin_project::pin_project] +pub struct IOHook<'a, T> { + #[pin] + pub io: T, + pre_write: Option Result<(), std::io::Error> + Send + 'a>>, + post_write: Option>, + post_read: Option>, +} +impl<'a, T> IOHook<'a, T> { + pub fn new(io: T) -> Self { + Self { + io, + pre_write: None, + post_write: None, + post_read: None, + } + } + pub fn into_inner(self) -> T { + self.io + } + pub fn pre_write Result<(), std::io::Error> + Send + 'a>(&mut self, f: F) { + self.pre_write = Some(Box::new(f)) + } + pub fn post_write(&mut self, f: F) { + self.post_write = Some(Box::new(f)) + } + pub fn post_read(&mut self, f: F) { + self.post_read = Some(Box::new(f)) + } +} +impl<'a, T: AsyncWrite> AsyncWrite for IOHook<'a, T> { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> Poll> { + let this = self.project(); + if let Some(pre_write) = this.pre_write { + pre_write(buf)?; + } + let written = futures::ready!(this.io.poll_write(cx, buf)?); + if let Some(post_write) = this.post_write { + post_write(&buf[..written]); + } + Poll::Ready(Ok(written)) + } + fn poll_flush( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + self.project().io.poll_flush(cx) + } + fn poll_shutdown( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + self.project().io.poll_shutdown(cx) + } +} +impl<'a, T: AsyncRead> AsyncRead for IOHook<'a, T> { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let this = self.project(); + let start = buf.filled().len(); + futures::ready!(this.io.poll_read(cx, buf)?); + if let Some(post_read) = this.post_read { + post_read(&buf.filled()[start..]); + } + Poll::Ready(Ok(())) + } +} + #[pin_project::pin_project] pub struct BufferedWriteReader { #[pin] @@ -768,7 +843,7 @@ fn poll_flush_prefix( flush_writer: bool, ) -> Poll> { while let Some(mut cur) = prefix.pop_front() { - let buf = cur.remaining_slice(); + let buf = CursorExt::remaining_slice(&cur); if !buf.is_empty() { match writer.as_mut().poll_write(cx, buf)? { Poll::Ready(n) if n == buf.len() => (), diff --git a/core/startos/src/util/mod.rs b/core/startos/src/util/mod.rs index 4346a0b1e..f2334632e 100644 --- a/core/startos/src/util/mod.rs +++ b/core/startos/src/util/mod.rs @@ -36,6 +36,7 @@ pub mod http_reader; pub mod io; pub mod logger; pub mod lshw; +pub mod net; pub mod rpc; pub mod rpc_client; pub mod serde; diff --git a/core/startos/src/util/net.rs b/core/startos/src/util/net.rs new file mode 100644 index 000000000..93131f16e --- /dev/null +++ b/core/startos/src/util/net.rs @@ -0,0 +1,24 @@ +use std::borrow::Cow; + +use axum::extract::ws::{self, CloseFrame}; +use futures::Future; + +use crate::prelude::*; + +pub trait WebSocketExt { + fn normal_close( + self, + msg: impl Into>, + ) -> impl Future>; +} + +impl WebSocketExt for ws::WebSocket { + async fn normal_close(mut self, msg: impl Into>) -> Result<(), Error> { + self.send(ws::Message::Close(Some(CloseFrame { + code: 1000, + reason: msg.into(), + }))) + .await + .with_kind(ErrorKind::Network) + } +} diff --git a/core/startos/src/util/serde.rs b/core/startos/src/util/serde.rs index 382ef2814..5d1289e7e 100644 --- a/core/startos/src/util/serde.rs +++ b/core/startos/src/util/serde.rs @@ -22,8 +22,8 @@ use ts_rs::TS; use super::IntoDoubleEndedIterator; use crate::prelude::*; -use crate::util::Apply; use crate::util::clap::FromStrParser; +use crate::util::Apply; pub fn deserialize_from_str< 'de, @@ -1040,15 +1040,19 @@ impl> std::fmt::Display for Base64 { f.write_str(&base64::encode(self.0.as_ref())) } } -impl>> FromStr for Base64 -{ +impl>> FromStr for Base64 { type Err = Error; fn from_str(s: &str) -> Result { base64::decode(&s) .with_kind(ErrorKind::Deserialization)? .apply(TryFrom::try_from) .map(Self) - .map_err(|_| Error::new(eyre!("failed to create from buffer"), ErrorKind::Deserialization)) + .map_err(|_| { + Error::new( + eyre!("failed to create from buffer"), + ErrorKind::Deserialization, + ) + }) } } impl<'de, T: TryFrom>> Deserialize<'de> for Base64 { diff --git a/core/startos/src/version/mod.rs b/core/startos/src/version/mod.rs index 7212b8801..d063558e2 100644 --- a/core/startos/src/version/mod.rs +++ b/core/startos/src/version/mod.rs @@ -7,6 +7,7 @@ use imbl_value::InternedString; use crate::db::model::Database; use crate::prelude::*; +use crate::progress::PhaseProgressTrackerHandle; use crate::Error; mod v0_3_5; @@ -85,11 +86,12 @@ where &self, version: &V, db: &TypedPatchDb, + progress: &mut PhaseProgressTrackerHandle, ) -> impl Future> + Send { async { match self.semver().cmp(&version.semver()) { - Ordering::Greater => self.rollback_to_unchecked(version, db).await, - Ordering::Less => version.migrate_from_unchecked(self, db).await, + Ordering::Greater => self.rollback_to_unchecked(version, db, progress).await, + Ordering::Less => version.migrate_from_unchecked(self, db, progress).await, Ordering::Equal => Ok(()), } } @@ -98,11 +100,15 @@ where &'a self, version: &'a V, db: &'a TypedPatchDb, + progress: &'a mut PhaseProgressTrackerHandle, ) -> BoxFuture<'a, Result<(), Error>> { + progress.add_total(1); async { let previous = Self::Previous::new(); if version.semver() < previous.semver() { - previous.migrate_from_unchecked(version, db).await?; + previous + .migrate_from_unchecked(version, db, progress) + .await?; } else if version.semver() > previous.semver() { return Err(Error::new( eyre!( @@ -115,6 +121,7 @@ where tracing::info!("{} -> {}", previous.semver(), self.semver(),); self.up(db).await?; self.commit(db).await?; + *progress += 1; Ok(()) } .boxed() @@ -123,14 +130,18 @@ where &'a self, version: &'a V, db: &'a TypedPatchDb, + progress: &'a mut PhaseProgressTrackerHandle, ) -> BoxFuture<'a, Result<(), Error>> { async { let previous = Self::Previous::new(); tracing::info!("{} -> {}", self.semver(), previous.semver(),); self.down(db).await?; previous.commit(db).await?; + *progress += 1; if version.semver() < previous.semver() { - previous.rollback_to_unchecked(version, db).await?; + previous + .rollback_to_unchecked(version, db, progress) + .await?; } else if version.semver() > previous.semver() { return Err(Error::new( eyre!( @@ -196,7 +207,11 @@ where } } -pub async fn init(db: &TypedPatchDb) -> Result<(), Error> { +pub async fn init( + db: &TypedPatchDb, + mut progress: PhaseProgressTrackerHandle, +) -> Result<(), Error> { + progress.start(); let version = Version::from_util_version( db.peek() .await @@ -213,10 +228,10 @@ pub async fn init(db: &TypedPatchDb) -> Result<(), Error> { ErrorKind::MigrationFailed, )); } - Version::V0_3_5(v) => v.0.migrate_to(&Current::new(), &db).await?, - Version::V0_3_5_1(v) => v.0.migrate_to(&Current::new(), &db).await?, - Version::V0_3_5_2(v) => v.0.migrate_to(&Current::new(), &db).await?, - Version::V0_3_6(v) => v.0.migrate_to(&Current::new(), &db).await?, + Version::V0_3_5(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, + Version::V0_3_5_1(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, + Version::V0_3_5_2(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, + Version::V0_3_6(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, Version::Other(_) => { return Err(Error::new( eyre!("Cannot downgrade"), @@ -224,6 +239,7 @@ pub async fn init(db: &TypedPatchDb) -> Result<(), Error> { )) } } + progress.complete(); Ok(()) } diff --git a/core/startos/src/volume.rs b/core/startos/src/volume.rs index e801da79b..c7165b202 100644 --- a/core/startos/src/volume.rs +++ b/core/startos/src/volume.rs @@ -20,7 +20,11 @@ pub fn data_dir>(datadir: P, pkg_id: &PackageId, volume_id: &Volu .join(volume_id) } -pub fn asset_dir>(datadir: P, pkg_id: &PackageId, version: &VersionString) -> PathBuf { +pub fn asset_dir>( + datadir: P, + pkg_id: &PackageId, + version: &VersionString, +) -> PathBuf { datadir .as_ref() .join(PKG_VOLUME_DIR) diff --git a/patch-db b/patch-db index 88a804f56..7aa53249f 160000 --- a/patch-db +++ b/patch-db @@ -1 +1 @@ -Subproject commit 88a804f56f446d34896ef331d915b821a581cf01 +Subproject commit 7aa53249f9353162475ea347abac92abcfba5493 diff --git a/sdk/lib/osBindings/AttachParams.ts b/sdk/lib/osBindings/AttachParams.ts new file mode 100644 index 000000000..048151d2f --- /dev/null +++ b/sdk/lib/osBindings/AttachParams.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { EncryptedWire } from "./EncryptedWire" + +export type AttachParams = { + startOsPassword: EncryptedWire | null + guid: string +} diff --git a/sdk/lib/osBindings/BackupTargetFS.ts b/sdk/lib/osBindings/BackupTargetFS.ts new file mode 100644 index 000000000..0cff2cc4e --- /dev/null +++ b/sdk/lib/osBindings/BackupTargetFS.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { BlockDev } from "./BlockDev" +import type { Cifs } from "./Cifs" + +export type BackupTargetFS = + | ({ type: "disk" } & BlockDev) + | ({ type: "cifs" } & Cifs) diff --git a/sdk/lib/osBindings/BlockDev.ts b/sdk/lib/osBindings/BlockDev.ts new file mode 100644 index 000000000..46db81011 --- /dev/null +++ b/sdk/lib/osBindings/BlockDev.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type BlockDev = { logicalname: string } diff --git a/sdk/lib/osBindings/Cifs.ts b/sdk/lib/osBindings/Cifs.ts new file mode 100644 index 000000000..f7099bd7f --- /dev/null +++ b/sdk/lib/osBindings/Cifs.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type Cifs = { + hostname: string + path: string + username: string + password: string | null +} diff --git a/sdk/lib/osBindings/InitProgressRes.ts b/sdk/lib/osBindings/InitProgressRes.ts new file mode 100644 index 000000000..38caf7bdb --- /dev/null +++ b/sdk/lib/osBindings/InitProgressRes.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FullProgress } from "./FullProgress" +import type { Guid } from "./Guid" + +export type InitProgressRes = { progress: FullProgress; guid: Guid } diff --git a/sdk/lib/osBindings/RecoverySource.ts b/sdk/lib/osBindings/RecoverySource.ts new file mode 100644 index 000000000..c40ec5132 --- /dev/null +++ b/sdk/lib/osBindings/RecoverySource.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { BackupTargetFS } from "./BackupTargetFS" + +export type RecoverySource = + | { type: "migrate"; guid: string } + | { type: "backup"; target: BackupTargetFS } diff --git a/sdk/lib/osBindings/SetupExecuteParams.ts b/sdk/lib/osBindings/SetupExecuteParams.ts new file mode 100644 index 000000000..4593e7667 --- /dev/null +++ b/sdk/lib/osBindings/SetupExecuteParams.ts @@ -0,0 +1,10 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { EncryptedWire } from "./EncryptedWire" +import type { RecoverySource } from "./RecoverySource" + +export type SetupExecuteParams = { + startOsLogicalname: string + startOsPassword: EncryptedWire + recoverySource: RecoverySource | null + recoveryPassword: EncryptedWire | null +} diff --git a/sdk/lib/osBindings/SetupProgress.ts b/sdk/lib/osBindings/SetupProgress.ts new file mode 100644 index 000000000..845636da3 --- /dev/null +++ b/sdk/lib/osBindings/SetupProgress.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { FullProgress } from "./FullProgress" +import type { Guid } from "./Guid" + +export type SetupProgress = { progress: FullProgress; guid: Guid } diff --git a/sdk/lib/osBindings/SetupResult.ts b/sdk/lib/osBindings/SetupResult.ts new file mode 100644 index 000000000..464aeb4b7 --- /dev/null +++ b/sdk/lib/osBindings/SetupResult.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SetupResult = { + torAddress: string + lanAddress: string + rootCa: string +} diff --git a/sdk/lib/osBindings/SetupStatusRes.ts b/sdk/lib/osBindings/SetupStatusRes.ts new file mode 100644 index 000000000..93d10c59b --- /dev/null +++ b/sdk/lib/osBindings/SetupStatusRes.ts @@ -0,0 +1,7 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { SetupProgress } from "./SetupProgress" +import type { SetupResult } from "./SetupResult" + +export type SetupStatusRes = + | ({ status: "complete" } & SetupResult) + | ({ status: "running" } & SetupProgress) diff --git a/sdk/lib/osBindings/VerifyCifsParams.ts b/sdk/lib/osBindings/VerifyCifsParams.ts new file mode 100644 index 000000000..407e6caaa --- /dev/null +++ b/sdk/lib/osBindings/VerifyCifsParams.ts @@ -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 { EncryptedWire } from "./EncryptedWire" + +export type VerifyCifsParams = { + hostname: string + path: string + username: string + password: EncryptedWire | null +} diff --git a/sdk/lib/osBindings/index.ts b/sdk/lib/osBindings/index.ts index 06a4bed7e..ac2e19e45 100644 --- a/sdk/lib/osBindings/index.ts +++ b/sdk/lib/osBindings/index.ts @@ -15,17 +15,21 @@ export { AlpnInfo } from "./AlpnInfo" export { AnySignature } from "./AnySignature" export { AnySigningKey } from "./AnySigningKey" export { AnyVerifyingKey } from "./AnyVerifyingKey" +export { AttachParams } from "./AttachParams" export { BackupProgress } from "./BackupProgress" +export { BackupTargetFS } from "./BackupTargetFS" export { Base64 } from "./Base64" export { BindInfo } from "./BindInfo" export { BindOptions } from "./BindOptions" export { BindParams } from "./BindParams" export { Blake3Commitment } from "./Blake3Commitment" +export { BlockDev } from "./BlockDev" export { Callback } from "./Callback" export { Category } from "./Category" export { CheckDependenciesParam } from "./CheckDependenciesParam" export { CheckDependenciesResult } from "./CheckDependenciesResult" export { ChrootParams } from "./ChrootParams" +export { Cifs } from "./Cifs" export { ContactInfo } from "./ContactInfo" export { CreateOverlayedImageParams } from "./CreateOverlayedImageParams" export { CurrentDependencies } from "./CurrentDependencies" @@ -73,6 +77,7 @@ export { ImageConfig } from "./ImageConfig" export { ImageId } from "./ImageId" export { ImageMetadata } from "./ImageMetadata" export { ImageSource } from "./ImageSource" +export { InitProgressRes } from "./InitProgressRes" export { InstalledState } from "./InstalledState" export { InstallingInfo } from "./InstallingInfo" export { InstallingState } from "./InstallingState" @@ -105,6 +110,7 @@ export { ParamsPackageId } from "./ParamsPackageId" export { PasswordType } from "./PasswordType" export { Progress } from "./Progress" export { Public } from "./Public" +export { RecoverySource } from "./RecoverySource" export { RegistryAsset } from "./RegistryAsset" export { RemoveActionParams } from "./RemoveActionParams" export { RemoveAddressParams } from "./RemoveAddressParams" @@ -127,10 +133,15 @@ export { SetMainStatusStatus } from "./SetMainStatusStatus" export { SetMainStatus } from "./SetMainStatus" export { SetStoreParams } from "./SetStoreParams" export { SetSystemSmtpParams } from "./SetSystemSmtpParams" +export { SetupExecuteParams } from "./SetupExecuteParams" +export { SetupProgress } from "./SetupProgress" +export { SetupResult } from "./SetupResult" +export { SetupStatusRes } from "./SetupStatusRes" export { SignAssetParams } from "./SignAssetParams" export { SignerInfo } from "./SignerInfo" export { Status } from "./Status" export { UpdatingState } from "./UpdatingState" +export { VerifyCifsParams } from "./VerifyCifsParams" export { VersionSignerParams } from "./VersionSignerParams" export { Version } from "./Version" export { VolumeId } from "./VolumeId" diff --git a/web/package-lock.json b/web/package-lock.json index 68156eb21..81b612ac1 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -31,6 +31,7 @@ "@taiga-ui/core": "3.20.0", "@taiga-ui/icons": "3.20.0", "@taiga-ui/kit": "3.20.0", + "@tinkoff/ng-dompurify": "4.0.0", "angular-svg-round-progressbar": "^9.0.0", "ansi-to-html": "^0.7.2", "base64-js": "^1.5.1", @@ -1973,7 +1974,7 @@ }, "../sdk/dist": { "name": "@start9labs/start-sdk", - "version": "0.4.0-rev0.lib0.rc8.beta10", + "version": "0.3.6-alpha1", "license": "MIT", "dependencies": { "isomorphic-fetch": "^3.0.0", @@ -5432,6 +5433,20 @@ "rxjs": ">=6.0.0" } }, + "node_modules/@tinkoff/ng-dompurify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@tinkoff/ng-dompurify/-/ng-dompurify-4.0.0.tgz", + "integrity": "sha512-BjKUweWLrOx8UOZw+Tl+Dae5keYuSbeMkppcXQdsvwASMrPfmP7d3Q206Q6HDqOV2WnpnFqGUB95IMbLAeRRuw==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": ">=12.0.0", + "@angular/platform-browser": ">=12.0.0", + "@types/dompurify": ">=2.3.0", + "dompurify": ">= 2.3.0" + } + }, "node_modules/@tinkoff/ng-event-plugins": { "version": "3.1.0", "license": "Apache-2.0", @@ -5549,7 +5564,6 @@ }, "node_modules/@types/dompurify": { "version": "2.3.4", - "dev": true, "license": "MIT", "dependencies": { "@types/trusted-types": "*" @@ -5726,7 +5740,6 @@ }, "node_modules/@types/trusted-types": { "version": "2.0.2", - "dev": true, "license": "MIT" }, "node_modules/@types/uuid": { diff --git a/web/package.json b/web/package.json index 9e842455e..3ea29c6fd 100644 --- a/web/package.json +++ b/web/package.json @@ -13,7 +13,6 @@ "check:setup": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck", "check:ui": "tsc --project projects/ui/tsconfig.json --noEmit --skipLibCheck", "build:deps": "rm -rf .angular/cache && (cd ../patch-db/client && npm ci && npm run build) && (cd ../sdk && make bundle)", - "build:dui": "ng run diagnostic-ui:build", "build:install-wiz": "ng run install-wizard:build", "build:setup": "ng run setup-wizard:build", "build:ui": "ng run ui:build", @@ -25,7 +24,6 @@ "analyze:ui": "webpack-bundle-analyzer dist/raw/ui/stats.json", "publish:shared": "npm run build:shared && npm publish ./dist/shared --access public", "publish:marketplace": "npm run build:marketplace && npm publish ./dist/marketplace --access public", - "start:dui": "npm run-script build-config && ionic serve --project diagnostic-ui --host 0.0.0.0", "start:install-wiz": "npm run-script build-config && ionic serve --project install-wizard --host 0.0.0.0", "start:setup": "npm run-script build-config && ionic serve --project setup-wizard --host 0.0.0.0", "start:ui": "npm run-script build-config && ionic serve --project ui --ip --host 0.0.0.0", @@ -56,6 +54,7 @@ "@taiga-ui/core": "3.20.0", "@taiga-ui/icons": "3.20.0", "@taiga-ui/kit": "3.20.0", + "@tinkoff/ng-dompurify": "4.0.0", "angular-svg-round-progressbar": "^9.0.0", "ansi-to-html": "^0.7.2", "base64-js": "^1.5.1", diff --git a/web/projects/diagnostic-ui/src/app/app-routing.module.ts b/web/projects/diagnostic-ui/src/app/app-routing.module.ts deleted file mode 100644 index f9f009b48..000000000 --- a/web/projects/diagnostic-ui/src/app/app-routing.module.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { NgModule } from '@angular/core' -import { PreloadAllModules, RouterModule, Routes } from '@angular/router' - -const routes: Routes = [ - { - path: '', - loadChildren: () => - import('./pages/home/home.module').then(m => m.HomePageModule), - }, - { - path: 'logs', - loadChildren: () => - import('./pages/logs/logs.module').then(m => m.LogsPageModule), - }, -] - -@NgModule({ - imports: [ - RouterModule.forRoot(routes, { - scrollPositionRestoration: 'enabled', - preloadingStrategy: PreloadAllModules, - useHash: true, - }), - ], - exports: [RouterModule], -}) -export class AppRoutingModule {} diff --git a/web/projects/diagnostic-ui/src/app/app.component.html b/web/projects/diagnostic-ui/src/app/app.component.html deleted file mode 100644 index cd28a7e80..000000000 --- a/web/projects/diagnostic-ui/src/app/app.component.html +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/web/projects/diagnostic-ui/src/app/app.component.scss b/web/projects/diagnostic-ui/src/app/app.component.scss deleted file mode 100644 index b528fd9bd..000000000 --- a/web/projects/diagnostic-ui/src/app/app.component.scss +++ /dev/null @@ -1,8 +0,0 @@ -:host { - display: block; - height: 100%; -} - -tui-root { - height: 100%; -} diff --git a/web/projects/diagnostic-ui/src/app/app.component.ts b/web/projects/diagnostic-ui/src/app/app.component.ts deleted file mode 100644 index 5ac82a652..000000000 --- a/web/projects/diagnostic-ui/src/app/app.component.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Component } from '@angular/core' - -@Component({ - selector: 'app-root', - templateUrl: 'app.component.html', - styleUrls: ['app.component.scss'], -}) -export class AppComponent { - constructor() {} -} diff --git a/web/projects/diagnostic-ui/src/app/app.module.ts b/web/projects/diagnostic-ui/src/app/app.module.ts deleted file mode 100644 index 1abde53a3..000000000 --- a/web/projects/diagnostic-ui/src/app/app.module.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { NgModule } from '@angular/core' -import { BrowserAnimationsModule } from '@angular/platform-browser/animations' -import { RouteReuseStrategy } from '@angular/router' -import { IonicModule, IonicRouteStrategy } from '@ionic/angular' -import { TuiRootModule } from '@taiga-ui/core' -import { AppComponent } from './app.component' -import { AppRoutingModule } from './app-routing.module' -import { HttpClientModule } from '@angular/common/http' -import { ApiService } from './services/api/api.service' -import { MockApiService } from './services/api/mock-api.service' -import { LiveApiService } from './services/api/live-api.service' -import { RELATIVE_URL, WorkspaceConfig } from '@start9labs/shared' - -const { - useMocks, - ui: { api }, -} = require('../../../../config.json') as WorkspaceConfig - -@NgModule({ - declarations: [AppComponent], - imports: [ - HttpClientModule, - BrowserAnimationsModule, - IonicModule.forRoot({ - mode: 'md', - }), - AppRoutingModule, - TuiRootModule, - ], - providers: [ - { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, - { - provide: ApiService, - useClass: useMocks ? MockApiService : LiveApiService, - }, - { - provide: RELATIVE_URL, - useValue: `/${api.url}/${api.version}`, - }, - ], - bootstrap: [AppComponent], -}) -export class AppModule {} diff --git a/web/projects/diagnostic-ui/src/app/pages/home/home-routing.module.ts b/web/projects/diagnostic-ui/src/app/pages/home/home-routing.module.ts deleted file mode 100644 index efb1977dc..000000000 --- a/web/projects/diagnostic-ui/src/app/pages/home/home-routing.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NgModule } from '@angular/core' -import { RouterModule, Routes } from '@angular/router' -import { HomePage } from './home.page' - -const routes: Routes = [ - { - path: '', - component: HomePage, - }, -] - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class HomePageRoutingModule {} diff --git a/web/projects/diagnostic-ui/src/app/services/api/api.service.ts b/web/projects/diagnostic-ui/src/app/services/api/api.service.ts deleted file mode 100644 index 562d486c3..000000000 --- a/web/projects/diagnostic-ui/src/app/services/api/api.service.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { LogsRes, ServerLogsReq } from '@start9labs/shared' - -export abstract class ApiService { - abstract getError(): Promise - abstract restart(): Promise - abstract forgetDrive(): Promise - abstract repairDisk(): Promise - abstract systemRebuild(): Promise - abstract getLogs(params: ServerLogsReq): Promise -} - -export interface GetErrorRes { - code: number - message: string - data: { details: string } -} diff --git a/web/projects/diagnostic-ui/src/app/services/api/live-api.service.ts b/web/projects/diagnostic-ui/src/app/services/api/live-api.service.ts deleted file mode 100644 index bbde6e5ba..000000000 --- a/web/projects/diagnostic-ui/src/app/services/api/live-api.service.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Injectable } from '@angular/core' -import { - HttpService, - isRpcError, - RpcError, - RPCOptions, -} from '@start9labs/shared' -import { ApiService, GetErrorRes } from './api.service' -import { LogsRes, ServerLogsReq } from '@start9labs/shared' - -@Injectable() -export class LiveApiService implements ApiService { - constructor(private readonly http: HttpService) {} - - async getError(): Promise { - return this.rpcRequest({ - method: 'diagnostic.error', - params: {}, - }) - } - - async restart(): Promise { - return this.rpcRequest({ - method: 'diagnostic.restart', - params: {}, - }) - } - - async forgetDrive(): Promise { - return this.rpcRequest({ - method: 'diagnostic.disk.forget', - params: {}, - }) - } - - async repairDisk(): Promise { - return this.rpcRequest({ - method: 'diagnostic.disk.repair', - params: {}, - }) - } - - async systemRebuild(): Promise { - return this.rpcRequest({ - method: 'diagnostic.rebuild', - params: {}, - }) - } - - async getLogs(params: ServerLogsReq): Promise { - return this.rpcRequest({ - method: 'diagnostic.logs', - params, - }) - } - - private async rpcRequest(opts: RPCOptions): Promise { - const res = await this.http.rpcRequest(opts) - - const rpcRes = res.body - - if (isRpcError(rpcRes)) { - throw new RpcError(rpcRes.error) - } - - return rpcRes.result - } -} diff --git a/web/projects/diagnostic-ui/src/app/services/api/mock-api.service.ts b/web/projects/diagnostic-ui/src/app/services/api/mock-api.service.ts deleted file mode 100644 index d991edd32..000000000 --- a/web/projects/diagnostic-ui/src/app/services/api/mock-api.service.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Injectable } from '@angular/core' -import { pauseFor } from '@start9labs/shared' -import { ApiService, GetErrorRes } from './api.service' -import { LogsRes, ServerLogsReq, Log } from '@start9labs/shared' - -@Injectable() -export class MockApiService implements ApiService { - async getError(): Promise { - await pauseFor(1000) - return { - code: 15, - message: 'Unknown server', - data: { details: 'Some details about the error here' }, - } - } - - async restart(): Promise { - await pauseFor(1000) - } - - async forgetDrive(): Promise { - await pauseFor(1000) - } - - async repairDisk(): Promise { - await pauseFor(1000) - } - - async systemRebuild(): Promise { - await pauseFor(1000) - } - - async getLogs(params: ServerLogsReq): Promise { - await pauseFor(1000) - let entries: Log[] - if (Math.random() < 0.2) { - entries = packageLogs - } else { - const arrLength = params.limit - ? Math.ceil(params.limit / packageLogs.length) - : 10 - entries = new Array(arrLength) - .fill(packageLogs) - .reduce((acc, val) => acc.concat(val), []) - } - return { - entries, - startCursor: 'start-cursor', - endCursor: 'end-cursor', - } - } -} - -const packageLogs = [ - { - timestamp: '2019-12-26T14:20:30.872Z', - message: '****** START *****', - }, - { - timestamp: '2019-12-26T14:21:30.872Z', - message: 'ServerLogs ServerLogs ServerLogs ServerLogs ServerLogs', - }, - { - timestamp: '2019-12-26T14:22:30.872Z', - message: '****** FINISH *****', - }, -] diff --git a/web/projects/diagnostic-ui/src/environments/environment.prod.ts b/web/projects/diagnostic-ui/src/environments/environment.prod.ts deleted file mode 100644 index 970e25bd7..000000000 --- a/web/projects/diagnostic-ui/src/environments/environment.prod.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const environment = { - production: true, -} diff --git a/web/projects/diagnostic-ui/src/environments/environment.ts b/web/projects/diagnostic-ui/src/environments/environment.ts deleted file mode 100644 index 5c68c17ab..000000000 --- a/web/projects/diagnostic-ui/src/environments/environment.ts +++ /dev/null @@ -1,16 +0,0 @@ -// This file can be replaced during build by using the `fileReplacements` array. -// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. -// The list of file replacements can be found in `angular.json`. - -export const environment = { - production: false, -} - -/* - * For easier debugging in development mode, you can import the following file - * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. - * - * This import should be commented out in production mode because it will have a negative impact - * on performance if an error is thrown. - */ -// import 'zone.js/dist/zone-error'; // Included with Angular CLI. diff --git a/web/projects/diagnostic-ui/src/index.html b/web/projects/diagnostic-ui/src/index.html deleted file mode 100644 index 1822018f3..000000000 --- a/web/projects/diagnostic-ui/src/index.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - StartOS Diagnostic UI - - - - - - - - - - - - - - - diff --git a/web/projects/diagnostic-ui/src/main.ts b/web/projects/diagnostic-ui/src/main.ts deleted file mode 100644 index 21499c3cd..000000000 --- a/web/projects/diagnostic-ui/src/main.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { enableProdMode } from '@angular/core' -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic' -import { AppModule } from './app/app.module' -import { environment } from './environments/environment' - -if (environment.production) { - enableProdMode() -} - -platformBrowserDynamic() - .bootstrapModule(AppModule) - .catch(err => console.error(err)) diff --git a/web/projects/diagnostic-ui/src/polyfills.ts b/web/projects/diagnostic-ui/src/polyfills.ts deleted file mode 100644 index 4437ced44..000000000 --- a/web/projects/diagnostic-ui/src/polyfills.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * This file includes polyfills needed by Angular and is loaded before the app. - * You can add your own extra polyfills to this file. - * - * This file is divided into 2 sections: - * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. - * 2. Application imports. Files imported after ZoneJS that should be loaded before your main - * file. - * - * The current setup is for so-called "evergreen" browsers; the last versions of browsers that - * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), - * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. - * - * Learn more in https://angular.io/guide/browser-support - */ - -/*************************************************************************************************** - * BROWSER POLYFILLS - */ - -/** IE11 requires the following for NgClass support on SVG elements */ -// import 'classlist.js'; // Run `npm install --save classlist.js`. - -/** - * Web Animations `@angular/platform-browser/animations` - * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. - * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). - */ -// import 'web-animations-js'; // Run `npm install --save web-animations-js`. - -/** - * By default, zone.js will patch all possible macroTask and DomEvents - * user can disable parts of macroTask/DomEvents patch by setting following flags - * because those flags need to be set before `zone.js` being loaded, and webpack - * will put import in the top of bundle, so user need to create a separate file - * in this directory (for example: zone-flags.ts), and put the following flags - * into that file, and then add the following code before importing zone.js. - * import './zone-flags'; - * - * The flags allowed in zone-flags.ts are listed here. - * - * The following flags will work for all browsers. - * - * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame - * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick - * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames - * - * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js - * with the following flag, it will bypass `zone.js` patch for IE/Edge - * - * (window as any).__Zone_enable_cross_context_check = true; - * - */ - -import './zone-flags' - -/*************************************************************************************************** - * Zone JS is required by default for Angular itself. - */ -import 'zone.js/dist/zone' // Included with Angular CLI. - -/*************************************************************************************************** - * APPLICATION IMPORTS - */ diff --git a/web/projects/diagnostic-ui/src/styles.scss b/web/projects/diagnostic-ui/src/styles.scss deleted file mode 100644 index ac0aadb69..000000000 --- a/web/projects/diagnostic-ui/src/styles.scss +++ /dev/null @@ -1,41 +0,0 @@ -@font-face { - font-family: 'Montserrat'; - font-style: normal; - font-weight: normal; - src: url('/assets/fonts/Montserrat/Montserrat-Regular.ttf'); -} - -/** Ionic CSS Variables overrides **/ -:root { - --ion-font-family: 'Montserrat'; - - --ion-color-primary: #0075e1; - - --ion-color-medium: #989aa2; - --ion-color-medium-rgb: 152,154,162; - --ion-color-medium-contrast: #000000; - --ion-color-medium-contrast-rgb: 0,0,0; - --ion-color-medium-shade: #86888f; - --ion-color-medium-tint: #a2a4ab; - - --ion-color-light: #222428; - --ion-color-light-rgb: 34,36,40; - --ion-color-light-contrast: #ffffff; - --ion-color-light-contrast-rgb: 255,255,255; - --ion-color-light-shade: #1e2023; - --ion-color-light-tint: #383a3e; - - --ion-item-background: #2b2b2b; - --ion-toolbar-background: #2b2b2b; - --ion-card-background: #2b2b2b; - - --ion-background-color: #282828; - --ion-background-color-rgb: 30,30,30; - --ion-text-color: var(--ion-color-dark); - --ion-text-color-rgb: var(--ion-color-dark-rgb); -} - -.loader { - --spinner-color: var(--ion-color-warning) !important; - z-index: 40000 !important; -} diff --git a/web/projects/diagnostic-ui/src/zone-flags.ts b/web/projects/diagnostic-ui/src/zone-flags.ts deleted file mode 100644 index 24ca60fe2..000000000 --- a/web/projects/diagnostic-ui/src/zone-flags.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Prevents Angular change detection from - * running with certain Web Component callbacks - */ -// eslint-disable-next-line no-underscore-dangle -(window as any).__Zone_disable_customElements = true diff --git a/web/projects/diagnostic-ui/tsconfig.json b/web/projects/diagnostic-ui/tsconfig.json deleted file mode 100644 index f642f09b3..000000000 --- a/web/projects/diagnostic-ui/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -/* To learn more about this file see: https://angular.io/config/tsconfig. */ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "baseUrl": "./" - }, - "files": ["src/main.ts", "src/polyfills.ts"], - "include": ["src/**/*.d.ts"] -} diff --git a/web/projects/setup-wizard/src/app/app.component.ts b/web/projects/setup-wizard/src/app/app.component.ts index e4bf41f5c..e10d672e5 100644 --- a/web/projects/setup-wizard/src/app/app.component.ts +++ b/web/projects/setup-wizard/src/app/app.component.ts @@ -21,7 +21,7 @@ export class AppComponent { let route = '/home' if (inProgress) { - route = inProgress.complete ? '/success' : '/loading' + route = inProgress.status === 'complete' ? '/success' : '/loading' } await this.navCtrl.navigateForward(route) diff --git a/web/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts b/web/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts index 1a5dd042d..7f7ee6241 100644 --- a/web/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts +++ b/web/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts @@ -5,12 +5,7 @@ import { ModalController, NavController, } from '@ionic/angular' -import { - ApiService, - BackupRecoverySource, - DiskRecoverySource, - DiskMigrateSource, -} from 'src/app/services/api/api.service' +import { ApiService } from 'src/app/services/api/api.service' import { DiskInfo, ErrorToastService, GuidPipe } from '@start9labs/shared' import { StateService } from 'src/app/services/state.service' import { PasswordPage } from '../../modals/password/password.page' @@ -58,18 +53,17 @@ export class EmbassyPage { } else if (this.stateService.setupType === 'restore') { this.storageDrives = disks.filter( d => + this.stateService.recoverySource?.type === 'backup' && + this.stateService.recoverySource.target?.type === 'disk' && !d.partitions .map(p => p.logicalname) - .includes( - ( - (this.stateService.recoverySource as BackupRecoverySource) - ?.target as DiskRecoverySource - )?.logicalname, - ), + .includes(this.stateService.recoverySource.target.logicalname), ) - } else if (this.stateService.setupType === 'transfer') { - const guid = (this.stateService.recoverySource as DiskMigrateSource) - .guid + } else if ( + this.stateService.setupType === 'transfer' && + this.stateService.recoverySource?.type === 'migrate' + ) { + const guid = this.stateService.recoverySource.guid this.storageDrives = disks.filter(d => { return ( d.guid !== guid && !d.partitions.map(p => p.guid).includes(guid) diff --git a/web/projects/setup-wizard/src/app/pages/loading/loading.module.ts b/web/projects/setup-wizard/src/app/pages/loading/loading.module.ts index e937a7e19..2f3507941 100644 --- a/web/projects/setup-wizard/src/app/pages/loading/loading.module.ts +++ b/web/projects/setup-wizard/src/app/pages/loading/loading.module.ts @@ -2,11 +2,11 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { FormsModule } from '@angular/forms' -import { LoadingPage, ToMessagePipe } from './loading.page' +import { LoadingPage } from './loading.page' import { LoadingPageRoutingModule } from './loading-routing.module' @NgModule({ imports: [CommonModule, FormsModule, IonicModule, LoadingPageRoutingModule], - declarations: [LoadingPage, ToMessagePipe], + declarations: [LoadingPage], }) export class LoadingPageModule {} diff --git a/web/projects/setup-wizard/src/app/pages/loading/loading.page.html b/web/projects/setup-wizard/src/app/pages/loading/loading.page.html index fd7fcc24c..6c9ca41ab 100644 --- a/web/projects/setup-wizard/src/app/pages/loading/loading.page.html +++ b/web/projects/setup-wizard/src/app/pages/loading/loading.page.html @@ -1,39 +1,17 @@ - - - - - - - Initializing StartOS -
- - {{ progress.transferred | toMessage }} - -
-
+
+

+ Setting up your server +

+
+ Progress: {{ (progress.total * 100).toFixed(0) }}% +
- - -

- - - Progress: {{ (transferred * 100).toFixed() }}% - - - {{ (progress.totalBytes / 1073741824).toFixed(2) }} GB - - -

-
- - - - - + +

{{ progress.message }}

+
diff --git a/web/projects/setup-wizard/src/app/pages/loading/loading.page.scss b/web/projects/setup-wizard/src/app/pages/loading/loading.page.scss index 87bfffa33..e69de29bb 100644 --- a/web/projects/setup-wizard/src/app/pages/loading/loading.page.scss +++ b/web/projects/setup-wizard/src/app/pages/loading/loading.page.scss @@ -1,3 +0,0 @@ -ion-card-title { - font-size: 42px; -} \ No newline at end of file diff --git a/web/projects/setup-wizard/src/app/pages/loading/loading.page.ts b/web/projects/setup-wizard/src/app/pages/loading/loading.page.ts index ce1a1b3c0..459be5c7a 100644 --- a/web/projects/setup-wizard/src/app/pages/loading/loading.page.ts +++ b/web/projects/setup-wizard/src/app/pages/loading/loading.page.ts @@ -1,15 +1,23 @@ import { Component } from '@angular/core' import { NavController } from '@ionic/angular' -import { StateService } from 'src/app/services/state.service' import { Pipe, PipeTransform } from '@angular/core' -import { BehaviorSubject } from 'rxjs' +import { + EMPTY, + Observable, + catchError, + filter, + from, + interval, + map, + of, + startWith, + switchMap, + take, + tap, +} from 'rxjs' import { ApiService } from 'src/app/services/api/api.service' -import { ErrorToastService, pauseFor } from '@start9labs/shared' - -type Progress = { - totalBytes: number | null - transferred: number -} +import { ErrorToastService } from '@start9labs/shared' +import { T } from '@start9labs/start-sdk' @Component({ selector: 'app-loading', @@ -17,10 +25,46 @@ type Progress = { styleUrls: ['loading.page.scss'], }) export class LoadingPage { - readonly progress$ = new BehaviorSubject({ - totalBytes: null, - transferred: 0, - }) + readonly progress$ = this.getRunningStatus$().pipe( + switchMap(res => + this.api.openProgressWebsocket$(res.guid).pipe( + startWith(res.progress), + catchError((_, watch$) => { + return interval(2000).pipe( + switchMap(() => + from(this.api.getStatus()).pipe(catchError(() => EMPTY)), + ), + take(1), + switchMap(() => watch$), + ) + }), + tap(progress => { + if (progress.overall === true) { + this.getStatus() + } + }), + ), + ), + map(({ phases, overall }) => { + return { + total: getDecimal(overall), + message: phases + .filter( + ( + p, + ): p is { + name: string + progress: { + done: number + total: number | null + } + } => p.progress !== true && p.progress !== null, + ) + .map(p => `${p.name}${getPhaseBytes(p.progress)}`) + .join(','), + } + }), + ) constructor( private readonly navCtrl: NavController, @@ -28,55 +72,55 @@ export class LoadingPage { private readonly errorToastService: ErrorToastService, ) {} - ngOnInit() { - this.poll() - } + private async getStatus(): Promise<{ + status: 'running' + guid: string + progress: T.FullProgress + } | void> { + const res = await this.api.getStatus() - async poll() { - try { - const progress = await this.api.getStatus() - - if (!progress) return - - const { totalBytes, bytesTransferred } = progress - - this.progress$.next({ - totalBytes, - transferred: totalBytes ? bytesTransferred / totalBytes : 0, - }) - - if (progress.complete) { - this.navCtrl.navigateForward(`/success`) - this.progress$.complete() - return - } - - await pauseFor(250) - - setTimeout(() => this.poll(), 0) // prevent call stack from growing - } catch (e: any) { - this.errorToastService.present(e) - } - } -} - -@Pipe({ - name: 'toMessage', -}) -export class ToMessagePipe implements PipeTransform { - constructor(private readonly stateService: StateService) {} - - transform(progress: number | null): string { - if (['fresh', 'attach'].includes(this.stateService.setupType || '')) { - return 'Setting up your server' - } - - if (!progress) { - return 'Calculating size' - } else if (progress < 1) { - return 'Copying data' + if (!res) { + this.navCtrl.navigateRoot('/home') + } else if (res.status === 'complete') { + this.navCtrl.navigateForward(`/success`) } else { - return 'Finalizing' + return res } } + + private getRunningStatus$(): Observable<{ + status: 'running' + guid: string + progress: T.FullProgress + }> { + return from(this.getStatus()).pipe( + filter(Boolean), + catchError(e => { + this.errorToastService.present(e) + return of(e) + }), + take(1), + ) + } +} + +function getDecimal(progress: T.Progress): number { + if (progress === true) { + return 1 + } else if (!progress || !progress.total) { + return 0 + } else { + return progress.total && progress.done / progress.total + } +} + +function getPhaseBytes( + progress: + | false + | { + done: number + total: number | null + }, +): string { + return progress === false ? '' : `: (${progress.done}/${progress.total})` } diff --git a/web/projects/setup-wizard/src/app/services/api/api.service.ts b/web/projects/setup-wizard/src/app/services/api/api.service.ts index 375e64a78..6719ce859 100644 --- a/web/projects/setup-wizard/src/app/services/api/api.service.ts +++ b/web/projects/setup-wizard/src/app/services/api/api.service.ts @@ -1,16 +1,21 @@ import * as jose from 'node-jose' import { DiskListResponse, StartOSDiskInfo } from '@start9labs/shared' +import { T } from '@start9labs/start-sdk' +import { WebSocketSubjectConfig } from 'rxjs/webSocket' +import { Observable } from 'rxjs' + export abstract class ApiService { pubkey?: jose.JWK.Key - abstract getStatus(): Promise // setup.status + abstract getStatus(): Promise // setup.status abstract getPubKey(): Promise // setup.get-pubkey abstract getDrives(): Promise // setup.disk.list - abstract verifyCifs(cifs: CifsRecoverySource): Promise // setup.cifs.verify - abstract attach(importInfo: AttachReq): Promise // setup.attach - abstract execute(setupInfo: ExecuteReq): Promise // setup.execute - abstract complete(): Promise // setup.complete + abstract verifyCifs(cifs: T.VerifyCifsParams): Promise // setup.cifs.verify + abstract attach(importInfo: T.AttachParams): Promise // setup.attach + abstract execute(setupInfo: T.SetupExecuteParams): Promise // setup.execute + abstract complete(): Promise // setup.complete abstract exit(): Promise // setup.exit + abstract openProgressWebsocket$(guid: string): Observable async encrypt(toEncrypt: string): Promise { if (!this.pubkey) throw new Error('No pubkey found!') @@ -27,29 +32,7 @@ type Encrypted = { encrypted: string } -export type StatusRes = { - bytesTransferred: number - totalBytes: number | null - complete: boolean -} | null - -export type AttachReq = { - guid: string - startOsPassword: Encrypted -} - -export type ExecuteReq = { - startOsLogicalname: string - startOsPassword: Encrypted - recoverySource: RecoverySource | null - recoveryPassword: Encrypted | null -} - -export type CompleteRes = { - torAddress: string - lanAddress: string - rootCa: string -} +export type WebsocketConfig = Omit, 'url'> export type DiskBackupTarget = { vendor: string | null @@ -68,27 +51,3 @@ export type CifsBackupTarget = { mountable: boolean startOs: StartOSDiskInfo | null } - -export type DiskRecoverySource = { - type: 'disk' - logicalname: string // partition logicalname -} - -export type BackupRecoverySource = { - type: 'backup' - target: CifsRecoverySource | DiskRecoverySource -} -export type RecoverySource = BackupRecoverySource | DiskMigrateSource - -export type DiskMigrateSource = { - type: 'migrate' - guid: string -} - -export type CifsRecoverySource = { - type: 'cifs' - hostname: string - path: string - username: string - password: Encrypted | null -} diff --git a/web/projects/setup-wizard/src/app/services/api/live-api.service.ts b/web/projects/setup-wizard/src/app/services/api/live-api.service.ts index 566cc84cf..f431f5151 100644 --- a/web/projects/setup-wizard/src/app/services/api/live-api.service.ts +++ b/web/projects/setup-wizard/src/app/services/api/live-api.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core' +import { Inject, Injectable } from '@angular/core' import { DiskListResponse, StartOSDiskInfo, @@ -8,27 +8,35 @@ import { RpcError, RPCOptions, } from '@start9labs/shared' -import { - ApiService, - CifsRecoverySource, - DiskRecoverySource, - StatusRes, - AttachReq, - ExecuteReq, - CompleteRes, -} from './api.service' +import { T } from '@start9labs/start-sdk' +import { ApiService, WebsocketConfig } from './api.service' import * as jose from 'node-jose' +import { Observable } from 'rxjs' +import { DOCUMENT } from '@angular/common' +import { webSocket } from 'rxjs/webSocket' @Injectable({ providedIn: 'root', }) export class LiveApiService extends ApiService { - constructor(private readonly http: HttpService) { + constructor( + private readonly http: HttpService, + @Inject(DOCUMENT) private readonly document: Document, + ) { super() } - async getStatus() { - return this.rpcRequest({ + openProgressWebsocket$(guid: string): Observable { + const { location } = this.document.defaultView! + const host = location.host + + return webSocket({ + url: `ws://${host}/ws/rpc/${guid}`, + }) + } + + async getStatus(): Promise { + return this.rpcRequest({ method: 'setup.status', params: {}, }) @@ -41,7 +49,7 @@ export class LiveApiService extends ApiService { * this wil all public/private key, which means that there is no information loss * through the network. */ - async getPubKey() { + async getPubKey(): Promise { const response: jose.JWK.Key = await this.rpcRequest({ method: 'setup.get-pubkey', params: {}, @@ -50,14 +58,14 @@ export class LiveApiService extends ApiService { this.pubkey = response } - async getDrives() { + async getDrives(): Promise { return this.rpcRequest({ method: 'setup.disk.list', params: {}, }) } - async verifyCifs(source: CifsRecoverySource) { + async verifyCifs(source: T.VerifyCifsParams): Promise { source.path = source.path.replace('/\\/g', '/') return this.rpcRequest({ method: 'setup.cifs.verify', @@ -65,14 +73,14 @@ export class LiveApiService extends ApiService { }) } - async attach(params: AttachReq) { - await this.rpcRequest({ + async attach(params: T.AttachParams): Promise { + return this.rpcRequest({ method: 'setup.attach', params, }) } - async execute(setupInfo: ExecuteReq) { + async execute(setupInfo: T.SetupExecuteParams): Promise { if (setupInfo.recoverySource?.type === 'backup') { if (isCifsSource(setupInfo.recoverySource.target)) { setupInfo.recoverySource.target.path = @@ -80,14 +88,14 @@ export class LiveApiService extends ApiService { } } - await this.rpcRequest({ + return this.rpcRequest({ method: 'setup.execute', params: setupInfo, }) } - async complete() { - const res = await this.rpcRequest({ + async complete(): Promise { + const res = await this.rpcRequest({ method: 'setup.complete', params: {}, }) @@ -98,7 +106,7 @@ export class LiveApiService extends ApiService { } } - async exit() { + async exit(): Promise { await this.rpcRequest({ method: 'setup.exit', params: {}, @@ -119,7 +127,7 @@ export class LiveApiService extends ApiService { } function isCifsSource( - source: CifsRecoverySource | DiskRecoverySource | null, -): source is CifsRecoverySource { - return !!(source as CifsRecoverySource)?.hostname + source: T.BackupTargetFS | null, +): source is T.Cifs & { type: 'cifs' } { + return !!(source as T.Cifs)?.hostname } diff --git a/web/projects/setup-wizard/src/app/services/api/mock-api.service.ts b/web/projects/setup-wizard/src/app/services/api/mock-api.service.ts index df32bd09e..0a1c221f7 100644 --- a/web/projects/setup-wizard/src/app/services/api/mock-api.service.ts +++ b/web/projects/setup-wizard/src/app/services/api/mock-api.service.ts @@ -1,42 +1,151 @@ import { Injectable } from '@angular/core' -import { encodeBase64, pauseFor } from '@start9labs/shared' import { - ApiService, - AttachReq, - CifsRecoverySource, - CompleteRes, - ExecuteReq, -} from './api.service' + DiskListResponse, + StartOSDiskInfo, + encodeBase64, + pauseFor, +} from '@start9labs/shared' +import { ApiService } from './api.service' import * as jose from 'node-jose' - -let tries: number +import { T } from '@start9labs/start-sdk' +import { + Observable, + concatMap, + delay, + from, + interval, + map, + mergeScan, + of, + startWith, + switchMap, + switchScan, + takeWhile, +} from 'rxjs' @Injectable({ providedIn: 'root', }) export class MockApiService extends ApiService { - async getStatus() { - const restoreOrMigrate = true + // fullProgress$(): Observable { + // const phases = [ + // { + // name: 'Preparing Data', + // progress: null, + // }, + // { + // name: 'Transferring Data', + // progress: null, + // }, + // { + // name: 'Finalizing Setup', + // progress: null, + // }, + // ] + + // return from(phases).pipe( + // switchScan((acc, val, i) => {}, { overall: null, phases }), + // ) + // } + + // namedProgress$(namedProgress: T.NamedProgress): Observable { + // return of(namedProgress).pipe(startWith(namedProgress)) + // } + + // progress$(progress: T.Progress): Observable {} + + // websocket + + openProgressWebsocket$(guid: string): Observable { + return of(PROGRESS) + // const numPhases = PROGRESS.phases.length + + // return of(PROGRESS).pipe( + // switchMap(full => + // from(PROGRESS.phases).pipe( + // mergeScan((full, phase, i) => { + // if ( + // !phase.progress || + // typeof phase.progress !== 'object' || + // !phase.progress.total + // ) { + // full.phases[i].progress = true + + // if ( + // full.overall && + // typeof full.overall === 'object' && + // full.overall.total + // ) { + // const step = full.overall.total / numPhases + // full.overall.done += step + // } + + // return of(full).pipe(delay(2000)) + // } else { + // const total = phase.progress.total + // const step = total / 4 + // let done = phase.progress.done + + // return interval(1000).pipe( + // takeWhile(() => done < total), + // map(() => { + // done += step + + // console.error(done) + + // if ( + // full.overall && + // typeof full.overall === 'object' && + // full.overall.total + // ) { + // const step = full.overall.total / numPhases / 4 + + // full.overall.done += step + // } + + // if (done === total) { + // full.phases[i].progress = true + + // if (i === numPhases - 1) { + // full.overall = true + // } + // } + // return full + // }), + // ) + // } + // }, full), + // ), + // ), + // ) + } + + private statusIndex = 0 + async getStatus(): Promise { await pauseFor(1000) - if (tries === undefined) { - tries = 0 - return null - } + this.statusIndex++ - tries++ - - const total = tries <= 4 ? tries * 268435456 : 1073741824 - const progress = tries > 4 ? (tries - 4) * 268435456 : 0 - - return { - bytesTransferred: restoreOrMigrate ? progress : 0, - totalBytes: restoreOrMigrate ? total : null, - complete: progress === total, + switch (this.statusIndex) { + case 2: + return { + status: 'running', + progress: PROGRESS, + guid: 'progress-guid', + } + case 3: + return { + status: 'complete', + torAddress: 'https://asdafsadasdasasdasdfasdfasdf.onion', + lanAddress: 'https://adjective-noun.local', + rootCa: encodeBase64(rootCA), + } + default: + return null } } - async getPubKey() { + async getPubKey(): Promise { await pauseFor(1000) // randomly generated @@ -52,7 +161,7 @@ export class MockApiService extends ApiService { }) } - async getDrives() { + async getDrives(): Promise { await pauseFor(1000) return [ { @@ -127,7 +236,7 @@ export class MockApiService extends ApiService { ] } - async verifyCifs(params: CifsRecoverySource) { + async verifyCifs(params: T.VerifyCifsParams): Promise { await pauseFor(1000) return { version: '0.3.0', @@ -138,15 +247,25 @@ export class MockApiService extends ApiService { } } - async attach(params: AttachReq) { + async attach(params: T.AttachParams): Promise { await pauseFor(1000) + + return { + progress: PROGRESS, + guid: 'progress-guid', + } } - async execute(setupInfo: ExecuteReq) { + async execute(setupInfo: T.SetupExecuteParams): Promise { await pauseFor(1000) + + return { + progress: PROGRESS, + guid: 'progress-guid', + } } - async complete(): Promise { + async complete(): Promise { await pauseFor(1000) return { torAddress: 'https://asdafsadasdasasdasdfasdfasdf.onion', @@ -155,7 +274,7 @@ export class MockApiService extends ApiService { } } - async exit() { + async exit(): Promise { await pauseFor(1000) } } @@ -182,3 +301,8 @@ Rf3ZOPm9QP92YpWyYDkfAU04xdDo1vR0MYjKPkl4LjRqSU/tcCJnPMbJiwq+bWpX 2WJoEBXB/p15Kn6JxjI0ze2SnSI48JZ8it4fvxrhOo0VoLNIuCuNXJOwU17Rdl1W YJidaq7je6k18AdgPA0Kh8y1XtfUH3fTaVw4 -----END CERTIFICATE-----` + +const PROGRESS = { + overall: null, + phases: [], +} diff --git a/web/projects/setup-wizard/src/app/services/state.service.ts b/web/projects/setup-wizard/src/app/services/state.service.ts index 916b066ee..8c653a088 100644 --- a/web/projects/setup-wizard/src/app/services/state.service.ts +++ b/web/projects/setup-wizard/src/app/services/state.service.ts @@ -1,13 +1,13 @@ import { Injectable } from '@angular/core' -import { ApiService, RecoverySource } from './api/api.service' +import { ApiService } from './api/api.service' +import { T } from '@start9labs/start-sdk' @Injectable({ providedIn: 'root', }) export class StateService { setupType?: 'fresh' | 'restore' | 'attach' | 'transfer' - - recoverySource?: RecoverySource + recoverySource?: T.RecoverySource recoveryPassword?: string constructor(private readonly api: ApiService) {} diff --git a/web/projects/shared/src/types/api.ts b/web/projects/shared/src/types/api.ts index 26cd00218..743ea6ac8 100644 --- a/web/projects/shared/src/types/api.ts +++ b/web/projects/shared/src/types/api.ts @@ -13,6 +13,7 @@ export type LogsRes = { export interface Log { timestamp: string message: string + bootId: string } export type DiskListResponse = DiskInfo[] diff --git a/web/projects/ui/src/app/app-routing.module.ts b/web/projects/ui/src/app/app-routing.module.ts index ddb66d3c5..e7a2036ec 100644 --- a/web/projects/ui/src/app/app-routing.module.ts +++ b/web/projects/ui/src/app/app-routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core' import { PreloadAllModules, RouterModule, Routes } from '@angular/router' +import { stateNot } from 'src/app/services/state.service' import { AuthGuard } from './guards/auth.guard' import { UnauthGuard } from './guards/unauth.guard' @@ -15,15 +16,29 @@ const routes: Routes = [ loadChildren: () => import('./pages/login/login.module').then(m => m.LoginPageModule), }, + { + path: 'diagnostic', + canActivate: [stateNot(['initializing', 'running'])], + loadChildren: () => + import('./pages/diagnostic-routes/diagnostic-routing.module').then( + m => m.DiagnosticModule, + ), + }, + { + path: 'initializing', + canActivate: [stateNot(['error', 'running'])], + loadChildren: () => + import('./pages/init/init.module').then(m => m.InitPageModule), + }, { path: 'home', - canActivate: [AuthGuard], + canActivate: [AuthGuard, stateNot(['error', 'initializing'])], loadChildren: () => import('./pages/home/home.module').then(m => m.HomePageModule), }, { path: 'system', - canActivate: [AuthGuard], + canActivate: [AuthGuard, stateNot(['error', 'initializing'])], canActivateChild: [AuthGuard], loadChildren: () => import('./pages/server-routes/server-routing.module').then( @@ -32,14 +47,14 @@ const routes: Routes = [ }, { path: 'updates', - canActivate: [AuthGuard], + canActivate: [AuthGuard, stateNot(['error', 'initializing'])], canActivateChild: [AuthGuard], loadChildren: () => import('./pages/updates/updates.module').then(m => m.UpdatesPageModule), }, { path: 'marketplace', - canActivate: [AuthGuard], + canActivate: [AuthGuard, stateNot(['error', 'initializing'])], canActivateChild: [AuthGuard], loadChildren: () => import('./pages/marketplace-routes/marketplace-routing.module').then( @@ -48,7 +63,7 @@ const routes: Routes = [ }, { path: 'notifications', - canActivate: [AuthGuard], + canActivate: [AuthGuard, stateNot(['error', 'initializing'])], loadChildren: () => import('./pages/notifications/notifications.module').then( m => m.NotificationsPageModule, @@ -56,7 +71,7 @@ const routes: Routes = [ }, { path: 'services', - canActivate: [AuthGuard], + canActivate: [AuthGuard, stateNot(['error', 'initializing'])], canActivateChild: [AuthGuard], loadChildren: () => import('./pages/apps-routes/apps-routing.module').then( diff --git a/web/projects/ui/src/app/app.component.html b/web/projects/ui/src/app/app.component.html index 0506d5214..0d9fb860f 100644 --- a/web/projects/ui/src/app/app.component.html +++ b/web/projects/ui/src/app/app.component.html @@ -15,6 +15,7 @@ type="overlay" side="start" class="left-menu" + [class.left-menu_hidden]="withoutMenu" > diff --git a/web/projects/ui/src/app/app.component.scss b/web/projects/ui/src/app/app.component.scss index 55135b1e5..aedbdc6c4 100644 --- a/web/projects/ui/src/app/app.component.scss +++ b/web/projects/ui/src/app/app.component.scss @@ -9,11 +9,15 @@ tui-root { .left-menu { --side-max-width: 280px; + + &_hidden { + display: none; + } } .menu { :host-context(body[data-theme='Light']) & { - --ion-color-base: #F4F4F5 !important; + --ion-color-base: #f4f4f5 !important; } } diff --git a/web/projects/ui/src/app/app.component.ts b/web/projects/ui/src/app/app.component.ts index 1210eba7a..ddf8c074f 100644 --- a/web/projects/ui/src/app/app.component.ts +++ b/web/projects/ui/src/app/app.component.ts @@ -1,4 +1,5 @@ import { Component, inject, OnDestroy } from '@angular/core' +import { IsActiveMatchOptions, Router } from '@angular/router' import { combineLatest, map, merge, startWith } from 'rxjs' import { AuthService } from './services/auth.service' import { SplitPaneTracker } from './services/split-pane.service' @@ -15,6 +16,13 @@ import { THEME } from '@start9labs/shared' import { PatchDB } from 'patch-db-client' import { DataModel } from './services/patch-db/data-model' +const OPTIONS: IsActiveMatchOptions = { + paths: 'subset', + queryParams: 'exact', + fragment: 'ignored', + matrixParams: 'ignored', +} + @Component({ selector: 'app-root', templateUrl: 'app.component.html', @@ -27,7 +35,7 @@ export class AppComponent implements OnDestroy { readonly theme$ = inject(THEME) readonly offline$ = combineLatest([ this.authService.isVerified$, - this.connection.connected$, + this.connection$, this.patch .watch$('serverInfo', 'statusInfo') .pipe(startWith({ restarting: false, shuttingDown: false })), @@ -44,8 +52,9 @@ export class AppComponent implements OnDestroy { private readonly patchMonitor: PatchMonitorService, private readonly splitPane: SplitPaneTracker, private readonly patch: PatchDB, + private readonly router: Router, readonly authService: AuthService, - readonly connection: ConnectionService, + readonly connection$: ConnectionService, readonly clientStorageService: ClientStorageService, readonly themeSwitcher: ThemeSwitcherService, ) {} @@ -56,6 +65,13 @@ export class AppComponent implements OnDestroy { .subscribe(name => this.titleService.setTitle(name || 'StartOS')) } + get withoutMenu(): boolean { + return ( + this.router.isActive('initializing', OPTIONS) || + this.router.isActive('diagnostic', OPTIONS) + ) + } + splitPaneVisible({ detail }: any) { this.splitPane.sidebarOpen$.next(detail.visible) } diff --git a/web/projects/ui/src/app/app.module.ts b/web/projects/ui/src/app/app.module.ts index 324300851..c0264f064 100644 --- a/web/projects/ui/src/app/app.module.ts +++ b/web/projects/ui/src/app/app.module.ts @@ -1,4 +1,5 @@ import { + TuiAlertModule, TuiDialogModule, TuiModeModule, TuiRootModule, @@ -58,6 +59,7 @@ import { environment } from '../environments/environment' ConnectionBarComponentModule, TuiRootModule, TuiDialogModule, + TuiAlertModule, TuiModeModule, TuiThemeNightModule, WidgetsPageModule, diff --git a/web/projects/ui/src/app/app.providers.ts b/web/projects/ui/src/app/app.providers.ts index 5d0ccc4f2..bf26a8cb9 100644 --- a/web/projects/ui/src/app/app.providers.ts +++ b/web/projects/ui/src/app/app.providers.ts @@ -10,6 +10,7 @@ import { AuthService } from './services/auth.service' import { ClientStorageService } from './services/client-storage.service' import { FilterPackagesPipe } from '../../../marketplace/src/pipes/filter-packages.pipe' import { ThemeSwitcherService } from './services/theme-switcher.service' +import { StorageService } from './services/storage.service' const { useMocks, @@ -30,7 +31,7 @@ export const APP_PROVIDERS: Provider[] = [ }, { provide: APP_INITIALIZER, - deps: [AuthService, ClientStorageService, Router], + deps: [StorageService, AuthService, ClientStorageService, Router], useFactory: appInitializer, multi: true, }, @@ -45,13 +46,15 @@ export const APP_PROVIDERS: Provider[] = [ ] export function appInitializer( + storage: StorageService, auth: AuthService, localStorage: ClientStorageService, router: Router, ): () => void { return () => { + storage.migrate036() auth.init() - localStorage.init() + localStorage.init() // @TODO pretty sure we can navigate before this step router.initialNavigation() } } diff --git a/web/projects/ui/src/app/app/menu/menu.component.ts b/web/projects/ui/src/app/app/menu/menu.component.ts index 5c1fbe8bd..b2ab62368 100644 --- a/web/projects/ui/src/app/app/menu/menu.component.ts +++ b/web/projects/ui/src/app/app/menu/menu.component.ts @@ -70,7 +70,7 @@ export class MenuComponent { readonly showEOSUpdate$ = this.eosService.showUpdate$ - private readonly local$ = this.connectionService.connected$.pipe( + private readonly local$ = this.connection$.pipe( filter(Boolean), switchMap(() => this.patch.watch$('packageData').pipe(first())), switchMap(outer => @@ -126,6 +126,6 @@ export class MenuComponent { private readonly marketplaceService: MarketplaceService, private readonly splitPane: SplitPaneTracker, private readonly emver: Emver, - private readonly connectionService: ConnectionService, + private readonly connection$: ConnectionService, ) {} } diff --git a/web/projects/ui/src/app/components/connection-bar/connection-bar.component.ts b/web/projects/ui/src/app/components/connection-bar/connection-bar.component.ts index da28f805f..cf0eab598 100644 --- a/web/projects/ui/src/app/components/connection-bar/connection-bar.component.ts +++ b/web/projects/ui/src/app/components/connection-bar/connection-bar.component.ts @@ -1,8 +1,9 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' import { PatchDB } from 'patch-db-client' import { combineLatest, map, Observable, startWith } from 'rxjs' -import { ConnectionService } from 'src/app/services/connection.service' +import { NetworkService } from 'src/app/services/network.service' import { DataModel } from 'src/app/services/patch-db/data-model' +import { StateService } from 'src/app/services/state.service' @Component({ selector: 'connection-bar', @@ -11,16 +12,14 @@ import { DataModel } from 'src/app/services/patch-db/data-model' changeDetection: ChangeDetectionStrategy.OnPush, }) export class ConnectionBarComponent { - private readonly websocket$ = this.connectionService.websocketConnected$ - readonly connection$: Observable<{ message: string color: string icon: string dots: boolean }> = combineLatest([ - this.connectionService.networkConnected$, - this.websocket$.pipe(startWith(false)), + this.network$, + this.state$.pipe(map(Boolean)), this.patch .watch$('serverInfo', 'statusInfo') .pipe(startWith({ restarting: false, shuttingDown: false })), @@ -65,7 +64,8 @@ export class ConnectionBarComponent { ) constructor( - private readonly connectionService: ConnectionService, + private readonly network$: NetworkService, + private readonly state$: StateService, private readonly patch: PatchDB, ) {} } diff --git a/web/projects/ui/src/app/components/logs/logs.component.ts b/web/projects/ui/src/app/components/logs/logs.component.ts index 5d033fc83..3d31313cb 100644 --- a/web/projects/ui/src/app/components/logs/logs.component.ts +++ b/web/projects/ui/src/app/components/logs/logs.component.ts @@ -11,7 +11,6 @@ import { takeUntil, tap, } from 'rxjs' -import { WebSocketSubjectConfig } from 'rxjs/webSocket' import { LogsRes, ServerLogsReq, @@ -72,7 +71,7 @@ export class LogsComponent { private readonly api: ApiService, private readonly loadingCtrl: LoadingController, private readonly downloadHtml: DownloadHTMLService, - private readonly connectionService: ConnectionService, + private readonly connection$: ConnectionService, ) {} async ngOnInit() { @@ -149,43 +148,42 @@ export class LogsComponent { private reconnect$(): Observable { return from(this.followLogs({})).pipe( tap(_ => this.recordConnectionChange()), - switchMap(({ guid }) => this.connect$(guid, true)), + switchMap(({ guid }) => this.connect$(guid)), ) } - private connect$(guid: string, reconnect = false) { - const config: WebSocketSubjectConfig = { - url: `/rpc/${guid}`, - openObserver: { - next: () => { - this.websocketStatus = 'connected' + private connect$(guid: string) { + return this.api + .openWebsocket$(guid, { + openObserver: { + next: () => { + this.websocketStatus = 'connected' + }, }, - }, - } - - return this.api.openLogsWebsocket$(config).pipe( - tap(_ => this.count++), - bufferTime(1000), - tap(msgs => { - this.loading = false - this.processRes({ entries: msgs }) - if (this.infiniteStatus === 0 && this.count >= this.limit) - this.infiniteStatus = 1 - }), - catchError(() => { - this.recordConnectionChange(false) - return this.connectionService.connected$.pipe( - tap( - connected => - (this.websocketStatus = connected - ? 'reconnecting' - : 'disconnected'), - ), - filter(Boolean), - switchMap(() => this.reconnect$()), - ) - }), - ) + }) + .pipe( + tap(_ => this.count++), + bufferTime(1000), + tap(msgs => { + this.loading = false + this.processRes({ entries: msgs }) + if (this.infiniteStatus === 0 && this.count >= this.limit) + this.infiniteStatus = 1 + }), + catchError(() => { + this.recordConnectionChange(false) + return this.connection$.pipe( + tap( + connected => + (this.websocketStatus = connected + ? 'reconnecting' + : 'disconnected'), + ), + filter(Boolean), + switchMap(() => this.reconnect$()), + ) + }), + ) } private recordConnectionChange(success = true) { diff --git a/web/projects/ui/src/app/components/status/status.component.html b/web/projects/ui/src/app/components/status/status.component.html index db9e8f8f7..65c142f4a 100644 --- a/web/projects/ui/src/app/components/status/status.component.html +++ b/web/projects/ui/src/app/components/status/status.component.html @@ -1,12 +1,12 @@

- {{ (connected$ | async) ? rendering.display : 'Unknown' }} + {{ (connection$ | async) ? rendering.display : 'Unknown' }} . This may take a while diff --git a/web/projects/ui/src/app/components/status/status.component.ts b/web/projects/ui/src/app/components/status/status.component.ts index c9fec4968..45f66f291 100644 --- a/web/projects/ui/src/app/components/status/status.component.ts +++ b/web/projects/ui/src/app/components/status/status.component.ts @@ -21,7 +21,5 @@ export class StatusComponent { @Input() installingInfo?: InstallingInfo @Input() sigtermTimeout?: string | null = null - readonly connected$ = this.connectionService.connected$ - - constructor(private readonly connectionService: ConnectionService) {} + constructor(readonly connection$: ConnectionService) {} } diff --git a/web/projects/ui/src/app/modals/os-update/os-update.page.ts b/web/projects/ui/src/app/modals/os-update/os-update.page.ts index 9c49b72bf..9900bbb47 100644 --- a/web/projects/ui/src/app/modals/os-update/os-update.page.ts +++ b/web/projects/ui/src/app/modals/os-update/os-update.page.ts @@ -20,9 +20,8 @@ export class OSUpdatePage { private readonly embassyApi: ApiService, private readonly eosService: EOSService, ) {} - ngOnInit() { - const releaseNotes = this.eosService.eos?.releaseNotes! + const releaseNotes = this.eosService.osUpdate?.releaseNotes! this.versions = Object.keys(releaseNotes) .sort() diff --git a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-icon/app-list-icon.component.html b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-icon/app-list-icon.component.html index 8f1af1470..b0bfbbb24 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-icon/app-list-icon.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-icon/app-list-icon.component.html @@ -1,4 +1,4 @@ - + Health Checks - + diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.ts index f3db08063..fef84a5ba 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.ts @@ -12,9 +12,7 @@ export class AppShowHealthChecksComponent { @Input() healthChecks!: Record - readonly connected$ = this.connectionService.connected$ - - constructor(private readonly connectionService: ConnectionService) {} + constructor(readonly connection$: ConnectionService) {} isLoading(result: T.HealthCheckResult['result']): boolean { return result === 'starting' || result === 'loading' diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html index f9db34c73..be7bc1c30 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html @@ -11,7 +11,7 @@ - + diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts index 4bc1bc464..ba60c331d 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts @@ -39,8 +39,6 @@ export class AppShowStatusComponent { isInstalled = isInstalled - readonly connected$ = this.connectionService.connected$ - constructor( private readonly alertCtrl: AlertController, private readonly errToast: ErrorToastService, @@ -48,7 +46,7 @@ export class AppShowStatusComponent { private readonly embassyApi: ApiService, private readonly launcherService: UiLauncherService, private readonly modalService: ModalService, - private readonly connectionService: ConnectionService, + readonly connection$: ConnectionService, private readonly patch: PatchDB, ) {} diff --git a/web/projects/ui/src/app/pages/diagnostic-routes/diagnostic-routing.module.ts b/web/projects/ui/src/app/pages/diagnostic-routes/diagnostic-routing.module.ts new file mode 100644 index 000000000..4409288c1 --- /dev/null +++ b/web/projects/ui/src/app/pages/diagnostic-routes/diagnostic-routing.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core' +import { RouterModule, Routes } from '@angular/router' + +const ROUTES: Routes = [ + { + path: '', + loadChildren: () => + import('./home/home.module').then(m => m.HomePageModule), + }, + { + path: 'logs', + loadChildren: () => + import('./logs/logs.module').then(m => m.LogsPageModule), + }, +] + +@NgModule({ + imports: [RouterModule.forChild(ROUTES)], + exports: [RouterModule], +}) +export class DiagnosticModule {} diff --git a/web/projects/diagnostic-ui/src/app/pages/home/home.module.ts b/web/projects/ui/src/app/pages/diagnostic-routes/home/home.module.ts similarity index 55% rename from web/projects/diagnostic-ui/src/app/pages/home/home.module.ts rename to web/projects/ui/src/app/pages/diagnostic-routes/home/home.module.ts index 1664b7c72..9565220ae 100644 --- a/web/projects/diagnostic-ui/src/app/pages/home/home.module.ts +++ b/web/projects/ui/src/app/pages/diagnostic-routes/home/home.module.ts @@ -1,12 +1,18 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' +import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' -import { FormsModule } from '@angular/forms' import { HomePage } from './home.page' -import { HomePageRoutingModule } from './home-routing.module' + +const routes: Routes = [ + { + path: '', + component: HomePage, + }, +] @NgModule({ - imports: [CommonModule, FormsModule, IonicModule, HomePageRoutingModule], + imports: [CommonModule, IonicModule, RouterModule.forChild(routes)], declarations: [HomePage], }) export class HomePageModule {} diff --git a/web/projects/diagnostic-ui/src/app/pages/home/home.page.html b/web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.html similarity index 92% rename from web/projects/diagnostic-ui/src/app/pages/home/home.page.html rename to web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.html index 9cba08258..69a58a3aa 100644 --- a/web/projects/diagnostic-ui/src/app/pages/home/home.page.html +++ b/web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.html @@ -51,12 +51,6 @@ }} -

- - System Rebuild - -
-
Repair Drive diff --git a/web/projects/diagnostic-ui/src/app/pages/home/home.page.scss b/web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.scss similarity index 100% rename from web/projects/diagnostic-ui/src/app/pages/home/home.page.scss rename to web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.scss diff --git a/web/projects/diagnostic-ui/src/app/pages/home/home.page.ts b/web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.ts similarity index 74% rename from web/projects/diagnostic-ui/src/app/pages/home/home.page.ts rename to web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.ts index bbda6939f..9bb7376bc 100644 --- a/web/projects/diagnostic-ui/src/app/pages/home/home.page.ts +++ b/web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.ts @@ -1,9 +1,9 @@ import { Component } from '@angular/core' import { AlertController, LoadingController } from '@ionic/angular' -import { ApiService } from 'src/app/services/api/api.service' +import { ApiService } from 'src/app/services/api/embassy-api.service' @Component({ - selector: 'app-home', + selector: 'diagnostic-home', templateUrl: 'home.page.html', styleUrls: ['home.page.scss'], }) @@ -25,7 +25,7 @@ export class HomePage { async ngOnInit() { try { - const error = await this.api.getError() + const error = await this.api.diagnosticGetError() // incorrect drive if (error.code === 15) { this.error = { @@ -92,7 +92,7 @@ export class HomePage { await loader.present() try { - await this.api.restart() + await this.api.diagnosticRestart() this.restarted = true } catch (e) { console.error(e) @@ -108,8 +108,8 @@ export class HomePage { await loader.present() try { - await this.api.forgetDrive() - await this.api.restart() + await this.api.diagnosticForgetDrive() + await this.api.diagnosticRestart() this.restarted = true } catch (e) { console.error(e) @@ -118,32 +118,6 @@ export class HomePage { } } - async presentAlertSystemRebuild() { - const alert = await this.alertCtrl.create({ - header: 'Warning', - message: - '

This action will tear down all service containers and rebuild them from scratch. No data will be deleted.

A system rebuild can be useful if your system gets into a bad state, and it should only be performed if you are experiencing general performance or reliability issues.

It may take up to an hour to complete. During this time, you will lose all connectivity to your Start9 server.

', - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Rebuild', - handler: () => { - try { - this.systemRebuild() - } catch (e) { - console.error(e) - } - }, - }, - ], - cssClass: 'alert-warning-message', - }) - await alert.present() - } - async presentAlertRepairDisk() { const alert = await this.alertCtrl.create({ header: 'Warning', @@ -174,23 +148,6 @@ export class HomePage { window.location.reload() } - private async systemRebuild(): Promise { - const loader = await this.loadingCtrl.create({ - cssClass: 'loader', - }) - await loader.present() - - try { - await this.api.systemRebuild() - await this.api.restart() - this.restarted = true - } catch (e) { - console.error(e) - } finally { - loader.dismiss() - } - } - private async repairDisk(): Promise { const loader = await this.loadingCtrl.create({ cssClass: 'loader', @@ -198,8 +155,8 @@ export class HomePage { await loader.present() try { - await this.api.repairDisk() - await this.api.restart() + await this.api.diagnosticRepairDisk() + await this.api.diagnosticRestart() this.restarted = true } catch (e) { console.error(e) diff --git a/web/projects/diagnostic-ui/src/app/pages/logs/logs.module.ts b/web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.module.ts similarity index 100% rename from web/projects/diagnostic-ui/src/app/pages/logs/logs.module.ts rename to web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.module.ts diff --git a/web/projects/diagnostic-ui/src/app/pages/logs/logs.page.html b/web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.page.html similarity index 100% rename from web/projects/diagnostic-ui/src/app/pages/logs/logs.page.html rename to web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.page.html diff --git a/web/projects/diagnostic-ui/src/app/pages/logs/logs.page.scss b/web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.page.scss similarity index 100% rename from web/projects/diagnostic-ui/src/app/pages/logs/logs.page.scss rename to web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.page.scss diff --git a/web/projects/diagnostic-ui/src/app/pages/logs/logs.page.ts b/web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.page.ts similarity index 93% rename from web/projects/diagnostic-ui/src/app/pages/logs/logs.page.ts rename to web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.page.ts index 44c314a96..5119b9d93 100644 --- a/web/projects/diagnostic-ui/src/app/pages/logs/logs.page.ts +++ b/web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.page.ts @@ -1,6 +1,6 @@ import { Component, ViewChild } from '@angular/core' import { IonContent } from '@ionic/angular' -import { ApiService } from 'src/app/services/api/api.service' +import { ApiService } from 'src/app/services/api/embassy-api.service' import { ErrorToastService, toLocalIsoString } from '@start9labs/shared' var Convert = require('ansi-to-html') @@ -49,7 +49,7 @@ export class LogsPage { private async getLogs() { try { - const { startCursor, entries } = await this.api.getLogs({ + const { startCursor, entries } = await this.api.diagnosticGetLogs({ cursor: this.startCursor, before: !!this.startCursor, limit: this.limit, diff --git a/web/projects/ui/src/app/pages/init/init.module.ts b/web/projects/ui/src/app/pages/init/init.module.ts new file mode 100644 index 000000000..07dd71185 --- /dev/null +++ b/web/projects/ui/src/app/pages/init/init.module.ts @@ -0,0 +1,24 @@ +import { CommonModule } from '@angular/common' +import { NgModule } from '@angular/core' +import { RouterModule, Routes } from '@angular/router' +import { TuiProgressModule } from '@taiga-ui/kit' +import { LogsModule } from 'src/app/pages/init/logs/logs.module' +import { InitPage } from './init.page' + +const routes: Routes = [ + { + path: '', + component: InitPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + LogsModule, + TuiProgressModule, + RouterModule.forChild(routes), + ], + declarations: [InitPage], +}) +export class InitPageModule {} diff --git a/web/projects/ui/src/app/pages/init/init.page.html b/web/projects/ui/src/app/pages/init/init.page.html new file mode 100644 index 000000000..bd3467bbb --- /dev/null +++ b/web/projects/ui/src/app/pages/init/init.page.html @@ -0,0 +1,18 @@ +
+

+ Initializing StartOS +

+
+ Progress: {{ (progress.total * 100).toFixed(0) }}% +
+ + +

+
+ diff --git a/web/projects/ui/src/app/pages/init/init.page.scss b/web/projects/ui/src/app/pages/init/init.page.scss new file mode 100644 index 000000000..9fbf7098a --- /dev/null +++ b/web/projects/ui/src/app/pages/init/init.page.scss @@ -0,0 +1,23 @@ +section { + border-radius: 0.25rem; + padding: 1rem; + margin: 1.5rem; + text-align: center; + /* TODO: Theme */ + background: #e0e0e0; + color: #333; + --tui-clear-inverse: rgba(0, 0, 0, 0.1); +} + +logs-window { + display: flex; + flex-direction: column; + height: 18rem; + padding: 1rem; + margin: 0 1.5rem auto; + text-align: left; + overflow: hidden; + border-radius: 2rem; + /* TODO: Theme */ + background: #181818; +} diff --git a/web/projects/ui/src/app/pages/init/init.page.ts b/web/projects/ui/src/app/pages/init/init.page.ts new file mode 100644 index 000000000..318881223 --- /dev/null +++ b/web/projects/ui/src/app/pages/init/init.page.ts @@ -0,0 +1,11 @@ +import { Component, inject } from '@angular/core' +import { InitService } from 'src/app/pages/init/init.service' + +@Component({ + selector: 'init-page', + templateUrl: 'init.page.html', + styleUrls: ['init.page.scss'], +}) +export class InitPage { + readonly progress$ = inject(InitService) +} diff --git a/web/projects/ui/src/app/pages/init/init.service.ts b/web/projects/ui/src/app/pages/init/init.service.ts new file mode 100644 index 000000000..3cca42a58 --- /dev/null +++ b/web/projects/ui/src/app/pages/init/init.service.ts @@ -0,0 +1,91 @@ +import { inject, Injectable } from '@angular/core' +import { ErrorToastService } from '@start9labs/shared' +import { T } from '@start9labs/start-sdk' +import { + catchError, + defer, + EMPTY, + from, + map, + Observable, + startWith, + switchMap, + tap, +} from 'rxjs' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { StateService } from 'src/app/services/state.service' + +interface MappedProgress { + readonly total: number | null + readonly message: string +} + +@Injectable({ providedIn: 'root' }) +export class InitService extends Observable { + private readonly state = inject(StateService) + private readonly api = inject(ApiService) + private readonly errorService = inject(ErrorToastService) + private readonly progress$ = defer(() => + from(this.api.initGetProgress()), + ).pipe( + switchMap(({ guid, progress }) => + this.api + .openWebsocket$(guid, {}) + .pipe(startWith(progress)), + ), + map(({ phases, overall }) => { + return { + total: getOverallDecimal(overall), + message: phases + .filter( + ( + p, + ): p is { + name: string + progress: { + done: number + total: number | null + } + } => p.progress !== true && p.progress !== null, + ) + .map(p => `${p.name}${getPhaseBytes(p.progress)}`) + .join(', '), + } + }), + tap(({ total }) => { + if (total === 1) { + this.state.syncState() + } + }), + catchError(e => { + this.errorService.present(e) + + return EMPTY + }), + ) + + constructor() { + super(subscriber => this.progress$.subscribe(subscriber)) + } +} + +function getOverallDecimal(progress: T.Progress): number { + if (progress === true) { + return 1 + } else if (!progress || !progress.total) { + return 0 + } else { + return progress.total && progress.done / progress.total + } +} + +function getPhaseBytes( + progress: + | false + | { + done: number + total: number | null + }, +): string { + return progress === false ? '' : `: (${progress.done}/${progress.total})` +} diff --git a/web/projects/ui/src/app/pages/init/logs/logs.component.ts b/web/projects/ui/src/app/pages/init/logs/logs.component.ts new file mode 100644 index 000000000..edce1f282 --- /dev/null +++ b/web/projects/ui/src/app/pages/init/logs/logs.component.ts @@ -0,0 +1,33 @@ +import { Component, ElementRef, inject } from '@angular/core' +import { INTERSECTION_ROOT } from '@ng-web-apis/intersection-observer' +import { LogsService } from 'src/app/pages/init/logs/logs.service' + +@Component({ + selector: 'logs-window', + templateUrl: 'logs.template.html', + styles: [ + ` + pre { + margin: 0; + } + `, + ], + providers: [ + { + provide: INTERSECTION_ROOT, + useExisting: ElementRef, + }, + ], +}) +export class LogsComponent { + readonly logs$ = inject(LogsService) + scroll = true + + scrollTo(bottom: HTMLElement) { + if (this.scroll) bottom.scrollIntoView({ behavior: 'smooth' }) + } + + onBottom([{ isIntersecting }]: readonly IntersectionObserverEntry[]) { + this.scroll = isIntersecting + } +} diff --git a/web/projects/ui/src/app/pages/init/logs/logs.module.ts b/web/projects/ui/src/app/pages/init/logs/logs.module.ts new file mode 100644 index 000000000..ee4a1bc1d --- /dev/null +++ b/web/projects/ui/src/app/pages/init/logs/logs.module.ts @@ -0,0 +1,20 @@ +import { CommonModule } from '@angular/common' +import { NgModule } from '@angular/core' +import { IntersectionObserverModule } from '@ng-web-apis/intersection-observer' +import { MutationObserverModule } from '@ng-web-apis/mutation-observer' +import { TuiScrollbarModule } from '@taiga-ui/core' +import { NgDompurifyModule } from '@tinkoff/ng-dompurify' +import { LogsComponent } from './logs.component' + +@NgModule({ + imports: [ + CommonModule, + MutationObserverModule, + IntersectionObserverModule, + NgDompurifyModule, + TuiScrollbarModule, + ], + declarations: [LogsComponent], + exports: [LogsComponent], +}) +export class LogsModule {} diff --git a/web/projects/ui/src/app/pages/init/logs/logs.service.ts b/web/projects/ui/src/app/pages/init/logs/logs.service.ts new file mode 100644 index 000000000..e06d56b42 --- /dev/null +++ b/web/projects/ui/src/app/pages/init/logs/logs.service.ts @@ -0,0 +1,49 @@ +import { inject, Injectable } from '@angular/core' +import { Log, toLocalIsoString } from '@start9labs/shared' +import { + bufferTime, + defer, + filter, + map, + Observable, + scan, + switchMap, +} from 'rxjs' +import { ApiService } from 'src/app/services/api/embassy-api.service' + +var Convert = require('ansi-to-html') +var convert = new Convert({ + newline: true, + bg: 'transparent', + colors: { + 4: 'Cyan', + }, + escapeXML: true, +}) + +function convertAnsi(entries: readonly any[]): string { + return entries + .map( + ({ timestamp, message }) => + `${toLocalIsoString( + new Date(timestamp), + )}  ${convert.toHtml(message)}`, + ) + .join('
') +} + +@Injectable({ providedIn: 'root' }) +export class LogsService extends Observable { + private readonly api = inject(ApiService) + private readonly log$ = defer(() => this.api.initFollowLogs({})).pipe( + switchMap(({ guid }) => this.api.openWebsocket$(guid, {})), + bufferTime(250), + filter(logs => !!logs.length), + map(convertAnsi), + scan((logs: readonly string[], log) => [...logs, log], []), + ) + + constructor() { + super(subscriber => this.log$.subscribe(subscriber)) + } +} diff --git a/web/projects/ui/src/app/pages/init/logs/logs.template.html b/web/projects/ui/src/app/pages/init/logs/logs.template.html new file mode 100644 index 000000000..24ea6d0c1 --- /dev/null +++ b/web/projects/ui/src/app/pages/init/logs/logs.template.html @@ -0,0 +1,9 @@ + +

+  
+
diff --git a/web/projects/ui/src/app/pages/login/ca-wizard/ca-wizard.component.ts b/web/projects/ui/src/app/pages/login/ca-wizard/ca-wizard.component.ts index fde1c968f..2c0e9c7fe 100644 --- a/web/projects/ui/src/app/pages/login/ca-wizard/ca-wizard.component.ts +++ b/web/projects/ui/src/app/pages/login/ca-wizard/ca-wizard.component.ts @@ -42,7 +42,7 @@ export class CAWizardComponent { private async testHttps() { const url = `https://${this.document.location.host}${this.relativeUrl}` - await this.api.echo({ message: 'ping' }, url).then(() => { + await this.api.getState().then(() => { this.caTrusted = true }) } diff --git a/web/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.html b/web/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.html index 70b977ecc..859d48129 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.html +++ b/web/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.html @@ -74,7 +74,7 @@ Memory Percentage Used - {{ memory.percentageUsed }} % + {{ memory.percentageUsed.value }} % Total @@ -98,7 +98,7 @@ zram Total - {{ memory.zramTotal }} MiB + {{ memory.zramTotal.value }} MiB zram Available diff --git a/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts b/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts index 73763578e..f375b0e86 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts @@ -319,30 +319,6 @@ export class ServerShowPage { await alert.present() } - async presentAlertSystemRebuild() { - const localPkgs = await getAllPackages(this.patch) - const minutes = Object.keys(localPkgs).length * 2 - const alert = await this.alertCtrl.create({ - header: 'Warning', - message: `This action will tear down all service containers and rebuild them from scratch. No data will be deleted. This action is useful if your system gets into a bad state, and it should only be performed if you are experiencing general performance or reliability issues. It may take up to ${minutes} minutes to complete. During this time, you will lose all connectivity to your server.`, - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Rebuild', - handler: () => { - this.systemRebuild() - }, - cssClass: 'enter-click', - }, - ], - cssClass: 'alert-warning-message', - }) - await alert.present() - } - async presentAlertRepairDisk() { const alert = await this.alertCtrl.create({ header: 'Warning', @@ -437,23 +413,6 @@ export class ServerShowPage { } } - private async systemRebuild() { - const action = 'System Rebuild' - - const loader = await this.loadingCtrl.create({ - message: `Beginning ${action}...`, - }) - await loader.present() - - try { - await this.embassyApi.systemRebuild({}) - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.dismiss() - } - } - private async checkForEosUpdate(): Promise { const loader = await this.loadingCtrl.create({ message: 'Checking for updates', @@ -718,14 +677,6 @@ export class ServerShowPage { detail: false, disabled$: of(false), }, - { - title: 'System Rebuild', - description: '', - icon: 'construct-outline', - action: () => this.presentAlertSystemRebuild(), - detail: false, - disabled$: of(false), - }, { title: 'Repair Disk', description: '', diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index 5642eece5..38ee4774b 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -16,7 +16,7 @@ export module Mock { restarting: false, shuttingDown: false, } - export const MarketplaceEos: RR.GetMarketplaceEosRes = { + export const MarketplaceEos: RR.CheckOSUpdateRes = { version: '0.3.5.2', headline: 'Our biggest release ever.', releaseNotes: { @@ -493,30 +493,23 @@ export module Mock { { timestamp: '2022-07-28T03:52:54.808769Z', message: '****** START *****', + bootId: 'hsjnfdklasndhjasvbjamsksajbndjn', }, { timestamp: '2019-12-26T14:21:30.872Z', message: '\u001b[34mPOST \u001b[0;32;49m200\u001b[0m photoview.startos/api/graphql \u001b[0;36;49m1.169406ms\u001b', + bootId: 'hsjnfdklasndhjasvbjamsksajbndjn', }, { timestamp: '2019-12-26T14:22:30.872Z', message: '****** FINISH *****', - }, - ] - - export const PackageLogs: Log[] = [ - { - timestamp: '2022-07-28T03:52:54.808769Z', - message: '****** START *****', + bootId: 'gvbwfiuasokdasjndasnjdmfvbahjdmdkfm', }, { - timestamp: '2019-12-26T14:21:30.872Z', - message: 'PackageLogs PackageLogs PackageLogs PackageLogs PackageLogs', - }, - { - timestamp: '2019-12-26T14:22:30.872Z', - message: '****** FINISH *****', + timestamp: '2019-12-26T15:22:30.872Z', + message: '****** AGAIN *****', + bootId: 'gvbwfiuasokdasjndasnjdmfvbahjdmdkfm', }, ] diff --git a/web/projects/ui/src/app/services/api/api.types.ts b/web/projects/ui/src/app/services/api/api.types.ts index 3539164f5..a5c52e9a2 100644 --- a/web/projects/ui/src/app/services/api/api.types.ts +++ b/web/projects/ui/src/app/services/api/api.types.ts @@ -1,17 +1,28 @@ -import { Dump, Revision } from 'patch-db-client' +import { Dump } from 'patch-db-client' import { MarketplacePkg, StoreInfo } from '@start9labs/marketplace' import { PackagePropertiesVersioned } from 'src/app/util/properties.util' import { ConfigSpec } from 'src/app/pkg-config/config-types' import { DataModel } from 'src/app/services/patch-db/data-model' import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared' import { T } from '@start9labs/start-sdk' +import { WebSocketSubjectConfig } from 'rxjs/webSocket' export module RR { + // websocket + + export type WebsocketConfig = Omit, 'url'> + + // server state + + export type ServerState = 'initializing' | 'error' | 'running' + // DB - export type GetRevisionsRes = Revision[] | Dump - - export type GetDumpRes = Dump + export type SubscribePatchReq = {} + export type SubscribePatchRes = { + dump: Dump + guid: string + } export type SetDBValueReq = { pointer: string; value: T } // db.put.ui export type SetDBValueRes = null @@ -33,10 +44,22 @@ export module RR { } // auth.reset-password export type ResetPasswordRes = null - // server + // diagnostic - export type EchoReq = { message: string; timeout?: number } // server.echo - export type EchoRes = string + export type DiagnosticErrorRes = { + code: number + message: string + data: { details: string } + } + + // init + + export type InitGetProgressRes = { + progress: T.FullProgress + guid: string + } + + // server export type GetSystemTimeReq = {} // server.time export type GetSystemTimeRes = { @@ -65,8 +88,8 @@ export module RR { export type ShutdownServerReq = {} // server.shutdown export type ShutdownServerRes = null - export type SystemRebuildReq = {} // server.rebuild - export type SystemRebuildRes = null + export type DiskRepairReq = {} // server.disk.repair + export type DiskRepairRes = null export type ResetTorReq = { wipeState: boolean @@ -254,8 +277,8 @@ export module RR { export type GetMarketplaceInfoReq = { serverId: string } export type GetMarketplaceInfoRes = StoreInfo - export type GetMarketplaceEosReq = { serverId: string } - export type GetMarketplaceEosRes = MarketplaceEOS + export type CheckOSUpdateReq = { serverId: string } + export type CheckOSUpdateRes = OSUpdate export type GetMarketplacePackagesReq = { ids?: { id: string; version: string }[] @@ -271,7 +294,7 @@ export module RR { export type GetReleaseNotesRes = { [version: string]: string } } -export interface MarketplaceEOS { +export interface OSUpdate { version: string headline: string releaseNotes: { [version: string]: string } diff --git a/web/projects/ui/src/app/services/api/embassy-api.service.ts b/web/projects/ui/src/app/services/api/embassy-api.service.ts index 3f1d9881d..d6bb11632 100644 --- a/web/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-api.service.ts @@ -1,9 +1,5 @@ import { Observable } from 'rxjs' -import { Update } from 'patch-db-client' import { RR } from './api.types' -import { DataModel } from 'src/app/services/patch-db/data-model' -import { Log } from '@start9labs/shared' -import { WebSocketSubjectConfig } from 'rxjs/webSocket' export abstract class ApiService { // http @@ -14,8 +10,23 @@ export abstract class ApiService { // for sideloading packages abstract uploadPackage(guid: string, body: Blob): Promise + // websocket + + abstract openWebsocket$( + guid: string, + config: RR.WebsocketConfig, + ): Observable + + // server state + + abstract getState(): Promise + // db + abstract subscribeToPatchDB( + params: RR.SubscribePatchReq, + ): Promise + abstract setDbValue( pathArr: Array, value: T, @@ -35,16 +46,26 @@ export abstract class ApiService { params: RR.ResetPasswordReq, ): Promise + // diagnostic + + abstract diagnosticGetError(): Promise + abstract diagnosticRestart(): Promise + abstract diagnosticForgetDrive(): Promise + abstract diagnosticRepairDisk(): Promise + abstract diagnosticGetLogs( + params: RR.GetServerLogsReq, + ): Promise + + // init + + abstract initGetProgress(): Promise + + abstract initFollowLogs( + params: RR.FollowServerLogsReq, + ): Promise + // server - abstract echo(params: RR.EchoReq, urlOverride?: string): Promise - - abstract openPatchWebsocket$(): Observable> - - abstract openLogsWebsocket$( - config: WebSocketSubjectConfig, - ): Observable - abstract getSystemTime( params: RR.GetSystemTimeReq, ): Promise @@ -89,11 +110,7 @@ export abstract class ApiService { params: RR.ShutdownServerReq, ): Promise - abstract systemRebuild( - params: RR.SystemRebuildReq, - ): Promise - - abstract repairDisk(params: RR.SystemRebuildReq): Promise + abstract repairDisk(params: RR.DiskRepairReq): Promise abstract resetTor(params: RR.ResetTorReq): Promise @@ -105,7 +122,7 @@ export abstract class ApiService { url: string, ): Promise - abstract getEos(): Promise + abstract checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise // notification diff --git a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts index 631c12e6b..8826b2883 100644 --- a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -3,7 +3,6 @@ import { HttpOptions, HttpService, isRpcError, - Log, Method, RpcError, RPCOptions, @@ -12,13 +11,12 @@ import { ApiService } from './embassy-api.service' import { RR } from './api.types' import { parsePropertiesPermissive } from 'src/app/util/properties.util' import { ConfigService } from '../config.service' -import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket' +import { webSocket } from 'rxjs/webSocket' import { Observable, filter, firstValueFrom } from 'rxjs' import { AuthService } from '../auth.service' import { DOCUMENT } from '@angular/common' import { DataModel } from '../patch-db/data-model' -import { PatchDB, pathFromArray, Update } from 'patch-db-client' -import { getServerInfo } from 'src/app/util/get-server-info' +import { PatchDB, pathFromArray } from 'patch-db-client' @Injectable() export class LiveApiService extends ApiService { @@ -30,10 +28,11 @@ export class LiveApiService extends ApiService { private readonly patch: PatchDB, ) { super() - ;(window as any).rpcClient = this + ; (window as any).rpcClient = this } // for getting static files: ex icons, instructions, licenses + async getStatic(url: string): Promise { return this.httpRequest({ method: Method.GET, @@ -43,6 +42,7 @@ export class LiveApiService extends ApiService { } // for sideloading packages + async uploadPackage(guid: string, body: Blob): Promise { return this.httpRequest({ method: Method.POST, @@ -52,8 +52,36 @@ export class LiveApiService extends ApiService { }) } + // websocket + + openWebsocket$( + guid: string, + config: RR.WebsocketConfig, + ): Observable { + const { location } = this.document.defaultView! + const protocol = location.protocol === 'http:' ? 'ws' : 'wss' + const host = location.host + + return webSocket({ + url: `${protocol}://${host}/ws/rpc/${guid}`, + ...config, + }) + } + + // state + + async getState(): Promise { + return this.rpcRequest({ method: 'state', params: {} }) + } + // db + async subscribeToPatchDB( + params: RR.SubscribePatchReq, + ): Promise { + return this.rpcRequest({ method: 'db.subscribe', params }) + } + async setDbValue( pathArr: Array, value: T, @@ -87,29 +115,57 @@ export class LiveApiService extends ApiService { return this.rpcRequest({ method: 'auth.reset-password', params }) } + // diagnostic + + async diagnosticGetError(): Promise { + return this.rpcRequest({ + method: 'diagnostic.error', + params: {}, + }) + } + + async diagnosticRestart(): Promise { + return this.rpcRequest({ + method: 'diagnostic.restart', + params: {}, + }) + } + + async diagnosticForgetDrive(): Promise { + return this.rpcRequest({ + method: 'diagnostic.disk.forget', + params: {}, + }) + } + + async diagnosticRepairDisk(): Promise { + return this.rpcRequest({ + method: 'diagnostic.disk.repair', + params: {}, + }) + } + + async diagnosticGetLogs( + params: RR.GetServerLogsReq, + ): Promise { + return this.rpcRequest({ + method: 'diagnostic.logs', + params, + }) + } + + // init + + async initGetProgress(): Promise { + return this.rpcRequest({ method: 'init.subscribe', params: {} }) + } + + async initFollowLogs(): Promise { + return this.rpcRequest({ method: 'init.logs.follow', params: {} }) + } + // server - async echo(params: RR.EchoReq, urlOverride?: string): Promise { - return this.rpcRequest({ method: 'echo', params }, urlOverride) - } - - openPatchWebsocket$(): Observable> { - const config: WebSocketSubjectConfig> = { - url: `/db`, - closeObserver: { - next: val => { - if (val.reason === 'UNAUTHORIZED') this.auth.setUnverified() - }, - }, - } - - return this.openWebsocket(config) - } - - openLogsWebsocket$(config: WebSocketSubjectConfig): Observable { - return this.openWebsocket(config) - } - async getSystemTime( params: RR.GetSystemTimeReq, ): Promise { @@ -175,12 +231,6 @@ export class LiveApiService extends ApiService { return this.rpcRequest({ method: 'server.shutdown', params }) } - async systemRebuild( - params: RR.RestartServerReq, - ): Promise { - return this.rpcRequest({ method: 'server.rebuild', params }) - } - async repairDisk(params: RR.RestartServerReq): Promise { return this.rpcRequest({ method: 'disk.repair', params }) } @@ -203,10 +253,7 @@ export class LiveApiService extends ApiService { }) } - async getEos(): Promise { - const { id } = await getServerInfo(this.patch) - const qp: RR.GetMarketplaceEosReq = { serverId: id } - + async checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise { return this.marketplaceProxy( '/eos/v0/latest', qp, @@ -417,16 +464,6 @@ export class LiveApiService extends ApiService { }) } - private openWebsocket(config: WebSocketSubjectConfig): Observable { - const { location } = this.document.defaultView! - const protocol = location.protocol === 'http:' ? 'ws' : 'wss' - const host = location.host - - config.url = `${protocol}://${host}/ws${config.url}` - - return webSocket(config) - } - private async rpcRequest( options: RPCOptions, urlOverride?: string, @@ -445,9 +482,7 @@ export class LiveApiService extends ApiService { const patchSequence = res.headers.get('x-patch-sequence') if (patchSequence) await firstValueFrom( - this.patch.cache$.pipe( - filter(({ sequence }) => sequence >= Number(patchSequence)), - ), + this.patch.cache$.pipe(filter(({ id }) => id >= Number(patchSequence))), ) return body.result diff --git a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts index f623f6db3..728b4ff35 100644 --- a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -1,15 +1,14 @@ import { Injectable } from '@angular/core' -import { Log, pauseFor } from '@start9labs/shared' +import { Log, RPCErrorDetails, pauseFor } from '@start9labs/shared' import { ApiService } from './embassy-api.service' import { Operation, PatchOp, pathFromArray, RemoveOperation, - Update, + Revision, } from 'patch-db-client' import { - DataModel, InstallingState, PackageDataEntry, StateInfo, @@ -20,22 +19,17 @@ import { parsePropertiesPermissive } from 'src/app/util/properties.util' import { Mock } from './api.fixures' import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md' import { - EMPTY, - iif, + from, interval, map, Observable, shareReplay, + startWith, Subject, - switchMap, tap, - timer, } from 'rxjs' -import { LocalStorageBootstrap } from '../patch-db/local-storage-bootstrap' import { mockPatchData } from './mock-patch' -import { WebSocketSubjectConfig } from 'rxjs/webSocket' import { AuthService } from '../auth.service' -import { ConnectionService } from '../connection.service' import { StoreInfo } from '@start9labs/marketplace' import { T } from '@start9labs/start-sdk' @@ -71,32 +65,17 @@ const PROGRESS: T.FullProgress = { @Injectable() export class MockApiService extends ApiService { - readonly mockWsSource$ = new Subject>() + readonly mockWsSource$ = new Subject() private readonly revertTime = 1800 sequence = 0 - constructor( - private readonly bootstrapper: LocalStorageBootstrap, - private readonly connectionService: ConnectionService, - private readonly auth: AuthService, - ) { + constructor(private readonly auth: AuthService) { super() this.auth.isVerified$ .pipe( tap(() => { this.sequence = 0 }), - switchMap(verified => - iif( - () => verified, - timer(2000).pipe( - tap(() => { - this.connectionService.websocketConnected$.next(true) - }), - ), - EMPTY, - ), - ), ) .subscribe() } @@ -111,8 +90,57 @@ export class MockApiService extends ApiService { return 'success' } + // websocket + + openWebsocket$( + guid: string, + config: RR.WebsocketConfig, + ): Observable { + if (guid === 'db-guid') { + return this.mockWsSource$.pipe( + shareReplay({ bufferSize: 1, refCount: true }), + ) + } else if (guid === 'logs-guid') { + return interval(50).pipe( + map((_, index) => { + // mock fire open observer + if (index === 0) config.openObserver?.next(new Event('')) + if (index === 100) throw new Error('HAAHHA') + return Mock.ServerLogs[0] + }), + ) + } else if (guid === 'init-progress-guid') { + return from(this.initProgress()).pipe( + startWith(PROGRESS), + ) as Observable + } else { + throw new Error('invalid guid type') + } + } + + // server state + + private stateIndex = 0 + async getState(): Promise { + await pauseFor(1000) + + this.stateIndex++ + + return this.stateIndex === 1 ? 'initializing' : 'running' + } + // db + async subscribeToPatchDB( + params: RR.SubscribePatchReq, + ): Promise { + await pauseFor(2000) + return { + dump: { id: 1, value: mockPatchData }, + guid: 'db-guid', + } + } + async setDbValue( pathArr: Array, value: T, @@ -136,11 +164,6 @@ export class MockApiService extends ApiService { async login(params: RR.LoginReq): Promise { await pauseFor(2000) - - setTimeout(() => { - this.mockWsSource$.next({ id: 1, value: mockPatchData }) - }, 2000) - return null } @@ -166,34 +189,63 @@ export class MockApiService extends ApiService { return null } - // server + // diagnostic - async echo(params: RR.EchoReq, url?: string): Promise { - if (url) { - const num = Math.floor(Math.random() * 10) + 1 - if (num > 8) return params.message - throw new Error() + async getError(): Promise { + await pauseFor(1000) + return { + code: 15, + message: 'Unknown server', + data: { details: 'Some details about the error here' }, } + } + + async diagnosticGetError(): Promise { + await pauseFor(1000) + return { + code: 15, + message: 'Unknown server', + data: { details: 'Some details about the error here' }, + } + } + + async diagnosticRestart(): Promise { + await pauseFor(1000) + } + + async diagnosticForgetDrive(): Promise { + await pauseFor(1000) + } + + async diagnosticRepairDisk(): Promise { + await pauseFor(1000) + } + + async diagnosticGetLogs( + params: RR.GetServerLogsReq, + ): Promise { + return this.getServerLogs(params) + } + + // init + + async initGetProgress(): Promise { + await pauseFor(250) + return { + progress: PROGRESS, + guid: 'init-progress-guid', + } + } + + async initFollowLogs(): Promise { await pauseFor(2000) - return params.message + return { + startCursor: 'start-cursor', + guid: 'logs-guid', + } } - openPatchWebsocket$(): Observable> { - return this.mockWsSource$.pipe( - shareReplay({ bufferSize: 1, refCount: true }), - ) - } - - openLogsWebsocket$(config: WebSocketSubjectConfig): Observable { - return interval(50).pipe( - map((_, index) => { - // mock fire open observer - if (index === 0) config.openObserver?.next(new Event('')) - if (index === 100) throw new Error('HAAHHA') - return Mock.ServerLogs[0] - }), - ) - } + // server async getSystemTime( params: RR.GetSystemTimeReq, @@ -248,7 +300,7 @@ export class MockApiService extends ApiService { await pauseFor(2000) return { startCursor: 'start-cursor', - guid: '7251d5be-645f-4362-a51b-3a85be92b31e', + guid: 'logs-guid', } } @@ -258,7 +310,7 @@ export class MockApiService extends ApiService { await pauseFor(2000) return { startCursor: 'start-cursor', - guid: '7251d5be-645f-4362-a51b-3a85be92b31e', + guid: 'logs-guid', } } @@ -268,11 +320,11 @@ export class MockApiService extends ApiService { await pauseFor(2000) return { startCursor: 'start-cursor', - guid: '7251d5be-645f-4362-a51b-3a85be92b31e', + guid: 'logs-guid', } } - randomLogs(limit = 1): Log[] { + private randomLogs(limit = 1): Log[] { const arrLength = Math.ceil(limit / Mock.ServerLogs.length) const logs = new Array(arrLength) .fill(Mock.ServerLogs) @@ -374,12 +426,6 @@ export class MockApiService extends ApiService { return null } - async systemRebuild( - params: RR.SystemRebuildReq, - ): Promise { - return this.restartServer(params) - } - async repairDisk(params: RR.RestartServerReq): Promise { await pauseFor(2000) return null @@ -422,7 +468,7 @@ export class MockApiService extends ApiService { } } - async getEos(): Promise { + async checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise { await pauseFor(2000) return Mock.MarketplaceEos } @@ -641,13 +687,13 @@ export class MockApiService extends ApiService { await pauseFor(2000) let entries if (Math.random() < 0.2) { - entries = Mock.PackageLogs + entries = Mock.ServerLogs } else { const arrLength = params.limit - ? Math.ceil(params.limit / Mock.PackageLogs.length) + ? Math.ceil(params.limit / Mock.ServerLogs.length) : 10 entries = new Array(arrLength) - .fill(Mock.PackageLogs) + .fill(Mock.ServerLogs) .reduce((acc, val) => acc.concat(val), []) } return { @@ -663,7 +709,7 @@ export class MockApiService extends ApiService { await pauseFor(2000) return { startCursor: 'start-cursor', - guid: '7251d5be-645f-4362-a51b-3a85be92b31e', + guid: 'logs-guid', } } @@ -673,7 +719,7 @@ export class MockApiService extends ApiService { await pauseFor(2000) setTimeout(async () => { - this.updateProgress(params.id) + this.installProgress(params.id) }, 1000) const patch: Operation< @@ -745,7 +791,7 @@ export class MockApiService extends ApiService { await pauseFor(2000) const patch: Operation[] = params.ids.map(id => { setTimeout(async () => { - this.updateProgress(id) + this.installProgress(id) }, 2000) return { @@ -1013,7 +1059,57 @@ export class MockApiService extends ApiService { return '4120e092-05ab-4de2-9fbd-c3f1f4b1df9e' // no significance, randomly generated } - private async updateProgress(id: string): Promise { + private async initProgress(): Promise { + const progress = JSON.parse(JSON.stringify(PROGRESS)) + + for (let [i, phase] of progress.phases.entries()) { + if ( + !phase.progress || + typeof phase.progress !== 'object' || + !phase.progress.total + ) { + await pauseFor(2000) + + progress.phases[i].progress = true + + if ( + progress.overall && + typeof progress.overall === 'object' && + progress.overall.total + ) { + const step = progress.overall.total / progress.phases.length + progress.overall.done += step + } + } else { + const step = phase.progress.total / 4 + + while (phase.progress.done < phase.progress.total) { + await pauseFor(200) + + phase.progress.done += step + + if ( + progress.overall && + typeof progress.overall === 'object' && + progress.overall.total + ) { + const step = progress.overall.total / progress.phases.length / 4 + + progress.overall.done += step + } + + if (phase.progress.done === phase.progress.total) { + await pauseFor(250) + + progress.phases[i].progress = true + } + } + } + } + return progress + } + + private async installProgress(id: string): Promise { const progress = JSON.parse(JSON.stringify(PROGRESS)) for (let [i, phase] of progress.phases.entries()) { @@ -1194,10 +1290,6 @@ export class MockApiService extends ApiService { } private async mockRevision(patch: Operation[]): Promise { - if (!this.sequence) { - const { sequence } = this.bootstrapper.init() - this.sequence = sequence - } const revision = { id: ++this.sequence, patch, diff --git a/web/projects/ui/src/app/services/auth.service.ts b/web/projects/ui/src/app/services/auth.service.ts index 5d755aa98..9c16d0e26 100644 --- a/web/projects/ui/src/app/services/auth.service.ts +++ b/web/projects/ui/src/app/services/auth.service.ts @@ -12,7 +12,7 @@ export enum AuthState { providedIn: 'root', }) export class AuthService { - private readonly LOGGED_IN_KEY = 'loggedInKey' + private readonly LOGGED_IN_KEY = 'loggedIn' private readonly authState$ = new ReplaySubject(1) readonly isVerified$ = this.authState$.pipe( diff --git a/web/projects/ui/src/app/services/connection.service.ts b/web/projects/ui/src/app/services/connection.service.ts index a45d5ec4c..7d3328503 100644 --- a/web/projects/ui/src/app/services/connection.service.ts +++ b/web/projects/ui/src/app/services/connection.service.ts @@ -1,25 +1,23 @@ -import { Injectable } from '@angular/core' -import { combineLatest, fromEvent, merge, ReplaySubject } from 'rxjs' -import { distinctUntilChanged, map, startWith } from 'rxjs/operators' +import { inject, Injectable } from '@angular/core' +import { combineLatest, Observable, shareReplay } from 'rxjs' +import { distinctUntilChanged, map } from 'rxjs/operators' +import { NetworkService } from 'src/app/services/network.service' +import { StateService } from 'src/app/services/state.service' @Injectable({ providedIn: 'root', }) -export class ConnectionService { - readonly networkConnected$ = merge( - fromEvent(window, 'online'), - fromEvent(window, 'offline'), - ).pipe( - startWith(null), - map(() => navigator.onLine), - distinctUntilChanged(), - ) - readonly websocketConnected$ = new ReplaySubject(1) - readonly connected$ = combineLatest([ - this.networkConnected$, - this.websocketConnected$.pipe(distinctUntilChanged()), +export class ConnectionService extends Observable { + private readonly stream$ = combineLatest([ + inject(NetworkService), + inject(StateService).pipe(map(Boolean)), ]).pipe( map(([network, websocket]) => network && websocket), distinctUntilChanged(), + shareReplay(1), ) + + constructor() { + super(subscriber => this.stream$.subscribe(subscriber)) + } } diff --git a/web/projects/ui/src/app/services/eos.service.ts b/web/projects/ui/src/app/services/eos.service.ts index dcd1d0de0..81b97bf11 100644 --- a/web/projects/ui/src/app/services/eos.service.ts +++ b/web/projects/ui/src/app/services/eos.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core' import { Emver } from '@start9labs/shared' import { BehaviorSubject, combineLatest } from 'rxjs' import { distinctUntilChanged, map } from 'rxjs/operators' -import { MarketplaceEOS } from 'src/app/services/api/api.types' +import { OSUpdate } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' import { PatchDB } from 'patch-db-client' import { getServerInfo } from 'src/app/util/get-server-info' @@ -12,7 +12,7 @@ import { DataModel } from './patch-db/data-model' providedIn: 'root', }) export class EOSService { - eos?: MarketplaceEOS + osUpdate?: OSUpdate updateAvailable$ = new BehaviorSubject(false) readonly updating$ = this.patch.watch$('serverInfo', 'statusInfo').pipe( @@ -52,9 +52,10 @@ export class EOSService { ) {} async loadEos(): Promise { - const { version } = await getServerInfo(this.patch) - this.eos = await this.api.getEos() - const updateAvailable = this.emver.compare(this.eos.version, version) === 1 + const { version, id } = await getServerInfo(this.patch) + this.osUpdate = await this.api.checkOSUpdate({ serverId: id }) + const updateAvailable = + this.emver.compare(this.osUpdate.version, version) === 1 this.updateAvailable$.next(updateAvailable) } } diff --git a/web/projects/ui/src/app/services/network.service.ts b/web/projects/ui/src/app/services/network.service.ts new file mode 100644 index 000000000..e1568603d --- /dev/null +++ b/web/projects/ui/src/app/services/network.service.ts @@ -0,0 +1,22 @@ +import { inject, Injectable } from '@angular/core' +import { WINDOW } from '@ng-web-apis/common' +import { fromEvent, merge, Observable, shareReplay } from 'rxjs' +import { distinctUntilChanged, map, startWith } from 'rxjs/operators' + +@Injectable({ providedIn: 'root' }) +export class NetworkService extends Observable { + private readonly win = inject(WINDOW) + private readonly stream$ = merge( + fromEvent(this.win, 'online'), + fromEvent(this.win, 'offline'), + ).pipe( + startWith(null), + map(() => this.win.navigator.onLine), + distinctUntilChanged(), + shareReplay(1), + ) + + constructor() { + super(subscriber => this.stream$.subscribe(subscriber)) + } +} diff --git a/web/projects/ui/src/app/services/patch-data.service.ts b/web/projects/ui/src/app/services/patch-data.service.ts index 5372a665c..50023c1f1 100644 --- a/web/projects/ui/src/app/services/patch-data.service.ts +++ b/web/projects/ui/src/app/services/patch-data.service.ts @@ -1,9 +1,9 @@ import { Inject, Injectable } from '@angular/core' import { ModalController } from '@ionic/angular' import { Observable } from 'rxjs' -import { filter, share, switchMap, take, tap } from 'rxjs/operators' +import { filter, map, share, switchMap, take, tap } from 'rxjs/operators' import { PatchDB } from 'patch-db-client' -import { DataModel, UIData } from 'src/app/services/patch-db/data-model' +import { DataModel } from 'src/app/services/patch-db/data-model' import { EOSService } from 'src/app/services/eos.service' import { OSWelcomePage } from 'src/app/modals/os-welcome/os-welcome.page' import { ConfigService } from 'src/app/services/config.service' @@ -11,21 +11,25 @@ import { ApiService } from 'src/app/services/api/embassy-api.service' import { MarketplaceService } from 'src/app/services/marketplace.service' import { AbstractMarketplaceService } from '@start9labs/marketplace' import { ConnectionService } from 'src/app/services/connection.service' +import { LocalStorageBootstrap } from './patch-db/local-storage-bootstrap' // Get data from PatchDb after is starts and act upon it @Injectable({ providedIn: 'root', }) -export class PatchDataService extends Observable { - private readonly stream$ = this.connectionService.connected$.pipe( +export class PatchDataService extends Observable { + private readonly stream$ = this.connection$.pipe( filter(Boolean), switchMap(() => this.patch.watch$()), - take(1), - tap(({ ui }) => { - // check for updates to eOS and services - this.checkForUpdates() - // show eos welcome message - this.showEosWelcome(ui.ackWelcome) + map((cache, index) => { + this.bootstrapper.update(cache) + + if (index === 0) { + // check for updates to StartOS and services + this.checkForUpdates() + // show eos welcome message + this.showEosWelcome(cache.ui.ackWelcome) + } }), share(), ) @@ -38,7 +42,8 @@ export class PatchDataService extends Observable { private readonly embassyApi: ApiService, @Inject(AbstractMarketplaceService) private readonly marketplaceService: MarketplaceService, - private readonly connectionService: ConnectionService, + private readonly connection$: ConnectionService, + private readonly bootstrapper: LocalStorageBootstrap, ) { super(subscriber => this.stream$.subscribe(subscriber)) } diff --git a/web/projects/ui/src/app/services/patch-db/local-storage-bootstrap.ts b/web/projects/ui/src/app/services/patch-db/local-storage-bootstrap.ts index 2ea5bef02..079def855 100644 --- a/web/projects/ui/src/app/services/patch-db/local-storage-bootstrap.ts +++ b/web/projects/ui/src/app/services/patch-db/local-storage-bootstrap.ts @@ -1,4 +1,4 @@ -import { Bootstrapper, DBCache } from 'patch-db-client' +import { Dump } from 'patch-db-client' import { DataModel } from 'src/app/services/patch-db/data-model' import { Injectable } from '@angular/core' import { StorageService } from '../storage.service' @@ -6,20 +6,18 @@ import { StorageService } from '../storage.service' @Injectable({ providedIn: 'root', }) -export class LocalStorageBootstrap implements Bootstrapper { - static CONTENT_KEY = 'patch-db-cache' +export class LocalStorageBootstrap { + static CONTENT_KEY = 'patchDB' constructor(private readonly storage: StorageService) {} - init(): DBCache { - const cache = this.storage.get>( - LocalStorageBootstrap.CONTENT_KEY, - ) + init(): Dump { + const cache = this.storage.get(LocalStorageBootstrap.CONTENT_KEY) - return cache || { sequence: 0, data: {} as DataModel } + return cache ? { id: 1, value: cache } : { id: 0, value: {} as DataModel } } - update(cache: DBCache): void { + update(cache: DataModel): void { this.storage.set(LocalStorageBootstrap.CONTENT_KEY, cache) } } diff --git a/web/projects/ui/src/app/services/patch-db/patch-db.factory.ts b/web/projects/ui/src/app/services/patch-db/patch-db.factory.ts index 51d29edc8..bb2dcdf57 100644 --- a/web/projects/ui/src/app/services/patch-db/patch-db.factory.ts +++ b/web/projects/ui/src/app/services/patch-db/patch-db.factory.ts @@ -1,19 +1,19 @@ import { InjectionToken, Injector } from '@angular/core' +import { Revision, Update } from 'patch-db-client' +import { defer, EMPTY, from, Observable } from 'rxjs' import { bufferTime, catchError, filter, + startWith, switchMap, take, - tap, } from 'rxjs/operators' -import { Update } from 'patch-db-client' -import { DataModel } from './data-model' -import { defer, EMPTY, from, interval, Observable } from 'rxjs' -import { AuthService } from '../auth.service' -import { ConnectionService } from '../connection.service' +import { StateService } from 'src/app/services/state.service' import { ApiService } from '../api/embassy-api.service' -import { ConfigService } from '../config.service' +import { AuthService } from '../auth.service' +import { DataModel } from './data-model' +import { LocalStorageBootstrap } from './local-storage-bootstrap' export const PATCH_SOURCE = new InjectionToken[]>>( '', @@ -25,33 +25,31 @@ export function sourceFactory( // defer() needed to avoid circular dependency with ApiService, since PatchDB is needed there return defer(() => { const api = injector.get(ApiService) - const authService = injector.get(AuthService) - const connectionService = injector.get(ConnectionService) - const configService = injector.get(ConfigService) - const isTor = configService.isTor() - const timeout = isTor ? 16000 : 4000 + const auth = injector.get(AuthService) + const state = injector.get(StateService) + const bootstrapper = injector.get(LocalStorageBootstrap) - const websocket$ = api.openPatchWebsocket$().pipe( - bufferTime(250), - filter(updates => !!updates.length), - catchError((_, watch$) => { - connectionService.websocketConnected$.next(false) + return auth.isVerified$.pipe( + switchMap(verified => + verified ? from(api.subscribeToPatchDB({})) : EMPTY, + ), + switchMap(({ dump, guid }) => + api.openWebsocket$(guid, {}).pipe( + bufferTime(250), + filter(revisions => !!revisions.length), + startWith([dump]), + ), + ), + catchError((_, original$) => { + state.retrigger() - return interval(timeout).pipe( - switchMap(() => - from(api.echo({ message: 'ping', timeout })).pipe( - catchError(() => EMPTY), - ), - ), + return state.pipe( + filter(current => current === 'running'), take(1), - switchMap(() => watch$), + switchMap(() => original$), ) }), - tap(() => connectionService.websocketConnected$.next(true)), - ) - - return authService.isVerified$.pipe( - switchMap(verified => (verified ? websocket$ : EMPTY)), + startWith([bootstrapper.init()]), ) }) } diff --git a/web/projects/ui/src/app/services/patch-monitor.service.ts b/web/projects/ui/src/app/services/patch-monitor.service.ts index cafb9f0fe..675531dda 100644 --- a/web/projects/ui/src/app/services/patch-monitor.service.ts +++ b/web/projects/ui/src/app/services/patch-monitor.service.ts @@ -4,24 +4,19 @@ import { tap } from 'rxjs/operators' import { PatchDB } from 'patch-db-client' import { AuthService } from 'src/app/services/auth.service' import { DataModel } from './patch-db/data-model' -import { LocalStorageBootstrap } from './patch-db/local-storage-bootstrap' // Start and stop PatchDb upon verification @Injectable({ providedIn: 'root', }) -export class PatchMonitorService extends Observable { - // @TODO not happy with Observable +export class PatchMonitorService extends Observable { private readonly stream$ = this.authService.isVerified$.pipe( - tap(verified => - verified ? this.patch.start(this.bootstrapper) : this.patch.stop(), - ), + tap(verified => (verified ? this.patch.start() : this.patch.stop())), ) constructor( private readonly authService: AuthService, private readonly patch: PatchDB, - private readonly bootstrapper: LocalStorageBootstrap, ) { super(subscriber => this.stream$.subscribe(subscriber)) } diff --git a/web/projects/ui/src/app/services/state.service.ts b/web/projects/ui/src/app/services/state.service.ts new file mode 100644 index 000000000..33569a751 --- /dev/null +++ b/web/projects/ui/src/app/services/state.service.ts @@ -0,0 +1,136 @@ +import { inject, Injectable } from '@angular/core' +import { CanActivateFn, IsActiveMatchOptions, Router } from '@angular/router' +import { ALWAYS_TRUE_HANDLER } from '@taiga-ui/cdk' +import { TuiAlertService, TuiNotification } from '@taiga-ui/core' +import { + BehaviorSubject, + combineLatest, + concat, + EMPTY, + exhaustMap, + from, + merge, + Observable, + startWith, + Subject, + timer, +} from 'rxjs' +import { + catchError, + filter, + map, + shareReplay, + skip, + switchMap, + take, + takeUntil, + tap, +} from 'rxjs/operators' +import { RR } from 'src/app/services/api/api.types' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { NetworkService } from 'src/app/services/network.service' + +const OPTIONS: IsActiveMatchOptions = { + paths: 'subset', + queryParams: 'exact', + fragment: 'ignored', + matrixParams: 'ignored', +} + +@Injectable({ + providedIn: 'root', +}) +export class StateService extends Observable { + private readonly alerts = inject(TuiAlertService) + private readonly api = inject(ApiService) + private readonly router = inject(Router) + private readonly network$ = inject(NetworkService) + + private readonly single$ = new Subject() + + private readonly trigger$ = new BehaviorSubject(undefined) + private readonly poll$ = this.trigger$.pipe( + switchMap(() => + timer(0, 2000).pipe( + switchMap(() => + from(this.api.getState()).pipe(catchError(() => EMPTY)), + ), + take(1), + ), + ), + ) + + private readonly stream$ = merge(this.single$, this.poll$).pipe( + tap(state => { + switch (state) { + case 'initializing': + this.router.navigate(['initializing'], { replaceUrl: true }) + break + case 'error': + this.router.navigate(['diagnostic'], { replaceUrl: true }) + break + case 'running': + if ( + this.router.isActive('initializing', OPTIONS) || + this.router.isActive('diagnostic', OPTIONS) + ) { + this.router.navigate([''], { replaceUrl: true }) + } + + break + } + }), + startWith(null), + shareReplay(1), + ) + + private readonly alert = merge( + this.trigger$.pipe(skip(1)), + this.network$.pipe(filter(v => !v)), + ) + .pipe( + exhaustMap(() => + concat( + this.alerts + .open('Trying to reach server', { + label: 'State unknown', + autoClose: false, + status: TuiNotification.Error, + }) + .pipe( + takeUntil( + combineLatest([this.stream$, this.network$]).pipe( + filter(state => state.every(Boolean)), + ), + ), + ), + this.alerts.open('Connection restored', { + label: 'Server reached', + status: TuiNotification.Success, + }), + ), + ), + ) + .subscribe() // @TODO shouldn't this be subscribed in app component with the others? Do we ever need to unsubscribe? + + constructor() { + super(subscriber => this.stream$.subscribe(subscriber)) + } + + retrigger() { + this.trigger$.next() + } + + async syncState() { + const state = await this.api.getState() + this.single$.next(state) + } +} + +export function stateNot(state: RR.ServerState[]): CanActivateFn { + return () => + inject(StateService).pipe( + filter(current => !current || !state.includes(current)), + map(ALWAYS_TRUE_HANDLER), + ) +} diff --git a/web/projects/ui/src/app/services/storage.service.ts b/web/projects/ui/src/app/services/storage.service.ts index ec87864b5..e59eba439 100644 --- a/web/projects/ui/src/app/services/storage.service.ts +++ b/web/projects/ui/src/app/services/storage.service.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@angular/core' import { DOCUMENT } from '@angular/common' -const PREFIX = '_embassystorage/_embassykv/' +const PREFIX = '_startos/' @Injectable({ providedIn: 'root', @@ -15,16 +15,21 @@ export class StorageService { return JSON.parse(String(this.storage.getItem(`${PREFIX}${key}`))) } - set(key: string, value: T) { + set(key: string, value: any) { this.storage.setItem(`${PREFIX}${key}`, JSON.stringify(value)) } clear() { - Array.from( - { length: this.storage.length }, - (_, i) => this.storage.key(i) || '', - ) - .filter(key => key.startsWith(PREFIX)) - .forEach(key => this.storage.removeItem(key)) + this.storage.clear() + } + + migrate036() { + const oldPrefix = '_embassystorage/_embassykv/' + if (!!this.storage.getItem(`${oldPrefix}loggedInKey`)) { + const cache = this.storage.getItem(`${oldPrefix}patch-db-cache`) + this.clear() + this.set('loggedIn', true) + this.set('patchDB', cache) + } } } From 355452cdb30c73fe3a9b134987310414a86f830b Mon Sep 17 00:00:00 2001 From: Jade <2364004+Blu-J@users.noreply.github.com> Date: Wed, 19 Jun 2024 17:30:05 -0600 Subject: [PATCH 037/125] Feat/next packages (#2646) * fix mac build * wip * chore: Update the effects to get rid of bad pattern * chore: Some small changes * wip * fix: Health checks don't show during race * fix: Restart working --------- Co-authored-by: Aiden McClelland --- .../src/Adapters/Systems/SystemForStartOs.ts | 6 ++- core/startos/src/service/mod.rs | 34 ++++++++++++++- .../src/service/service_effect_handler.rs | 6 +-- sdk/Makefile | 4 +- sdk/lib/mainFn/Daemons.ts | 4 +- sdk/lib/mainFn/HealthDaemon.ts | 4 +- sdk/lib/util/Overlay.ts | 41 +++++++++---------- 7 files changed, 65 insertions(+), 34 deletions(-) diff --git a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts index e10434032..e7f09952a 100644 --- a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts +++ b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts @@ -123,9 +123,9 @@ export class SystemForStartOs implements System { return this.abi.uninit({ effects, nextVersion }) } case "/main/start": { + if (this.onTerm) await this.onTerm() const started = async (onTerm: () => Promise) => { await effects.setMainStatus({ status: "running" }) - if (this.onTerm) await this.onTerm() this.onTerm = onTerm } const daemons = await ( @@ -135,10 +135,11 @@ export class SystemForStartOs implements System { }) ).build() this.onTerm = daemons.term + return } case "/main/stop": { - await effects.setMainStatus({ status: "stopped" }) if (this.onTerm) await this.onTerm() + await effects.setMainStatus({ status: "stopped" }) delete this.onTerm return duration(30, "s") } @@ -183,6 +184,7 @@ export class SystemForStartOs implements System { return dependencyConfig.update(options.input as any) // TODO } } + return } throw new Error(`Method ${options.procedure} not implemented.`) } diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index 2eecc565e..92564b8e4 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -485,7 +485,7 @@ impl Actor for ServiceActor { let mut current = seed.persistent_container.state.subscribe(); loop { - let kinds = dbg!(current.borrow().kinds()); + let kinds = current.borrow().kinds(); if let Err(e) = async { let main_status = match ( @@ -493,6 +493,14 @@ impl Actor for ServiceActor { kinds.desired_state, kinds.running_status, ) { + (Some(TransitionKind::Restarting), StartStop::Stop, Some(_)) => { + seed.persistent_container.stop().await?; + MainStatus::Restarting + } + (Some(TransitionKind::Restarting), StartStop::Start, _) => { + seed.persistent_container.start().await?; + MainStatus::Restarting + } (Some(TransitionKind::Restarting), _, _) => MainStatus::Restarting, (Some(TransitionKind::Restoring), _, _) => MainStatus::Restoring, (Some(TransitionKind::BackingUp), _, Some(status)) => { @@ -523,6 +531,30 @@ impl Actor for ServiceActor { .mutate(|d| { if let Some(i) = d.as_public_mut().as_package_data_mut().as_idx_mut(&id) { + let previous = i.as_status().as_main().de()?; + let previous_health = previous.health(); + let previous_started = previous.started(); + let mut main_status = main_status; + match &mut main_status { + &mut MainStatus::Running { ref mut health, .. } + | &mut MainStatus::BackingUp { ref mut health, .. } => { + *health = previous_health.unwrap_or(health).clone(); + } + _ => (), + }; + match &mut main_status { + MainStatus::Running { + ref mut started, .. + } => { + *started = previous_started.unwrap_or(*started); + } + MainStatus::BackingUp { + ref mut started, .. + } => { + *started = previous_started.map(Some).unwrap_or(*started); + } + _ => (), + }; i.as_status_mut().as_main_mut().ser(&main_status)?; } Ok(()) diff --git a/core/startos/src/service/service_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs index 28a611854..bcd6da7d1 100644 --- a/core/startos/src/service/service_effect_handler.rs +++ b/core/startos/src/service/service_effect_handler.rs @@ -936,7 +936,6 @@ async fn stopped(context: EffectContext, params: ParamsMaybePackageId) -> Result Ok(json!(matches!(package, MainStatus::Stopped))) } async fn running(context: EffectContext, params: ParamsPackageId) -> Result { - dbg!("Starting the running {params:?}"); let context = context.deref()?; let peeked = context.seed.ctx.db.peek().await; let package_id = params.package_id; @@ -956,9 +955,7 @@ async fn restart( WithProcedureId { procedure_id, .. }: WithProcedureId, ) -> Result<(), Error> { let context = context.deref()?; - dbg!("here"); context.restart(procedure_id).await?; - dbg!("here"); Ok(()) } @@ -1032,12 +1029,11 @@ struct SetMainStatus { status: SetMainStatusStatus, } async fn set_main_status(context: EffectContext, params: SetMainStatus) -> Result { - dbg!(format!("Status for main will be is {params:?}")); let context = context.deref()?; match params.status { SetMainStatusStatus::Running => context.seed.started(), SetMainStatusStatus::Stopped => context.seed.stopped(), - SetMainStatusStatus::Starting => context.seed.stopped(), + SetMainStatusStatus::Starting => context.seed.started(), } Ok(Value::Null) } diff --git a/sdk/Makefile b/sdk/Makefile index 091f4c7bb..4d01fa3d7 100644 --- a/sdk/Makefile +++ b/sdk/Makefile @@ -25,7 +25,7 @@ dist: $(TS_FILES) package.json node_modules README.md LICENSE cp LICENSE dist/LICENSE touch dist -full-bundle: clean bundle +full-bundle: bundle check: npm run check @@ -36,7 +36,7 @@ fmt: node_modules node_modules: package.json npm ci -publish: clean bundle package.json README.md LICENSE +publish: bundle package.json README.md LICENSE cd dist && npm publish --access=public link: bundle diff --git a/sdk/lib/mainFn/Daemons.ts b/sdk/lib/mainFn/Daemons.ts index d51d444a8..f48dd51c0 100644 --- a/sdk/lib/mainFn/Daemons.ts +++ b/sdk/lib/mainFn/Daemons.ts @@ -154,7 +154,7 @@ export class Daemons { this.healthDaemons.forEach((x) => x.addWatcher(() => this.updateMainHealth()), ) - return { + const built = { term: async (options?: { signal?: Signals; timeout?: number }) => { try { await Promise.all(this.healthDaemons.map((x) => x.term(options))) @@ -163,6 +163,8 @@ export class Daemons { } }, } + this.started(() => built.term()) + return built } private updateMainHealth() { diff --git a/sdk/lib/mainFn/HealthDaemon.ts b/sdk/lib/mainFn/HealthDaemon.ts index 84e9e34d7..48f3fab55 100644 --- a/sdk/lib/mainFn/HealthDaemon.ts +++ b/sdk/lib/mainFn/HealthDaemon.ts @@ -132,14 +132,14 @@ export class HealthDaemon { this.effects.setHealth({ result: status, message: health.message, - id: display, + id: this.id, name: display, }) } else { this.effects.setHealth({ result: health.status, message: health.message || "", - id: display, + id: this.id, name: display, }) } diff --git a/sdk/lib/util/Overlay.ts b/sdk/lib/util/Overlay.ts index fbc854c57..4d6d36b34 100644 --- a/sdk/lib/util/Overlay.ts +++ b/sdk/lib/util/Overlay.ts @@ -27,12 +27,11 @@ export class Overlay { } for (const dirPart of shared) { - await fs.mkdir(`${rootfs}/${dirPart}`, { recursive: true }) - await execFile("mount", [ - "--rbind", - `/${dirPart}`, - `${rootfs}/${dirPart}`, - ]) + const from = `/${dirPart}` + const to = `${rootfs}/${dirPart}` + await fs.mkdir(from, { recursive: true }) + await fs.mkdir(to, { recursive: true }) + await execFile("mount", ["--rbind", from, to]) } return new Overlay(effects, id, rootfs, guid) @@ -48,22 +47,22 @@ export class Overlay { ? options.subpath : `/${options.subpath}` : "/" - await execFile("mount", [ - "--bind", - `/media/startos/volumes/${options.id}${subpath}`, - path, - ]) + const from = `/media/startos/volumes/${options.id}${subpath}` + + await fs.mkdir(from, { recursive: true }) + await fs.mkdir(path, { recursive: true }) + await await execFile("mount", ["--bind", from, path]) } else if (options.type === "assets") { const subpath = options.subpath ? options.subpath.startsWith("/") ? options.subpath : `/${options.subpath}` : "/" - await execFile("mount", [ - "--bind", - `/media/startos/assets/${options.id}${subpath}`, - path, - ]) + const from = `/media/startos/assets/${options.id}${subpath}` + + await fs.mkdir(from, { recursive: true }) + await fs.mkdir(path, { recursive: true }) + await execFile("mount", ["--bind", from, path]) } else if (options.type === "pointer") { await this.effects.mount({ location: path, target: options }) } else if (options.type === "backup") { @@ -72,11 +71,11 @@ export class Overlay { ? options.subpath : `/${options.subpath}` : "/" - await execFile("mount", [ - "--bind", - `/media/startos/backup${subpath}`, - path, - ]) + const from = `/media/startos/backup${subpath}` + + await fs.mkdir(from, { recursive: true }) + await fs.mkdir(path, { recursive: true }) + await execFile("mount", ["--bind", from, path]) } else { throw new Error(`unknown type ${(options as any).type}`) } From c6c97491ac3467c4a602aa82cbe7ce2d3ec56a55 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 20 Jun 2024 10:07:39 -0600 Subject: [PATCH 038/125] add boot param to logs subscription --- .../ui/src/app/components/logs/logs.component.ts | 2 +- .../src/app/pages/diagnostic-routes/logs/logs.page.ts | 2 +- .../ui/src/app/pages/init/logs/logs.service.ts | 4 +++- web/projects/ui/src/app/services/api/api.types.ts | 7 ++++++- .../src/app/services/api/embassy-live-api.service.ts | 10 ++++++---- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/web/projects/ui/src/app/components/logs/logs.component.ts b/web/projects/ui/src/app/components/logs/logs.component.ts index 3d31313cb..8ce328f91 100644 --- a/web/projects/ui/src/app/components/logs/logs.component.ts +++ b/web/projects/ui/src/app/components/logs/logs.component.ts @@ -62,7 +62,7 @@ export class LogsComponent { | 'connected' | 'reconnecting' | 'disconnected' = 'connecting' - limit = 400 + limit = 200 count = 0 constructor( diff --git a/web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.page.ts b/web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.page.ts index 5119b9d93..976abc2a0 100644 --- a/web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.page.ts +++ b/web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.page.ts @@ -18,7 +18,7 @@ export class LogsPage { loading = true needInfinite = true startCursor?: string - limit = 200 + limit = 400 isOnBottom = true constructor( diff --git a/web/projects/ui/src/app/pages/init/logs/logs.service.ts b/web/projects/ui/src/app/pages/init/logs/logs.service.ts index e06d56b42..5fe550d45 100644 --- a/web/projects/ui/src/app/pages/init/logs/logs.service.ts +++ b/web/projects/ui/src/app/pages/init/logs/logs.service.ts @@ -35,7 +35,9 @@ function convertAnsi(entries: readonly any[]): string { @Injectable({ providedIn: 'root' }) export class LogsService extends Observable { private readonly api = inject(ApiService) - private readonly log$ = defer(() => this.api.initFollowLogs({})).pipe( + private readonly log$ = defer(() => + this.api.initFollowLogs({ boot: 0 }), + ).pipe( switchMap(({ guid }) => this.api.openWebsocket$(guid, {})), bufferTime(250), filter(logs => !!logs.length), diff --git a/web/projects/ui/src/app/services/api/api.types.ts b/web/projects/ui/src/app/services/api/api.types.ts index a5c52e9a2..256f6848b 100644 --- a/web/projects/ui/src/app/services/api/api.types.ts +++ b/web/projects/ui/src/app/services/api/api.types.ts @@ -70,7 +70,12 @@ export module RR { export type GetServerLogsReq = ServerLogsReq // server.logs & server.kernel-logs export type GetServerLogsRes = LogsRes - export type FollowServerLogsReq = { limit?: number } // server.logs.follow & server.kernel-logs.follow + // @param limit: BE default is 50 + // @param boot: number is offset (0: current, -1 prev, +1 first), string is a specific boot id, and null is all + export type FollowServerLogsReq = { + limit?: number + boot?: number | string | null + } // server.logs.follow & server.kernel-logs.follow export type FollowServerLogsRes = { startCursor: string guid: string diff --git a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts index 8826b2883..b460e00ff 100644 --- a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -160,8 +160,10 @@ export class LiveApiService extends ApiService { return this.rpcRequest({ method: 'init.subscribe', params: {} }) } - async initFollowLogs(): Promise { - return this.rpcRequest({ method: 'init.logs.follow', params: {} }) + async initFollowLogs( + params: RR.FollowServerLogsReq, + ): Promise { + return this.rpcRequest({ method: 'init.logs.follow', params }) } // server @@ -379,8 +381,8 @@ export class LiveApiService extends ApiService { } async followPackageLogs( - params: RR.FollowServerLogsReq, - ): Promise { + params: RR.FollowPackageLogsReq, + ): Promise { return this.rpcRequest({ method: 'package.logs.follow', params }) } From f39b85abf2a36622ba7b19d1c4fa6da0ef722215 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 20 Jun 2024 10:08:00 -0600 Subject: [PATCH 039/125] bump to 036 --- web/package-lock.json | 4 +- web/package.json | 2 +- web/patchdb-ui-seed.json | 2 +- .../modals/os-welcome/os-welcome.page.html | 51 ++----------------- .../ui/src/app/services/api/api.fixures.ts | 4 +- .../ui/src/app/services/api/mock-patch.ts | 2 +- 6 files changed, 12 insertions(+), 53 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 81b612ac1..97a049b98 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "startos-ui", - "version": "0.3.5.2", + "version": "0.3.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "startos-ui", - "version": "0.3.5.2", + "version": "0.3.6", "dependencies": { "@angular/animations": "^14.1.0", "@angular/common": "^14.1.0", diff --git a/web/package.json b/web/package.json index 3ea29c6fd..6d7db5863 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "startos-ui", - "version": "0.3.5.2", + "version": "0.3.6", "author": "Start9 Labs, Inc", "homepage": "https://start9.com/", "scripts": { diff --git a/web/patchdb-ui-seed.json b/web/patchdb-ui-seed.json index 30eb8900b..7bea3ea14 100644 --- a/web/patchdb-ui-seed.json +++ b/web/patchdb-ui-seed.json @@ -1,6 +1,6 @@ { "name": null, - "ack-welcome": "0.3.5.2", + "ack-welcome": "0.3.6", "marketplace": { "selected-url": "https://registry.start9.com/", "known-hosts": { diff --git a/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html b/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html index 292480e09..9603d454a 100644 --- a/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html +++ b/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html @@ -12,40 +12,7 @@

This Release

-

0.3.5.2

-

- View the complete - - release notes - - for more details. -

-
Highlights
-
    -
  • Revert perpetual performance mode for quieter fan
  • -
  • Minor bug fixes
  • -
- -

Previous 0.3.5.x Releases

- -

0.3.5.1

-

- View the complete - - release notes - - for more details. -

- -

0.3.5

+

0.3.6

View the complete

Highlights
    -
  • - This release contains significant under-the-hood improvements to - performance and reliability -
  • -
  • Ditch Docker, replace with Podman
  • -
  • Remove locking behavior from PatchDB and optimize
  • -
  • Boost efficiency of service manager
  • -
  • Require HTTPS on LAN, and improve setup flow for trusting Root CA
  • -
  • Better default privacy settings for Firefox kiosk mode
  • -
  • Eliminate memory leak from Javascript runtime
  • -
  • Other small bug fixes
  • -
  • Update license to MIT
  • +
  • Ditch Podman, replace with LXC
  • +
  • +
  • +
diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index 38ee4774b..8c0f447a0 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -17,10 +17,10 @@ export module Mock { shuttingDown: false, } export const MarketplaceEos: RR.CheckOSUpdateRes = { - version: '0.3.5.2', + version: '0.3.6', headline: 'Our biggest release ever.', releaseNotes: { - '0.3.5.2': 'Some **Markdown** release _notes_ for 0.3.5.2', + '0.3.6': 'Some **Markdown** release _notes_ for 0.3.6', '0.3.5.1': 'Some **Markdown** release _notes_ for 0.3.5.1', '0.3.4.4': 'Some **Markdown** release _notes_ for 0.3.4.4', '0.3.4.3': 'Some **Markdown** release _notes_ for 0.3.4.3', diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index ee96f9fe2..14fdaa724 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -37,7 +37,7 @@ export const mockPatchData: DataModel = { arch: 'x86_64', onionAddress: 'myveryownspecialtoraddress', id: 'abcdefgh', - version: '0.3.5.2', + version: '0.3.6', lastBackup: new Date(new Date().valueOf() - 604800001).toISOString(), lanAddress: 'https://adjective-noun.local', torAddress: 'https://myveryownspecialtoraddress.onion', From 07104b18f579e96a7fdea27a613355176eb710ca Mon Sep 17 00:00:00 2001 From: Mariusz Kogen Date: Thu, 20 Jun 2024 20:59:16 +0200 Subject: [PATCH 040/125] Update workflows actions (#2628) * Update workflows actions to the latest versions --- .github/workflows/startos-iso.yaml | 20 ++++++++++---------- .github/workflows/test.yaml | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/startos-iso.yaml b/.github/workflows/startos-iso.yaml index 30b2bec0f..c51eafc5c 100644 --- a/.github/workflows/startos-iso.yaml +++ b/.github/workflows/startos-iso.yaml @@ -71,27 +71,27 @@ jobs: sudo mount -t tmpfs tmpfs . if: ${{ github.event.inputs.runner == 'fast' }} - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: ${{ env.NODEJS_VERSION }} - name: Set up docker QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up system dependencies run: sudo apt-get update && sudo apt-get install -y qemu-user-static systemd-container - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Make run: make ARCH=${{ matrix.arch }} compiled-${{ matrix.arch }}.tar - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: compiled-${{ matrix.arch }}.tar path: compiled-${{ matrix.arch }}.tar @@ -144,7 +144,7 @@ jobs: run: rm -rf /opt/hostedtoolcache* if: ${{ github.event.inputs.runner != 'fast' }} - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive @@ -166,7 +166,7 @@ jobs: if: ${{ github.event.inputs.runner == 'fast' && (matrix.platform == 'x86_64' || matrix.platform == 'x86_64-nonfree') }} - name: Download compiled artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: compiled-${{ env.ARCH }}.tar @@ -190,18 +190,18 @@ jobs: run: PLATFORM=${{ matrix.platform }} make img if: ${{ matrix.platform == 'raspberrypi' }} - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: ${{ matrix.platform }}.squashfs path: results/*.squashfs - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: ${{ matrix.platform }}.iso path: results/*.iso if: ${{ matrix.platform != 'raspberrypi' }} - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: ${{ matrix.platform }}.img path: results/*.img diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c6082ac25..0a5eb38e9 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,11 +19,11 @@ jobs: name: Run Automated Tests runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: ${{ env.NODEJS_VERSION }} From e6abf4e33b841961ff37c701daa2eb4e203d7e9f Mon Sep 17 00:00:00 2001 From: waterplea Date: Fri, 21 Jun 2024 15:51:04 +0500 Subject: [PATCH 041/125] feat: get rid of cyclic dep between patch-db and api service Signed-off-by: waterplea --- web/projects/ui/src/app/app.module.ts | 2 - web/projects/ui/src/app/app.providers.ts | 10 ++++ .../services/api/embassy-live-api.service.ts | 7 ++- .../app/services/patch-db/patch-db-source.ts | 56 +++++++++++++++++++ .../app/services/patch-db/patch-db.factory.ts | 55 ------------------ .../app/services/patch-db/patch-db.module.ts | 20 ------- 6 files changed, 70 insertions(+), 80 deletions(-) create mode 100644 web/projects/ui/src/app/services/patch-db/patch-db-source.ts delete mode 100644 web/projects/ui/src/app/services/patch-db/patch-db.factory.ts delete mode 100644 web/projects/ui/src/app/services/patch-db/patch-db.module.ts diff --git a/web/projects/ui/src/app/app.module.ts b/web/projects/ui/src/app/app.module.ts index c0264f064..f13b4fbfa 100644 --- a/web/projects/ui/src/app/app.module.ts +++ b/web/projects/ui/src/app/app.module.ts @@ -28,7 +28,6 @@ import { PreloaderModule } from './app/preloader/preloader.module' import { FooterModule } from './app/footer/footer.module' import { MenuModule } from './app/menu/menu.module' import { APP_PROVIDERS } from './app.providers' -import { PatchDbModule } from './services/patch-db/patch-db.module' import { ToastContainerModule } from './components/toast-container/toast-container.module' import { ConnectionBarComponentModule } from './components/connection-bar/connection-bar.component.module' import { WidgetsPageModule } from './pages/widgets/widgets.module' @@ -54,7 +53,6 @@ import { environment } from '../environments/environment' MonacoEditorModule, SharedPipesModule, MarketplaceModule, - PatchDbModule, ToastContainerModule, ConnectionBarComponentModule, TuiRootModule, diff --git a/web/projects/ui/src/app/app.providers.ts b/web/projects/ui/src/app/app.providers.ts index bf26a8cb9..c6a8d756e 100644 --- a/web/projects/ui/src/app/app.providers.ts +++ b/web/projects/ui/src/app/app.providers.ts @@ -3,6 +3,11 @@ import { UntypedFormBuilder } from '@angular/forms' import { Router, RouteReuseStrategy } from '@angular/router' import { IonicRouteStrategy, IonNav } from '@ionic/angular' import { RELATIVE_URL, THEME, WorkspaceConfig } from '@start9labs/shared' +import { PatchDB } from 'patch-db-client' +import { + PATCH_CACHE, + PatchDbSource, +} from 'src/app/services/patch-db/patch-db-source' import { ApiService } from './services/api/embassy-api.service' import { MockApiService } from './services/api/embassy-mock-api.service' import { LiveApiService } from './services/api/embassy-live-api.service' @@ -29,6 +34,11 @@ export const APP_PROVIDERS: Provider[] = [ provide: ApiService, useClass: useMocks ? MockApiService : LiveApiService, }, + { + provide: PatchDB, + deps: [PatchDbSource, PATCH_CACHE], + useClass: PatchDB, + }, { provide: APP_INITIALIZER, deps: [StorageService, AuthService, ClientStorageService, Router], diff --git a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts index 8826b2883..954a4475e 100644 --- a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -7,6 +7,7 @@ import { RpcError, RPCOptions, } from '@start9labs/shared' +import { PATCH_CACHE } from 'src/app/services/patch-db/patch-db-source' import { ApiService } from './embassy-api.service' import { RR } from './api.types' import { parsePropertiesPermissive } from 'src/app/util/properties.util' @@ -16,7 +17,7 @@ import { Observable, filter, firstValueFrom } from 'rxjs' import { AuthService } from '../auth.service' import { DOCUMENT } from '@angular/common' import { DataModel } from '../patch-db/data-model' -import { PatchDB, pathFromArray } from 'patch-db-client' +import { Dump, pathFromArray } from 'patch-db-client' @Injectable() export class LiveApiService extends ApiService { @@ -25,7 +26,7 @@ export class LiveApiService extends ApiService { private readonly http: HttpService, private readonly config: ConfigService, private readonly auth: AuthService, - private readonly patch: PatchDB, + @Inject(PATCH_CACHE) private readonly cache$: Observable>, ) { super() ; (window as any).rpcClient = this @@ -482,7 +483,7 @@ export class LiveApiService extends ApiService { const patchSequence = res.headers.get('x-patch-sequence') if (patchSequence) await firstValueFrom( - this.patch.cache$.pipe(filter(({ id }) => id >= Number(patchSequence))), + this.cache$.pipe(filter(({ id }) => id >= Number(patchSequence))), ) return body.result diff --git a/web/projects/ui/src/app/services/patch-db/patch-db-source.ts b/web/projects/ui/src/app/services/patch-db/patch-db-source.ts new file mode 100644 index 000000000..803fbce2c --- /dev/null +++ b/web/projects/ui/src/app/services/patch-db/patch-db-source.ts @@ -0,0 +1,56 @@ +import { inject, Injectable, InjectionToken } from '@angular/core' +import { Dump, Revision, Update } from 'patch-db-client' +import { BehaviorSubject, EMPTY, Observable } from 'rxjs' +import { + bufferTime, + catchError, + filter, + startWith, + switchMap, + take, +} from 'rxjs/operators' +import { StateService } from 'src/app/services/state.service' +import { ApiService } from '../api/embassy-api.service' +import { AuthService } from '../auth.service' +import { DataModel } from './data-model' +import { LocalStorageBootstrap } from './local-storage-bootstrap' + +export const PATCH_CACHE = new InjectionToken('', { + factory: () => + new BehaviorSubject>({ + id: 0, + value: {} as DataModel, + }), +}) + +@Injectable({ + providedIn: 'root', +}) +export class PatchDbSource extends Observable[]> { + private readonly api = inject(ApiService) + private readonly state = inject(StateService) + private readonly stream$ = inject(AuthService).isVerified$.pipe( + switchMap(verified => (verified ? this.api.subscribeToPatchDB({}) : EMPTY)), + switchMap(({ dump, guid }) => + this.api.openWebsocket$(guid, {}).pipe( + bufferTime(250), + filter(revisions => !!revisions.length), + startWith([dump]), + ), + ), + catchError((_, original$) => { + this.state.retrigger() + + return this.state.pipe( + filter(current => current === 'running'), + take(1), + switchMap(() => original$), + ) + }), + startWith([inject(LocalStorageBootstrap).init()]), + ) + + constructor() { + super(subscriber => this.stream$.subscribe(subscriber)) + } +} diff --git a/web/projects/ui/src/app/services/patch-db/patch-db.factory.ts b/web/projects/ui/src/app/services/patch-db/patch-db.factory.ts deleted file mode 100644 index bb2dcdf57..000000000 --- a/web/projects/ui/src/app/services/patch-db/patch-db.factory.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { InjectionToken, Injector } from '@angular/core' -import { Revision, Update } from 'patch-db-client' -import { defer, EMPTY, from, Observable } from 'rxjs' -import { - bufferTime, - catchError, - filter, - startWith, - switchMap, - take, -} from 'rxjs/operators' -import { StateService } from 'src/app/services/state.service' -import { ApiService } from '../api/embassy-api.service' -import { AuthService } from '../auth.service' -import { DataModel } from './data-model' -import { LocalStorageBootstrap } from './local-storage-bootstrap' - -export const PATCH_SOURCE = new InjectionToken[]>>( - '', -) - -export function sourceFactory( - injector: Injector, -): Observable[]> { - // defer() needed to avoid circular dependency with ApiService, since PatchDB is needed there - return defer(() => { - const api = injector.get(ApiService) - const auth = injector.get(AuthService) - const state = injector.get(StateService) - const bootstrapper = injector.get(LocalStorageBootstrap) - - return auth.isVerified$.pipe( - switchMap(verified => - verified ? from(api.subscribeToPatchDB({})) : EMPTY, - ), - switchMap(({ dump, guid }) => - api.openWebsocket$(guid, {}).pipe( - bufferTime(250), - filter(revisions => !!revisions.length), - startWith([dump]), - ), - ), - catchError((_, original$) => { - state.retrigger() - - return state.pipe( - filter(current => current === 'running'), - take(1), - switchMap(() => original$), - ) - }), - startWith([bootstrapper.init()]), - ) - }) -} diff --git a/web/projects/ui/src/app/services/patch-db/patch-db.module.ts b/web/projects/ui/src/app/services/patch-db/patch-db.module.ts deleted file mode 100644 index 3c816e339..000000000 --- a/web/projects/ui/src/app/services/patch-db/patch-db.module.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { PatchDB } from 'patch-db-client' -import { Injector, NgModule } from '@angular/core' -import { PATCH_SOURCE, sourceFactory } from './patch-db.factory' - -// This module is purely for providers organization purposes -@NgModule({ - providers: [ - { - provide: PATCH_SOURCE, - deps: [Injector], - useFactory: sourceFactory, - }, - { - provide: PatchDB, - deps: [PATCH_SOURCE], - useClass: PatchDB, - }, - ], -}) -export class PatchDbModule {} From 133dfd5063a36b8db2b5bd1b54a20a3e462f4082 Mon Sep 17 00:00:00 2001 From: Shadowy Super Coder Date: Fri, 21 Jun 2024 18:39:05 -0600 Subject: [PATCH 042/125] match query to registry table --- core/startos/src/registry/os/version/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/startos/src/registry/os/version/mod.rs b/core/startos/src/registry/os/version/mod.rs index cb36f078a..234143295 100644 --- a/core/startos/src/registry/os/version/mod.rs +++ b/core/startos/src/registry/os/version/mod.rs @@ -145,10 +145,10 @@ pub async fn get_version( arch, }: GetVersionParams, ) -> Result, Error> { - if let (Some(pool), Some(server_id), Some(arch)) = (ctx.pool, server_id, arch) { - let created_at = Utc::now().to_rfc3339(); + if let (Some(pool), Some(server_id), Some(arch)) = (&ctx.pool, server_id, arch) { + let created_at = Utc::now(); - query!("INSERT INTO user_activity (created_at, server_id, os_version, arch) VALUES ($1, $2, $3, $4)", + query!("INSERT INTO user_activity (created_at, server_id, arch) VALUES ($1, $2, $3)", created_at, server_id, arch From b0c0cd7fda2400712fc928a25da5e4cbd42031cf Mon Sep 17 00:00:00 2001 From: Shadowy Super Coder Date: Fri, 21 Jun 2024 18:40:32 -0600 Subject: [PATCH 043/125] add script to cache registry db --- ...3452e7a69768da179e78479cd35ee42b493ae.json | 16 + .../os/version/db/registry-sqlx-data.sh | 27 + .../os/version/db/registry_schema.sql | 828 ++++++++++++++++++ 3 files changed, 871 insertions(+) create mode 100644 core/startos/.sqlx/query-bc9382d34bf93f468c64d0d02613452e7a69768da179e78479cd35ee42b493ae.json create mode 100755 core/startos/src/registry/os/version/db/registry-sqlx-data.sh create mode 100644 core/startos/src/registry/os/version/db/registry_schema.sql diff --git a/core/startos/.sqlx/query-bc9382d34bf93f468c64d0d02613452e7a69768da179e78479cd35ee42b493ae.json b/core/startos/.sqlx/query-bc9382d34bf93f468c64d0d02613452e7a69768da179e78479cd35ee42b493ae.json new file mode 100644 index 000000000..d5fae12b7 --- /dev/null +++ b/core/startos/.sqlx/query-bc9382d34bf93f468c64d0d02613452e7a69768da179e78479cd35ee42b493ae.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO user_activity (created_at, server_id, arch) VALUES ($1, $2, $3)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Timestamptz", + "Varchar", + "Varchar" + ] + }, + "nullable": [] + }, + "hash": "bc9382d34bf93f468c64d0d02613452e7a69768da179e78479cd35ee42b493ae" +} diff --git a/core/startos/src/registry/os/version/db/registry-sqlx-data.sh b/core/startos/src/registry/os/version/db/registry-sqlx-data.sh new file mode 100755 index 000000000..2a422bf38 --- /dev/null +++ b/core/startos/src/registry/os/version/db/registry-sqlx-data.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +TMP_DIR=$(mktemp -d) +mkdir $TMP_DIR/pgdata +docker run -d --rm --name=tmp_postgres -e POSTGRES_PASSWORD=password -v $TMP_DIR/pgdata:/var/lib/postgresql/data postgres + +( + set -e + ctr=0 + until docker exec tmp_postgres psql -U postgres || [ $ctr -ge 5 ]; do + ctr=$[ctr + 1] + sleep 5; + done + + PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' tmp_postgres) + + SCHEMA_DUMP="registry_schema.sql" + + DATABASE_URL=postgres://postgres:password@$PG_IP/postgres + psql $DATABASE_URL -f "$SCRIPT_DIR/$SCHEMA_DUMP" + DATABASE_URL=postgres://postgres:password@$PG_IP/postgres PLATFORM=$(uname -m) cargo sqlx prepare -- --lib --profile=test --workspace + echo "Subscript Complete" +) + +docker stop tmp_postgres +sudo rm -rf $TMP_DIR diff --git a/core/startos/src/registry/os/version/db/registry_schema.sql b/core/startos/src/registry/os/version/db/registry_schema.sql new file mode 100644 index 000000000..abd9f4ea6 --- /dev/null +++ b/core/startos/src/registry/os/version/db/registry_schema.sql @@ -0,0 +1,828 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 14.12 (Ubuntu 14.12-0ubuntu0.22.04.1) +-- Dumped by pg_dump version 14.12 (Ubuntu 14.12-0ubuntu0.22.04.1) + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: admin; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.admin ( + id character varying NOT NULL, + created_at timestamp with time zone NOT NULL, + pass_hash character varying NOT NULL, + deleted_at timestamp with time zone +); + + +ALTER TABLE public.admin OWNER TO alpha_admin; + +-- +-- Name: admin_pkgs; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.admin_pkgs ( + id bigint NOT NULL, + admin character varying NOT NULL, + pkg_id character varying NOT NULL +); + + +ALTER TABLE public.admin_pkgs OWNER TO alpha_admin; + +-- +-- Name: admin_pkgs_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin +-- + +CREATE SEQUENCE public.admin_pkgs_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.admin_pkgs_id_seq OWNER TO alpha_admin; + +-- +-- Name: admin_pkgs_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin +-- + +ALTER SEQUENCE public.admin_pkgs_id_seq OWNED BY public.admin_pkgs.id; + + +-- +-- Name: category; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.category ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + name character varying NOT NULL, + description character varying NOT NULL, + priority bigint DEFAULT 0 NOT NULL +); + + +ALTER TABLE public.category OWNER TO alpha_admin; + +-- +-- Name: category_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin +-- + +CREATE SEQUENCE public.category_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.category_id_seq OWNER TO alpha_admin; + +-- +-- Name: category_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin +-- + +ALTER SEQUENCE public.category_id_seq OWNED BY public.category.id; + + +-- +-- Name: eos_hash; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.eos_hash ( + id bigint NOT NULL, + version character varying NOT NULL, + hash character varying NOT NULL +); + + +ALTER TABLE public.eos_hash OWNER TO alpha_admin; + +-- +-- Name: eos_hash_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin +-- + +CREATE SEQUENCE public.eos_hash_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.eos_hash_id_seq OWNER TO alpha_admin; + +-- +-- Name: eos_hash_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin +-- + +ALTER SEQUENCE public.eos_hash_id_seq OWNED BY public.eos_hash.id; + + +-- +-- Name: error_log_record; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.error_log_record ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + epoch character varying NOT NULL, + commit_hash character varying NOT NULL, + source_file character varying NOT NULL, + line bigint NOT NULL, + target character varying NOT NULL, + level character varying NOT NULL, + message character varying NOT NULL, + incidents bigint NOT NULL +); + + +ALTER TABLE public.error_log_record OWNER TO alpha_admin; + +-- +-- Name: error_log_record_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin +-- + +CREATE SEQUENCE public.error_log_record_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.error_log_record_id_seq OWNER TO alpha_admin; + +-- +-- Name: error_log_record_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin +-- + +ALTER SEQUENCE public.error_log_record_id_seq OWNED BY public.error_log_record.id; + + +-- +-- Name: metric; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.metric ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + version character varying NOT NULL, + pkg_id character varying NOT NULL +); + + +ALTER TABLE public.metric OWNER TO alpha_admin; + +-- +-- Name: metric_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin +-- + +CREATE SEQUENCE public.metric_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.metric_id_seq OWNER TO alpha_admin; + +-- +-- Name: metric_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin +-- + +ALTER SEQUENCE public.metric_id_seq OWNED BY public.metric.id; + + +-- +-- Name: os_version; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.os_version ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + number character varying NOT NULL, + headline character varying NOT NULL, + release_notes character varying NOT NULL, + arch character varying +); + + +ALTER TABLE public.os_version OWNER TO alpha_admin; + +-- +-- Name: os_version_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin +-- + +CREATE SEQUENCE public.os_version_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.os_version_id_seq OWNER TO alpha_admin; + +-- +-- Name: os_version_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin +-- + +ALTER SEQUENCE public.os_version_id_seq OWNED BY public.os_version.id; + + +-- +-- Name: persistent_migration; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.persistent_migration ( + id integer NOT NULL, + version integer NOT NULL, + label character varying, + "timestamp" timestamp with time zone NOT NULL +); + + +ALTER TABLE public.persistent_migration OWNER TO alpha_admin; + +-- +-- Name: persistent_migration_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin +-- + +CREATE SEQUENCE public.persistent_migration_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.persistent_migration_id_seq OWNER TO alpha_admin; + +-- +-- Name: persistent_migration_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin +-- + +ALTER SEQUENCE public.persistent_migration_id_seq OWNED BY public.persistent_migration.id; + + +-- +-- Name: pkg_category; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.pkg_category ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + category_id bigint NOT NULL, + pkg_id character varying NOT NULL +); + + +ALTER TABLE public.pkg_category OWNER TO alpha_admin; + +-- +-- Name: pkg_dependency; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.pkg_dependency ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + pkg_id character varying NOT NULL, + pkg_version character varying NOT NULL, + dep_id character varying NOT NULL, + dep_version_range character varying NOT NULL +); + + +ALTER TABLE public.pkg_dependency OWNER TO alpha_admin; + +-- +-- Name: pkg_dependency_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin +-- + +CREATE SEQUENCE public.pkg_dependency_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.pkg_dependency_id_seq OWNER TO alpha_admin; + +-- +-- Name: pkg_dependency_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin +-- + +ALTER SEQUENCE public.pkg_dependency_id_seq OWNED BY public.pkg_dependency.id; + + +-- +-- Name: pkg_record; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.pkg_record ( + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone, + pkg_id character varying NOT NULL, + hidden boolean DEFAULT false NOT NULL +); + + +ALTER TABLE public.pkg_record OWNER TO alpha_admin; + +-- +-- Name: service_category_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin +-- + +CREATE SEQUENCE public.service_category_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.service_category_id_seq OWNER TO alpha_admin; + +-- +-- Name: service_category_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin +-- + +ALTER SEQUENCE public.service_category_id_seq OWNED BY public.pkg_category.id; + + +-- +-- Name: upload; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.upload ( + id bigint NOT NULL, + uploader character varying NOT NULL, + pkg_id character varying NOT NULL, + pkg_version character varying NOT NULL, + created_at timestamp with time zone NOT NULL +); + + +ALTER TABLE public.upload OWNER TO alpha_admin; + +-- +-- Name: upload_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin +-- + +CREATE SEQUENCE public.upload_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.upload_id_seq OWNER TO alpha_admin; + +-- +-- Name: upload_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin +-- + +ALTER SEQUENCE public.upload_id_seq OWNED BY public.upload.id; + + +-- +-- Name: user_activity; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.user_activity ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + server_id character varying NOT NULL, + os_version character varying, + arch character varying +); + + +ALTER TABLE public.user_activity OWNER TO alpha_admin; + +-- +-- Name: user_activity_id_seq; Type: SEQUENCE; Schema: public; Owner: alpha_admin +-- + +CREATE SEQUENCE public.user_activity_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.user_activity_id_seq OWNER TO alpha_admin; + +-- +-- Name: user_activity_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: alpha_admin +-- + +ALTER SEQUENCE public.user_activity_id_seq OWNED BY public.user_activity.id; + + +-- +-- Name: version; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.version ( + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone, + number character varying NOT NULL, + release_notes character varying NOT NULL, + os_version character varying NOT NULL, + pkg_id character varying NOT NULL, + title character varying NOT NULL, + desc_short character varying NOT NULL, + desc_long character varying NOT NULL, + icon_type character varying NOT NULL, + deprecated_at timestamp with time zone +); + + +ALTER TABLE public.version OWNER TO alpha_admin; + +-- +-- Name: version_platform; Type: TABLE; Schema: public; Owner: alpha_admin +-- + +CREATE TABLE public.version_platform ( + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone, + pkg_id character varying NOT NULL, + version_number character varying NOT NULL, + arch character varying NOT NULL, + ram bigint, + device jsonb +); + + +ALTER TABLE public.version_platform OWNER TO alpha_admin; + +-- +-- Name: admin_pkgs id; Type: DEFAULT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.admin_pkgs ALTER COLUMN id SET DEFAULT nextval('public.admin_pkgs_id_seq'::regclass); + + +-- +-- Name: category id; Type: DEFAULT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.category ALTER COLUMN id SET DEFAULT nextval('public.category_id_seq'::regclass); + + +-- +-- Name: eos_hash id; Type: DEFAULT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.eos_hash ALTER COLUMN id SET DEFAULT nextval('public.eos_hash_id_seq'::regclass); + + +-- +-- Name: error_log_record id; Type: DEFAULT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.error_log_record ALTER COLUMN id SET DEFAULT nextval('public.error_log_record_id_seq'::regclass); + + +-- +-- Name: metric id; Type: DEFAULT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.metric ALTER COLUMN id SET DEFAULT nextval('public.metric_id_seq'::regclass); + + +-- +-- Name: os_version id; Type: DEFAULT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.os_version ALTER COLUMN id SET DEFAULT nextval('public.os_version_id_seq'::regclass); + + +-- +-- Name: persistent_migration id; Type: DEFAULT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.persistent_migration ALTER COLUMN id SET DEFAULT nextval('public.persistent_migration_id_seq'::regclass); + + +-- +-- Name: pkg_category id; Type: DEFAULT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.pkg_category ALTER COLUMN id SET DEFAULT nextval('public.service_category_id_seq'::regclass); + + +-- +-- Name: pkg_dependency id; Type: DEFAULT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.pkg_dependency ALTER COLUMN id SET DEFAULT nextval('public.pkg_dependency_id_seq'::regclass); + + +-- +-- Name: upload id; Type: DEFAULT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.upload ALTER COLUMN id SET DEFAULT nextval('public.upload_id_seq'::regclass); + + +-- +-- Name: user_activity id; Type: DEFAULT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.user_activity ALTER COLUMN id SET DEFAULT nextval('public.user_activity_id_seq'::regclass); + + +-- +-- Name: admin admin_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.admin + ADD CONSTRAINT admin_pkey PRIMARY KEY (id); + + +-- +-- Name: admin_pkgs admin_pkgs_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.admin_pkgs + ADD CONSTRAINT admin_pkgs_pkey PRIMARY KEY (id); + + +-- +-- Name: category category_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.category + ADD CONSTRAINT category_pkey PRIMARY KEY (id); + + +-- +-- Name: eos_hash eos_hash_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.eos_hash + ADD CONSTRAINT eos_hash_pkey PRIMARY KEY (id); + + +-- +-- Name: error_log_record error_log_record_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.error_log_record + ADD CONSTRAINT error_log_record_pkey PRIMARY KEY (id); + + +-- +-- Name: metric metric_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.metric + ADD CONSTRAINT metric_pkey PRIMARY KEY (id); + + +-- +-- Name: os_version os_version_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.os_version + ADD CONSTRAINT os_version_pkey PRIMARY KEY (id); + + +-- +-- Name: persistent_migration persistent_migration_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.persistent_migration + ADD CONSTRAINT persistent_migration_pkey PRIMARY KEY (id); + + +-- +-- Name: pkg_category pkg_category_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.pkg_category + ADD CONSTRAINT pkg_category_pkey PRIMARY KEY (id); + + +-- +-- Name: pkg_dependency pkg_dependency_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.pkg_dependency + ADD CONSTRAINT pkg_dependency_pkey PRIMARY KEY (id); + + +-- +-- Name: admin_pkgs unique_admin_pkg; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.admin_pkgs + ADD CONSTRAINT unique_admin_pkg UNIQUE (pkg_id, admin); + + +-- +-- Name: error_log_record unique_log_record; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.error_log_record + ADD CONSTRAINT unique_log_record UNIQUE (epoch, commit_hash, source_file, line, target, level, message); + + +-- +-- Name: category unique_name; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.category + ADD CONSTRAINT unique_name UNIQUE (name); + + +-- +-- Name: pkg_category unique_pkg_category; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.pkg_category + ADD CONSTRAINT unique_pkg_category UNIQUE (pkg_id, category_id); + + +-- +-- Name: pkg_dependency unique_pkg_dep_version; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.pkg_dependency + ADD CONSTRAINT unique_pkg_dep_version UNIQUE (pkg_id, pkg_version, dep_id); + + +-- +-- Name: eos_hash unique_version; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.eos_hash + ADD CONSTRAINT unique_version UNIQUE (version); + + +-- +-- Name: upload upload_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.upload + ADD CONSTRAINT upload_pkey PRIMARY KEY (id); + + +-- +-- Name: user_activity user_activity_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.user_activity + ADD CONSTRAINT user_activity_pkey PRIMARY KEY (id); + + +-- +-- Name: version version_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.version + ADD CONSTRAINT version_pkey PRIMARY KEY (pkg_id, number); + + +-- +-- Name: version_platform version_platform_pkey; Type: CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.version_platform + ADD CONSTRAINT version_platform_pkey PRIMARY KEY (pkg_id, version_number, arch); + + +-- +-- Name: category_name_idx; Type: INDEX; Schema: public; Owner: alpha_admin +-- + +CREATE UNIQUE INDEX category_name_idx ON public.category USING btree (name); + + +-- +-- Name: pkg_record_pkg_id_idx; Type: INDEX; Schema: public; Owner: alpha_admin +-- + +CREATE UNIQUE INDEX pkg_record_pkg_id_idx ON public.pkg_record USING btree (pkg_id); + + +-- +-- Name: version_number_idx; Type: INDEX; Schema: public; Owner: alpha_admin +-- + +CREATE INDEX version_number_idx ON public.version USING btree (number); + + +-- +-- Name: admin_pkgs admin_pkgs_admin_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.admin_pkgs + ADD CONSTRAINT admin_pkgs_admin_fkey FOREIGN KEY (admin) REFERENCES public.admin(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: metric metric_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.metric + ADD CONSTRAINT metric_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: pkg_category pkg_category_category_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.pkg_category + ADD CONSTRAINT pkg_category_category_id_fkey FOREIGN KEY (category_id) REFERENCES public.category(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: pkg_category pkg_category_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.pkg_category + ADD CONSTRAINT pkg_category_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: pkg_dependency pkg_dependency_dep_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.pkg_dependency + ADD CONSTRAINT pkg_dependency_dep_id_fkey FOREIGN KEY (dep_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: pkg_dependency pkg_dependency_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.pkg_dependency + ADD CONSTRAINT pkg_dependency_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: upload upload_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.upload + ADD CONSTRAINT upload_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: upload upload_uploader_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.upload + ADD CONSTRAINT upload_uploader_fkey FOREIGN KEY (uploader) REFERENCES public.admin(id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: version version_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.version + ADD CONSTRAINT version_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: version_platform version_platform_pkg_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: alpha_admin +-- + +ALTER TABLE ONLY public.version_platform + ADD CONSTRAINT version_platform_pkg_id_fkey FOREIGN KEY (pkg_id) REFERENCES public.pkg_record(pkg_id) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- PostgreSQL database dump complete +-- + From e0d23f4436cb7bc28106702efaf45a5c581fca9e Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Sat, 22 Jun 2024 11:33:30 -0600 Subject: [PATCH 044/125] bump patchDB dep --- patch-db | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patch-db b/patch-db index 7aa53249f..c537a07ea 160000 --- a/patch-db +++ b/patch-db @@ -1 +1 @@ -Subproject commit 7aa53249f9353162475ea347abac92abcfba5493 +Subproject commit c537a07ea937e69b66841d903c70fd75623e5457 From 68ed1c80ce5edf546081e86e9cb62309fff4112a Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Sat, 22 Jun 2024 21:47:18 -0600 Subject: [PATCH 045/125] update todos --- web/projects/ui/src/app/app.providers.ts | 2 +- .../ui/src/app/modals/generic-form/generic-form.page.ts | 1 - .../pages/apps-routes/app-actions/app-actions.page.ts | 9 ++------- web/projects/ui/src/app/pages/init/init.service.ts | 1 + web/projects/ui/src/app/services/api/api.fixures.ts | 6 +++--- web/projects/ui/src/app/services/api/mock-patch.ts | 2 +- .../ui/src/app/services/patch-db/patch-db-source.ts | 1 + 7 files changed, 9 insertions(+), 13 deletions(-) diff --git a/web/projects/ui/src/app/app.providers.ts b/web/projects/ui/src/app/app.providers.ts index c6a8d756e..e93c323d0 100644 --- a/web/projects/ui/src/app/app.providers.ts +++ b/web/projects/ui/src/app/app.providers.ts @@ -64,7 +64,7 @@ export function appInitializer( return () => { storage.migrate036() auth.init() - localStorage.init() // @TODO pretty sure we can navigate before this step + localStorage.init() router.initialNavigation() } } diff --git a/web/projects/ui/src/app/modals/generic-form/generic-form.page.ts b/web/projects/ui/src/app/modals/generic-form/generic-form.page.ts index eb690a787..e74c65d47 100644 --- a/web/projects/ui/src/app/modals/generic-form/generic-form.page.ts +++ b/web/projects/ui/src/app/modals/generic-form/generic-form.page.ts @@ -54,7 +54,6 @@ export class GenericFormPage { return } - // @TODO make this more like generic input component dismissal const success = await handler(this.formGroup.value) if (success !== false) this.modalCtrl.dismiss() } diff --git a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts index a48ac8185..fc008a17a 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts @@ -44,12 +44,7 @@ export class AppActionsPage { status: T.Status, action: { key: string; value: T.ActionMetadata }, ) { - if ( - status && - action.value.allowedStatuses.includes( - status.main.status, // @TODO - ) - ) { + if (status && action.value.allowedStatuses.includes(status.main.status)) { if (!isEmptyObject(action.value.input || {})) { const modal = await this.modalCtrl.create({ component: GenericFormPage, @@ -91,7 +86,7 @@ export class AppActionsPage { await alert.present() } } else { - const statuses = [...action.value.allowedStatuses] // @TODO + const statuses = [...action.value.allowedStatuses] const last = statuses.pop() let statusesStr = statuses.join(', ') let error = '' diff --git a/web/projects/ui/src/app/pages/init/init.service.ts b/web/projects/ui/src/app/pages/init/init.service.ts index 3cca42a58..7102c9db5 100644 --- a/web/projects/ui/src/app/pages/init/init.service.ts +++ b/web/projects/ui/src/app/pages/init/init.service.ts @@ -58,6 +58,7 @@ export class InitService extends Observable { } }), catchError(e => { + // @TODO this toast is presenting when we navigate away from init page. It seems other websockets exhibit the same behavior, but we never noticed because the error were not being caught and presented in this manner this.errorService.present(e) return EMPTY diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index 38ee4774b..acebdfdfb 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -714,7 +714,7 @@ export module Mock { value: 'https://guessagain.com', }, }, - } as any // @TODO why is this necessary? + } export const ConfigSpec: RR.GetPackageConfigRes['spec'] = { bitcoin: { @@ -1419,7 +1419,7 @@ export module Mock { health: {}, }, }, - actions: {}, // @TODO need mocks + actions: {}, serviceInterfaces: { ui: { id: 'ui', @@ -1618,7 +1618,7 @@ export module Mock { icon: 'assets/img/service-icons/btc-rpc-proxy.png', kind: 'exists', registryUrl: 'https://community-registry.start9.com', - versionSpec: '>2.0.0', // @TODO + versionSpec: '>2.0.0', configSatisfied: false, }, }, diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index ee96f9fe2..755799522 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -127,7 +127,7 @@ export const mockPatchData: DataModel = { }, }, }, - actions: {}, // @TODO + actions: {}, serviceInterfaces: { ui: { id: 'ui', diff --git a/web/projects/ui/src/app/services/patch-db/patch-db-source.ts b/web/projects/ui/src/app/services/patch-db/patch-db-source.ts index 803fbce2c..0b80370d0 100644 --- a/web/projects/ui/src/app/services/patch-db/patch-db-source.ts +++ b/web/projects/ui/src/app/services/patch-db/patch-db-source.ts @@ -41,6 +41,7 @@ export class PatchDbSource extends Observable[]> { catchError((_, original$) => { this.state.retrigger() + // @TODO this is returning right away, but we need to wait until state emits again from the retrigger() above. return this.state.pipe( filter(current => current === 'running'), take(1), From 2c255b6dfefb31e9ef9187cf8f54b8240a176049 Mon Sep 17 00:00:00 2001 From: Jade <2364004+Blu-J@users.noreply.github.com> Date: Mon, 24 Jun 2024 16:00:31 -0600 Subject: [PATCH 046/125] chore: Do some type cleanups (#2650) chore: fix the WithProcedureId --- .../src/service/service_effect_handler.rs | 124 ++++++++---------- sdk/lib/mainFn/Daemons.ts | 6 +- sdk/lib/osBindings/ExecuteAction.ts | 2 + sdk/lib/osBindings/ProcedureId.ts | 4 + sdk/lib/osBindings/SetDependenciesParams.ts | 2 + sdk/lib/osBindings/SetMainStatusStatus.ts | 2 +- sdk/lib/osBindings/index.ts | 1 + 7 files changed, 69 insertions(+), 72 deletions(-) create mode 100644 sdk/lib/osBindings/ProcedureId.ts diff --git a/core/startos/src/service/service_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs index bcd6da7d1..a61a00b9b 100644 --- a/core/startos/src/service/service_effect_handler.rs +++ b/core/startos/src/service/service_effect_handler.rs @@ -64,56 +64,6 @@ impl EffectContext { } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct WithProcedureId { - #[serde(default)] - procedure_id: Guid, - #[serde(flatten)] - rest: T, -} -impl FromArgMatches for WithProcedureId { - fn from_arg_matches(matches: &clap::ArgMatches) -> Result { - let rest = T::from_arg_matches(matches)?; - Ok(Self { - procedure_id: matches.get_one("procedure-id").cloned().unwrap_or_default(), - rest, - }) - } - fn from_arg_matches_mut(matches: &mut clap::ArgMatches) -> Result { - let rest = T::from_arg_matches_mut(matches)?; - Ok(Self { - procedure_id: matches.get_one("procedure-id").cloned().unwrap_or_default(), - rest, - }) - } - fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> { - self.rest.update_from_arg_matches(matches)?; - self.procedure_id = matches.get_one("procedure-id").cloned().unwrap_or_default(); - Ok(()) - } - fn update_from_arg_matches_mut( - &mut self, - matches: &mut clap::ArgMatches, - ) -> Result<(), clap::Error> { - self.rest.update_from_arg_matches_mut(matches)?; - self.procedure_id = matches.get_one("procedure-id").cloned().unwrap_or_default(); - Ok(()) - } -} -impl CommandFactory for WithProcedureId { - fn command() -> clap::Command { - T::command_for_update().arg( - clap::Arg::new("procedure-id") - .action(clap::ArgAction::Set) - .value_parser(clap::value_parser!(Guid)), - ) - } - fn command_for_update() -> clap::Command { - Self::command() - } -} - pub fn service_effect_handler() -> ParentHandler { ParentHandler::new() .subcommand("gitInfo", from_fn(|_: C| crate::version::git_info())) @@ -877,6 +827,8 @@ async fn exists(context: EffectContext, params: ParamsPackageId) -> Result, #[ts(type = "string")] @@ -886,15 +838,12 @@ struct ExecuteAction { } async fn execute_action( context: EffectContext, - WithProcedureId { + ExecuteAction { procedure_id, - rest: - ExecuteAction { - service_id, - action_id, - input, - }, - }: WithProcedureId, + service_id, + action_id, + input, + }: ExecuteAction, ) -> Result { let context = context.deref()?; let package_id = service_id @@ -950,9 +899,53 @@ async fn running(context: EffectContext, params: ParamsPackageId) -> Result Result { + Ok(Self { + procedure_id: matches.get_one("procedure-id").cloned().unwrap_or_default(), + }) + } + fn from_arg_matches_mut(matches: &mut clap::ArgMatches) -> Result { + Ok(Self { + procedure_id: matches.get_one("procedure-id").cloned().unwrap_or_default(), + }) + } + fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> { + self.procedure_id = matches.get_one("procedure-id").cloned().unwrap_or_default(); + Ok(()) + } + fn update_from_arg_matches_mut( + &mut self, + matches: &mut clap::ArgMatches, + ) -> Result<(), clap::Error> { + self.procedure_id = matches.get_one("procedure-id").cloned().unwrap_or_default(); + Ok(()) + } +} +impl CommandFactory for ProcedureId { + fn command() -> clap::Command { + Self::command_for_update().arg( + clap::Arg::new("procedure-id") + .action(clap::ArgAction::Set) + .value_parser(clap::value_parser!(Guid)), + ) + } + fn command_for_update() -> clap::Command { + Self::command() + } +} + async fn restart( context: EffectContext, - WithProcedureId { procedure_id, .. }: WithProcedureId, + ProcedureId { procedure_id }: ProcedureId, ) -> Result<(), Error> { let context = context.deref()?; context.restart(procedure_id).await?; @@ -961,7 +954,7 @@ async fn restart( async fn shutdown( context: EffectContext, - WithProcedureId { procedure_id, .. }: WithProcedureId, + ProcedureId { procedure_id }: ProcedureId, ) -> Result<(), Error> { let context = context.deref()?; context.stop(procedure_id).await?; @@ -1001,7 +994,6 @@ async fn set_configured(context: EffectContext, params: SetConfigured) -> Result enum SetMainStatusStatus { Running, Stopped, - Starting, } impl FromStr for SetMainStatusStatus { type Err = color_eyre::eyre::Report; @@ -1009,7 +1001,6 @@ impl FromStr for SetMainStatusStatus { match s { "running" => Ok(Self::Running), "stopped" => Ok(Self::Stopped), - "starting" => Ok(Self::Starting), _ => Err(eyre!("unknown status {s}")), } } @@ -1033,7 +1024,6 @@ async fn set_main_status(context: EffectContext, params: SetMainStatus) -> Resul match params.status { SetMainStatusStatus::Running => context.seed.started(), SetMainStatusStatus::Stopped => context.seed.stopped(), - SetMainStatusStatus::Starting => context.seed.started(), } Ok(Value::Null) } @@ -1262,15 +1252,17 @@ impl ValueParserFactory for DependencyRequirement { #[command(rename_all = "camelCase")] #[ts(export)] struct SetDependenciesParams { + #[serde(default)] + procedure_id: Guid, dependencies: Vec, } async fn set_dependencies( context: EffectContext, - WithProcedureId { + SetDependenciesParams { procedure_id, - rest: SetDependenciesParams { dependencies }, - }: WithProcedureId, + dependencies, + }: SetDependenciesParams, ) -> Result<(), Error> { let context = context.deref()?; let id = &context.seed.id; diff --git a/sdk/lib/mainFn/Daemons.ts b/sdk/lib/mainFn/Daemons.ts index f48dd51c0..059d148ab 100644 --- a/sdk/lib/mainFn/Daemons.ts +++ b/sdk/lib/mainFn/Daemons.ts @@ -168,10 +168,6 @@ export class Daemons { } private updateMainHealth() { - if (this.healthDaemons.every((x) => x.health.status === "success")) { - this.effects.setMainStatus({ status: "running" }) - } else { - this.effects.setMainStatus({ status: "starting" }) - } + this.effects.setMainStatus({ status: "running" }) } } diff --git a/sdk/lib/osBindings/ExecuteAction.ts b/sdk/lib/osBindings/ExecuteAction.ts index abd8c151f..b4eb60949 100644 --- a/sdk/lib/osBindings/ExecuteAction.ts +++ b/sdk/lib/osBindings/ExecuteAction.ts @@ -1,6 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Guid } from "./Guid" export type ExecuteAction = { + procedureId: Guid serviceId: string | null actionId: string input: any diff --git a/sdk/lib/osBindings/ProcedureId.ts b/sdk/lib/osBindings/ProcedureId.ts new file mode 100644 index 000000000..4d9a0debd --- /dev/null +++ b/sdk/lib/osBindings/ProcedureId.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Guid } from "./Guid" + +export type ProcedureId = { procedureId: Guid } diff --git a/sdk/lib/osBindings/SetDependenciesParams.ts b/sdk/lib/osBindings/SetDependenciesParams.ts index 7b34b50c9..bbc9b325f 100644 --- a/sdk/lib/osBindings/SetDependenciesParams.ts +++ b/sdk/lib/osBindings/SetDependenciesParams.ts @@ -1,6 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { DependencyRequirement } from "./DependencyRequirement" +import type { Guid } from "./Guid" export type SetDependenciesParams = { + procedureId: Guid dependencies: Array } diff --git a/sdk/lib/osBindings/SetMainStatusStatus.ts b/sdk/lib/osBindings/SetMainStatusStatus.ts index 6db32e7bf..03bb4a119 100644 --- a/sdk/lib/osBindings/SetMainStatusStatus.ts +++ b/sdk/lib/osBindings/SetMainStatusStatus.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type SetMainStatusStatus = "running" | "stopped" | "starting" +export type SetMainStatusStatus = "running" | "stopped" diff --git a/sdk/lib/osBindings/index.ts b/sdk/lib/osBindings/index.ts index ac2e19e45..22b03a7ca 100644 --- a/sdk/lib/osBindings/index.ts +++ b/sdk/lib/osBindings/index.ts @@ -108,6 +108,7 @@ export { PackageVersionInfo } from "./PackageVersionInfo" export { ParamsMaybePackageId } from "./ParamsMaybePackageId" export { ParamsPackageId } from "./ParamsPackageId" export { PasswordType } from "./PasswordType" +export { ProcedureId } from "./ProcedureId" export { Progress } from "./Progress" export { Public } from "./Public" export { RecoverySource } from "./RecoverySource" From 00f7fa507b1560393366eb6d29867ad855efcd9c Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Mon, 24 Jun 2024 16:15:32 -0600 Subject: [PATCH 047/125] remove analyticsd, clean up script --- core/build-analyticsd.sh | 42 ------ ...00f2b22433c86617a0dbf796bf2170186dd2e.json | 16 --- ...ea745ffa5da4479478c1fd2158a45324b1930.json | 14 -- ...36e463c20c841a7d6a0eb15be0f24f4a928ec.json | 40 ------ ...b8c606ca400e43e31b9a05d2937217e0f6962.json | 15 --- ...ef6cb0f1e9a4e158260700f1639dd4b438997.json | 34 ----- ...4f925f61dc5ea371903e62cdffa5d7b67ca96.json | 50 -------- ...c3afb81c2be5c681cfa8b4be0ce70610e9c3a.json | 14 -- ...a83dbc6afd07cae69d246987f62cf0cc35c2a.json | 20 --- ...bd8361776290a9411d527eaf1fdb40bef399d.json | 23 ---- ...9c1550a41ee580dad3f49d35cb843ebef10ca.json | 14 -- ...5b987c5af5807151ff71a59000014586752e0.json | 24 ---- ...d514a66c421a11ab04c26d89a7aa8f6b67210.json | 65 ---------- ...b98a73045adf618696cd838d79203ef5383fb.json | 19 --- ...9f96a8bdb0fc97ee8a3c6df1069e1e2b98576.json | 14 -- ...e3526b70bccf3e407327917294a993bc17ed5.json | 16 --- ...bbf6bea226d38efaf6559923c79a36d5ca08c.json | 64 --------- ...5ab58252362694cf0f56999bf60194d20583a.json | 44 ------- ...337527233c84eee758ac9be967906e5841d27.json | 14 -- ...9424e6b5a1d1402e028219e21726f1ebd282e.json | 32 ----- ...c4393a9a8fde2833cf096af766a979d94dee6.json | 18 --- ...9bd10d5717b2a0ddecf22330b7d531aac7c5d.json | 14 -- ...090d45cfa4c85168a587b1a41dc5393cccea1.json | 12 -- ...429d4c9b2cf534e76f35c80a2bf60e8599cca.json | 20 --- ...21ea72db8cfb362e7181c8226d9297507c62b.json | 19 --- ...4351ba0cae3825988b3e0ecd3b6781bcff524.json | 15 --- ...9e34aeaef1154dcd3d6811ab504915497ccf7.json | 14 -- ...f3ff3f9aed7f90027a9ba97634bcb47d772f0.json | 20 --- ...799bd4f82e761837b526a0972c3d4439a264d.json | 16 --- ...b81803f1e9ec9e8ebbf15cafddfc1c5a028ed.json | 40 ------ ...139c7b7569602adb58f2d6b3a94da4f167b0a.json | 14 -- ...4ac6a5f34dd820c4073b7728c7067aab9fded.json | 25 ---- ...67b9deb46da75788c7fe6082b43821c22d556.json | 16 --- ...cc96547f3f6dbec11bf2eadfaf53ad8ab51b5.json | 20 --- ...ad285680ce157aae9d7837ac020c8b2945e7f.json | 62 --------- core/startos/Cargo.toml | 5 - core/startos/src/analytics/context.rs | 121 ------------------ core/startos/src/analytics/mod.rs | 78 ----------- core/startos/src/bins/analyticsd.rs | 86 ------------- core/startos/src/bins/mod.rs | 4 - core/startos/src/context/config.rs | 3 - core/startos/src/context/rpc.rs | 2 - core/startos/src/lib.rs | 1 - core/startos/src/os_install/mod.rs | 4 - .../db => metrics-db}/registry-sqlx-data.sh | 10 +- .../db => metrics-db}/registry_schema.sql | 0 patch-db | 2 +- sdk/lib/osBindings/GetVersionParams.ts | 7 +- 48 files changed, 11 insertions(+), 1211 deletions(-) delete mode 100755 core/build-analyticsd.sh delete mode 100644 core/startos/.sqlx/query-1ce5254f27de971fd87f5ab66d300f2b22433c86617a0dbf796bf2170186dd2e.json delete mode 100644 core/startos/.sqlx/query-21471490cdc3adb206274cc68e1ea745ffa5da4479478c1fd2158a45324b1930.json delete mode 100644 core/startos/.sqlx/query-28ea34bbde836e0618c5fc9bb7c36e463c20c841a7d6a0eb15be0f24f4a928ec.json delete mode 100644 core/startos/.sqlx/query-350ab82048fb4a049042e4fdbe1b8c606ca400e43e31b9a05d2937217e0f6962.json delete mode 100644 core/startos/.sqlx/query-4099028a5c0de578255bf54a67cef6cb0f1e9a4e158260700f1639dd4b438997.json delete mode 100644 core/startos/.sqlx/query-4691e3a2ce80b59009ac17124f54f925f61dc5ea371903e62cdffa5d7b67ca96.json delete mode 100644 core/startos/.sqlx/query-4bcfbefb1eb3181343871a1cd7fc3afb81c2be5c681cfa8b4be0ce70610e9c3a.json delete mode 100644 core/startos/.sqlx/query-629be61c3c341c131ddbbff0293a83dbc6afd07cae69d246987f62cf0cc35c2a.json delete mode 100644 core/startos/.sqlx/query-687688055e63d27123cdc89a5bbbd8361776290a9411d527eaf1fdb40bef399d.json delete mode 100644 core/startos/.sqlx/query-6d35ccf780fb2bb62586dd1d3df9c1550a41ee580dad3f49d35cb843ebef10ca.json delete mode 100644 core/startos/.sqlx/query-770c1017734720453dc87b58c385b987c5af5807151ff71a59000014586752e0.json delete mode 100644 core/startos/.sqlx/query-7b64f032d507e8ffe37c41f4c7ad514a66c421a11ab04c26d89a7aa8f6b67210.json delete mode 100644 core/startos/.sqlx/query-7c7a3549c997eb75bf964ea65fbb98a73045adf618696cd838d79203ef5383fb.json delete mode 100644 core/startos/.sqlx/query-7e0649d839927e57fa03ee51a2c9f96a8bdb0fc97ee8a3c6df1069e1e2b98576.json delete mode 100644 core/startos/.sqlx/query-8951b9126fbf60dbb5997241e11e3526b70bccf3e407327917294a993bc17ed5.json delete mode 100644 core/startos/.sqlx/query-94d471bb374b4965c6cbedf8c17bbf6bea226d38efaf6559923c79a36d5ca08c.json delete mode 100644 core/startos/.sqlx/query-95c4ab4c645f3302568c6ff13d85ab58252362694cf0f56999bf60194d20583a.json delete mode 100644 core/startos/.sqlx/query-a60d6e66719325b08dc4ecfacaf337527233c84eee758ac9be967906e5841d27.json delete mode 100644 core/startos/.sqlx/query-a6b0c8909a3a5d6d9156aebfb359424e6b5a1d1402e028219e21726f1ebd282e.json delete mode 100644 core/startos/.sqlx/query-b1147beaaabbed89f2ab8c1e13ec4393a9a8fde2833cf096af766a979d94dee6.json delete mode 100644 core/startos/.sqlx/query-b203820ee1c553a4b246eac74b79bd10d5717b2a0ddecf22330b7d531aac7c5d.json delete mode 100644 core/startos/.sqlx/query-b81592b3a74940ab56d41537484090d45cfa4c85168a587b1a41dc5393cccea1.json delete mode 100644 core/startos/.sqlx/query-d5117054072476377f3c4f040ea429d4c9b2cf534e76f35c80a2bf60e8599cca.json delete mode 100644 core/startos/.sqlx/query-da71f94b29798d1738d2b10b9a721ea72db8cfb362e7181c8226d9297507c62b.json delete mode 100644 core/startos/.sqlx/query-dfc23b7e966c3853284753a7e934351ba0cae3825988b3e0ecd3b6781bcff524.json delete mode 100644 core/startos/.sqlx/query-e185203cf84e43b801dfb23b4159e34aeaef1154dcd3d6811ab504915497ccf7.json delete mode 100644 core/startos/.sqlx/query-e545696735f202f9d13cf22a561f3ff3f9aed7f90027a9ba97634bcb47d772f0.json delete mode 100644 core/startos/.sqlx/query-e5843c5b0e7819b29aa1abf2266799bd4f82e761837b526a0972c3d4439a264d.json delete mode 100644 core/startos/.sqlx/query-e95322a8e2ae3b93f1e974b24c0b81803f1e9ec9e8ebbf15cafddfc1c5a028ed.json delete mode 100644 core/startos/.sqlx/query-eb750adaa305bdbf3c5b70aaf59139c7b7569602adb58f2d6b3a94da4f167b0a.json delete mode 100644 core/startos/.sqlx/query-ecc765d8205c0876956f95f76944ac6a5f34dd820c4073b7728c7067aab9fded.json delete mode 100644 core/startos/.sqlx/query-f6d1c5ef0f9d9577bea8382318967b9deb46da75788c7fe6082b43821c22d556.json delete mode 100644 core/startos/.sqlx/query-f7d2dae84613bcef330f7403352cc96547f3f6dbec11bf2eadfaf53ad8ab51b5.json delete mode 100644 core/startos/.sqlx/query-fe6e4f09f3028e5b6b6259e86cbad285680ce157aae9d7837ac020c8b2945e7f.json delete mode 100644 core/startos/src/analytics/context.rs delete mode 100644 core/startos/src/analytics/mod.rs delete mode 100644 core/startos/src/bins/analyticsd.rs rename core/startos/src/registry/{os/version/db => metrics-db}/registry-sqlx-data.sh (65%) rename core/startos/src/registry/{os/version/db => metrics-db}/registry_schema.sql (100%) diff --git a/core/build-analyticsd.sh b/core/build-analyticsd.sh deleted file mode 100755 index 4f8578da1..000000000 --- a/core/build-analyticsd.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash - -cd "$(dirname "${BASH_SOURCE[0]}")" - -set -e -shopt -s expand_aliases - -if [ -z "$ARCH" ]; then - ARCH=$(uname -m) -fi - -USE_TTY= -if tty -s; then - USE_TTY="-it" -fi - -cd .. -FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')" -RUSTFLAGS="" - -if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then - RUSTFLAGS="--cfg tokio_unstable" -fi - -alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl' - -set +e -fail= -echo "FEATURES=\"$FEATURES\"" -echo "RUSTFLAGS=\"$RUSTFLAGS\"" -if ! rust-musl-builder sh -c "(cd core && cargo build --release --no-default-features --features analyticsd,$FEATURES --locked --bin analyticsd --target=$ARCH-unknown-linux-musl)"; then - fail=true -fi -set -e -cd core - -sudo chown -R $USER target -sudo chown -R $USER ~/.cargo - -if [ -n "$fail" ]; then - exit 1 -fi diff --git a/core/startos/.sqlx/query-1ce5254f27de971fd87f5ab66d300f2b22433c86617a0dbf796bf2170186dd2e.json b/core/startos/.sqlx/query-1ce5254f27de971fd87f5ab66d300f2b22433c86617a0dbf796bf2170186dd2e.json deleted file mode 100644 index d36100fef..000000000 --- a/core/startos/.sqlx/query-1ce5254f27de971fd87f5ab66d300f2b22433c86617a0dbf796bf2170186dd2e.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO network_keys (package, interface, key) VALUES ($1, $2, $3) ON CONFLICT (package, interface) DO NOTHING", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text", - "Bytea" - ] - }, - "nullable": [] - }, - "hash": "1ce5254f27de971fd87f5ab66d300f2b22433c86617a0dbf796bf2170186dd2e" -} diff --git a/core/startos/.sqlx/query-21471490cdc3adb206274cc68e1ea745ffa5da4479478c1fd2158a45324b1930.json b/core/startos/.sqlx/query-21471490cdc3adb206274cc68e1ea745ffa5da4479478c1fd2158a45324b1930.json deleted file mode 100644 index e0b1d7cf2..000000000 --- a/core/startos/.sqlx/query-21471490cdc3adb206274cc68e1ea745ffa5da4479478c1fd2158a45324b1930.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM ssh_keys WHERE fingerprint = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [] - }, - "hash": "21471490cdc3adb206274cc68e1ea745ffa5da4479478c1fd2158a45324b1930" -} diff --git a/core/startos/.sqlx/query-28ea34bbde836e0618c5fc9bb7c36e463c20c841a7d6a0eb15be0f24f4a928ec.json b/core/startos/.sqlx/query-28ea34bbde836e0618c5fc9bb7c36e463c20c841a7d6a0eb15be0f24f4a928ec.json deleted file mode 100644 index e234a72a9..000000000 --- a/core/startos/.sqlx/query-28ea34bbde836e0618c5fc9bb7c36e463c20c841a7d6a0eb15be0f24f4a928ec.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT hostname, path, username, password FROM cifs_shares WHERE id = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "hostname", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "path", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "username", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "password", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Int4" - ] - }, - "nullable": [ - false, - false, - false, - true - ] - }, - "hash": "28ea34bbde836e0618c5fc9bb7c36e463c20c841a7d6a0eb15be0f24f4a928ec" -} diff --git a/core/startos/.sqlx/query-350ab82048fb4a049042e4fdbe1b8c606ca400e43e31b9a05d2937217e0f6962.json b/core/startos/.sqlx/query-350ab82048fb4a049042e4fdbe1b8c606ca400e43e31b9a05d2937217e0f6962.json deleted file mode 100644 index c451ce9f3..000000000 --- a/core/startos/.sqlx/query-350ab82048fb4a049042e4fdbe1b8c606ca400e43e31b9a05d2937217e0f6962.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM tor WHERE package = $1 AND interface = $2", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "350ab82048fb4a049042e4fdbe1b8c606ca400e43e31b9a05d2937217e0f6962" -} diff --git a/core/startos/.sqlx/query-4099028a5c0de578255bf54a67cef6cb0f1e9a4e158260700f1639dd4b438997.json b/core/startos/.sqlx/query-4099028a5c0de578255bf54a67cef6cb0f1e9a4e158260700f1639dd4b438997.json deleted file mode 100644 index 761af064b..000000000 --- a/core/startos/.sqlx/query-4099028a5c0de578255bf54a67cef6cb0f1e9a4e158260700f1639dd4b438997.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT * FROM ssh_keys WHERE fingerprint = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "fingerprint", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "openssh_pubkey", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "created_at", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "4099028a5c0de578255bf54a67cef6cb0f1e9a4e158260700f1639dd4b438997" -} diff --git a/core/startos/.sqlx/query-4691e3a2ce80b59009ac17124f54f925f61dc5ea371903e62cdffa5d7b67ca96.json b/core/startos/.sqlx/query-4691e3a2ce80b59009ac17124f54f925f61dc5ea371903e62cdffa5d7b67ca96.json deleted file mode 100644 index 1f7edd1ce..000000000 --- a/core/startos/.sqlx/query-4691e3a2ce80b59009ac17124f54f925f61dc5ea371903e62cdffa5d7b67ca96.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT * FROM session WHERE logged_out IS NULL OR logged_out > CURRENT_TIMESTAMP", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "logged_in", - "type_info": "Timestamp" - }, - { - "ordinal": 2, - "name": "logged_out", - "type_info": "Timestamp" - }, - { - "ordinal": 3, - "name": "last_active", - "type_info": "Timestamp" - }, - { - "ordinal": 4, - "name": "user_agent", - "type_info": "Text" - }, - { - "ordinal": 5, - "name": "metadata", - "type_info": "Text" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - true, - false, - true, - false - ] - }, - "hash": "4691e3a2ce80b59009ac17124f54f925f61dc5ea371903e62cdffa5d7b67ca96" -} diff --git a/core/startos/.sqlx/query-4bcfbefb1eb3181343871a1cd7fc3afb81c2be5c681cfa8b4be0ce70610e9c3a.json b/core/startos/.sqlx/query-4bcfbefb1eb3181343871a1cd7fc3afb81c2be5c681cfa8b4be0ce70610e9c3a.json deleted file mode 100644 index 2157198e5..000000000 --- a/core/startos/.sqlx/query-4bcfbefb1eb3181343871a1cd7fc3afb81c2be5c681cfa8b4be0ce70610e9c3a.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE session SET logged_out = CURRENT_TIMESTAMP WHERE id = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [] - }, - "hash": "4bcfbefb1eb3181343871a1cd7fc3afb81c2be5c681cfa8b4be0ce70610e9c3a" -} diff --git a/core/startos/.sqlx/query-629be61c3c341c131ddbbff0293a83dbc6afd07cae69d246987f62cf0cc35c2a.json b/core/startos/.sqlx/query-629be61c3c341c131ddbbff0293a83dbc6afd07cae69d246987f62cf0cc35c2a.json deleted file mode 100644 index 764cff84a..000000000 --- a/core/startos/.sqlx/query-629be61c3c341c131ddbbff0293a83dbc6afd07cae69d246987f62cf0cc35c2a.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT password FROM account", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "password", - "type_info": "Text" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false - ] - }, - "hash": "629be61c3c341c131ddbbff0293a83dbc6afd07cae69d246987f62cf0cc35c2a" -} diff --git a/core/startos/.sqlx/query-687688055e63d27123cdc89a5bbbd8361776290a9411d527eaf1fdb40bef399d.json b/core/startos/.sqlx/query-687688055e63d27123cdc89a5bbbd8361776290a9411d527eaf1fdb40bef399d.json deleted file mode 100644 index 2e8a9ee0e..000000000 --- a/core/startos/.sqlx/query-687688055e63d27123cdc89a5bbbd8361776290a9411d527eaf1fdb40bef399d.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT key FROM tor WHERE package = $1 AND interface = $2", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "key", - "type_info": "Bytea" - } - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [ - false - ] - }, - "hash": "687688055e63d27123cdc89a5bbbd8361776290a9411d527eaf1fdb40bef399d" -} diff --git a/core/startos/.sqlx/query-6d35ccf780fb2bb62586dd1d3df9c1550a41ee580dad3f49d35cb843ebef10ca.json b/core/startos/.sqlx/query-6d35ccf780fb2bb62586dd1d3df9c1550a41ee580dad3f49d35cb843ebef10ca.json deleted file mode 100644 index 3f859bd10..000000000 --- a/core/startos/.sqlx/query-6d35ccf780fb2bb62586dd1d3df9c1550a41ee580dad3f49d35cb843ebef10ca.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE session SET last_active = CURRENT_TIMESTAMP WHERE id = $1 AND logged_out IS NULL OR logged_out > CURRENT_TIMESTAMP", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [] - }, - "hash": "6d35ccf780fb2bb62586dd1d3df9c1550a41ee580dad3f49d35cb843ebef10ca" -} diff --git a/core/startos/.sqlx/query-770c1017734720453dc87b58c385b987c5af5807151ff71a59000014586752e0.json b/core/startos/.sqlx/query-770c1017734720453dc87b58c385b987c5af5807151ff71a59000014586752e0.json deleted file mode 100644 index cf3591e01..000000000 --- a/core/startos/.sqlx/query-770c1017734720453dc87b58c385b987c5af5807151ff71a59000014586752e0.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO network_keys (package, interface, key) VALUES ($1, $2, $3) ON CONFLICT (package, interface) DO UPDATE SET package = EXCLUDED.package RETURNING key", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "key", - "type_info": "Bytea" - } - ], - "parameters": { - "Left": [ - "Text", - "Text", - "Bytea" - ] - }, - "nullable": [ - false - ] - }, - "hash": "770c1017734720453dc87b58c385b987c5af5807151ff71a59000014586752e0" -} diff --git a/core/startos/.sqlx/query-7b64f032d507e8ffe37c41f4c7ad514a66c421a11ab04c26d89a7aa8f6b67210.json b/core/startos/.sqlx/query-7b64f032d507e8ffe37c41f4c7ad514a66c421a11ab04c26d89a7aa8f6b67210.json deleted file mode 100644 index 53fc6f066..000000000 --- a/core/startos/.sqlx/query-7b64f032d507e8ffe37c41f4c7ad514a66c421a11ab04c26d89a7aa8f6b67210.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT id, package_id, created_at, code, level, title, message, data FROM notifications WHERE id < $1 ORDER BY id DESC LIMIT $2", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "package_id", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "created_at", - "type_info": "Timestamp" - }, - { - "ordinal": 3, - "name": "code", - "type_info": "Int4" - }, - { - "ordinal": 4, - "name": "level", - "type_info": "Text" - }, - { - "ordinal": 5, - "name": "title", - "type_info": "Text" - }, - { - "ordinal": 6, - "name": "message", - "type_info": "Text" - }, - { - "ordinal": 7, - "name": "data", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Int4", - "Int8" - ] - }, - "nullable": [ - false, - true, - false, - false, - false, - false, - false, - true - ] - }, - "hash": "7b64f032d507e8ffe37c41f4c7ad514a66c421a11ab04c26d89a7aa8f6b67210" -} diff --git a/core/startos/.sqlx/query-7c7a3549c997eb75bf964ea65fbb98a73045adf618696cd838d79203ef5383fb.json b/core/startos/.sqlx/query-7c7a3549c997eb75bf964ea65fbb98a73045adf618696cd838d79203ef5383fb.json deleted file mode 100644 index 245a838d8..000000000 --- a/core/startos/.sqlx/query-7c7a3549c997eb75bf964ea65fbb98a73045adf618696cd838d79203ef5383fb.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO account (\n id,\n server_id,\n hostname,\n password,\n network_key,\n root_ca_key_pem,\n root_ca_cert_pem\n ) VALUES (\n 0, $1, $2, $3, $4, $5, $6\n ) ON CONFLICT (id) DO UPDATE SET\n server_id = EXCLUDED.server_id,\n hostname = EXCLUDED.hostname,\n password = EXCLUDED.password,\n network_key = EXCLUDED.network_key,\n root_ca_key_pem = EXCLUDED.root_ca_key_pem,\n root_ca_cert_pem = EXCLUDED.root_ca_cert_pem\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text", - "Text", - "Bytea", - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "7c7a3549c997eb75bf964ea65fbb98a73045adf618696cd838d79203ef5383fb" -} diff --git a/core/startos/.sqlx/query-7e0649d839927e57fa03ee51a2c9f96a8bdb0fc97ee8a3c6df1069e1e2b98576.json b/core/startos/.sqlx/query-7e0649d839927e57fa03ee51a2c9f96a8bdb0fc97ee8a3c6df1069e1e2b98576.json deleted file mode 100644 index e3ce7957d..000000000 --- a/core/startos/.sqlx/query-7e0649d839927e57fa03ee51a2c9f96a8bdb0fc97ee8a3c6df1069e1e2b98576.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM tor WHERE package = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [] - }, - "hash": "7e0649d839927e57fa03ee51a2c9f96a8bdb0fc97ee8a3c6df1069e1e2b98576" -} diff --git a/core/startos/.sqlx/query-8951b9126fbf60dbb5997241e11e3526b70bccf3e407327917294a993bc17ed5.json b/core/startos/.sqlx/query-8951b9126fbf60dbb5997241e11e3526b70bccf3e407327917294a993bc17ed5.json deleted file mode 100644 index e39aebf69..000000000 --- a/core/startos/.sqlx/query-8951b9126fbf60dbb5997241e11e3526b70bccf3e407327917294a993bc17ed5.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO tor (package, interface, key) VALUES ($1, $2, $3) ON CONFLICT (package, interface) DO NOTHING", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text", - "Bytea" - ] - }, - "nullable": [] - }, - "hash": "8951b9126fbf60dbb5997241e11e3526b70bccf3e407327917294a993bc17ed5" -} diff --git a/core/startos/.sqlx/query-94d471bb374b4965c6cbedf8c17bbf6bea226d38efaf6559923c79a36d5ca08c.json b/core/startos/.sqlx/query-94d471bb374b4965c6cbedf8c17bbf6bea226d38efaf6559923c79a36d5ca08c.json deleted file mode 100644 index e7fe8d38c..000000000 --- a/core/startos/.sqlx/query-94d471bb374b4965c6cbedf8c17bbf6bea226d38efaf6559923c79a36d5ca08c.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT id, package_id, created_at, code, level, title, message, data FROM notifications ORDER BY id DESC LIMIT $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "package_id", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "created_at", - "type_info": "Timestamp" - }, - { - "ordinal": 3, - "name": "code", - "type_info": "Int4" - }, - { - "ordinal": 4, - "name": "level", - "type_info": "Text" - }, - { - "ordinal": 5, - "name": "title", - "type_info": "Text" - }, - { - "ordinal": 6, - "name": "message", - "type_info": "Text" - }, - { - "ordinal": 7, - "name": "data", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - true, - false, - false, - false, - false, - false, - true - ] - }, - "hash": "94d471bb374b4965c6cbedf8c17bbf6bea226d38efaf6559923c79a36d5ca08c" -} diff --git a/core/startos/.sqlx/query-95c4ab4c645f3302568c6ff13d85ab58252362694cf0f56999bf60194d20583a.json b/core/startos/.sqlx/query-95c4ab4c645f3302568c6ff13d85ab58252362694cf0f56999bf60194d20583a.json deleted file mode 100644 index aadc0fc3a..000000000 --- a/core/startos/.sqlx/query-95c4ab4c645f3302568c6ff13d85ab58252362694cf0f56999bf60194d20583a.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT id, hostname, path, username, password FROM cifs_shares", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "hostname", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "path", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "username", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "password", - "type_info": "Text" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false, - false, - true - ] - }, - "hash": "95c4ab4c645f3302568c6ff13d85ab58252362694cf0f56999bf60194d20583a" -} diff --git a/core/startos/.sqlx/query-a60d6e66719325b08dc4ecfacaf337527233c84eee758ac9be967906e5841d27.json b/core/startos/.sqlx/query-a60d6e66719325b08dc4ecfacaf337527233c84eee758ac9be967906e5841d27.json deleted file mode 100644 index c56a9ebd1..000000000 --- a/core/startos/.sqlx/query-a60d6e66719325b08dc4ecfacaf337527233c84eee758ac9be967906e5841d27.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM cifs_shares WHERE id = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int4" - ] - }, - "nullable": [] - }, - "hash": "a60d6e66719325b08dc4ecfacaf337527233c84eee758ac9be967906e5841d27" -} diff --git a/core/startos/.sqlx/query-a6b0c8909a3a5d6d9156aebfb359424e6b5a1d1402e028219e21726f1ebd282e.json b/core/startos/.sqlx/query-a6b0c8909a3a5d6d9156aebfb359424e6b5a1d1402e028219e21726f1ebd282e.json deleted file mode 100644 index 86bd9250e..000000000 --- a/core/startos/.sqlx/query-a6b0c8909a3a5d6d9156aebfb359424e6b5a1d1402e028219e21726f1ebd282e.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT fingerprint, openssh_pubkey, created_at FROM ssh_keys", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "fingerprint", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "openssh_pubkey", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "created_at", - "type_info": "Text" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "a6b0c8909a3a5d6d9156aebfb359424e6b5a1d1402e028219e21726f1ebd282e" -} diff --git a/core/startos/.sqlx/query-b1147beaaabbed89f2ab8c1e13ec4393a9a8fde2833cf096af766a979d94dee6.json b/core/startos/.sqlx/query-b1147beaaabbed89f2ab8c1e13ec4393a9a8fde2833cf096af766a979d94dee6.json deleted file mode 100644 index c8ff84277..000000000 --- a/core/startos/.sqlx/query-b1147beaaabbed89f2ab8c1e13ec4393a9a8fde2833cf096af766a979d94dee6.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE cifs_shares SET hostname = $1, path = $2, username = $3, password = $4 WHERE id = $5", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text", - "Text", - "Text", - "Int4" - ] - }, - "nullable": [] - }, - "hash": "b1147beaaabbed89f2ab8c1e13ec4393a9a8fde2833cf096af766a979d94dee6" -} diff --git a/core/startos/.sqlx/query-b203820ee1c553a4b246eac74b79bd10d5717b2a0ddecf22330b7d531aac7c5d.json b/core/startos/.sqlx/query-b203820ee1c553a4b246eac74b79bd10d5717b2a0ddecf22330b7d531aac7c5d.json deleted file mode 100644 index b76542db8..000000000 --- a/core/startos/.sqlx/query-b203820ee1c553a4b246eac74b79bd10d5717b2a0ddecf22330b7d531aac7c5d.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM network_keys WHERE package = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [] - }, - "hash": "b203820ee1c553a4b246eac74b79bd10d5717b2a0ddecf22330b7d531aac7c5d" -} diff --git a/core/startos/.sqlx/query-b81592b3a74940ab56d41537484090d45cfa4c85168a587b1a41dc5393cccea1.json b/core/startos/.sqlx/query-b81592b3a74940ab56d41537484090d45cfa4c85168a587b1a41dc5393cccea1.json deleted file mode 100644 index e2e8a1620..000000000 --- a/core/startos/.sqlx/query-b81592b3a74940ab56d41537484090d45cfa4c85168a587b1a41dc5393cccea1.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE account SET tor_key = NULL, network_key = gen_random_bytes(32)", - "describe": { - "columns": [], - "parameters": { - "Left": [] - }, - "nullable": [] - }, - "hash": "b81592b3a74940ab56d41537484090d45cfa4c85168a587b1a41dc5393cccea1" -} diff --git a/core/startos/.sqlx/query-d5117054072476377f3c4f040ea429d4c9b2cf534e76f35c80a2bf60e8599cca.json b/core/startos/.sqlx/query-d5117054072476377f3c4f040ea429d4c9b2cf534e76f35c80a2bf60e8599cca.json deleted file mode 100644 index b77ba7ce9..000000000 --- a/core/startos/.sqlx/query-d5117054072476377f3c4f040ea429d4c9b2cf534e76f35c80a2bf60e8599cca.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT openssh_pubkey FROM ssh_keys", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "openssh_pubkey", - "type_info": "Text" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false - ] - }, - "hash": "d5117054072476377f3c4f040ea429d4c9b2cf534e76f35c80a2bf60e8599cca" -} diff --git a/core/startos/.sqlx/query-da71f94b29798d1738d2b10b9a721ea72db8cfb362e7181c8226d9297507c62b.json b/core/startos/.sqlx/query-da71f94b29798d1738d2b10b9a721ea72db8cfb362e7181c8226d9297507c62b.json deleted file mode 100644 index 5c5c89c27..000000000 --- a/core/startos/.sqlx/query-da71f94b29798d1738d2b10b9a721ea72db8cfb362e7181c8226d9297507c62b.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO notifications (package_id, code, level, title, message, data) VALUES ($1, $2, $3, $4, $5, $6)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Int4", - "Text", - "Text", - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "da71f94b29798d1738d2b10b9a721ea72db8cfb362e7181c8226d9297507c62b" -} diff --git a/core/startos/.sqlx/query-dfc23b7e966c3853284753a7e934351ba0cae3825988b3e0ecd3b6781bcff524.json b/core/startos/.sqlx/query-dfc23b7e966c3853284753a7e934351ba0cae3825988b3e0ecd3b6781bcff524.json deleted file mode 100644 index 2fc8ad1ba..000000000 --- a/core/startos/.sqlx/query-dfc23b7e966c3853284753a7e934351ba0cae3825988b3e0ecd3b6781bcff524.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM network_keys WHERE package = $1 AND interface = $2", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "dfc23b7e966c3853284753a7e934351ba0cae3825988b3e0ecd3b6781bcff524" -} diff --git a/core/startos/.sqlx/query-e185203cf84e43b801dfb23b4159e34aeaef1154dcd3d6811ab504915497ccf7.json b/core/startos/.sqlx/query-e185203cf84e43b801dfb23b4159e34aeaef1154dcd3d6811ab504915497ccf7.json deleted file mode 100644 index a4dc187cd..000000000 --- a/core/startos/.sqlx/query-e185203cf84e43b801dfb23b4159e34aeaef1154dcd3d6811ab504915497ccf7.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM notifications WHERE id = $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int4" - ] - }, - "nullable": [] - }, - "hash": "e185203cf84e43b801dfb23b4159e34aeaef1154dcd3d6811ab504915497ccf7" -} diff --git a/core/startos/.sqlx/query-e545696735f202f9d13cf22a561f3ff3f9aed7f90027a9ba97634bcb47d772f0.json b/core/startos/.sqlx/query-e545696735f202f9d13cf22a561f3ff3f9aed7f90027a9ba97634bcb47d772f0.json deleted file mode 100644 index 97a4ec95a..000000000 --- a/core/startos/.sqlx/query-e545696735f202f9d13cf22a561f3ff3f9aed7f90027a9ba97634bcb47d772f0.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT tor_key FROM account WHERE id = 0", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "tor_key", - "type_info": "Bytea" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - true - ] - }, - "hash": "e545696735f202f9d13cf22a561f3ff3f9aed7f90027a9ba97634bcb47d772f0" -} diff --git a/core/startos/.sqlx/query-e5843c5b0e7819b29aa1abf2266799bd4f82e761837b526a0972c3d4439a264d.json b/core/startos/.sqlx/query-e5843c5b0e7819b29aa1abf2266799bd4f82e761837b526a0972c3d4439a264d.json deleted file mode 100644 index b2aa04370..000000000 --- a/core/startos/.sqlx/query-e5843c5b0e7819b29aa1abf2266799bd4f82e761837b526a0972c3d4439a264d.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO session (id, user_agent, metadata) VALUES ($1, $2, $3)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "e5843c5b0e7819b29aa1abf2266799bd4f82e761837b526a0972c3d4439a264d" -} diff --git a/core/startos/.sqlx/query-e95322a8e2ae3b93f1e974b24c0b81803f1e9ec9e8ebbf15cafddfc1c5a028ed.json b/core/startos/.sqlx/query-e95322a8e2ae3b93f1e974b24c0b81803f1e9ec9e8ebbf15cafddfc1c5a028ed.json deleted file mode 100644 index fd5a467ec..000000000 --- a/core/startos/.sqlx/query-e95322a8e2ae3b93f1e974b24c0b81803f1e9ec9e8ebbf15cafddfc1c5a028ed.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT\n network_keys.package,\n network_keys.interface,\n network_keys.key,\n tor.key AS \"tor_key?\"\n FROM\n network_keys\n LEFT JOIN\n tor\n ON\n network_keys.package = tor.package\n AND\n network_keys.interface = tor.interface\n WHERE\n network_keys.package = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "package", - "type_info": "Text" - }, - { - "ordinal": 1, - "name": "interface", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "key", - "type_info": "Bytea" - }, - { - "ordinal": 3, - "name": "tor_key?", - "type_info": "Bytea" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false - ] - }, - "hash": "e95322a8e2ae3b93f1e974b24c0b81803f1e9ec9e8ebbf15cafddfc1c5a028ed" -} diff --git a/core/startos/.sqlx/query-eb750adaa305bdbf3c5b70aaf59139c7b7569602adb58f2d6b3a94da4f167b0a.json b/core/startos/.sqlx/query-eb750adaa305bdbf3c5b70aaf59139c7b7569602adb58f2d6b3a94da4f167b0a.json deleted file mode 100644 index fb8a7c1e5..000000000 --- a/core/startos/.sqlx/query-eb750adaa305bdbf3c5b70aaf59139c7b7569602adb58f2d6b3a94da4f167b0a.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM notifications WHERE id < $1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int4" - ] - }, - "nullable": [] - }, - "hash": "eb750adaa305bdbf3c5b70aaf59139c7b7569602adb58f2d6b3a94da4f167b0a" -} diff --git a/core/startos/.sqlx/query-ecc765d8205c0876956f95f76944ac6a5f34dd820c4073b7728c7067aab9fded.json b/core/startos/.sqlx/query-ecc765d8205c0876956f95f76944ac6a5f34dd820c4073b7728c7067aab9fded.json deleted file mode 100644 index 27c9752b2..000000000 --- a/core/startos/.sqlx/query-ecc765d8205c0876956f95f76944ac6a5f34dd820c4073b7728c7067aab9fded.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO cifs_shares (hostname, path, username, password) VALUES ($1, $2, $3, $4) RETURNING id", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Text", - "Text", - "Text", - "Text" - ] - }, - "nullable": [ - false - ] - }, - "hash": "ecc765d8205c0876956f95f76944ac6a5f34dd820c4073b7728c7067aab9fded" -} diff --git a/core/startos/.sqlx/query-f6d1c5ef0f9d9577bea8382318967b9deb46da75788c7fe6082b43821c22d556.json b/core/startos/.sqlx/query-f6d1c5ef0f9d9577bea8382318967b9deb46da75788c7fe6082b43821c22d556.json deleted file mode 100644 index 6ed9898f6..000000000 --- a/core/startos/.sqlx/query-f6d1c5ef0f9d9577bea8382318967b9deb46da75788c7fe6082b43821c22d556.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO ssh_keys (fingerprint, openssh_pubkey, created_at) VALUES ($1, $2, $3)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text", - "Text", - "Text" - ] - }, - "nullable": [] - }, - "hash": "f6d1c5ef0f9d9577bea8382318967b9deb46da75788c7fe6082b43821c22d556" -} diff --git a/core/startos/.sqlx/query-f7d2dae84613bcef330f7403352cc96547f3f6dbec11bf2eadfaf53ad8ab51b5.json b/core/startos/.sqlx/query-f7d2dae84613bcef330f7403352cc96547f3f6dbec11bf2eadfaf53ad8ab51b5.json deleted file mode 100644 index f48ccb074..000000000 --- a/core/startos/.sqlx/query-f7d2dae84613bcef330f7403352cc96547f3f6dbec11bf2eadfaf53ad8ab51b5.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT network_key FROM account WHERE id = 0", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "network_key", - "type_info": "Bytea" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false - ] - }, - "hash": "f7d2dae84613bcef330f7403352cc96547f3f6dbec11bf2eadfaf53ad8ab51b5" -} diff --git a/core/startos/.sqlx/query-fe6e4f09f3028e5b6b6259e86cbad285680ce157aae9d7837ac020c8b2945e7f.json b/core/startos/.sqlx/query-fe6e4f09f3028e5b6b6259e86cbad285680ce157aae9d7837ac020c8b2945e7f.json deleted file mode 100644 index 6ef1d5023..000000000 --- a/core/startos/.sqlx/query-fe6e4f09f3028e5b6b6259e86cbad285680ce157aae9d7837ac020c8b2945e7f.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT * FROM account WHERE id = 0", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "password", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "tor_key", - "type_info": "Bytea" - }, - { - "ordinal": 3, - "name": "server_id", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "hostname", - "type_info": "Text" - }, - { - "ordinal": 5, - "name": "network_key", - "type_info": "Bytea" - }, - { - "ordinal": 6, - "name": "root_ca_key_pem", - "type_info": "Text" - }, - { - "ordinal": 7, - "name": "root_ca_cert_pem", - "type_info": "Text" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - true, - true, - true, - false, - false, - false - ] - }, - "hash": "fe6e4f09f3028e5b6b6259e86cbad285680ce157aae9d7837ac020c8b2945e7f" -} diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index fa2bac69d..539f3de11 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -37,16 +37,11 @@ path = "src/main.rs" name = "registrybox" path = "src/main.rs" -[[bin]] -name = "analyticsd" -path = "src/main.rs" - [features] cli = [] container-runtime = [] daemon = [] registry = [] -analyticsd = [] default = ["cli", "daemon"] dev = [] unstable = ["console-subscriber", "tokio/tracing"] diff --git a/core/startos/src/analytics/context.rs b/core/startos/src/analytics/context.rs deleted file mode 100644 index 8ece8479b..000000000 --- a/core/startos/src/analytics/context.rs +++ /dev/null @@ -1,121 +0,0 @@ -use std::net::{Ipv4Addr, SocketAddr}; -use std::ops::Deref; -use std::path::PathBuf; -use std::sync::Arc; - -use clap::Parser; -use reqwest::{Client, Proxy}; -use rpc_toolkit::yajrc::RpcError; -use rpc_toolkit::{call_remote_http, CallRemote, Context, Empty}; -use serde::{Deserialize, Serialize}; -use sqlx::PgPool; -use tokio::sync::broadcast::Sender; -use tracing::instrument; -use url::Url; - -use crate::context::config::{ContextConfig, CONFIG_PATH}; -use crate::context::RpcContext; -use crate::prelude::*; -use crate::rpc_continuations::RpcContinuations; - -#[derive(Debug, Clone, Default, Deserialize, Serialize, Parser)] -#[serde(rename_all = "kebab-case")] -#[command(rename_all = "kebab-case")] -pub struct AnalyticsConfig { - #[arg(short = 'c', long = "config")] - pub config: Option, - #[arg(short = 'l', long = "listen")] - pub listen: Option, - #[arg(short = 'p', long = "proxy")] - pub tor_proxy: Option, - #[arg(short = 'd', long = "dbconnect")] - pub dbconnect: Option, -} -impl ContextConfig for AnalyticsConfig { - fn next(&mut self) -> Option { - self.config.take() - } - fn merge_with(&mut self, other: Self) { - self.listen = self.listen.take().or(other.listen); - self.tor_proxy = self.tor_proxy.take().or(other.tor_proxy); - self.dbconnect = self.dbconnect.take().or(other.dbconnect); - } -} - -impl AnalyticsConfig { - pub fn load(mut self) -> Result { - let path = self.next(); - self.load_path_rec(path)?; - self.load_path_rec(Some(CONFIG_PATH))?; - Ok(self) - } -} - -pub struct AnalyticsContextSeed { - pub listen: SocketAddr, - pub db: PgPool, - pub rpc_continuations: RpcContinuations, - pub client: Client, - pub shutdown: Sender<()>, -} - -#[derive(Clone)] -pub struct AnalyticsContext(Arc); -impl AnalyticsContext { - #[instrument(skip_all)] - pub async fn init(config: &AnalyticsConfig) -> Result { - let (shutdown, _) = tokio::sync::broadcast::channel(1); - let dbconnect = config - .dbconnect - .clone() - .unwrap_or_else(|| "postgres://localhost/analytics".parse().unwrap()) - .to_owned(); - let db = PgPool::connect(dbconnect.as_str()).await?; - let tor_proxy_url = config - .tor_proxy - .clone() - .map(Ok) - .unwrap_or_else(|| "socks5h://localhost:9050".parse())?; - Ok(Self(Arc::new(AnalyticsContextSeed { - listen: config - .listen - .unwrap_or(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 5959)), - db, - rpc_continuations: RpcContinuations::new(), - client: Client::builder() - .proxy(Proxy::custom(move |url| { - if url.host_str().map_or(false, |h| h.ends_with(".onion")) { - Some(tor_proxy_url.clone()) - } else { - None - } - })) - .build() - .with_kind(crate::ErrorKind::ParseUrl)?, - shutdown, - }))) - } -} -impl AsRef for AnalyticsContext { - fn as_ref(&self) -> &RpcContinuations { - &self.rpc_continuations - } -} - -impl Context for AnalyticsContext {} -impl Deref for AnalyticsContext { - type Target = AnalyticsContextSeed; - fn deref(&self) -> &Self::Target { - &*self.0 - } -} - -impl CallRemote for RpcContext { - async fn call_remote(&self, method: &str, params: Value, _: Empty) -> Result { - if let Some(analytics_url) = self.analytics_url.clone() { - call_remote_http(&self.client, analytics_url, method, params).await - } else { - Ok(Value::Null) - } - } -} diff --git a/core/startos/src/analytics/mod.rs b/core/startos/src/analytics/mod.rs deleted file mode 100644 index 7c84ff386..000000000 --- a/core/startos/src/analytics/mod.rs +++ /dev/null @@ -1,78 +0,0 @@ -use std::net::SocketAddr; - -use axum::Router; -use futures::future::ready; -use rpc_toolkit::{Context, ParentHandler, Server}; - -use crate::analytics::context::AnalyticsContext; -use crate::middleware::cors::Cors; -use crate::net::static_server::{bad_request, not_found, server_error}; -use crate::net::web_server::WebServer; -use crate::rpc_continuations::Guid; - -pub mod context; - -pub fn analytics_api() -> ParentHandler { - ParentHandler::new() -} - -pub fn analytics_server_router(ctx: AnalyticsContext) -> Router { - use axum::extract as x; - use axum::routing::{any, get, post}; - Router::new() - .route("/rpc/*path", { - let ctx = ctx.clone(); - post( - Server::new(move || ready(Ok(ctx.clone())), analytics_api()) - .middleware(Cors::new()) - ) - }) - .route( - "/ws/rpc/*path", - get({ - let ctx = ctx.clone(); - move |x::Path(path): x::Path, - ws: axum::extract::ws::WebSocketUpgrade| async move { - match Guid::from(&path) { - None => { - tracing::debug!("No Guid Path"); - bad_request() - } - Some(guid) => match ctx.rpc_continuations.get_ws_handler(&guid).await { - Some(cont) => ws.on_upgrade(cont), - _ => not_found(), - }, - } - } - }), - ) - .route( - "/rest/rpc/*path", - any({ - let ctx = ctx.clone(); - move |request: x::Request| async move { - let path = request - .uri() - .path() - .strip_prefix("/rest/rpc/") - .unwrap_or_default(); - match Guid::from(&path) { - None => { - tracing::debug!("No Guid Path"); - bad_request() - } - Some(guid) => match ctx.rpc_continuations.get_rest_handler(&guid).await { - None => not_found(), - Some(cont) => cont(request).await.unwrap_or_else(server_error), - }, - } - } - }), - ) -} - -impl WebServer { - pub fn analytics(bind: SocketAddr, ctx: AnalyticsContext) -> Self { - Self::new(bind, analytics_server_router(ctx)) - } -} diff --git a/core/startos/src/bins/analyticsd.rs b/core/startos/src/bins/analyticsd.rs deleted file mode 100644 index e37a7dae1..000000000 --- a/core/startos/src/bins/analyticsd.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::ffi::OsString; - -use clap::Parser; -use futures::FutureExt; -use tokio::signal::unix::signal; -use tracing::instrument; - -use crate::analytics::context::{AnalyticsConfig, AnalyticsContext}; -use crate::net::web_server::WebServer; -use crate::prelude::*; -use crate::util::logger::EmbassyLogger; - -#[instrument(skip_all)] -async fn inner_main(config: &AnalyticsConfig) -> Result<(), Error> { - let server = async { - let ctx = AnalyticsContext::init(config).await?; - let server = WebServer::analytics(ctx.listen, ctx.clone()); - - let mut shutdown_recv = ctx.shutdown.subscribe(); - - let sig_handler_ctx = ctx; - let sig_handler = tokio::spawn(async move { - use tokio::signal::unix::SignalKind; - futures::future::select_all( - [ - SignalKind::interrupt(), - SignalKind::quit(), - SignalKind::terminate(), - ] - .iter() - .map(|s| { - async move { - signal(*s) - .unwrap_or_else(|_| panic!("register {:?} handler", s)) - .recv() - .await - } - .boxed() - }), - ) - .await; - sig_handler_ctx - .shutdown - .send(()) - .map_err(|_| ()) - .expect("send shutdown signal"); - }); - - shutdown_recv - .recv() - .await - .with_kind(crate::ErrorKind::Unknown)?; - - sig_handler.abort(); - - Ok::<_, Error>(server) - } - .await?; - server.shutdown().await; - - Ok(()) -} - -pub fn main(args: impl IntoIterator) { - EmbassyLogger::init(); - - let config = AnalyticsConfig::parse_from(args).load().unwrap(); - - let res = { - let rt = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .expect("failed to initialize runtime"); - rt.block_on(inner_main(&config)) - }; - - match res { - Ok(()) => (), - Err(e) => { - eprintln!("{}", e.source); - tracing::debug!("{:?}", e.source); - drop(e.source); - std::process::exit(e.kind as i32) - } - } -} diff --git a/core/startos/src/bins/mod.rs b/core/startos/src/bins/mod.rs index 70f87aa99..4a4670a5b 100644 --- a/core/startos/src/bins/mod.rs +++ b/core/startos/src/bins/mod.rs @@ -2,8 +2,6 @@ use std::collections::VecDeque; use std::ffi::OsString; use std::path::Path; -#[cfg(feature = "analytics")] -pub mod analytics; #[cfg(feature = "container-runtime")] pub mod container_cli; pub mod deprecated; @@ -26,8 +24,6 @@ fn select_executable(name: &str) -> Option)> { "startd" => Some(startd::main), #[cfg(feature = "registry")] "registry" => Some(registry::main), - #[cfg(feature = "analyticsd")] - "analyticsd" => Some(analyticsd::main), "embassy-cli" => Some(|_| deprecated::renamed("embassy-cli", "start-cli")), "embassy-sdk" => Some(|_| deprecated::renamed("embassy-sdk", "start-sdk")), "embassyd" => Some(|_| deprecated::renamed("embassyd", "startd")), diff --git a/core/startos/src/context/config.rs b/core/startos/src/context/config.rs index 886361777..bc2da00e2 100644 --- a/core/startos/src/context/config.rs +++ b/core/startos/src/context/config.rs @@ -113,8 +113,6 @@ pub struct ServerConfig { pub datadir: Option, #[arg(long = "disable-encryption")] pub disable_encryption: Option, - #[arg(short = 'a', long = "analytics-url")] - pub analytics_url: Option, } impl ContextConfig for ServerConfig { fn next(&mut self) -> Option { @@ -133,7 +131,6 @@ impl ContextConfig for ServerConfig { .or(other.revision_cache_size); self.datadir = self.datadir.take().or(other.datadir); self.disable_encryption = self.disable_encryption.take().or(other.disable_encryption); - self.analytics_url = self.analytics_url.take().or(other.analytics_url); } } diff --git a/core/startos/src/context/rpc.rs b/core/startos/src/context/rpc.rs index 1042def74..cac1b46e0 100644 --- a/core/startos/src/context/rpc.rs +++ b/core/startos/src/context/rpc.rs @@ -58,7 +58,6 @@ pub struct RpcContextSeed { pub start_time: Instant, #[cfg(feature = "dev")] pub dev: Dev, - pub analytics_url: Option, } pub struct Dev { @@ -191,7 +190,6 @@ impl RpcContext { dev: Dev { lxc: Mutex::new(BTreeMap::new()), }, - analytics_url: config.analytics_url.clone(), }); let res = Self(seed.clone()); diff --git a/core/startos/src/lib.rs b/core/startos/src/lib.rs index 96210114f..228ac27c8 100644 --- a/core/startos/src/lib.rs +++ b/core/startos/src/lib.rs @@ -24,7 +24,6 @@ lazy_static::lazy_static! { pub mod account; pub mod action; -pub mod analytics; pub mod auth; pub mod backup; pub mod bins; diff --git a/core/startos/src/os_install/mod.rs b/core/startos/src/os_install/mod.rs index b1d9ef1c4..c3931b236 100644 --- a/core/startos/src/os_install/mod.rs +++ b/core/startos/src/os_install/mod.rs @@ -281,10 +281,6 @@ pub async fn execute( IoFormat::Yaml.to_vec(&ServerConfig { os_partitions: Some(part_info.clone()), ethernet_interface: Some(eth_iface), - #[cfg(feature = "dev")] - analytics_url: None, - #[cfg(not(feature = "dev"))] - analytics_url: Some("https://analytics.start9.com".parse()?), // TODO: FullMetal ..Default::default() })?, ) diff --git a/core/startos/src/registry/os/version/db/registry-sqlx-data.sh b/core/startos/src/registry/metrics-db/registry-sqlx-data.sh similarity index 65% rename from core/startos/src/registry/os/version/db/registry-sqlx-data.sh rename to core/startos/src/registry/metrics-db/registry-sqlx-data.sh index 2a422bf38..2b24873a4 100755 --- a/core/startos/src/registry/os/version/db/registry-sqlx-data.sh +++ b/core/startos/src/registry/metrics-db/registry-sqlx-data.sh @@ -1,6 +1,6 @@ #!/bin/bash -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +cd "$(dirname "${BASH_SOURCE[0]}")" TMP_DIR=$(mktemp -d) mkdir $TMP_DIR/pgdata docker run -d --rm --name=tmp_postgres -e POSTGRES_PASSWORD=password -v $TMP_DIR/pgdata:/var/lib/postgresql/data postgres @@ -8,17 +8,15 @@ docker run -d --rm --name=tmp_postgres -e POSTGRES_PASSWORD=password -v $TMP_DIR ( set -e ctr=0 - until docker exec tmp_postgres psql -U postgres || [ $ctr -ge 5 ]; do + until docker exec tmp_postgres psql -U postgres 2> /dev/null || [ $ctr -ge 5 ]; do ctr=$[ctr + 1] sleep 5; done PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' tmp_postgres) - - SCHEMA_DUMP="registry_schema.sql" - DATABASE_URL=postgres://postgres:password@$PG_IP/postgres - psql $DATABASE_URL -f "$SCRIPT_DIR/$SCHEMA_DUMP" + cat "./registry_schema.sql" | docker exec -i tmp_postgres psql -U postgres -d postgres -f- + cd ../../.. DATABASE_URL=postgres://postgres:password@$PG_IP/postgres PLATFORM=$(uname -m) cargo sqlx prepare -- --lib --profile=test --workspace echo "Subscript Complete" ) diff --git a/core/startos/src/registry/os/version/db/registry_schema.sql b/core/startos/src/registry/metrics-db/registry_schema.sql similarity index 100% rename from core/startos/src/registry/os/version/db/registry_schema.sql rename to core/startos/src/registry/metrics-db/registry_schema.sql diff --git a/patch-db b/patch-db index 88a804f56..99076d349 160000 --- a/patch-db +++ b/patch-db @@ -1 +1 @@ -Subproject commit 88a804f56f446d34896ef331d915b821a581cf01 +Subproject commit 99076d349c6768000483ea8d47216d273586552e diff --git a/sdk/lib/osBindings/GetVersionParams.ts b/sdk/lib/osBindings/GetVersionParams.ts index 71762c8dd..853d76022 100644 --- a/sdk/lib/osBindings/GetVersionParams.ts +++ b/sdk/lib/osBindings/GetVersionParams.ts @@ -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 GetVersionParams = { source: string | null; target: string | null } +export type GetVersionParams = { + source: string | null + target: string | null + serverId: string | null + arch: string | null +} From 9da49be44d0da70c28755dd531ea689813fc10c6 Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Mon, 24 Jun 2024 16:15:56 -0600 Subject: [PATCH 048/125] Bugfix/patch db subscriber (#2652) * fix socket sending empty patches * do not timeout tcp connections, just poll them more * switch from poll to tcp keepalive --- core/Cargo.lock | 2 + core/startos/Cargo.toml | 2 + core/startos/src/net/utils.rs | 18 ------ core/startos/src/net/vhost.rs | 85 ++++++++++++++++------------ core/startos/src/s9pk/v2/manifest.rs | 2 +- core/startos/src/util/io.rs | 36 +++++++++--- patch-db | 2 +- 7 files changed, 83 insertions(+), 64 deletions(-) diff --git a/core/Cargo.lock b/core/Cargo.lock index 031ec41ab..f8db0bd7e 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -4881,6 +4881,7 @@ dependencies = [ "hmac", "http 1.1.0", "http-body-util", + "hyper-util", "id-pool", "imbl", "imbl-value", @@ -4936,6 +4937,7 @@ dependencies = [ "sha2 0.10.8", "shell-words", "simple-logging", + "socket2", "sqlx", "sscanf", "ssh-key", diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index a8707bf65..750bc4953 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -97,6 +97,7 @@ hex = "0.4.3" hmac = "0.12.1" http = "1.0.0" http-body-util = "0.1" +hyper-util = { version = "0.1.5", features = ["tokio", "service"] } id-pool = { version = "0.2.2", default-features = false, features = [ "serde", "u16", @@ -159,6 +160,7 @@ serde_yaml = { package = "serde_yml", version = "0.0.10" } sha2 = "0.10.2" shell-words = "1" simple-logging = "2.0.2" +socket2 = "0.5.7" sqlx = { version = "0.7.2", features = [ "chrono", "runtime-tokio-rustls", diff --git a/core/startos/src/net/utils.rs b/core/startos/src/net/utils.rs index 6de319a5e..9cba8a0cd 100644 --- a/core/startos/src/net/utils.rs +++ b/core/startos/src/net/utils.rs @@ -112,24 +112,6 @@ pub async fn find_eth_iface() -> Result { )) } -#[pin_project::pin_project] -pub struct SingleAccept(Option); -impl SingleAccept { - pub fn new(conn: T) -> Self { - Self(Some(conn)) - } -} -// impl axum_server::accept::Accept for SingleAccept { -// type Conn = T; -// type Error = Infallible; -// fn poll_accept( -// self: std::pin::Pin<&mut Self>, -// _cx: &mut std::task::Context<'_>, -// ) -> std::task::Poll>> { -// std::task::Poll::Ready(self.project().0.take().map(Ok)) -// } -// } - pub struct TcpListeners { listeners: Vec, } diff --git a/core/startos/src/net/vhost.rs b/core/startos/src/net/vhost.rs index b4f5715ae..e6a9d5b21 100644 --- a/core/startos/src/net/vhost.rs +++ b/core/startos/src/net/vhost.rs @@ -1,10 +1,15 @@ use std::collections::BTreeMap; use std::net::{IpAddr, Ipv6Addr, SocketAddr}; +use std::str::FromStr; use std::sync::{Arc, Weak}; use std::time::Duration; +use axum::body::Body; +use axum::extract::Request; +use axum::response::Response; use color_eyre::eyre::eyre; use helpers::NonDetachingJoinHandle; +use http::Uri; use imbl_value::InternedString; use models::ResultExt; use serde::{Deserialize, Serialize}; @@ -20,8 +25,9 @@ use tracing::instrument; use ts_rs::TS; use crate::db::model::Database; +use crate::net::static_server::server_error; use crate::prelude::*; -use crate::util::io::{BackTrackingReader, TimeoutStream}; +use crate::util::io::BackTrackingReader; use crate::util::serde::MaybeUtf8String; // not allowed: <=1024, >=32768, 5355, 5432, 9050, 6010, 9051, 5353 @@ -113,8 +119,16 @@ impl VHostServer { loop { match listener.accept().await { Ok((stream, _)) => { - let stream = - Box::pin(TimeoutStream::new(stream, Duration::from_secs(300))); + if let Err(e) = socket2::SockRef::from(&stream).set_tcp_keepalive( + &socket2::TcpKeepalive::new() + .with_time(Duration::from_secs(900)) + .with_interval(Duration::from_secs(60)) + .with_retries(5), + ) { + tracing::error!("Failed to set tcp keepalive: {e}"); + tracing::debug!("{e:?}"); + } + let mut stream = BackTrackingReader::new(stream); stream.start_buffering(); let mapping = mapping.clone(); @@ -129,38 +143,39 @@ impl VHostServer { { Ok(a) => a, Err(_) => { - // stream.rewind(); - // return hyper::server::Server::builder( - // SingleAccept::new(stream), - // ) - // .serve(make_service_fn(|_| async { - // Ok::<_, Infallible>(service_fn(|req| async move { - // let host = req - // .headers() - // .get(http::header::HOST) - // .and_then(|host| host.to_str().ok()); - // let uri = Uri::from_parts({ - // let mut parts = - // req.uri().to_owned().into_parts(); - // parts.authority = host - // .map(FromStr::from_str) - // .transpose()?; - // parts - // })?; - // Response::builder() - // .status( - // http::StatusCode::TEMPORARY_REDIRECT, - // ) - // .header( - // http::header::LOCATION, - // uri.to_string(), - // ) - // .body(Body::default()) - // })) - // })) - // .await - // .with_kind(crate::ErrorKind::Network); - todo!() + stream.rewind(); + return hyper_util::server::conn::auto::Builder::new(hyper_util::rt::TokioExecutor::new()) + .serve_connection( + hyper_util::rt::TokioIo::new(stream), + hyper_util::service::TowerToHyperService::new(axum::Router::new().fallback( + axum::routing::method_routing::any(move |req: Request| async move { + match async move { + let host = req + .headers() + .get(http::header::HOST) + .and_then(|host| host.to_str().ok()); + let uri = Uri::from_parts({ + let mut parts = req.uri().to_owned().into_parts(); + parts.authority = host.map(FromStr::from_str).transpose()?; + parts + })?; + Response::builder() + .status(http::StatusCode::TEMPORARY_REDIRECT) + .header(http::header::LOCATION, uri.to_string()) + .body(Body::default()) + }.await { + Ok(a) => a, + Err(e) => { + tracing::warn!("Error redirecting http request on ssl port: {e}"); + tracing::error!("{e:?}"); + server_error(Error::new(e, ErrorKind::Network)) + } + } + }), + )), + ) + .await + .map_err(|e| Error::new(color_eyre::eyre::Report::msg(e), ErrorKind::Network)); } }; let target_name = diff --git a/core/startos/src/s9pk/v2/manifest.rs b/core/startos/src/s9pk/v2/manifest.rs index 77e48c126..9ae8524fa 100644 --- a/core/startos/src/s9pk/v2/manifest.rs +++ b/core/startos/src/s9pk/v2/manifest.rs @@ -146,7 +146,7 @@ impl Manifest { #[ts(export)] pub struct HardwareRequirements { #[serde(default)] - #[ts(type = "{ [key: string]: string }")] + #[ts(type = "{ [key: string]: string }")] // TODO more specific key pub device: BTreeMap, #[ts(type = "number | null")] pub ram: Option, diff --git a/core/startos/src/util/io.rs b/core/startos/src/util/io.rs index 9a6bab64b..b5748d1d9 100644 --- a/core/startos/src/util/io.rs +++ b/core/startos/src/util/io.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeSet, VecDeque}; +use std::collections::VecDeque; use std::future::Future; use std::io::Cursor; use std::os::unix::prelude::MetadataExt; @@ -706,16 +706,16 @@ impl AsyncRead for TimeoutStream { buf: &mut tokio::io::ReadBuf<'_>, ) -> std::task::Poll> { let mut this = self.project(); - if let std::task::Poll::Ready(_) = this.sleep.as_mut().poll(cx) { + let timeout = this.sleep.as_mut().poll(cx); + let res = this.stream.poll_read(cx, buf); + if res.is_ready() { + this.sleep.reset(Instant::now() + *this.timeout); + } else if timeout.is_ready() { return std::task::Poll::Ready(Err(std::io::Error::new( std::io::ErrorKind::TimedOut, "timed out", ))); } - let res = this.stream.poll_read(cx, buf); - if res.is_ready() { - this.sleep.reset(Instant::now() + *this.timeout); - } res } } @@ -725,10 +725,16 @@ impl AsyncWrite for TimeoutStream { cx: &mut std::task::Context<'_>, buf: &[u8], ) -> std::task::Poll> { - let this = self.project(); + let mut this = self.project(); + let timeout = this.sleep.as_mut().poll(cx); let res = this.stream.poll_write(cx, buf); if res.is_ready() { this.sleep.reset(Instant::now() + *this.timeout); + } else if timeout.is_ready() { + return std::task::Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::TimedOut, + "timed out", + ))); } res } @@ -736,10 +742,16 @@ impl AsyncWrite for TimeoutStream { self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { - let this = self.project(); + let mut this = self.project(); + let timeout = this.sleep.as_mut().poll(cx); let res = this.stream.poll_flush(cx); if res.is_ready() { this.sleep.reset(Instant::now() + *this.timeout); + } else if timeout.is_ready() { + return std::task::Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::TimedOut, + "timed out", + ))); } res } @@ -747,10 +759,16 @@ impl AsyncWrite for TimeoutStream { self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { - let this = self.project(); + let mut this = self.project(); + let timeout = this.sleep.as_mut().poll(cx); let res = this.stream.poll_shutdown(cx); if res.is_ready() { this.sleep.reset(Instant::now() + *this.timeout); + } else if timeout.is_ready() { + return std::task::Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::TimedOut, + "timed out", + ))); } res } diff --git a/patch-db b/patch-db index c537a07ea..99076d349 160000 --- a/patch-db +++ b/patch-db @@ -1 +1 @@ -Subproject commit c537a07ea937e69b66841d903c70fd75623e5457 +Subproject commit 99076d349c6768000483ea8d47216d273586552e From 0c188f6d10fe7380a3b6b757906304aeea28944a Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Tue, 25 Jun 2024 10:54:09 -0600 Subject: [PATCH 049/125] fix ca trust test and snek high score --- web/package-lock.json | 12 +++++++----- web/projects/ui/src/app/app/snek/snek.directive.ts | 2 +- .../app/pages/login/ca-wizard/ca-wizard.component.ts | 2 +- web/projects/ui/src/app/services/api/api.types.ts | 5 ++++- .../ui/src/app/services/api/embassy-api.service.ts | 4 +++- .../src/app/services/api/embassy-live-api.service.ts | 4 ++++ .../src/app/services/api/embassy-mock-api.service.ts | 12 +++++++++++- 7 files changed, 31 insertions(+), 10 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 81b612ac1..ca5c8efe8 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1974,22 +1974,24 @@ }, "../sdk/dist": { "name": "@start9labs/start-sdk", - "version": "0.3.6-alpha1", + "version": "0.3.6-alpha5", "license": "MIT", "dependencies": { + "@iarna/toml": "^2.2.5", "isomorphic-fetch": "^3.0.0", - "ts-matches": "^5.4.1" + "lodash": "^4.17.21", + "ts-matches": "^5.4.1", + "yaml": "^2.2.2" }, "devDependencies": { - "@iarna/toml": "^2.2.5", "@types/jest": "^29.4.0", + "@types/lodash": "^4.17.5", "jest": "^29.4.3", "prettier": "^3.2.5", "ts-jest": "^29.0.5", "ts-node": "^10.9.1", "tsx": "^4.7.1", - "typescript": "^5.0.4", - "yaml": "^2.2.2" + "typescript": "^5.0.4" } }, "node_modules/@adobe/css-tools": { diff --git a/web/projects/ui/src/app/app/snek/snek.directive.ts b/web/projects/ui/src/app/app/snek/snek.directive.ts index 255926792..db812ae4a 100644 --- a/web/projects/ui/src/app/app/snek/snek.directive.ts +++ b/web/projects/ui/src/app/app/snek/snek.directive.ts @@ -39,7 +39,7 @@ export class SnekDirective { try { await this.embassyApi.setDbValue( - ['gaming', 'snake', 'high-score'], + ['gaming', 'snake', 'highScore'], data.highScore, ) } catch (e: any) { diff --git a/web/projects/ui/src/app/pages/login/ca-wizard/ca-wizard.component.ts b/web/projects/ui/src/app/pages/login/ca-wizard/ca-wizard.component.ts index 2c0e9c7fe..fde1c968f 100644 --- a/web/projects/ui/src/app/pages/login/ca-wizard/ca-wizard.component.ts +++ b/web/projects/ui/src/app/pages/login/ca-wizard/ca-wizard.component.ts @@ -42,7 +42,7 @@ export class CAWizardComponent { private async testHttps() { const url = `https://${this.document.location.host}${this.relativeUrl}` - await this.api.getState().then(() => { + await this.api.echo({ message: 'ping' }, url).then(() => { this.caTrusted = true }) } diff --git a/web/projects/ui/src/app/services/api/api.types.ts b/web/projects/ui/src/app/services/api/api.types.ts index a5c52e9a2..0c7679754 100644 --- a/web/projects/ui/src/app/services/api/api.types.ts +++ b/web/projects/ui/src/app/services/api/api.types.ts @@ -12,7 +12,10 @@ export module RR { export type WebsocketConfig = Omit, 'url'> - // server state + // state + + export type EchoReq = { message: string } // server.echo + export type EchoRes = string export type ServerState = 'initializing' | 'error' | 'running' diff --git a/web/projects/ui/src/app/services/api/embassy-api.service.ts b/web/projects/ui/src/app/services/api/embassy-api.service.ts index d6bb11632..1d9c6f805 100644 --- a/web/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-api.service.ts @@ -17,7 +17,9 @@ export abstract class ApiService { config: RR.WebsocketConfig, ): Observable - // server state + // state + + abstract echo(params: RR.EchoReq, url: string): Promise abstract getState(): Promise diff --git a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts index 954a4475e..057c51013 100644 --- a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -71,6 +71,10 @@ export class LiveApiService extends ApiService { // state + async echo(params: RR.EchoReq, url: string): Promise { + return this.rpcRequest({ method: 'echo', params }, url) + } + async getState(): Promise { return this.rpcRequest({ method: 'state', params: {} }) } diff --git a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts index 728b4ff35..98efee2ef 100644 --- a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -118,7 +118,17 @@ export class MockApiService extends ApiService { } } - // server state + // state + + async echo(params: RR.EchoReq, url: string): Promise { + if (url) { + const num = Math.floor(Math.random() * 10) + 1 + if (num > 8) return params.message + throw new Error() + } + await pauseFor(2000) + return params.message + } private stateIndex = 0 async getState(): Promise { From 0e506f571611a3b67b94e6a8482b268f02642a1a Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Tue, 25 Jun 2024 12:34:47 -0600 Subject: [PATCH 050/125] fix container cli (#2654) --- core/Cargo.lock | 490 +++++------------- core/startos/src/context/cli.rs | 27 +- core/startos/src/service/cli.rs | 27 +- .../src/service/service_effect_handler.rs | 39 +- 4 files changed, 173 insertions(+), 410 deletions(-) diff --git a/core/Cargo.lock b/core/Cargo.lock index f8db0bd7e..6db13b8cd 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -225,7 +225,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -236,7 +236,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -432,7 +432,7 @@ checksum = "be5951c75bdabb58753d140dd5802f12ff3a483cb2e16fb5276e111b94b19e87" dependencies = [ "concurrent-queue 1.2.4", "event-listener", - "spin 0.9.8", + "spin", ] [[package]] @@ -624,9 +624,9 @@ checksum = "981520c98f422fcc584dc1a95c334e6953900b9106bc47a9839b81790009eb21" [[package]] name = "cc" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" +checksum = "c891175c3fb232128f48de6590095e59198bbeb8620c310be349bfc3afd12c7b" dependencies = [ "jobserver", "libc", @@ -740,7 +740,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -914,6 +914,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ + "percent-encoding", "time", "version_check", ] @@ -935,6 +936,23 @@ dependencies = [ "url", ] +[[package]] +name = "cookie_store" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4934e6b7e8419148b6ef56950d277af8561060b56afd59e2aadf98b59fce6baa" +dependencies = [ + "cookie 0.18.1", + "idna 0.5.0", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1126,16 +1144,15 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.1.2" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", "digest 0.10.7", "fiat-crypto", - "platforms", "rustc_version", "subtle", "zeroize", @@ -1149,7 +1166,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -1173,7 +1190,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -1184,7 +1201,7 @@ checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ "darling_core", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -1215,7 +1232,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -1238,7 +1255,7 @@ checksum = "5fe87ce4529967e0ba1dcf8450bab64d97dfd5010a6256187ffe2e43e6f0e049" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -1253,15 +1270,15 @@ dependencies = [ [[package]] name = "derive_more" -version = "0.99.17" +version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version", - "syn 1.0.109", + "syn 2.0.68", ] [[package]] @@ -1306,17 +1323,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "displaydoc" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - [[package]] name = "divrem" version = "1.0.0" @@ -1398,7 +1404,7 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ - "curve25519-dalek 4.1.2", + "curve25519-dalek 4.1.3", "ed25519 2.2.3", "rand_core 0.6.4", "serde", @@ -1487,7 +1493,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -1600,7 +1606,7 @@ checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ "futures-core", "futures-sink", - "spin 0.9.8", + "spin", ] [[package]] @@ -1715,7 +1721,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -2055,9 +2061,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.3" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0e7a4dd27b9476dc40cb050d3632d3bba3a70ddbff012285f7f8559a1e7e545" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "httpdate" @@ -2116,6 +2122,23 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper 1.3.1", + "hyper-util", + "rustls 0.23.10", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.0", + "tower-service", +] + [[package]] name = "hyper-timeout" version = "0.4.1" @@ -2187,124 +2210,6 @@ dependencies = [ "cc", ] -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" - -[[package]] -name = "icu_properties" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - [[package]] name = "id-pool" version = "0.2.2" @@ -2342,14 +2247,12 @@ dependencies = [ [[package]] name = "idna" -version = "1.0.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ - "icu_normalizer", - "icu_properties", - "smallvec", - "utf8_iter", + "unicode-bidi", + "unicode-normalization", ] [[package]] @@ -2389,18 +2292,18 @@ dependencies = [ [[package]] name = "include_dir" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18762faeff7122e89e0857b02f7ce6fcc0d101d5e9ad2ad7846cc01d61b7f19e" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" dependencies = [ "include_dir_macros", ] [[package]] name = "include_dir_macros" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" dependencies = [ "proc-macro2", "quote", @@ -2727,11 +2630,11 @@ checksum = "e479e99b287d578ed5f6cd4c92cdf48db219088adb9c5b14f7c155b71dfba792" [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin 0.5.2", + "spin", ] [[package]] @@ -2779,12 +2682,6 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" -[[package]] -name = "litemap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" - [[package]] name = "lock_api" version = "0.4.12" @@ -2852,9 +2749,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memmap2" @@ -2897,9 +2794,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] @@ -3170,7 +3067,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -3236,7 +3133,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -3335,7 +3232,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.1", + "redox_syscall 0.5.2", "smallvec", "windows-targets 0.52.5", ] @@ -3454,7 +3351,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -3496,12 +3393,6 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" -[[package]] -name = "platforms" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7" - [[package]] name = "portable-atomic" version = "1.6.0" @@ -3560,18 +3451,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.85" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "proptest" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" +checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" dependencies = [ "bit-set", "bit-vec", @@ -3618,7 +3509,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -3802,9 +3693,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ "bitflags 2.5.0", ] @@ -3866,14 +3757,14 @@ checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "reqwest" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" +checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" dependencies = [ "base64 0.22.1", "bytes", - "cookie 0.17.0", - "cookie_store", + "cookie 0.18.1", + "cookie_store 0.21.0", "encoding_rs", "futures-core", "futures-util", @@ -3882,6 +3773,7 @@ dependencies = [ "http-body 1.0.0", "http-body-util", "hyper 1.3.1", + "hyper-rustls", "hyper-tls", "hyper-util", "ipnet", @@ -3896,7 +3788,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.1", "system-configuration", "tokio", "tokio-native-tls", @@ -3918,7 +3810,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93ea5c6f30c19d766efe8d823c88f9abd1c56516648a0d4264ab2dc04cc19472" dependencies = [ "bytes", - "cookie_store", + "cookie_store 0.20.0", "reqwest", "url", ] @@ -3943,7 +3835,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.15", "libc", - "spin 0.9.8", + "spin", "untrusted", "windows-sys 0.52.0", ] @@ -3962,7 +3854,7 @@ dependencies = [ [[package]] name = "rpc-toolkit" version = "0.2.3" -source = "git+https://github.com/Start9Labs/rpc-toolkit.git?branch=refactor/no-dyn-ctx#5a24903031e72ac75fd23889215361edc7b20842" +source = "git+https://github.com/Start9Labs/rpc-toolkit.git?branch=refactor/no-dyn-ctx#f608480034942f1f521ab95949ab33fbc51d99a9" dependencies = [ "async-stream", "async-trait", @@ -4088,6 +3980,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls" +version = "0.23.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.102.4", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -4288,7 +4193,7 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -4361,7 +4266,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -4529,12 +4434,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spin" version = "0.9.8" @@ -4788,7 +4687,7 @@ dependencies = [ "quote", "regex-syntax 0.6.29", "strsim 0.10.0", - "syn 2.0.66", + "syn 2.0.68", "unicode-width", ] @@ -4834,12 +4733,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "start-os" version = "0.3.5-rev.2" @@ -4865,7 +4758,7 @@ dependencies = [ "console", "console-subscriber", "cookie 0.18.1", - "cookie_store", + "cookie_store 0.20.0", "der", "digest 0.10.7", "divrem", @@ -4945,7 +4838,7 @@ dependencies = [ "tar", "thiserror", "tokio", - "tokio-rustls", + "tokio-rustls 0.25.0", "tokio-socks", "tokio-stream", "tokio-tar", @@ -5019,9 +4912,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -5036,9 +4929,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.66" +version = "2.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" dependencies = [ "proc-macro2", "quote", @@ -5057,17 +4950,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" -[[package]] -name = "synstructure" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] - [[package]] name = "system-configuration" version = "0.5.1" @@ -5165,7 +5047,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -5229,16 +5111,6 @@ dependencies = [ "crunchy", ] -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - [[package]] name = "tinyvec" version = "1.6.0" @@ -5292,7 +5164,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -5316,6 +5188,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls 0.23.10", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-socks" version = "0.5.1" @@ -5549,7 +5432,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -5701,7 +5584,7 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", "termcolor", ] @@ -5742,7 +5625,7 @@ checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -5825,12 +5708,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", - "idna 1.0.0", + "idna 0.5.0", "percent-encoding", "serde", ] @@ -5847,18 +5730,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "utf8parse" version = "0.2.2" @@ -5867,9 +5738,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.8.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" dependencies = [ "getrandom 0.2.15", ] @@ -5959,7 +5830,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", "wasm-bindgen-shared", ] @@ -5993,7 +5864,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.68", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6250,18 +6121,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - [[package]] name = "wyz" version = "0.5.1" @@ -6324,30 +6183,6 @@ dependencies = [ "serde", ] -[[package]] -name = "yoke" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", - "synstructure", -] - [[package]] name = "zerocopy" version = "0.7.34" @@ -6365,28 +6200,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", -] - -[[package]] -name = "zerofrom" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", - "synstructure", + "syn 2.0.68", ] [[package]] @@ -6406,29 +6220,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", -] - -[[package]] -name = "zerovec" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", + "syn 2.0.68", ] [[package]] @@ -6451,9 +6243,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.10+zstd.1.5.6" +version = "2.0.11+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" +checksum = "75652c55c0b6f3e6f12eb786fe1bc960396bf05a1eb3bf1f3691c3610ac2e6d4" dependencies = [ "cc", "pkg-config", diff --git a/core/startos/src/context/cli.rs b/core/startos/src/context/cli.rs index 22084bbe1..d560d575d 100644 --- a/core/startos/src/context/cli.rs +++ b/core/startos/src/context/cli.rs @@ -25,7 +25,7 @@ use crate::rpc_continuations::Guid; #[derive(Debug)] pub struct CliContextSeed { - pub runtime: OnceCell, + pub runtime: OnceCell>, pub base_url: Url, pub rpc_url: Url, pub registry_url: Option, @@ -249,16 +249,19 @@ impl std::ops::Deref for CliContext { } } impl Context for CliContext { - fn runtime(&self) -> tokio::runtime::Handle { - self.runtime - .get_or_init(|| { - tokio::runtime::Builder::new_multi_thread() - .enable_all() - .build() - .unwrap() - }) - .handle() - .clone() + fn runtime(&self) -> Option> { + Some( + self.runtime + .get_or_init(|| { + Arc::new( + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(), + ) + }) + .clone(), + ) } } impl CallRemote for CliContext { @@ -290,7 +293,7 @@ impl CallRemote for CliContext { #[test] fn test() { let ctx = CliContext::init(ClientConfig::default()).unwrap(); - ctx.runtime().block_on(async { + ctx.runtime().unwrap().block_on(async { reqwest::Client::new() .get("http://example.com") .send() diff --git a/core/startos/src/service/cli.rs b/core/startos/src/service/cli.rs index 87491932b..f5e04999c 100644 --- a/core/startos/src/service/cli.rs +++ b/core/startos/src/service/cli.rs @@ -19,7 +19,7 @@ pub struct ContainerClientConfig { pub struct ContainerCliSeed { socket: PathBuf, - runtime: OnceCell, + runtime: OnceCell>, } #[derive(Clone)] @@ -35,17 +35,20 @@ impl ContainerCliContext { } } impl Context for ContainerCliContext { - fn runtime(&self) -> tokio::runtime::Handle { - self.0 - .runtime - .get_or_init(|| { - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap() - }) - .handle() - .clone() + fn runtime(&self) -> Option> { + Some( + self.0 + .runtime + .get_or_init(|| { + Arc::new( + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(), + ) + }) + .clone(), + ) } } diff --git a/core/startos/src/service/service_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs index a61a00b9b..63c313ead 100644 --- a/core/startos/src/service/service_effect_handler.rs +++ b/core/startos/src/service/service_effect_handler.rs @@ -899,50 +899,15 @@ async fn running(context: EffectContext, params: ParamsPackageId) -> Result Result { - Ok(Self { - procedure_id: matches.get_one("procedure-id").cloned().unwrap_or_default(), - }) - } - fn from_arg_matches_mut(matches: &mut clap::ArgMatches) -> Result { - Ok(Self { - procedure_id: matches.get_one("procedure-id").cloned().unwrap_or_default(), - }) - } - fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> { - self.procedure_id = matches.get_one("procedure-id").cloned().unwrap_or_default(); - Ok(()) - } - fn update_from_arg_matches_mut( - &mut self, - matches: &mut clap::ArgMatches, - ) -> Result<(), clap::Error> { - self.procedure_id = matches.get_one("procedure-id").cloned().unwrap_or_default(); - Ok(()) - } -} -impl CommandFactory for ProcedureId { - fn command() -> clap::Command { - Self::command_for_update().arg( - clap::Arg::new("procedure-id") - .action(clap::ArgAction::Set) - .value_parser(clap::value_parser!(Guid)), - ) - } - fn command_for_update() -> clap::Command { - Self::command() - } -} - async fn restart( context: EffectContext, ProcedureId { procedure_id }: ProcedureId, From ab1fdf69c8ea4b3ef260c9881f433cc09e32ca6f Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Tue, 25 Jun 2024 18:11:11 -0600 Subject: [PATCH 051/125] add docs for development environment (#2655) --- DEVELOPMENT.md | 133 +++++++++++++++++++++++++++ Makefile | 37 +++++++- assets/create-vm/step-1.png | Bin 0 -> 28070 bytes assets/create-vm/step-10.png | Bin 0 -> 45200 bytes assets/create-vm/step-11.png | Bin 0 -> 54662 bytes assets/create-vm/step-12.png | Bin 0 -> 49039 bytes assets/create-vm/step-2.png | Bin 0 -> 47071 bytes assets/create-vm/step-3.png | Bin 0 -> 46699 bytes assets/create-vm/step-4.png | Bin 0 -> 51398 bytes assets/create-vm/step-5.png | Bin 0 -> 65075 bytes assets/create-vm/step-6.png | Bin 0 -> 53461 bytes assets/create-vm/step-7.png | Bin 0 -> 65527 bytes assets/create-vm/step-8.png | Bin 0 -> 53991 bytes assets/create-vm/step-9.png | Bin 0 -> 45452 bytes build/lib/scripts/chroot-and-upgrade | 17 +--- build/lib/scripts/prune-images | 49 ++++++++++ 16 files changed, 217 insertions(+), 19 deletions(-) create mode 100644 DEVELOPMENT.md create mode 100644 assets/create-vm/step-1.png create mode 100644 assets/create-vm/step-10.png create mode 100644 assets/create-vm/step-11.png create mode 100644 assets/create-vm/step-12.png create mode 100644 assets/create-vm/step-2.png create mode 100644 assets/create-vm/step-3.png create mode 100644 assets/create-vm/step-4.png create mode 100644 assets/create-vm/step-5.png create mode 100644 assets/create-vm/step-6.png create mode 100644 assets/create-vm/step-7.png create mode 100644 assets/create-vm/step-8.png create mode 100644 assets/create-vm/step-9.png create mode 100755 build/lib/scripts/prune-images diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 000000000..cb9385f41 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,133 @@ +# Setting up your development environment on Debian/Ubuntu + +A step-by-step guide + +> This is the only officially supported build environment. +> MacOS has limited build capabilities and Windows requires [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install) + +## Installing dependencies + +Run the following commands one at a time + +```sh +sudo apt update +sudo apt install -y ca-certificates curl gpg build-essential +curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg +echo "deb [arch=$(dpkg-architecture -q DEB_HOST_ARCH) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian bookworm stable" | sudo tee /etc/apt/sources.list.d/docker.list +sudo apt update +sudo apt install -y sed grep gawk jq gzip brotli containerd.io docker-ce docker-ce-cli docker-compose-plugin qemu-user-static binfmt-support squashfs-tools git debspawn rsync b3sum +sudo mkdir -p /etc/debspawn/ +echo "AllowUnsafePermissions=true" | sudo tee /etc/debspawn/global.toml +sudo usermod -aG docker $USER +sudo su $USER +docker run --privileged --rm tonistiigi/binfmt --install all +docker buildx create --use +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # proceed with default installation +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash +source ~/.bashrc +nvm install 20 +nvm use 20 +``` + +## Cloning the repository + +```sh +git clone --recursive https://github.com/Start9Labs/start-os.git --branch next/minor +cd start-os +``` + +## Building an ISO + +```sh +PLATFORM=$(uname -m) ENVIRONMENT=dev make iso +``` + +This will build an ISO for your current architecture. If you are building to run on an architecture other than the one you are currently on, replace `$(uname -m)` with the correct platform for the device (one of `aarch64`, `aarch64-nonfree`, `x86_64`, `x86_64-nonfree`, `raspberrypi`) + +## Creating a VM + +### Install virt-manager + +```sh +sudo apt update +sudo apt install -y virt-manager +sudo usermod -aG libvirt $USER +sudo su $USER +``` + +### Launch virt-manager + +```sh +virt-manager +``` + +### Create new virtual machine + +![Select "Create a new virtual machine"](assets/create-vm/step-1.png) +![Click "Forward"](assets/create-vm/step-2.png) +![Click "Browse"](assets/create-vm/step-3.png) +![Click "+"](assets/create-vm/step-4.png) + +#### make sure to set "Target Path" to the path to your results directory in start-os + +![Create storage pool](assets/create-vm/step-5.png) +![Select storage pool](assets/create-vm/step-6.png) +![Select ISO](assets/create-vm/step-7.png) +![Select "Generic or unknown OS" and click "Forward"](assets/create-vm/step-8.png) +![Set Memory and CPUs](assets/create-vm/step-9.png) +![Create disk](assets/create-vm/step-10.png) +![Name VM](assets/create-vm/step-11.png) +![Create network](assets/create-vm/step-12.png) + +## Updating a VM + +The fastest way to update a VM to your latest code depends on what you changed: + +### UI or startd: + +```sh +PLATFORM=$(uname -m) ENVIRONMENT=dev make update-startbox REMOTE=start9@ +``` + +### Container runtime or debian dependencies: + +```sh +PLATFORM=$(uname -m) ENVIRONMENT=dev make update-deb REMOTE=start9@ +``` + +### Image recipe: + +```sh +PLATFORM=$(uname -m) ENVIRONMENT=dev make update-squashfs REMOTE=start9@ +``` + +--- + +If the device you are building for is not available via ssh, it is also possible to use `magic-wormhole` to send the relevant files. + +### Prerequisites: + +```sh +sudo apt update +sudo apt install -y magic-wormhole +``` + +As before, the fastest way to update a VM to your latest code depends on what you changed. Each of the following commands will return a command to paste into the shell of the device you would like to upgrade. + +### UI or startd: + +```sh +PLATFORM=$(uname -m) ENVIRONMENT=dev make wormhole +``` + +### Container runtime or debian dependencies: + +```sh +PLATFORM=$(uname -m) ENVIRONMENT=dev make wormhole-deb +``` + +### Image recipe: + +```sh +PLATFORM=$(uname -m) ENVIRONMENT=dev make wormhole-squashfs +``` diff --git a/Makefile b/Makefile index 5a0440196..5e038a08d 100644 --- a/Makefile +++ b/Makefile @@ -152,16 +152,21 @@ update-overlay: $(ALL_TARGETS) $(call ssh,"sudo systemctl start startd") wormhole: core/target/$(ARCH)-unknown-linux-musl/release/startbox - @echo "Paste the following command into the shell of your start-os server:" + @echo "Paste the following command into the shell of your StartOS server:" + @echo @wormhole send core/target/$(ARCH)-unknown-linux-musl/release/startbox 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo /usr/lib/startos/scripts/chroot-and-upgrade \"cd /usr/bin && rm startbox && wormhole receive --accept-file %s && chmod +x startbox\"\n", $$3 }' wormhole-deb: results/$(BASENAME).deb - @echo "Paste the following command into the shell of your start-os server:" + @echo "Paste the following command into the shell of your StartOS server:" + @echo @wormhole send results/$(BASENAME).deb 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo /usr/lib/startos/scripts/chroot-and-upgrade '"'"'cd $$(mktemp -d) && wormhole receive --accept-file %s && apt-get install -y --reinstall ./$(BASENAME).deb'"'"'\n", $$3 }' -wormhole-cli: core/target/$(ARCH)-unknown-linux-musl/release/start-cli - @echo "Paste the following command into the shell of your start-os server:" - @wormhole send results/$(BASENAME).deb 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo /usr/lib/startos/scripts/chroot-and-upgrade '"'"'cd $$(mktemp -d) && wormhole receive --accept-file %s && apt-get install -y --reinstall ./$(BASENAME).deb'"'"'\n", $$3 }' +wormhole-squashfs: results/$(BASENAME).squashfs + $(eval SQFS_SUM := $(shell b3sum results/$(BASENAME).squashfs | head -c 32)) + $(eval SQFS_SIZE := $(shell du -s --bytes results/$(BASENAME).squashfs | awk '{print $$1}')) + @echo "Paste the following command into the shell of your StartOS server:" + @echo + @wormhole send results/$(BASENAME).squashfs 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo sh -c '"'"'/usr/lib/startos/scripts/prune-images $(SQFS_SIZE) && cd /media/startos/images && wormhole receive --accept-file %s && mv $(BASENAME).squashfs $(SQFS_SUM).rootfs && ln -rsf ./$(SQFS_SUM).rootfs ../config/current.rootfs && sync && reboot'"'"'\n", $$3 }' update: $(ALL_TARGETS) @if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi @@ -169,6 +174,28 @@ update: $(ALL_TARGETS) $(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/startos/next PLATFORM=$(PLATFORM) $(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync "apt-get install -y $(shell cat ./build/lib/depends)"') +update-startbox: core/target/$(ARCH)-unknown-linux-musl/release/startbox # only update binary (faster than full update) + @if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi + $(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create') + $(call cp,core/target/$(ARCH)-unknown-linux-musl/release/startbox,/media/startos/next/usr/bin/startbox) + $(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync true') + +update-deb: results/$(BASENAME).deb # better than update, but only available from debian + @if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi + $(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create') + $(call mkdir,/media/startos/next/tmp/startos-deb) + $(call cp,results/$(BASENAME).deb,/media/startos/next/tmp/startos-deb/$(BASENAME).deb) + $(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync "apt-get install -y --reinstall /tmp/startos-deb/$(BASENAME).deb"') + +update-squashfs: results/$(BASENAME).squashfs + @if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi + $(eval SQFS_SUM := $(shell b3sum results/$(BASENAME).squashfs)) + $(eval SQFS_SIZE := $(shell du -s --bytes results/$(BASENAME).squashfs | awk '{print $$1}')) + $(call ssh,'/usr/lib/startos/scripts/prune-images $(SQFS_SIZE)') + $(call cp,results/$(BASENAME).squashfs,/media/startos/images/$(SQFS_SUM).rootfs) + $(call ssh,'sudo ln -rsf /media/startos/images/$(SQFS_SUM).rootfs /media/startos/config/current.rootfs') + $(call ssh,'sudo reboot') + emulate-reflash: $(ALL_TARGETS) @if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi $(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create') diff --git a/assets/create-vm/step-1.png b/assets/create-vm/step-1.png new file mode 100644 index 0000000000000000000000000000000000000000..2dfafc25f28c9aed407ade6bba082031a7198b7b GIT binary patch literal 28070 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%0A~~67(4io63xsrHFma81p~rN%Iq5) z9($+E&Aa~BY=E?KTMR4K2Umbc@hVjtvCsh&ZanXLugPL>mjBv>vdj@?mJ{~d03L9_ zbs>pK7RKhLq6S$P0Y}?T)(21sPdxAp0TAIeHuoNF970P<+8b+Sy?soLbss!o{!g6#tF$FJ$xw=e^n<;YDT z-Ek{p*dvRt;a-ZprfMx=Of|%Dk{7E08V8Cv-4wPPrW)4Tf<>vRP90yDS~d>%@thPg zv7j0hk|IS7g$rz)hA_;Gk)0zUL2>sEL*YPHG@}@ZX5fd<+2c~|z%+%R`(vwa$B73Z z3~2DqMO)w&?P!4pi{U2kg>r!*)X;hy#2C~9;{JuvBgjhLYqfWTB6GHOe^eWE50)#gzjV28J>L7G1 z^$-RMQW=lgT6mCQM2|&3@Eh>Y_u0+0&F{cP`K~h^k8R#q>Ob14+j<4Mgh;R$Yg{s# zQ&6|ToAj@ zi`HwS-C#t{_%ss(?uFH{e4UOtrh%ch)R*jdSqM2}BvKoeG_(#0YZH=cWAed-k^=6L z_qf7{A;eW&kNdpQadEiz3PxMIX};DlHSD47bUu6ge%njzat5jGFtVT3$N%-4RyE!P zC7=>4(es23ckFQPwwL+aRAlbW76;PVB+uJVA*iun4em8+^5P=P+ zQ(Kx|FW7crZ}(@MsU-II&wMRVS0zRMNpUWsjq>IQkh6d5SiupH2+z@u4Qy{yFY3|z z_TQF4RgXe{5!J7Q$?7y4VGioafAZ!ZGwpxcu`gB#nwl@AgpQVBC4MVU|Cp2iBbiVj zie`P&6x-9avyVJ(^|UF_3qZe>wZTWj-X2OG#`bgX8tEK!@pr`mGXD1i;6ZrE5mjcv W^-BxB_J>~f^>JMbayj!ch^1o_5I!Z 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 DyPTO6 literal 0 HcmV?d00001 diff --git a/assets/create-vm/step-11.png b/assets/create-vm/step-11.png new file mode 100644 index 0000000000000000000000000000000000000000..322dd5394c9558979e09eafe00074dcefb0f5e80 GIT binary patch literal 54662 zcmaHT1z1#F_b;N9lt@Yo(%n)b4bt5?bi>do(lM0M(j_TFN+U6JcXzjR--GY_{qOzm zeV)s+VP^K6z0cZf{r0N84q=M&lISl9U&6t`p-W4NDZ|0RivYh`$nZc<@Av>F92_~E zw3x7}`?LKt1YI1<$JXI! zFbMPSoB#hH{}D?Yy#KkkHyb=?c;}e#*0?Sx@ZVb?55Hp-$wSX!OX+DR0_giYX)~*T zY?2Kv#Ajt+_|JbjgWs~2^jKKgOqOVIm=Atu1Hs6^TLWe$>FwQjn3Q8ek+PLrtgVQ^ zGM<*UjEZ$-jj?6`CV0H_I=tjC>;KLR28N2JM%Kd=c?Xl&){g2eK(>k+%{&c&oBMx$Evy7uK@L+MmSs{CwmUbxt;0Nx)^-ya#xb&1Ym^Wc5F!{dj%l z#XL&0r|}v+9z72_6WKqElJgQD1OC|)TU!&USuaj~K*VeV(F;z;7swl|JDFT1B&9G^PvRPijzy4>6X9E6?HSGom zXVL|15@t2k6sy{^!LvG%$EgW5E819&5@y^2rpSJB@WnwL)z1Aicj23pc75t)X~!ne z7x5F$raV848#E2S#wFxNQq-XOW4(^~rldT^NkG9~`jU&?MYFD{Irh#c2n`qmtgF0yx

-

- Setting up your server -

-
- Progress: {{ (progress.total * 100).toFixed(0) }}% -
- - -

{{ progress.message }}

-
+ + + + +
+

+ Setting up your server +

+
+ Progress: {{ (progress.total * 100).toFixed(0) }}% +
+ +

{{ progress.message }}

+
+
+
+
+
diff --git a/web/projects/setup-wizard/src/app/pages/loading/loading.page.scss b/web/projects/setup-wizard/src/app/pages/loading/loading.page.scss index e69de29bb..099f7084e 100644 --- a/web/projects/setup-wizard/src/app/pages/loading/loading.page.scss +++ b/web/projects/setup-wizard/src/app/pages/loading/loading.page.scss @@ -0,0 +1,9 @@ +section { + border-radius: 0.25rem; + padding: 3rem; + margin: 2rem; + text-align: center; + background: #e0e0e0; + color: #333; + --tui-clear-inverse: rgba(0, 0, 0, 0.1); +} \ No newline at end of file diff --git a/web/projects/ui/src/app/pages/init/init.page.html b/web/projects/ui/src/app/pages/init/init.page.html index bd3467bbb..5cd21bb07 100644 --- a/web/projects/ui/src/app/pages/init/init.page.html +++ b/web/projects/ui/src/app/pages/init/init.page.html @@ -2,7 +2,7 @@

Initializing StartOS

-
+
Progress: {{ (progress.total * 100).toFixed(0) }}%
From 822dd5e1004bacf836b098c4eba0b753e6005ef6 Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Fri, 28 Jun 2024 15:03:01 -0600 Subject: [PATCH 053/125] Feature/UI sideload (#2658) * ui sideloading * remove subtlecrypto import * fix parser * misc fixes * allow docker pull during compat conversion --- Makefile | 12 +- container-runtime/package-lock.json | 81 +++- container-runtime/package.json | 4 +- core/Cargo.lock | 223 +++++++-- core/models/Cargo.toml | 2 +- core/models/src/errors.rs | 4 +- core/models/src/version.rs | 31 +- core/startos/Cargo.toml | 2 +- core/startos/src/account.rs | 8 + core/startos/src/backup/os.rs | 19 +- core/startos/src/backup/restore.rs | 8 +- core/startos/src/backup/target/mod.rs | 13 +- core/startos/src/context/cli.rs | 13 +- core/startos/src/context/config.rs | 5 +- core/startos/src/context/rpc.rs | 1 - core/startos/src/db/model/mod.rs | 1 + core/startos/src/db/model/package.rs | 2 +- core/startos/src/db/model/private.rs | 6 + core/startos/src/db/model/public.rs | 8 +- core/startos/src/developer/mod.rs | 5 +- core/startos/src/disk/mod.rs | 6 +- core/startos/src/disk/util.rs | 4 +- core/startos/src/firmware.rs | 5 +- core/startos/src/init.rs | 9 +- core/startos/src/install/mod.rs | 432 ++++++++++-------- core/startos/src/lib.rs | 8 +- core/startos/src/lxc/mod.rs | 4 +- core/startos/src/net/service_interface.rs | 2 - core/startos/src/net/static_server.rs | 9 +- core/startos/src/notifications.rs | 2 + core/startos/src/os_install/mod.rs | 12 +- core/startos/src/registry/asset.rs | 16 + core/startos/src/registry/device_info.rs | 2 +- core/startos/src/registry/mod.rs | 1 - core/startos/src/registry/os/asset/add.rs | 6 +- core/startos/src/registry/os/asset/get.rs | 5 +- core/startos/src/registry/os/asset/sign.rs | 3 +- core/startos/src/registry/os/index.rs | 2 +- core/startos/src/registry/os/version/mod.rs | 11 +- core/startos/src/registry/package/get.rs | 9 +- core/startos/src/registry/package/index.rs | 5 +- core/startos/src/registry/signer/sign/mod.rs | 1 - .../s9pk/merkle_archive/directory_contents.rs | 15 + core/startos/src/s9pk/merkle_archive/mod.rs | 7 +- .../src/s9pk/merkle_archive/source/http.rs | 9 +- .../src/s9pk/merkle_archive/source/mod.rs | 106 ++++- .../source/multi_cursor_file.rs | 39 +- .../startos/src/s9pk/merkle_archive/varint.rs | 14 +- core/startos/src/s9pk/mod.rs | 54 ++- core/startos/src/s9pk/rpc.rs | 60 ++- core/startos/src/s9pk/v1/manifest.rs | 14 +- core/startos/src/s9pk/v1/reader.rs | 5 +- core/startos/src/s9pk/v2/compat.rs | 184 +++----- core/startos/src/s9pk/v2/manifest.rs | 8 +- core/startos/src/s9pk/v2/mod.rs | 16 +- core/startos/src/s9pk/v2/pack.rs | 113 +++-- core/startos/src/service/mod.rs | 4 +- .../src/service/persistent_container.rs | 7 +- .../src/service/service_effect_handler.rs | 6 +- core/startos/src/service/service_map.rs | 15 +- core/startos/src/setup.rs | 5 +- core/startos/src/ssh.rs | 3 +- core/startos/src/system.rs | 3 +- core/startos/src/update/mod.rs | 4 +- core/startos/src/upload.rs | 129 +++++- core/startos/src/util/io.rs | 11 +- core/startos/src/util/mod.rs | 13 +- core/startos/src/util/rpc.rs | 5 +- core/startos/src/util/serde.rs | 27 +- core/startos/src/version/mod.rs | 51 ++- core/startos/src/version/v0_3_5.rs | 30 +- core/startos/src/version/v0_3_5_1.rs | 10 +- core/startos/src/version/v0_3_5_2.rs | 10 +- core/startos/src/version/v0_3_6.rs | 10 +- debian/postinst | 1 + package-lock.json | 6 + sdk/lib/emverLite/mod.ts | 4 +- sdk/lib/index.browser.ts | 1 + sdk/lib/index.ts | 1 + sdk/lib/osBindings/GetPackageParams.ts | 3 +- sdk/lib/osBindings/Manifest.ts | 2 +- sdk/lib/osBindings/PackageVersionInfo.ts | 3 +- sdk/lib/osBindings/ServerInfo.ts | 3 +- sdk/lib/s9pk/index.ts | 67 +++ .../s9pk/merkleArchive/directoryContents.ts | 80 ++++ sdk/lib/s9pk/merkleArchive/fileContents.ts | 24 + sdk/lib/s9pk/merkleArchive/index.ts | 167 +++++++ sdk/lib/s9pk/merkleArchive/varint.ts | 62 +++ sdk/lib/test/emverList.test.ts | 9 - sdk/lib/util/fileHelper.ts | 4 +- sdk/package-lock.json | 84 +++- sdk/package.json | 11 +- system-images/compat/src/config/mod.rs | 2 +- web/package-lock.json | 80 +++- web/package.json | 6 +- .../server-routes/sideload/sideload.page.ts | 56 ++- .../ui/src/app/services/api/api.types.ts | 5 +- .../app/services/api/embassy-api.service.ts | 4 +- .../services/api/embassy-live-api.service.ts | 8 +- .../services/api/embassy-mock-api.service.ts | 9 +- web/projects/ui/src/polyfills.ts | 7 +- 101 files changed, 1901 insertions(+), 797 deletions(-) create mode 100644 package-lock.json create mode 100644 sdk/lib/s9pk/index.ts create mode 100644 sdk/lib/s9pk/merkleArchive/directoryContents.ts create mode 100644 sdk/lib/s9pk/merkleArchive/fileContents.ts create mode 100644 sdk/lib/s9pk/merkleArchive/index.ts create mode 100644 sdk/lib/s9pk/merkleArchive/varint.ts diff --git a/Makefile b/Makefile index 5e038a08d..9a9a001b7 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ COMPAT_SRC := $(shell git ls-files system-images/compat/) UTILS_SRC := $(shell git ls-files system-images/utils/) BINFMT_SRC := $(shell git ls-files system-images/binfmt/) CORE_SRC := $(shell git ls-files core) $(shell git ls-files --recurse-submodules patch-db) web/dist/static web/patchdb-ui-seed.json $(GIT_HASH_FILE) -WEB_SHARED_SRC := $(shell git ls-files web/projects/shared) $(shell ls -p web/ | grep -v / | sed 's/^/web\//g') web/node_modules/.package-lock.json web/config.json patch-db/client/dist web/patchdb-ui-seed.json +WEB_SHARED_SRC := $(shell git ls-files web/projects/shared) $(shell ls -p web/ | grep -v / | sed 's/^/web\//g') web/node_modules/.package-lock.json web/config.json patch-db/client/dist web/patchdb-ui-seed.json sdk/dist WEB_UI_SRC := $(shell git ls-files web/projects/ui) WEB_SETUP_WIZARD_SRC := $(shell git ls-files web/projects/setup-wizard) WEB_INSTALL_WIZARD_SRC := $(shell git ls-files web/projects/install-wizard) @@ -262,15 +262,19 @@ web/node_modules/.package-lock.json: web/package.json sdk/dist npm --prefix web ci touch web/node_modules/.package-lock.json -web/dist/raw/ui: $(WEB_UI_SRC) $(WEB_SHARED_SRC) +web/.angular: patch-db/client/dist sdk/dist web/node_modules/.package-lock.json + rm -rf web/.angular + mkdir -p web/.angular + +web/dist/raw/ui: $(WEB_UI_SRC) $(WEB_SHARED_SRC) web/.angular npm --prefix web run build:ui touch web/dist/raw/ui -web/dist/raw/setup-wizard: $(WEB_SETUP_WIZARD_SRC) $(WEB_SHARED_SRC) +web/dist/raw/setup-wizard: $(WEB_SETUP_WIZARD_SRC) $(WEB_SHARED_SRC) web/.angular npm --prefix web run build:setup touch web/dist/raw/setup-wizard -web/dist/raw/install-wizard: $(WEB_INSTALL_WIZARD_SRC) $(WEB_SHARED_SRC) +web/dist/raw/install-wizard: $(WEB_INSTALL_WIZARD_SRC) $(WEB_SHARED_SRC) web/.angular npm --prefix web run build:install-wiz touch web/dist/raw/install-wizard diff --git a/container-runtime/package-lock.json b/container-runtime/package-lock.json index 9b211c077..e63bec6a1 100644 --- a/container-runtime/package-lock.json +++ b/container-runtime/package-lock.json @@ -9,11 +9,13 @@ "version": "0.0.0", "dependencies": { "@iarna/toml": "^2.2.5", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", "@start9labs/start-sdk": "file:../sdk/dist", "esbuild-plugin-resolve": "^2.0.0", "filebrowser": "^1.0.0", "isomorphic-fetch": "^3.0.0", - "lodash": "^4.17.21", + "lodash.merge": "^4.6.2", "node-fetch": "^3.1.0", "ts-matches": "^5.5.1", "tslib": "^2.5.3", @@ -30,24 +32,27 @@ }, "../sdk/dist": { "name": "@start9labs/start-sdk", - "version": "0.3.6-alpha1", + "version": "0.3.6-alpha5", "license": "MIT", "dependencies": { + "@iarna/toml": "^2.2.5", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", "isomorphic-fetch": "^3.0.0", - "lodash": "^4.17.21", - "ts-matches": "^5.4.1" + "lodash.merge": "^4.6.2", + "mime": "^4.0.3", + "ts-matches": "^5.5.1", + "yaml": "^2.2.2" }, "devDependencies": { - "@iarna/toml": "^2.2.5", "@types/jest": "^29.4.0", - "@types/lodash": "^4.17.5", + "@types/lodash.merge": "^4.6.2", "jest": "^29.4.3", "prettier": "^3.2.5", "ts-jest": "^29.0.5", "ts-node": "^10.9.1", "tsx": "^4.7.1", - "typescript": "^5.0.4", - "yaml": "^2.2.2" + "typescript": "^5.0.4" } }, "node_modules/@iarna/toml": { @@ -72,6 +77,28 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/@noble/curves": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "dev": true, @@ -1316,10 +1343,10 @@ "json-buffer": "3.0.1" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, "node_modules/lowercase-keys": { "version": "2.0.0", @@ -2233,6 +2260,19 @@ "os-filter-obj": "^2.0.0" } }, + "@noble/curves": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "requires": { + "@noble/hashes": "1.4.0" + } + }, + "@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==" + }, "@nodelib/fs.scandir": { "version": "2.1.5", "dev": true, @@ -2261,14 +2301,17 @@ "version": "file:../sdk/dist", "requires": { "@iarna/toml": "^2.2.5", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", "@types/jest": "^29.4.0", - "@types/lodash": "^4.17.5", + "@types/lodash.merge": "^4.6.2", "isomorphic-fetch": "^3.0.0", "jest": "^29.4.3", - "lodash": "^4.17.21", + "lodash.merge": "^4.6.2", + "mime": "^4.0.3", "prettier": "^3.2.5", "ts-jest": "^29.0.5", - "ts-matches": "^5.4.1", + "ts-matches": "^5.5.1", "ts-node": "^10.9.1", "tsx": "^4.7.1", "typescript": "^5.0.4", @@ -2988,10 +3031,10 @@ "json-buffer": "3.0.1" } }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, "lowercase-keys": { "version": "2.0.0", diff --git a/container-runtime/package.json b/container-runtime/package.json index 357c606fc..515f50ee7 100644 --- a/container-runtime/package.json +++ b/container-runtime/package.json @@ -18,10 +18,12 @@ "dependencies": { "@iarna/toml": "^2.2.5", "@start9labs/start-sdk": "file:../sdk/dist", + "@noble/hashes": "^1.4.0", + "@noble/curves": "^1.4.0", "esbuild-plugin-resolve": "^2.0.0", "filebrowser": "^1.0.0", "isomorphic-fetch": "^3.0.0", - "lodash": "^4.17.21", + "lodash.merge": "^4.6.2", "node-fetch": "^3.1.0", "ts-matches": "^5.5.1", "tslib": "^2.5.3", diff --git a/core/Cargo.lock b/core/Cargo.lock index 6db13b8cd..80c2dcc0e 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -166,6 +166,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "arrayvec" version = "0.7.4" @@ -508,9 +514,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" dependencies = [ "serde", ] @@ -521,16 +527,28 @@ version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6" +[[package]] +name = "bitvec" +version = "0.19.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55f93d0ef3363c364d5976646a38f04cf67cfe1d4c8d160cdea02cab2c116b33" +dependencies = [ + "funty 1.1.0", + "radium 0.5.3", + "tap", + "wyz 0.2.0", +] + [[package]] name = "bitvec" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" dependencies = [ - "funty", - "radium", + "funty 2.0.0", + "radium 0.7.0", "tap", - "wyz", + "wyz 0.5.1", ] [[package]] @@ -540,7 +558,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23285ad32269793932e830392f2fe2f83e26488fd3ec778883a93c8323735780" dependencies = [ "arrayref", - "arrayvec", + "arrayvec 0.7.4", "constant_time_eq", ] @@ -551,7 +569,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52" dependencies = [ "arrayref", - "arrayvec", + "arrayvec 0.7.4", "cc", "cfg-if", "constant_time_eq", @@ -624,9 +642,9 @@ checksum = "981520c98f422fcc584dc1a95c334e6953900b9106bc47a9839b81790009eb21" [[package]] name = "cc" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c891175c3fb232128f48de6590095e59198bbeb8620c310be349bfc3afd12c7b" +checksum = "ac367972e516d45567c7eafc73d24e1c193dcf200a8d94e9db7b3d38b349572d" dependencies = [ "jobserver", "libc", @@ -1051,7 +1069,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "crossterm_winapi", "futures-core", "libc", @@ -1216,7 +1234,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "709ade444d53896e60f6265660eb50480dd08b77bfc822e5dcc233b88b0b2fba" dependencies = [ - "bitvec", + "bitvec 1.0.1", "deku_derive", "no_std_io", "rustversion", @@ -1416,9 +1434,9 @@ dependencies = [ [[package]] name = "either" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" dependencies = [ "serde", ] @@ -1445,12 +1463,13 @@ dependencies = [ [[package]] name = "emver" -version = "0.1.7" -source = "git+https://github.com/Start9Labs/emver-rs.git#61cf0bc96711b4d6f3f30df8efef025e0cc02bad" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed260c4d7efaec031b9c4f6c4d3cf136e3df2bbfe50925800236f5e847f28704" dependencies = [ "either", "fp-core", - "nom", + "nom 6.1.2", "serde", ] @@ -1529,6 +1548,24 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "exver" +version = "0.2.0" +source = "git+https://github.com/Start9Labs/exver-rs.git#29f52c1be18a0fe187670beac92822994b0d1949" +dependencies = [ + "either", + "emver", + "fp-core", + "getrandom 0.2.15", + "itertools 0.13.0", + "memchr", + "pest", + "pest_derive", + "serde", + "smallvec", + "yasi", +] + [[package]] name = "eyre" version = "0.6.12" @@ -1648,6 +1685,12 @@ dependencies = [ "itertools 0.8.2", ] +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + [[package]] name = "funty" version = "2.0.0" @@ -1799,7 +1842,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8283e7331b8c93b9756e0cfdbcfb90312852f953c6faf9bf741e684cc3b6ad69" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "crc", "log", "uuid", @@ -1913,7 +1956,7 @@ dependencies = [ "base64 0.21.7", "byteorder", "flate2", - "nom", + "nom 7.1.3", "num-traits", ] @@ -2461,6 +2504,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -2637,6 +2689,19 @@ dependencies = [ "spin", ] +[[package]] +name = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec 0.5.2", + "bitflags 1.3.2", + "cfg-if", + "ryu", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.155" @@ -2655,7 +2720,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "libc", ] @@ -2731,7 +2796,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c487024623ae38584610237dd1be8932bb2b324474b23c37a25f9fbe6bf5e9e" dependencies = [ "bincode", - "bitvec", + "bitvec 1.0.1", "serde", "serde-big-array", "thiserror", @@ -2821,7 +2886,7 @@ dependencies = [ "base64 0.21.7", "color-eyre", "ed25519-dalek 2.1.1", - "emver", + "exver", "ipnet", "lazy_static", "mbrman", @@ -2908,7 +2973,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cfg-if", "libc", ] @@ -2922,6 +2987,19 @@ dependencies = [ "memchr", ] +[[package]] +name = "nom" +version = "6.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" +dependencies = [ + "bitvec 0.19.6", + "funty 1.1.0", + "lexical-core", + "memchr", + "version_check", +] + [[package]] name = "nom" version = "7.1.3" @@ -2958,9 +3036,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", @@ -3116,7 +3194,7 @@ version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cfg-if", "foreign-types", "libc", @@ -3309,6 +3387,51 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.68", +] + +[[package]] +name = "pest_meta" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" +dependencies = [ + "once_cell", + "pest", + "sha2 0.10.8", +] + [[package]] name = "petgraph" version = "0.6.5" @@ -3466,7 +3589,7 @@ checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.5.0", + "bitflags 2.6.0", "lazy_static", "num-traits", "rand 0.8.5", @@ -3552,6 +3675,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" + [[package]] name = "radium" version = "0.7.0" @@ -3697,7 +3826,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] @@ -3948,7 +4077,7 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -4133,7 +4262,7 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -4198,9 +4327,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4" dependencies = [ "indexmap 2.2.6", "itoa", @@ -4459,7 +4588,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" dependencies = [ - "nom", + "nom 7.1.3", "unicode_categories", ] @@ -4566,7 +4695,7 @@ checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" dependencies = [ "atoi", "base64 0.21.7", - "bitflags 2.5.0", + "bitflags 2.6.0", "byteorder", "bytes", "chrono", @@ -4609,7 +4738,7 @@ checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" dependencies = [ "atoi", "base64 0.21.7", - "bitflags 2.5.0", + "bitflags 2.6.0", "byteorder", "chrono", "crc", @@ -4765,7 +4894,7 @@ dependencies = [ "ed25519 2.2.3", "ed25519-dalek 1.0.1", "ed25519-dalek 2.1.1", - "emver", + "exver", "fd-lock-rs", "futures", "gpt", @@ -4799,7 +4928,7 @@ dependencies = [ "models", "new_mime_guess", "nix 0.27.1", - "nom", + "nom 7.1.3", "num", "num_enum", "once_cell", @@ -4861,6 +4990,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stderrlog" version = "0.5.4" @@ -5113,9 +5248,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "c55115c6fbe2d2bef26eb09ad74bde02d8255476fc0c7b515ef09fbb35742d82" dependencies = [ "tinyvec_macros", ] @@ -5634,6 +5769,12 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + [[package]] name = "unarray" version = "0.1.4" @@ -6121,6 +6262,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + [[package]] name = "wyz" version = "0.5.1" diff --git a/core/models/Cargo.toml b/core/models/Cargo.toml index 76c66b4f2..44295745d 100644 --- a/core/models/Cargo.toml +++ b/core/models/Cargo.toml @@ -12,7 +12,7 @@ color-eyre = "0.6.2" ed25519-dalek = { version = "2.0.0", features = ["serde"] } lazy_static = "1.4" mbrman = "0.5.2" -emver = { version = "0.1", git = "https://github.com/Start9Labs/emver-rs.git", features = [ +exver = { version = "0.2.0", git = "https://github.com/Start9Labs/exver-rs.git", features = [ "serde", ] } ipnet = "2.8.0" diff --git a/core/models/src/errors.rs b/core/models/src/errors.rs index 95416ec80..8bbc705ee 100644 --- a/core/models/src/errors.rs +++ b/core/models/src/errors.rs @@ -242,8 +242,8 @@ impl From for Error { Error::new(e, ErrorKind::Utf8) } } -impl From for Error { - fn from(e: emver::ParseError) -> Self { +impl From for Error { + fn from(e: exver::ParseError) -> Self { Error::new(e, ErrorKind::ParseVersion) } } diff --git a/core/models/src/version.rs b/core/models/src/version.rs index 48871e3a1..f0c7b19ae 100644 --- a/core/models/src/version.rs +++ b/core/models/src/version.rs @@ -8,14 +8,14 @@ use ts_rs::TS; #[derive(Debug, Clone, TS)] #[ts(type = "string", rename = "Version")] pub struct VersionString { - version: emver::Version, + version: exver::ExtendedVersion, string: String, } impl VersionString { pub fn as_str(&self) -> &str { self.string.as_str() } - pub fn into_version(self) -> emver::Version { + pub fn into_version(self) -> exver::ExtendedVersion { self.version } } @@ -25,7 +25,7 @@ impl std::fmt::Display for VersionString { } } impl std::str::FromStr for VersionString { - type Err = ::Err; + type Err = ::Err; fn from_str(s: &str) -> Result { Ok(VersionString { string: s.to_owned(), @@ -33,32 +33,32 @@ impl std::str::FromStr for VersionString { }) } } -impl From for VersionString { - fn from(v: emver::Version) -> Self { +impl From for VersionString { + fn from(v: exver::ExtendedVersion) -> Self { VersionString { string: v.to_string(), version: v, } } } -impl From for emver::Version { +impl From for exver::ExtendedVersion { fn from(v: VersionString) -> Self { v.version } } impl Default for VersionString { fn default() -> Self { - Self::from(emver::Version::default()) + Self::from(exver::ExtendedVersion::default()) } } impl Deref for VersionString { - type Target = emver::Version; + type Target = exver::ExtendedVersion; fn deref(&self) -> &Self::Target { &self.version } } -impl AsRef for VersionString { - fn as_ref(&self) -> &emver::Version { +impl AsRef for VersionString { + fn as_ref(&self) -> &exver::ExtendedVersion { &self.version } } @@ -80,7 +80,13 @@ impl PartialOrd for VersionString { } impl Ord for VersionString { fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.version.cmp(&other.version) + self.version.partial_cmp(&other.version).unwrap_or_else(|| { + match (self.version.flavor(), other.version.flavor()) { + (None, Some(_)) => std::cmp::Ordering::Greater, + (Some(_), None) => std::cmp::Ordering::Less, + (a, b) => a.cmp(&b), + } + }) } } impl Hash for VersionString { @@ -94,7 +100,8 @@ impl<'de> Deserialize<'de> for VersionString { D: Deserializer<'de>, { let string = String::deserialize(deserializer)?; - let version = emver::Version::from_str(&string).map_err(::serde::de::Error::custom)?; + let version = + exver::ExtendedVersion::from_str(&string).map_err(::serde::de::Error::custom)?; Ok(Self { string, version }) } } diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index 401ee66a8..d2304fedb 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -86,7 +86,7 @@ ed25519-dalek = { version = "2.1.1", features = [ "pkcs8", ] } ed25519-dalek-v1 = { package = "ed25519-dalek", version = "1" } -emver = { version = "0.1.7", git = "https://github.com/Start9Labs/emver-rs.git", features = [ +exver = { version = "0.2.0", git = "https://github.com/Start9Labs/exver-rs.git", features = [ "serde", ] } fd-lock-rs = "0.1.4" diff --git a/core/startos/src/account.rs b/core/startos/src/account.rs index e074d301d..9e755342f 100644 --- a/core/startos/src/account.rs +++ b/core/startos/src/account.rs @@ -28,6 +28,7 @@ pub struct AccountInfo { pub root_ca_key: PKey, pub root_ca_cert: X509, pub ssh_key: ssh_key::PrivateKey, + pub compat_s9pk_key: ed25519_dalek::SigningKey, } impl AccountInfo { pub fn new(password: &str, start_time: SystemTime) -> Result { @@ -39,6 +40,7 @@ impl AccountInfo { let ssh_key = ssh_key::PrivateKey::from(ssh_key::private::Ed25519Keypair::random( &mut rand::thread_rng(), )); + let compat_s9pk_key = ed25519_dalek::SigningKey::generate(&mut rand::thread_rng()); Ok(Self { server_id, hostname, @@ -47,6 +49,7 @@ impl AccountInfo { root_ca_key, root_ca_cert, ssh_key, + compat_s9pk_key, }) } @@ -61,6 +64,7 @@ impl AccountInfo { let root_ca_key = cert_store.as_root_key().de()?.0; let root_ca_cert = cert_store.as_root_cert().de()?.0; let ssh_key = db.as_private().as_ssh_privkey().de()?.0; + let compat_s9pk_key = db.as_private().as_compat_s9pk_key().de()?.0; Ok(Self { server_id, @@ -70,6 +74,7 @@ impl AccountInfo { root_ca_key, root_ca_cert, ssh_key, + compat_s9pk_key, }) } @@ -92,6 +97,9 @@ impl AccountInfo { db.as_private_mut() .as_ssh_privkey_mut() .ser(Pem::new_ref(&self.ssh_key))?; + db.as_private_mut() + .as_compat_s9pk_key_mut() + .ser(Pem::new_ref(&self.compat_s9pk_key))?; let key_store = db.as_private_mut().as_key_store_mut(); key_store.as_onion_mut().insert_key(&self.tor_key)?; let cert_store = key_store.as_local_certs_mut(); diff --git a/core/startos/src/backup/os.rs b/core/startos/src/backup/os.rs index 6848473a7..7c8119e79 100644 --- a/core/startos/src/backup/os.rs +++ b/core/startos/src/backup/os.rs @@ -85,6 +85,7 @@ impl OsBackupV0 { ssh_key::Algorithm::Ed25519, )?, tor_key: TorSecretKeyV3::from(self.tor_key.0), + compat_s9pk_key: ed25519_dalek::SigningKey::generate(&mut rand::thread_rng()), }, ui: self.ui, }) @@ -113,6 +114,7 @@ impl OsBackupV1 { root_ca_cert: self.root_ca_cert.0, ssh_key: ssh_key::PrivateKey::from(Ed25519Keypair::from_seed(&self.net_key.0)), tor_key: TorSecretKeyV3::from(ed25519_expand_key(&self.net_key.0)), + compat_s9pk_key: ed25519_dalek::SigningKey::from_bytes(&self.net_key), }, ui: self.ui, } @@ -124,13 +126,14 @@ impl OsBackupV1 { #[serde(rename = "kebab-case")] struct OsBackupV2 { - server_id: String, // uuidv4 - hostname: String, // - - root_ca_key: Pem>, // PEM Encoded OpenSSL Key - root_ca_cert: Pem, // PEM Encoded OpenSSL X509 Certificate - ssh_key: Pem, // PEM Encoded OpenSSH Key - tor_key: TorSecretKeyV3, // Base64 Encoded Ed25519 Expanded Secret Key - ui: Value, // JSON Value + server_id: String, // uuidv4 + hostname: String, // - + root_ca_key: Pem>, // PEM Encoded OpenSSL Key + root_ca_cert: Pem, // PEM Encoded OpenSSL X509 Certificate + ssh_key: Pem, // PEM Encoded OpenSSH Key + tor_key: TorSecretKeyV3, // Base64 Encoded Ed25519 Expanded Secret Key + compat_s9pk_key: Pem, // PEM Encoded ED25519 Key + ui: Value, // JSON Value } impl OsBackupV2 { fn project(self) -> OsBackup { @@ -143,6 +146,7 @@ impl OsBackupV2 { root_ca_cert: self.root_ca_cert.0, ssh_key: self.ssh_key.0, tor_key: self.tor_key, + compat_s9pk_key: self.compat_s9pk_key.0, }, ui: self.ui, } @@ -155,6 +159,7 @@ impl OsBackupV2 { root_ca_cert: Pem(backup.account.root_ca_cert.clone()), ssh_key: Pem(backup.account.ssh_key.clone()), tor_key: backup.account.tor_key.clone(), + compat_s9pk_key: Pem(backup.account.compat_s9pk_key.clone()), ui: backup.ui.clone(), } } diff --git a/core/startos/src/backup/restore.rs b/core/startos/src/backup/restore.rs index 556f750ec..3f0aacbe8 100644 --- a/core/startos/src/backup/restore.rs +++ b/core/startos/src/backup/restore.rs @@ -156,16 +156,14 @@ async fn restore_packages( let mut tasks = BTreeMap::new(); for id in ids { let backup_dir = backup_guard.clone().package_backup(&id); + let s9pk_path = backup_dir.path().join(&id).with_extension("s9pk"); let task = ctx .services .install( ctx.clone(), - S9pk::open( - backup_dir.path().join(&id).with_extension("s9pk"), - Some(&id), - ) - .await?, + || S9pk::open(s9pk_path, Some(&id)), Some(backup_dir), + None, ) .await?; tasks.insert(id, task); diff --git a/core/startos/src/backup/target/mod.rs b/core/startos/src/backup/target/mod.rs index 2c7a28d94..f1273aa6a 100644 --- a/core/startos/src/backup/target/mod.rs +++ b/core/startos/src/backup/target/mod.rs @@ -7,6 +7,7 @@ use clap::Parser; use color_eyre::eyre::eyre; use digest::generic_array::GenericArray; use digest::OutputSizeUser; +use exver::Version; use models::PackageId; use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; @@ -194,7 +195,7 @@ pub async fn list(ctx: RpcContext) -> Result>, pub package_backups: BTreeMap, } @@ -204,7 +205,7 @@ pub struct BackupInfo { pub struct PackageBackupInfo { pub title: String, pub version: VersionString, - pub os_version: VersionString, + pub os_version: Version, pub timestamp: DateTime, } @@ -223,9 +224,9 @@ fn display_backup_info(params: WithIoFormat, info: BackupInfo) { "TIMESTAMP", ]); table.add_row(row![ - "EMBASSY OS", - info.version.as_str(), - info.version.as_str(), + "StartOS", + &info.version.to_string(), + &info.version.to_string(), &if let Some(ts) = &info.timestamp { ts.to_string() } else { @@ -236,7 +237,7 @@ fn display_backup_info(params: WithIoFormat, info: BackupInfo) { let row = row![ &*id, info.version.as_str(), - info.os_version.as_str(), + &info.os_version.to_string(), &info.timestamp.to_string(), ]; table.add_row(row); diff --git a/core/startos/src/context/cli.rs b/core/startos/src/context/cli.rs index d560d575d..0eca1d2c2 100644 --- a/core/startos/src/context/cli.rs +++ b/core/startos/src/context/cli.rs @@ -43,7 +43,9 @@ impl Drop for CliContextSeed { std::fs::create_dir_all(&parent_dir).unwrap(); } let mut writer = fd_lock_rs::FdLock::lock( - File::create(&tmp).unwrap(), + File::create(&tmp) + .with_ctx(|_| (ErrorKind::Filesystem, &tmp)) + .unwrap(), fd_lock_rs::LockType::Exclusive, true, ) @@ -80,9 +82,12 @@ impl CliContext { }); let cookie_store = Arc::new(CookieStoreMutex::new({ let mut store = if cookie_path.exists() { - CookieStore::load_json(BufReader::new(File::open(&cookie_path)?)) - .map_err(|e| eyre!("{}", e)) - .with_kind(crate::ErrorKind::Deserialization)? + CookieStore::load_json(BufReader::new( + File::open(&cookie_path) + .with_ctx(|_| (ErrorKind::Filesystem, cookie_path.display()))?, + )) + .map_err(|e| eyre!("{}", e)) + .with_kind(crate::ErrorKind::Deserialization)? } else { CookieStore::default() }; diff --git a/core/startos/src/context/config.rs b/core/startos/src/context/config.rs index 7b4865301..e02648919 100644 --- a/core/startos/src/context/config.rs +++ b/core/startos/src/context/config.rs @@ -37,7 +37,10 @@ pub trait ContextConfig: DeserializeOwned + Default { .map(|f| f.parse()) .transpose()? .unwrap_or_default(); - format.from_reader(File::open(path)?) + format.from_reader( + File::open(path.as_ref()) + .with_ctx(|_| (ErrorKind::Filesystem, path.as_ref().display()))?, + ) } fn load_path_rec(&mut self, path: Option>) -> Result<(), Error> { if let Some(path) = path.filter(|p| p.as_ref().exists()) { diff --git a/core/startos/src/context/rpc.rs b/core/startos/src/context/rpc.rs index cf758e0fb..4920a6223 100644 --- a/core/startos/src/context/rpc.rs +++ b/core/startos/src/context/rpc.rs @@ -14,7 +14,6 @@ use rpc_toolkit::{CallRemote, Context, Empty}; use tokio::sync::{broadcast, Mutex, RwLock}; use tokio::time::Instant; use tracing::instrument; -use url::Url; use super::setup::CURRENT_SECRET; use crate::account::AccountInfo; diff --git a/core/startos/src/db/model/mod.rs b/core/startos/src/db/model/mod.rs index cae1bfe51..678f7e5fb 100644 --- a/core/startos/src/db/model/mod.rs +++ b/core/startos/src/db/model/mod.rs @@ -40,6 +40,7 @@ impl Database { notifications: Notifications::new(), cifs: CifsTargets::new(), package_stores: BTreeMap::new(), + compat_s9pk_key: Pem(account.compat_s9pk_key.clone()), }, // TODO }) } diff --git a/core/startos/src/db/model/package.rs b/core/startos/src/db/model/package.rs index 8bb5a9517..22d6440bb 100644 --- a/core/startos/src/db/model/package.rs +++ b/core/startos/src/db/model/package.rs @@ -1,7 +1,7 @@ use std::collections::{BTreeMap, BTreeSet}; use chrono::{DateTime, Utc}; -use emver::VersionRange; +use exver::VersionRange; use imbl_value::InternedString; use models::{ActionId, DataUrl, HealthCheckId, HostId, PackageId, ServiceInterfaceId}; use patch_db::json_ptr::JsonPointer; diff --git a/core/startos/src/db/model/private.rs b/core/startos/src/db/model/private.rs index 2b8c55dbd..c57364fc3 100644 --- a/core/startos/src/db/model/private.rs +++ b/core/startos/src/db/model/private.rs @@ -19,6 +19,8 @@ use crate::util::serde::Pem; pub struct Private { pub key_store: KeyStore, pub password: String, // argon2 hash + #[serde(default = "generate_compat_key")] + pub compat_s9pk_key: Pem, pub ssh_privkey: Pem, pub ssh_pubkeys: SshKeys, pub available_ports: AvailablePorts, @@ -28,3 +30,7 @@ pub struct Private { #[serde(default)] pub package_stores: BTreeMap, } + +fn generate_compat_key() -> Pem { + Pem(ed25519_dalek::SigningKey::generate(&mut rand::thread_rng())) +} diff --git a/core/startos/src/db/model/public.rs b/core/startos/src/db/model/public.rs index e5257f2a4..5f8dc029a 100644 --- a/core/startos/src/db/model/public.rs +++ b/core/startos/src/db/model/public.rs @@ -2,7 +2,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::net::{Ipv4Addr, Ipv6Addr}; use chrono::{DateTime, Utc}; -use emver::VersionRange; +use exver::{Version, VersionRange}; use imbl_value::InternedString; use ipnet::{Ipv4Net, Ipv6Net}; use isocountry::CountryCode; @@ -21,7 +21,6 @@ use crate::net::utils::{get_iface_ipv4_addr, get_iface_ipv6_addr}; use crate::prelude::*; use crate::progress::FullProgress; use crate::util::cpupower::Governor; -use crate::util::VersionString; use crate::version::{Current, VersionT}; use crate::{ARCH, PLATFORM}; @@ -43,7 +42,7 @@ impl Public { arch: get_arch(), platform: get_platform(), id: account.server_id.clone(), - version: Current::new().semver().into(), + version: Current::new().semver(), hostname: account.hostname.no_dot_host_name(), last_backup: None, eos_version_compat: Current::new().compat().clone(), @@ -109,7 +108,8 @@ pub struct ServerInfo { pub platform: InternedString, pub id: String, pub hostname: String, - pub version: VersionString, + #[ts(type = "string")] + pub version: Version, #[ts(type = "string | null")] pub last_backup: Option>, #[ts(type = "string")] diff --git a/core/startos/src/developer/mod.rs b/core/startos/src/developer/mod.rs index 79a875dfc..4a2a4c3df 100644 --- a/core/startos/src/developer/mod.rs +++ b/core/startos/src/developer/mod.rs @@ -8,8 +8,8 @@ use ed25519_dalek::{SigningKey, VerifyingKey}; use tracing::instrument; use crate::context::CliContext; +use crate::prelude::*; use crate::util::serde::Pem; -use crate::{Error, ResultExt}; #[instrument(skip_all)] pub fn init(ctx: CliContext) -> Result<(), Error> { @@ -26,7 +26,8 @@ pub fn init(ctx: CliContext) -> Result<(), Error> { secret_key: secret.to_bytes(), public_key: Some(PublicKeyBytes(VerifyingKey::from(&secret).to_bytes())), }; - let mut dev_key_file = File::create(&ctx.developer_key_path)?; + let mut dev_key_file = File::create(&ctx.developer_key_path) + .with_ctx(|_| (ErrorKind::Filesystem, ctx.developer_key_path.display()))?; dev_key_file.write_all( keypair_bytes .to_pkcs8_pem(base64ct::LineEnding::default()) diff --git a/core/startos/src/disk/mod.rs b/core/startos/src/disk/mod.rs index 705a34f98..d7ce5f766 100644 --- a/core/startos/src/disk/mod.rs +++ b/core/startos/src/disk/mod.rs @@ -102,10 +102,10 @@ fn display_disk_info(params: WithIoFormat, args: Vec) { } else { "N/A" }, - if let Some(eos) = part.start_os.as_ref() { - eos.version.as_str() + &if let Some(eos) = part.start_os.as_ref() { + eos.version.to_string() } else { - "N/A" + "N/A".to_owned() }, ]; table.add_row(row); diff --git a/core/startos/src/disk/util.rs b/core/startos/src/disk/util.rs index a98c52418..d3f2a8d27 100644 --- a/core/startos/src/disk/util.rs +++ b/core/startos/src/disk/util.rs @@ -20,7 +20,7 @@ use super::mount::guard::TmpMountGuard; use crate::disk::mount::guard::GenericMountGuard; use crate::disk::OsPartitionInfo; use crate::util::serde::IoFormat; -use crate::util::{Invoke, VersionString}; +use crate::util::Invoke; use crate::{Error, ResultExt as _}; #[derive(Clone, Copy, Debug, Deserialize, Serialize)] @@ -56,7 +56,7 @@ pub struct PartitionInfo { #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct EmbassyOsRecoveryInfo { - pub version: VersionString, + pub version: exver::Version, pub full: bool, pub password_hash: Option, pub wrapped_key: Option, diff --git a/core/startos/src/firmware.rs b/core/startos/src/firmware.rs index a9d5ced79..7e7b9d70f 100644 --- a/core/startos/src/firmware.rs +++ b/core/startos/src/firmware.rs @@ -3,13 +3,12 @@ use std::path::Path; use async_compression::tokio::bufread::GzipDecoder; use serde::{Deserialize, Serialize}; -use tokio::fs::File; use tokio::io::BufReader; use tokio::process::Command; use crate::disk::fsck::RequiresReboot; use crate::prelude::*; -use crate::progress::PhaseProgressTrackerHandle; +use crate::util::io::open_file; use crate::util::Invoke; use crate::PLATFORM; @@ -134,7 +133,7 @@ pub async fn update_firmware(firmware: Firmware) -> Result<(), Error> { .invoke(ErrorKind::Filesystem) .await?; let mut rdr = if tokio::fs::metadata(&firmware_path).await.is_ok() { - GzipDecoder::new(BufReader::new(File::open(&firmware_path).await?)) + GzipDecoder::new(BufReader::new(open_file(&firmware_path).await?)) } else { return Err(Error::new( eyre!("Firmware {id}.rom.gz not found in {firmware_dir:?}"), diff --git a/core/startos/src/init.rs b/core/startos/src/init.rs index cdc444c32..fdc38b60d 100644 --- a/core/startos/src/init.rs +++ b/core/startos/src/init.rs @@ -5,7 +5,7 @@ use std::os::unix::fs::PermissionsExt; use std::path::Path; use std::time::{Duration, SystemTime}; -use axum::extract::ws::{self, CloseFrame}; +use axum::extract::ws::{self}; use color_eyre::eyre::eyre; use futures::{StreamExt, TryStreamExt}; use itertools::Itertools; @@ -31,7 +31,7 @@ use crate::progress::{ }; use crate::rpc_continuations::{Guid, RpcContinuation}; use crate::ssh::SSH_AUTHORIZED_KEYS_FILE; -use crate::util::io::IOHook; +use crate::util::io::{create_file, IOHook}; use crate::util::net::WebSocketExt; use crate::util::{cpupower, Invoke}; use crate::Error; @@ -138,10 +138,7 @@ pub async fn init_postgres(datadir: impl AsRef) -> Result<(), Error> { old_version -= 1; let old_datadir = db_dir.join(old_version.to_string()); if tokio::fs::metadata(&old_datadir).await.is_ok() { - tokio::fs::File::create(&incomplete_path) - .await? - .sync_all() - .await?; + create_file(&incomplete_path).await?.sync_all().await?; Command::new("pg_upgradecluster") .arg(old_version.to_string()) .arg("main") diff --git a/core/startos/src/install/mod.rs b/core/startos/src/install/mod.rs index 5d50da27d..18591dc8d 100644 --- a/core/startos/src/install/mod.rs +++ b/core/startos/src/install/mod.rs @@ -1,21 +1,21 @@ +use std::ops::Deref; use std::path::PathBuf; -use std::sync::Arc; use std::time::Duration; use clap::builder::ValueParserFactory; use clap::{value_parser, CommandFactory, FromArgMatches, Parser}; use color_eyre::eyre::eyre; -use emver::VersionRange; -use futures::StreamExt; -use imbl_value::InternedString; +use exver::VersionRange; +use futures::{AsyncWriteExt, StreamExt}; +use imbl_value::{json, InternedString}; use itertools::Itertools; -use patch_db::json_ptr::JsonPointer; +use models::VersionString; use reqwest::header::{HeaderMap, CONTENT_LENGTH}; use reqwest::Url; use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::HandlerArgs; +use rustyline_async::ReadlineEvent; use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; use tokio::sync::oneshot; use tracing::instrument; use ts_rs::TS; @@ -23,13 +23,14 @@ use ts_rs::TS; use crate::context::{CliContext, RpcContext}; use crate::db::model::package::{ManifestPreference, PackageState, PackageStateMatchModelRef}; use crate::prelude::*; -use crate::progress::{FullProgress, PhasedProgressBar}; +use crate::progress::{FullProgress, FullProgressTracker, PhasedProgressBar}; +use crate::registry::context::{RegistryContext, RegistryUrlParams}; +use crate::registry::package::get::GetPackageResponse; use crate::rpc_continuations::{Guid, RpcContinuation}; use crate::s9pk::manifest::PackageId; -use crate::s9pk::merkle_archive::source::http::HttpSource; -use crate::s9pk::S9pk; use crate::upload::upload; use crate::util::clap::FromStrParser; +use crate::util::io::open_file; use crate::util::net::WebSocketExt; use crate::util::Never; @@ -38,32 +39,33 @@ pub const PKG_PUBLIC_DIR: &str = "package-data/public"; pub const PKG_WASM_DIR: &str = "package-data/wasm"; // #[command(display(display_serializable))] -pub async fn list(ctx: RpcContext) -> Result { - Ok(ctx.db.peek().await.as_public().as_package_data().as_entries()? +pub async fn list(ctx: RpcContext) -> Result, Error> { + Ok(ctx + .db + .peek() + .await + .as_public() + .as_package_data() + .as_entries()? .iter() .filter_map(|(id, pde)| { let status = match pde.as_state_info().as_match() { - PackageStateMatchModelRef::Installed(_) => { - "installed" - } - PackageStateMatchModelRef::Installing(_) => { - "installing" - } - PackageStateMatchModelRef::Updating(_) => { - "updating" - } - PackageStateMatchModelRef::Restoring(_) => { - "restoring" - } - PackageStateMatchModelRef::Removing(_) => { - "removing" - } - PackageStateMatchModelRef::Error(_) => { - "error" - } + PackageStateMatchModelRef::Installed(_) => "installed", + PackageStateMatchModelRef::Installing(_) => "installing", + PackageStateMatchModelRef::Updating(_) => "updating", + PackageStateMatchModelRef::Restoring(_) => "restoring", + PackageStateMatchModelRef::Removing(_) => "removing", + PackageStateMatchModelRef::Error(_) => "error", }; - serde_json::to_value(json!({ "status": status, "id": id.clone(), "version": pde.as_state_info().as_manifest(ManifestPreference::Old).as_version().de().ok()?})) - .ok() + Some(json!({ + "status": status, + "id": id.clone(), + "version": pde.as_state_info() + .as_manifest(ManifestPreference::Old) + .as_version() + .de() + .ok()? + })) }) .collect()) } @@ -107,65 +109,57 @@ impl std::fmt::Display for MinMax { } } -#[derive(Deserialize, Serialize, Parser, TS)] +#[derive(Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] -#[command(rename_all = "kebab-case")] pub struct InstallParams { + #[ts(type = "string")] + registry: Url, id: PackageId, - #[arg(short = 'm', long = "marketplace-url")] - #[ts(type = "string | null")] - registry: Option, - #[arg(short = 'v', long = "version-spec")] - version_spec: Option, - #[arg(long = "version-priority")] - version_priority: Option, + version: VersionString, } -// #[command( -// custom_cli(cli_install(async, context(CliContext))), -// )] #[instrument(skip_all)] pub async fn install( ctx: RpcContext, InstallParams { - id, registry, - version_spec, - version_priority, + id, + version, }: InstallParams, ) -> Result<(), Error> { - let version_str = match &version_spec { - None => "*", - Some(v) => &*v, - }; - let version: VersionRange = version_str.parse()?; - let registry = registry.unwrap_or_else(|| crate::DEFAULT_MARKETPLACE.parse().unwrap()); - let version_priority = version_priority.unwrap_or_default(); - let s9pk = S9pk::deserialize( - &Arc::new( - HttpSource::new( - ctx.client.clone(), - format!( - "{}/package/v0/{}.s9pk?spec={}&version-priority={}", - registry, id, version, version_priority, - ) - .parse()?, - ) - .await?, - ), - None, // TODO - ) - .await?; + let package: GetPackageResponse = from_value( + ctx.call_remote_with::( + "package.get", + json!({ + "id": id, + "version": VersionRange::exactly(version.deref().clone()), + }), + RegistryUrlParams { + registry: registry.clone(), + }, + ) + .await?, + )?; - ensure_code!( - &s9pk.as_manifest().id == &id, - ErrorKind::ValidateS9pk, - "manifest.id does not match expected" - ); + let asset = &package + .best + .get(&version) + .ok_or_else(|| { + Error::new( + eyre!("{id}@{version} not found on {registry}"), + ErrorKind::NotFound, + ) + })? + .s9pk; let download = ctx .services - .install(ctx.clone(), s9pk, None::) + .install( + ctx.clone(), + || asset.deserialize_s9pk(ctx.client.clone()), + None::, + None, + ) .await?; tokio::spawn(async move { download.await?.await }); @@ -193,113 +187,74 @@ pub async fn sideload( SideloadParams { session }: SideloadParams, ) -> Result { let (upload, file) = upload(&ctx, session.clone()).await?; - let (id_send, id_recv) = oneshot::channel(); let (err_send, err_recv) = oneshot::channel(); let progress = Guid::new(); - let db = ctx.db.clone(); - let mut sub = db - .subscribe( - "/package-data/{id}/install-progress" - .parse::() - .with_kind(ErrorKind::Database)?, - ) - .await; - ctx.rpc_continuations.add( - progress.clone(), - RpcContinuation::ws_authed(&ctx, session, - |mut ws| { - use axum::extract::ws::Message; - async move { - if let Err(e) = async { - let id = match id_recv.await.map_err(|_| { - Error::new( - eyre!("Could not get id to watch progress"), - ErrorKind::Cancelled, - ) - }).and_then(|a|a) { - Ok(a) => a, - Err(e) =>{ ws.send(Message::Text( - serde_json::to_string(&Err::<(), _>(RpcError::from(e.clone_output()))) - .with_kind(ErrorKind::Serialization)?, - )) - .await - .with_kind(ErrorKind::Network)?; - return Err(e); - } - }; - tokio::select! { - res = async { - while let Some(_) = sub.recv().await { - ws.send(Message::Text( - serde_json::to_string(&if let Some(p) = db - .peek() - .await - .as_public() - .as_package_data() - .as_idx(&id) - .and_then(|e| e.as_state_info().as_installing_info()).map(|i| i.as_progress()) - { - Ok::<_, ()>(p.de()?) - } else { - let mut p = FullProgress::new(); - p.overall.complete(); - Ok(p) - }) - .with_kind(ErrorKind::Serialization)?, - )) - .await - .with_kind(ErrorKind::Network)?; - } - Ok::<_, Error>(()) - } => res?, - err = err_recv => { - if let Ok(e) = err { - ws.send(Message::Text( - serde_json::to_string(&Err::<(), _>(e)) - .with_kind(ErrorKind::Serialization)?, - )) - .await - .with_kind(ErrorKind::Network)?; + let progress_tracker = FullProgressTracker::new(); + let mut progress_listener = progress_tracker.stream(Some(Duration::from_millis(200))); + ctx.rpc_continuations + .add( + progress.clone(), + RpcContinuation::ws_authed( + &ctx, + session, + |mut ws| { + use axum::extract::ws::Message; + async move { + if let Err(e) = async { + tokio::select! { + res = async { + while let Some(progress) = progress_listener.next().await { + ws.send(Message::Text( + serde_json::to_string(&Ok::<_, ()>(progress)) + .with_kind(ErrorKind::Serialization)?, + )) + .await + .with_kind(ErrorKind::Network)?; + } + Ok::<_, Error>(()) + } => res?, + err = err_recv => { + if let Ok(e) = err { + ws.send(Message::Text( + serde_json::to_string(&Err::<(), _>(e)) + .with_kind(ErrorKind::Serialization)?, + )) + .await + .with_kind(ErrorKind::Network)?; + } } } + + ws.normal_close("complete").await?; + + Ok::<_, Error>(()) + } + .await + { + tracing::error!("Error tracking sideload progress: {e}"); + tracing::debug!("{e:?}"); } - - ws.normal_close("complete").await?; - - Ok::<_, Error>(()) } - .await - { - tracing::error!("Error tracking sideload progress: {e}"); - tracing::debug!("{e:?}"); - } - } - }, - Duration::from_secs(600), - ), - ) - .await; + }, + Duration::from_secs(600), + ), + ) + .await; tokio::spawn(async move { if let Err(e) = async { - match S9pk::deserialize( - &file, None, // TODO - ) - .await - { - Ok(s9pk) => { - let _ = id_send.send(Ok(s9pk.as_manifest().id.clone())); - ctx.services - .install(ctx.clone(), s9pk, None::) - .await? - .await? - .await?; - file.delete().await - } - Err(e) => { - let _ = id_send.send(Err(e.clone_output())); - return Err(e); - } - } + let key = ctx.db.peek().await.into_private().into_compat_s9pk_key(); + + ctx.services + .install( + ctx.clone(), + || crate::s9pk::load(file.clone(), || Ok(key.de()?.0), Some(&progress_tracker)), + None::, + Some(progress_tracker.clone()), + ) + .await? + .await? + .await?; + file.delete().await } .await { @@ -311,10 +266,16 @@ pub async fn sideload( Ok(SideloadResponse { upload, progress }) } +#[derive(Deserialize, Serialize, Parser)] +pub struct QueryPackageParams { + id: PackageId, + version: Option, +} + #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub enum CliInstallParams { - Marketplace(InstallParams), + Marketplace(QueryPackageParams), Sideload(PathBuf), } impl CommandFactory for CliInstallParams { @@ -328,14 +289,19 @@ impl CommandFactory for CliInstallParams { .required_unless_present("id") .value_parser(value_parser!(PathBuf)), ) - .args(InstallParams::command().get_arguments().cloned().map(|a| { - if a.get_id() == "id" { - a.required(false).required_unless_present("sideload") - } else { - a - } - .conflicts_with("sideload") - })) + .args( + QueryPackageParams::command() + .get_arguments() + .cloned() + .map(|a| { + if a.get_id() == "id" { + a.required(false).required_unless_present("sideload") + } else { + a + } + .conflicts_with("sideload") + }), + ) } fn command_for_update() -> clap::Command { Self::command() @@ -346,7 +312,9 @@ impl FromArgMatches for CliInstallParams { if let Some(sideload) = matches.get_one::("sideload") { Ok(Self::Sideload(sideload.clone())) } else { - Ok(Self::Marketplace(InstallParams::from_arg_matches(matches)?)) + Ok(Self::Marketplace(QueryPackageParams::from_arg_matches( + matches, + )?)) } } fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> { @@ -355,6 +323,35 @@ impl FromArgMatches for CliInstallParams { } } +#[derive(Deserialize, Serialize, Parser, TS)] +#[ts(export)] +pub struct InstalledVersionParams { + id: PackageId, +} + +pub async fn installed_version( + ctx: RpcContext, + InstalledVersionParams { id }: InstalledVersionParams, +) -> Result, Error> { + if let Some(pde) = ctx + .db + .peek() + .await + .into_public() + .into_package_data() + .into_idx(&id) + { + Ok(Some( + pde.into_state_info() + .as_manifest(ManifestPreference::Old) + .as_version() + .de()?, + )) + } else { + Ok(None) + } +} + #[instrument(skip_all)] pub async fn cli_install( HandlerArgs { @@ -368,7 +365,7 @@ pub async fn cli_install( let method = parent_method.into_iter().chain(method).collect_vec(); match params { CliInstallParams::Sideload(path) => { - let file = crate::s9pk::load(&ctx, path).await?; + let file = open_file(path).await?; // rpc call remote sideload let SideloadResponse { upload, progress } = from_value::( @@ -435,9 +432,70 @@ pub async fn cli_install( progress?; upload?; } - CliInstallParams::Marketplace(params) => { - ctx.call_remote::(&method.join("."), to_value(¶ms)?) - .await?; + CliInstallParams::Marketplace(QueryPackageParams { id, version }) => { + let source_version: Option = from_value( + ctx.call_remote::("package.installed-version", json!({ "id": &id })) + .await?, + )?; + let mut packages: GetPackageResponse = from_value( + ctx.call_remote::( + "package.get", + json!({ "id": &id, "version": version, "sourceVersion": source_version }), + ) + .await?, + )?; + let version = if packages.best.len() == 1 { + packages.best.pop_first().map(|(k, _)| k).unwrap() + } else { + println!("Multiple flavors of {id} found. Please select one of the following versions to install:"); + let version; + loop { + let (mut read, mut output) = rustyline_async::Readline::new("> ".into()) + .with_kind(ErrorKind::Filesystem)?; + for (idx, version) in packages.best.keys().enumerate() { + output + .write_all(format!(" {}) {}\n", idx + 1, version).as_bytes()) + .await?; + read.add_history_entry(version.to_string()); + } + if let ReadlineEvent::Line(line) = read.readline().await? { + let trimmed = line.trim(); + match trimmed.parse() { + Ok(v) => { + if let Some((k, _)) = packages.best.remove_entry(&v) { + version = k; + break; + } + } + Err(_) => match trimmed.parse::() { + Ok(i) if (1..=packages.best.len()).contains(&i) => { + version = packages.best.keys().nth(i - 1).unwrap().clone(); + break; + } + _ => (), + }, + } + eprintln!("invalid selection: {trimmed}"); + println!("Please select one of the following versions to install:"); + } else { + return Err(Error::new( + eyre!("Could not determine precise version to install"), + ErrorKind::InvalidRequest, + ) + .into()); + } + } + version + }; + ctx.call_remote::( + &method.join("."), + to_value(&InstallParams { + id, + registry: ctx.registry_url.clone().or_not_found("--registry")?, + version, + })?, + ) + .await?; } } Ok(()) diff --git a/core/startos/src/lib.rs b/core/startos/src/lib.rs index 5a9561daf..4882d998e 100644 --- a/core/startos/src/lib.rs +++ b/core/startos/src/lib.rs @@ -1,4 +1,4 @@ -pub const DEFAULT_MARKETPLACE: &str = "https://registry.start9.com"; +pub const DEFAULT_REGISTRY: &str = "https://registry.start9.com"; // pub const COMMUNITY_MARKETPLACE: &str = "https://community-registry.start9.com"; pub const HOST_IP: [u8; 4] = [172, 18, 0, 1]; pub use std::env::consts::ARCH; @@ -263,6 +263,12 @@ pub fn package() -> ParentHandler { .with_display_serializable() .with_call_remote::(), ) + .subcommand( + "installed-version", + from_fn_async(install::installed_version) + .with_display_serializable() + .with_call_remote::(), + ) .subcommand("config", config::config::()) .subcommand( "start", diff --git a/core/startos/src/lxc/mod.rs b/core/startos/src/lxc/mod.rs index 6af707a6d..99f019d5a 100644 --- a/core/startos/src/lxc/mod.rs +++ b/core/startos/src/lxc/mod.rs @@ -12,7 +12,6 @@ use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::{GenericRpcMethod, RpcRequest, RpcResponse}; use rustyline_async::{ReadlineEvent, SharedWriter}; use serde::{Deserialize, Serialize}; -use tokio::fs::File; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::Command; use tokio::sync::Mutex; @@ -30,6 +29,7 @@ use crate::disk::mount::util::unmount; use crate::prelude::*; use crate::rpc_continuations::{Guid, RpcContinuation}; use crate::util::clap::FromStrParser; +use crate::util::io::open_file; use crate::util::rpc_client::UnixRpcClient; use crate::util::{new_guid, Invoke}; @@ -342,7 +342,7 @@ impl Drop for LxcContainer { if let Err(e) = async { let err_path = rootfs.path().join("var/log/containerRuntime.err"); if tokio::fs::metadata(&err_path).await.is_ok() { - let mut lines = BufReader::new(File::open(&err_path).await?).lines(); + let mut lines = BufReader::new(open_file(&err_path).await?).lines(); while let Some(line) = lines.next_line().await? { let container = &**guid; tracing::error!(container, "{}", line); diff --git a/core/startos/src/net/service_interface.rs b/core/startos/src/net/service_interface.rs index e905be545..dbe228ef2 100644 --- a/core/startos/src/net/service_interface.rs +++ b/core/startos/src/net/service_interface.rs @@ -5,8 +5,6 @@ use models::{HostId, ServiceInterfaceId}; use serde::{Deserialize, Serialize}; use ts_rs::TS; -use crate::net::host::address::HostAddress; - #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] diff --git a/core/startos/src/net/static_server.rs b/core/startos/src/net/static_server.rs index 7e8034d99..79adad504 100644 --- a/core/startos/src/net/static_server.rs +++ b/core/startos/src/net/static_server.rs @@ -19,7 +19,6 @@ use new_mime_guess::MimeGuess; use openssl::hash::MessageDigest; use openssl::x509::X509; use rpc_toolkit::{Context, HttpServer, Server}; -use tokio::fs::File; use tokio::io::BufReader; use tokio_util::io::ReaderStream; @@ -29,6 +28,7 @@ use crate::middleware::auth::{Auth, HasValidSession}; use crate::middleware::cors::Cors; use crate::middleware::db::SyncDb; use crate::rpc_continuations::{Guid, RpcContinuations}; +use crate::util::io::open_file; use crate::{ diagnostic_api, init_api, install_api, main_api, setup_api, Error, ErrorKind, ResultExt, }; @@ -44,8 +44,6 @@ const EMBEDDED_UIS: Dir<'_> = #[cfg(not(all(feature = "daemon", not(feature = "test"))))] const EMBEDDED_UIS: Dir<'_> = Dir::new("", &[]); -const PROXY_STRIP_HEADERS: &[&str] = &["cookie", "host", "origin", "referer", "user-agent"]; - #[derive(Clone)] pub enum UiMode { Setup, @@ -340,9 +338,8 @@ impl FileData { .any(|e| e == "gzip") .then_some("gzip"); - let file = File::open(path) - .await - .with_ctx(|_| (ErrorKind::Filesystem, path.display().to_string()))?; + let file = open_file(path) + .await?; let metadata = file .metadata() .await diff --git a/core/startos/src/notifications.rs b/core/startos/src/notifications.rs index fde3d5e80..c99ffb356 100644 --- a/core/startos/src/notifications.rs +++ b/core/startos/src/notifications.rs @@ -74,6 +74,7 @@ pub async fn list( .as_notifications() .as_entries()? .into_iter() + .rev() .take(limit); let notifs = records .into_iter() @@ -97,6 +98,7 @@ pub async fn list( .as_entries()? .into_iter() .filter(|(id, _)| *id < before) + .rev() .take(limit); records .into_iter() diff --git a/core/startos/src/os_install/mod.rs b/core/startos/src/os_install/mod.rs index 4e5c7ed15..28fd2a3be 100644 --- a/core/startos/src/os_install/mod.rs +++ b/core/startos/src/os_install/mod.rs @@ -21,7 +21,7 @@ use crate::disk::OsPartitionInfo; use crate::net::utils::find_eth_iface; use crate::prelude::*; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; -use crate::util::io::TmpDir; +use crate::util::io::{open_file, TmpDir}; use crate::util::serde::IoFormat; use crate::util::Invoke; use crate::ARCH; @@ -241,12 +241,10 @@ pub async fn execute( tokio::fs::create_dir_all(&images_path).await?; let image_path = images_path .join(hex::encode( - &MultiCursorFile::from( - tokio::fs::File::open("/run/live/medium/live/filesystem.squashfs").await?, - ) - .blake3_mmap() - .await? - .as_bytes()[..16], + &MultiCursorFile::from(open_file("/run/live/medium/live/filesystem.squashfs").await?) + .blake3_mmap() + .await? + .as_bytes()[..16], )) .with_extension("rootfs"); tokio::fs::copy("/run/live/medium/live/filesystem.squashfs", &image_path).await?; diff --git a/core/startos/src/registry/asset.rs b/core/startos/src/registry/asset.rs index ea37b2309..ab251d2aa 100644 --- a/core/startos/src/registry/asset.rs +++ b/core/startos/src/registry/asset.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::sync::Arc; use reqwest::Client; use serde::{Deserialize, Serialize}; @@ -7,10 +8,13 @@ use ts_rs::TS; use url::Url; use crate::prelude::*; +use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; use crate::registry::signer::commitment::{Commitment, Digestable}; use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey}; use crate::registry::signer::AcceptSigners; use crate::s9pk::merkle_archive::source::http::HttpSource; +use crate::s9pk::merkle_archive::source::Section; +use crate::s9pk::S9pk; #[derive(Debug, Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] @@ -52,3 +56,15 @@ impl Commitment<&'a HttpSource>> RegistryAsset { .await } } +impl RegistryAsset { + pub async fn deserialize_s9pk( + &self, + client: Client, + ) -> Result>>, Error> { + S9pk::deserialize( + &Arc::new(HttpSource::new(client, self.url.clone()).await?), + Some(&self.commitment), + ) + .await + } +} diff --git a/core/startos/src/registry/device_info.rs b/core/startos/src/registry/device_info.rs index 51d6ac46b..9a357358a 100644 --- a/core/startos/src/registry/device_info.rs +++ b/core/startos/src/registry/device_info.rs @@ -4,7 +4,7 @@ use std::ops::Deref; use axum::extract::Request; use axum::response::Response; -use emver::{Version, VersionRange}; +use exver::{Version, VersionRange}; use http::HeaderValue; use imbl_value::InternedString; use rpc_toolkit::{Middleware, RpcRequest, RpcResponse}; diff --git a/core/startos/src/registry/mod.rs b/core/startos/src/registry/mod.rs index 9a53d6338..1039264df 100644 --- a/core/startos/src/registry/mod.rs +++ b/core/startos/src/registry/mod.rs @@ -1,5 +1,4 @@ use std::collections::{BTreeMap, BTreeSet}; -use std::net::SocketAddr; use axum::Router; use futures::future::ready; diff --git a/core/startos/src/registry/os/asset/add.rs b/core/startos/src/registry/os/asset/add.rs index 33f5ef90f..6108dd5bc 100644 --- a/core/startos/src/registry/os/asset/add.rs +++ b/core/startos/src/registry/os/asset/add.rs @@ -3,7 +3,6 @@ use std::panic::UnwindSafe; use std::path::PathBuf; use clap::Parser; -use helpers::NonDetachingJoinHandle; use imbl_value::InternedString; use itertools::Itertools; use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; @@ -13,7 +12,7 @@ use url::Url; use crate::context::CliContext; use crate::prelude::*; -use crate::progress::{FullProgressTracker, PhasedProgressBar}; +use crate::progress::{FullProgressTracker}; use crate::registry::asset::RegistryAsset; use crate::registry::context::RegistryContext; use crate::registry::os::index::OsVersionInfo; @@ -25,6 +24,7 @@ use crate::s9pk::merkle_archive::hash::VerifyingWriter; use crate::s9pk::merkle_archive::source::http::HttpSource; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::ArchiveSource; +use crate::util::io::open_file; use crate::util::serde::Base64; use crate::util::VersionString; @@ -184,7 +184,7 @@ pub async fn cli_add_asset( } }; - let file = MultiCursorFile::from(tokio::fs::File::open(&path).await?); + let file = MultiCursorFile::from(open_file(&path).await?); let progress = FullProgressTracker::new(); let mut sign_phase = progress.add_phase(InternedString::intern("Signing File"), Some(10)); diff --git a/core/startos/src/registry/os/asset/get.rs b/core/startos/src/registry/os/asset/get.rs index 29ff24da6..b185cf6a4 100644 --- a/core/startos/src/registry/os/asset/get.rs +++ b/core/startos/src/registry/os/asset/get.rs @@ -20,6 +20,7 @@ use crate::registry::os::SIG_CONTEXT; use crate::registry::signer::commitment::blake3::Blake3Commitment; use crate::registry::signer::commitment::Commitment; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; +use crate::util::io::open_file; use crate::util::VersionString; pub fn get_api() -> ParentHandler { @@ -158,9 +159,7 @@ async fn cli_get_os_asset( if let Some(mut reverify_phase) = reverify_phase { reverify_phase.start(); res.commitment - .check(&MultiCursorFile::from( - tokio::fs::File::open(download).await?, - )) + .check(&MultiCursorFile::from(open_file(download).await?)) .await?; reverify_phase.complete(); } diff --git a/core/startos/src/registry/os/asset/sign.rs b/core/startos/src/registry/os/asset/sign.rs index 50c583593..8bf1cfeb5 100644 --- a/core/startos/src/registry/os/asset/sign.rs +++ b/core/startos/src/registry/os/asset/sign.rs @@ -21,6 +21,7 @@ use crate::registry::signer::sign::ed25519::Ed25519; use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey, SignatureScheme}; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::ArchiveSource; +use crate::util::io::open_file; use crate::util::serde::Base64; use crate::util::VersionString; @@ -166,7 +167,7 @@ pub async fn cli_sign_asset( } }; - let file = MultiCursorFile::from(tokio::fs::File::open(&path).await?); + let file = MultiCursorFile::from(open_file(&path).await?); let progress = FullProgressTracker::new(); let mut sign_phase = progress.add_phase(InternedString::intern("Signing File"), Some(10)); diff --git a/core/startos/src/registry/os/index.rs b/core/startos/src/registry/os/index.rs index 3ee75bc6a..0b1ca5b89 100644 --- a/core/startos/src/registry/os/index.rs +++ b/core/startos/src/registry/os/index.rs @@ -1,6 +1,6 @@ use std::collections::{BTreeMap, BTreeSet}; -use emver::VersionRange; +use exver::VersionRange; use imbl_value::InternedString; use serde::{Deserialize, Serialize}; use ts_rs::TS; diff --git a/core/startos/src/registry/os/version/mod.rs b/core/startos/src/registry/os/version/mod.rs index 234143295..9ebe8a696 100644 --- a/core/startos/src/registry/os/version/mod.rs +++ b/core/startos/src/registry/os/version/mod.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use chrono::Utc; use clap::Parser; -use emver::VersionRange; +use exver::VersionRange; use itertools::Itertools; use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; @@ -148,10 +148,11 @@ pub async fn get_version( if let (Some(pool), Some(server_id), Some(arch)) = (&ctx.pool, server_id, arch) { let created_at = Utc::now(); - query!("INSERT INTO user_activity (created_at, server_id, arch) VALUES ($1, $2, $3)", - created_at, - server_id, - arch + query!( + "INSERT INTO user_activity (created_at, server_id, arch) VALUES ($1, $2, $3)", + created_at, + server_id, + arch ) .execute(pool) .await?; diff --git a/core/startos/src/registry/package/get.rs b/core/startos/src/registry/package/get.rs index 835192361..fb63be1bc 100644 --- a/core/startos/src/registry/package/get.rs +++ b/core/startos/src/registry/package/get.rs @@ -1,7 +1,7 @@ use std::collections::{BTreeMap, BTreeSet}; use clap::{Parser, ValueEnum}; -use emver::{Version, VersionRange}; +use exver::{ExtendedVersion, VersionRange}; use imbl_value::InternedString; use itertools::Itertools; use models::PackageId; @@ -45,8 +45,7 @@ pub struct GetPackageParams { pub id: Option, #[ts(type = "string | null")] pub version: Option, - #[ts(type = "string | null")] - pub source_version: Option, + pub source_version: Option, #[ts(skip)] #[arg(skip)] #[serde(rename = "__device_info")] @@ -132,7 +131,7 @@ fn get_matching_models<'a>( device_info, .. }: &GetPackageParams, -) -> Result)>, Error> { +) -> Result)>, Error> { if let Some(id) = id { if let Some(pkg) = db.as_packages().as_idx(id) { vec![(id.clone(), pkg)] @@ -166,7 +165,7 @@ fn get_matching_models<'a>( .as_ref() .map_or(Ok(true), |device_info| info.works_for_device(device_info))? { - Some((k.clone(), Version::from(v), info)) + Some((k.clone(), ExtendedVersion::from(v), info)) } else { None }, diff --git a/core/startos/src/registry/package/index.rs b/core/startos/src/registry/package/index.rs index 0e6969fa5..80055f06d 100644 --- a/core/startos/src/registry/package/index.rs +++ b/core/startos/src/registry/package/index.rs @@ -1,6 +1,6 @@ use std::collections::{BTreeMap, BTreeSet}; -use emver::{Version, VersionRange}; +use exver::{Version, VersionRange}; use imbl_value::InternedString; use models::{DataUrl, PackageId, VersionString}; use serde::{Deserialize, Serialize}; @@ -70,7 +70,8 @@ pub struct PackageVersionInfo { pub support_site: Url, #[ts(type = "string")] pub marketing_site: Url, - pub os_version: VersionString, + #[ts(type = "string")] + pub os_version: Version, pub hardware_requirements: HardwareRequirements, #[ts(type = "string | null")] pub source_version: Option, diff --git a/core/startos/src/registry/signer/sign/mod.rs b/core/startos/src/registry/signer/sign/mod.rs index 50576a198..a29109864 100644 --- a/core/startos/src/registry/signer/sign/mod.rs +++ b/core/startos/src/registry/signer/sign/mod.rs @@ -4,7 +4,6 @@ use std::str::FromStr; use ::ed25519::pkcs8::BitStringRef; use clap::builder::ValueParserFactory; use der::referenced::OwnedToRef; -use der::{Decode, Encode}; use pkcs8::der::AnyRef; use pkcs8::{PrivateKeyInfo, SubjectPublicKeyInfo}; use serde::{Deserialize, Serialize}; diff --git a/core/startos/src/s9pk/merkle_archive/directory_contents.rs b/core/startos/src/s9pk/merkle_archive/directory_contents.rs index c5e5c4a7d..b39789222 100644 --- a/core/startos/src/s9pk/merkle_archive/directory_contents.rs +++ b/core/startos/src/s9pk/merkle_archive/directory_contents.rs @@ -274,6 +274,21 @@ impl DirectoryContents { ((_, a), (_, b), _) if !a.as_contents().is_dir() && b.as_contents().is_dir() => { std::cmp::Ordering::Greater } + ((_, a), (_, b), _) + if a.as_contents().is_missing() && !b.as_contents().is_missing() => + { + std::cmp::Ordering::Greater + } + ((_, a), (_, b), _) + if !a.as_contents().is_missing() && b.as_contents().is_missing() => + { + std::cmp::Ordering::Less + } + ((n_a, a), (n_b, b), _) + if a.as_contents().is_missing() && b.as_contents().is_missing() => + { + n_a.cmp(n_b) + } ((a, _), (b, _), Some(sort_by)) => sort_by(&***a, &***b), _ => std::cmp::Ordering::Equal, }) { diff --git a/core/startos/src/s9pk/merkle_archive/mod.rs b/core/startos/src/s9pk/merkle_archive/mod.rs index 00ead65c5..977e5ebb2 100644 --- a/core/startos/src/s9pk/merkle_archive/mod.rs +++ b/core/startos/src/s9pk/merkle_archive/mod.rs @@ -121,14 +121,14 @@ impl MerkleArchive> { } if max_size > *root_maxsize { return Err(Error::new( - eyre!("merkle root directory max size too large"), + eyre!("root directory max size too large"), ErrorKind::InvalidSignature, )); } } else { if max_size > CAP_1_MiB as u64 { return Err(Error::new( - eyre!("merkle root directory max size over 1MiB, cancelling download in case of DOS attack"), + eyre!("root directory max size over 1MiB, cancelling download in case of DOS attack"), ErrorKind::InvalidSignature, )); } @@ -377,6 +377,9 @@ impl EntryContents { pub fn is_dir(&self) -> bool { matches!(self, &EntryContents::Directory(_)) } + pub fn is_missing(&self) -> bool { + matches!(self, &EntryContents::Missing) + } } impl EntryContents> { #[instrument(skip_all)] diff --git a/core/startos/src/s9pk/merkle_archive/source/http.rs b/core/startos/src/s9pk/merkle_archive/source/http.rs index fda9d32ed..e58208277 100644 --- a/core/startos/src/s9pk/merkle_archive/source/http.rs +++ b/core/startos/src/s9pk/merkle_archive/source/http.rs @@ -4,7 +4,7 @@ use std::sync::{Arc, Mutex}; use std::task::Poll; use bytes::Bytes; -use futures::{Stream, StreamExt, TryStreamExt}; +use futures::{Stream, TryStreamExt}; use reqwest::header::{ACCEPT_RANGES, CONTENT_LENGTH, RANGE}; use reqwest::{Client, Url}; use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf, Take}; @@ -54,11 +54,12 @@ impl HttpSource { } } impl ArchiveSource for HttpSource { - type Reader = HttpReader; + type FetchReader = HttpReader; + type FetchAllReader = StreamReader>, Bytes>; async fn size(&self) -> Option { self.size } - async fn fetch_all(&self) -> Result { + async fn fetch_all(&self) -> Result { Ok(StreamReader::new( self.client .get(self.url.clone()) @@ -72,7 +73,7 @@ impl ArchiveSource for HttpSource { .apply(boxed), )) } - async fn fetch(&self, position: u64, size: u64) -> Result { + async fn fetch(&self, position: u64, size: u64) -> Result { match &self.range_support { Ok(_) => Ok(HttpReader::Range( StreamReader::new(if size > 0 { diff --git a/core/startos/src/s9pk/merkle_archive/source/mod.rs b/core/startos/src/s9pk/merkle_archive/source/mod.rs index 0a00b18dd..6b7459787 100644 --- a/core/startos/src/s9pk/merkle_archive/source/mod.rs +++ b/core/startos/src/s9pk/merkle_archive/source/mod.rs @@ -10,6 +10,7 @@ use tokio::io::{AsyncRead, AsyncWrite}; use crate::prelude::*; use crate::s9pk::merkle_archive::hash::VerifyingWriter; +use crate::util::io::{open_file, TmpDir}; pub mod http; pub mod multi_cursor_file; @@ -159,7 +160,7 @@ impl FileSource for PathBuf { Ok(tokio::fs::metadata(self).await?.len()) } async fn reader(&self) -> Result { - Ok(File::open(self).await?) + Ok(open_file(self).await?) } } @@ -180,18 +181,17 @@ impl FileSource for Arc<[u8]> { } pub trait ArchiveSource: Send + Sync + Sized + 'static { - type Reader: AsyncRead + Unpin + Send; + type FetchReader: AsyncRead + Unpin + Send; + type FetchAllReader: AsyncRead + Unpin + Send; fn size(&self) -> impl Future> + Send { async { None } } - fn fetch_all( - &self, - ) -> impl Future> + Send; + fn fetch_all(&self) -> impl Future> + Send; fn fetch( &self, position: u64, size: u64, - ) -> impl Future> + Send; + ) -> impl Future> + Send; fn copy_all_to( &self, w: &mut W, @@ -222,14 +222,15 @@ pub trait ArchiveSource: Send + Sync + Sized + 'static { } impl ArchiveSource for Arc { - type Reader = T::Reader; + type FetchReader = T::FetchReader; + type FetchAllReader = T::FetchAllReader; async fn size(&self) -> Option { self.deref().size().await } - async fn fetch_all(&self) -> Result { + async fn fetch_all(&self) -> Result { self.deref().fetch_all().await } - async fn fetch(&self, position: u64, size: u64) -> Result { + async fn fetch(&self, position: u64, size: u64) -> Result { self.deref().fetch(position, size).await } async fn copy_all_to( @@ -249,11 +250,12 @@ impl ArchiveSource for Arc { } impl ArchiveSource for Arc<[u8]> { - type Reader = tokio::io::Take>; - async fn fetch_all(&self) -> Result { + type FetchReader = tokio::io::Take>; + type FetchAllReader = std::io::Cursor; + async fn fetch_all(&self) -> Result { Ok(std::io::Cursor::new(self.clone())) } - async fn fetch(&self, position: u64, size: u64) -> Result { + async fn fetch(&self, position: u64, size: u64) -> Result { use tokio::io::AsyncReadExt; let mut cur = std::io::Cursor::new(self.clone()); @@ -269,7 +271,7 @@ pub struct Section { size: u64, } impl FileSource for Section { - type Reader = S::Reader; + type Reader = S::FetchReader; async fn size(&self) -> Result { Ok(self.size) } @@ -285,3 +287,81 @@ pub type DynRead = Box; pub fn into_dyn_read(r: R) -> DynRead { Box::new(r) } + +#[derive(Clone)] +pub struct TmpSource { + tmp_dir: Arc, + source: S, +} +impl TmpSource { + pub fn new(tmp_dir: Arc, source: S) -> Self { + Self { tmp_dir, source } + } + pub async fn gc(self) -> Result<(), Error> { + self.tmp_dir.gc().await + } +} +impl std::ops::Deref for TmpSource { + type Target = S; + fn deref(&self) -> &Self::Target { + &self.source + } +} +impl ArchiveSource for TmpSource { + type FetchReader = ::FetchReader; + type FetchAllReader = ::FetchAllReader; + async fn size(&self) -> Option { + self.source.size().await + } + async fn fetch_all(&self) -> Result { + self.source.fetch_all().await + } + async fn fetch(&self, position: u64, size: u64) -> Result { + self.source.fetch(position, size).await + } + async fn copy_all_to( + &self, + w: &mut W, + ) -> Result<(), Error> { + self.source.copy_all_to(w).await + } + async fn copy_to( + &self, + position: u64, + size: u64, + w: &mut W, + ) -> Result<(), Error> { + self.source.copy_to(position, size, w).await + } +} +impl From> for DynFileSource { + fn from(value: TmpSource) -> Self { + DynFileSource::new(value) + } +} + +impl FileSource for TmpSource { + type Reader = ::Reader; + async fn size(&self) -> Result { + self.source.size().await + } + async fn reader(&self) -> Result { + self.source.reader().await + } + async fn copy( + &self, + mut w: &mut W, + ) -> Result<(), Error> { + self.source.copy(&mut w).await + } + async fn copy_verify( + &self, + mut w: &mut W, + verify: Option<(Hash, u64)>, + ) -> Result<(), Error> { + self.source.copy_verify(&mut w, verify).await + } + async fn to_vec(&self, verify: Option<(Hash, u64)>) -> Result, Error> { + self.source.to_vec(verify).await + } +} diff --git a/core/startos/src/s9pk/merkle_archive/source/multi_cursor_file.rs b/core/startos/src/s9pk/merkle_archive/source/multi_cursor_file.rs index 4b7d5736d..658f3f923 100644 --- a/core/startos/src/s9pk/merkle_archive/source/multi_cursor_file.rs +++ b/core/startos/src/s9pk/merkle_archive/source/multi_cursor_file.rs @@ -6,12 +6,13 @@ use std::sync::Arc; use std::task::Poll; use tokio::fs::File; -use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf, Take}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, ReadBuf, Take}; use tokio::sync::{Mutex, OwnedMutexGuard}; use crate::disk::mount::filesystem::loop_dev::LoopDev; use crate::prelude::*; use crate::s9pk::merkle_archive::source::{ArchiveSource, Section}; +use crate::util::io::open_file; fn path_from_fd(fd: RawFd) -> Result { #[cfg(target_os = "linux")] @@ -42,7 +43,7 @@ impl MultiCursorFile { path_from_fd(self.fd) } pub async fn open(fd: &impl AsRawFd) -> Result { - let f = File::open(path_from_fd(fd.as_raw_fd())?).await?; + let f = open_file(path_from_fd(fd.as_raw_fd())?).await?; Ok(Self::from(f)) } pub async fn cursor(&self) -> Result { @@ -50,7 +51,7 @@ impl MultiCursorFile { if let Ok(file) = self.file.clone().try_lock_owned() { file } else { - Arc::new(Mutex::new(File::open(self.path()?).await?)) + Arc::new(Mutex::new(open_file(self.path()?).await?)) .try_lock_owned() .expect("freshly created") }, @@ -88,24 +89,48 @@ impl AsyncRead for FileCursor { Pin::new(&mut (&mut **this.0.get_mut())).poll_read(cx, buf) } } +impl AsyncSeek for FileCursor { + fn start_seek(self: Pin<&mut Self>, position: SeekFrom) -> std::io::Result<()> { + let this = self.project(); + Pin::new(&mut (&mut **this.0.get_mut())).start_seek(position) + } + fn poll_complete( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let this = self.project(); + Pin::new(&mut (&mut **this.0.get_mut())).poll_complete(cx) + } +} +impl std::ops::Deref for FileCursor { + type Target = File; + fn deref(&self) -> &Self::Target { + &*self.0 + } +} +impl std::ops::DerefMut for FileCursor { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut *self.0 + } +} impl ArchiveSource for MultiCursorFile { - type Reader = Take; + type FetchReader = Take; + type FetchAllReader = FileCursor; async fn size(&self) -> Option { tokio::fs::metadata(self.path().ok()?) .await .ok() .map(|m| m.len()) } - #[allow(refining_impl_trait)] - async fn fetch_all(&self) -> Result { + async fn fetch_all(&self) -> Result { use tokio::io::AsyncSeekExt; let mut file = self.cursor().await?; file.0.seek(SeekFrom::Start(0)).await?; Ok(file) } - async fn fetch(&self, position: u64, size: u64) -> Result { + async fn fetch(&self, position: u64, size: u64) -> Result { use tokio::io::AsyncSeekExt; let mut file = self.cursor().await?; diff --git a/core/startos/src/s9pk/merkle_archive/varint.rs b/core/startos/src/s9pk/merkle_archive/varint.rs index 479b488e6..f4f18d140 100644 --- a/core/startos/src/s9pk/merkle_archive/varint.rs +++ b/core/startos/src/s9pk/merkle_archive/varint.rs @@ -3,7 +3,7 @@ use tokio::io::{AsyncRead, AsyncWrite}; use crate::prelude::*; -/// Most-significant byte, == 0x80 +/// Most-significant bit, == 0x80 pub const MSB: u8 = 0b1000_0000; const MAX_STR_LEN: u64 = 1024 * 1024; // 1 MiB @@ -39,22 +39,20 @@ pub async fn serialize_varstring( Ok(()) } +const MAX_SIZE: usize = (std::mem::size_of::() * 8 + 7) / 7; + #[derive(Default)] struct VarIntProcessor { - buf: [u8; 10], - maxsize: usize, + buf: [u8; MAX_SIZE], i: usize, } impl VarIntProcessor { fn new() -> VarIntProcessor { - VarIntProcessor { - maxsize: (std::mem::size_of::() * 8 + 7) / 7, - ..VarIntProcessor::default() - } + Self::default() } fn push(&mut self, b: u8) -> Result<(), Error> { - if self.i >= self.maxsize { + if self.i >= MAX_SIZE { return Err(Error::new( eyre!("Unterminated varint"), ErrorKind::ParseS9pk, diff --git a/core/startos/src/s9pk/mod.rs b/core/startos/src/s9pk/mod.rs index fcf9379a0..a06218d40 100644 --- a/core/startos/src/s9pk/mod.rs +++ b/core/startos/src/s9pk/mod.rs @@ -4,37 +4,57 @@ pub mod rpc; pub mod v1; pub mod v2; -use std::io::SeekFrom; -use std::path::Path; +use std::sync::Arc; -use tokio::fs::File; -use tokio::io::{AsyncReadExt, AsyncSeekExt}; +use tokio::io::{AsyncReadExt, AsyncSeek}; pub use v2::{manifest, S9pk}; -use crate::context::CliContext; use crate::prelude::*; +use crate::progress::FullProgressTracker; +use crate::s9pk::merkle_archive::source::{ArchiveSource, DynFileSource}; use crate::s9pk::v1::reader::S9pkReader; use crate::s9pk::v2::compat::MAGIC_AND_VERSION; +use crate::util::io::TmpDir; -pub async fn load(ctx: &CliContext, path: impl AsRef) -> Result { +pub async fn load( + source: S, + key: K, + progress: Option<&FullProgressTracker>, +) -> Result, Error> +where + S: ArchiveSource, + S::FetchAllReader: AsyncSeek + Sync, + K: FnOnce() -> Result, +{ // TODO: return s9pk const MAGIC_LEN: usize = MAGIC_AND_VERSION.len(); let mut magic = [0_u8; MAGIC_LEN]; - let mut file = tokio::fs::File::open(&path).await?; - file.read_exact(&mut magic).await?; - file.seek(SeekFrom::Start(0)).await?; + source.fetch(0, 3).await?.read_exact(&mut magic).await?; if magic == v2::compat::MAGIC_AND_VERSION { + let phase = if let Some(progress) = progress { + let mut phase = progress.add_phase( + "Converting Package to V2".into(), + Some(source.size().await.unwrap_or(60)), + ); + phase.start(); + Some(phase) + } else { + None + }; tracing::info!("Converting package to v2 s9pk"); - let new_path = path.as_ref().with_extension("compat.s9pk"); - S9pk::from_v1( - S9pkReader::from_reader(file, true).await?, - &new_path, - ctx.developer_key()?.clone(), + let tmp_dir = TmpDir::new().await?; + let s9pk = S9pk::from_v1( + S9pkReader::from_reader(source.fetch_all().await?, true).await?, + Arc::new(tmp_dir), + key()?, ) .await?; - tokio::fs::rename(&new_path, &path).await?; - file = tokio::fs::File::open(&path).await?; tracing::info!("Converted s9pk successfully"); + if let Some(mut phase) = phase { + phase.complete(); + } + Ok(s9pk.into_dyn()) + } else { + Ok(S9pk::deserialize(&Arc::new(source), None).await?.into_dyn()) } - Ok(file) } diff --git a/core/startos/src/s9pk/rpc.rs b/core/startos/src/s9pk/rpc.rs index fac9e6724..83b78dad7 100644 --- a/core/startos/src/s9pk/rpc.rs +++ b/core/startos/src/s9pk/rpc.rs @@ -1,19 +1,19 @@ use std::path::PathBuf; +use std::sync::Arc; use clap::Parser; use models::ImageId; use rpc_toolkit::{from_fn_async, Empty, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; -use tokio::fs::File; use ts_rs::TS; use crate::context::CliContext; use crate::prelude::*; use crate::s9pk::manifest::Manifest; +use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::v2::pack::ImageConfig; use crate::s9pk::v2::SIG_CONTEXT; -use crate::s9pk::S9pk; -use crate::util::io::TmpDir; +use crate::util::io::{create_file, open_file, TmpDir}; use crate::util::serde::{apply_expr, HandlerExtSerde}; pub const SKIP_ENV: &[&str] = &["TERM", "container", "HOME", "HOSTNAME"]; @@ -79,19 +79,25 @@ async fn add_image( AddImageParams { id, config }: AddImageParams, S9pkPath { s9pk: s9pk_path }: S9pkPath, ) -> Result<(), Error> { - let mut s9pk = S9pk::from_file(super::load(&ctx, &s9pk_path).await?) - .await? - .into_dyn(); + let mut s9pk = super::load( + MultiCursorFile::from(open_file(&s9pk_path).await?), + || ctx.developer_key().cloned(), + None, + ) + .await?; s9pk.as_manifest_mut().images.insert(id, config); - let tmpdir = TmpDir::new().await?; - s9pk.load_images(&tmpdir).await?; + let tmp_dir = Arc::new(TmpDir::new().await?); + s9pk.load_images(tmp_dir.clone()).await?; s9pk.validate_and_filter(None)?; let tmp_path = s9pk_path.with_extension("s9pk.tmp"); - let mut tmp_file = File::create(&tmp_path).await?; + let mut tmp_file = create_file(&tmp_path).await?; s9pk.serialize(&mut tmp_file, true).await?; + drop(s9pk); tmp_file.sync_all().await?; tokio::fs::rename(&tmp_path, &s9pk_path).await?; + tmp_dir.gc().await?; + Ok(()) } @@ -104,13 +110,18 @@ async fn edit_manifest( EditManifestParams { expression }: EditManifestParams, S9pkPath { s9pk: s9pk_path }: S9pkPath, ) -> Result { - let mut s9pk = S9pk::from_file(super::load(&ctx, &s9pk_path).await?).await?; + let mut s9pk = super::load( + MultiCursorFile::from(open_file(&s9pk_path).await?), + || ctx.developer_key().cloned(), + None, + ) + .await?; let old = serde_json::to_value(s9pk.as_manifest()).with_kind(ErrorKind::Serialization)?; *s9pk.as_manifest_mut() = serde_json::from_value(apply_expr(old.into(), &expression)?.into()) .with_kind(ErrorKind::Serialization)?; let manifest = s9pk.as_manifest().clone(); let tmp_path = s9pk_path.with_extension("s9pk.tmp"); - let mut tmp_file = File::create(&tmp_path).await?; + let mut tmp_file = create_file(&tmp_path).await?; s9pk.as_archive_mut() .set_signer(ctx.developer_key()?.clone(), SIG_CONTEXT); s9pk.serialize(&mut tmp_file, true).await?; @@ -123,9 +134,14 @@ async fn edit_manifest( async fn file_tree( ctx: CliContext, _: Empty, - S9pkPath { s9pk }: S9pkPath, + S9pkPath { s9pk: s9pk_path }: S9pkPath, ) -> Result, Error> { - let s9pk = S9pk::from_file(super::load(&ctx, &s9pk).await?).await?; + let s9pk = super::load( + MultiCursorFile::from(open_file(&s9pk_path).await?), + || ctx.developer_key().cloned(), + None, + ) + .await?; Ok(s9pk.as_archive().contents().file_paths("")) } @@ -138,11 +154,16 @@ struct CatParams { async fn cat( ctx: CliContext, CatParams { file_path }: CatParams, - S9pkPath { s9pk }: S9pkPath, + S9pkPath { s9pk: s9pk_path }: S9pkPath, ) -> Result<(), Error> { use crate::s9pk::merkle_archive::source::FileSource; - let s9pk = S9pk::from_file(super::load(&ctx, &s9pk).await?).await?; + let s9pk = super::load( + MultiCursorFile::from(open_file(&s9pk_path).await?), + || ctx.developer_key().cloned(), + None, + ) + .await?; tokio::io::copy( &mut s9pk .as_archive() @@ -162,8 +183,13 @@ async fn cat( async fn inspect_manifest( ctx: CliContext, _: Empty, - S9pkPath { s9pk }: S9pkPath, + S9pkPath { s9pk: s9pk_path }: S9pkPath, ) -> Result { - let s9pk = S9pk::from_file(super::load(&ctx, &s9pk).await?).await?; + let s9pk = super::load( + MultiCursorFile::from(open_file(&s9pk_path).await?), + || ctx.developer_key().cloned(), + None, + ) + .await?; Ok(s9pk.as_manifest().clone()) } diff --git a/core/startos/src/s9pk/v1/manifest.rs b/core/startos/src/s9pk/v1/manifest.rs index 845d8f773..4a9956f9f 100644 --- a/core/startos/src/s9pk/v1/manifest.rs +++ b/core/startos/src/s9pk/v1/manifest.rs @@ -1,8 +1,7 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; -use emver::VersionRange; -use imbl_value::InOMap; +use exver::{Version, VersionRange}; use indexmap::IndexMap; pub use models::PackageId; use models::{ActionId, HealthCheckId, ImageId, VolumeId}; @@ -13,23 +12,16 @@ use crate::prelude::*; use crate::s9pk::git_hash::GitHash; use crate::s9pk::manifest::{Alerts, Description, HardwareRequirements}; use crate::util::serde::{Duration, IoFormat}; -use crate::util::VersionString; -use crate::version::{Current, VersionT}; - -fn current_version() -> VersionString { - Current::new().semver().into() -} #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct Manifest { - #[serde(default = "current_version")] - pub eos_version: VersionString, + pub eos_version: Version, pub id: PackageId, #[serde(default)] pub git_hash: Option, pub title: String, - pub version: VersionString, + pub version: exver::emver::Version, pub description: Description, #[serde(default)] pub assets: Assets, diff --git a/core/startos/src/s9pk/v1/reader.rs b/core/startos/src/s9pk/v1/reader.rs index 7437fdaa9..f2bbf578e 100644 --- a/core/startos/src/s9pk/v1/reader.rs +++ b/core/startos/src/s9pk/v1/reader.rs @@ -20,6 +20,7 @@ use super::header::{FileSection, Header, TableOfContents}; use super::SIG_CONTEXT; use crate::prelude::*; use crate::s9pk::v1::docker::DockerReader; +use crate::util::io::open_file; use crate::util::VersionString; #[pin_project::pin_project] @@ -150,9 +151,7 @@ pub struct S9pkReader>(path: P, check_sig: bool) -> Result { let p = path.as_ref(); - let rdr = File::open(p) - .await - .with_ctx(|_| (crate::error::ErrorKind::Filesystem, p.display().to_string()))?; + let rdr = open_file(p).await?; Self::from_reader(BufReader::new(rdr), check_sig).await } diff --git a/core/startos/src/s9pk/v2/compat.rs b/core/startos/src/s9pk/v2/compat.rs index ec5b586ea..914d2e5aa 100644 --- a/core/startos/src/s9pk/v2/compat.rs +++ b/core/startos/src/s9pk/v2/compat.rs @@ -2,9 +2,8 @@ use std::collections::BTreeMap; use std::path::Path; use std::sync::Arc; -use itertools::Itertools; +use exver::ExtendedVersion; use models::ImageId; -use tokio::fs::File; use tokio::io::{AsyncRead, AsyncSeek, AsyncWriteExt}; use tokio::process::Command; @@ -12,29 +11,35 @@ use crate::dependencies::{DepInfo, Dependencies}; use crate::prelude::*; use crate::s9pk::manifest::Manifest; use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; -use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; -use crate::s9pk::merkle_archive::source::Section; +use crate::s9pk::merkle_archive::source::TmpSource; use crate::s9pk::merkle_archive::{Entry, MerkleArchive}; -use crate::s9pk::rpc::SKIP_ENV; use crate::s9pk::v1::manifest::{Manifest as ManifestV1, PackageProcedure}; use crate::s9pk::v1::reader::S9pkReader; -use crate::s9pk::v2::pack::{PackSource, CONTAINER_TOOL}; +use crate::s9pk::v2::pack::{ImageSource, PackSource, CONTAINER_TOOL}; use crate::s9pk::v2::{S9pk, SIG_CONTEXT}; -use crate::util::io::TmpDir; +use crate::util::io::{create_file, TmpDir}; use crate::util::Invoke; pub const MAGIC_AND_VERSION: &[u8] = &[0x3b, 0x3b, 0x01]; -impl S9pk> { +impl S9pk> { #[instrument(skip_all)] pub async fn from_v1( mut reader: S9pkReader, - destination: impl AsRef, + tmp_dir: Arc, signer: ed25519_dalek::SigningKey, ) -> Result { - let scratch_dir = TmpDir::new().await?; + Command::new(CONTAINER_TOOL) + .arg("run") + .arg("--rm") + .arg("--privileged") + .arg("tonistiigi/binfmt") + .arg("--install") + .arg("all") + .invoke(ErrorKind::Docker) + .await?; - let mut archive = DirectoryContents::::new(); + let mut archive = DirectoryContents::>::new(); // manifest.json let manifest_raw = reader.manifest().await?; @@ -56,33 +61,35 @@ impl S9pk> { let license: Arc<[u8]> = reader.license().await?.to_vec().await?.into(); archive.insert_path( "LICENSE.md", - Entry::file(PackSource::Buffered(license.into())), + Entry::file(TmpSource::new( + tmp_dir.clone(), + PackSource::Buffered(license.into()), + )), )?; // instructions.md let instructions: Arc<[u8]> = reader.instructions().await?.to_vec().await?.into(); archive.insert_path( "instructions.md", - Entry::file(PackSource::Buffered(instructions.into())), + Entry::file(TmpSource::new( + tmp_dir.clone(), + PackSource::Buffered(instructions.into()), + )), )?; // icon.md let icon: Arc<[u8]> = reader.icon().await?.to_vec().await?.into(); archive.insert_path( format!("icon.{}", manifest.assets.icon_type()), - Entry::file(PackSource::Buffered(icon.into())), + Entry::file(TmpSource::new( + tmp_dir.clone(), + PackSource::Buffered(icon.into()), + )), )?; // images for arch in reader.docker_arches().await? { - let images_dir = scratch_dir.join("images").join(&arch); - let docker_platform = if arch == "x86_64" { - "--platform=linux/amd64".to_owned() - } else if arch == "aarch64" { - "--platform=linux/arm64".to_owned() - } else { - format!("--platform=linux/{arch}") - }; + let images_dir = tmp_dir.join("images").join(&arch); tokio::fs::create_dir_all(&images_dir).await?; Command::new(CONTAINER_TOOL) .arg("load") @@ -93,97 +100,24 @@ impl S9pk> { let mut image_config = new_manifest.images.remove(image).unwrap_or_default(); image_config.arch.insert(arch.as_str().into()); new_manifest.images.insert(image.clone(), image_config); - let sqfs_path = images_dir.join(image).with_extension("squashfs"); let image_name = if *system { format!("start9/{}:latest", image) } else { format!("start9/{}/{}:{}", manifest.id, image, manifest.version) }; - let id = String::from_utf8( - Command::new(CONTAINER_TOOL) - .arg("create") - .arg(&docker_platform) - .arg(&image_name) - .invoke(ErrorKind::Docker) - .await?, - )?; - let env = String::from_utf8( - Command::new(CONTAINER_TOOL) - .arg("run") - .arg("--rm") - .arg(&docker_platform) - .arg("--entrypoint") - .arg("env") - .arg(&image_name) - .invoke(ErrorKind::Docker) - .await?, - )? - .lines() - .filter(|l| { - l.trim() - .split_once("=") - .map_or(false, |(v, _)| !SKIP_ENV.contains(&v)) - }) - .join("\n") - + "\n"; - let workdir = Path::new( - String::from_utf8( - Command::new(CONTAINER_TOOL) - .arg("run") - .arg("--rm") - .arg(&docker_platform) - .arg("--entrypoint") - .arg("pwd") - .arg(&image_name) - .invoke(ErrorKind::Docker) - .await?, - )? - .trim(), - ) - .to_owned(); - Command::new("bash") - .arg("-c") - .arg(format!( - "{CONTAINER_TOOL} export {id} | mksquashfs - {sqfs} -tar", - id = id.trim(), - sqfs = sqfs_path.display() - )) - .invoke(ErrorKind::Docker) + ImageSource::DockerTag(image_name.clone()) + .load( + tmp_dir.clone(), + &new_manifest.id, + &new_manifest.version, + image, + &arch, + &mut archive, + ) .await?; - Command::new(CONTAINER_TOOL) - .arg("rm") - .arg(id.trim()) - .invoke(ErrorKind::Docker) - .await?; - archive.insert_path( - Path::new("images") - .join(&arch) - .join(&image) - .with_extension("squashfs"), - Entry::file(PackSource::File(sqfs_path)), - )?; - archive.insert_path( - Path::new("images") - .join(&arch) - .join(&image) - .with_extension("env"), - Entry::file(PackSource::Buffered(Vec::from(env).into())), - )?; - archive.insert_path( - Path::new("images") - .join(&arch) - .join(&image) - .with_extension("json"), - Entry::file(PackSource::Buffered( - serde_json::to_vec(&serde_json::json!({ - "workdir": workdir - })) - .with_kind(ErrorKind::Serialization)? - .into(), - )), - )?; Command::new(CONTAINER_TOOL) .arg("rmi") + .arg("-f") .arg(&image_name) .invoke(ErrorKind::Docker) .await?; @@ -191,7 +125,7 @@ impl S9pk> { } // assets - let asset_dir = scratch_dir.join("assets"); + let asset_dir = tmp_dir.join("assets"); tokio::fs::create_dir_all(&asset_dir).await?; tokio_tar::Archive::new(reader.assets().await?) .unpack(&asset_dir) @@ -212,21 +146,21 @@ impl S9pk> { Path::new("assets") .join(&asset_id) .with_extension("squashfs"), - Entry::file(PackSource::File(sqfs_path)), + Entry::file(TmpSource::new(tmp_dir.clone(), PackSource::File(sqfs_path))), )?; } // javascript - let js_dir = scratch_dir.join("javascript"); + let js_dir = tmp_dir.join("javascript"); let sqfs_path = js_dir.with_extension("squashfs"); tokio::fs::create_dir_all(&js_dir).await?; if let Some(mut scripts) = reader.scripts().await? { - let mut js_file = File::create(js_dir.join("embassy.js")).await?; + let mut js_file = create_file(js_dir.join("embassy.js")).await?; tokio::io::copy(&mut scripts, &mut js_file).await?; js_file.sync_all().await?; } { - let mut js_file = File::create(js_dir.join("embassyManifest.json")).await?; + let mut js_file = create_file(js_dir.join("embassyManifest.json")).await?; js_file .write_all(&serde_json::to_vec(&manifest_raw).with_kind(ErrorKind::Serialization)?) .await?; @@ -239,30 +173,24 @@ impl S9pk> { .await?; archive.insert_path( Path::new("javascript.squashfs"), - Entry::file(PackSource::File(sqfs_path)), + Entry::file(TmpSource::new(tmp_dir.clone(), PackSource::File(sqfs_path))), )?; archive.insert_path( "manifest.json", - Entry::file(PackSource::Buffered( - serde_json::to_vec::(&new_manifest) - .with_kind(ErrorKind::Serialization)? - .into(), + Entry::file(TmpSource::new( + tmp_dir.clone(), + PackSource::Buffered( + serde_json::to_vec::(&new_manifest) + .with_kind(ErrorKind::Serialization)? + .into(), + ), )), )?; - let mut s9pk = S9pk::new(MerkleArchive::new(archive, signer, SIG_CONTEXT), None).await?; - let mut dest_file = File::create(destination.as_ref()).await?; - s9pk.serialize(&mut dest_file, false).await?; - dest_file.sync_all().await?; - - scratch_dir.delete().await?; - - Ok(S9pk::deserialize( - &MultiCursorFile::from(File::open(destination.as_ref()).await?), - None, - ) - .await?) + let mut res = S9pk::new(MerkleArchive::new(archive, signer, SIG_CONTEXT), None).await?; + res.as_archive_mut().update_hashes(true).await?; + Ok(res) } } @@ -272,7 +200,7 @@ impl From for Manifest { Self { id: value.id, title: value.title, - version: value.version, + version: ExtendedVersion::from(value.version).into(), release_notes: value.release_notes, license: value.license.into(), wrapper_repo: value.wrapper_repo, diff --git a/core/startos/src/s9pk/v2/manifest.rs b/core/startos/src/s9pk/v2/manifest.rs index 9ae8524fa..9607bb654 100644 --- a/core/startos/src/s9pk/v2/manifest.rs +++ b/core/startos/src/s9pk/v2/manifest.rs @@ -2,6 +2,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::path::Path; use color_eyre::eyre::eyre; +use exver::Version; use helpers::const_true; use imbl_value::InternedString; pub use models::PackageId; @@ -20,8 +21,8 @@ use crate::util::serde::Regex; use crate::util::VersionString; use crate::version::{Current, VersionT}; -fn current_version() -> VersionString { - Current::new().semver().into() +fn current_version() -> Version { + Current::new().semver() } #[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS)] @@ -59,7 +60,8 @@ pub struct Manifest { #[ts(type = "string | null")] pub git_hash: Option, #[serde(default = "current_version")] - pub os_version: VersionString, + #[ts(type = "string")] + pub os_version: Version, #[serde(default = "const_true")] pub has_config: bool, } diff --git a/core/startos/src/s9pk/v2/mod.rs b/core/startos/src/s9pk/v2/mod.rs index a1183efa8..2477e63a0 100644 --- a/core/startos/src/s9pk/v2/mod.rs +++ b/core/startos/src/s9pk/v2/mod.rs @@ -12,10 +12,12 @@ use crate::s9pk::manifest::Manifest; use crate::s9pk::merkle_archive::file_contents::FileContents; use crate::s9pk::merkle_archive::sink::Sink; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; -use crate::s9pk::merkle_archive::source::{ArchiveSource, DynFileSource, FileSource, Section}; +use crate::s9pk::merkle_archive::source::{ + ArchiveSource, DynFileSource, FileSource, Section, TmpSource, +}; use crate::s9pk::merkle_archive::{Entry, MerkleArchive}; use crate::s9pk::v2::pack::{ImageSource, PackSource}; -use crate::util::io::TmpDir; +use crate::util::io::{open_file, TmpDir}; const MAGIC_AND_VERSION: &[u8] = &[0x3b, 0x3b, 0x02]; @@ -165,8 +167,8 @@ impl S9pk { } } -impl + FileSource + Clone> S9pk { - pub async fn load_images(&mut self, tmpdir: &TmpDir) -> Result<(), Error> { +impl> + FileSource + Clone> S9pk { + pub async fn load_images(&mut self, tmp_dir: Arc) -> Result<(), Error> { let id = &self.manifest.id; let version = &self.manifest.version; for (image_id, image_config) in &mut self.manifest.images { @@ -175,7 +177,7 @@ impl + FileSource + Clone> S9pk { image_config .source .load( - tmpdir, + tmp_dir.clone(), id, version, image_id, @@ -206,7 +208,7 @@ impl S9pk> { ) .await?; - let mut magic_version = [0u8; 3]; + let mut magic_version = [0u8; MAGIC_AND_VERSION.len()]; header.read_exact(&mut magic_version).await?; ensure_code!( &magic_version == MAGIC_AND_VERSION, @@ -232,7 +234,7 @@ impl S9pk { Self::deserialize(&MultiCursorFile::from(file), None).await } pub async fn open(path: impl AsRef, id: Option<&PackageId>) -> Result { - let res = Self::from_file(tokio::fs::File::open(path).await?).await?; + let res = Self::from_file(open_file(path).await?).await?; if let Some(id) = id { ensure_code!( &res.as_manifest().id == id, diff --git a/core/startos/src/s9pk/v2/pack.rs b/core/startos/src/s9pk/v2/pack.rs index 18fe0472b..fb271e785 100644 --- a/core/startos/src/s9pk/v2/pack.rs +++ b/core/startos/src/s9pk/v2/pack.rs @@ -10,7 +10,6 @@ use futures::{FutureExt, TryStreamExt}; use imbl_value::InternedString; use models::{ImageId, PackageId, VersionString}; use serde::{Deserialize, Serialize}; -use tokio::fs::File; use tokio::io::AsyncRead; use tokio::process::Command; use tokio::sync::OnceCell; @@ -23,12 +22,12 @@ use crate::rpc_continuations::Guid; use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::{ - into_dyn_read, ArchiveSource, DynFileSource, FileSource, + into_dyn_read, ArchiveSource, DynFileSource, FileSource, TmpSource, }; use crate::s9pk::merkle_archive::{Entry, MerkleArchive}; use crate::s9pk::v2::SIG_CONTEXT; use crate::s9pk::S9pk; -use crate::util::io::TmpDir; +use crate::util::io::{create_file, open_file, TmpDir}; use crate::util::Invoke; #[cfg(not(feature = "docker"))] @@ -64,7 +63,7 @@ impl SqfsDir { .invoke(ErrorKind::Filesystem) .await?; Ok(MultiCursorFile::from( - File::open(&path) + open_file(&path) .await .with_ctx(|_| (ErrorKind::Filesystem, path.display()))?, )) @@ -100,11 +99,7 @@ impl FileSource for PackSource { async fn reader(&self) -> Result { match self { Self::Buffered(a) => Ok(into_dyn_read(Cursor::new(a.clone()))), - Self::File(f) => Ok(into_dyn_read( - File::open(f) - .await - .with_ctx(|_| (ErrorKind::Filesystem, f.display()))?, - )), + Self::File(f) => Ok(into_dyn_read(open_file(f).await?)), Self::Squashfs(dir) => dir.file().await?.fetch_all().await.map(into_dyn_read), } } @@ -284,9 +279,9 @@ pub enum ImageSource { } impl ImageSource { #[instrument(skip_all)] - pub fn load<'a, S: From + FileSource + Clone>( + pub fn load<'a, S: From> + FileSource + Clone>( &'a self, - tmpdir: &'a TmpDir, + tmp_dir: Arc, id: &'a PackageId, version: &'a VersionString, image_id: &'a ImageId, @@ -331,12 +326,13 @@ impl ImageSource { .arg(&tag) .arg(&docker_platform) .arg("-o") - .arg("type=image") + .arg("type=docker,dest=-") .capture(false) + .pipe(Command::new(CONTAINER_TOOL).arg("load")) .invoke(ErrorKind::Docker) .await?; ImageSource::DockerTag(tag.clone()) - .load(tmpdir, id, version, image_id, arch, into) + .load(tmp_dir, id, version, image_id, arch, into) .await?; Command::new(CONTAINER_TOOL) .arg("rmi") @@ -390,21 +386,24 @@ impl ImageSource { into.insert_path( base_path.with_extension("json"), Entry::file( - PackSource::Buffered( - serde_json::to_vec(&ImageMetadata { - workdir: if config.working_dir == Path::new("") { - "/".into() - } else { - config.working_dir - }, - user: if config.user.is_empty() { - "root".into() - } else { - config.user.into() - }, - }) - .with_kind(ErrorKind::Serialization)? - .into(), + TmpSource::new( + tmp_dir.clone(), + PackSource::Buffered( + serde_json::to_vec(&ImageMetadata { + workdir: if config.working_dir == Path::new("") { + "/".into() + } else { + config.working_dir + }, + user: if config.user.is_empty() { + "root".into() + } else { + config.user.into() + }, + }) + .with_kind(ErrorKind::Serialization)? + .into(), + ), ) .into(), ), @@ -412,10 +411,16 @@ impl ImageSource { into.insert_path( base_path.with_extension("env"), Entry::file( - PackSource::Buffered(config.env.join("\n").into_bytes().into()).into(), + TmpSource::new( + tmp_dir.clone(), + PackSource::Buffered(config.env.join("\n").into_bytes().into()), + ) + .into(), ), )?; - let dest = tmpdir.join(Guid::new().as_ref()).with_extension("squashfs"); + let dest = tmp_dir + .join(Guid::new().as_ref()) + .with_extension("squashfs"); let container = String::from_utf8( Command::new(CONTAINER_TOOL) .arg("create") @@ -438,7 +443,7 @@ impl ImageSource { .await?; into.insert_path( base_path.with_extension("squashfs"), - Entry::file(PackSource::File(dest).into()), + Entry::file(TmpSource::new(tmp_dir.clone(), PackSource::File(dest)).into()), )?; Ok(()) @@ -460,8 +465,8 @@ pub struct ImageMetadata { #[instrument(skip_all)] pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> { - let tmpdir = Arc::new(TmpDir::new().await?); - let mut files = DirectoryContents::::new(); + let tmp_dir = Arc::new(TmpDir::new().await?); + let mut files = DirectoryContents::>::new(); let js_dir = params.javascript(); let manifest: Arc<[u8]> = Command::new("node") .arg("-e") @@ -474,7 +479,10 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> { .into(); files.insert( "manifest.json".into(), - Entry::file(PackSource::Buffered(manifest.clone())), + Entry::file(TmpSource::new( + tmp_dir.clone(), + PackSource::Buffered(manifest.clone()), + )), ); let icon = params.icon().await?; let icon_ext = icon @@ -483,22 +491,28 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> { .to_string_lossy(); files.insert( InternedString::from_display(&lazy_format!("icon.{}", icon_ext)), - Entry::file(PackSource::File(icon)), + Entry::file(TmpSource::new(tmp_dir.clone(), PackSource::File(icon))), ); files.insert( "LICENSE.md".into(), - Entry::file(PackSource::File(params.license())), + Entry::file(TmpSource::new( + tmp_dir.clone(), + PackSource::File(params.license()), + )), ); files.insert( "instructions.md".into(), - Entry::file(PackSource::File(params.instructions())), + Entry::file(TmpSource::new( + tmp_dir.clone(), + PackSource::File(params.instructions()), + )), ); files.insert( "javascript.squashfs".into(), - Entry::file(PackSource::Squashfs(Arc::new(SqfsDir::new( - js_dir, - tmpdir.clone(), - )))), + Entry::file(TmpSource::new( + tmp_dir.clone(), + PackSource::Squashfs(Arc::new(SqfsDir::new(js_dir, tmp_dir.clone()))), + )), ); let mut s9pk = S9pk::new( @@ -511,26 +525,29 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> { for assets in s9pk.as_manifest().assets.clone() { s9pk.as_archive_mut().contents_mut().insert_path( Path::new("assets").join(&assets).with_extension("squashfs"), - Entry::file(PackSource::Squashfs(Arc::new(SqfsDir::new( - assets_dir.join(&assets), - tmpdir.clone(), - )))), + Entry::file(TmpSource::new( + tmp_dir.clone(), + PackSource::Squashfs(Arc::new(SqfsDir::new( + assets_dir.join(&assets), + tmp_dir.clone(), + ))), + )), )?; } - s9pk.load_images(&*tmpdir).await?; + s9pk.load_images(tmp_dir.clone()).await?; s9pk.validate_and_filter(None)?; s9pk.serialize( - &mut File::create(params.output(&s9pk.as_manifest().id)).await?, + &mut create_file(params.output(&s9pk.as_manifest().id)).await?, false, ) .await?; drop(s9pk); - tmpdir.gc().await?; + tmp_dir.gc().await?; Ok(()) } diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index 92564b8e4..b85bd8d6d 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -11,7 +11,6 @@ use persistent_container::PersistentContainer; use rpc_toolkit::{from_fn_async, CallRemoteHandler, Empty, HandlerArgs, HandlerFor}; use serde::{Deserialize, Serialize}; use start_stop::StartStop; -use tokio::fs::File; use tokio::sync::Notify; use ts_rs::TS; @@ -33,6 +32,7 @@ use crate::status::MainStatus; use crate::util::actor::background::BackgroundJobQueue; use crate::util::actor::concurrent::ConcurrentActor; use crate::util::actor::Actor; +use crate::util::io::create_file; use crate::util::serde::Pem; use crate::volume::data_dir; @@ -403,7 +403,7 @@ impl Service { #[instrument(skip_all)] pub async fn backup(&self, guard: impl GenericMountGuard) -> Result<(), Error> { let id = &self.seed.id; - let mut file = File::create(guard.path().join(id).with_extension("s9pk")).await?; + let mut file = create_file(guard.path().join(id).with_extension("s9pk")).await?; self.seed .persistent_container .s9pk diff --git a/core/startos/src/service/persistent_container.rs b/core/startos/src/service/persistent_container.rs index d9d08e5d3..e0b31ea97 100644 --- a/core/startos/src/service/persistent_container.rs +++ b/core/startos/src/service/persistent_container.rs @@ -9,14 +9,12 @@ use helpers::NonDetachingJoinHandle; use models::{ImageId, ProcedureName, VolumeId}; use rpc_toolkit::{Empty, Server, ShutdownHandle}; use serde::de::DeserializeOwned; -use tokio::fs::File; use tokio::process::Command; use tokio::sync::{oneshot, watch, Mutex, OnceCell}; use tracing::instrument; use super::service_effect_handler::{service_effect_handler, EffectContext}; use super::transition::{TransitionKind, TransitionState}; -use super::ServiceActorSeed; use crate::context::RpcContext; use crate::disk::mount::filesystem::bind::Bind; use crate::disk::mount::filesystem::idmapped::IdMapped; @@ -32,6 +30,7 @@ use crate::s9pk::merkle_archive::source::FileSource; use crate::s9pk::S9pk; use crate::service::start_stop::StartStop; use crate::service::{rpc, RunningStatus, Service}; +use crate::util::io::create_file; use crate::util::rpc_client::UnixRpcClient; use crate::util::Invoke; use crate::volume::{asset_dir, data_dir}; @@ -237,7 +236,7 @@ impl PersistentContainer { .get_path(Path::new("images").join(arch).join(&env_filename)) .and_then(|e| e.as_file()) { - env.copy(&mut File::create(image_path.join(&env_filename)).await?) + env.copy(&mut create_file(image_path.join(&env_filename)).await?) .await?; } let json_filename = Path::new(image.as_ref()).with_extension("json"); @@ -247,7 +246,7 @@ impl PersistentContainer { .get_path(Path::new("images").join(arch).join(&json_filename)) .and_then(|e| e.as_file()) { - json.copy(&mut File::create(image_path.join(&json_filename)).await?) + json.copy(&mut create_file(image_path.join(&json_filename)).await?) .await?; } } diff --git a/core/startos/src/service/service_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs index 63c313ead..7547584e0 100644 --- a/core/startos/src/service/service_effect_handler.rs +++ b/core/startos/src/service/service_effect_handler.rs @@ -7,8 +7,8 @@ use std::str::FromStr; use std::sync::{Arc, Weak}; use clap::builder::ValueParserFactory; -use clap::{CommandFactory, FromArgMatches, Parser}; -use emver::VersionRange; +use clap::Parser; +use exver::VersionRange; use imbl_value::json; use itertools::Itertools; use models::{ @@ -1383,7 +1383,7 @@ struct CheckDependenciesResult { is_running: bool, health_checks: Vec, #[ts(type = "string | null")] - version: Option, + version: Option, } async fn check_dependencies( diff --git a/core/startos/src/service/service_map.rs b/core/startos/src/service/service_map.rs index f8874d21a..02244169c 100644 --- a/core/startos/src/service/service_map.rs +++ b/core/startos/src/service/service_map.rs @@ -94,12 +94,19 @@ impl ServiceMap { } #[instrument(skip_all)] - pub async fn install( + pub async fn install( &self, ctx: RpcContext, - mut s9pk: S9pk, + s9pk: F, recovery_source: Option, - ) -> Result { + progress: Option, + ) -> Result + where + F: FnOnce() -> Fut, + Fut: Future, Error>>, + S: FileSource + Clone, + { + let mut s9pk = s9pk().await?; s9pk.validate_and_filter(ctx.s9pk_arch)?; let manifest = s9pk.as_manifest().clone(); let id = manifest.id.clone(); @@ -118,7 +125,7 @@ impl ServiceMap { }; let size = s9pk.size(); - let progress = FullProgressTracker::new(); + let progress = progress.unwrap_or_else(|| FullProgressTracker::new()); let download_progress_contribution = size.unwrap_or(60); let mut download_progress = progress.add_phase( InternedString::intern("Download"), diff --git a/core/startos/src/setup.rs b/core/startos/src/setup.rs index 2b701c01f..8fb7cf7c1 100644 --- a/core/startos/src/setup.rs +++ b/core/startos/src/setup.rs @@ -8,7 +8,6 @@ use patch_db::json_ptr::ROOT; use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; -use tokio::fs::File; use tokio::io::AsyncWriteExt; use tokio::try_join; use tracing::instrument; @@ -35,7 +34,7 @@ use crate::prelude::*; use crate::progress::{FullProgress, PhaseProgressTrackerHandle}; use crate::rpc_continuations::Guid; use crate::util::crypto::EncryptedWire; -use crate::util::io::{dir_copy, dir_size, Counter}; +use crate::util::io::{create_file, dir_copy, dir_size, Counter}; use crate::{Error, ErrorKind, ResultExt}; pub fn setup() -> ParentHandler { @@ -324,7 +323,7 @@ pub async fn execute( pub async fn complete(ctx: SetupContext) -> Result { match ctx.result.get() { Some(Ok((res, ctx))) => { - let mut guid_file = File::create("/media/startos/config/disk.guid").await?; + let mut guid_file = create_file("/media/startos/config/disk.guid").await?; guid_file.write_all(ctx.disk_guid.as_bytes()).await?; guid_file.sync_all().await?; Ok(res.clone()) diff --git a/core/startos/src/ssh.rs b/core/startos/src/ssh.rs index 82d06be4c..b97fba3e4 100644 --- a/core/startos/src/ssh.rs +++ b/core/startos/src/ssh.rs @@ -13,6 +13,7 @@ use ts_rs::TS; use crate::context::{CliContext, RpcContext}; use crate::prelude::*; use crate::util::clap::FromStrParser; +use crate::util::io::create_file; use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat}; pub const SSH_AUTHORIZED_KEYS_FILE: &str = "/home/start9/.ssh/authorized_keys"; @@ -229,7 +230,7 @@ pub async fn sync_keys>(keys: &SshKeys, dest: P) -> Result<(), Er if tokio::fs::metadata(ssh_dir).await.is_err() { tokio::fs::create_dir_all(ssh_dir).await?; } - let mut f = tokio::fs::File::create(dest).await?; + let mut f = create_file(dest).await?; for key in keys.0.values() { f.write_all(key.0.to_key_format().as_bytes()).await?; f.write_all(b"\n").await?; diff --git a/core/startos/src/system.rs b/core/startos/src/system.rs index 50b17b54a..7a8cb9afe 100644 --- a/core/startos/src/system.rs +++ b/core/startos/src/system.rs @@ -20,6 +20,7 @@ use crate::prelude::*; use crate::rpc_continuations::RpcContinuations; use crate::shutdown::Shutdown; use crate::util::cpupower::{get_available_governors, set_governor, Governor}; +use crate::util::io::open_file; use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat}; use crate::util::Invoke; @@ -657,7 +658,7 @@ impl ProcStat { async fn get_proc_stat() -> Result { use tokio::io::AsyncBufReadExt; let mut cpu_line = String::new(); - let _n = tokio::io::BufReader::new(tokio::fs::File::open("/proc/stat").await?) + let _n = tokio::io::BufReader::new(open_file("/proc/stat").await?) .read_line(&mut cpu_line) .await?; let stats: Vec = cpu_line diff --git a/core/startos/src/update/mod.rs b/core/startos/src/update/mod.rs index dc164d2cd..79d852754 100644 --- a/core/startos/src/update/mod.rs +++ b/core/startos/src/update/mod.rs @@ -4,8 +4,8 @@ use std::time::Duration; use clap::{ArgAction, Parser}; use color_eyre::eyre::{eyre, Result}; -use emver::{Version, VersionRange}; -use futures::{FutureExt, TryStreamExt}; +use exver::{Version, VersionRange}; +use futures::TryStreamExt; use helpers::{AtomicFile, NonDetachingJoinHandle}; use imbl_value::json; use itertools::Itertools; diff --git a/core/startos/src/upload.rs b/core/startos/src/upload.rs index b922ab9d2..4735a63fb 100644 --- a/core/startos/src/upload.rs +++ b/core/startos/src/upload.rs @@ -1,3 +1,4 @@ +use std::io::SeekFrom; use std::pin::Pin; use std::sync::Arc; use std::task::Poll; @@ -5,20 +6,20 @@ use std::time::Duration; use axum::body::Body; use axum::response::Response; -use futures::StreamExt; +use futures::{ready, FutureExt, StreamExt}; use http::header::CONTENT_LENGTH; use http::StatusCode; use imbl_value::InternedString; use tokio::fs::File; -use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; +use tokio::io::{AsyncRead, AsyncSeek, AsyncSeekExt, AsyncWrite, AsyncWriteExt}; use tokio::sync::watch; use crate::context::RpcContext; use crate::prelude::*; use crate::rpc_continuations::{Guid, RpcContinuation}; -use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; +use crate::s9pk::merkle_archive::source::multi_cursor_file::{FileCursor, MultiCursorFile}; use crate::s9pk::merkle_archive::source::ArchiveSource; -use crate::util::io::TmpDir; +use crate::util::io::{create_file, TmpDir}; pub async fn upload( ctx: &RpcContext, @@ -215,14 +216,15 @@ impl UploadingFile { pub async fn new() -> Result<(UploadHandle, Self), Error> { let progress = watch::channel(Progress::default()); let tmp_dir = Arc::new(TmpDir::new().await?); - let file = File::create(tmp_dir.join("upload.tmp")).await?; + let file = create_file(tmp_dir.join("upload.tmp")).await?; let uploading = Self { - tmp_dir, + tmp_dir: tmp_dir.clone(), file: MultiCursorFile::open(&file).await?, progress: progress.1, }; Ok(( UploadHandle { + tmp_dir, file, progress: progress.0, }, @@ -237,22 +239,127 @@ impl UploadingFile { } } impl ArchiveSource for UploadingFile { - type Reader = ::Reader; + type FetchReader = ::FetchReader; + type FetchAllReader = UploadingFileReader; async fn size(&self) -> Option { Progress::expected_size(&mut self.progress.clone()).await } - async fn fetch_all(&self) -> Result { - Progress::ready(&mut self.progress.clone()).await?; - self.file.fetch_all().await + async fn fetch_all(&self) -> Result { + let mut file = self.file.cursor().await?; + file.seek(SeekFrom::Start(0)).await?; + Ok(UploadingFileReader { + tmp_dir: self.tmp_dir.clone(), + file, + position: 0, + to_seek: None, + progress: self.progress.clone(), + }) } - async fn fetch(&self, position: u64, size: u64) -> Result { + async fn fetch(&self, position: u64, size: u64) -> Result { Progress::ready_for(&mut self.progress.clone(), position + size).await?; self.file.fetch(position, size).await } } +#[pin_project::pin_project(project = UploadingFileReaderProjection)] +pub struct UploadingFileReader { + tmp_dir: Arc, + position: u64, + to_seek: Option, + #[pin] + file: FileCursor, + progress: watch::Receiver, +} +impl<'a> UploadingFileReaderProjection<'a> { + fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> Result { + let ready = Progress::ready(&mut *self.progress); + tokio::pin!(ready); + Ok(ready + .poll_unpin(cx) + .map_err(|e| std::io::Error::other(e.source))? + .is_ready()) + } + fn poll_ready_for( + &mut self, + cx: &mut std::task::Context<'_>, + size: u64, + ) -> Result { + let ready = Progress::ready_for(&mut *self.progress, size); + tokio::pin!(ready); + Ok(ready + .poll_unpin(cx) + .map_err(|e| std::io::Error::other(e.source))? + .is_ready()) + } +} +impl AsyncRead for UploadingFileReader { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + let mut this = self.project(); + + let position = *this.position; + if this.poll_ready(cx)? || this.poll_ready_for(cx, position + buf.remaining() as u64)? { + let start = buf.filled().len(); + let res = this.file.poll_read(cx, buf); + *this.position += (buf.filled().len() - start) as u64; + res + } else { + Poll::Pending + } + } +} +impl AsyncSeek for UploadingFileReader { + fn start_seek(self: Pin<&mut Self>, position: SeekFrom) -> std::io::Result<()> { + let this = self.project(); + *this.to_seek = Some(position); + Ok(()) + } + fn poll_complete( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + let mut this = self.project(); + if let Some(to_seek) = *this.to_seek { + let size = match to_seek { + SeekFrom::Current(n) => (*this.position as i64 + n) as u64, + SeekFrom::Start(n) => n, + SeekFrom::End(n) => { + let expected_size = this.progress.borrow().expected_size; + match expected_size { + Some(end) => (end as i64 + n) as u64, + None => { + if !this.poll_ready(cx)? { + return Poll::Pending; + } + (this.progress.borrow().expected_size.ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::Other, + eyre!("upload maked complete without expected size"), + ) + })? as i64 + + n) as u64 + } + } + } + }; + if !this.poll_ready_for(cx, size)? { + return Poll::Pending; + } + } + if let Some(seek) = this.to_seek.take() { + this.file.as_mut().start_seek(seek)?; + } + *this.position = ready!(this.file.as_mut().poll_complete(cx)?); + Poll::Ready(Ok(*this.position)) + } +} + #[pin_project::pin_project(PinnedDrop)] pub struct UploadHandle { + tmp_dir: Arc, #[pin] file: File, progress: watch::Sender, diff --git a/core/startos/src/util/io.rs b/core/startos/src/util/io.rs index b5748d1d9..1ec789f23 100644 --- a/core/startos/src/util/io.rs +++ b/core/startos/src/util/io.rs @@ -610,13 +610,13 @@ pub fn dir_copy<'a, P0: AsRef + 'a + Send + Sync, P1: AsRef + 'a + S let src_path = e.path(); let dst_path = dst_path.join(e.file_name()); if m.is_file() { - let mut dst_file = tokio::fs::File::create(&dst_path).await.with_ctx(|_| { + let mut dst_file = create_file(&dst_path).await.with_ctx(|_| { ( crate::ErrorKind::Filesystem, format!("create {}", dst_path.display()), ) })?; - let mut rdr = tokio::fs::File::open(&src_path).await.with_ctx(|_| { + let mut rdr = open_file(&src_path).await.with_ctx(|_| { ( crate::ErrorKind::Filesystem, format!("open {}", src_path.display()), @@ -829,6 +829,13 @@ impl Drop for TmpDir { } } +pub async fn open_file(path: impl AsRef) -> Result { + let path = path.as_ref(); + File::open(path) + .await + .with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("open {path:?}"))) +} + pub async fn create_file(path: impl AsRef) -> Result { let path = path.as_ref(); if let Some(parent) = path.parent() { diff --git a/core/startos/src/util/mod.rs b/core/startos/src/util/mod.rs index f2334632e..7200eaaba 100644 --- a/core/startos/src/util/mod.rs +++ b/core/startos/src/util/mod.rs @@ -26,6 +26,7 @@ use tokio::sync::{oneshot, Mutex, OwnedMutexGuard, RwLock}; use tracing::instrument; use crate::shutdown::Shutdown; +use crate::util::io::create_file; use crate::{Error, ErrorKind, ResultExt as _}; pub mod actor; pub mod clap; @@ -385,16 +386,16 @@ impl SOption for SNone {} #[async_trait] pub trait AsyncFileExt: Sized { - async fn maybe_open + Send + Sync>(path: P) -> std::io::Result>; + async fn maybe_open + Send + Sync>(path: P) -> Result, Error>; async fn delete + Send + Sync>(path: P) -> std::io::Result<()>; } #[async_trait] impl AsyncFileExt for File { - async fn maybe_open + Send + Sync>(path: P) -> std::io::Result> { - match File::open(path).await { + async fn maybe_open + Send + Sync>(path: P) -> Result, Error> { + match File::open(path.as_ref()).await { Ok(f) => Ok(Some(f)), Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), - Err(e) => Err(e), + Err(e) => Err(e).with_ctx(|_| (ErrorKind::Filesystem, path.as_ref().display())), } } async fn delete + Send + Sync>(path: P) -> std::io::Result<()> { @@ -590,9 +591,7 @@ impl FileLock { .await .with_ctx(|_| (crate::ErrorKind::Filesystem, parent.display().to_string()))?; } - let f = File::create(&path) - .await - .with_ctx(|_| (crate::ErrorKind::Filesystem, path.display().to_string()))?; + let f = create_file(&path).await?; let file_guard = tokio::task::spawn_blocking(move || { fd_lock_rs::FdLock::lock(f, fd_lock_rs::LockType::Exclusive, blocking) }) diff --git a/core/startos/src/util/rpc.rs b/core/startos/src/util/rpc.rs index e80aac6cb..54664833b 100644 --- a/core/startos/src/util/rpc.rs +++ b/core/startos/src/util/rpc.rs @@ -3,7 +3,6 @@ use std::path::Path; use clap::Parser; use rpc_toolkit::{from_fn_async, Context, ParentHandler}; use serde::{Deserialize, Serialize}; -use tokio::fs::File; use url::Url; use crate::context::CliContext; @@ -11,7 +10,7 @@ use crate::prelude::*; use crate::s9pk::merkle_archive::source::http::HttpSource; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::ArchiveSource; -use crate::util::io::ParallelBlake3Writer; +use crate::util::io::{open_file, ParallelBlake3Writer}; use crate::util::serde::Base16; use crate::util::Apply; use crate::CAP_10_MiB; @@ -40,7 +39,7 @@ pub async fn b3sum( path: impl AsRef, allow_mmap: bool, ) -> Result, Error> { - let file = MultiCursorFile::from(File::open(path).await?); + let file = MultiCursorFile::from(open_file(path).await?); if allow_mmap { return file.blake3_mmap().await.map(|h| *h.as_bytes()).map(Base16); } diff --git a/core/startos/src/util/serde.rs b/core/startos/src/util/serde.rs index 5d1289e7e..b3b2dea6a 100644 --- a/core/startos/src/util/serde.rs +++ b/core/startos/src/util/serde.rs @@ -1,4 +1,3 @@ -use std::any::TypeId; use std::collections::VecDeque; use std::marker::PhantomData; use std::ops::Deref; @@ -9,10 +8,9 @@ use clap::{ArgMatches, CommandFactory, FromArgMatches}; use color_eyre::eyre::eyre; use imbl::OrdMap; use openssl::pkey::{PKey, Private}; -use openssl::x509::{X509Ref, X509}; +use openssl::x509::X509; use rpc_toolkit::{ - CliBindings, Context, Handler, HandlerArgs, HandlerArgsFor, HandlerFor, HandlerTypes, - PrintCliResult, + CliBindings, Context, HandlerArgs, HandlerArgsFor, HandlerFor, HandlerTypes, PrintCliResult, }; use serde::de::DeserializeOwned; use serde::ser::{SerializeMap, SerializeSeq}; @@ -1188,7 +1186,7 @@ pub trait PemEncoding: Sized { impl PemEncoding for X509 { fn from_pem(pem: &str) -> Result { - X509::from_pem(pem.as_bytes()).map_err(E::custom) + Self::from_pem(pem.as_bytes()).map_err(E::custom) } fn to_pem(&self) -> Result { String::from_utf8((&**self).to_pem().map_err(E::custom)?).map_err(E::custom) @@ -1197,7 +1195,7 @@ impl PemEncoding for X509 { impl PemEncoding for PKey { fn from_pem(pem: &str) -> Result { - PKey::::private_key_from_pem(pem.as_bytes()).map_err(E::custom) + Self::private_key_from_pem(pem.as_bytes()).map_err(E::custom) } fn to_pem(&self) -> Result { String::from_utf8((&**self).private_key_to_pem_pkcs8().map_err(E::custom)?) @@ -1207,7 +1205,7 @@ impl PemEncoding for PKey { impl PemEncoding for ssh_key::PrivateKey { fn from_pem(pem: &str) -> Result { - ssh_key::PrivateKey::from_openssh(pem.as_bytes()).map_err(E::custom) + Self::from_openssh(pem.as_bytes()).map_err(E::custom) } fn to_pem(&self) -> Result { self.to_openssh(ssh_key::LineEnding::LF) @@ -1219,7 +1217,7 @@ impl PemEncoding for ssh_key::PrivateKey { impl PemEncoding for ed25519_dalek::VerifyingKey { fn from_pem(pem: &str) -> Result { use ed25519_dalek::pkcs8::DecodePublicKey; - ed25519_dalek::VerifyingKey::from_public_key_pem(pem).map_err(E::custom) + Self::from_public_key_pem(pem).map_err(E::custom) } fn to_pem(&self) -> Result { use ed25519_dalek::pkcs8::EncodePublicKey; @@ -1228,6 +1226,19 @@ impl PemEncoding for ed25519_dalek::VerifyingKey { } } +impl PemEncoding for ed25519_dalek::SigningKey { + fn from_pem(pem: &str) -> Result { + use ed25519_dalek::pkcs8::DecodePrivateKey; + Self::from_pkcs8_pem(pem).map_err(E::custom) + } + fn to_pem(&self) -> Result { + use ed25519_dalek::pkcs8::EncodePrivateKey; + self.to_pkcs8_pem(pkcs8::LineEnding::LF) + .map_err(E::custom) + .map(|s| s.as_str().to_owned()) + } +} + pub mod pem { use serde::{Deserialize, Deserializer, Serializer}; diff --git a/core/startos/src/version/mod.rs b/core/startos/src/version/mod.rs index d063558e2..003a44326 100644 --- a/core/startos/src/version/mod.rs +++ b/core/startos/src/version/mod.rs @@ -25,20 +25,20 @@ enum Version { V0_3_5_1(Wrapper), V0_3_5_2(Wrapper), V0_3_6(Wrapper), - Other(emver::Version), + Other(exver::Version), } impl Version { - fn from_util_version(version: crate::util::VersionString) -> Self { + fn from_exver_version(version: exver::Version) -> Self { serde_json::to_value(version.clone()) .and_then(serde_json::from_value) .unwrap_or_else(|_e| { tracing::warn!("Can't deserialize: {:?} and falling back to other", version); - Version::Other(version.into_version()) + Version::Other(version) }) } #[cfg(test)] - fn as_sem_ver(&self) -> emver::Version { + fn as_exver(&self) -> exver::Version { match self { Version::LT0_3_5(LTWrapper(_, x)) => x.clone(), Version::V0_3_5(Wrapper(x)) => x.semver(), @@ -56,8 +56,8 @@ where { type Previous: VersionT; fn new() -> Self; - fn semver(&self) -> emver::Version; - fn compat(&self) -> &'static emver::VersionRange; + fn semver(&self) -> exver::Version; + fn compat(&self) -> &'static exver::VersionRange; fn up(&self, db: &TypedPatchDb) -> impl Future> + Send; fn down(&self, db: &TypedPatchDb) -> impl Future> + Send; fn commit( @@ -158,7 +158,7 @@ where } #[derive(Debug, Clone)] -struct LTWrapper(T, emver::Version); +struct LTWrapper(T, exver::Version); impl serde::Serialize for LTWrapper where T: VersionT, @@ -172,10 +172,10 @@ where T: VersionT, { fn deserialize>(deserializer: D) -> Result { - let v = crate::util::VersionString::deserialize(deserializer)?; + let v = exver::Version::deserialize(deserializer)?; let version = T::new(); - if *v < version.semver() { - Ok(Self(version, v.into_version())) + if v < version.semver() { + Ok(Self(version, v)) } else { Err(serde::de::Error::custom("Mismatched Version")) } @@ -197,9 +197,9 @@ where T: VersionT, { fn deserialize>(deserializer: D) -> Result { - let v = crate::util::VersionString::deserialize(deserializer)?; + let v = exver::Version::deserialize(deserializer)?; let version = T::new(); - if *v == version.semver() { + if v == version.semver() { Ok(Wrapper(version)) } else { Err(serde::de::Error::custom("Mismatched Version")) @@ -212,7 +212,7 @@ pub async fn init( mut progress: PhaseProgressTrackerHandle, ) -> Result<(), Error> { progress.start(); - let version = Version::from_util_version( + let version = Version::from_exver_version( db.peek() .await .as_public() @@ -256,9 +256,18 @@ mod tests { use super::*; - fn em_version() -> impl Strategy { - any::<(usize, usize, usize, usize)>().prop_map(|(major, minor, patch, super_minor)| { - emver::Version::new(major, minor, patch, super_minor) + fn em_version() -> impl Strategy { + any::<(usize, usize, usize, bool)>().prop_map(|(major, minor, patch, alpha)| { + if alpha { + exver::Version::new( + [0, major, minor] + .into_iter() + .chain(Some(patch).filter(|n| *n != 0)), + [], + ) + } else { + exver::Version::new([major, minor, patch], []) + } }) } @@ -273,15 +282,15 @@ mod tests { proptest! { #[test] - fn emversion_isomorphic_version(original in em_version()) { - let version = Version::from_util_version(original.clone().into()); - let back = version.as_sem_ver(); + fn exversion_isomorphic_version(original in em_version()) { + let version = Version::from_exver_version(original.clone().into()); + let back = version.as_exver(); prop_assert_eq!(original, back, "All versions should round trip"); } #[test] fn version_isomorphic_em_version(version in versions()) { - let sem_ver = version.as_sem_ver(); - let back = Version::from_util_version(sem_ver.into()); + let sem_ver = version.as_exver(); + let back = Version::from_exver_version(sem_ver.into()); prop_assert_eq!(format!("{:?}",version), format!("{:?}", back), "All versions should round trip"); } } diff --git a/core/startos/src/version/v0_3_5.rs b/core/startos/src/version/v0_3_5.rs index 21be1ca49..167132784 100644 --- a/core/startos/src/version/v0_3_5.rs +++ b/core/startos/src/version/v0_3_5.rs @@ -1,4 +1,4 @@ -use emver::VersionRange; +use exver::{ExtendedVersion, VersionRange}; use super::VersionT; use crate::db::model::Database; @@ -6,17 +6,25 @@ use crate::prelude::*; use crate::version::Current; lazy_static::lazy_static! { - pub static ref V0_3_0_COMPAT: VersionRange = VersionRange::Conj( - Box::new(VersionRange::Anchor( - emver::GTE, - emver::Version::new(0, 3, 0, 0), - )), - Box::new(VersionRange::Anchor(emver::LTE, Current::new().semver())), + pub static ref V0_3_0_COMPAT: VersionRange = VersionRange::and( + VersionRange::anchor( + exver::GTE, + ExtendedVersion::new( + exver::Version::new([0, 3, 0], []), + exver::Version::default(), + ), + ), + VersionRange::anchor( + exver::LTE, + ExtendedVersion::new( + Current::new().semver(), + exver::Version::default(), + ) + ), ); + static ref V0_3_5: exver::Version = exver::Version::new([0, 3, 5], []); } -const V0_3_5: emver::Version = emver::Version::new(0, 3, 5, 0); - #[derive(Clone, Debug)] pub struct Version; @@ -25,8 +33,8 @@ impl VersionT for Version { fn new() -> Self { Version } - fn semver(&self) -> emver::Version { - V0_3_5 + fn semver(&self) -> exver::Version { + V0_3_5.clone() } fn compat(&self) -> &'static VersionRange { &V0_3_0_COMPAT diff --git a/core/startos/src/version/v0_3_5_1.rs b/core/startos/src/version/v0_3_5_1.rs index d0f99c153..c6c328f6d 100644 --- a/core/startos/src/version/v0_3_5_1.rs +++ b/core/startos/src/version/v0_3_5_1.rs @@ -1,11 +1,13 @@ -use emver::VersionRange; +use exver::VersionRange; use super::v0_3_5::V0_3_0_COMPAT; use super::{v0_3_5, VersionT}; use crate::db::model::Database; use crate::prelude::*; -const V0_3_5_1: emver::Version = emver::Version::new(0, 3, 5, 1); +lazy_static::lazy_static! { + static ref V0_3_5_1: exver::Version = exver::Version::new([0, 3, 5, 1], []); +} #[derive(Clone, Debug)] pub struct Version; @@ -15,8 +17,8 @@ impl VersionT for Version { fn new() -> Self { Version } - fn semver(&self) -> emver::Version { - V0_3_5_1 + fn semver(&self) -> exver::Version { + V0_3_5_1.clone() } fn compat(&self) -> &'static VersionRange { &V0_3_0_COMPAT diff --git a/core/startos/src/version/v0_3_5_2.rs b/core/startos/src/version/v0_3_5_2.rs index fae689997..00143f8cd 100644 --- a/core/startos/src/version/v0_3_5_2.rs +++ b/core/startos/src/version/v0_3_5_2.rs @@ -1,11 +1,13 @@ -use emver::VersionRange; +use exver::VersionRange; use super::v0_3_5::V0_3_0_COMPAT; use super::{v0_3_5_1, VersionT}; use crate::db::model::Database; use crate::prelude::*; -const V0_3_5_2: emver::Version = emver::Version::new(0, 3, 5, 2); +lazy_static::lazy_static! { + static ref V0_3_5_2: exver::Version = exver::Version::new([0, 3, 5, 2], []); +} #[derive(Clone, Debug)] pub struct Version; @@ -15,8 +17,8 @@ impl VersionT for Version { fn new() -> Self { Version } - fn semver(&self) -> emver::Version { - V0_3_5_2 + fn semver(&self) -> exver::Version { + V0_3_5_2.clone() } fn compat(&self) -> &'static VersionRange { &V0_3_0_COMPAT diff --git a/core/startos/src/version/v0_3_6.rs b/core/startos/src/version/v0_3_6.rs index 0ff6d662a..9564a7376 100644 --- a/core/startos/src/version/v0_3_6.rs +++ b/core/startos/src/version/v0_3_6.rs @@ -1,11 +1,13 @@ -use emver::VersionRange; +use exver::VersionRange; use super::v0_3_5::V0_3_0_COMPAT; use super::{v0_3_5_1, VersionT}; use crate::db::model::Database; use crate::prelude::*; -const V0_3_6: emver::Version = emver::Version::new(0, 3, 6, 0); +lazy_static::lazy_static! { + static ref V0_3_6: exver::Version = exver::Version::new([0, 3, 6], []); +} #[derive(Clone, Debug)] pub struct Version; @@ -15,8 +17,8 @@ impl VersionT for Version { fn new() -> Self { Version } - fn semver(&self) -> emver::Version { - V0_3_6 + fn semver(&self) -> exver::Version { + V0_3_6.clone() } fn compat(&self) -> &'static VersionRange { &V0_3_0_COMPAT diff --git a/debian/postinst b/debian/postinst index 238bd9457..96e392fc8 100755 --- a/debian/postinst +++ b/debian/postinst @@ -78,6 +78,7 @@ sed -i '/\(^\|#\)Compress=/c\Compress=yes' /etc/systemd/journald.conf sed -i '/\(^\|#\)SystemMaxUse=/c\SystemMaxUse=1G' /etc/systemd/journald.conf sed -i '/\(^\|#\)ForwardToSyslog=/c\ForwardToSyslog=no' /etc/systemd/journald.conf sed -i '/^\s*#\?\s*issue_discards\s*=\s*/c\issue_discards = 1' /etc/lvm/lvm.conf +sed -i '/\(^\|#\)\s*unqualified-search-registries\s*=\s*/c\unqualified-search-registries = ["docker.io"]' /etc/containers/registries.conf mkdir -p /etc/nginx/ssl diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..497f28c49 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "embassy-os", + "lockfileVersion": 2, + "requires": true, + "packages": {} +} diff --git a/sdk/lib/emverLite/mod.ts b/sdk/lib/emverLite/mod.ts index f69d4f35d..52fb4e347 100644 --- a/sdk/lib/emverLite/mod.ts +++ b/sdk/lib/emverLite/mod.ts @@ -2,9 +2,9 @@ import * as matches from "ts-matches" const starSub = /((\d+\.)*\d+)\.\*/ // prettier-ignore -export type ValidEmVer = `${number}${`.${number}` | ""}${`.${number}` | ""}${`-${string}` | ""}`; +export type ValidEmVer = string; // prettier-ignore -export type ValidEmVerRange = `${'>=' | '<='| '<' | '>' | ''}${'^' | '~' | ''}${number | '*'}${`.${number | '*'}` | ""}${`.${number | '*'}` | ""}${`-${string}` | ""}`; +export type ValidEmVerRange = string; function incrementLastNumber(list: number[]) { const newList = [...list] diff --git a/sdk/lib/index.browser.ts b/sdk/lib/index.browser.ts index d7c10093f..075bfcf50 100644 --- a/sdk/lib/index.browser.ts +++ b/sdk/lib/index.browser.ts @@ -1,6 +1,7 @@ export { EmVer } from "./emverLite/mod" export { setupManifest } from "./manifest/setupManifest" export { setupExposeStore } from "./store/setupExposeStore" +export { S9pk } from "./s9pk" export * as config from "./config" export * as CB from "./config/builder" export * as CT from "./config/configTypes" diff --git a/sdk/lib/index.ts b/sdk/lib/index.ts index 89d147ed1..c99798d72 100644 --- a/sdk/lib/index.ts +++ b/sdk/lib/index.ts @@ -6,6 +6,7 @@ export { setupManifest } from "./manifest/setupManifest" export { FileHelper } from "./util/fileHelper" export { setupExposeStore } from "./store/setupExposeStore" export { pathBuilder } from "./store/PathBuilder" +export { S9pk } from "./s9pk" export * as actions from "./actions" export * as backup from "./backup" diff --git a/sdk/lib/osBindings/GetPackageParams.ts b/sdk/lib/osBindings/GetPackageParams.ts index 6dfecc9b2..8b852c35c 100644 --- a/sdk/lib/osBindings/GetPackageParams.ts +++ b/sdk/lib/osBindings/GetPackageParams.ts @@ -1,10 +1,11 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { PackageDetailLevel } from "./PackageDetailLevel" import type { PackageId } from "./PackageId" +import type { Version } from "./Version" export type GetPackageParams = { id: PackageId | null version: string | null - sourceVersion: string | null + sourceVersion: Version | null otherVersions: PackageDetailLevel | null } diff --git a/sdk/lib/osBindings/Manifest.ts b/sdk/lib/osBindings/Manifest.ts index 15b96bd13..d808f47a2 100644 --- a/sdk/lib/osBindings/Manifest.ts +++ b/sdk/lib/osBindings/Manifest.ts @@ -28,6 +28,6 @@ export type Manifest = { dependencies: Dependencies hardwareRequirements: HardwareRequirements gitHash: string | null - osVersion: Version + osVersion: string hasConfig: boolean } diff --git a/sdk/lib/osBindings/PackageVersionInfo.ts b/sdk/lib/osBindings/PackageVersionInfo.ts index bdded46bd..364c530f2 100644 --- a/sdk/lib/osBindings/PackageVersionInfo.ts +++ b/sdk/lib/osBindings/PackageVersionInfo.ts @@ -4,7 +4,6 @@ import type { Description } from "./Description" import type { HardwareRequirements } from "./HardwareRequirements" import type { MerkleArchiveCommitment } from "./MerkleArchiveCommitment" import type { RegistryAsset } from "./RegistryAsset" -import type { Version } from "./Version" export type PackageVersionInfo = { title: string @@ -17,7 +16,7 @@ export type PackageVersionInfo = { upstreamRepo: string supportSite: string marketingSite: string - osVersion: Version + osVersion: string hardwareRequirements: HardwareRequirements sourceVersion: string | null s9pk: RegistryAsset diff --git a/sdk/lib/osBindings/ServerInfo.ts b/sdk/lib/osBindings/ServerInfo.ts index 92b8b9b8f..935e3a99f 100644 --- a/sdk/lib/osBindings/ServerInfo.ts +++ b/sdk/lib/osBindings/ServerInfo.ts @@ -2,7 +2,6 @@ import type { Governor } from "./Governor" import type { IpInfo } from "./IpInfo" import type { ServerStatus } from "./ServerStatus" -import type { Version } from "./Version" import type { WifiInfo } from "./WifiInfo" export type ServerInfo = { @@ -10,7 +9,7 @@ export type ServerInfo = { platform: string id: string hostname: string - version: Version + version: string lastBackup: string | null eosVersionCompat: string lanAddress: string diff --git a/sdk/lib/s9pk/index.ts b/sdk/lib/s9pk/index.ts new file mode 100644 index 000000000..eba8ed3f8 --- /dev/null +++ b/sdk/lib/s9pk/index.ts @@ -0,0 +1,67 @@ +import { DataUrl, Manifest, MerkleArchiveCommitment } from "../osBindings" +import { ArrayBufferReader, MerkleArchive } from "./merkleArchive" +import mime from "mime" + +const magicAndVersion = new Uint8Array([59, 59, 2]) + +export function compare(a: Uint8Array, b: Uint8Array) { + if (a.length !== b.length) return false + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false + } + return true +} + +export class S9pk { + private constructor( + readonly manifest: Manifest, + readonly archive: MerkleArchive, + readonly size: number, + ) {} + static async deserialize( + source: Blob, + commitment: MerkleArchiveCommitment | null, + ): Promise { + const header = new ArrayBufferReader( + await source + .slice(0, magicAndVersion.length + MerkleArchive.headerSize) + .arrayBuffer(), + ) + const magicVersion = new Uint8Array(header.next(magicAndVersion.length)) + if (!compare(magicVersion, magicAndVersion)) { + throw new Error("Invalid Magic or Unexpected Version") + } + + const archive = await MerkleArchive.deserialize( + source, + "s9pk", + header, + commitment, + ) + + const manifest = JSON.parse( + new TextDecoder().decode( + await archive.contents + .getPath(["manifest.json"]) + ?.verifiedFileContents(), + ), + ) + + return new S9pk(manifest, archive, source.length) + } + async icon(): Promise { + const iconName = Object.keys(this.archive.contents.contents).find( + (name) => + name.startsWith("icon.") && mime.getType(name)?.startsWith("image/"), + ) + if (!iconName) { + throw new Error("no icon found in archive") + } + return ( + `data:${mime.getType(iconName)};base64,` + + Buffer.from( + await this.archive.contents.getPath([iconName])!.verifiedFileContents(), + ).toString("base64") + ) + } +} diff --git a/sdk/lib/s9pk/merkleArchive/directoryContents.ts b/sdk/lib/s9pk/merkleArchive/directoryContents.ts new file mode 100644 index 000000000..dab7ef53a --- /dev/null +++ b/sdk/lib/s9pk/merkleArchive/directoryContents.ts @@ -0,0 +1,80 @@ +import { ArrayBufferReader, Entry } from "." +import { blake3 } from "@noble/hashes/blake3" +import { serializeVarint } from "./varint" +import { FileContents } from "./fileContents" +import { compare } from ".." + +export class DirectoryContents { + static readonly headerSize = + 8 + // position: u64 BE + 8 // size: u64 BE + private constructor(readonly contents: { [name: string]: Entry }) {} + static async deserialize( + source: Blob, + header: ArrayBufferReader, + sighash: Uint8Array, + maxSize: bigint, + ): Promise { + const position = header.nextU64() + const size = header.nextU64() + if (size > maxSize) { + throw new Error("size is greater than signed") + } + + const tocReader = new ArrayBufferReader( + await source + .slice(Number(position), Number(position + size)) + .arrayBuffer(), + ) + const len = tocReader.nextVarint() + const entries: { [name: string]: Entry } = {} + for (let i = 0; i < len; i++) { + const name = tocReader.nextVarstring() + const entry = await Entry.deserialize(source, tocReader) + entries[name] = entry + } + + const res = new DirectoryContents(entries) + + if (!compare(res.sighash(), sighash)) { + throw new Error("hash sum does not match") + } + + return res + } + sighash(): Uint8Array { + const hasher = blake3.create({}) + const names = Object.keys(this.contents).sort() + hasher.update(new Uint8Array(serializeVarint(names.length))) + for (const name of names) { + const entry = this.contents[name] + const nameBuf = new TextEncoder().encode(name) + hasher.update(new Uint8Array(serializeVarint(nameBuf.length))) + hasher.update(nameBuf) + hasher.update(new Uint8Array(entry.hash)) + const sizeBuf = new Uint8Array(8) + new DataView(sizeBuf.buffer).setBigUint64(0, entry.size) + hasher.update(sizeBuf) + hasher.update(new Uint8Array([0])) + } + + return hasher.digest() + } + getPath(path: string[]): Entry | null { + if (path.length === 0) { + return null + } + const next = this.contents[path[0]] + const rest = path.slice(1) + if (next === undefined) { + return null + } + if (rest.length === 0) { + return next + } + if (next.contents instanceof DirectoryContents) { + return next.contents.getPath(rest) + } + return null + } +} diff --git a/sdk/lib/s9pk/merkleArchive/fileContents.ts b/sdk/lib/s9pk/merkleArchive/fileContents.ts new file mode 100644 index 000000000..7a936f1e8 --- /dev/null +++ b/sdk/lib/s9pk/merkleArchive/fileContents.ts @@ -0,0 +1,24 @@ +import { blake3 } from "@noble/hashes/blake3" +import { ArrayBufferReader } from "." +import { compare } from ".." + +export class FileContents { + private constructor(readonly contents: Blob) {} + static deserialize( + source: Blob, + header: ArrayBufferReader, + size: bigint, + ): FileContents { + const position = header.nextU64() + return new FileContents( + source.slice(Number(position), Number(position + size)), + ) + } + async verified(hash: Uint8Array): Promise { + const res = await this.contents.arrayBuffer() + if (!compare(hash, blake3(new Uint8Array(res)))) { + throw new Error("hash sum mismatch") + } + return res + } +} diff --git a/sdk/lib/s9pk/merkleArchive/index.ts b/sdk/lib/s9pk/merkleArchive/index.ts new file mode 100644 index 000000000..068363599 --- /dev/null +++ b/sdk/lib/s9pk/merkleArchive/index.ts @@ -0,0 +1,167 @@ +import { MerkleArchiveCommitment } from "../../osBindings" +import { DirectoryContents } from "./directoryContents" +import { FileContents } from "./fileContents" +import { ed25519ph } from "@noble/curves/ed25519" +import { sha512 } from "@noble/hashes/sha2" +import { VarIntProcessor } from "./varint" +import { compare } from ".." + +const maxVarstringLen = 1024 * 1024 + +export type Signer = { + pubkey: Uint8Array + signature: Uint8Array + maxSize: bigint + context: string +} + +export class ArrayBufferReader { + constructor(private buffer: ArrayBuffer) {} + next(length: number): ArrayBuffer { + const res = this.buffer.slice(0, length) + this.buffer = this.buffer.slice(length) + return res + } + nextU64(): bigint { + return new DataView(this.next(8)).getBigUint64(0) + } + nextVarint(): number { + const p = new VarIntProcessor() + while (!p.finished()) { + p.push(new Uint8Array(this.buffer.slice(0, 1))[0]) + this.buffer = this.buffer.slice(1) + } + const res = p.decode() + if (res === null) { + throw new Error("Reached EOF") + } + return res + } + nextVarstring(): string { + const len = Math.min(this.nextVarint(), maxVarstringLen) + return new TextDecoder().decode(this.next(len)) + } +} + +export class MerkleArchive { + static readonly headerSize = + 32 + // pubkey + 64 + // signature + 32 + // sighash + 8 + // size + DirectoryContents.headerSize + private constructor( + readonly signer: Signer, + readonly contents: DirectoryContents, + ) {} + static async deserialize( + source: Blob, + context: string, + header: ArrayBufferReader, + commitment: MerkleArchiveCommitment | null, + ): Promise { + const pubkey = new Uint8Array(header.next(32)) + const signature = new Uint8Array(header.next(64)) + const sighash = new Uint8Array(header.next(32)) + const rootMaxSizeBytes = header.next(8) + const maxSize = new DataView(rootMaxSizeBytes).getBigUint64(0) + + if ( + !ed25519ph.verify( + signature, + new Uint8Array( + await new Blob([sighash, rootMaxSizeBytes]).arrayBuffer(), + ), + pubkey, + { + context: new TextEncoder().encode(context), + zip215: true, + }, + ) + ) { + throw new Error("signature verification failed") + } + + if (commitment) { + if ( + !compare( + sighash, + new Uint8Array(Buffer.from(commitment.rootSighash, "base64").buffer), + ) + ) { + throw new Error("merkle root mismatch") + } + if (maxSize > commitment.rootMaxsize) { + throw new Error("root directory max size too large") + } + } else if (maxSize > 1024 * 1024) { + throw new Error( + "root directory max size over 1MiB, cancelling download in case of DOS attack", + ) + } + + const contents = await DirectoryContents.deserialize( + source, + header, + sighash, + maxSize, + ) + + return new MerkleArchive( + { + pubkey, + signature, + maxSize, + context, + }, + contents, + ) + } +} + +export class Entry { + private constructor( + readonly hash: Uint8Array, + readonly size: bigint, + readonly contents: EntryContents, + ) {} + static async deserialize( + source: Blob, + header: ArrayBufferReader, + ): Promise { + const hash = new Uint8Array(header.next(32)) + const size = header.nextU64() + const contents = await deserializeEntryContents(source, header, hash, size) + + return new Entry(new Uint8Array(hash), size, contents) + } + async verifiedFileContents(): Promise { + if (!this.contents) { + throw new Error("file is missing from archive") + } + if (!(this.contents instanceof FileContents)) { + throw new Error("is not a regular file") + } + return this.contents.verified(this.hash) + } +} + +export type EntryContents = null | FileContents | DirectoryContents +async function deserializeEntryContents( + source: Blob, + header: ArrayBufferReader, + hash: Uint8Array, + size: bigint, +): Promise { + const typeId = new Uint8Array(header.next(1))[0] + switch (typeId) { + case 0: + return null + case 1: + return FileContents.deserialize(source, header, size) + case 2: + return DirectoryContents.deserialize(source, header, hash, size) + default: + throw new Error(`Unknown type id ${typeId} found in MerkleArchive`) + } +} diff --git a/sdk/lib/s9pk/merkleArchive/varint.ts b/sdk/lib/s9pk/merkleArchive/varint.ts new file mode 100644 index 000000000..2bf4793b1 --- /dev/null +++ b/sdk/lib/s9pk/merkleArchive/varint.ts @@ -0,0 +1,62 @@ +const msb = 0x80 +const dropMsb = 0x7f +const maxSize = Math.floor((8 * 8 + 7) / 7) + +export class VarIntProcessor { + private buf: Uint8Array + private i: number + constructor() { + this.buf = new Uint8Array(maxSize) + this.i = 0 + } + push(b: number) { + if (this.i >= maxSize) { + throw new Error("Unterminated varint") + } + this.buf[this.i] = b + this.i += 1 + } + finished(): boolean { + return this.i > 0 && (this.buf[this.i - 1] & msb) === 0 + } + decode(): number | null { + let result = 0 + let shift = 0 + let success = false + for (let i = 0; i < this.i; i++) { + const b = this.buf[i] + const msbDropped = b & dropMsb + result |= msbDropped << shift + shift += 7 + + if ((b & msb) == 0 || shift > 9 * 7) { + success = (b & msb) === 0 + break + } + } + + if (success) { + return result + } else { + console.error(this.buf) + return null + } + } +} + +export function serializeVarint(int: number): ArrayBuffer { + const buf = new Uint8Array(maxSize) + let n = int + let i = 0 + + while (n >= msb) { + buf[i] = msb | n + i += 1 + n >>= 7 + } + + buf[i] = n + i += 1 + + return buf.slice(0, i).buffer +} diff --git a/sdk/lib/test/emverList.test.ts b/sdk/lib/test/emverList.test.ts index 43919aa83..07dbb5aaf 100644 --- a/sdk/lib/test/emverList.test.ts +++ b/sdk/lib/test/emverList.test.ts @@ -8,18 +8,14 @@ describe("EmVer", () => { checker.check("1.2") checker.check("1.2.3") checker.check("1.2.3.4") - // @ts-expect-error checker.check("1.2.3.4.5") - // @ts-expect-error checker.check("1.2.3.4.5.6") expect(checker.check("1")).toEqual(true) expect(checker.check("1.2")).toEqual(true) expect(checker.check("1.2.3.4")).toEqual(true) }) test("rangeOf('*') invalid", () => { - // @ts-expect-error expect(() => checker.check("a")).toThrow() - // @ts-expect-error expect(() => checker.check("")).toThrow() expect(() => checker.check("1..3")).toThrow() }) @@ -31,7 +27,6 @@ describe("EmVer", () => { expect(checker.check("2-beta123")).toEqual(true) expect(checker.check("2")).toEqual(true) expect(checker.check("1.2.3.5")).toEqual(true) - // @ts-expect-error expect(checker.check("1.2.3.4.1")).toEqual(true) }) @@ -58,7 +53,6 @@ describe("EmVer", () => { test(`rangeOf(">=1.2.3.4") valid`, () => { expect(checker.check("2")).toEqual(true) expect(checker.check("1.2.3.5")).toEqual(true) - // @ts-expect-error expect(checker.check("1.2.3.4.1")).toEqual(true) expect(checker.check("1.2.3.4")).toEqual(true) }) @@ -73,7 +67,6 @@ describe("EmVer", () => { test(`rangeOf("<1.2.3.4") invalid`, () => { expect(checker.check("2")).toEqual(false) expect(checker.check("1.2.3.5")).toEqual(false) - // @ts-expect-error expect(checker.check("1.2.3.4.1")).toEqual(false) expect(checker.check("1.2.3.4")).toEqual(false) }) @@ -88,7 +81,6 @@ describe("EmVer", () => { test(`rangeOf("<=1.2.3.4") invalid`, () => { expect(checker.check("2")).toEqual(false) expect(checker.check("1.2.3.5")).toEqual(false) - // @ts-expect-error expect(checker.check("1.2.3.4.1")).toEqual(false) }) @@ -196,7 +188,6 @@ describe("EmVer", () => { test(`rangeOf("!>1.2.3.4") invalid`, () => { expect(checker.check("2")).toEqual(false) expect(checker.check("1.2.3.5")).toEqual(false) - // @ts-expect-error expect(checker.check("1.2.3.4.1")).toEqual(false) }) diff --git a/sdk/lib/util/fileHelper.ts b/sdk/lib/util/fileHelper.ts index 54f1eca06..383b4fd31 100644 --- a/sdk/lib/util/fileHelper.ts +++ b/sdk/lib/util/fileHelper.ts @@ -1,7 +1,7 @@ import * as matches from "ts-matches" import * as YAML from "yaml" import * as TOML from "@iarna/toml" -import _ from "lodash" +import merge from "lodash.merge" import * as T from "../types" import * as fs from "node:fs/promises" @@ -82,7 +82,7 @@ export class FileHelper
{ async merge(data: A, effects: T.Effects) { const fileData = (await this.read(effects).catch(() => ({}))) || {} - const mergeData = _.merge({}, fileData, data) + const mergeData = merge({}, fileData, data) return await this.write(mergeData, effects) } /** diff --git a/sdk/package-lock.json b/sdk/package-lock.json index a50066b34..0a3655a7d 100644 --- a/sdk/package-lock.json +++ b/sdk/package-lock.json @@ -1,29 +1,32 @@ { "name": "@start9labs/start-sdk", - "version": "0.3.6-alpha1", + "version": "0.3.6-alpha5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@start9labs/start-sdk", - "version": "0.3.6-alpha1", + "version": "0.3.6-alpha5", "license": "MIT", "dependencies": { + "@iarna/toml": "^2.2.5", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", "isomorphic-fetch": "^3.0.0", - "lodash": "^4.17.21", - "ts-matches": "^5.4.1" + "lodash.merge": "^4.6.2", + "mime": "^4.0.3", + "ts-matches": "^5.5.1", + "yaml": "^2.2.2" }, "devDependencies": { - "@iarna/toml": "^2.2.5", "@types/jest": "^29.4.0", - "@types/lodash": "^4.17.5", + "@types/lodash.merge": "^4.6.2", "jest": "^29.4.3", "prettier": "^3.2.5", "ts-jest": "^29.0.5", "ts-node": "^10.9.1", "tsx": "^4.7.1", - "typescript": "^5.0.4", - "yaml": "^2.2.2" + "typescript": "^5.0.4" } }, "node_modules/@ampproject/remapping": { @@ -653,8 +656,7 @@ "node_modules/@iarna/toml": { "version": "2.2.5", "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", - "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", - "dev": true + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==" }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", @@ -1006,6 +1008,28 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "node_modules/@noble/curves": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@sinclair/typebox": { "version": "0.25.24", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", @@ -1144,6 +1168,15 @@ "integrity": "sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw==", "dev": true }, + "node_modules/@types/lodash.merge": { + "version": "4.6.9", + "resolved": "https://registry.npmjs.org/@types/lodash.merge/-/lodash.merge-4.6.9.tgz", + "integrity": "sha512-23sHDPmzd59kUgWyKGiOMO2Qb9YtqRO/x4IhkgNUiPQ1+5MUVqi6bCZeq9nBJ17msjIMbEIO5u+XW4Kz6aGUhQ==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/node": { "version": "18.15.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.10.tgz", @@ -2849,17 +2882,17 @@ "node": ">=8" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", "dev": true }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2918,6 +2951,20 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.3.tgz", + "integrity": "sha512-KgUb15Oorc0NEKPbvfa0wRU+PItIEZmiv+pyAO2i0oTIVTJhlzMclU7w4RXWQrSOVH5ax/p/CkIO7KI4OyFJTQ==", + "funding": [ + "https://github.com/sponsors/broofa" + ], + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -3580,9 +3627,9 @@ "dev": true }, "node_modules/ts-matches": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-5.4.1.tgz", - "integrity": "sha512-kXrY75F0s0WD15N2bWKDScKlKgwnusN6dTRzGs1N7LlxQRnazrsBISC1HL4sy2adsyk65Zbx3Ui3IGN8leAFOQ==" + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-5.5.1.tgz", + "integrity": "sha512-UFYaKgfqlg9FROK7bdpYqFwG1CJvP4kOJdjXuWoqxo9jCmANoDw1GxkSCpJgoTeIiSTaTH5Qr1klSspb8c+ydg==" }, "node_modules/ts-node": { "version": "10.9.1", @@ -4249,7 +4296,6 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", - "dev": true, "engines": { "node": ">= 14" } diff --git a/sdk/package.json b/sdk/package.json index d6c3a1ca5..8ed984a82 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -31,10 +31,13 @@ "homepage": "https://github.com/Start9Labs/start-sdk#readme", "dependencies": { "isomorphic-fetch": "^3.0.0", - "lodash": "^4.17.21", - "ts-matches": "^5.4.1", + "lodash.merge": "^4.6.2", + "mime": "^4.0.3", + "ts-matches": "^5.5.1", "yaml": "^2.2.2", - "@iarna/toml": "^2.2.5" + "@iarna/toml": "^2.2.5", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0" }, "prettier": { "trailingComma": "all", @@ -44,7 +47,7 @@ }, "devDependencies": { "@types/jest": "^29.4.0", - "@types/lodash": "^4.17.5", + "@types/lodash.merge": "^4.6.2", "jest": "^29.4.3", "prettier": "^3.2.5", "ts-jest": "^29.0.5", diff --git a/system-images/compat/src/config/mod.rs b/system-images/compat/src/config/mod.rs index ce591b06a..c677dd3d5 100644 --- a/system-images/compat/src/config/mod.rs +++ b/system-images/compat/src/config/mod.rs @@ -55,7 +55,7 @@ pub fn validate_configuration( Ok(_) => { // create temp config file serde_yaml::to_writer( - std::fs::File::create(config_path.with_extension("tmp"))?, + std::fs::create_file(config_path.with_extension("tmp"))?, &config, )?; std::fs::rename(config_path.with_extension("tmp"), config_path)?; diff --git a/web/package-lock.json b/web/package-lock.json index ca5c8efe8..8a48b18fa 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -23,6 +23,8 @@ "@ng-web-apis/common": "^2.0.0", "@ng-web-apis/mutation-observer": "^2.0.0", "@ng-web-apis/resize-observer": "^2.0.0", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", "@start9labs/argon2": "^0.2.2", "@start9labs/emver": "^0.1.5", "@start9labs/start-sdk": "file:../sdk/dist", @@ -35,6 +37,7 @@ "angular-svg-round-progressbar": "^9.0.0", "ansi-to-html": "^0.7.2", "base64-js": "^1.5.1", + "buffer": "^6.0.3", "cbor": "npm:@jprochazk/cbor@^0.4.9", "cbor-web": "^8.1.0", "core-js": "^3.21.1", @@ -45,6 +48,7 @@ "jose": "^4.9.0", "js-yaml": "^4.1.0", "marked": "^4.0.0", + "mime": "^4.0.3", "monaco-editor": "^0.33.0", "mustache": "^4.2.0", "ng-qrcode": "^7.0.0", @@ -53,7 +57,7 @@ "pbkdf2": "^3.1.2", "rxjs": "^7.8.1", "swiper": "^8.2.4", - "ts-matches": "^5.2.1", + "ts-matches": "^5.5.1", "tslib": "^2.3.0", "uuid": "^8.3.2", "zone.js": "^0.11.5" @@ -1978,14 +1982,17 @@ "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", "isomorphic-fetch": "^3.0.0", - "lodash": "^4.17.21", - "ts-matches": "^5.4.1", + "lodash.merge": "^4.6.2", + "mime": "^4.0.3", + "ts-matches": "^5.5.1", "yaml": "^2.2.2" }, "devDependencies": { "@types/jest": "^29.4.0", - "@types/lodash": "^4.17.5", + "@types/lodash.merge": "^4.6.2", "jest": "^29.4.3", "prettier": "^3.2.5", "ts-jest": "^29.0.5", @@ -5111,6 +5118,28 @@ "@ng-web-apis/common": ">=2.0.0" } }, + "node_modules/@noble/curves": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "devOptional": true, @@ -9954,6 +9983,19 @@ "node": ">=6" } }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/less/node_modules/pify": { "version": "4.0.1", "dev": true, @@ -10551,14 +10593,17 @@ } }, "node_modules/mime": { - "version": "1.6.0", - "dev": true, - "license": "MIT", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.3.tgz", + "integrity": "sha512-KgUb15Oorc0NEKPbvfa0wRU+PItIEZmiv+pyAO2i0oTIVTJhlzMclU7w4RXWQrSOVH5ax/p/CkIO7KI4OyFJTQ==", + "funding": [ + "https://github.com/sponsors/broofa" + ], "bin": { - "mime": "cli.js" + "mime": "bin/cli.js" }, "engines": { - "node": ">=4" + "node": ">=16" } }, "node_modules/mime-db": { @@ -13644,6 +13689,18 @@ "node": ">= 0.8" } }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "dev": true, @@ -14617,8 +14674,9 @@ } }, "node_modules/ts-matches": { - "version": "v5.2.1", - "license": "MIT" + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-5.5.1.tgz", + "integrity": "sha512-UFYaKgfqlg9FROK7bdpYqFwG1CJvP4kOJdjXuWoqxo9jCmANoDw1GxkSCpJgoTeIiSTaTH5Qr1klSspb8c+ydg==" }, "node_modules/ts-morph": { "version": "10.0.2", diff --git a/web/package.json b/web/package.json index 3ea29c6fd..1db0dead4 100644 --- a/web/package.json +++ b/web/package.json @@ -46,6 +46,8 @@ "@ng-web-apis/common": "^2.0.0", "@ng-web-apis/mutation-observer": "^2.0.0", "@ng-web-apis/resize-observer": "^2.0.0", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", "@start9labs/argon2": "^0.2.2", "@start9labs/emver": "^0.1.5", "@start9labs/start-sdk": "file:../sdk/dist", @@ -58,6 +60,7 @@ "angular-svg-round-progressbar": "^9.0.0", "ansi-to-html": "^0.7.2", "base64-js": "^1.5.1", + "buffer": "^6.0.3", "cbor": "npm:@jprochazk/cbor@^0.4.9", "cbor-web": "^8.1.0", "core-js": "^3.21.1", @@ -68,6 +71,7 @@ "jose": "^4.9.0", "js-yaml": "^4.1.0", "marked": "^4.0.0", + "mime": "^4.0.3", "monaco-editor": "^0.33.0", "mustache": "^4.2.0", "ng-qrcode": "^7.0.0", @@ -76,7 +80,7 @@ "pbkdf2": "^3.1.2", "rxjs": "^7.8.1", "swiper": "^8.2.4", - "ts-matches": "^5.2.1", + "ts-matches": "^5.5.1", "tslib": "^2.3.0", "uuid": "^8.3.2", "zone.js": "^0.11.5" diff --git a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts index 4026d5183..f6410e748 100644 --- a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts @@ -4,14 +4,15 @@ import { ApiService } from 'src/app/services/api/embassy-api.service' import { ConfigService } from 'src/app/services/config.service' import cbor from 'cbor' import { ErrorToastService } from '@start9labs/shared' -import { T } from '@start9labs/start-sdk' +import { S9pk, T } from '@start9labs/start-sdk' interface Positions { [key: string]: [bigint, bigint] // [position, length] } const MAGIC = new Uint8Array([59, 59]) -const VERSION = new Uint8Array([1]) +const VERSION_1 = new Uint8Array([1]) +const VERSION_2 = new Uint8Array([2]) @Component({ selector: 'sideload', @@ -64,11 +65,36 @@ export class SideloadPage { async validateS9pk(file: File) { const magic = new Uint8Array(await blobToBuffer(file.slice(0, 2))) const version = new Uint8Array(await blobToBuffer(file.slice(2, 3))) - if (compare(magic, MAGIC) && compare(version, VERSION)) { - await this.parseS9pk(file) - return { - invalid: false, - message: 'A valid package file has been detected!', + if (compare(magic, MAGIC)) { + try { + if (compare(version, VERSION_1)) { + await this.parseS9pkV1(file) + return { + invalid: false, + message: 'A valid package file has been detected!', + } + } else if (compare(version, VERSION_2)) { + await this.parseS9pkV2(file) + return { + invalid: false, + message: 'A valid package file has been detected!', + } + } else { + console.error(version) + return { + invalid: true, + message: 'Invalid package file', + } + } + } catch (e) { + console.error(e) + return { + invalid: true, + message: + e instanceof Error + ? `Invalid package file: ${e.message}` + : 'Invalid package file', + } } } else { return { @@ -91,12 +117,9 @@ export class SideloadPage { }) await loader.present() try { - const guid = await this.api.sideloadPackage({ - manifest: this.toUpload.manifest!, - icon: this.toUpload.icon!, - }) + const res = await this.api.sideloadPackage() this.api - .uploadPackage(guid, this.toUpload.file!) + .uploadPackage(res.upload, this.toUpload.file!) .catch(e => console.error(e)) this.navCtrl.navigateRoot('/services') @@ -108,7 +131,7 @@ export class SideloadPage { } } - async parseS9pk(file: File) { + async parseS9pkV1(file: File) { const positions: Positions = {} // magic=2bytes, version=1bytes, pubkey=32bytes, signature=64bytes, toc_length=4bytes = 103byte is starting point let start = 103 @@ -122,6 +145,12 @@ export class SideloadPage { await this.getIcon(positions, file) } + async parseS9pkV2(file: File) { + const s9pk = await S9pk.deserialize(file, null) + this.toUpload.manifest = s9pk.manifest + this.toUpload.icon = await s9pk.icon() + } + async getManifest(positions: Positions, file: Blob) { const data = await blobToBuffer( file.slice( @@ -225,6 +254,7 @@ async function readBlobToArrayBuffer( } function compare(a: Uint8Array, b: Uint8Array) { + if (a.length !== b.length) return false for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) return false } diff --git a/web/projects/ui/src/app/services/api/api.types.ts b/web/projects/ui/src/app/services/api/api.types.ts index 0c7679754..96be4850b 100644 --- a/web/projects/ui/src/app/services/api/api.types.ts +++ b/web/projects/ui/src/app/services/api/api.types.ts @@ -273,7 +273,10 @@ export module RR { manifest: T.Manifest icon: string // base64 } - export type SideloadPacakgeRes = string //guid + export type SideloadPackageRes = { + upload: string // guid + progress: string // guid + } // marketplace diff --git a/web/projects/ui/src/app/services/api/embassy-api.service.ts b/web/projects/ui/src/app/services/api/embassy-api.service.ts index 1d9c6f805..735b8f1d1 100644 --- a/web/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-api.service.ts @@ -243,7 +243,5 @@ export abstract class ApiService { params: RR.DryConfigureDependencyReq, ): Promise - abstract sideloadPackage( - params: RR.SideloadPackageReq, - ): Promise + abstract sideloadPackage(): Promise } diff --git a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts index 057c51013..8cdd0d5e3 100644 --- a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -29,7 +29,7 @@ export class LiveApiService extends ApiService { @Inject(PATCH_CACHE) private readonly cache$: Observable>, ) { super() - ; (window as any).rpcClient = this + ;(window as any).rpcClient = this } // for getting static files: ex icons, instructions, licenses @@ -460,12 +460,10 @@ export class LiveApiService extends ApiService { }) } - async sideloadPackage( - params: RR.SideloadPackageReq, - ): Promise { + async sideloadPackage(): Promise { return this.rpcRequest({ method: 'package.sideload', - params, + params: {}, }) } diff --git a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts index 98efee2ef..0a82ac850 100644 --- a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -1062,11 +1062,12 @@ export class MockApiService extends ApiService { } } - async sideloadPackage( - params: RR.SideloadPackageReq, - ): Promise { + async sideloadPackage(): Promise { await pauseFor(2000) - return '4120e092-05ab-4de2-9fbd-c3f1f4b1df9e' // no significance, randomly generated + return { + upload: '4120e092-05ab-4de2-9fbd-c3f1f4b1df9e', // no significance, randomly generated + progress: '5120e092-05ab-4de2-9fbd-c3f1f4b1df9e', // no significance, randomly generated + } } private async initProgress(): Promise { diff --git a/web/projects/ui/src/polyfills.ts b/web/projects/ui/src/polyfills.ts index a392d45cf..67caa24e8 100644 --- a/web/projects/ui/src/polyfills.ts +++ b/web/projects/ui/src/polyfills.ts @@ -52,8 +52,11 @@ * */ -(window as any).global = window -; (window as any).process = { env: { DEBUG: undefined }, browser: true } +;(window as any).global = window +;(window as any).process = { env: { DEBUG: undefined }, browser: true } + +import { Buffer } from 'buffer' +window.Buffer = Buffer import './zone-flags' From f76e822381c63834cd66aef8682fe1fb48fe56f9 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Wed, 10 Jul 2024 11:58:02 -0600 Subject: [PATCH 054/125] port 040 config (#2657) * port 040 config, WIP * update fixtures * use taiga modal for backups too * fix: update Taiga UI and refactor everything to work * chore: package-lock * fix interfaces and mocks for interfaces * better mocks * function to transform old spec to new * delete unused fns * delete unused FE config utils * fix exports from sdk * reorganize exports * functions to translate config * rename unionSelectKey and unionValueKey * Adding in the transformation of the getConfig to the new types. * chore: add Taiga UI to preloader --------- Co-authored-by: waterplea Co-authored-by: Aiden McClelland Co-authored-by: J H --- .../Systems/SystemForEmbassy/index.ts | 17 +- .../SystemForEmbassy/transformConfigSpec.ts | 550 + sdk/lib/StartSdk.ts | 24 - sdk/lib/config/builder/list.ts | 90 - sdk/lib/config/builder/variants.ts | 8 +- sdk/lib/config/configTypes.ts | 186 +- sdk/lib/index.browser.ts | 2 +- sdk/lib/osBindings/InstalledVersionParams.ts | 4 + sdk/lib/osBindings/index.ts | 1 + sdk/lib/test/configBuilder.test.ts | 51 +- sdk/lib/test/configTypes.test.ts | 10 +- sdk/lib/test/output.test.ts | 21 +- sdk/lib/util/getRandomCharInSet.ts | 5 +- sdk/lib/util/getServiceInterface.ts | 4 +- sdk/lib/util/index.browser.ts | 27 +- sdk/lib/util/index.ts | 25 +- sdk/lib/util/typeHelpers.ts | 23 + sdk/scripts/oldSpecToBuilder.ts | 40 +- web/package-lock.json | 9004 +++++++++-------- web/package.json | 18 +- .../setup-wizard/src/app/app.component.ts | 6 +- .../setup-wizard/src/app/app.module.ts | 3 +- .../src/app/pages/attach/attach.page.ts | 12 +- .../src/app/pages/embassy/embassy.page.ts | 8 +- .../src/app/pages/home/home.page.ts | 8 +- .../src/app/pages/loading/loading.page.ts | 13 +- .../src/app/pages/recover/recover.page.ts | 6 +- .../src/app/pages/success/success.page.ts | 9 +- .../src/app/pages/transfer/transfer.page.ts | 6 +- .../shared/assets/img/icon_transparent.png | Bin 0 -> 53685 bytes .../shared/assets/img/storefront-outline.png | Bin 0 -> 10912 bytes .../taiga-ui/icons/tuiIconPaintOutline.svg | 10 + .../components/loading/loading.component.scss | 20 + .../components/loading/loading.component.ts | 17 + .../src/components/loading/loading.module.ts | 13 + .../src/components/loading/loading.service.ts | 10 + .../components/markdown/markdown.component.ts | 2 +- web/projects/shared/src/public-api.ts | 6 +- .../src/services/error-toast.service.ts | 70 - .../shared/src/services/error.service.ts | 43 + web/projects/shared/src/util/invert.ts | 12 + web/projects/ui/src/app/app.module.ts | 4 +- web/projects/ui/src/app/app.providers.ts | 5 + .../app/preloader/preloader.component.html | 42 +- .../app/app/preloader/preloader.component.ts | 34 + .../src/app/app/preloader/preloader.module.ts | 56 +- .../ui/src/app/app/snek/snek.directive.ts | 19 +- .../backup-drives.component.module.ts | 2 - .../backup-drives/backup-drives.component.ts | 146 +- .../form-object/form-label.component.html | 16 - .../form-object/form-object.component.html | 372 - .../form-object.component.module.ts | 47 - .../form-object/form-object.component.scss | 42 - .../form-object/form-object.component.ts | 425 - .../form-object/form-object.pipes.ts | 92 - .../form-object/form-union.component.html | 42 - .../ui/src/app/components/form.component.ts | 167 + .../app/components/form/control.directive.ts | 30 + .../ui/src/app/components/form/control.ts | 35 + .../form/form-array/form-array.component.html | 58 + .../form/form-array/form-array.component.scss | 50 + .../form/form-array/form-array.component.ts | 91 + .../form/form-color/form-color.component.html | 31 + .../form/form-color/form-color.component.scss | 33 + .../form/form-color/form-color.component.ts | 15 + .../form-control/form-control.component.html | 40 + .../form-control/form-control.component.scss | 11 + .../form-control/form-control.component.ts | 71 + .../form-control/form-control.providers.ts | 25 + .../form-datetime.component.html | 43 + .../form-datetime/form-datetime.component.ts | 36 + .../form/form-file/form-file.component.html | 31 + .../form/form-file/form-file.component.scss | 46 + .../form/form-file/form-file.component.ts | 11 + .../form/form-group/form-group.component.html | 30 + .../form/form-group/form-group.component.scss | 35 + .../form/form-group/form-group.component.ts | 35 + .../form/form-group/form-group.providers.ts | 34 + .../form-multiselect.component.html | 18 + .../form-multiselect.component.ts | 49 + .../form-number/form-number.component.html | 18 + .../form/form-number/form-number.component.ts | 11 + .../form-object/form-object.component.html | 25 + .../form-object/form-object.component.scss | 41 + .../form/form-object/form-object.component.ts | 38 + .../form-select/form-select.component.html | 17 + .../form/form-select/form-select.component.ts | 30 + .../form/form-text/form-text.component.html | 44 + .../form/form-text/form-text.component.scss | 8 + .../form/form-text/form-text.component.ts | 16 + .../form-textarea.component.html | 15 + .../form-textarea/form-textarea.component.ts | 12 + .../form-toggle/form-toggle.component.html | 11 + .../form/form-toggle/form-toggle.component.ts | 10 + .../form/form-union/form-union.component.html | 11 + .../form/form-union/form-union.component.scss | 8 + .../form/form-union/form-union.component.ts | 55 + .../ui/src/app/components/form/form.module.ts | 109 + .../ui/src/app/components/form/hint.pipe.ts | 21 + .../app/components/form/invalid.service.ts | 19 + .../src/app/components/form/mustache.pipe.ts | 12 + .../src/app/components/logs/logs.component.ts | 36 +- .../refresh-alert/refresh-alert.component.ts | 13 +- .../update-toast/update-toast.component.ts | 21 +- .../modals/app-config/app-config.module.ts | 21 - .../modals/app-config/app-config.page.html | 144 - .../modals/app-config/app-config.page.scss | 12 - .../app/modals/app-config/app-config.page.ts | 349 - .../app-recover-select.page.ts | 23 +- .../ui/src/app/modals/config-dep.component.ts | 104 + .../ui/src/app/modals/config.component.ts | 281 + .../app/modals/enum-list/enum-list.module.ts | 12 - .../app/modals/enum-list/enum-list.page.html | 45 - .../app/modals/enum-list/enum-list.page.scss | 0 .../app/modals/enum-list/enum-list.page.ts | 50 - .../generic-form/generic-form.module.ts | 19 - .../generic-form/generic-form.page.html | 35 - .../generic-form/generic-form.page.scss | 9 - .../modals/generic-form/generic-form.page.ts | 60 - .../generic-input.component.html | 67 - .../generic-input.component.module.ts | 20 - .../generic-input.component.scss | 0 .../generic-input/generic-input.component.ts | 96 - .../marketplace-settings.module.ts | 17 - .../marketplace-settings.page.html | 95 +- .../marketplace-settings.page.scss | 5 + .../marketplace-settings.page.ts | 354 +- .../registry.component.ts | 42 + .../store-icon.component.ts | 46 + .../app/modals/os-update/os-update.page.ts | 19 +- .../ui/src/app/modals/prompt.component.ts | 123 + .../app-actions/app-actions.module.ts | 2 - .../app-actions/app-actions.page.ts | 60 +- .../app-interfaces/app-interfaces.page.html | 4 +- .../app-interfaces/app-interfaces.page.ts | 97 +- .../app-metrics/app-metrics.page.ts | 6 +- .../app-properties/app-properties.page.ts | 20 +- .../apps-routes/app-show/app-show.module.ts | 3 - .../apps-routes/app-show/app-show.page.ts | 28 +- .../app-show-health-checks.component.html | 2 +- .../app-show-status.component.ts | 68 +- .../app-show/pipes/to-buttons.pipe.ts | 10 +- .../pages/diagnostic-routes/home/home.page.ts | 26 +- .../pages/diagnostic-routes/logs/logs.page.ts | 6 +- .../ui/src/app/pages/init/init.service.ts | 6 +- .../ui/src/app/pages/login/login.page.ts | 16 +- .../marketplace-list.module.ts | 2 - .../marketplace-list/marketplace-list.page.ts | 15 +- .../marketplace-show-controls.component.ts | 30 +- .../pages/notifications/notifications.page.ts | 44 +- .../restore/restore.component.ts | 49 +- .../server-backup/server-backup.page.ts | 73 +- .../server-metrics/server-metrics.page.ts | 8 +- .../server-show/server-show.page.ts | 294 +- .../server-routes/sessions/sessions.page.ts | 23 +- .../server-routes/sideload/sideload.page.ts | 23 +- .../server-routes/ssh-keys/ssh-keys.page.html | 6 +- .../server-routes/ssh-keys/ssh-keys.page.ts | 145 +- .../app/pages/server-routes/wifi/wifi.page.ts | 145 +- .../ui/src/app/pkg-config/config-types.ts | 159 - .../ui/src/app/pkg-config/config-utilities.ts | 235 - .../ui/src/app/services/api/api.fixures.ts | 1221 ++- .../ui/src/app/services/api/api.types.ts | 7 +- .../app/services/api/embassy-api.service.ts | 2 + .../services/api/embassy-live-api.service.ts | 11 +- .../services/api/embassy-mock-api.service.ts | 9 +- .../ui/src/app/services/api/mock-patch.ts | 102 +- .../src/app/services/form-dialog.service.ts | 41 + .../ui/src/app/services/form.service.ts | 487 +- .../ui/src/app/services/modal.service.ts | 24 - .../ui/src/app/util/configBuilderToSpec.ts | 9 + web/projects/ui/src/index.html | 28 +- web/projects/ui/src/styles.scss | 13 +- 173 files changed, 9761 insertions(+), 9200 deletions(-) create mode 100644 container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts create mode 100644 sdk/lib/osBindings/InstalledVersionParams.ts create mode 100644 sdk/lib/util/typeHelpers.ts create mode 100644 web/projects/shared/assets/img/icon_transparent.png create mode 100644 web/projects/shared/assets/img/storefront-outline.png create mode 100644 web/projects/shared/assets/taiga-ui/icons/tuiIconPaintOutline.svg create mode 100644 web/projects/shared/src/components/loading/loading.component.scss create mode 100644 web/projects/shared/src/components/loading/loading.component.ts create mode 100644 web/projects/shared/src/components/loading/loading.module.ts create mode 100644 web/projects/shared/src/components/loading/loading.service.ts delete mode 100644 web/projects/shared/src/services/error-toast.service.ts create mode 100644 web/projects/shared/src/services/error.service.ts create mode 100644 web/projects/shared/src/util/invert.ts delete mode 100644 web/projects/ui/src/app/components/form-object/form-label.component.html delete mode 100644 web/projects/ui/src/app/components/form-object/form-object.component.html delete mode 100644 web/projects/ui/src/app/components/form-object/form-object.component.module.ts delete mode 100644 web/projects/ui/src/app/components/form-object/form-object.component.scss delete mode 100644 web/projects/ui/src/app/components/form-object/form-object.component.ts delete mode 100644 web/projects/ui/src/app/components/form-object/form-object.pipes.ts delete mode 100644 web/projects/ui/src/app/components/form-object/form-union.component.html create mode 100644 web/projects/ui/src/app/components/form.component.ts create mode 100644 web/projects/ui/src/app/components/form/control.directive.ts create mode 100644 web/projects/ui/src/app/components/form/control.ts create mode 100644 web/projects/ui/src/app/components/form/form-array/form-array.component.html create mode 100644 web/projects/ui/src/app/components/form/form-array/form-array.component.scss create mode 100644 web/projects/ui/src/app/components/form/form-array/form-array.component.ts create mode 100644 web/projects/ui/src/app/components/form/form-color/form-color.component.html create mode 100644 web/projects/ui/src/app/components/form/form-color/form-color.component.scss create mode 100644 web/projects/ui/src/app/components/form/form-color/form-color.component.ts create mode 100644 web/projects/ui/src/app/components/form/form-control/form-control.component.html create mode 100644 web/projects/ui/src/app/components/form/form-control/form-control.component.scss create mode 100644 web/projects/ui/src/app/components/form/form-control/form-control.component.ts create mode 100644 web/projects/ui/src/app/components/form/form-control/form-control.providers.ts create mode 100644 web/projects/ui/src/app/components/form/form-datetime/form-datetime.component.html create mode 100644 web/projects/ui/src/app/components/form/form-datetime/form-datetime.component.ts create mode 100644 web/projects/ui/src/app/components/form/form-file/form-file.component.html create mode 100644 web/projects/ui/src/app/components/form/form-file/form-file.component.scss create mode 100644 web/projects/ui/src/app/components/form/form-file/form-file.component.ts create mode 100644 web/projects/ui/src/app/components/form/form-group/form-group.component.html create mode 100644 web/projects/ui/src/app/components/form/form-group/form-group.component.scss create mode 100644 web/projects/ui/src/app/components/form/form-group/form-group.component.ts create mode 100644 web/projects/ui/src/app/components/form/form-group/form-group.providers.ts create mode 100644 web/projects/ui/src/app/components/form/form-multiselect/form-multiselect.component.html create mode 100644 web/projects/ui/src/app/components/form/form-multiselect/form-multiselect.component.ts create mode 100644 web/projects/ui/src/app/components/form/form-number/form-number.component.html create mode 100644 web/projects/ui/src/app/components/form/form-number/form-number.component.ts create mode 100644 web/projects/ui/src/app/components/form/form-object/form-object.component.html create mode 100644 web/projects/ui/src/app/components/form/form-object/form-object.component.scss create mode 100644 web/projects/ui/src/app/components/form/form-object/form-object.component.ts create mode 100644 web/projects/ui/src/app/components/form/form-select/form-select.component.html create mode 100644 web/projects/ui/src/app/components/form/form-select/form-select.component.ts create mode 100644 web/projects/ui/src/app/components/form/form-text/form-text.component.html create mode 100644 web/projects/ui/src/app/components/form/form-text/form-text.component.scss create mode 100644 web/projects/ui/src/app/components/form/form-text/form-text.component.ts create mode 100644 web/projects/ui/src/app/components/form/form-textarea/form-textarea.component.html create mode 100644 web/projects/ui/src/app/components/form/form-textarea/form-textarea.component.ts create mode 100644 web/projects/ui/src/app/components/form/form-toggle/form-toggle.component.html create mode 100644 web/projects/ui/src/app/components/form/form-toggle/form-toggle.component.ts create mode 100644 web/projects/ui/src/app/components/form/form-union/form-union.component.html create mode 100644 web/projects/ui/src/app/components/form/form-union/form-union.component.scss create mode 100644 web/projects/ui/src/app/components/form/form-union/form-union.component.ts create mode 100644 web/projects/ui/src/app/components/form/form.module.ts create mode 100644 web/projects/ui/src/app/components/form/hint.pipe.ts create mode 100644 web/projects/ui/src/app/components/form/invalid.service.ts create mode 100644 web/projects/ui/src/app/components/form/mustache.pipe.ts delete mode 100644 web/projects/ui/src/app/modals/app-config/app-config.module.ts delete mode 100644 web/projects/ui/src/app/modals/app-config/app-config.page.html delete mode 100644 web/projects/ui/src/app/modals/app-config/app-config.page.scss delete mode 100644 web/projects/ui/src/app/modals/app-config/app-config.page.ts create mode 100644 web/projects/ui/src/app/modals/config-dep.component.ts create mode 100644 web/projects/ui/src/app/modals/config.component.ts delete mode 100644 web/projects/ui/src/app/modals/enum-list/enum-list.module.ts delete mode 100644 web/projects/ui/src/app/modals/enum-list/enum-list.page.html delete mode 100644 web/projects/ui/src/app/modals/enum-list/enum-list.page.scss delete mode 100644 web/projects/ui/src/app/modals/enum-list/enum-list.page.ts delete mode 100644 web/projects/ui/src/app/modals/generic-form/generic-form.module.ts delete mode 100644 web/projects/ui/src/app/modals/generic-form/generic-form.page.html delete mode 100644 web/projects/ui/src/app/modals/generic-form/generic-form.page.scss delete mode 100644 web/projects/ui/src/app/modals/generic-form/generic-form.page.ts delete mode 100644 web/projects/ui/src/app/modals/generic-input/generic-input.component.html delete mode 100644 web/projects/ui/src/app/modals/generic-input/generic-input.component.module.ts delete mode 100644 web/projects/ui/src/app/modals/generic-input/generic-input.component.scss delete mode 100644 web/projects/ui/src/app/modals/generic-input/generic-input.component.ts delete mode 100644 web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.module.ts create mode 100644 web/projects/ui/src/app/modals/marketplace-settings/registry.component.ts create mode 100644 web/projects/ui/src/app/modals/marketplace-settings/store-icon.component.ts create mode 100644 web/projects/ui/src/app/modals/prompt.component.ts delete mode 100644 web/projects/ui/src/app/pkg-config/config-types.ts delete mode 100644 web/projects/ui/src/app/pkg-config/config-utilities.ts create mode 100644 web/projects/ui/src/app/services/form-dialog.service.ts delete mode 100644 web/projects/ui/src/app/services/modal.service.ts create mode 100644 web/projects/ui/src/app/util/configBuilderToSpec.ts diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index 127fb0e09..3a8d4f4b2 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -42,6 +42,12 @@ import { } from "@start9labs/start-sdk/cjs/lib/interfaces/Host" import { ServiceInterfaceBuilder } from "@start9labs/start-sdk/cjs/lib/interfaces/ServiceInterfaceBuilder" import { Effects } from "../../../Models/Effects" +import { + OldConfigSpec, + matchOldConfigSpec, + transformConfigSpec, + transformOldConfigToNew, +} from "./transformConfigSpec" type Optional = A | undefined | null function todo(): never { @@ -533,7 +539,9 @@ export class SystemForEmbassy implements System { effects: Effects, timeoutMs: number | null, ): Promise { - return this.getConfigUncleaned(effects, timeoutMs).then(removePointers) + return this.getConfigUncleaned(effects, timeoutMs) + .then(removePointers) + .then(convertToNewConfig) } private async getConfigUncleaned( effects: Effects, @@ -1054,3 +1062,10 @@ function extractServiceInterfaceId(manifest: Manifest, specInterface: string) { const serviceInterfaceId = `${specInterface}-${internalPort}` return serviceInterfaceId } +async function convertToNewConfig(value: T.ConfigRes): Promise { + const valueSpec: OldConfigSpec = matchOldConfigSpec.unsafeCast(value.spec) + const spec = transformConfigSpec(valueSpec) + if (!value.config) return { spec, config: null } + const config = transformOldConfigToNew(valueSpec, value.config) + return { spec, config } +} diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts new file mode 100644 index 000000000..cb7809903 --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts @@ -0,0 +1,550 @@ +import { CT } from "@start9labs/start-sdk" +import { + dictionary, + object, + anyOf, + string, + literals, + array, + number, + boolean, + Parser, + deferred, + every, + nill, +} from "ts-matches" + +export function transformConfigSpec(oldSpec: OldConfigSpec): CT.InputSpec { + return Object.entries(oldSpec).reduce((inputSpec, [key, oldVal]) => { + let newVal: CT.ValueSpec + + if (oldVal.type === "boolean") { + newVal = { + type: "toggle", + name: oldVal.name, + default: oldVal.default, + description: oldVal.description || null, + warning: oldVal.warning || null, + disabled: false, + immutable: false, + } + } else if (oldVal.type === "enum") { + newVal = { + type: "select", + name: oldVal.name, + description: oldVal.description || null, + warning: oldVal.warning || null, + default: oldVal.default, + values: oldVal.values.reduce( + (obj, curr) => ({ + ...obj, + [curr]: oldVal["value-names"][curr], + }), + {}, + ), + required: false, + disabled: false, + immutable: false, + } + } else if (oldVal.type === "list") { + newVal = getListSpec(oldVal) + } else if (oldVal.type === "number") { + const range = Range.from(oldVal.range) + + newVal = { + type: "number", + name: oldVal.name, + default: oldVal.default || null, + description: oldVal.description || null, + warning: oldVal.warning || null, + disabled: false, + immutable: false, + required: !oldVal.nullable, + min: range.min + ? range.minInclusive + ? range.min + : range.min + 1 + : null, + max: range.max + ? range.maxInclusive + ? range.max + : range.max - 1 + : null, + integer: oldVal.integral, + step: null, + units: oldVal.units || null, + placeholder: oldVal.placeholder || null, + } + } else if (oldVal.type === "object") { + newVal = { + type: "object", + name: oldVal.name, + description: oldVal.description || null, + warning: oldVal.warning || null, + spec: transformConfigSpec(matchOldConfigSpec.unsafeCast(oldVal.spec)), + } + } else if (oldVal.type === "string") { + newVal = { + type: "text", + name: oldVal.name, + default: oldVal.default || null, + description: oldVal.description || null, + warning: oldVal.warning || null, + disabled: false, + immutable: false, + required: !oldVal.nullable, + patterns: + oldVal.pattern && oldVal["pattern-description"] + ? [ + { + regex: oldVal.pattern, + description: oldVal["pattern-description"], + }, + ] + : [], + minLength: null, + maxLength: null, + masked: oldVal.masked, + generate: null, + inputmode: "text", + placeholder: oldVal.placeholder || null, + } + } else { + newVal = { + type: "union", + name: oldVal.tag.name, + description: oldVal.tag.description || null, + warning: oldVal.tag.warning || null, + variants: Object.entries(oldVal.variants).reduce( + (obj, [id, spec]) => ({ + ...obj, + [id]: { + name: oldVal.tag["variant-names"][id], + spec: transformConfigSpec(matchOldConfigSpec.unsafeCast(spec)), + }, + }), + {} as Record, + ), + disabled: false, + required: true, + default: oldVal.default, + immutable: false, + } + } + + return { + ...inputSpec, + [key]: newVal, + } + }, {} as CT.InputSpec) +} + +export function transformOldConfigToNew( + spec: OldConfigSpec, + config: Record, +): Record { + return Object.entries(spec).reduce((obj, [key, val]) => { + let newVal = config[key] + + if (isObject(val)) { + newVal = transformOldConfigToNew( + matchOldConfigSpec.unsafeCast(val.spec), + config[key], + ) + } + + if (isUnion(val)) { + const selection = config[key][val.tag.id] + delete config[key][val.tag.id] + + newVal = { + selection, + value: transformOldConfigToNew( + matchOldConfigSpec.unsafeCast(val.variants[selection]), + config[key], + ), + } + } + + if (isList(val) && isObjectList(val)) { + newVal = (config[key] as object[]).map((obj) => + transformOldConfigToNew( + matchOldConfigSpec.unsafeCast(val.spec.spec), + obj, + ), + ) + } + + return { + ...obj, + [key]: newVal, + } + }, {}) +} + +export function transformNewConfigToOld( + spec: OldConfigSpec, + config: Record, +): Record { + return Object.entries(spec).reduce((obj, [key, val]) => { + let newVal = config[key] + + if (isObject(val)) { + newVal = transformNewConfigToOld( + matchOldConfigSpec.unsafeCast(val.spec), + config[key], + ) + } + + if (isUnion(val)) { + newVal = { + [val.tag.id]: config[key].selection, + ...transformNewConfigToOld( + matchOldConfigSpec.unsafeCast(val.variants[config[key].selection]), + config[key].unionSelectValue, + ), + } + } + + if (isList(val) && isObjectList(val)) { + newVal = (config[key] as object[]).map((obj) => + transformNewConfigToOld( + matchOldConfigSpec.unsafeCast(val.spec.spec), + obj, + ), + ) + } + + return { + ...obj, + [key]: newVal, + } + }, {}) +} + +function getListSpec( + oldVal: OldValueSpecList, +): CT.ValueSpecMultiselect | CT.ValueSpecList { + const range = Range.from(oldVal.range) + + let partial: Omit = { + name: oldVal.name, + description: oldVal.description || null, + warning: oldVal.warning || null, + minLength: range.min + ? range.minInclusive + ? range.min + : range.min + 1 + : null, + maxLength: range.max + ? range.maxInclusive + ? range.max + : range.max - 1 + : null, + disabled: false, + } + + if (isEnumList(oldVal)) { + return { + ...partial, + type: "multiselect", + default: oldVal.default as string[], + immutable: false, + values: oldVal.spec.values.reduce( + (obj, curr) => ({ + ...obj, + [curr]: oldVal.spec["value-names"][curr], + }), + {}, + ), + } + } else if (isStringList(oldVal)) { + return { + ...partial, + type: "list", + default: oldVal.default as string[], + spec: { + type: "text", + patterns: + oldVal.spec.pattern && oldVal.spec["pattern-description"] + ? [ + { + regex: oldVal.spec.pattern, + description: oldVal.spec["pattern-description"], + }, + ] + : [], + minLength: null, + maxLength: null, + masked: oldVal.spec.masked, + generate: null, + inputmode: "text", + placeholder: oldVal.spec.placeholder || null, + }, + } + } else if (isObjectList(oldVal)) { + return { + ...partial, + type: "list", + default: oldVal.default as Record[], + spec: { + type: "object", + spec: transformConfigSpec( + matchOldConfigSpec.unsafeCast(oldVal.spec.spec), + ), + uniqueBy: oldVal.spec["unique-by"], + displayAs: oldVal.spec["display-as"] || null, + }, + } + } else { + throw new Error("Invalid list subtype. enum, string, and object permitted.") + } +} + +function isObject(val: OldValueSpec): val is OldValueSpecObject { + return val.type === "object" +} + +function isUnion(val: OldValueSpec): val is OldValueSpecUnion { + return val.type === "union" +} + +function isList(val: OldValueSpec): val is OldValueSpecList { + return val.type === "list" +} + +function isEnumList( + val: OldValueSpecList, +): val is OldValueSpecList & { subtype: "enum" } { + return val.subtype === "enum" +} + +function isStringList( + val: OldValueSpecList, +): val is OldValueSpecList & { subtype: "string" } { + return val.subtype === "string" +} + +function isObjectList( + val: OldValueSpecList, +): val is OldValueSpecList & { subtype: "object" } { + if (["number", "union"].includes(val.subtype)) { + throw new Error("Invalid list subtype. enum, string, and object permitted.") + } + return val.subtype === "object" +} +export type OldConfigSpec = Record +const [_matchOldConfigSpec, setMatchOldConfigSpec] = deferred() +export const matchOldConfigSpec = _matchOldConfigSpec as Parser< + unknown, + OldConfigSpec +> +export const matchOldDefaultString = anyOf( + string, + object({ charset: string, len: number }), +) +type OldDefaultString = typeof matchOldDefaultString._TYPE + +export const matchOldValueSpecString = object( + { + masked: boolean, + copyable: boolean, + type: literals("string"), + nullable: boolean, + name: string, + placeholder: string, + pattern: string, + "pattern-description": string, + default: matchOldDefaultString, + textarea: boolean, + description: string, + warning: string, + }, + [ + "placeholder", + "pattern", + "pattern-description", + "default", + "textarea", + "description", + "warning", + ], +) + +export const matchOldValueSpecNumber = object( + { + type: literals("number"), + nullable: boolean, + name: string, + range: string, + integral: boolean, + default: number, + description: string, + warning: string, + units: string, + placeholder: string, + }, + ["default", "description", "warning", "units", "placeholder"], +) +type OldValueSpecNumber = typeof matchOldValueSpecNumber._TYPE + +export const matchOldValueSpecBoolean = object( + { + type: literals("boolean"), + default: boolean, + name: string, + description: string, + warning: string, + }, + ["description", "warning"], +) +type OldValueSpecBoolean = typeof matchOldValueSpecBoolean._TYPE + +const matchOldValueSpecObject = object( + { + type: literals("object"), + spec: _matchOldConfigSpec, + name: string, + description: string, + warning: string, + }, + ["description", "warning"], +) +type OldValueSpecObject = typeof matchOldValueSpecObject._TYPE + +const matchOldValueSpecEnum = object( + { + values: array(string), + "value-names": dictionary([string, string]), + type: literals("enum"), + default: string, + name: string, + description: string, + warning: string, + }, + ["description", "warning"], +) +type OldValueSpecEnum = typeof matchOldValueSpecEnum._TYPE + +const matchOldUnionTagSpec = object( + { + id: string, // The name of the field containing one of the union variants + "variant-names": dictionary([string, string]), // The name of each variant + name: string, + description: string, + warning: string, + }, + ["description", "warning"], +) +const matchOldValueSpecUnion = object({ + type: literals("union"), + tag: matchOldUnionTagSpec, + variants: dictionary([string, _matchOldConfigSpec]), + default: string, +}) +type OldValueSpecUnion = typeof matchOldValueSpecUnion._TYPE + +const [matchOldUniqueBy, setOldUniqueBy] = deferred() +type OldUniqueBy = + | null + | string + | { any: OldUniqueBy[] } + | { all: OldUniqueBy[] } + +setOldUniqueBy( + anyOf( + nill, + string, + object({ any: array(matchOldUniqueBy) }), + object({ all: array(matchOldUniqueBy) }), + ), +) + +const matchOldListValueSpecObject = object( + { + spec: _matchOldConfigSpec, // this is a mapped type of the config object at this level, replacing the object's values with specs on those values + "unique-by": matchOldUniqueBy, // indicates whether duplicates can be permitted in the list + "display-as": string, // this should be a handlebars template which can make use of the entire config which corresponds to 'spec' + }, + ["display-as"], +) +const matchOldListValueSpecString = object( + { + masked: boolean, + copyable: boolean, + pattern: string, + "pattern-description": string, + placeholder: string, + }, + ["pattern", "pattern-description", "placeholder"], +) + +const matchOldListValueSpecEnum = object({ + values: array(string), + "value-names": dictionary([string, string]), +}) + +// represents a spec for a list +const matchOldValueSpecList = every( + object( + { + type: literals("list"), + range: string, // '[0,1]' (inclusive) OR '[0,*)' (right unbounded), normal math rules + default: anyOf( + array(string), + array(number), + array(matchOldDefaultString), + array(object), + ), + name: string, + description: string, + warning: string, + }, + ["description", "warning"], + ), + anyOf( + object({ + subtype: literals("string"), + spec: matchOldListValueSpecString, + }), + object({ + subtype: literals("enum"), + spec: matchOldListValueSpecEnum, + }), + object({ + subtype: literals("object"), + spec: matchOldListValueSpecObject, + }), + ), +) +type OldValueSpecList = typeof matchOldValueSpecList._TYPE + +export const matchOldValueSpec = anyOf( + matchOldValueSpecString, + matchOldValueSpecNumber, + matchOldValueSpecBoolean, + matchOldValueSpecObject, + matchOldValueSpecEnum, + matchOldValueSpecList, + matchOldValueSpecUnion, +) +type OldValueSpec = typeof matchOldValueSpec._TYPE + +setMatchOldConfigSpec(dictionary([string, matchOldValueSpec])) + +export class Range { + min?: number + max?: number + minInclusive!: boolean + maxInclusive!: boolean + + static from(s: string = "(*,*)"): Range { + const r = new Range() + r.minInclusive = s.startsWith("[") + r.maxInclusive = s.endsWith("]") + const [minStr, maxStr] = s.split(",").map((a) => a.trim()) + r.min = minStr === "(*" ? undefined : Number(minStr.slice(1)) + r.max = maxStr === "*)" ? undefined : Number(maxStr.slice(0, -1)) + return r + } +} diff --git a/sdk/lib/StartSdk.ts b/sdk/lib/StartSdk.ts index 4208928c9..fac78a4af 100644 --- a/sdk/lib/StartSdk.ts +++ b/sdk/lib/StartSdk.ts @@ -480,7 +480,6 @@ export class StartSdk { }, List: { text: List.text, - number: List.number, obj: >( a: { name: string @@ -523,29 +522,6 @@ export class StartSdk { } >, ) => List.dynamicText(getA), - dynamicNumber: ( - getA: LazyBuild< - Store, - { - name: string - description?: string | null - warning?: string | null - /** Default = [] */ - default?: string[] - minLength?: number | null - maxLength?: number | null - disabled?: false | string - spec: { - integer: boolean - min?: number | null - max?: number | null - step?: number | null - units?: string | null - placeholder?: string | null - } - } - >, - ) => List.dynamicNumber(getA), }, Migration: { of: (options: { diff --git a/sdk/lib/config/builder/list.ts b/sdk/lib/config/builder/list.ts index 8a251069d..f230b8608 100644 --- a/sdk/lib/config/builder/list.ts +++ b/sdk/lib/config/builder/list.ts @@ -125,96 +125,6 @@ export class List { return built }, arrayOf(string)) } - static number( - a: { - name: string - description?: string | null - warning?: string | null - /** Default = [] */ - default?: string[] - minLength?: number | null - maxLength?: number | null - }, - aSpec: { - integer: boolean - min?: number | null - max?: number | null - step?: number | null - units?: string | null - placeholder?: string | null - }, - ) { - return new List(() => { - const spec = { - type: "number" as const, - placeholder: null, - min: null, - max: null, - step: null, - units: null, - ...aSpec, - } - const built: ValueSpecListOf<"number"> = { - description: null, - warning: null, - minLength: null, - maxLength: null, - default: [], - type: "list" as const, - disabled: false, - ...a, - spec, - } - return built - }, arrayOf(number)) - } - static dynamicNumber( - getA: LazyBuild< - Store, - { - name: string - description?: string | null - warning?: string | null - /** Default = [] */ - default?: string[] - minLength?: number | null - maxLength?: number | null - disabled?: false | string - spec: { - integer: boolean - min?: number | null - max?: number | null - step?: number | null - units?: string | null - placeholder?: string | null - } - } - >, - ) { - return new List(async (options) => { - const { spec: aSpec, ...a } = await getA(options) - const spec = { - type: "number" as const, - placeholder: null, - min: null, - max: null, - step: null, - units: null, - ...aSpec, - } - return { - description: null, - warning: null, - minLength: null, - maxLength: null, - default: [], - type: "list" as const, - disabled: false, - ...a, - spec, - } - }, arrayOf(number)) - } static obj, Store>( a: { name: string diff --git a/sdk/lib/config/builder/variants.ts b/sdk/lib/config/builder/variants.ts index 1e7a2a384..352f16828 100644 --- a/sdk/lib/config/builder/variants.ts +++ b/sdk/lib/config/builder/variants.ts @@ -69,8 +69,8 @@ export class Variants { const validator = anyOf( ...Object.entries(a).map(([name, { spec }]) => object({ - unionSelectKey: literals(name), - unionValueKey: spec.validator, + selection: literals(name), + value: spec.validator, }), ), ) as Parser @@ -78,9 +78,9 @@ export class Variants { return new Variants< { [K in keyof VariantValues]: { - unionSelectKey: K + selection: K // prettier-ignore - unionValueKey: + value: VariantValues[K]["spec"] extends (Config | Config) ? B : never } diff --git a/sdk/lib/config/configTypes.ts b/sdk/lib/config/configTypes.ts index 14e0e1d1d..14d857433 100644 --- a/sdk/lib/config/configTypes.ts +++ b/sdk/lib/config/configTypes.ts @@ -15,70 +15,93 @@ export type ValueType = export type ValueSpec = ValueSpecOf /** core spec types. These types provide the metadata for performing validations */ // prettier-ignore -export type ValueSpecOf = T extends "text" - ? ValueSpecText - : T extends "textarea" - ? ValueSpecTextarea - : T extends "number" - ? ValueSpecNumber - : T extends "color" - ? ValueSpecColor - : T extends "datetime" - ? ValueSpecDatetime - : T extends "toggle" - ? ValueSpecToggle - : T extends "select" - ? ValueSpecSelect - : T extends "multiselect" - ? ValueSpecMultiselect - : T extends "list" - ? ValueSpecList - : T extends "object" - ? ValueSpecObject - : T extends "file" - ? ValueSpecFile - : T extends "union" - ? ValueSpecUnion - : never +export type ValueSpecOf = + T extends "text" ? ValueSpecText : + T extends "textarea" ? ValueSpecTextarea : + T extends "number" ? ValueSpecNumber : + T extends "color" ? ValueSpecColor : + T extends "datetime" ? ValueSpecDatetime : + T extends "toggle" ? ValueSpecToggle : + T extends "select" ? ValueSpecSelect : + T extends "multiselect" ? ValueSpecMultiselect : + T extends "list" ? ValueSpecList : + T extends "object" ? ValueSpecObject : + T extends "file" ? ValueSpecFile : + T extends "union" ? ValueSpecUnion : + never + +export type ValueSpecText = { + name: string + description: string | null + warning: string | null + + type: "text" + patterns: Pattern[] + minLength: number | null + maxLength: number | null + masked: boolean + + inputmode: "text" | "email" | "tel" | "url" + placeholder: string | null -export interface ValueSpecText extends ListValueSpecText, WithStandalone { required: boolean default: DefaultString | null disabled: false | string generate: null | RandomString - /** Immutable means it can only be configed at the first config then never again */ + /** Immutable means it can only be configured at the first config then never again */ immutable: boolean } -export interface ValueSpecTextarea extends WithStandalone { +export type ValueSpecTextarea = { + name: string + description: string | null + warning: string | null + type: "textarea" placeholder: string | null minLength: number | null maxLength: number | null required: boolean disabled: false | string - /** Immutable means it can only be configed at the first config then never again */ + /** Immutable means it can only be configured at the first config then never again */ immutable: boolean } export type FilePath = { filePath: string } -export interface ValueSpecNumber extends ListValueSpecNumber, WithStandalone { +export type ValueSpecNumber = { + type: "number" + min: number | null + max: number | null + integer: boolean + step: number | null + units: string | null + placeholder: string | null + name: string + description: string | null + warning: string | null required: boolean default: number | null disabled: false | string - /** Immutable means it can only be configed at the first config then never again */ + /** Immutable means it can only be configured at the first config then never again */ immutable: boolean } -export interface ValueSpecColor extends WithStandalone { +export type ValueSpecColor = { + name: string + description: string | null + warning: string | null + type: "color" required: boolean default: string | null disabled: false | string - /** Immutable means it can only be configed at the first config then never again */ + /** Immutable means it can only be configured at the first config then never again */ immutable: boolean } -export interface ValueSpecDatetime extends WithStandalone { +export type ValueSpecDatetime = { + name: string + description: string | null + warning: string | null type: "datetime" required: boolean inputmode: "date" | "time" | "datetime-local" @@ -86,10 +109,14 @@ export interface ValueSpecDatetime extends WithStandalone { max: string | null default: string | null disabled: false | string - /** Immutable means it can only be configed at the first config then never again */ + /** Immutable means it can only be configured at the first config then never again */ immutable: boolean } -export interface ValueSpecSelect extends SelectBase, WithStandalone { +export type ValueSpecSelect = { + values: Record + name: string + description: string | null + warning: string | null type: "select" required: boolean default: string | null @@ -99,10 +126,16 @@ export interface ValueSpecSelect extends SelectBase, WithStandalone { * string[] means that the options are disabled */ disabled: false | string | string[] - /** Immutable means it can only be configed at the first config then never again */ + /** Immutable means it can only be configured at the first config then never again */ immutable: boolean } -export interface ValueSpecMultiselect extends SelectBase, WithStandalone { +export type ValueSpecMultiselect = { + values: Record + + name: string + description: string | null + warning: string | null + type: "multiselect" minLength: number | null maxLength: number | null @@ -113,17 +146,25 @@ export interface ValueSpecMultiselect extends SelectBase, WithStandalone { */ disabled: false | string | string[] default: string[] - /** Immutable means it can only be configed at the first config then never again */ + /** Immutable means it can only be configured at the first config then never again */ immutable: boolean } -export interface ValueSpecToggle extends WithStandalone { +export type ValueSpecToggle = { + name: string + description: string | null + warning: string | null + type: "toggle" default: boolean | null disabled: false | string - /** Immutable means it can only be configed at the first config then never again */ + /** Immutable means it can only be configured at the first config then never again */ immutable: boolean } -export interface ValueSpecUnion extends WithStandalone { +export type ValueSpecUnion = { + name: string + description: string | null + warning: string | null + type: "union" variants: Record< string, @@ -140,39 +181,37 @@ export interface ValueSpecUnion extends WithStandalone { disabled: false | string | string[] required: boolean default: string | null - /** Immutable means it can only be configed at the first config then never again */ + /** Immutable means it can only be configured at the first config then never again */ immutable: boolean } -export interface ValueSpecFile extends WithStandalone { +export type ValueSpecFile = { + name: string + description: string | null + warning: string | null type: "file" extensions: string[] required: boolean } -export interface ValueSpecObject extends WithStandalone { - type: "object" - spec: InputSpec -} -export interface WithStandalone { +export type ValueSpecObject = { name: string description: string | null warning: string | null + type: "object" + spec: InputSpec } -export interface SelectBase { - values: Record -} -export type ListValueSpecType = "text" | "number" | "object" +export type ListValueSpecType = "text" | "object" /** represents a spec for the values of a list */ -export type ListValueSpecOf = T extends "text" - ? ListValueSpecText - : T extends "number" - ? ListValueSpecNumber - : T extends "object" - ? ListValueSpecObject - : never +// prettier-ignore +export type ListValueSpecOf = + T extends "text" ? ListValueSpecText : + T extends "object" ? ListValueSpecObject : + never /** represents a spec for a list */ export type ValueSpecList = ValueSpecListOf -export interface ValueSpecListOf - extends WithStandalone { +export type ValueSpecListOf = { + name: string + description: string | null + warning: string | null type: "list" spec: ListValueSpecOf minLength: number | null @@ -180,19 +219,17 @@ export interface ValueSpecListOf disabled: false | string default: | string[] - | number[] | DefaultString[] | Record[] | readonly string[] - | readonly number[] | readonly DefaultString[] | readonly Record[] } -export interface Pattern { +export type Pattern = { regex: string description: string } -export interface ListValueSpecText { +export type ListValueSpecText = { type: "text" patterns: Pattern[] minLength: number | null @@ -203,16 +240,8 @@ export interface ListValueSpecText { inputmode: "text" | "email" | "tel" | "url" placeholder: string | null } -export interface ListValueSpecNumber { - type: "number" - min: number | null - max: number | null - integer: boolean - step: number | null - units: string | null - placeholder: string | null -} -export interface ListValueSpecObject { + +export type ListValueSpecObject = { type: "object" /** this is a mapped type of the config object at this level, replacing the object's values with specs on those values */ spec: InputSpec @@ -242,8 +271,3 @@ export function isValueSpecListOf( ): t is ValueSpecListOf & { spec: ListValueSpecOf } { return "spec" in t && t.spec.type === s } -export const unionSelectKey = "unionSelectKey" as const -export type UnionSelectKey = typeof unionSelectKey - -export const unionValueKey = "unionValueKey" as const -export type UnionValueKey = typeof unionValueKey diff --git a/sdk/lib/index.browser.ts b/sdk/lib/index.browser.ts index 075bfcf50..c7ab45e60 100644 --- a/sdk/lib/index.browser.ts +++ b/sdk/lib/index.browser.ts @@ -12,4 +12,4 @@ export * as T from "./types" export * as yaml from "yaml" export * as matches from "ts-matches" -export * as util from "./util/index.browser" +export * as utils from "./util/index.browser" diff --git a/sdk/lib/osBindings/InstalledVersionParams.ts b/sdk/lib/osBindings/InstalledVersionParams.ts new file mode 100644 index 000000000..637f9b6ce --- /dev/null +++ b/sdk/lib/osBindings/InstalledVersionParams.ts @@ -0,0 +1,4 @@ +// 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" + +export type InstalledVersionParams = { id: PackageId } diff --git a/sdk/lib/osBindings/index.ts b/sdk/lib/osBindings/index.ts index 22b03a7ca..32e57956a 100644 --- a/sdk/lib/osBindings/index.ts +++ b/sdk/lib/osBindings/index.ts @@ -79,6 +79,7 @@ export { ImageMetadata } from "./ImageMetadata" export { ImageSource } from "./ImageSource" export { InitProgressRes } from "./InitProgressRes" export { InstalledState } from "./InstalledState" +export { InstalledVersionParams } from "./InstalledVersionParams" export { InstallingInfo } from "./InstallingInfo" export { InstallingState } from "./InstallingState" export { IpHostname } from "./IpHostname" diff --git a/sdk/lib/test/configBuilder.test.ts b/sdk/lib/test/configBuilder.test.ts index 9738475a6..c5003c55b 100644 --- a/sdk/lib/test/configBuilder.test.ts +++ b/sdk/lib/test/configBuilder.test.ts @@ -268,26 +268,9 @@ describe("values", () => { }), ) const validator = value.validator - validator.unsafeCast({ unionSelectKey: "a", unionValueKey: { b: false } }) + validator.unsafeCast({ selection: "a", value: { b: false } }) type Test = typeof validator._TYPE - testOutput()( - null, - ) - }) - test("list", async () => { - const value = Value.list( - List.number( - { - name: "test", - }, - { - integer: false, - }, - ), - ) - const validator = value.validator - validator.unsafeCast([1, 2, 3]) - testOutput()(null) + testOutput()(null) }) describe("dynamic", () => { @@ -577,12 +560,12 @@ describe("values", () => { }), ) const validator = value.validator - validator.unsafeCast({ unionSelectKey: "a", unionValueKey: { b: false } }) + validator.unsafeCast({ selection: "a", value: { b: false } }) type Test = typeof validator._TYPE testOutput< Test, - | { unionSelectKey: "a"; unionValueKey: { b: boolean } } - | { unionSelectKey: "b"; unionValueKey: { b: boolean } } + | { selection: "a"; value: { b: boolean } } + | { selection: "b"; value: { b: boolean } } >()(null) const built = await value.build({} as any) @@ -644,12 +627,12 @@ describe("values", () => { }), ) const validator = value.validator - validator.unsafeCast({ unionSelectKey: "a", unionValueKey: { b: false } }) + validator.unsafeCast({ selection: "a", value: { b: false } }) type Test = typeof validator._TYPE testOutput< Test, - | { unionSelectKey: "a"; unionValueKey: { b: boolean } } - | { unionSelectKey: "b"; unionValueKey: { b: boolean } } + | { selection: "a"; value: { b: boolean } } + | { selection: "b"; value: { b: boolean } } | null | undefined >()(null) @@ -736,24 +719,6 @@ describe("Builder List", () => { }) }) }) - test("number", async () => { - const value = Value.list( - List.dynamicNumber(() => ({ - name: "test", - spec: { integer: true }, - })), - ) - const validator = value.validator - expect(() => validator.unsafeCast(["test", "text"])).toThrowError() - validator.unsafeCast([4, 2]) - expect(() => validator.unsafeCast(null)).toThrowError() - validator.unsafeCast([]) - testOutput()(null) - expect(await value.build({} as any)).toMatchObject({ - name: "test", - spec: { integer: true }, - }) - }) }) describe("Nested nullable values", () => { diff --git a/sdk/lib/test/configTypes.test.ts b/sdk/lib/test/configTypes.test.ts index 7e3ff5ca6..6d0b9a3d8 100644 --- a/sdk/lib/test/configTypes.test.ts +++ b/sdk/lib/test/configTypes.test.ts @@ -1,15 +1,11 @@ -import { - ListValueSpecOf, - ValueSpec, - isValueSpecListOf, -} from "../config/configTypes" +import { ListValueSpecOf, isValueSpecListOf } from "../config/configTypes" import { Config } from "../config/builder/config" import { List } from "../config/builder/list" import { Value } from "../config/builder/value" describe("Config Types", () => { test("isValueSpecListOf", async () => { - const options = [List.obj, List.text, List.number] + const options = [List.obj, List.text] for (const option of options) { const test = (option as any)( {} as any, @@ -18,8 +14,6 @@ describe("Config Types", () => { const someList = await Value.list(test).build({} as any) if (isValueSpecListOf(someList, "text")) { someList.spec satisfies ListValueSpecOf<"text"> - } else if (isValueSpecListOf(someList, "number")) { - someList.spec satisfies ListValueSpecOf<"number"> } else if (isValueSpecListOf(someList, "object")) { someList.spec satisfies ListValueSpecOf<"object"> } else { diff --git a/sdk/lib/test/output.test.ts b/sdk/lib/test/output.test.ts index 2b3afb5de..1df84f3af 100644 --- a/sdk/lib/test/output.test.ts +++ b/sdk/lib/test/output.test.ts @@ -1,9 +1,3 @@ -import { - UnionSelectKey, - unionSelectKey, - UnionValueKey, - unionValueKey, -} from "../config/configTypes" import { ConfigSpec, matchConfigSpec } from "./output" import * as _I from "../index" import { camelCase } from "../../scripts/oldSpecToBuilder" @@ -30,13 +24,10 @@ testOutput< ConfigSpec["advanced"]["peers"]["addnode"][0]["hostname"], string | null | undefined >()(null) -testOutput< - ConfigSpec["testListUnion"][0]["union"][UnionValueKey]["name"], - string ->()(null) -testOutput()( +testOutput()( null, ) +testOutput()(null) testOutput>()( null, ) @@ -45,7 +36,7 @@ testOutput>()( testOutput()(null) // prettier-ignore // @ts-expect-error Expect that the string is the one above -testOutput()(null); +testOutput()(null); /// Here we test the output of the matchConfigSpec function describe("Inputs", () => { @@ -53,7 +44,7 @@ describe("Inputs", () => { mediasources: ["filebrowser"], testListUnion: [ { - union: { [unionSelectKey]: "lnd", [unionValueKey]: { name: "string" } }, + union: { selection: "lnd", value: { name: "string" } }, }, ], rpc: { @@ -92,8 +83,8 @@ describe("Inputs", () => { }, dbcache: 5, pruning: { - unionSelectKey: "disabled", - unionValueKey: {}, + selection: "disabled", + value: {}, }, blockfilters: { blockfilterindex: false, diff --git a/sdk/lib/util/getRandomCharInSet.ts b/sdk/lib/util/getRandomCharInSet.ts index b26eef648..7914209a3 100644 --- a/sdk/lib/util/getRandomCharInSet.ts +++ b/sdk/lib/util/getRandomCharInSet.ts @@ -1,9 +1,10 @@ // a,g,h,A-Z,,,,- -import * as crypto from "crypto" export function getRandomCharInSet(charset: string): string { const set = stringToCharSet(charset) - let charIdx = crypto.randomInt(0, set.len) + let charIdx = Math.floor( + (crypto.getRandomValues(new Uint32Array(1))[0] / 2 ** 32) * set.len, + ) for (let range of set.ranges) { if (range.len > charIdx) { return String.fromCharCode(range.start.charCodeAt(0) + charIdx) diff --git a/sdk/lib/util/getServiceInterface.ts b/sdk/lib/util/getServiceInterface.ts index 737cbbc16..a2f17be10 100644 --- a/sdk/lib/util/getServiceInterface.ts +++ b/sdk/lib/util/getServiceInterface.ts @@ -80,7 +80,7 @@ export const addressHostToUrl = ( ): UrlString[] => { const res = [] const fmt = (scheme: string | null, host: HostnameInfo, port: number) => { - const includePort = + const excludePort = scheme && scheme in knownProtocols && port === knownProtocols[scheme as keyof typeof knownProtocols].defaultPort @@ -96,7 +96,7 @@ export const addressHostToUrl = ( } return `${scheme ? `${scheme}://` : ""}${ username ? `${username}@` : "" - }${hostname}${includePort ? `:${port}` : ""}${suffix}` + }${hostname}${excludePort ? "" : `:${port}`}${suffix}` } if (host.hostname.sslPort !== null) { res.push(fmt(sslScheme, host, host.hostname.sslPort)) diff --git a/sdk/lib/util/index.browser.ts b/sdk/lib/util/index.browser.ts index 6ff7ed01c..94339e7f7 100644 --- a/sdk/lib/util/index.browser.ts +++ b/sdk/lib/util/index.browser.ts @@ -1,25 +1,10 @@ import * as T from "../types" +/// Currently being used +export { addressHostToUrl } from "./getServiceInterface" +export { getDefaultString } from "./getDefaultString" + +/// Not being used, but known to be browser compatible export { GetServiceInterface, getServiceInterface } from "./getServiceInterface" export { getServiceInterfaces } from "./getServiceInterfaces" -// prettier-ignore -export type FlattenIntersection = -T extends ArrayLike ? T : -T extends object ? {} & {[P in keyof T]: T[P]} : - T; - -export type _ = FlattenIntersection - -export const isKnownError = (e: unknown): e is T.KnownError => - e instanceof Object && ("error" in e || "error-code" in e) - -declare const affine: unique symbol - -export type Affine = { [affine]: A } - -type NeverPossible = { [affine]: string } -export type NoAny = NeverPossible extends A - ? keyof NeverPossible extends keyof A - ? never - : A - : A +export * from "./typeHelpers" diff --git a/sdk/lib/util/index.ts b/sdk/lib/util/index.ts index 9b19aeb38..63629dfaa 100644 --- a/sdk/lib/util/index.ts +++ b/sdk/lib/util/index.ts @@ -1,5 +1,3 @@ -import * as T from "../types" - import "./nullIfEmpty" import "./fileHelper" import "../store/getStore" @@ -12,26 +10,5 @@ export { GetServiceInterface, getServiceInterface } from "./getServiceInterface" export { getServiceInterfaces } from "./getServiceInterfaces" export { addressHostToUrl } from "./getServiceInterface" export { hostnameInfoToAddress } from "./Hostname" -// prettier-ignore -export type FlattenIntersection = -T extends ArrayLike ? T : -T extends object ? {} & {[P in keyof T]: T[P]} : - T; - -export type _ = FlattenIntersection - -export const isKnownError = (e: unknown): e is T.KnownError => - e instanceof Object && ("error" in e || "error-code" in e) - -declare const affine: unique symbol - -export type Affine = { [affine]: A } - -type NeverPossible = { [affine]: string } -export type NoAny = NeverPossible extends A - ? keyof NeverPossible extends keyof A - ? never - : A - : A - +export * from "./typeHelpers" export { getDefaultString } from "./getDefaultString" diff --git a/sdk/lib/util/typeHelpers.ts b/sdk/lib/util/typeHelpers.ts new file mode 100644 index 000000000..f45a46f1e --- /dev/null +++ b/sdk/lib/util/typeHelpers.ts @@ -0,0 +1,23 @@ +import * as T from "../types" + +// prettier-ignore +export type FlattenIntersection = +T extends ArrayLike ? T : +T extends object ? {} & {[P in keyof T]: T[P]} : + T; + +export type _ = FlattenIntersection + +export const isKnownError = (e: unknown): e is T.KnownError => + e instanceof Object && ("error" in e || "error-code" in e) + +declare const affine: unique symbol + +export type Affine = { [affine]: A } + +type NeverPossible = { [affine]: string } +export type NoAny = NeverPossible extends A + ? keyof NeverPossible extends keyof A + ? never + : A + : A diff --git a/sdk/scripts/oldSpecToBuilder.ts b/sdk/scripts/oldSpecToBuilder.ts index ce8ea4e5f..6dd726e1b 100644 --- a/sdk/scripts/oldSpecToBuilder.ts +++ b/sdk/scripts/oldSpecToBuilder.ts @@ -285,26 +285,26 @@ const {Config, List, Value, Variants} = sdk maxLength: null, })})` } - case "number": { - return `${rangeToTodoComment(value?.range)}List.number(${JSON.stringify( - { - name: value.name || null, - minLength: null, - maxLength: null, - default: value.default || null, - description: value.description || null, - warning: value.warning || null, - }, - null, - 2, - )}, ${JSON.stringify({ - integer: value?.spec?.integral || false, - min: null, - max: null, - units: value?.spec?.units || null, - placeholder: value?.spec?.placeholder || null, - })})` - } + // case "number": { + // return `${rangeToTodoComment(value?.range)}List.number(${JSON.stringify( + // { + // name: value.name || null, + // minLength: null, + // maxLength: null, + // default: value.default || null, + // description: value.description || null, + // warning: value.warning || null, + // }, + // null, + // 2, + // )}, ${JSON.stringify({ + // integer: value?.spec?.integral || false, + // min: null, + // max: null, + // units: value?.spec?.units || null, + // placeholder: value?.spec?.placeholder || null, + // })})` + // } case "enum": { return "/* error!! list.enum */" } diff --git a/web/package-lock.json b/web/package-lock.json index 8a48b18fa..b1f1802e0 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -20,20 +20,22 @@ "@angular/service-worker": "^14.2.2", "@ionic/angular": "^6.1.15", "@materia-ui/ngx-monaco-editor": "^6.0.0", - "@ng-web-apis/common": "^2.0.0", - "@ng-web-apis/mutation-observer": "^2.0.0", - "@ng-web-apis/resize-observer": "^2.0.0", + "@ng-web-apis/common": "^3.0.6", + "@ng-web-apis/mutation-observer": "^3.2.1", + "@ng-web-apis/resize-observer": "^3.2.1", "@noble/curves": "^1.4.0", "@noble/hashes": "^1.4.0", "@start9labs/argon2": "^0.2.2", "@start9labs/emver": "^0.1.5", "@start9labs/start-sdk": "file:../sdk/dist", - "@taiga-ui/addon-charts": "3.20.0", - "@taiga-ui/cdk": "3.20.0", - "@taiga-ui/core": "3.20.0", - "@taiga-ui/icons": "3.20.0", - "@taiga-ui/kit": "3.20.0", + "@taiga-ui/addon-charts": "3.84.0", + "@taiga-ui/cdk": "3.84.0", + "@taiga-ui/core": "3.84.0", + "@taiga-ui/experimental": "3.84.0", + "@taiga-ui/icons": "3.84.0", + "@taiga-ui/kit": "3.84.0", "@tinkoff/ng-dompurify": "4.0.0", + "@tinkoff/ng-event-plugins": "3.2.0", "angular-svg-round-progressbar": "^9.0.0", "ansi-to-html": "^0.7.2", "base64-js": "^1.5.1", @@ -111,1873 +113,7 @@ "rxjs": ">=7.0.0" } }, - "../patch-db/client/node_modules/@babel/code-frame": { - "version": "7.21.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/highlight": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "../patch-db/client/node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "../patch-db/client/node_modules/@babel/highlight": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "../patch-db/client/node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "../patch-db/client/node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "../patch-db/client/node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "../patch-db/client/node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "../patch-db/client/node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "../patch-db/client/node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "../patch-db/client/node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "../patch-db/client/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "../patch-db/client/node_modules/@tsconfig/node10": { - "version": "1.0.9", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/@tsconfig/node12": { - "version": "1.0.11", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/@tsconfig/node14": { - "version": "1.0.3", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/@tsconfig/node16": { - "version": "1.0.4", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/@types/node": { - "version": "18.15.0", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/@types/parse-json": { - "version": "4.0.0", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/@types/uuid": { - "version": "8.3.1", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/acorn": { - "version": "8.8.2", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "../patch-db/client/node_modules/acorn-walk": { - "version": "8.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "../patch-db/client/node_modules/aggregate-error": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/ansi-escapes": { - "version": "4.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../patch-db/client/node_modules/ansi-regex": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "../patch-db/client/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "../patch-db/client/node_modules/arg": { - "version": "4.1.3", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/argparse": { - "version": "1.0.10", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "../patch-db/client/node_modules/astral-regex": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/balanced-match": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/brace-expansion": { - "version": "1.1.11", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "../patch-db/client/node_modules/braces": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/builtin-modules": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../patch-db/client/node_modules/callsites": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "../patch-db/client/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "../patch-db/client/node_modules/ci-info": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/clean-stack": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "../patch-db/client/node_modules/cli-cursor": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/cli-truncate": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../patch-db/client/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "../patch-db/client/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/colorette": { - "version": "2.0.20", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/commander": { - "version": "10.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "../patch-db/client/node_modules/compare-versions": { - "version": "3.6.0", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/concat-map": { - "version": "0.0.1", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/cosmiconfig": { - "version": "7.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "../patch-db/client/node_modules/create-require": { - "version": "1.1.1", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/cross-spawn": { - "version": "7.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "../patch-db/client/node_modules/debug": { - "version": "4.3.4", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "../patch-db/client/node_modules/diff": { - "version": "4.0.2", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "../patch-db/client/node_modules/eastasianwidth": { - "version": "0.2.0", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/emoji-regex": { - "version": "9.2.2", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/error-ex": { - "version": "1.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "../patch-db/client/node_modules/escape-string-regexp": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "../patch-db/client/node_modules/esprima": { - "version": "4.0.1", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "../patch-db/client/node_modules/execa": { - "version": "7.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^4.3.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^3.0.7", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": "^14.18.0 || ^16.14.0 || >=18.0.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "../patch-db/client/node_modules/fill-range": { - "version": "7.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/find-up": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../patch-db/client/node_modules/find-versions": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "semver-regex": "^3.1.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../patch-db/client/node_modules/fs.realpath": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "../patch-db/client/node_modules/function-bind": { - "version": "1.1.1", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/get-stream": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../patch-db/client/node_modules/glob": { - "version": "7.2.3", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "../patch-db/client/node_modules/has": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "../patch-db/client/node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/human-signals": { - "version": "4.3.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=14.18.0" - } - }, - "../patch-db/client/node_modules/husky": { - "version": "4.3.8", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "ci-info": "^2.0.0", - "compare-versions": "^3.6.0", - "cosmiconfig": "^7.0.0", - "find-versions": "^4.0.0", - "opencollective-postinstall": "^2.0.2", - "pkg-dir": "^5.0.0", - "please-upgrade-node": "^3.2.0", - "slash": "^3.0.0", - "which-pm-runs": "^1.0.0" - }, - "bin": { - "husky-run": "bin/run.js", - "husky-upgrade": "lib/upgrader/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/husky" - } - }, - "../patch-db/client/node_modules/import-fresh": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../patch-db/client/node_modules/indent-string": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/inflight": { - "version": "1.0.6", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "../patch-db/client/node_modules/inherits": { - "version": "2.0.4", - "dev": true, - "license": "ISC" - }, - "../patch-db/client/node_modules/is-arrayish": { - "version": "0.2.1", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/is-core-module": { - "version": "2.12.1", - "dev": true, - "license": "MIT", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "../patch-db/client/node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../patch-db/client/node_modules/is-number": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "../patch-db/client/node_modules/is-stream": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../patch-db/client/node_modules/isexe": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "../patch-db/client/node_modules/js-tokens": { - "version": "4.0.0", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/js-yaml": { - "version": "3.14.1", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "../patch-db/client/node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/lilconfig": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "../patch-db/client/node_modules/lines-and-columns": { - "version": "1.2.4", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/lint-staged": { - "version": "13.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "5.2.0", - "cli-truncate": "^3.1.0", - "commander": "^10.0.0", - "debug": "^4.3.4", - "execa": "^7.0.0", - "lilconfig": "2.1.0", - "listr2": "^5.0.7", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-inspect": "^1.12.3", - "pidtree": "^0.6.0", - "string-argv": "^0.3.1", - "yaml": "^2.2.2" - }, - "bin": { - "lint-staged": "bin/lint-staged.js" - }, - "engines": { - "node": "^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/lint-staged" - } - }, - "../patch-db/client/node_modules/lint-staged/node_modules/chalk": { - "version": "5.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "../patch-db/client/node_modules/lint-staged/node_modules/yaml": { - "version": "2.3.1", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 14" - } - }, - "../patch-db/client/node_modules/listr2": { - "version": "5.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "cli-truncate": "^2.1.0", - "colorette": "^2.0.19", - "log-update": "^4.0.0", - "p-map": "^4.0.0", - "rfdc": "^1.3.0", - "rxjs": "^7.8.0", - "through": "^2.3.8", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": "^14.13.1 || >=16.0.0" - }, - "peerDependencies": { - "enquirer": ">= 2.3.0 < 3" - }, - "peerDependenciesMeta": { - "enquirer": { - "optional": true - } - } - }, - "../patch-db/client/node_modules/listr2/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/listr2/node_modules/cli-truncate": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../patch-db/client/node_modules/listr2/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/listr2/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/listr2/node_modules/slice-ansi": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/listr2/node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/listr2/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/locate-path": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../patch-db/client/node_modules/log-update": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^4.3.0", - "cli-cursor": "^3.1.0", - "slice-ansi": "^4.0.0", - "wrap-ansi": "^6.2.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../patch-db/client/node_modules/log-update/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/log-update/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/log-update/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/log-update/node_modules/slice-ansi": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "../patch-db/client/node_modules/log-update/node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/log-update/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/log-update/node_modules/wrap-ansi": { - "version": "6.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/make-error": { - "version": "1.3.6", - "dev": true, - "license": "ISC" - }, - "../patch-db/client/node_modules/merge-stream": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/micromatch": { - "version": "4.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "../patch-db/client/node_modules/mimic-fn": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../patch-db/client/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "../patch-db/client/node_modules/minimist": { - "version": "1.2.8", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "../patch-db/client/node_modules/mkdirp": { - "version": "0.5.6", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "../patch-db/client/node_modules/ms": { - "version": "2.1.2", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/normalize-path": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../patch-db/client/node_modules/npm-run-path": { - "version": "5.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../patch-db/client/node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../patch-db/client/node_modules/object-inspect": { - "version": "1.12.3", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "../patch-db/client/node_modules/once": { - "version": "1.4.0", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "../patch-db/client/node_modules/onetime": { - "version": "6.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../patch-db/client/node_modules/opencollective-postinstall": { - "version": "2.0.3", - "dev": true, - "license": "MIT", - "bin": { - "opencollective-postinstall": "index.js" - } - }, - "../patch-db/client/node_modules/p-limit": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../patch-db/client/node_modules/p-locate": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../patch-db/client/node_modules/p-map": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../patch-db/client/node_modules/parent-module": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "../patch-db/client/node_modules/parse-json": { - "version": "5.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../patch-db/client/node_modules/path-exists": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/path-is-absolute": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "../patch-db/client/node_modules/path-key": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/path-parse": { - "version": "1.0.7", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/path-type": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/picomatch": { - "version": "2.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "../patch-db/client/node_modules/pidtree": { - "version": "0.6.0", - "dev": true, - "license": "MIT", - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "../patch-db/client/node_modules/pkg-dir": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^5.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "../patch-db/client/node_modules/please-upgrade-node": { - "version": "3.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "semver-compare": "^1.0.0" - } - }, - "../patch-db/client/node_modules/prettier": { - "version": "2.8.8", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "../patch-db/client/node_modules/resolve": { - "version": "1.22.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.11.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "../patch-db/client/node_modules/resolve-from": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "../patch-db/client/node_modules/restore-cursor": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/restore-cursor/node_modules/mimic-fn": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "../patch-db/client/node_modules/restore-cursor/node_modules/onetime": { - "version": "5.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../patch-db/client/node_modules/rfdc": { - "version": "1.3.0", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/rxjs": { - "version": "7.8.1", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "../patch-db/client/node_modules/semver": { - "version": "5.7.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "../patch-db/client/node_modules/semver-compare": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/semver-regex": { - "version": "3.1.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../patch-db/client/node_modules/shebang-command": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/shebang-regex": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/signal-exit": { - "version": "3.0.7", - "dev": true, - "license": "ISC" - }, - "../patch-db/client/node_modules/slash": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/slice-ansi": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "../patch-db/client/node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "../patch-db/client/node_modules/sorted-btree": { - "version": "1.5.0", - "license": "MIT" - }, - "../patch-db/client/node_modules/sprintf-js": { - "version": "1.0.3", - "dev": true, - "license": "BSD-3-Clause" - }, - "../patch-db/client/node_modules/string-argv": { - "version": "0.3.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.19" - } - }, - "../patch-db/client/node_modules/string-width": { - "version": "5.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../patch-db/client/node_modules/strip-ansi": { - "version": "7.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "../patch-db/client/node_modules/strip-final-newline": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../patch-db/client/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "../patch-db/client/node_modules/through": { - "version": "2.3.8", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/to-regex-range": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "../patch-db/client/node_modules/ts-node": { - "version": "10.9.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "../patch-db/client/node_modules/tslib": { - "version": "2.5.3", - "license": "0BSD" - }, - "../patch-db/client/node_modules/tslint": { - "version": "6.1.3", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "builtin-modules": "^1.1.1", - "chalk": "^2.3.0", - "commander": "^2.12.1", - "diff": "^4.0.1", - "glob": "^7.1.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.3", - "resolve": "^1.3.2", - "semver": "^5.3.0", - "tslib": "^1.13.0", - "tsutils": "^2.29.0" - }, - "bin": { - "tslint": "bin/tslint" - }, - "engines": { - "node": ">=4.8.0" - }, - "peerDependencies": { - "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev || >= 4.0.0-dev" - } - }, - "../patch-db/client/node_modules/tslint/node_modules/ansi-styles": { - "version": "3.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "../patch-db/client/node_modules/tslint/node_modules/chalk": { - "version": "2.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "../patch-db/client/node_modules/tslint/node_modules/color-convert": { - "version": "1.9.3", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "../patch-db/client/node_modules/tslint/node_modules/color-name": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/tslint/node_modules/commander": { - "version": "2.20.3", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/tslint/node_modules/has-flag": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "../patch-db/client/node_modules/tslint/node_modules/supports-color": { - "version": "5.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "../patch-db/client/node_modules/tslint/node_modules/tslib": { - "version": "1.14.1", - "dev": true, - "license": "0BSD" - }, - "../patch-db/client/node_modules/tsutils": { - "version": "2.29.0", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^1.8.1" - }, - "peerDependencies": { - "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev" - } - }, - "../patch-db/client/node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "dev": true, - "license": "0BSD" - }, - "../patch-db/client/node_modules/type-fest": { - "version": "0.21.3", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "../patch-db/client/node_modules/typescript": { - "version": "4.9.5", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "../patch-db/client/node_modules/uuid": { - "version": "8.3.2", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "../patch-db/client/node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/which": { - "version": "2.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "../patch-db/client/node_modules/which-pm-runs": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "../patch-db/client/node_modules/wrap-ansi": { - "version": "7.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "../patch-db/client/node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, - "../patch-db/client/node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.3", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "../patch-db/client/node_modules/wrappy": { - "version": "1.0.2", - "dev": true, - "license": "ISC" - }, - "../patch-db/client/node_modules/yaml": { - "version": "1.10.2", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "../patch-db/client/node_modules/yn": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "../patch-db/client/node_modules/yocto-queue": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "../sdk/dist": { - "name": "@start9labs/start-sdk", "version": "0.3.6-alpha5", "license": "MIT", "dependencies": { @@ -2002,14 +138,16 @@ } }, "node_modules/@adobe/css-tools": { - "version": "4.0.1", - "dev": true, - "license": "MIT" + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", + "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", + "dev": true }, "node_modules/@ampproject/remapping": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.1.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -2019,11 +157,12 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1402.3", + "version": "0.1402.13", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1402.13.tgz", + "integrity": "sha512-n0ISBuvkZHoOpAzuAZql1TU9VLHUE9e/a9g4VNOPHewjMzpN02VqeGKvJfOCKtzkCs6gVssIlILm2/SXxkIFxQ==", "devOptional": true, - "license": "MIT", "dependencies": { - "@angular-devkit/core": "14.2.3", + "@angular-devkit/core": "14.2.13", "rxjs": "6.6.7" }, "engines": { @@ -2034,8 +173,9 @@ }, "node_modules/@angular-devkit/architect/node_modules/rxjs": { "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "devOptional": true, - "license": "Apache-2.0", "dependencies": { "tslib": "^1.9.0" }, @@ -2045,18 +185,20 @@ }, "node_modules/@angular-devkit/architect/node_modules/tslib": { "version": "1.14.1", - "devOptional": true, - "license": "0BSD" + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "devOptional": true }, "node_modules/@angular-devkit/build-angular": { - "version": "14.2.3", + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-14.2.13.tgz", + "integrity": "sha512-FJZKQ3xYFvEJ807sxVy4bCVyGU2NMl3UUPNfLIdIdzwwDEP9tx/cc+c4VtVPEZZfU8jVenu8XOvL6L0vpjt3yg==", "dev": true, - "license": "MIT", "dependencies": { "@ampproject/remapping": "2.2.0", - "@angular-devkit/architect": "0.1402.3", - "@angular-devkit/build-webpack": "0.1402.3", - "@angular-devkit/core": "14.2.3", + "@angular-devkit/architect": "0.1402.13", + "@angular-devkit/build-webpack": "0.1402.13", + "@angular-devkit/core": "14.2.13", "@babel/core": "7.18.10", "@babel/generator": "7.18.12", "@babel/helper-annotate-as-pure": "7.18.6", @@ -2067,7 +209,7 @@ "@babel/runtime": "7.18.9", "@babel/template": "7.18.10", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "14.2.3", + "@ngtools/webpack": "14.2.13", "ansi-colors": "4.1.3", "babel-loader": "8.2.5", "babel-plugin-istanbul": "6.1.1", @@ -2085,14 +227,14 @@ "less": "4.1.3", "less-loader": "11.0.0", "license-webpack-plugin": "4.0.2", - "loader-utils": "3.2.0", + "loader-utils": "3.2.1", "mini-css-extract-plugin": "2.6.1", "minimatch": "5.1.0", "open": "8.4.0", "ora": "5.4.1", "parse5-html-rewriting-stream": "6.0.1", "piscina": "3.2.0", - "postcss": "8.4.16", + "postcss": "8.4.31", "postcss-import": "15.0.0", "postcss-loader": "7.0.1", "postcss-preset-env": "7.8.0", @@ -2101,7 +243,7 @@ "rxjs": "6.6.7", "sass": "1.54.4", "sass-loader": "13.0.2", - "semver": "7.3.7", + "semver": "7.5.3", "source-map-loader": "4.0.0", "source-map-support": "0.5.21", "stylus": "0.59.0", @@ -2110,7 +252,7 @@ "text-table": "0.2.0", "tree-kill": "1.2.2", "tslib": "2.4.0", - "webpack": "5.74.0", + "webpack": "5.76.1", "webpack-dev-middleware": "5.3.3", "webpack-dev-server": "4.11.0", "webpack-merge": "5.8.0", @@ -2155,25 +297,194 @@ } } }, - "node_modules/@angular-devkit/build-angular/node_modules/@ngtools/webpack": { - "version": "14.2.3", + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/ast": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", + "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || >=16.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "@angular/compiler-cli": "^14.0.0", - "typescript": ">=4.6.2 <4.9", - "webpack": "^5.54.0" + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" } }, + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", + "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", + "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/ieee754": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", + "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/leb128": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", + "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/utf8": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", + "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", + "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", + "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", + "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", + "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { "tslib": "^1.9.0" }, @@ -2183,15 +494,88 @@ }, "node_modules/@angular-devkit/build-angular/node_modules/rxjs/node_modules/tslib": { "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, - "license": "0BSD" + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/webpack": { + "version": "5.76.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz", + "integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.10.0", + "es-module-lexer": "^0.9.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1402.3", + "version": "0.1402.13", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1402.13.tgz", + "integrity": "sha512-K27aJmuw86ZOdiu5PoGeGDJ2v7g2ZCK0bGwc8jzkjTLRfvd4FRKIIZumGv3hbQ3vQRLikiU6WMDRTFyCZky/EA==", "dev": true, - "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1402.3", + "@angular-devkit/architect": "0.1402.13", "rxjs": "6.6.7" }, "engines": { @@ -2206,8 +590,9 @@ }, "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { "tslib": "^1.9.0" }, @@ -2217,13 +602,14 @@ }, "node_modules/@angular-devkit/build-webpack/node_modules/tslib": { "version": "1.14.1", - "dev": true, - "license": "0BSD" + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true }, "node_modules/@angular-devkit/core": { - "version": "14.2.3", - "devOptional": true, - "license": "MIT", + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.2.13.tgz", + "integrity": "sha512-aIefeZcbjghQg/V6U9CTLtyB5fXDJ63KwYqVYkWP+i0XriS5A9puFgq2u/OVsWxAfYvqpDqp5AdQ0g0bi3CAsA==", "dependencies": { "ajv": "8.11.0", "ajv-formats": "2.1.1", @@ -2247,8 +633,8 @@ }, "node_modules/@angular-devkit/core/node_modules/rxjs": { "version": "6.6.7", - "devOptional": true, - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "dependencies": { "tslib": "^1.9.0" }, @@ -2258,15 +644,15 @@ }, "node_modules/@angular-devkit/core/node_modules/tslib": { "version": "1.14.1", - "devOptional": true, - "license": "0BSD" + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/@angular-devkit/schematics": { - "version": "14.2.3", - "devOptional": true, - "license": "MIT", + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-14.2.13.tgz", + "integrity": "sha512-2zczyeNzeBcrT2HOysv52X9SH3tZoHfWJvVf6H0SIa74rfDKEl7hFpKNXnh3x8sIMLj5mZn05n5RCqGxCczcIg==", "dependencies": { - "@angular-devkit/core": "14.2.3", + "@angular-devkit/core": "14.2.13", "jsonc-parser": "3.1.0", "magic-string": "0.26.2", "ora": "5.4.1", @@ -2280,8 +666,8 @@ }, "node_modules/@angular-devkit/schematics/node_modules/rxjs": { "version": "6.6.7", - "devOptional": true, - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "dependencies": { "tslib": "^1.9.0" }, @@ -2291,12 +677,13 @@ }, "node_modules/@angular-devkit/schematics/node_modules/tslib": { "version": "1.14.1", - "devOptional": true, - "license": "0BSD" + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/@angular/animations": { - "version": "14.2.2", - "license": "MIT", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-14.3.0.tgz", + "integrity": "sha512-QoBcIKy1ZiU+4qJsAh5Ls20BupWiXiZzKb0s6L9/dntPt5Msr4Ao289XR2P6O1L+kTsCprH9Kt41zyGQ/bkRqg==", "dependencies": { "tslib": "^2.3.0" }, @@ -2304,18 +691,19 @@ "node": "^14.15.0 || >=16.10.0" }, "peerDependencies": { - "@angular/core": "14.2.2" + "@angular/core": "14.3.0" } }, "node_modules/@angular/cli": { - "version": "14.2.3", + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-14.2.13.tgz", + "integrity": "sha512-I5EepRem2CCyS3GDzQxZ2ZrqQwVqoGoLY+ZQhsK1QGWUnUyFOjbv3OlUGxRUYwcedu19V1EBAKjmQ96HzMIcVQ==", "devOptional": true, - "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1402.3", - "@angular-devkit/core": "14.2.3", - "@angular-devkit/schematics": "14.2.3", - "@schematics/angular": "14.2.3", + "@angular-devkit/architect": "0.1402.13", + "@angular-devkit/core": "14.2.13", + "@angular-devkit/schematics": "14.2.13", + "@schematics/angular": "14.2.13", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "debug": "4.3.4", @@ -2328,7 +716,7 @@ "ora": "5.4.1", "pacote": "13.6.2", "resolve": "1.22.1", - "semver": "7.3.7", + "semver": "7.5.3", "symbol-observable": "4.0.0", "uuid": "8.3.2", "yargs": "17.5.1" @@ -2343,8 +731,9 @@ } }, "node_modules/@angular/common": { - "version": "14.2.2", - "license": "MIT", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-14.3.0.tgz", + "integrity": "sha512-pV9oyG3JhGWeQ+TFB0Qub6a1VZWMNZ6/7zEopvYivdqa5yDLLDSBRWb6P80RuONXyGnM1pa7l5nYopX+r/23GQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -2352,13 +741,14 @@ "node": "^14.15.0 || >=16.10.0" }, "peerDependencies": { - "@angular/core": "14.2.2", + "@angular/core": "14.3.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "14.2.2", - "license": "MIT", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-14.3.0.tgz", + "integrity": "sha512-E15Rh0t3vA+bctbKnBCaDmLvc3ix+ZBt6yFZmhZalReQ+KpOlvOJv+L9oiFEgg+rYVl2QdvN7US1fvT0PqswLw==", "dependencies": { "tslib": "^2.3.0" }, @@ -2366,7 +756,7 @@ "node": "^14.15.0 || >=16.10.0" }, "peerDependencies": { - "@angular/core": "14.2.2" + "@angular/core": "14.3.0" }, "peerDependenciesMeta": { "@angular/core": { @@ -2375,9 +765,10 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "14.2.2", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-14.3.0.tgz", + "integrity": "sha512-eoKpKdQ2X6axMgzcPUMZVYl3bIlTMzMeTo5V29No4BzgiUB+QoOTYGNJZkGRyqTNpwD9uSBJvmT2vG9+eC4ghQ==", "dev": true, - "license": "MIT", "dependencies": { "@babel/core": "^7.17.2", "chokidar": "^3.0.0", @@ -2399,13 +790,14 @@ "node": "^14.15.0 || >=16.10.0" }, "peerDependencies": { - "@angular/compiler": "14.2.2", + "@angular/compiler": "14.3.0", "typescript": ">=4.6.2 <4.9" } }, "node_modules/@angular/core": { - "version": "14.2.2", - "license": "MIT", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-14.3.0.tgz", + "integrity": "sha512-wYiwItc0Uyn4FWZ/OAx/Ubp2/WrD3EgUJ476y1XI7yATGPF8n9Ld5iCXT08HOvc4eBcYlDfh90kTXR6/MfhzdQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -2414,12 +806,13 @@ }, "peerDependencies": { "rxjs": "^6.5.3 || ^7.4.0", - "zone.js": "~0.11.4" + "zone.js": "~0.11.4 || ~0.12.0" } }, "node_modules/@angular/forms": { - "version": "14.2.2", - "license": "MIT", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-14.3.0.tgz", + "integrity": "sha512-fBZZC2UFMom2AZPjGQzROPXFWO6kvCsPDKctjJwClVC8PuMrkm+RRyiYRdBbt2qxWHEqOZM2OCQo73xUyZOYHw==", "dependencies": { "tslib": "^2.3.0" }, @@ -2427,23 +820,25 @@ "node": "^14.15.0 || >=16.10.0" }, "peerDependencies": { - "@angular/common": "14.2.2", - "@angular/core": "14.2.2", - "@angular/platform-browser": "14.2.2", + "@angular/common": "14.3.0", + "@angular/core": "14.3.0", + "@angular/platform-browser": "14.3.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/language-service": { - "version": "14.2.2", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-14.3.0.tgz", + "integrity": "sha512-Sij3OQzj1UGs1O8H9PxVAY/o27+oqZwQRnib66rsWvtbIBTjHp4FV3dTs5iVcr62GGv4V4Mff/2I82NP10GPQg==", "dev": true, - "license": "MIT", "engines": { "node": "^14.15.0 || >=16.10.0" } }, "node_modules/@angular/platform-browser": { - "version": "14.2.2", - "license": "MIT", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-14.3.0.tgz", + "integrity": "sha512-w9Y3740UmTz44T0Egvc+4QV9sEbO61L+aRHbpkLTJdlEGzHByZvxJmJyBYmdqeyTPwc/Zpy7c02frlpfAlyB7A==", "dependencies": { "tslib": "^2.3.0" }, @@ -2451,9 +846,9 @@ "node": "^14.15.0 || >=16.10.0" }, "peerDependencies": { - "@angular/animations": "14.2.2", - "@angular/common": "14.2.2", - "@angular/core": "14.2.2" + "@angular/animations": "14.3.0", + "@angular/common": "14.3.0", + "@angular/core": "14.3.0" }, "peerDependenciesMeta": { "@angular/animations": { @@ -2462,8 +857,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "14.2.2", - "license": "MIT", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-14.3.0.tgz", + "integrity": "sha512-rneZiMrIiYRhrkQvdL40E2ErKRn4Zdo6EtjBM9pAmWeyoM8oMnOZb9gz5vhrkNWg06kVMVg0yKqluP5How7j3A==", "dependencies": { "tslib": "^2.3.0" }, @@ -2471,18 +867,19 @@ "node": "^14.15.0 || >=16.10.0" }, "peerDependencies": { - "@angular/common": "14.2.2", - "@angular/compiler": "14.2.2", - "@angular/core": "14.2.2", - "@angular/platform-browser": "14.2.2" + "@angular/common": "14.3.0", + "@angular/compiler": "14.3.0", + "@angular/core": "14.3.0", + "@angular/platform-browser": "14.3.0" } }, "node_modules/@angular/pwa": { - "version": "14.1.0", - "license": "MIT", + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@angular/pwa/-/pwa-14.2.13.tgz", + "integrity": "sha512-WPkTgT3+VC/KeZMydZnTQJWTG5IVTSdkJfqmNQWfHXzpmm1CG4KvFRj3xEOXvaDmcL56nnqKhL/o66kpai15Qw==", "dependencies": { - "@angular-devkit/schematics": "14.1.0", - "@schematics/angular": "14.1.0", + "@angular-devkit/schematics": "14.2.13", + "@schematics/angular": "14.2.13", "parse5-html-rewriting-stream": "6.0.1" }, "engines": { @@ -2499,77 +896,10 @@ } } }, - "node_modules/@angular/pwa/node_modules/@angular-devkit/core": { - "version": "14.1.0", - "license": "MIT", - "dependencies": { - "ajv": "8.11.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.1.0", - "rxjs": "6.6.7", - "source-map": "0.7.4" - }, - "engines": { - "node": "^14.15.0 || >=16.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@angular/pwa/node_modules/@angular-devkit/schematics": { - "version": "14.1.0", - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "14.1.0", - "jsonc-parser": "3.1.0", - "magic-string": "0.26.2", - "ora": "5.4.1", - "rxjs": "6.6.7" - }, - "engines": { - "node": "^14.15.0 || >=16.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular/pwa/node_modules/@schematics/angular": { - "version": "14.1.0", - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "14.1.0", - "@angular-devkit/schematics": "14.1.0", - "jsonc-parser": "3.1.0" - }, - "engines": { - "node": "^14.15.0 || >=16.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular/pwa/node_modules/rxjs": { - "version": "6.6.7", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, - "node_modules/@angular/pwa/node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" - }, "node_modules/@angular/router": { - "version": "14.2.2", - "license": "MIT", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-14.3.0.tgz", + "integrity": "sha512-uip0V7w7k7xyxxpTPbr7EuMnYLj3FzJrwkLVJSEw3TMMGHt5VU5t4BBa9veGZOta2C205XFrTAHnp8mD+XYY1w==", "dependencies": { "tslib": "^2.3.0" }, @@ -2577,15 +907,16 @@ "node": "^14.15.0 || >=16.10.0" }, "peerDependencies": { - "@angular/common": "14.2.2", - "@angular/core": "14.2.2", - "@angular/platform-browser": "14.2.2", + "@angular/common": "14.3.0", + "@angular/core": "14.3.0", + "@angular/platform-browser": "14.3.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/service-worker": { - "version": "14.2.2", - "license": "MIT", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-14.3.0.tgz", + "integrity": "sha512-i5O7m1gQijWm7cgva0XTmOVBFrPrttNxFDwoMLMYCh8rHOCQUQ4DcVO1qTBPWU4SrY5BYPEvR+r05dYQLFYCBw==", "dependencies": { "tslib": "^2.3.0" }, @@ -2596,38 +927,43 @@ "node": "^14.15.0 || >=16.10.0" }, "peerDependencies": { - "@angular/common": "14.2.2", - "@angular/core": "14.2.2" + "@angular/common": "14.3.0", + "@angular/core": "14.3.0" } }, "node_modules/@assemblyscript/loader": { "version": "0.10.1", - "dev": true, - "license": "Apache-2.0" + "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.10.1.tgz", + "integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==", + "dev": true }, "node_modules/@babel/code-frame": { - "version": "7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.19.1", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.10.tgz", + "integrity": "sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==", "dev": true, - "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.18.6", @@ -2654,17 +990,19 @@ } }, "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/generator": { "version": "7.18.12", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.12.tgz", + "integrity": "sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/types": "^7.18.10", "@jridgewell/gen-mapping": "^0.3.2", @@ -2675,13 +1013,14 @@ } }, "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, - "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -2689,8 +1028,9 @@ }, "node_modules/@babel/helper-annotate-as-pure": { "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", + "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", "dev": true, - "license": "MIT", "dependencies": { "@babel/types": "^7.18.6" }, @@ -2699,54 +1039,58 @@ } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.18.9", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", + "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-explode-assignable-expression": "^7.18.6", - "@babel/types": "^7.18.9" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.19.1", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.19.1", - "@babel/helper-validator-option": "^7.18.6", - "browserslist": "^4.21.3", - "semver": "^6.3.0" + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.19.0", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz", + "integrity": "sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-member-expression-to-functions": "^7.18.9", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.9", - "@babel/helper-split-export-declaration": "^7.18.6" + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -2755,13 +1099,36 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.19.0", + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "regexpu-core": "^5.1.0" + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz", + "integrity": "sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -2770,10 +1137,32 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-define-polyfill-provider": { "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", + "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.17.7", "@babel/helper-plugin-utils": "^7.16.7", @@ -2787,123 +1176,102 @@ } }, "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": { - "version": "6.3.0", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-explode-assignable-expression": { - "version": "7.18.6", - "dev": true, - "license": "MIT", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.19.0", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name/node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.18.9", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz", + "integrity": "sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/types": "^7.18.9" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.19.0", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.18.6", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.0", - "@babel/types": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.19.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.18.9", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-wrap-function": "^7.18.9", - "@babel/types": "^7.18.9" + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -2912,122 +1280,214 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.19.1", + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", + "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-member-expression-to-functions": "^7.18.9", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/traverse": "^7.19.1", - "@babel/types": "^7.19.0" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.18.6", + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz", + "integrity": "sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-wrap-function": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz", + "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.18.9", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", + "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/types": "^7.18.9" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.18.10", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.19.0", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz", + "integrity": "sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-function-name": "^7.19.0", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.0", - "@babel/types": "^7.19.0" + "@babel/helper-function-name": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function/node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.19.0", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.0", - "@babel/types": "^7.19.0" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers/node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.19.1", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", "dev": true, - "license": "MIT", "bin": { "parser": "bin/babel-parser.js" }, @@ -3036,11 +1496,12 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.7.tgz", + "integrity": "sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3050,13 +1511,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.18.9", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", + "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", - "@babel/plugin-proposal-optional-chaining": "^7.18.9" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3067,8 +1529,10 @@ }, "node_modules/@babel/plugin-proposal-async-generator-functions": { "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.18.10.tgz", + "integrity": "sha512-1mFuY2TOsR1hxbjCo4QL+qlIjV07p4H4EUYw2J/WCqsvFV6V9X9z9YhXbWndc/4fw+hYGlDT7egYxliMp5O6Ew==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead.", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-plugin-utils": "^7.18.9", @@ -3084,8 +1548,10 @@ }, "node_modules/@babel/plugin-proposal-class-properties": { "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" @@ -3098,12 +1564,14 @@ } }, "node_modules/@babel/plugin-proposal-class-static-block": { - "version": "7.18.6", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz", + "integrity": "sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-static-block instead.", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", "@babel/plugin-syntax-class-static-block": "^7.14.5" }, "engines": { @@ -3115,8 +1583,10 @@ }, "node_modules/@babel/plugin-proposal-dynamic-import": { "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", + "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-dynamic-import instead.", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", "@babel/plugin-syntax-dynamic-import": "^7.8.3" @@ -3130,8 +1600,10 @@ }, "node_modules/@babel/plugin-proposal-export-namespace-from": { "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", + "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead.", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.9", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" @@ -3145,8 +1617,10 @@ }, "node_modules/@babel/plugin-proposal-json-strings": { "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", + "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-json-strings instead.", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", "@babel/plugin-syntax-json-strings": "^7.8.3" @@ -3159,11 +1633,13 @@ } }, "node_modules/@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.18.9", + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", + "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead.", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" }, "engines": { @@ -3175,8 +1651,10 @@ }, "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" @@ -3190,8 +1668,10 @@ }, "node_modules/@babel/plugin-proposal-numeric-separator": { "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", + "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", "@babel/plugin-syntax-numeric-separator": "^7.10.4" @@ -3204,15 +1684,17 @@ } }, "node_modules/@babel/plugin-proposal-object-rest-spread": { - "version": "7.18.9", + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", + "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.", "dev": true, - "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.18.8", - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9", + "@babel/compat-data": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-plugin-utils": "^7.20.2", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.18.8" + "@babel/plugin-transform-parameters": "^7.20.7" }, "engines": { "node": ">=6.9.0" @@ -3223,8 +1705,10 @@ }, "node_modules/@babel/plugin-proposal-optional-catch-binding": { "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", + "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead.", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" @@ -3237,12 +1721,14 @@ } }, "node_modules/@babel/plugin-proposal-optional-chaining": { - "version": "7.18.9", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "engines": { @@ -3254,8 +1740,10 @@ }, "node_modules/@babel/plugin-proposal-private-methods": { "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" @@ -3268,13 +1756,15 @@ } }, "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.18.6", + "version": "7.21.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz", + "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", "@babel/plugin-syntax-private-property-in-object": "^7.14.5" }, "engines": { @@ -3286,8 +1776,10 @@ }, "node_modules/@babel/plugin-proposal-unicode-property-regex": { "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead.", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" @@ -3301,8 +1793,9 @@ }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -3312,8 +1805,9 @@ }, "node_modules/@babel/plugin-syntax-class-properties": { "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, @@ -3323,8 +1817,9 @@ }, "node_modules/@babel/plugin-syntax-class-static-block": { "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -3337,8 +1832,9 @@ }, "node_modules/@babel/plugin-syntax-dynamic-import": { "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -3348,8 +1844,9 @@ }, "node_modules/@babel/plugin-syntax-export-namespace-from": { "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.3" }, @@ -3358,11 +1855,12 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", + "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3373,8 +1871,9 @@ }, "node_modules/@babel/plugin-syntax-json-strings": { "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -3384,8 +1883,9 @@ }, "node_modules/@babel/plugin-syntax-logical-assignment-operators": { "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -3395,8 +1895,9 @@ }, "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -3406,8 +1907,9 @@ }, "node_modules/@babel/plugin-syntax-numeric-separator": { "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -3417,8 +1919,9 @@ }, "node_modules/@babel/plugin-syntax-object-rest-spread": { "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -3428,8 +1931,9 @@ }, "node_modules/@babel/plugin-syntax-optional-catch-binding": { "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -3439,8 +1943,9 @@ }, "node_modules/@babel/plugin-syntax-optional-chaining": { "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -3450,8 +1955,9 @@ }, "node_modules/@babel/plugin-syntax-private-property-in-object": { "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -3464,8 +1970,9 @@ }, "node_modules/@babel/plugin-syntax-top-level-await": { "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -3477,11 +1984,12 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", + "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3492,8 +2000,9 @@ }, "node_modules/@babel/plugin-transform-async-to-generator": { "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz", + "integrity": "sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6", @@ -3507,11 +2016,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", + "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3521,11 +2031,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.18.9", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.7.tgz", + "integrity": "sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3535,18 +2046,18 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.19.0", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz", + "integrity": "sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-compilation-targets": "^7.19.0", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-replace-supers": "^7.18.9", - "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", "globals": "^11.1.0" }, "engines": { @@ -3556,12 +2067,26 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.18.9", + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", + "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/template": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3570,12 +2095,27 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.18.13", + "node_modules/@babel/plugin-transform-computed-properties/node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz", + "integrity": "sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3585,12 +2125,13 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", + "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3600,11 +2141,12 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.18.9", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", + "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3614,12 +2156,13 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", + "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3629,11 +2172,13 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.18.8", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", + "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3643,13 +2188,14 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.18.9", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz", + "integrity": "sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-function-name": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3659,11 +2205,12 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.18.9", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz", + "integrity": "sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3673,11 +2220,12 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", + "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3687,13 +2235,13 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", + "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "babel-plugin-dynamic-import-node": "^2.3.3" + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3703,14 +2251,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz", + "integrity": "sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-simple-access": "^7.18.6", - "babel-plugin-dynamic-import-node": "^2.3.3" + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3720,15 +2268,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.19.0", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.7.tgz", + "integrity": "sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-module-transforms": "^7.19.0", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-validator-identifier": "^7.18.6", - "babel-plugin-dynamic-import-node": "^2.3.3" + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3738,12 +2286,13 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", + "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3753,12 +2302,13 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.19.1", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", + "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.19.0", - "@babel/helper-plugin-utils": "^7.19.0" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3768,11 +2318,12 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", + "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3782,12 +2333,30 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", + "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.6" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz", + "integrity": "sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "engines": { "node": ">=6.9.0" @@ -3797,11 +2366,12 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.18.8", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", + "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3811,11 +2381,12 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", + "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3825,12 +2396,13 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", + "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "regenerator-transform": "^0.15.0" + "@babel/helper-plugin-utils": "^7.24.7", + "regenerator-transform": "^0.15.2" }, "engines": { "node": ">=6.9.0" @@ -3840,11 +2412,12 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", + "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3855,8 +2428,9 @@ }, "node_modules/@babel/plugin-transform-runtime": { "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.18.10.tgz", + "integrity": "sha512-q5mMeYAdfEbpBAgzl7tBre/la3LeCxmDO1+wMXRdPWbcoMjR3GiXlCLk7JBZVVye0bqTGNMbt0yYVXX1B1jEWQ==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.9", @@ -3873,19 +2447,21 @@ } }, "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { - "version": "6.3.0", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", + "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3895,12 +2471,13 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.19.0", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", + "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9" + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3910,11 +2487,12 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", + "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3924,11 +2502,12 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.18.9", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", + "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3938,11 +2517,12 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.18.9", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz", + "integrity": "sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3952,11 +2532,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.18.10", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", + "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3966,12 +2547,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.18.6", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", + "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -3982,8 +2564,9 @@ }, "node_modules/@babel/preset-env": { "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.18.10.tgz", + "integrity": "sha512-wVxs1yjFdW3Z/XkNfXKoblxoHgbtUF7/l3PvvP4m02Qz9TZ6uZGxRVYjSQeR87oQmHco9zWitW5J82DJ7sCjvA==", "dev": true, - "license": "MIT", "dependencies": { "@babel/compat-data": "^7.18.8", "@babel/helper-compilation-targets": "^7.18.9", @@ -4069,17 +2652,19 @@ } }, "node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.0", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/preset-modules": { - "version": "0.1.5", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6.tgz", + "integrity": "sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", @@ -4088,13 +2673,20 @@ "esutils": "^2.0.2" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, "node_modules/@babel/runtime": { "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz", + "integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==", "dev": true, - "license": "MIT", "dependencies": { "regenerator-runtime": "^0.13.4" }, @@ -4104,8 +2696,9 @@ }, "node_modules/@babel/template": { "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", + "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", "dev": true, - "license": "MIT", "dependencies": { "@babel/code-frame": "^7.18.6", "@babel/parser": "^7.18.10", @@ -4116,19 +2709,20 @@ } }, "node_modules/@babel/traverse": { - "version": "7.19.1", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.19.0", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.19.1", - "@babel/types": "^7.19.0", - "debug": "^4.1.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -4136,12 +2730,14 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.19.0", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/types": "^7.19.0", - "@jridgewell/gen-mapping": "^0.3.2", + "@babel/types": "^7.24.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { @@ -4149,25 +2745,27 @@ } }, "node_modules/@babel/traverse/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, - "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@babel/types": { - "version": "7.19.0", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.18.10", - "@babel/helper-validator-identifier": "^7.18.6", + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -4176,8 +2774,9 @@ }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, - "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -4187,8 +2786,9 @@ }, "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "dev": true, - "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -4196,8 +2796,9 @@ }, "node_modules/@csstools/postcss-cascade-layers": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz", + "integrity": "sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==", "dev": true, - "license": "CC0-1.0", "dependencies": { "@csstools/selector-specificity": "^2.0.2", "postcss-selector-parser": "^6.0.10" @@ -4215,8 +2816,9 @@ }, "node_modules/@csstools/postcss-color-function": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz", + "integrity": "sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==", "dev": true, - "license": "CC0-1.0", "dependencies": { "@csstools/postcss-progressive-custom-properties": "^1.1.0", "postcss-value-parser": "^4.2.0" @@ -4234,8 +2836,9 @@ }, "node_modules/@csstools/postcss-font-format-keywords": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz", + "integrity": "sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==", "dev": true, - "license": "CC0-1.0", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -4252,8 +2855,9 @@ }, "node_modules/@csstools/postcss-hwb-function": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz", + "integrity": "sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==", "dev": true, - "license": "CC0-1.0", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -4270,8 +2874,9 @@ }, "node_modules/@csstools/postcss-ic-unit": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz", + "integrity": "sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==", "dev": true, - "license": "CC0-1.0", "dependencies": { "@csstools/postcss-progressive-custom-properties": "^1.1.0", "postcss-value-parser": "^4.2.0" @@ -4289,8 +2894,9 @@ }, "node_modules/@csstools/postcss-is-pseudo-class": { "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz", + "integrity": "sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==", "dev": true, - "license": "CC0-1.0", "dependencies": { "@csstools/selector-specificity": "^2.0.0", "postcss-selector-parser": "^6.0.10" @@ -4308,8 +2914,9 @@ }, "node_modules/@csstools/postcss-nested-calc": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz", + "integrity": "sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ==", "dev": true, - "license": "CC0-1.0", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -4326,8 +2933,9 @@ }, "node_modules/@csstools/postcss-normalize-display-values": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz", + "integrity": "sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==", "dev": true, - "license": "CC0-1.0", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -4344,8 +2952,9 @@ }, "node_modules/@csstools/postcss-oklab-function": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz", + "integrity": "sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==", "dev": true, - "license": "CC0-1.0", "dependencies": { "@csstools/postcss-progressive-custom-properties": "^1.1.0", "postcss-value-parser": "^4.2.0" @@ -4363,8 +2972,9 @@ }, "node_modules/@csstools/postcss-progressive-custom-properties": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz", + "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==", "dev": true, - "license": "CC0-1.0", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -4377,8 +2987,9 @@ }, "node_modules/@csstools/postcss-stepped-value-functions": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz", + "integrity": "sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==", "dev": true, - "license": "CC0-1.0", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -4395,8 +3006,9 @@ }, "node_modules/@csstools/postcss-text-decoration-shorthand": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz", + "integrity": "sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw==", "dev": true, - "license": "CC0-1.0", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -4413,8 +3025,9 @@ }, "node_modules/@csstools/postcss-trigonometric-functions": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz", + "integrity": "sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og==", "dev": true, - "license": "CC0-1.0", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -4431,8 +3044,9 @@ }, "node_modules/@csstools/postcss-unset-value": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz", + "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==", "dev": true, - "license": "CC0-1.0", "engines": { "node": "^12 || ^14 || >=16" }, @@ -4445,39 +3059,59 @@ } }, "node_modules/@csstools/selector-specificity": { - "version": "2.0.2", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz", + "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==", "dev": true, - "license": "CC0-1.0", "engines": { - "node": "^12 || ^14 || >=16" + "node": "^14 || ^16 || >=18" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/csstools" }, "peerDependencies": { - "postcss": "^8.2", "postcss-selector-parser": "^6.0.10" } }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "dev": true, - "license": "MIT", "engines": { "node": ">=10.0.0" } }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.5.tgz", + "integrity": "sha512-UHkDFCfSGTuXq08oQltXxSZmH1TXyWsL+4QhZDWvvLl6mEJQqk3u7/wq1LjhrrAXYIllaTtRSzUXl4Olkf2J8A==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", - "devOptional": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "devOptional": true }, "node_modules/@ionic/angular": { - "version": "6.2.7", - "license": "MIT", + "version": "6.7.5", + "resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-6.7.5.tgz", + "integrity": "sha512-nV8HP7RedjYkIAT8nVr5ifHNT0D3XzA74RPG3/WCCFJKunERNJ9SBiNkCTWhUpSkqsYYwEB4+SOOHz+R5NLk/w==", "dependencies": { - "@ionic/core": "^6.2.7", + "@ionic/core": "6.7.5", + "ionicons": "^6.1.3", "jsonc-parser": "^3.0.0", "tslib": "^2.0.0" }, @@ -4490,9 +3124,10 @@ } }, "node_modules/@ionic/cli": { - "version": "6.20.1", + "version": "6.20.9", + "resolved": "https://registry.npmjs.org/@ionic/cli/-/cli-6.20.9.tgz", + "integrity": "sha512-sItLCi7zXq1zARWIpZDinHhK8hvy+wzOx176QMOJV90BjDybkjGYu3rGu5TBjoqn104dRIZTC8rtCsnD/P3cQw==", "dev": true, - "license": "MIT", "dependencies": { "@ionic/cli-framework": "5.1.3", "@ionic/cli-framework-output": "2.2.5", @@ -4530,8 +3165,9 @@ }, "node_modules/@ionic/cli-framework": { "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework/-/cli-framework-5.1.3.tgz", + "integrity": "sha512-T2KN/TurzNoAcc3iDt1KHU6GeEa7x9kXngMnu5xs+DzJv5HhBKjVOoo74b8rgVxdPx+dLOV8aLrorlyvsHR/tQ==", "dev": true, - "license": "MIT", "dependencies": { "@ionic/cli-framework-output": "2.2.5", "@ionic/utils-array": "2.1.5", @@ -4555,8 +3191,9 @@ }, "node_modules/@ionic/cli-framework-output": { "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.5.tgz", + "integrity": "sha512-YeDLTnTaE6V4IDUxT8GDIep0GuRIFaR7YZDLANMuuWJZDmnTku6DP+MmQoltBeLmVvz1BAAZgk41xzxdq6H2FQ==", "dev": true, - "license": "MIT", "dependencies": { "@ionic/utils-terminal": "2.3.3", "debug": "^4.0.0", @@ -4568,8 +3205,9 @@ }, "node_modules/@ionic/cli-framework-prompts": { "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework-prompts/-/cli-framework-prompts-2.1.10.tgz", + "integrity": "sha512-h8HbA0teR0vWtGKB3ahzRbDq4yYaxfukgbOqhu9CAEJHosoFlBmDB8PbPnGFYxUg2J1MuCqeiN2ftJQYV/BO1w==", "dev": true, - "license": "MIT", "dependencies": { "@ionic/utils-terminal": "2.3.3", "debug": "^4.0.0", @@ -4582,8 +3220,9 @@ }, "node_modules/@ionic/cli-framework-prompts/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -4596,8 +3235,9 @@ }, "node_modules/@ionic/cli-framework-prompts/node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -4611,8 +3251,9 @@ }, "node_modules/@ionic/cli-framework-prompts/node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -4622,21 +3263,24 @@ }, "node_modules/@ionic/cli-framework-prompts/node_modules/color-name": { "version": "1.1.4", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/@ionic/cli-framework-prompts/node_modules/has-flag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@ionic/cli-framework-prompts/node_modules/inquirer": { "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", "dev": true, - "license": "MIT", "dependencies": { "ansi-escapes": "^4.2.1", "chalk": "^4.1.0", @@ -4658,8 +3302,9 @@ }, "node_modules/@ionic/cli-framework-prompts/node_modules/rxjs": { "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "dev": true, - "license": "Apache-2.0", "dependencies": { "tslib": "^1.9.0" }, @@ -4669,13 +3314,15 @@ }, "node_modules/@ionic/cli-framework-prompts/node_modules/rxjs/node_modules/tslib": { "version": "1.14.1", - "dev": true, - "license": "0BSD" + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true }, "node_modules/@ionic/cli-framework-prompts/node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -4685,8 +3332,9 @@ }, "node_modules/@ionic/cli-framework/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -4699,8 +3347,9 @@ }, "node_modules/@ionic/cli-framework/node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -4714,8 +3363,9 @@ }, "node_modules/@ionic/cli-framework/node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -4725,21 +3375,24 @@ }, "node_modules/@ionic/cli-framework/node_modules/color-name": { "version": "1.1.4", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/@ionic/cli-framework/node_modules/has-flag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@ionic/cli-framework/node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -4749,8 +3402,9 @@ }, "node_modules/@ionic/cli/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -4763,8 +3417,9 @@ }, "node_modules/@ionic/cli/node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -4778,8 +3433,9 @@ }, "node_modules/@ionic/cli/node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -4789,21 +3445,24 @@ }, "node_modules/@ionic/cli/node_modules/color-name": { "version": "1.1.4", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/@ionic/cli/node_modules/has-flag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@ionic/cli/node_modules/open": { "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", "dev": true, - "license": "MIT", "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" @@ -4817,8 +3476,9 @@ }, "node_modules/@ionic/cli/node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -4827,18 +3487,20 @@ } }, "node_modules/@ionic/core": { - "version": "6.2.7", - "license": "MIT", + "version": "6.7.5", + "resolved": "https://registry.npmjs.org/@ionic/core/-/core-6.7.5.tgz", + "integrity": "sha512-zRkRn+h/Vs3xt/EVgBdShMKDyeGOM4RU31NPF2icfu3CUTH+VrMV569MUnNjYvd1Lu2xK90pYy4TaicSWmC1Pw==", "dependencies": { - "@stencil/core": "^2.17.4", - "ionicons": "^6.0.3", + "@stencil/core": "^2.18.0", + "ionicons": "^6.1.3", "tslib": "^2.1.0" } }, "node_modules/@ionic/utils-array": { "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.5.tgz", + "integrity": "sha512-HD72a71IQVBmQckDwmA8RxNVMTbxnaLbgFOl+dO5tbvW9CkkSFCv41h6fUuNsSEVgngfkn0i98HDuZC8mk+lTA==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^4.0.0", "tslib": "^2.0.1" @@ -4849,8 +3511,9 @@ }, "node_modules/@ionic/utils-fs": { "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.6.tgz", + "integrity": "sha512-eikrNkK89CfGPmexjTfSWl4EYqsPSBh0Ka7by4F0PLc1hJZYtJxUZV3X4r5ecA8ikjicUmcbU7zJmAjmqutG/w==", "dev": true, - "license": "MIT", "dependencies": { "@types/fs-extra": "^8.0.0", "debug": "^4.0.0", @@ -4863,8 +3526,9 @@ }, "node_modules/@ionic/utils-network": { "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-network/-/utils-network-2.1.5.tgz", + "integrity": "sha512-HUQ1Ec4Mh2MXzzKdbbbDS6xYKwpFJ2XRY7SYXbaZT8+jiNahfHbsOfe62/p8bk41Yil7E9EagzGC2JvIFJh01w==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^4.0.0", "tslib": "^2.0.1" @@ -4875,8 +3539,9 @@ }, "node_modules/@ionic/utils-object": { "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.5.tgz", + "integrity": "sha512-XnYNSwfewUqxq+yjER1hxTKggftpNjFLJH0s37jcrNDwbzmbpFTQTVAp4ikNK4rd9DOebX/jbeZb8jfD86IYxw==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^4.0.0", "tslib": "^2.0.1" @@ -4887,8 +3552,9 @@ }, "node_modules/@ionic/utils-process": { "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.10.tgz", + "integrity": "sha512-mZ7JEowcuGQK+SKsJXi0liYTcXd2bNMR3nE0CyTROpMECUpJeAvvaBaPGZf5ERQUPeWBVuwqAqjUmIdxhz5bxw==", "dev": true, - "license": "MIT", "dependencies": { "@ionic/utils-object": "2.1.5", "@ionic/utils-terminal": "2.3.3", @@ -4903,8 +3569,9 @@ }, "node_modules/@ionic/utils-stream": { "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.5.tgz", + "integrity": "sha512-hkm46uHvEC05X/8PHgdJi4l4zv9VQDELZTM+Kz69odtO9zZYfnt8DkfXHJqJ+PxmtiE5mk/ehJWLnn/XAczTUw==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^4.0.0", "tslib": "^2.0.1" @@ -4915,8 +3582,9 @@ }, "node_modules/@ionic/utils-subprocess": { "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-2.1.11.tgz", + "integrity": "sha512-6zCDixNmZCbMCy5np8klSxOZF85kuDyzZSTTQKQP90ZtYNCcPYmuFSzaqDwApJT4r5L3MY3JrqK1gLkc6xiUPw==", "dev": true, - "license": "MIT", "dependencies": { "@ionic/utils-array": "2.1.5", "@ionic/utils-fs": "3.1.6", @@ -4933,8 +3601,9 @@ }, "node_modules/@ionic/utils-terminal": { "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.3.tgz", + "integrity": "sha512-RnuSfNZ5fLEyX3R5mtcMY97cGD1A0NVBbarsSQ6yMMfRJ5YHU7hHVyUfvZeClbqkBC/pAqI/rYJuXKCT9YeMCQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/slice-ansi": "^4.0.0", "debug": "^4.0.0", @@ -4952,8 +3621,9 @@ }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, - "license": "ISC", "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", @@ -4967,16 +3637,18 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, - "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -4985,18 +3657,26 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@jridgewell/gen-mapping": { "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", "dev": true, - "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.0.0", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -5006,65 +3686,106 @@ } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.2", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, - "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, - "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "dev": true, - "license": "MIT" + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.15", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, - "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.4", - "dev": true, - "license": "MIT" + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true + }, + "node_modules/@maskito/angular": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@maskito/angular/-/angular-1.9.0.tgz", + "integrity": "sha512-Wa/9nM9Nv0oieVZ6yxQNXfDRA4obFDR15xO16o1GKF8i9W1IdQQn+tuMRjkmx6HhJDN9+x3k8OTJ1f80BIrhjA==", + "dependencies": { + "tslib": "2.6.2" + }, + "peerDependencies": { + "@angular/common": ">=12.0.0", + "@angular/core": ">=12.0.0", + "@angular/forms": ">=12.0.0", + "@maskito/core": "^1.9.0", + "rxjs": ">=6.0.0" + } + }, + "node_modules/@maskito/angular/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/@maskito/core": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@maskito/core/-/core-1.9.0.tgz", + "integrity": "sha512-WQIUrwkdIUg6PzAb4Apa0RjTPHB0EqZLc9/7kWCKVIixhkITRFXFg2BhiDVSRv0mIKVlAEJcOvvjHK5T3UaIig==" + }, + "node_modules/@maskito/kit": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@maskito/kit/-/kit-1.9.0.tgz", + "integrity": "sha512-LNNgOJ0tAfrPoPehvoP+ZyYF9giOYL02sOMKyDC3IcqDNA8BAU0PARmS7TNsVEBpvSuJhU6xVt40nxJaONgUdw==", + "peerDependencies": { + "@maskito/core": "^1.9.0" + } }, "node_modules/@materia-ui/ngx-monaco-editor": { "version": "6.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@materia-ui/ngx-monaco-editor/-/ngx-monaco-editor-6.0.0.tgz", + "integrity": "sha512-gTqNQjOGznZxOC0NlmKdKSGCJuTts8YmK4dsTQAGc5IgIV7cZdQWiW6AL742h0ruED6q0cAunEYjXT6jzHBoIQ==", "dependencies": { "tslib": "^2.0.0" }, @@ -5074,8 +3795,9 @@ } }, "node_modules/@ng-web-apis/common": { - "version": "2.1.0", - "license": "MIT", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@ng-web-apis/common/-/common-3.0.6.tgz", + "integrity": "sha512-ral+lzGpFS3aOCFB5DcHOI4lZhhp8GH4BnjSbngH2Xk8J0FKYdxRzvcPQVy7hS+TPUu0tW9uFVp6cC7odu3iyQ==", "dependencies": { "tslib": "^2.2.0" }, @@ -5086,8 +3808,9 @@ } }, "node_modules/@ng-web-apis/intersection-observer": { - "version": "3.0.0", - "license": "MIT", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@ng-web-apis/intersection-observer/-/intersection-observer-3.2.0.tgz", + "integrity": "sha512-EhwqEZJFKR9pz55TWp82qyWTXdg8TZeMP6bUw26bVHz8CTkgrpzaXdtxurMTvJ/+gwuFy4JSJLjBeV9nfZ/SXA==", "dependencies": { "tslib": "^2.2.0" }, @@ -5097,8 +3820,9 @@ } }, "node_modules/@ng-web-apis/mutation-observer": { - "version": "2.0.0", - "license": "MIT", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@ng-web-apis/mutation-observer/-/mutation-observer-3.2.1.tgz", + "integrity": "sha512-a7krkMx0e9cfnutClwDylWjbTQVRHUP3oUik/nBvUdKlk/Q4anNww9aIKJ64VgiXR+1ZF8OmHGl0+XUzN6xP9Q==", "dependencies": { "tslib": "^2.2.0" }, @@ -5108,8 +3832,9 @@ } }, "node_modules/@ng-web-apis/resize-observer": { - "version": "2.0.0", - "license": "MIT", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@ng-web-apis/resize-observer/-/resize-observer-3.2.1.tgz", + "integrity": "sha512-r1YaZUo6DIDeR+4/C/pM4Ar0eTQBxjK0FUhYYJ512EnN8RqAn8d3a0wM8ZYunucwDICwW1sx4IIV6PZ2G77xsg==", "dependencies": { "tslib": "^2.2.0" }, @@ -5118,10 +3843,26 @@ "@ng-web-apis/common": ">=2.0.0" } }, + "node_modules/@ngtools/webpack": { + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.13.tgz", + "integrity": "sha512-RQx/rGX7K/+R55x1R6Ax1JzyeHi8cW11dEXpzHWipyuSpusQLUN53F02eMB4VTakXsL3mFNWWy4bX3/LSq8/9w==", + "dev": true, + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^14.0.0", + "typescript": ">=4.6.2 <4.9", + "webpack": "^5.54.0" + } + }, "node_modules/@noble/curves": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", - "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", "dependencies": { "@noble/hashes": "1.4.0" }, @@ -5142,8 +3883,9 @@ }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "devOptional": true, - "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -5154,16 +3896,18 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "devOptional": true, - "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "devOptional": true, - "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -5174,8 +3918,9 @@ }, "node_modules/@npmcli/fs": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", "devOptional": true, - "license": "ISC", "dependencies": { "@gar/promisify": "^1.1.3", "semver": "^7.3.5" @@ -5186,8 +3931,9 @@ }, "node_modules/@npmcli/git": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-3.0.2.tgz", + "integrity": "sha512-CAcd08y3DWBJqJDpfuVL0uijlq5oaXaOJEKHKc4wqrjd00gkvTZB+nFuLn+doOOKddaQS9JfqtNoFCO2LCvA3w==", "devOptional": true, - "license": "ISC", "dependencies": { "@npmcli/promise-spawn": "^3.0.0", "lru-cache": "^7.4.4", @@ -5203,10 +3949,20 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "devOptional": true, + "engines": { + "node": ">=12" + } + }, "node_modules/@npmcli/installed-package-contents": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-1.0.7.tgz", + "integrity": "sha512-9rufe0wnJusCQoLpV9ZPKIVP55itrM5BxOXs10DmdbRfgWtHy1LDyskbwRnBghuB0PrF7pNPOqREVtpz4HqzKw==", "devOptional": true, - "license": "ISC", "dependencies": { "npm-bundled": "^1.1.1", "npm-normalize-package-bin": "^1.0.1" @@ -5220,8 +3976,10 @@ }, "node_modules/@npmcli/move-file": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", "devOptional": true, - "license": "MIT", "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" @@ -5232,16 +3990,18 @@ }, "node_modules/@npmcli/node-gyp": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-2.0.0.tgz", + "integrity": "sha512-doNI35wIe3bBaEgrlPfdJPaCpUR89pJWep4Hq3aRdh6gKazIVWfs0jHttvSSoq47ZXgC7h73kDsUl8AoIQUB+A==", "devOptional": true, - "license": "ISC", "engines": { "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/@npmcli/promise-spawn": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-3.0.0.tgz", + "integrity": "sha512-s9SgS+p3a9Eohe68cSI3fi+hpcZUmXq5P7w0kMlAsWVtR7XbK3ptkZqKT2cK1zLDObJ3sR+8P59sJE0w/KTL1g==", "devOptional": true, - "license": "ISC", "dependencies": { "infer-owner": "^1.0.4" }, @@ -5251,8 +4011,9 @@ }, "node_modules/@npmcli/run-script": { "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-4.2.1.tgz", + "integrity": "sha512-7dqywvVudPSrRCW5nTHpHgeWnbBtz8cFkOuKrecm6ih+oO9ciydhWt6OF7HlqupRRmB8Q/gECVdB9LMfToJbRg==", "devOptional": true, - "license": "ISC", "dependencies": { "@npmcli/node-gyp": "^2.0.0", "@npmcli/promise-spawn": "^3.0.0", @@ -5265,14 +4026,16 @@ } }, "node_modules/@polka/url": { - "version": "1.0.0-next.21", - "dev": true, - "license": "MIT" + "version": "1.0.0-next.25", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", + "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==", + "dev": true }, "node_modules/@rollup/plugin-json": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-4.1.0.tgz", + "integrity": "sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==", "dev": true, - "license": "MIT", "dependencies": { "@rollup/pluginutils": "^3.0.8" }, @@ -5282,8 +4045,9 @@ }, "node_modules/@rollup/plugin-node-resolve": { "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.3.0.tgz", + "integrity": "sha512-Lus8rbUo1eEcnS4yTFKLZrVumLPY+YayBdWXgFSHYhTT2iJbMhoaaBL3xl5NCdeRytErGr8tZ0L71BMRmnlwSw==", "dev": true, - "license": "MIT", "dependencies": { "@rollup/pluginutils": "^3.1.0", "@types/resolve": "1.17.1", @@ -5301,8 +4065,9 @@ }, "node_modules/@rollup/pluginutils": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", "dev": true, - "license": "MIT", "dependencies": { "@types/estree": "0.0.39", "estree-walker": "^1.0.1", @@ -5317,16 +4082,17 @@ }, "node_modules/@rollup/pluginutils/node_modules/@types/estree": { "version": "0.0.39", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true }, "node_modules/@schematics/angular": { - "version": "14.2.3", - "devOptional": true, - "license": "MIT", + "version": "14.2.13", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-14.2.13.tgz", + "integrity": "sha512-MLxTpTU3E8QACQ/5c0sENMR2gRiMXpGaKeD5IHY+3wyU2fUSJVB0QPU/l1WhoyZbX8N9ospBgf5UEG7taVF9rg==", "dependencies": { - "@angular-devkit/core": "14.2.3", - "@angular-devkit/schematics": "14.2.3", + "@angular-devkit/core": "14.2.13", + "@angular-devkit/schematics": "14.2.13", "jsonc-parser": "3.1.0" }, "engines": { @@ -5342,15 +4108,17 @@ }, "node_modules/@start9labs/emver": { "version": "0.1.5", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@start9labs/emver/-/emver-0.1.5.tgz", + "integrity": "sha512-1dhiG03VkfEwSLx/JPKVms6srAbYFQgwfSGhwpUKMDliMXuAHGVaueStmqzVxn3JpH/HEVz0QW8w/PXHqjdiIg==" }, "node_modules/@start9labs/start-sdk": { "resolved": "../sdk/dist", "link": true }, "node_modules/@stencil/core": { - "version": "2.18.0", - "license": "MIT", + "version": "2.22.3", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.22.3.tgz", + "integrity": "sha512-kmVA0M/HojwsfkeHsifvHVIYe4l5tin7J5+DLgtl8h6WWfiMClND5K3ifCXXI2ETDNKiEk21p6jql3Fx9o2rng==", "bin": { "stencil": "bin/stencil" }, @@ -5360,33 +4128,59 @@ } }, "node_modules/@taiga-ui/addon-charts": { - "version": "3.20.0", - "license": "Apache-2.0", + "version": "3.84.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-3.84.0.tgz", + "integrity": "sha512-XR7UFywnrv4NRLHOCbba63gXDYYDL4Rt0MbjnF54p5U2EXnbt2of7VbjlB6cPx40XkQqfqa3CNayYxWZP82Ijg==", "dependencies": { - "tslib": ">=2.0.0" + "tslib": "^2.6.2" }, "peerDependencies": { "@angular/common": ">=12.0.0", "@angular/core": ">=12.0.0", - "@ng-web-apis/common": ">=2.0.0", - "@taiga-ui/cdk": ">=3.20.0", - "@taiga-ui/core": ">=3.20.0", - "@tinkoff/ng-polymorpheus": ">=4.0.0" + "@ng-web-apis/common": "^3.0.6", + "@taiga-ui/cdk": "^3.84.0", + "@taiga-ui/core": "^3.84.0", + "@tinkoff/ng-polymorpheus": "^4.3.0" + } + }, + "node_modules/@taiga-ui/addon-commerce": { + "version": "3.84.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-3.84.0.tgz", + "integrity": "sha512-1zqLwnZLAYYcHvjH89d7JmtV2+QeZ2YnSJ3YWEMNLjGPzpev4RvQXtDfglIyu0LCyTxqpXmuzes9v/cgq2P5TQ==", + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "peerDependencies": { + "@angular/common": ">=12.0.0", + "@angular/core": ">=12.0.0", + "@angular/forms": ">=12.0.0", + "@maskito/angular": "^1.9.0", + "@maskito/core": "^1.9.0", + "@maskito/kit": "^1.9.0", + "@ng-web-apis/common": "^3.0.6", + "@taiga-ui/cdk": "^3.84.0", + "@taiga-ui/core": "^3.84.0", + "@taiga-ui/i18n": "^3.84.0", + "@taiga-ui/kit": "^3.84.0", + "@tinkoff/ng-polymorpheus": "^4.3.0", + "rxjs": ">=6.0.0" } }, "node_modules/@taiga-ui/cdk": { - "version": "3.20.0", - "license": "Apache-2.0", + "version": "3.84.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-3.84.0.tgz", + "integrity": "sha512-0umw/CUmYNEYOCUNQVTQS53zXzxZsH/6+lj1mFVzocvfJFJWAUT6ltCH9QvxYmxSDDGWwNGg16AaVo2K+aGL0w==", "dependencies": { - "@ng-web-apis/common": "2.1.0", - "@ng-web-apis/mutation-observer": "2.0.0", - "@ng-web-apis/resize-observer": "2.0.0", - "@tinkoff/ng-event-plugins": "3.1.0", - "@tinkoff/ng-polymorpheus": "4.0.10", - "tslib": "2.5.0" + "@ng-web-apis/common": "3.0.6", + "@ng-web-apis/mutation-observer": "3.1.0", + "@ng-web-apis/resize-observer": "3.0.6", + "@tinkoff/ng-event-plugins": "3.2.0", + "@tinkoff/ng-polymorpheus": "4.3.0", + "tslib": "2.6.2" }, "optionalDependencies": { - "ng-morph": "2.1.0", + "ng-morph": "4.0.5", "parse5": "6.0.1" }, "peerDependencies": { @@ -5397,16 +4191,42 @@ "rxjs": ">=6.0.0" } }, + "node_modules/@taiga-ui/cdk/node_modules/@ng-web-apis/mutation-observer": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@ng-web-apis/mutation-observer/-/mutation-observer-3.1.0.tgz", + "integrity": "sha512-MFN0TLLBMFJJPpXkGFe9ChRCSOKvMHZRRtBq5jHWS7tv5/CtdUkqW5CU7RC9KTzZjGeMzYe0cXO4JRkjL5aZ9g==", + "dependencies": { + "tslib": "^2.2.0" + }, + "peerDependencies": { + "@angular/core": ">=12.0.0", + "@ng-web-apis/common": ">=2.0.0" + } + }, + "node_modules/@taiga-ui/cdk/node_modules/@ng-web-apis/resize-observer": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@ng-web-apis/resize-observer/-/resize-observer-3.0.6.tgz", + "integrity": "sha512-QdGYdEdC0AzFonLfNOnyYyeCwnvK9jlskoeefvJN3Yyvds3ivBrrTjpeDOdiLsQpCPBp9/673imgq7355vkQow==", + "dependencies": { + "tslib": "^2.2.0" + }, + "peerDependencies": { + "@angular/core": ">=12.0.0", + "@ng-web-apis/common": ">=2.0.0" + } + }, "node_modules/@taiga-ui/cdk/node_modules/tslib": { - "version": "2.5.0", - "license": "0BSD" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@taiga-ui/core": { - "version": "3.20.0", - "license": "Apache-2.0", + "version": "3.84.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-3.84.0.tgz", + "integrity": "sha512-FZy77z0E4qjYcszVcp+qPFkPwJPl8qXZb7t2P+juUtJvSmSn2foQHHdyhbIYN808H26tqCdgkTMG1BWQxVuDSg==", "dependencies": { - "@taiga-ui/i18n": "^3.20.0", - "tslib": ">=2.0.0" + "@taiga-ui/i18n": "^3.84.0", + "tslib": "^2.6.2" }, "peerDependencies": { "@angular/animations": ">=12.0.0", @@ -5415,52 +4235,81 @@ "@angular/forms": ">=12.0.0", "@angular/platform-browser": ">=12.0.0", "@angular/router": ">=12.0.0", - "@ng-web-apis/common": ">=2.0.0", - "@ng-web-apis/mutation-observer": ">=2.0.0", - "@taiga-ui/cdk": ">=3.20.0", - "@taiga-ui/i18n": ">=3.20.0", - "@tinkoff/ng-event-plugins": ">=3.1.0", - "@tinkoff/ng-polymorpheus": ">=4.0.0", + "@ng-web-apis/common": "^3.0.6", + "@ng-web-apis/mutation-observer": "^3.1.0", + "@taiga-ui/cdk": "^3.84.0", + "@taiga-ui/i18n": "^3.84.0", + "@tinkoff/ng-event-plugins": "^3.2.0", + "@tinkoff/ng-polymorpheus": "^4.3.0", + "rxjs": ">=6.0.0" + } + }, + "node_modules/@taiga-ui/experimental": { + "version": "3.84.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/experimental/-/experimental-3.84.0.tgz", + "integrity": "sha512-q0hNVy+EmywCG8hpZlg/+haKIFhnmxicQiSeV/D1P7CHO10safjGo0ptT6e1hYMFa5/cJZOM4OwDPen2xs17Wg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "peerDependencies": { + "@angular/common": ">=12.0.0", + "@angular/core": ">=12.0.0", + "@taiga-ui/addon-commerce": "^3.84.0", + "@taiga-ui/cdk": "^3.84.0", + "@taiga-ui/core": "^3.84.0", + "@taiga-ui/kit": "^3.84.0", + "@tinkoff/ng-polymorpheus": "^4.3.0", "rxjs": ">=6.0.0" } }, "node_modules/@taiga-ui/i18n": { - "version": "3.20.0", - "license": "Apache-2.0", + "version": "3.85.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-3.85.0.tgz", + "integrity": "sha512-CGoxfq9WY+psX5ZOfWmuQZ6OA/0CAPYJTlbHkw5sRKAyhEQ3NM/Wbx3xcwrcYRRJDnt9yOlfibz+3a+WDF2bFA==", "dependencies": { - "tslib": ">=2.0.0" + "tslib": "^2.6.2" }, "peerDependencies": { "@angular/core": ">=12.0.0", + "@ng-web-apis/common": "^3.0.6", "rxjs": ">=6.0.0" } }, "node_modules/@taiga-ui/icons": { - "version": "3.20.0", - "license": "Apache-2.0", + "version": "3.84.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-3.84.0.tgz", + "integrity": "sha512-KiH7BJRZ6wbkOHlJAS0XHq2gYnQTpRgdEogKW+GoD0da/4trCdM66vhDk2j0DwDFdBGq5U0inHJCjnskBI1nSQ==", "dependencies": { - "tslib": "^2.2.0" + "tslib": "^2.6.2" + }, + "peerDependencies": { + "@taiga-ui/cdk": "^3.84.0" } }, "node_modules/@taiga-ui/kit": { - "version": "3.20.0", - "license": "Apache-2.0", + "version": "3.84.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-3.84.0.tgz", + "integrity": "sha512-lSUPDco5FeBYK3ESnXeEPLCdMCmNXwcdHNK/we+0ZoH4VPx/OGg2hpEP0Fej7jfGHwXFTzDbufQD0hT6WlfTAw==", "dependencies": { - "@ng-web-apis/intersection-observer": "3.0.0", + "@maskito/angular": "1.9.0", + "@maskito/core": "1.9.0", + "@maskito/kit": "1.9.0", + "@ng-web-apis/intersection-observer": "3.2.0", "text-mask-core": "5.1.2", - "tslib": ">=2.0.0" + "tslib": "^2.6.2" }, "peerDependencies": { "@angular/common": ">=12.0.0", "@angular/core": ">=12.0.0", "@angular/forms": ">=12.0.0", "@angular/router": ">=12.0.0", - "@ng-web-apis/common": ">=2.0.0", - "@ng-web-apis/mutation-observer": ">=2.0.0", - "@taiga-ui/cdk": ">=3.20.0", - "@taiga-ui/core": ">=3.20.0", - "@taiga-ui/i18n": ">=3.20.0", - "@tinkoff/ng-polymorpheus": ">=4.0.0", + "@ng-web-apis/common": "3.0.6", + "@ng-web-apis/mutation-observer": "^3.1.0", + "@ng-web-apis/resize-observer": "^3.0.6", + "@taiga-ui/cdk": "^3.84.0", + "@taiga-ui/core": "^3.84.0", + "@taiga-ui/i18n": "^3.84.0", + "@tinkoff/ng-polymorpheus": "^4.3.0", "rxjs": ">=6.0.0" } }, @@ -5479,8 +4328,9 @@ } }, "node_modules/@tinkoff/ng-event-plugins": { - "version": "3.1.0", - "license": "Apache-2.0", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@tinkoff/ng-event-plugins/-/ng-event-plugins-3.2.0.tgz", + "integrity": "sha512-n56R5xNfiytabh2WmWdQXfNU6m7dfOo3LLxlARE+DX7f5yciW2xBdDkuEHX74q8dlCuAVlW9aslSfz8c//ymwA==", "dependencies": { "tslib": "^2.2.0" }, @@ -5491,128 +4341,158 @@ } }, "node_modules/@tinkoff/ng-polymorpheus": { - "version": "4.0.10", - "license": "Apache-2.0", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tinkoff/ng-polymorpheus/-/ng-polymorpheus-4.3.0.tgz", + "integrity": "sha512-Ck/XCLuBwlUgvK22PxTlLTZhGG6I32kLqLYtDQh8N/QZZhs40+hb/78/ElFGzD567CCvrzNnueFkaOoXhuEVrw==", "dependencies": { - "tslib": "^2.0.0" + "tslib": "2.6.2" }, "peerDependencies": { - "@angular/core": ">=12.0.0" + "@angular/core": ">=12.0.0", + "@angular/platform-browser": ">=12.0.0" } }, + "node_modules/@tinkoff/ng-polymorpheus/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/@tootallnate/once": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", "devOptional": true, - "license": "MIT", "engines": { "node": ">= 10" } }, "node_modules/@ts-morph/common": { - "version": "0.9.2", - "license": "MIT", + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.22.0.tgz", + "integrity": "sha512-HqNBuV/oIlMKdkLshXd1zKBqNQCsuPEsgQOkfFQ/eUKjRlwndXW1AjN9LVkBEIukm00gGXSRmfkl0Wv5VXLnlw==", "optional": true, "dependencies": { - "fast-glob": "^3.2.5", - "minimatch": "^3.0.4", - "mkdirp": "^1.0.4", + "fast-glob": "^3.3.2", + "minimatch": "^9.0.3", + "mkdirp": "^3.0.1", "path-browserify": "^1.0.1" } }, - "node_modules/@ts-morph/common/node_modules/brace-expansion": { - "version": "1.1.11", - "license": "MIT", - "optional": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/@ts-morph/common/node_modules/minimatch": { - "version": "3.1.2", - "license": "ISC", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "optional": true, "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@ts-morph/common/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "optional": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@tsconfig/node10": { - "version": "1.0.9", - "dev": true, - "license": "MIT" + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true }, "node_modules/@tsconfig/node12": { "version": "1.0.11", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true }, "node_modules/@tsconfig/node14": { "version": "1.0.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true }, "node_modules/@tsconfig/node16": { - "version": "1.0.3", - "dev": true, - "license": "MIT" + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true }, "node_modules/@types/body-parser": { - "version": "1.19.2", + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", "dev": true, - "license": "MIT", "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "node_modules/@types/bonjour": { - "version": "3.5.10", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/connect": { - "version": "3.4.35", + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/connect-history-api-fallback": { - "version": "1.3.5", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", "dev": true, - "license": "MIT", "dependencies": { "@types/express-serve-static-core": "*", "@types/node": "*" } }, "node_modules/@types/dompurify": { - "version": "2.3.4", - "license": "MIT", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.4.0.tgz", + "integrity": "sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg==", "dependencies": { "@types/trusted-types": "*" } }, "node_modules/@types/eslint": { - "version": "8.4.6", + "version": "8.56.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", + "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "node_modules/@types/eslint-scope": { - "version": "3.7.4", + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, - "license": "MIT", "dependencies": { "@types/eslint": "*", "@types/estree": "*" @@ -5620,332 +4500,422 @@ }, "node_modules/@types/estree": { "version": "0.0.51", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "dev": true }, "node_modules/@types/express": { - "version": "4.17.14", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", + "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.31", + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*", "@types/qs": "*", - "@types/range-parser": "*" + "@types/range-parser": "*", + "@types/send": "*" } }, "node_modules/@types/fs-extra": { - "version": "8.1.2", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz", + "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, "node_modules/@types/http-proxy": { - "version": "1.17.9", + "version": "1.17.14", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", + "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/js-yaml": { - "version": "4.0.5", - "dev": true, - "license": "MIT" + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true }, "node_modules/@types/json-schema": { - "version": "7.0.11", - "dev": true, - "license": "MIT" + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true }, "node_modules/@types/marked": { - "version": "4.0.7", - "dev": true, - "license": "MIT" + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.2.tgz", + "integrity": "sha512-a79Yc3TOk6dGdituy8hmTTJXjOkZ7zsFYV10L337ttq/rec8lRMDBpV7fL3uLx6TgbFCa5DU/h8FmIBQPSbU0w==", + "dev": true }, "node_modules/@types/mime": { - "version": "3.0.1", - "dev": true, - "license": "MIT" + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true }, "node_modules/@types/minimatch": { "version": "3.0.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", "optional": true }, "node_modules/@types/mustache": { - "version": "4.2.1", - "dev": true, - "license": "MIT" + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.5.tgz", + "integrity": "sha512-PLwiVvTBg59tGFL/8VpcGvqOu3L4OuveNvPi0EYbWchRdEVP++yRUXJPFl+CApKEq13017/4Nf7aQ5lTtHUNsA==", + "dev": true }, "node_modules/@types/node": { - "version": "16.11.59", + "version": "16.18.101", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.101.tgz", + "integrity": "sha512-AAsx9Rgz2IzG8KJ6tXd6ndNkVcu+GYB6U/SnFAaokSPNx2N7dcIIfnighYUNumvj6YS2q39Dejz5tT0NCV7CWA==", + "dev": true + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", "dev": true, - "license": "MIT" + "dependencies": { + "@types/node": "*" + } }, "node_modules/@types/node-jose": { - "version": "1.1.10", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@types/node-jose/-/node-jose-1.1.13.tgz", + "integrity": "sha512-QjMd4yhwy1EvSToQn0YI3cD29YhyfxFwj7NecuymjLys2/P0FwxWnkgBlFxCai6Y3aBCe7rbwmqwJJawxlgcXw==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/parse-json": { - "version": "4.0.0", - "dev": true, - "license": "MIT" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true }, "node_modules/@types/pbkdf2": { - "version": "3.1.0", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.1.2.tgz", + "integrity": "sha512-uRwJqmiXmh9++aSu1VNEn3iIxWOhd8AHXNSdlaLfdAAdSTY9jYVeGWnzejM3dvrkbqE3/hyQkQQ29IFATEGlew==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/qs": { - "version": "6.9.7", - "dev": true, - "license": "MIT" + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "dev": true }, "node_modules/@types/range-parser": { - "version": "1.2.4", - "dev": true, - "license": "MIT" + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true }, "node_modules/@types/resolve": { "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/retry": { "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", "dev": true, - "license": "MIT" + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } }, "node_modules/@types/serve-index": { - "version": "1.9.1", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", "dev": true, - "license": "MIT", "dependencies": { "@types/express": "*" } }, "node_modules/@types/serve-static": { - "version": "1.15.0", + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", "dev": true, - "license": "MIT", "dependencies": { - "@types/mime": "*", - "@types/node": "*" + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" } }, "node_modules/@types/slice-ansi": { "version": "4.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==", + "dev": true }, "node_modules/@types/sockjs": { - "version": "0.3.33", + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/trusted-types": { - "version": "2.0.2", - "license": "MIT" + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" }, "node_modules/@types/uuid": { "version": "8.3.4", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "dev": true }, "node_modules/@types/ws": { - "version": "8.5.3", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@webassemblyjs/ast": { - "version": "1.11.1", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", "dev": true, - "license": "MIT" + "peer": true }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.1", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", "dev": true, - "license": "MIT" + "peer": true }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.1", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", "dev": true, - "license": "MIT" + "peer": true }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.1", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", "dev": true, - "license": "MIT" + "peer": true }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.12.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.1", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.1", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", "dev": true, - "license": "Apache-2.0", + "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.1", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", "dev": true, - "license": "MIT" + "peer": true }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.1", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.1", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.1", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.1", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.1", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/ast": "1.12.1", "@xtuc/long": "4.2.2" } }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", - "dev": true, - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true }, "node_modules/@xtuc/long": { "version": "4.2.2", - "dev": true, - "license": "Apache-2.0" + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", - "devOptional": true, - "license": "BSD-2-Clause" + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "devOptional": true }, "node_modules/abab": { "version": "2.0.6", - "dev": true, - "license": "BSD-3-Clause" + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true }, "node_modules/abbrev": { "version": "1.1.1", - "devOptional": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "devOptional": true }, "node_modules/accepts": { "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "dev": true, - "license": "MIT", "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -5955,9 +4925,10 @@ } }, "node_modules/acorn": { - "version": "8.8.0", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, - "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -5966,25 +4937,41 @@ } }, "node_modules/acorn-import-assertions": { - "version": "1.8.0", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", "dev": true, - "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "peer": true, "peerDependencies": { "acorn": "^8" } }, "node_modules/acorn-walk": { - "version": "8.2.0", + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", + "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", "dev": true, - "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, "engines": { "node": ">=0.4.0" } }, "node_modules/adjust-sourcemap-loader": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", "dev": true, - "license": "MIT", "dependencies": { "loader-utils": "^2.0.0", "regex-parser": "^2.2.11" @@ -5995,8 +4982,9 @@ }, "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, - "license": "MIT", "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", @@ -6008,8 +4996,9 @@ }, "node_modules/agent-base": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "devOptional": true, - "license": "MIT", "dependencies": { "debug": "4" }, @@ -6018,12 +5007,11 @@ } }, "node_modules/agentkeepalive": { - "version": "4.2.1", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", "devOptional": true, - "license": "MIT", "dependencies": { - "debug": "^4.1.0", - "depd": "^1.1.2", "humanize-ms": "^1.2.1" }, "engines": { @@ -6032,8 +5020,9 @@ }, "node_modules/aggregate-error": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", "devOptional": true, - "license": "MIT", "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" @@ -6044,7 +5033,8 @@ }, "node_modules/ajv": { "version": "8.11.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -6058,7 +5048,8 @@ }, "node_modules/ajv-formats": { "version": "2.1.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dependencies": { "ajv": "^8.0.0" }, @@ -6073,8 +5064,9 @@ }, "node_modules/ajv-keywords": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -6084,7 +5076,8 @@ }, "node_modules/angular-svg-round-progressbar": { "version": "9.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/angular-svg-round-progressbar/-/angular-svg-round-progressbar-9.0.0.tgz", + "integrity": "sha512-q8d2AEG9u+GMAMrZY40NgejN5fHwR4iK+rRxtJ7NnMEvvuAMqt9UEtKe0SqVQHvZYE6W16L5J9yaO+TEtfRjpw==", "dependencies": { "tslib": "^2.3.0" }, @@ -6095,16 +5088,18 @@ }, "node_modules/ansi-colors": { "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "devOptional": true, - "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/ansi-escapes": { "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "devOptional": true, - "license": "MIT", "dependencies": { "type-fest": "^0.21.3" }, @@ -6117,26 +5112,29 @@ }, "node_modules/ansi-html-community": { "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", "dev": true, "engines": [ "node >= 0.8.0" ], - "license": "Apache-2.0", "bin": { "ansi-html": "bin/ansi-html" } }, "node_modules/ansi-regex": { "version": "5.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "engines": { "node": ">=8" } }, "node_modules/ansi-styles": { "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, - "license": "MIT", "dependencies": { "color-convert": "^1.9.0" }, @@ -6146,7 +5144,8 @@ }, "node_modules/ansi-to-html": { "version": "0.7.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz", + "integrity": "sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==", "dependencies": { "entities": "^2.2.0" }, @@ -6158,9 +5157,10 @@ } }, "node_modules/anymatch": { - "version": "3.1.2", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "devOptional": true, - "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -6171,13 +5171,16 @@ }, "node_modules/aproba": { "version": "2.0.0", - "devOptional": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "devOptional": true }, "node_modules/are-we-there-yet": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", "devOptional": true, - "license": "ISC", "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" @@ -6188,29 +5191,34 @@ }, "node_modules/arg": { "version": "4.1.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true }, "node_modules/argparse": { "version": "2.0.1", - "license": "Python-2.0" + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/array-differ": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", "optional": true, "engines": { "node": ">=8" } }, "node_modules/array-flatten": { - "version": "2.1.2", - "dev": true, - "license": "MIT" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true }, "node_modules/array-union": { "version": "2.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "optional": true, "engines": { "node": ">=8" @@ -6218,7 +5226,8 @@ }, "node_modules/arrify": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", "optional": true, "engines": { "node": ">=8" @@ -6226,8 +5235,9 @@ }, "node_modules/ast-types": { "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", "dev": true, - "license": "MIT", "dependencies": { "tslib": "^2.0.1" }, @@ -6237,29 +5247,33 @@ }, "node_modules/astral-regex": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/asynckit": { "version": "0.4.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true }, "node_modules/at-least-node": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "dev": true, - "license": "ISC", "engines": { "node": ">= 4.0.0" } }, "node_modules/atob": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true, - "license": "(MIT OR Apache-2.0)", "bin": { "atob": "bin/atob.js" }, @@ -6268,7 +5282,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.11", + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", "dev": true, "funding": [ { @@ -6278,13 +5294,16 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "browserslist": "^4.21.3", - "caniuse-lite": "^1.0.30001399", - "fraction.js": "^4.2.0", + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001599", + "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", "postcss-value-parser": "^4.2.0" @@ -6301,8 +5320,9 @@ }, "node_modules/babel-loader": { "version": "8.2.5", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.5.tgz", + "integrity": "sha512-OSiFfH89LrEMiWd4pLNqGz4CwJDtbs2ZVc+iGu2HrkRfPxId9F2anQj38IxWpmRfsUY0aBZYi1EFcd3mhtRMLQ==", "dev": true, - "license": "MIT", "dependencies": { "find-cache-dir": "^3.3.1", "loader-utils": "^2.0.0", @@ -6319,8 +5339,9 @@ }, "node_modules/babel-loader/node_modules/loader-utils": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, - "license": "MIT", "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", @@ -6330,18 +5351,11 @@ "node": ">=8.9.0" } }, - "node_modules/babel-plugin-dynamic-import-node": { - "version": "2.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "object.assign": "^4.1.0" - } - }, "node_modules/babel-plugin-istanbul": { "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", @@ -6355,8 +5369,9 @@ }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", + "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", "dev": true, - "license": "MIT", "dependencies": { "@babel/compat-data": "^7.17.7", "@babel/helper-define-polyfill-provider": "^0.3.3", @@ -6367,17 +5382,19 @@ } }, "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.0", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/babel-plugin-polyfill-corejs3": { "version": "0.5.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.3.tgz", + "integrity": "sha512-zKsXDh0XjnrUEW0mxIHLfjBfnXSMr5Q/goMe/fxpQnLm07mcOZiIZHBNWCMx60HmdvjxfXcalac0tfFg0wqxyw==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.3.2", "core-js-compat": "^3.21.0" @@ -6388,8 +5405,9 @@ }, "node_modules/babel-plugin-polyfill-regenerator": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", + "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", "dev": true, - "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.3.3" }, @@ -6399,11 +5417,14 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "devOptional": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "devOptional": true }, "node_modules/base64-js": { "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "funding": [ { "type": "github", @@ -6417,40 +5438,47 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "MIT" + ] }, "node_modules/base64url": { "version": "3.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", "engines": { "node": ">=6.0.0" } }, "node_modules/batch": { "version": "0.6.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true }, "node_modules/big.js": { "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", "dev": true, - "license": "MIT", "engines": { "node": "*" } }, "node_modules/binary-extensions": { - "version": "2.2.0", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "devOptional": true, - "license": "MIT", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/bl": { "version": "4.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -6459,6 +5487,8 @@ }, "node_modules/bl/node_modules/buffer": { "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "funding": [ { "type": "github", @@ -6473,27 +5503,27 @@ "url": "https://feross.org/support" } ], - "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "node_modules/body-parser": { - "version": "1.20.0", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dev": true, - "license": "MIT", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.10.3", - "raw-body": "2.5.1", + "qs": "6.11.0", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -6504,29 +5534,24 @@ }, "node_modules/body-parser/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "license": "MIT", "dependencies": { "ms": "2.0.0" } }, - "node_modules/body-parser/node_modules/depd": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true }, "node_modules/body-parser/node_modules/qs": { - "version": "6.10.3", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.0.4" }, @@ -6538,42 +5563,46 @@ } }, "node_modules/bonjour-service": { - "version": "1.0.14", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", + "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", "dev": true, - "license": "MIT", "dependencies": { - "array-flatten": "^2.1.2", - "dns-equal": "^1.0.0", "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "node_modules/boolbase": { "version": "1.0.0", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true }, "node_modules/brace-expansion": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "devOptional": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/braces": { - "version": "3.0.2", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "devOptional": true, - "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, "node_modules/browserslist": { - "version": "4.21.4", + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", "dev": true, "funding": [ { @@ -6583,14 +5612,17 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001400", - "electron-to-chromium": "^1.4.251", - "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.9" + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" }, "bin": { "browserslist": "cli.js" @@ -6601,6 +5633,8 @@ }, "node_modules/buffer": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "funding": [ { "type": "github", @@ -6615,7 +5649,6 @@ "url": "https://feross.org/support" } ], - "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" @@ -6623,13 +5656,15 @@ }, "node_modules/buffer-from": { "version": "1.1.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true }, "node_modules/builtin-modules": { "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" }, @@ -6638,25 +5673,28 @@ } }, "node_modules/builtins": { - "version": "5.0.1", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz", + "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==", "devOptional": true, - "license": "MIT", "dependencies": { "semver": "^7.0.0" } }, "node_modules/bytes": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/cacache": { "version": "16.1.2", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.2.tgz", + "integrity": "sha512-Xx+xPlfCZIUHagysjjOAje9nRo8pRDczQCcXb4J2O0BLtH+xeVue6ba4y1kfJfQMAnM2mkcoMIAyOctlaRGWYA==", "devOptional": true, - "license": "ISC", "dependencies": { "@npmcli/fs": "^2.1.0", "@npmcli/move-file": "^2.0.0", @@ -6681,13 +5719,29 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "devOptional": true, + "engines": { + "node": ">=12" + } + }, "node_modules/call-bind": { - "version": "1.0.2", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dev": true, - "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6695,21 +5749,25 @@ }, "node_modules/callsites": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/camelcase": { "version": "5.3.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "engines": { "node": ">=6" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001407", + "version": "1.0.30001640", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001640.tgz", + "integrity": "sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==", "dev": true, "funding": [ { @@ -6719,26 +5777,32 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/cbor": { "name": "@jprochazk/cbor", "version": "0.4.9", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@jprochazk/cbor/-/cbor-0.4.9.tgz", + "integrity": "sha512-FWNnkOtWrFOLXKG2nzOHR/EnCCGZZPvatAvWXDmkTDxgjj9JHDK3DkMUHcFCY3a9weylMCSO/nLOUM170NAO0Q==" }, "node_modules/cbor-web": { "version": "8.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/cbor-web/-/cbor-web-8.1.0.tgz", + "integrity": "sha512-2hWHHMVrfffgoEmsAUh8vCxHoLa1vgodtC73+C5cSarkJlwTapnqAzcHINlP6Ej0DXuP4OmmJ9LF+JaNM5Lj/g==", "engines": { "node": ">=12.19" } }, "node_modules/chalk": { "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -6750,19 +5814,15 @@ }, "node_modules/chardet": { "version": "0.7.0", - "devOptional": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "devOptional": true }, "node_modules/chokidar": { - "version": "3.5.3", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "devOptional": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -6775,34 +5835,41 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "node_modules/chownr": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "devOptional": true, - "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/chrome-trace-event": { - "version": "1.0.3", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.0" } }, "node_modules/ci-info": { "version": "2.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true }, "node_modules/cipher-base": { "version": "1.0.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -6810,15 +5877,17 @@ }, "node_modules/clean-stack": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "devOptional": true, - "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/cli-cursor": { "version": "3.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "dependencies": { "restore-cursor": "^3.1.0" }, @@ -6827,8 +5896,9 @@ } }, "node_modules/cli-spinners": { - "version": "2.7.0", - "license": "MIT", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "engines": { "node": ">=6" }, @@ -6838,8 +5908,9 @@ }, "node_modules/cli-truncate": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", "dev": true, - "license": "MIT", "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^5.0.0" @@ -6853,8 +5924,9 @@ }, "node_modules/cli-truncate/node_modules/ansi-regex": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -6863,9 +5935,10 @@ } }, "node_modules/cli-truncate/node_modules/ansi-styles": { - "version": "6.1.1", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -6875,13 +5948,15 @@ }, "node_modules/cli-truncate/node_modules/emoji-regex": { "version": "9.2.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true }, "node_modules/cli-truncate/node_modules/is-fullwidth-code-point": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -6891,8 +5966,9 @@ }, "node_modules/cli-truncate/node_modules/slice-ansi": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" @@ -6906,8 +5982,9 @@ }, "node_modules/cli-truncate/node_modules/string-width": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, - "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -6921,9 +5998,10 @@ } }, "node_modules/cli-truncate/node_modules/strip-ansi": { - "version": "7.0.1", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, - "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -6936,16 +6014,18 @@ }, "node_modules/cli-width": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", "devOptional": true, - "license": "ISC", "engines": { "node": ">= 10" } }, "node_modules/cliui": { "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "devOptional": true, - "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -6954,15 +6034,17 @@ }, "node_modules/clone": { "version": "1.0.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", "engines": { "node": ">=0.8" } }, "node_modules/clone-deep": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", "dev": true, - "license": "MIT", "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", @@ -6973,40 +6055,46 @@ } }, "node_modules/code-block-writer": { - "version": "10.1.1", - "license": "MIT", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz", + "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==", "optional": true }, "node_modules/color-convert": { "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, - "license": "MIT", "dependencies": { "color-name": "1.1.3" } }, "node_modules/color-name": { "version": "1.1.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true }, "node_modules/color-support": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", "devOptional": true, - "license": "ISC", "bin": { "color-support": "bin.js" } }, "node_modules/colorette": { - "version": "2.0.19", - "dev": true, - "license": "MIT" + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true }, "node_modules/combined-stream": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, - "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -7015,32 +6103,40 @@ } }, "node_modules/commander": { - "version": "9.4.0", + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", "dev": true, - "license": "MIT", "engines": { "node": "^12.20.0 || >=14" } }, "node_modules/commondir": { "version": "1.0.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true }, "node_modules/compare-versions": { "version": "3.6.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", + "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", + "dev": true }, "node_modules/component-emitter": { - "version": "1.3.0", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", "dev": true, - "license": "MIT" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/compressible": { "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "dev": true, - "license": "MIT", "dependencies": { "mime-db": ">= 1.43.0 < 2" }, @@ -7050,8 +6146,9 @@ }, "node_modules/compression": { "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", "dev": true, - "license": "MIT", "dependencies": { "accepts": "~1.3.5", "bytes": "3.0.0", @@ -7067,47 +6164,60 @@ }, "node_modules/compression/node_modules/bytes": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/compression/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/compression/node_modules/ms": { "version": "2.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true }, "node_modules/concat-map": { "version": "0.0.1", - "devOptional": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "devOptional": true }, "node_modules/connect-history-api-fallback": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.8" } }, "node_modules/console-control-strings": { "version": "1.1.0", - "devOptional": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "devOptional": true }, "node_modules/content-disposition": { "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dev": true, - "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" }, @@ -7115,63 +6225,47 @@ "node": ">= 0.6" } }, - "node_modules/content-disposition/node_modules/safe-buffer": { - "version": "5.2.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/content-type": { - "version": "1.0.4", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/convert-source-map": { - "version": "1.8.0", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.1" - } + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true }, "node_modules/cookie": { - "version": "0.5.0", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie-signature": { "version": "1.0.6", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true }, "node_modules/cookiejar": { "version": "2.1.4", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true }, "node_modules/copy-anything": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", "dev": true, - "license": "MIT", "dependencies": { "is-what": "^3.14.1" }, @@ -7181,8 +6275,9 @@ }, "node_modules/copy-webpack-plugin": { "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", "dev": true, - "license": "MIT", "dependencies": { "fast-glob": "^3.2.11", "glob-parent": "^6.0.1", @@ -7204,8 +6299,9 @@ }, "node_modules/copy-webpack-plugin/node_modules/glob-parent": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, - "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -7214,14 +6310,15 @@ } }, "node_modules/copy-webpack-plugin/node_modules/schema-utils": { - "version": "4.0.0", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", "dev": true, - "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", + "ajv": "^8.9.0", "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" + "ajv-keywords": "^5.1.0" }, "engines": { "node": ">= 12.13.0" @@ -7232,20 +6329,22 @@ } }, "node_modules/core-js": { - "version": "3.25.2", + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz", + "integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==", "hasInstallScript": true, - "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" } }, "node_modules/core-js-compat": { - "version": "3.25.2", + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", + "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", "dev": true, - "license": "MIT", "dependencies": { - "browserslist": "^4.21.4" + "browserslist": "^4.23.0" }, "funding": { "type": "opencollective", @@ -7254,13 +6353,15 @@ }, "node_modules/core-util-is": { "version": "1.0.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true }, "node_modules/cosmiconfig": { - "version": "7.0.1", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", "dev": true, - "license": "MIT", "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", @@ -7274,7 +6375,8 @@ }, "node_modules/create-hash": { "version": "1.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "dependencies": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", @@ -7285,7 +6387,8 @@ }, "node_modules/create-hmac": { "version": "1.1.7", - "license": "MIT", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "dependencies": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", @@ -7297,13 +6400,15 @@ }, "node_modules/create-require": { "version": "1.1.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true }, "node_modules/critters": { "version": "0.0.16", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.16.tgz", + "integrity": "sha512-JwjgmO6i3y6RWtLYmXwO5jMd+maZt8Tnfu7VVISmEWyQqfLpB8soBswf8/2bu6SBXxtKA68Al3c+qIG1ApT68A==", "dev": true, - "license": "Apache-2.0", "dependencies": { "chalk": "^4.1.0", "css-select": "^4.2.0", @@ -7315,8 +6420,9 @@ }, "node_modules/critters/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -7329,8 +6435,9 @@ }, "node_modules/critters/node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -7344,8 +6451,9 @@ }, "node_modules/critters/node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -7355,21 +6463,24 @@ }, "node_modules/critters/node_modules/color-name": { "version": "1.1.4", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/critters/node_modules/has-flag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/critters/node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -7379,8 +6490,9 @@ }, "node_modules/cross-spawn": { "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, - "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -7392,8 +6504,9 @@ }, "node_modules/css-blank-pseudo": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz", + "integrity": "sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==", "dev": true, - "license": "CC0-1.0", "dependencies": { "postcss-selector-parser": "^6.0.9" }, @@ -7409,8 +6522,9 @@ }, "node_modules/css-has-pseudo": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz", + "integrity": "sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==", "dev": true, - "license": "CC0-1.0", "dependencies": { "postcss-selector-parser": "^6.0.9" }, @@ -7426,8 +6540,9 @@ }, "node_modules/css-loader": { "version": "6.7.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz", + "integrity": "sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==", "dev": true, - "license": "MIT", "dependencies": { "icss-utils": "^5.1.0", "postcss": "^8.4.7", @@ -7451,8 +6566,9 @@ }, "node_modules/css-prefers-color-scheme": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", + "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", "dev": true, - "license": "CC0-1.0", "bin": { "css-prefers-color-scheme": "dist/cli.cjs" }, @@ -7465,8 +6581,9 @@ }, "node_modules/css-select": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.0.1", @@ -7480,8 +6597,9 @@ }, "node_modules/css-what": { "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", "dev": true, - "license": "BSD-2-Clause", "engines": { "node": ">= 6" }, @@ -7490,18 +6608,26 @@ } }, "node_modules/cssdb": { - "version": "7.0.1", + "version": "7.11.2", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.11.2.tgz", + "integrity": "sha512-lhQ32TFkc1X4eTefGfYPvgovRSzIMofHkigfH8nWtyRL4XJLsRhJFreRvEgKzept7x1rjBuy3J/MurXLaFxW/A==", "dev": true, - "license": "CC0-1.0", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + } + ] }, "node_modules/cssesc": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, - "license": "MIT", "bin": { "cssesc": "bin/cssesc" }, @@ -7511,21 +6637,30 @@ }, "node_modules/cuint": { "version": "0.2.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz", + "integrity": "sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==", + "dev": true }, "node_modules/data-uri-to-buffer": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", + "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==", "dev": true, - "license": "MIT", "engines": { "node": ">= 6" } }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true + }, "node_modules/debug": { "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "devOptional": true, - "license": "MIT", "dependencies": { "ms": "2.1.2" }, @@ -7540,36 +6675,41 @@ }, "node_modules/decamelize": { "version": "1.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "engines": { "node": ">=0.10.0" } }, "node_modules/decode-uri-component": { "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10" } }, "node_modules/deep-is": { "version": "0.1.4", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true }, "node_modules/deepmerge": { - "version": "4.2.2", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/default-gateway": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "execa": "^5.0.0" }, @@ -7578,43 +6718,25 @@ } }, "node_modules/defaults": { - "version": "1.0.3", - "license": "MIT", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", "dependencies": { "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/define-data-property": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz", - "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -7623,15 +6745,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/degenerator": { - "version": "3.0.2", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-3.0.4.tgz", + "integrity": "sha512-Z66uPeBfHZAHVmue3HPfyKu2Q0rC2cRxbTOsvmU/po5fvvcx27W4mIu9n0PUlQih4oUYvcG1BsbtVv8x7KDOSw==", "dev": true, - "license": "MIT", "dependencies": { "ast-types": "^0.13.2", "escodegen": "^1.8.1", "esprima": "^4.0.0", - "vm2": "^3.9.8" + "vm2": "^3.9.17" }, "engines": { "node": ">= 6" @@ -7639,37 +6771,42 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.4.0" } }, "node_modules/delegates": { "version": "1.0.0", - "devOptional": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "devOptional": true }, "node_modules/depd": { - "version": "1.1.2", - "devOptional": true, - "license": "MIT", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/dependency-graph": { "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6.0" } }, "node_modules/destroy": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" @@ -7677,25 +6814,29 @@ }, "node_modules/detect-node": { "version": "2.1.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true }, "node_modules/diff": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } }, "node_modules/dijkstrajs": { - "version": "1.0.2", - "license": "MIT" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" }, "node_modules/dir-glob": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, - "license": "MIT", "dependencies": { "path-type": "^4.0.0" }, @@ -7703,15 +6844,11 @@ "node": ">=8" } }, - "node_modules/dns-equal": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, "node_modules/dns-packet": { - "version": "5.4.0", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", "dev": true, - "license": "MIT", "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" }, @@ -7721,8 +6858,9 @@ }, "node_modules/dom-serializer": { "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", "dev": true, - "license": "MIT", "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", @@ -7733,27 +6871,30 @@ } }, "node_modules/dom7": { - "version": "4.0.4", - "license": "MIT", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/dom7/-/dom7-4.0.6.tgz", + "integrity": "sha512-emjdpPLhpNubapLFdjNL9tP06Sr+GZkrIHEXLWvOGsytACUrkbeIdjO5g77m00BrHTznnlcNqgmn7pCN192TBA==", "dependencies": { "ssr-window": "^4.0.0" } }, "node_modules/domelementtype": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/fb55" } - ], - "license": "BSD-2-Clause" + ] }, "node_modules/domhandler": { "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.2.0" }, @@ -7765,13 +6906,15 @@ } }, "node_modules/dompurify": { - "version": "2.4.0", - "license": "(MPL-2.0 OR Apache-2.0)" + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.5.tgz", + "integrity": "sha512-FgbqnEPiv5Vdtwt6Mxl7XSylttCC03cqP5ldNT2z+Kj0nLxPHJH4+1Cyf5Jasxhw93Rl4Oo11qRoUV72fmya2Q==" }, "node_modules/domutils": { "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", @@ -7783,21 +6926,24 @@ }, "node_modules/duplexer": { "version": "0.1.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true }, "node_modules/duplexer2": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "readable-stream": "^2.0.2" } }, "node_modules/duplexer2/node_modules/readable-stream": { - "version": "2.3.7", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, - "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -7808,33 +6954,44 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, "node_modules/duplexer2/node_modules/string_decoder": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, - "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } }, "node_modules/eastasianwidth": { "version": "0.2.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true }, "node_modules/ee-first": { "version": "1.1.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.255", - "dev": true, - "license": "ISC" + "version": "1.4.816", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.816.tgz", + "integrity": "sha512-EKH5X5oqC6hLmiS7/vYtZHZFTNdhsYG5NVPRN6Yn0kQHNBlT59+xSM8HBy66P5fxWpKgZbPqb+diC64ng295Jw==", + "dev": true }, "node_modules/elementtree": { "version": "0.1.7", + "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz", + "integrity": "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==", "dev": true, - "license": "Apache-2.0", "dependencies": { "sax": "1.1.4" }, @@ -7844,32 +7001,37 @@ }, "node_modules/emoji-regex": { "version": "8.0.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/emojis-list": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/encode-utf8": { "version": "1.0.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", + "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==" }, "node_modules/encodeurl": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/encoding": { "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "iconv-lite": "^0.6.2" @@ -7877,8 +7039,9 @@ }, "node_modules/encoding/node_modules/iconv-lite": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -7889,16 +7052,18 @@ }, "node_modules/end-of-stream": { "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, - "license": "MIT", "dependencies": { "once": "^1.4.0" } }, "node_modules/enhanced-resolve": { - "version": "5.10.0", + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", + "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", "dev": true, - "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -7909,28 +7074,32 @@ }, "node_modules/entities": { "version": "2.2.0", - "license": "BSD-2-Clause", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/env-paths": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "devOptional": true, - "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/err-code": { "version": "2.0.3", - "devOptional": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "devOptional": true }, "node_modules/errno": { "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "prr": "~1.0.1" @@ -7941,26 +7110,52 @@ }, "node_modules/error-ex": { "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dev": true, - "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } }, - "node_modules/es-module-lexer": { - "version": "0.9.3", + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", "dev": true, - "license": "MIT" + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true, + "peer": true }, "node_modules/es6-promise": { "version": "4.2.8", - "license": "MIT" + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" }, "node_modules/esbuild": { "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.5.tgz", + "integrity": "sha512-VSf6S1QVqvxfIsSKb3UKr3VhUCis7wgDbtF4Vd9z84UJr05/Sp2fRKmzC+CSPG/dNAPPJZ0BTBLTT1Fhd6N9Gg==", "dev": true, "hasInstallScript": true, - "license": "MIT", "optional": true, "bin": { "esbuild": "bin/esbuild" @@ -7992,13 +7187,46 @@ "esbuild-windows-arm64": "0.15.5" } }, - "node_modules/esbuild-darwin-arm64": { + "node_modules/esbuild-android-64": { "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.5.tgz", + "integrity": "sha512-dYPPkiGNskvZqmIK29OPxolyY3tp+c47+Fsc2WYSOVjEPWNCHNyqhtFqQadcXMJDQt8eN0NMDukbyQgFcHquXg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.5.tgz", + "integrity": "sha512-YyEkaQl08ze3cBzI/4Cm1S+rVh8HMOpCdq8B78JLbNFHhzi4NixVN93xDrHZLztlocEYqi45rHHCgA8kZFidFg==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.5.tgz", + "integrity": "sha512-Cr0iIqnWKx3ZTvDUAzG0H/u9dWjLE4c2gTtRLz4pqOBGjfjqdcZSfAObFzKTInLLSmD0ZV1I/mshhPoYSBMMCQ==", + "cpu": [ + "x64" + ], + "dev": true, "optional": true, "os": [ "darwin" @@ -8007,10 +7235,235 @@ "node": ">=12" } }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.5.tgz", + "integrity": "sha512-WIfQkocGtFrz7vCu44ypY5YmiFXpsxvz2xqwe688jFfSVCnUsCn2qkEVDo7gT8EpsLOz1J/OmqjExePL1dr1Kg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.5.tgz", + "integrity": "sha512-M5/EfzV2RsMd/wqwR18CELcenZ8+fFxQAAEO7TJKDmP3knhWSbD72ILzrXFMMwshlPAS1ShCZ90jsxkm+8FlaA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.5.tgz", + "integrity": "sha512-2JQQ5Qs9J0440F/n/aUBNvY6lTo4XP/4lt1TwDfHuo0DY3w5++anw+jTjfouLzbJmFFiwmX7SmUhMnysocx96w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.5.tgz", + "integrity": "sha512-gO9vNnIN0FTUGjvTFucIXtBSr1Woymmx/aHQtuU+2OllGU6YFLs99960UD4Dib1kFovVgs59MTXwpFdVoSMZoQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.5.tgz", + "integrity": "sha512-ne0GFdNLsm4veXbTnYAWjbx3shpNKZJUd6XpNbKNUZaNllDZfYQt0/zRqOg0sc7O8GQ+PjSMv9IpIEULXVTVmg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.5.tgz", + "integrity": "sha512-wvAoHEN+gJ/22gnvhZnS/+2H14HyAxM07m59RSLn3iXrQsdS518jnEWRBnJz3fR6BJa+VUTo0NxYjGaNt7RA7Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.5.tgz", + "integrity": "sha512-7EgFyP2zjO065XTfdCxiXVEk+f83RQ1JsryN1X/VSX2li9rnHAt2swRbpoz5Vlrl6qjHrCmq5b6yxD13z6RheA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.5.tgz", + "integrity": "sha512-KdnSkHxWrJ6Y40ABu+ipTZeRhFtc8dowGyFsZY5prsmMSr1ZTG9zQawguN4/tunJ0wy3+kD54GaGwdcpwWAvZQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.5.tgz", + "integrity": "sha512-QdRHGeZ2ykl5P0KRmfGBZIHmqcwIsUKWmmpZTOq573jRWwmpfRmS7xOhmDHBj9pxv+6qRMH8tLr2fe+ZKQvCYw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.5.tgz", + "integrity": "sha512-p+WE6RX+jNILsf+exR29DwgV6B73khEQV0qWUbzxaycxawZ8NE0wA6HnnTxbiw5f4Gx9sJDUBemh9v49lKOORA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.5.tgz", + "integrity": "sha512-J2ngOB4cNzmqLHh6TYMM/ips8aoZIuzxJnDdWutBw5482jGXiOzsPoEF4j2WJ2mGnm7FBCO4StGcwzOgic70JQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.5.tgz", + "integrity": "sha512-MmKUYGDizYjFia0Rwt8oOgmiFH7zaYlsoQ3tIOfPxOqLssAsEgG0MUdRDm5lliqjiuoog8LyDu9srQk5YwWF3w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.5.tgz", + "integrity": "sha512-2mMFfkLk3oPWfopA9Plj4hyhqHNuGyp5KQyTT9Rc8hFd8wAn5ZrbJg+gNcLMo2yzf8Uiu0RT6G9B15YN9WQyMA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.5.tgz", + "integrity": "sha512-2sIzhMUfLNoD+rdmV6AacilCHSxZIoGAU2oT7XmJ0lXcZWnCvCtObvO6D4puxX9YRE97GodciRGDLBaiC6x1SA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/esbuild-wasm": { "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.15.5.tgz", + "integrity": "sha512-lTJOEKekN/4JI/eOEq0wLcx53co2N6vaT/XjBz46D1tvIVoUEyM0o2K6txW6gEotf31szFD/J1PbxmnbkGlK9A==", "dev": true, - "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -8018,31 +7471,83 @@ "node": ">=12" } }, + "node_modules/esbuild-windows-32": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.5.tgz", + "integrity": "sha512-e+duNED9UBop7Vnlap6XKedA/53lIi12xv2ebeNS4gFmu7aKyTrok7DPIZyU5w/ftHD4MUDs5PJUkQPP9xJRzg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.5.tgz", + "integrity": "sha512-v+PjvNtSASHOjPDMIai9Yi+aP+Vwox+3WVdg2JB8N9aivJ7lyhp4NVU+J0MV2OkWFPnVO8AE/7xH+72ibUUEnw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.5.tgz", + "integrity": "sha512-Yz8w/D8CUPYstvVQujByu6mlf48lKmXkq6bkeSZZxTA626efQOJb26aDGLzmFWx6eg/FwrXgt6SZs9V8Pwy/aA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/escalade": { - "version": "3.1.1", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "devOptional": true, - "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/escape-html": { "version": "1.0.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true }, "node_modules/escape-string-regexp": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "devOptional": true, - "license": "MIT", "engines": { "node": ">=0.8.0" } }, "node_modules/escodegen": { "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "esprima": "^4.0.1", "estraverse": "^4.2.0", @@ -8062,8 +7567,9 @@ }, "node_modules/escodegen/node_modules/source-map": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "license": "BSD-3-Clause", "optional": true, "engines": { "node": ">=0.10.0" @@ -8071,8 +7577,9 @@ }, "node_modules/eslint-scope": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -8083,8 +7590,9 @@ }, "node_modules/esprima": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, - "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -8095,8 +7603,9 @@ }, "node_modules/esrecurse": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -8106,63 +7615,72 @@ }, "node_modules/esrecurse/node_modules/estraverse": { "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, "node_modules/estraverse": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, - "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, "node_modules/estree-walker": { "version": "1.0.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true }, "node_modules/esutils": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, - "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/etag": { "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/eventemitter-asyncresource": { "version": "1.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz", + "integrity": "sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ==", + "dev": true }, "node_modules/eventemitter3": { "version": "4.0.7", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true }, "node_modules/events": { "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.8.x" } }, "node_modules/execa": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, - "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -8181,17 +7699,24 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "devOptional": true + }, "node_modules/express": { - "version": "4.18.1", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dev": true, - "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.0", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -8207,7 +7732,7 @@ "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", "proxy-addr": "~2.0.7", - "qs": "6.10.3", + "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.18.0", @@ -8222,36 +7747,26 @@ "node": ">= 0.10.0" } }, - "node_modules/express/node_modules/array-flatten": { - "version": "1.1.1", - "dev": true, - "license": "MIT" - }, "node_modules/express/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "license": "MIT", "dependencies": { "ms": "2.0.0" } }, - "node_modules/express/node_modules/depd": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/express/node_modules/ms": { "version": "2.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true }, "node_modules/express/node_modules/qs": { - "version": "6.10.3", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.0.4" }, @@ -8262,29 +7777,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/express/node_modules/safe-buffer": { - "version": "5.2.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/external-editor": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", "devOptional": true, - "license": "MIT", "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", @@ -8300,9 +7797,10 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { - "version": "3.2.12", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "devOptional": true, - "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -8316,35 +7814,41 @@ }, "node_modules/fast-json-patch": { "version": "3.1.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true }, "node_modules/fast-safe-stringify": { "version": "2.1.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true }, "node_modules/fastq": { - "version": "1.13.0", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "devOptional": true, - "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, "node_modules/faye-websocket": { "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", "dev": true, - "license": "Apache-2.0", "dependencies": { "websocket-driver": ">=0.5.1" }, @@ -8354,8 +7858,9 @@ }, "node_modules/figures": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "devOptional": true, - "license": "MIT", "dependencies": { "escape-string-regexp": "^1.0.5" }, @@ -8368,16 +7873,18 @@ }, "node_modules/file-uri-to-path": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-2.0.0.tgz", + "integrity": "sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/fill-range": { - "version": "7.0.1", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "devOptional": true, - "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -8387,8 +7894,9 @@ }, "node_modules/finalhandler": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", "dev": true, - "license": "MIT", "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", @@ -8404,21 +7912,24 @@ }, "node_modules/finalhandler/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true }, "node_modules/find-cache-dir": { "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", "dev": true, - "license": "MIT", "dependencies": { "commondir": "^1.0.1", "make-dir": "^3.0.2", @@ -8433,7 +7944,8 @@ }, "node_modules/find-up": { "version": "4.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -8444,8 +7956,9 @@ }, "node_modules/find-versions": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-4.0.0.tgz", + "integrity": "sha512-wgpWy002tA+wgmO27buH/9KzyEOQnKsG/R0yrcjPT9BOFm0zRBVQbZ95nRGXWMywS8YR5knRbpohio0bcJABxQ==", "dev": true, - "license": "MIT", "dependencies": { "semver-regex": "^3.1.2" }, @@ -8457,7 +7970,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.2", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "dev": true, "funding": [ { @@ -8465,7 +7980,6 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], - "license": "MIT", "engines": { "node": ">=4.0" }, @@ -8477,8 +7991,9 @@ }, "node_modules/form-data": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", "dev": true, - "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -8490,44 +8005,50 @@ }, "node_modules/formidable": { "version": "1.2.6", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", + "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==", + "deprecated": "Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau", "dev": true, - "license": "MIT", "funding": { "url": "https://ko-fi.com/tunnckoCore/commissions" } }, "node_modules/forwarded": { "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/fraction.js": { - "version": "4.2.0", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "dev": true, - "license": "MIT", "engines": { "node": "*" }, "funding": { "type": "patreon", - "url": "https://www.patreon.com/infusion" + "url": "https://github.com/sponsors/rawify" } }, "node_modules/fresh": { "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/fs-extra": { "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, - "license": "MIT", "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", @@ -8540,8 +8061,9 @@ }, "node_modules/fs-minipass": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "devOptional": true, - "license": "ISC", "dependencies": { "minipass": "^3.0.0" }, @@ -8550,19 +8072,23 @@ } }, "node_modules/fs-monkey": { - "version": "1.0.3", - "dev": true, - "license": "Unlicense" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", + "dev": true }, "node_modules/fs.realpath": { "version": "1.0.0", - "devOptional": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "devOptional": true }, "node_modules/fsevents": { - "version": "2.3.2", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "license": "MIT", + "hasInstallScript": true, "optional": true, "os": [ "darwin" @@ -8573,6 +8099,8 @@ }, "node_modules/ftp": { "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", + "integrity": "sha512-faFVML1aBx2UoDStmLwv2Wptt4vw5x03xxX172nhA5Y5HBshW5JweqQ2W4xL4dezQTG8inJsuYcpPHHU3X5OTQ==", "dev": true, "dependencies": { "readable-stream": "1.1.x", @@ -8584,13 +8112,15 @@ }, "node_modules/ftp/node_modules/isarray": { "version": "0.0.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true }, "node_modules/ftp/node_modules/readable-stream": { "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", "dev": true, - "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", @@ -8600,25 +8130,33 @@ }, "node_modules/ftp/node_modules/string_decoder": { "version": "0.10.31", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true }, "node_modules/function-bind": { - "version": "1.1.1", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "devOptional": true, - "license": "MIT" + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/fuse.js": { "version": "6.6.2", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz", + "integrity": "sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==", "engines": { "node": ">=10" } }, "node_modules/gauge": { "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", "devOptional": true, - "license": "ISC", "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.3", @@ -8635,29 +8173,35 @@ }, "node_modules/gensync": { "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/get-caller-file": { "version": "2.0.5", - "license": "ISC", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "engines": { "node": "6.* || 8.* || >= 10.*" } }, "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8665,16 +8209,18 @@ }, "node_modules/get-package-type": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.0.0" } }, "node_modules/get-stream": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -8684,8 +8230,9 @@ }, "node_modules/get-uri": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-3.0.2.tgz", + "integrity": "sha512-+5s0SJbGoyiJTZZ2JTpFPLMPSch72KEqGOTvQsBqg0RBWvwhWUSYZFAtz3TPW0GXJuLBJPts1E241iHg+VRfhg==", "dev": true, - "license": "MIT", "dependencies": { "@tootallnate/once": "1", "data-uri-to-buffer": "3", @@ -8700,16 +8247,18 @@ }, "node_modules/get-uri/node_modules/@tootallnate/once": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/get-uri/node_modules/fs-extra": { "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, - "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", @@ -8721,24 +8270,28 @@ }, "node_modules/get-uri/node_modules/jsonfile": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, - "license": "MIT", "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "node_modules/get-uri/node_modules/universalify": { "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4.0.0" } }, "node_modules/glob": { "version": "8.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", + "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "devOptional": true, - "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -8755,8 +8308,9 @@ }, "node_modules/glob-parent": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "devOptional": true, - "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -8766,25 +8320,28 @@ }, "node_modules/glob-to-regexp": { "version": "0.4.1", - "dev": true, - "license": "BSD-2-Clause" + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true }, "node_modules/globals": { "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/globby": { - "version": "13.1.2", + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", "dev": true, - "license": "MIT", "dependencies": { "dir-glob": "^3.0.1", - "fast-glob": "^3.2.11", - "ignore": "^5.2.0", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", "merge2": "^1.4.1", "slash": "^4.0.0" }, @@ -8808,14 +8365,16 @@ } }, "node_modules/graceful-fs": { - "version": "4.2.10", - "devOptional": true, - "license": "ISC" + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "devOptional": true }, "node_modules/gzip-size": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", "dev": true, - "license": "MIT", "dependencies": { "duplexer": "^0.1.2" }, @@ -8828,43 +8387,35 @@ }, "node_modules/handle-thing": { "version": "2.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/has": { - "version": "1.0.3", - "devOptional": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true }, "node_modules/has-flag": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, - "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "dev": true, "engines": { "node": ">= 0.4" @@ -8875,8 +8426,9 @@ }, "node_modules/has-symbols": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -8886,12 +8438,14 @@ }, "node_modules/has-unicode": { "version": "2.0.1", - "devOptional": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "devOptional": true }, "node_modules/hash-base": { "version": "3.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", "dependencies": { "inherits": "^2.0.4", "readable-stream": "^3.6.0", @@ -8901,28 +8455,23 @@ "node": ">=4" } }, - "node_modules/hash-base/node_modules/safe-buffer": { - "version": "5.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "devOptional": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/hdr-histogram-js": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz", + "integrity": "sha512-Hkn78wwzWHNCp2uarhzQ2SGFLU3JY8SBDDd3TAABK4fc30wm+MuPOrg5QVFVfkKOQd6Bfz3ukJEI+q9sXEkK1g==", "dev": true, - "license": "BSD", "dependencies": { "@assemblyscript/loader": "^0.10.1", "base64-js": "^1.2.0", @@ -8931,26 +8480,30 @@ }, "node_modules/hdr-histogram-js/node_modules/pako": { "version": "1.0.11", - "dev": true, - "license": "(MIT AND Zlib)" + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true }, "node_modules/hdr-histogram-percentiles-obj": { "version": "3.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz", + "integrity": "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==", + "dev": true }, "node_modules/he": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true, - "license": "MIT", "bin": { "he": "bin/he" } }, "node_modules/hosted-git-info": { - "version": "5.1.0", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.2.1.tgz", + "integrity": "sha512-xIcQYMnhcx2Nr4JTjsFmwwnr9vldugPy9uVm0o87bjqqWMv9GaqsTeT+i99wTl0mk1uLxJtHxLb8kymqTENQsw==", "devOptional": true, - "license": "ISC", "dependencies": { "lru-cache": "^7.5.1" }, @@ -8958,10 +8511,20 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "devOptional": true, + "engines": { + "node": ">=12" + } + }, "node_modules/hpack.js": { "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", "dev": true, - "license": "MIT", "dependencies": { "inherits": "^2.0.1", "obuf": "^1.0.0", @@ -8970,9 +8533,10 @@ } }, "node_modules/hpack.js/node_modules/readable-stream": { - "version": "2.3.7", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, - "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -8983,33 +8547,60 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, "node_modules/hpack.js/node_modules/string_decoder": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, - "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } }, "node_modules/html-entities": { - "version": "2.3.3", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", "dev": true, - "license": "MIT" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true }, "node_modules/http-cache-semantics": { "version": "4.1.1", - "devOptional": true, - "license": "BSD-2-Clause" + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "devOptional": true }, "node_modules/http-deceiver": { "version": "1.2.7", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true }, "node_modules/http-errors": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "dev": true, - "license": "MIT", "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", @@ -9021,23 +8612,17 @@ "node": ">= 0.8" } }, - "node_modules/http-errors/node_modules/depd": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/http-parser-js": { "version": "0.5.8", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true }, "node_modules/http-proxy": { "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dev": true, - "license": "MIT", "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", @@ -9049,8 +8634,9 @@ }, "node_modules/http-proxy-agent": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", "devOptional": true, - "license": "MIT", "dependencies": { "@tootallnate/once": "2", "agent-base": "6", @@ -9062,8 +8648,9 @@ }, "node_modules/http-proxy-middleware": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", "dev": true, - "license": "MIT", "dependencies": { "@types/http-proxy": "^1.17.8", "http-proxy": "^1.18.1", @@ -9085,8 +8672,9 @@ }, "node_modules/https-proxy-agent": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "devOptional": true, - "license": "MIT", "dependencies": { "agent-base": "6", "debug": "4" @@ -9097,25 +8685,28 @@ }, "node_modules/human-signals": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=10.17.0" } }, "node_modules/humanize-ms": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", "devOptional": true, - "license": "MIT", "dependencies": { "ms": "^2.0.0" } }, "node_modules/husky": { "version": "4.3.8", + "resolved": "https://registry.npmjs.org/husky/-/husky-4.3.8.tgz", + "integrity": "sha512-LCqqsB0PzJQ/AlCgfrfzRe3e3+NvmefAdKQhRYpxS4u6clblBoDdzzvHi8fmxKRzvMxPY/1WZWzomPZww0Anow==", "dev": true, "hasInstallScript": true, - "license": "MIT", "dependencies": { "chalk": "^4.0.0", "ci-info": "^2.0.0", @@ -9142,8 +8733,9 @@ }, "node_modules/husky/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -9156,8 +8748,9 @@ }, "node_modules/husky/node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -9171,8 +8764,9 @@ }, "node_modules/husky/node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -9182,13 +8776,15 @@ }, "node_modules/husky/node_modules/color-name": { "version": "1.1.4", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/husky/node_modules/find-up": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, - "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -9202,16 +8798,18 @@ }, "node_modules/husky/node_modules/has-flag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/husky/node_modules/locate-path": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, - "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -9224,8 +8822,9 @@ }, "node_modules/husky/node_modules/p-limit": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, - "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -9238,8 +8837,9 @@ }, "node_modules/husky/node_modules/p-locate": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, - "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -9252,8 +8852,9 @@ }, "node_modules/husky/node_modules/pkg-dir": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", + "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", "dev": true, - "license": "MIT", "dependencies": { "find-up": "^5.0.0" }, @@ -9263,16 +8864,18 @@ }, "node_modules/husky/node_modules/slash": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/husky/node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -9282,8 +8885,9 @@ }, "node_modules/iconv-lite": { "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "devOptional": true, - "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -9293,8 +8897,9 @@ }, "node_modules/icss-utils": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", "dev": true, - "license": "ISC", "engines": { "node": "^10 || ^12 || >= 14" }, @@ -9304,6 +8909,8 @@ }, "node_modules/ieee754": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "funding": [ { "type": "github", @@ -9317,21 +8924,22 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "BSD-3-Clause" + ] }, "node_modules/ignore": { - "version": "5.2.0", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/ignore-walk": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-5.0.1.tgz", + "integrity": "sha512-yemi4pMf51WKT7khInJqAvsIGzoqYXblnsz0ql8tM+yi1EKYTY1evX4NAbJrLL/Aanr2HyZeluqU+Oi7MGHokw==", "devOptional": true, - "license": "ISC", "dependencies": { "minimatch": "^5.0.1" }, @@ -9341,8 +8949,9 @@ }, "node_modules/image-size": { "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", "dev": true, - "license": "MIT", "optional": true, "bin": { "image-size": "bin/image-size.js" @@ -9352,14 +8961,16 @@ } }, "node_modules/immutable": { - "version": "4.1.0", - "dev": true, - "license": "MIT" + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", + "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", + "dev": true }, "node_modules/import-fresh": { "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, - "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -9373,37 +8984,43 @@ }, "node_modules/import-fresh/node_modules/resolve-from": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/imurmurhash": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "devOptional": true, - "license": "MIT", "engines": { "node": ">=0.8.19" } }, "node_modules/indent-string": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "devOptional": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/infer-owner": { "version": "1.0.4", - "devOptional": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "devOptional": true }, "node_modules/inflight": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "devOptional": true, - "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -9411,28 +9028,32 @@ }, "node_modules/inherits": { "version": "2.0.4", - "license": "ISC" + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ini": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-3.0.0.tgz", + "integrity": "sha512-TxYQaeNW/N8ymDvwAxPyRbhMBtnEwuvaTYpOQkFx1nSeusgezHniEc/l35Vo4iCq/mMiTJbpD7oYxN98hFlfmw==", "devOptional": true, - "license": "ISC", "engines": { "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/injection-js": { "version": "2.4.0", + "resolved": "https://registry.npmjs.org/injection-js/-/injection-js-2.4.0.tgz", + "integrity": "sha512-6jiJt0tCAo9zjHbcwLiPL+IuNe9SQ6a9g0PEzafThW3fOQi0mrmiJGBJvDD6tmhPh8cQHIQtCOrJuBfQME4kPA==", "dev": true, - "license": "MIT", "dependencies": { "tslib": "^2.0.0" } }, "node_modules/inquirer": { "version": "8.2.4", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz", + "integrity": "sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==", "devOptional": true, - "license": "MIT", "dependencies": { "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", @@ -9456,8 +9077,9 @@ }, "node_modules/inquirer/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "devOptional": true, - "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -9470,8 +9092,9 @@ }, "node_modules/inquirer/node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "devOptional": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -9485,8 +9108,9 @@ }, "node_modules/inquirer/node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "devOptional": true, - "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -9496,21 +9120,24 @@ }, "node_modules/inquirer/node_modules/color-name": { "version": "1.1.4", - "devOptional": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true }, "node_modules/inquirer/node_modules/has-flag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "devOptional": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/inquirer/node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "devOptional": true, - "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -9519,45 +9146,52 @@ } }, "node_modules/ionicons": { - "version": "6.0.3", - "license": "MIT", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.1.3.tgz", + "integrity": "sha512-ptzz38dd/Yq+PgjhXegh7yhb/SLIk1bvL9vQDtLv1aoSc7alO6mX2DIMgcKYzt9vrNWkRu1f9Jr78zIFFyOXqw==", "dependencies": { - "@stencil/core": "~2.16.0" - } - }, - "node_modules/ionicons/node_modules/@stencil/core": { - "version": "2.16.1", - "license": "MIT", - "bin": { - "stencil": "bin/stencil" - }, - "engines": { - "node": ">=12.10.0", - "npm": ">=6.0.0" + "@stencil/core": "^2.18.0" } }, "node_modules/ip": { - "version": "2.0.0", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==", + "dev": true + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", "devOptional": true, - "license": "MIT" + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } }, "node_modules/ipaddr.js": { - "version": "2.0.1", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 10" } }, "node_modules/is-arrayish": { "version": "0.2.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true }, "node_modules/is-binary-path": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "devOptional": true, - "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -9566,9 +9200,10 @@ } }, "node_modules/is-builtin-module": { - "version": "3.2.0", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", "dev": true, - "license": "MIT", "dependencies": { "builtin-modules": "^3.3.0" }, @@ -9580,11 +9215,15 @@ } }, "node_modules/is-core-module": { - "version": "2.10.0", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz", + "integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==", "devOptional": true, - "license": "MIT", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9592,8 +9231,9 @@ }, "node_modules/is-docker": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "devOptional": true, - "license": "MIT", "bin": { "is-docker": "cli.js" }, @@ -9606,23 +9246,26 @@ }, "node_modules/is-extglob": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "devOptional": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "engines": { "node": ">=8" } }, "node_modules/is-glob": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "devOptional": true, - "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -9632,33 +9275,38 @@ }, "node_modules/is-interactive": { "version": "1.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", "engines": { "node": ">=8" } }, "node_modules/is-lambda": { "version": "1.0.1", - "devOptional": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "devOptional": true }, "node_modules/is-module": { "version": "1.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true }, "node_modules/is-number": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "devOptional": true, - "license": "MIT", "engines": { "node": ">=0.12.0" } }, "node_modules/is-plain-obj": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -9668,8 +9316,9 @@ }, "node_modules/is-plain-object": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, - "license": "MIT", "dependencies": { "isobject": "^3.0.1" }, @@ -9679,8 +9328,9 @@ }, "node_modules/is-stream": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" }, @@ -9690,12 +9340,14 @@ }, "node_modules/is-typedarray": { "version": "1.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true }, "node_modules/is-unicode-supported": { "version": "0.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "engines": { "node": ">=10" }, @@ -9705,13 +9357,15 @@ }, "node_modules/is-what": { "version": "3.14.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true }, "node_modules/is-wsl": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "devOptional": true, - "license": "MIT", "dependencies": { "is-docker": "^2.0.0" }, @@ -9721,34 +9375,39 @@ }, "node_modules/isarray": { "version": "1.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true }, "node_modules/isexe": { "version": "2.0.0", - "devOptional": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "devOptional": true }, "node_modules/isobject": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=8" } }, "node_modules/istanbul-lib-instrument": { - "version": "5.2.0", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", @@ -9761,17 +9420,19 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.0", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/jest-worker": { "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -9783,16 +9444,18 @@ }, "node_modules/jest-worker/node_modules/has-flag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/jest-worker/node_modules/supports-color": { "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, - "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -9804,20 +9467,23 @@ } }, "node_modules/jose": { - "version": "4.9.3", - "license": "MIT", + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", "funding": { "url": "https://github.com/sponsors/panva" } }, "node_modules/js-tokens": { "version": "4.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true }, "node_modules/js-yaml": { "version": "4.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dependencies": { "argparse": "^2.0.1" }, @@ -9825,10 +9491,17 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "devOptional": true + }, "node_modules/jsesc": { "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true, - "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, @@ -9838,17 +9511,20 @@ }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", - "devOptional": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "devOptional": true }, "node_modules/json-schema-traverse": { "version": "1.0.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/json5": { "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, - "license": "MIT", "bin": { "json5": "lib/cli.js" }, @@ -9858,12 +9534,14 @@ }, "node_modules/jsonc-parser": { "version": "3.1.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.1.0.tgz", + "integrity": "sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg==" }, "node_modules/jsonfile": { "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, - "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -9873,40 +9551,45 @@ }, "node_modules/jsonparse": { "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", "devOptional": true, "engines": [ "node >= 0.2.0" - ], - "license": "MIT" + ] }, "node_modules/karma-source-map-support": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", "dev": true, - "license": "MIT", "dependencies": { "source-map-support": "^0.5.5" } }, "node_modules/kind-of": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/klona": { - "version": "2.0.5", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/leek": { "version": "0.0.24", + "resolved": "https://registry.npmjs.org/leek/-/leek-0.0.24.tgz", + "integrity": "sha512-6PVFIYXxlYF0o6hrAsHtGpTmi06otkwNrMcmQ0K96SeSRHPREPa9J3nJZ1frliVH7XT0XFswoJFQoXsDukzGNQ==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^2.1.0", "lodash.assign": "^3.2.0", @@ -9915,21 +9598,24 @@ }, "node_modules/leek/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/leek/node_modules/ms": { "version": "2.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true }, "node_modules/less": { "version": "4.1.3", + "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", + "integrity": "sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==", "dev": true, - "license": "Apache-2.0", "dependencies": { "copy-anything": "^2.0.1", "parse-node-version": "^1.0.1", @@ -9953,8 +9639,9 @@ }, "node_modules/less-loader": { "version": "11.0.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-11.0.0.tgz", + "integrity": "sha512-9+LOWWjuoectIEx3zrfN83NAGxSUB5pWEabbbidVQVgZhN+wN68pOvuyirVlH1IK4VT1f3TmlyvAnCXh8O5KEw==", "dev": true, - "license": "MIT", "dependencies": { "klona": "^2.0.4" }, @@ -9972,8 +9659,9 @@ }, "node_modules/less/node_modules/make-dir": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "pify": "^4.0.1", @@ -9998,17 +9686,19 @@ }, "node_modules/less/node_modules/pify": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true, - "license": "MIT", "optional": true, "engines": { "node": ">=6" } }, "node_modules/less/node_modules/semver": { - "version": "5.7.1", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, - "license": "ISC", "optional": true, "bin": { "semver": "bin/semver" @@ -10016,8 +9706,9 @@ }, "node_modules/less/node_modules/source-map": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "license": "BSD-3-Clause", "optional": true, "engines": { "node": ">=0.10.0" @@ -10025,8 +9716,9 @@ }, "node_modules/levn": { "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", "dev": true, - "license": "MIT", "dependencies": { "prelude-ls": "~1.1.2", "type-check": "~0.3.2" @@ -10037,8 +9729,9 @@ }, "node_modules/license-webpack-plugin": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", "dev": true, - "license": "ISC", "dependencies": { "webpack-sources": "^3.0.0" }, @@ -10053,21 +9746,24 @@ }, "node_modules/lilconfig": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz", + "integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/lines-and-columns": { "version": "1.2.4", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true }, "node_modules/lint-staged": { "version": "12.5.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-12.5.0.tgz", + "integrity": "sha512-BKLUjWDsKquV/JuIcoQW4MSAI3ggwEImF1+sB4zaKvyVx1wBk3FsG7UK9bpnmBTN1pm7EH2BBcMwINJzCRv12g==", "dev": true, - "license": "MIT", "dependencies": { "cli-truncate": "^3.1.0", "colorette": "^2.0.16", @@ -10095,9 +9791,10 @@ } }, "node_modules/lint-staged/node_modules/supports-color": { - "version": "9.2.3", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", + "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -10107,8 +9804,9 @@ }, "node_modules/listr2": { "version": "4.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-4.0.5.tgz", + "integrity": "sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==", "dev": true, - "license": "MIT", "dependencies": { "cli-truncate": "^2.1.0", "colorette": "^2.0.16", @@ -10133,8 +9831,9 @@ }, "node_modules/listr2/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -10147,8 +9846,9 @@ }, "node_modules/listr2/node_modules/cli-truncate": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", "dev": true, - "license": "MIT", "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" @@ -10162,8 +9862,9 @@ }, "node_modules/listr2/node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -10173,13 +9874,15 @@ }, "node_modules/listr2/node_modules/color-name": { "version": "1.1.4", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/listr2/node_modules/slice-ansi": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", @@ -10191,23 +9894,26 @@ }, "node_modules/loader-runner": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.11.5" } }, "node_modules/loader-utils": { - "version": "3.2.0", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 12.13.0" } }, "node_modules/locate-path": { "version": "5.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dependencies": { "p-locate": "^4.1.0" }, @@ -10217,12 +9923,14 @@ }, "node_modules/lodash": { "version": "4.17.21", - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash._baseassign": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha512-t3N26QR2IdSN+gqSy9Ds9pBu/J1EAFEshKlUHpJG3rvyJOYgcELIxcIeKKfZk7sjOz11cFfzJRsyFry/JyabJQ==", "dev": true, - "license": "MIT", "dependencies": { "lodash._basecopy": "^3.0.0", "lodash.keys": "^3.0.0" @@ -10230,18 +9938,21 @@ }, "node_modules/lodash._basecopy": { "version": "3.0.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha512-rFR6Vpm4HeCK1WPGvjZSJ+7yik8d8PVUdCJx5rT2pogG4Ve/2ZS7kfmO5l5T2o5V2mqlNIfSF5MZlr1+xOoYQQ==", + "dev": true }, "node_modules/lodash._bindcallback": { "version": "3.0.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", + "integrity": "sha512-2wlI0JRAGX8WEf4Gm1p/mv/SZ+jLijpj0jyaE/AXeuQphzCgD8ZQW4oSpoN8JAopujOFGU3KMuq7qfHBWlGpjQ==", + "dev": true }, "node_modules/lodash._createassigner": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz", + "integrity": "sha512-LziVL7IDnJjQeeV95Wvhw6G28Z8Q6da87LWKOPWmzBLv4u6FAT/x5v00pyGW0u38UoogNF2JnD3bGgZZDaNEBw==", "dev": true, - "license": "MIT", "dependencies": { "lodash._bindcallback": "^3.0.0", "lodash._isiterateecall": "^3.0.0", @@ -10250,18 +9961,21 @@ }, "node_modules/lodash._getnative": { "version": "3.9.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha512-RrL9VxMEPyDMHOd9uFbvMe8X55X16/cGM5IgOKgRElQZutpX89iS6vwl64duTV1/16w5JY7tuFNXqoekmh1EmA==", + "dev": true }, "node_modules/lodash._isiterateecall": { "version": "3.0.9", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha512-De+ZbrMu6eThFti/CSzhRvTKMgQToLxbij58LMfM8JnYDNSOjkjTCIaa8ixglOeGh2nyPlakbt5bJWJ7gvpYlQ==", + "dev": true }, "node_modules/lodash.assign": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-3.2.0.tgz", + "integrity": "sha512-/VVxzgGBmbphasTg51FrztxQJ/VgAUpol6zmJuSVSGcNg4g7FA4z7rQV8Ovr9V3vFBNWZhvKWHfpAytjTVUfFA==", "dev": true, - "license": "MIT", "dependencies": { "lodash._baseassign": "^3.0.0", "lodash._createassigner": "^3.0.0", @@ -10270,23 +9984,27 @@ }, "node_modules/lodash.debounce": { "version": "4.0.8", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true }, "node_modules/lodash.isarguments": { "version": "3.1.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "dev": true }, "node_modules/lodash.isarray": { "version": "3.0.4", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ==", + "dev": true }, "node_modules/lodash.keys": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha512-CuBsapFjcubOGMn3VD+24HOAPxM79tH+V6ivJL3CHYjtrawauDJHUk//Yew9Hvc6e9rbCrURGk8z6PC+8WJBfQ==", "dev": true, - "license": "MIT", "dependencies": { "lodash._getnative": "^3.0.0", "lodash.isarguments": "^3.0.0", @@ -10295,12 +10013,14 @@ }, "node_modules/lodash.restparam": { "version": "3.6.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", + "integrity": "sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==", + "dev": true }, "node_modules/log-symbols": { "version": "4.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" @@ -10314,7 +10034,8 @@ }, "node_modules/log-symbols/node_modules/ansi-styles": { "version": "4.3.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dependencies": { "color-convert": "^2.0.1" }, @@ -10327,7 +10048,8 @@ }, "node_modules/log-symbols/node_modules/chalk": { "version": "4.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -10341,7 +10063,8 @@ }, "node_modules/log-symbols/node_modules/color-convert": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dependencies": { "color-name": "~1.1.4" }, @@ -10351,18 +10074,21 @@ }, "node_modules/log-symbols/node_modules/color-name": { "version": "1.1.4", - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/log-symbols/node_modules/has-flag": { "version": "4.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "engines": { "node": ">=8" } }, "node_modules/log-symbols/node_modules/supports-color": { "version": "7.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dependencies": { "has-flag": "^4.0.0" }, @@ -10372,8 +10098,9 @@ }, "node_modules/log-update": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", "dev": true, - "license": "MIT", "dependencies": { "ansi-escapes": "^4.3.0", "cli-cursor": "^3.1.0", @@ -10389,8 +10116,9 @@ }, "node_modules/log-update/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -10403,8 +10131,9 @@ }, "node_modules/log-update/node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -10414,13 +10143,15 @@ }, "node_modules/log-update/node_modules/color-name": { "version": "1.1.4", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/log-update/node_modules/wrap-ansi": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -10431,21 +10162,24 @@ } }, "node_modules/long": { - "version": "5.2.0", - "license": "Apache-2.0" + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" }, "node_modules/lru-cache": { - "version": "7.14.0", - "devOptional": true, - "license": "ISC", - "engines": { - "node": ">=12" + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" } }, "node_modules/macos-release": { - "version": "2.5.0", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.1.tgz", + "integrity": "sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" }, @@ -10455,7 +10189,8 @@ }, "node_modules/magic-string": { "version": "0.26.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.2.tgz", + "integrity": "sha512-NzzlXpclt5zAbmo6h6jNc8zl2gNRGHvmsZW4IvZhTC4W7k4OlLP+S5YLussa/r3ixNT66KOQfNORlXHSOy/X4A==", "dependencies": { "sourcemap-codec": "^1.4.8" }, @@ -10465,8 +10200,9 @@ }, "node_modules/make-dir": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dev": true, - "license": "MIT", "dependencies": { "semver": "^6.0.0" }, @@ -10478,22 +10214,25 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/make-error": { "version": "1.3.6", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true }, "node_modules/make-fetch-happen": { "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", "devOptional": true, - "license": "ISC", "dependencies": { "agentkeepalive": "^4.2.1", "cacache": "^16.1.0", @@ -10516,9 +10255,19 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "devOptional": true, + "engines": { + "node": ">=12" + } + }, "node_modules/marked": { - "version": "4.1.0", - "license": "MIT", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "bin": { "marked": "bin/marked.js" }, @@ -10528,7 +10277,8 @@ }, "node_modules/md5.js": { "version": "1.3.5", - "license": "MIT", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1", @@ -10537,18 +10287,20 @@ }, "node_modules/media-typer": { "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/memfs": { - "version": "3.4.7", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", "dev": true, - "license": "Unlicense", "dependencies": { - "fs-monkey": "^1.0.3" + "fs-monkey": "^1.0.4" }, "engines": { "node": ">= 4.0.0" @@ -10556,36 +10308,41 @@ }, "node_modules/merge-descriptors": { "version": "1.0.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true }, "node_modules/merge-stream": { "version": "2.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true }, "node_modules/merge2": { "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "devOptional": true, - "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/methods": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/micromatch": { - "version": "4.0.5", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", "devOptional": true, - "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -10608,16 +10365,18 @@ }, "node_modules/mime-db": { "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, - "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -10627,15 +10386,17 @@ }, "node_modules/mimic-fn": { "version": "2.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "engines": { "node": ">=6" } }, "node_modules/mini-css-extract-plugin": { "version": "2.6.1", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.1.tgz", + "integrity": "sha512-wd+SD57/K6DiV7jIR34P+s3uckTRuQvx0tKPcvjFlrEylk6P4mQ2KSWk1hblj1Kxaqok7LogKOieygXqBczNlg==", "dev": true, - "license": "MIT", "dependencies": { "schema-utils": "^4.0.0" }, @@ -10651,14 +10412,15 @@ } }, "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { - "version": "4.0.0", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", "dev": true, - "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", + "ajv": "^8.9.0", "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" + "ajv-keywords": "^5.1.0" }, "engines": { "node": ">= 12.13.0" @@ -10670,13 +10432,15 @@ }, "node_modules/minimalistic-assert": { "version": "1.0.1", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true }, "node_modules/minimatch": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz", + "integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==", "devOptional": true, - "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -10685,14 +10449,19 @@ } }, "node_modules/minimist": { - "version": "1.2.6", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, - "license": "MIT" + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/minipass": { - "version": "3.3.4", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "devOptional": true, - "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -10702,8 +10471,9 @@ }, "node_modules/minipass-collect": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", "devOptional": true, - "license": "ISC", "dependencies": { "minipass": "^3.0.0" }, @@ -10713,8 +10483,9 @@ }, "node_modules/minipass-fetch": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", "devOptional": true, - "license": "MIT", "dependencies": { "minipass": "^3.1.6", "minipass-sized": "^1.0.3", @@ -10729,8 +10500,9 @@ }, "node_modules/minipass-flush": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", "devOptional": true, - "license": "ISC", "dependencies": { "minipass": "^3.0.0" }, @@ -10740,8 +10512,9 @@ }, "node_modules/minipass-json-stream": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", + "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", "devOptional": true, - "license": "MIT", "dependencies": { "jsonparse": "^1.3.1", "minipass": "^3.0.0" @@ -10749,8 +10522,9 @@ }, "node_modules/minipass-pipeline": { "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", "devOptional": true, - "license": "ISC", "dependencies": { "minipass": "^3.0.0" }, @@ -10760,8 +10534,9 @@ }, "node_modules/minipass-sized": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", "devOptional": true, - "license": "ISC", "dependencies": { "minipass": "^3.0.0" }, @@ -10769,10 +10544,17 @@ "node": ">=8" } }, + "node_modules/minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true + }, "node_modules/minizlib": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", "devOptional": true, - "license": "MIT", "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" @@ -10781,10 +10563,17 @@ "node": ">= 8" } }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true + }, "node_modules/mkdirp": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "devOptional": true, - "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" }, @@ -10794,25 +10583,29 @@ }, "node_modules/monaco-editor": { "version": "0.33.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.33.0.tgz", + "integrity": "sha512-VcRWPSLIUEgQJQIE0pVT8FcGBIgFoxz7jtqctE+IiCxWugD0DwgyQBcZBhdSrdMC84eumoqMZsGl2GTreOzwqw==" }, "node_modules/mrmime": { - "version": "1.0.1", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/ms": { "version": "2.1.2", - "devOptional": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true }, "node_modules/multicast-dns": { "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", "dev": true, - "license": "MIT", "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" @@ -10823,7 +10616,8 @@ }, "node_modules/multimatch": { "version": "5.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", "optional": true, "dependencies": { "@types/minimatch": "^3.0.3", @@ -10841,7 +10635,8 @@ }, "node_modules/multimatch/node_modules/brace-expansion": { "version": "1.1.11", - "license": "MIT", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "optional": true, "dependencies": { "balanced-match": "^1.0.0", @@ -10850,7 +10645,8 @@ }, "node_modules/multimatch/node_modules/minimatch": { "version": "3.1.2", - "license": "ISC", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "optional": true, "dependencies": { "brace-expansion": "^1.1.7" @@ -10861,20 +10657,29 @@ }, "node_modules/mustache": { "version": "4.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", "bin": { "mustache": "bin/mustache" } }, "node_modules/mute-stream": { "version": "0.0.8", - "devOptional": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "devOptional": true }, "node_modules/nanoid": { - "version": "3.3.4", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true, - "license": "MIT", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -10883,12 +10688,12 @@ } }, "node_modules/needle": { - "version": "3.1.0", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { - "debug": "^3.2.6", "iconv-lite": "^0.6.3", "sax": "^1.2.4" }, @@ -10899,19 +10704,11 @@ "node": ">= 4.4.x" } }, - "node_modules/needle/node_modules/debug": { - "version": "3.2.7", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/needle/node_modules/iconv-lite": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, - "license": "MIT", "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -10921,76 +10718,85 @@ } }, "node_modules/needle/node_modules/sax": { - "version": "1.2.4", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", "dev": true, - "license": "ISC", "optional": true }, "node_modules/negotiator": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "devOptional": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/neo-async": { "version": "2.6.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true }, "node_modules/netmask": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4.0" } }, "node_modules/ng-morph": { - "version": "2.1.0", - "license": "Apache-2.0", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/ng-morph/-/ng-morph-4.0.5.tgz", + "integrity": "sha512-5tnlb5WrGKeo2E7VRcV7ZHhScyNgliYqpbXqt103kynmfj6Ic8kzhJAhHu9iLkF1yRnKv2kyCE+O7UGZx5RraQ==", "optional": true, "dependencies": { - "jsonc-parser": "3.0.0", - "minimatch": "3.0.4", + "jsonc-parser": "3.2.0", + "minimatch": "9.0.3", "multimatch": "5.0.0", - "ts-morph": "10.0.2" + "ts-morph": "21.0.1", + "tslib": "2.6.2" }, "peerDependencies": { "@angular-devkit/core": ">=11.0.0", "@angular-devkit/schematics": ">=11.0.0" } }, - "node_modules/ng-morph/node_modules/brace-expansion": { - "version": "1.1.11", - "license": "MIT", - "optional": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/ng-morph/node_modules/jsonc-parser": { - "version": "3.0.0", - "license": "MIT", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", "optional": true }, "node_modules/ng-morph/node_modules/minimatch": { - "version": "3.0.4", - "license": "ISC", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "optional": true, "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/ng-morph/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "optional": true + }, "node_modules/ng-packagr": { - "version": "14.2.1", + "version": "14.2.2", + "resolved": "https://registry.npmjs.org/ng-packagr/-/ng-packagr-14.2.2.tgz", + "integrity": "sha512-AqwHcMM6x+JkCHT++IsbulnTdyoXcC2Cr4tbPamuieacc77+fFbB195hdcqEFwsKX5410cymx/ZUyHird9rxlg==", "dev": true, - "license": "MIT", "dependencies": { "@rollup/plugin-json": "^4.1.0", "@rollup/plugin-node-resolve": "^13.1.3", @@ -11034,7 +10840,8 @@ }, "node_modules/ng-qrcode": { "version": "7.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ng-qrcode/-/ng-qrcode-7.0.0.tgz", + "integrity": "sha512-Mx7nf8rtGMVYxGe2qfy8/JNiCnxKD7uFsqpP2Hm5eJSQrOEapQl9FR0yuK0I4MMQorJ7s8mZZDxmszQiH8R2Kg==", "dependencies": { "qrcode": "^1.5.0", "tslib": "^2.4.0" @@ -11046,9 +10853,10 @@ }, "node_modules/nice-napi": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", "dev": true, "hasInstallScript": true, - "license": "MIT", "optional": true, "os": [ "!win32" @@ -11060,27 +10868,31 @@ }, "node_modules/node-addon-api": { "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", "dev": true, - "license": "MIT", "optional": true }, "node_modules/node-forge": { "version": "1.3.1", - "license": "(BSD-3-Clause OR GPL-2.0)", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", "engines": { "node": ">= 6.13.0" } }, "node_modules/node-gyp": { - "version": "9.1.0", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", + "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", "devOptional": true, - "license": "MIT", "dependencies": { "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", "glob": "^7.1.4", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.0.3", - "nopt": "^5.0.0", + "nopt": "^6.0.0", "npmlog": "^6.0.0", "rimraf": "^3.0.2", "semver": "^7.3.5", @@ -11091,13 +10903,14 @@ "node-gyp": "bin/node-gyp.js" }, "engines": { - "node": "^12.22 || ^14.13 || >=16" + "node": "^12.13 || ^14.13 || >=16" } }, "node_modules/node-gyp-build": { - "version": "4.5.0", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", + "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", "dev": true, - "license": "MIT", "optional": true, "bin": { "node-gyp-build": "bin.js", @@ -11107,8 +10920,9 @@ }, "node_modules/node-gyp/node_modules/brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "devOptional": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -11116,8 +10930,10 @@ }, "node_modules/node-gyp/node_modules/glob": { "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "devOptional": true, - "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -11135,8 +10951,9 @@ }, "node_modules/node-gyp/node_modules/minimatch": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "devOptional": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -11146,8 +10963,9 @@ }, "node_modules/node-html-parser": { "version": "5.4.2", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-5.4.2.tgz", + "integrity": "sha512-RaBPP3+51hPne/OolXxcz89iYvQvKOydaqoePpOgXcrOKZhjVIzmpKZz+Hd/RBO2/zN2q6CNJhQzucVz+u3Jyw==", "dev": true, - "license": "MIT", "dependencies": { "css-select": "^4.2.1", "he": "1.2.0" @@ -11155,7 +10973,8 @@ }, "node_modules/node-jose": { "version": "2.2.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/node-jose/-/node-jose-2.2.0.tgz", + "integrity": "sha512-XPCvJRr94SjLrSIm4pbYHKLEaOsDvJCpyFw/6V/KK/IXmyZ6SFBzAUDO9HQf4DB/nTEFcRGH87mNciOP23kFjw==", "dependencies": { "base64url": "^3.0.1", "buffer": "^6.0.3", @@ -11169,35 +10988,43 @@ } }, "node_modules/node-jose/node_modules/uuid": { - "version": "9.0.0", - "license": "MIT", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } }, "node_modules/node-releases": { - "version": "2.0.6", - "dev": true, - "license": "MIT" + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true }, "node_modules/nopt": { - "version": "5.0.0", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", "devOptional": true, - "license": "ISC", "dependencies": { - "abbrev": "1" + "abbrev": "^1.0.0" }, "bin": { "nopt": "bin/nopt.js" }, "engines": { - "node": ">=6" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/normalize-package-data": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-4.0.1.tgz", + "integrity": "sha512-EBk5QKKuocMJhB3BILuKhmaPjI8vNRSpIfO9woLC6NyHVkKKdVEdAO1mrT0ZfxNR1lKwCcTkuZfmGIFdizZ8Pg==", "devOptional": true, - "license": "BSD-2-Clause", "dependencies": { "hosted-git-info": "^5.0.0", "is-core-module": "^2.8.1", @@ -11210,32 +11037,36 @@ }, "node_modules/normalize-path": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "devOptional": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/normalize-range": { "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/npm-bundled": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", + "integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==", "devOptional": true, - "license": "ISC", "dependencies": { "npm-normalize-package-bin": "^1.0.1" } }, "node_modules/npm-install-checks": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-5.0.0.tgz", + "integrity": "sha512-65lUsMI8ztHCxFz5ckCEC44DRvEGdZX5usQFriauxHEwt7upv1FKaQEmAtU0YnOAdwuNWCmk64xYiQABNrEyLA==", "devOptional": true, - "license": "BSD-2-Clause", "dependencies": { "semver": "^7.1.1" }, @@ -11245,13 +11076,15 @@ }, "node_modules/npm-normalize-package-bin": { "version": "1.0.1", - "devOptional": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "devOptional": true }, "node_modules/npm-package-arg": { "version": "9.1.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-9.1.0.tgz", + "integrity": "sha512-4J0GL+u2Nh6OnhvUKXRr2ZMG4lR8qtLp+kv7UiV00Y+nGiSxtttCyIRHCt5L5BNkXQld/RceYItau3MDOoGiBw==", "devOptional": true, - "license": "ISC", "dependencies": { "hosted-git-info": "^5.0.0", "proc-log": "^2.0.1", @@ -11264,8 +11097,9 @@ }, "node_modules/npm-packlist": { "version": "5.1.3", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-5.1.3.tgz", + "integrity": "sha512-263/0NGrn32YFYi4J533qzrQ/krmmrWwhKkzwTuM4f/07ug51odoaNjUexxO4vxlzURHcmYMH1QjvHjsNDKLVg==", "devOptional": true, - "license": "ISC", "dependencies": { "glob": "^8.0.1", "ignore-walk": "^5.0.1", @@ -11281,8 +11115,9 @@ }, "node_modules/npm-packlist/node_modules/npm-bundled": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-2.0.1.tgz", + "integrity": "sha512-gZLxXdjEzE/+mOstGDqR6b0EkhJ+kM6fxM6vUuckuctuVPh80Q6pw/rSZj9s4Gex9GxWtIicO1pc8DB9KZWudw==", "devOptional": true, - "license": "ISC", "dependencies": { "npm-normalize-package-bin": "^2.0.0" }, @@ -11292,16 +11127,18 @@ }, "node_modules/npm-packlist/node_modules/npm-normalize-package-bin": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-2.0.0.tgz", + "integrity": "sha512-awzfKUO7v0FscrSpRoogyNm0sajikhBWpU0QMrW09AMi9n1PoKU6WaIqUzuJSQnpciZZmJ/jMZ2Egfmb/9LiWQ==", "devOptional": true, - "license": "ISC", "engines": { "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/npm-pick-manifest": { "version": "7.0.1", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-7.0.1.tgz", + "integrity": "sha512-IA8+tuv8KujbsbLQvselW2XQgmXWS47t3CB0ZrzsRZ82DbDfkcFunOaPm4X7qNuhMfq+FmV7hQT4iFVpHqV7mg==", "devOptional": true, - "license": "ISC", "dependencies": { "npm-install-checks": "^5.0.0", "npm-normalize-package-bin": "^1.0.1", @@ -11314,8 +11151,9 @@ }, "node_modules/npm-registry-fetch": { "version": "13.3.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-13.3.1.tgz", + "integrity": "sha512-eukJPi++DKRTjSBRcDZSDDsGqRK3ehbxfFUcgaRd0Yp6kRwOwh2WVn0r+8rMB4nnuzvAk6rQVzl6K5CkYOmnvw==", "devOptional": true, - "license": "ISC", "dependencies": { "make-fetch-happen": "^10.0.6", "minipass": "^3.1.6", @@ -11331,8 +11169,9 @@ }, "node_modules/npm-run-path": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, - "license": "MIT", "dependencies": { "path-key": "^3.0.0" }, @@ -11342,8 +11181,10 @@ }, "node_modules/npmlog": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", "devOptional": true, - "license": "ISC", "dependencies": { "are-we-there-yet": "^3.0.0", "console-control-strings": "^1.1.0", @@ -11356,8 +11197,9 @@ }, "node_modules/nth-check": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0" }, @@ -11366,31 +11208,10 @@ } }, "node_modules/object-inspect": { - "version": "1.12.2", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, "engines": { "node": ">= 0.4" }, @@ -11400,13 +11221,15 @@ }, "node_modules/obuf": { "version": "1.1.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true }, "node_modules/on-finished": { "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dev": true, - "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, @@ -11416,23 +11239,26 @@ }, "node_modules/on-headers": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/once": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "devOptional": true, - "license": "ISC", "dependencies": { "wrappy": "1" } }, "node_modules/onetime": { "version": "5.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dependencies": { "mimic-fn": "^2.1.0" }, @@ -11445,8 +11271,9 @@ }, "node_modules/open": { "version": "8.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", + "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", "devOptional": true, - "license": "MIT", "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", @@ -11461,24 +11288,27 @@ }, "node_modules/opencollective-postinstall": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", "dev": true, - "license": "MIT", "bin": { "opencollective-postinstall": "index.js" } }, "node_modules/opener": { "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", "dev": true, - "license": "(WTFPL OR MIT)", "bin": { "opener": "bin/opener-bin.js" } }, "node_modules/optionator": { "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", "dev": true, - "license": "MIT", "dependencies": { "deep-is": "~0.1.3", "fast-levenshtein": "~2.0.6", @@ -11493,7 +11323,8 @@ }, "node_modules/ora": { "version": "5.4.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", @@ -11514,7 +11345,8 @@ }, "node_modules/ora/node_modules/ansi-styles": { "version": "4.3.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dependencies": { "color-convert": "^2.0.1" }, @@ -11527,7 +11359,8 @@ }, "node_modules/ora/node_modules/chalk": { "version": "4.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -11541,7 +11374,8 @@ }, "node_modules/ora/node_modules/color-convert": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dependencies": { "color-name": "~1.1.4" }, @@ -11551,18 +11385,21 @@ }, "node_modules/ora/node_modules/color-name": { "version": "1.1.4", - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/ora/node_modules/has-flag": { "version": "4.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "engines": { "node": ">=8" } }, "node_modules/ora/node_modules/supports-color": { "version": "7.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dependencies": { "has-flag": "^4.0.0" }, @@ -11572,8 +11409,9 @@ }, "node_modules/os-name": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-4.0.1.tgz", + "integrity": "sha512-xl9MAoU97MH1Xt5K9ERft2YfCAoaO6msy1OBA0ozxEC0x0TmIoE6K3QvgJMMZA9yKGLmHXNY/YZoDbiGDj4zYw==", "dev": true, - "license": "MIT", "dependencies": { "macos-release": "^2.5.0", "windows-release": "^4.0.0" @@ -11587,15 +11425,17 @@ }, "node_modules/os-tmpdir": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", "devOptional": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/p-limit": { "version": "2.3.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dependencies": { "p-try": "^2.0.0" }, @@ -11608,7 +11448,8 @@ }, "node_modules/p-locate": { "version": "4.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dependencies": { "p-limit": "^2.2.0" }, @@ -11618,8 +11459,9 @@ }, "node_modules/p-map": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "devOptional": true, - "license": "MIT", "dependencies": { "aggregate-error": "^3.0.0" }, @@ -11632,8 +11474,9 @@ }, "node_modules/p-retry": { "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" @@ -11644,23 +11487,26 @@ }, "node_modules/p-retry/node_modules/retry": { "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/p-try": { "version": "2.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "engines": { "node": ">=6" } }, "node_modules/pac-proxy-agent": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-5.0.0.tgz", + "integrity": "sha512-CcFG3ZtnxO8McDigozwE3AqAw15zDvGH+OjXO4kzf7IkEKkQ4gxQ+3sdF50WmhQ4P/bVusXcqNE2S3XrNURwzQ==", "dev": true, - "license": "MIT", "dependencies": { "@tootallnate/once": "1", "agent-base": "6", @@ -11678,16 +11524,18 @@ }, "node_modules/pac-proxy-agent/node_modules/@tootallnate/once": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/pac-proxy-agent/node_modules/http-proxy-agent": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", "dev": true, - "license": "MIT", "dependencies": { "@tootallnate/once": "1", "agent-base": "6", @@ -11699,8 +11547,9 @@ }, "node_modules/pac-proxy-agent/node_modules/socks-proxy-agent": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz", + "integrity": "sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ==", "dev": true, - "license": "MIT", "dependencies": { "agent-base": "^6.0.2", "debug": "4", @@ -11712,8 +11561,9 @@ }, "node_modules/pac-resolver": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-5.0.1.tgz", + "integrity": "sha512-cy7u00ko2KVgBAjuhevqpPeHIkCIqPe1v24cydhWjmeuzaBfmUWFCZJ1iAh5TuVzVZoUzXIW7K8sMYOZ84uZ9Q==", "dev": true, - "license": "MIT", "dependencies": { "degenerator": "^3.0.2", "ip": "^1.1.5", @@ -11723,15 +11573,11 @@ "node": ">= 8" } }, - "node_modules/pac-resolver/node_modules/ip": { - "version": "1.1.8", - "dev": true, - "license": "MIT" - }, "node_modules/pacote": { "version": "13.6.2", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-13.6.2.tgz", + "integrity": "sha512-Gu8fU3GsvOPkak2CkbojR7vjs3k3P9cA6uazKTHdsdV0gpCEQq2opelnEv30KRQWgVzP5Vd/5umjcedma3MKtg==", "devOptional": true, - "license": "ISC", "dependencies": { "@npmcli/git": "^3.0.0", "@npmcli/installed-package-contents": "^1.0.7", @@ -11763,13 +11609,15 @@ } }, "node_modules/pako": { - "version": "2.0.4", - "license": "(MIT AND Zlib)" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" }, "node_modules/parent-module": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, - "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -11779,8 +11627,9 @@ }, "node_modules/parse-json": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -11796,19 +11645,22 @@ }, "node_modules/parse-node-version": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.10" } }, "node_modules/parse5": { "version": "6.0.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" }, "node_modules/parse5-html-rewriting-stream": { "version": "6.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-6.0.1.tgz", + "integrity": "sha512-vwLQzynJVEfUlURxgnf51yAJDQTtVpNyGD8tKi2Za7m+akukNHxCcUQMAa/mUGLhCeicFdpy7Tlvj8ZNKadprg==", "dependencies": { "parse5": "^6.0.1", "parse5-sax-parser": "^6.0.1" @@ -11816,23 +11668,26 @@ }, "node_modules/parse5-htmlparser2-tree-adapter": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", "dev": true, - "license": "MIT", "dependencies": { "parse5": "^6.0.1" } }, "node_modules/parse5-sax-parser": { "version": "6.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-6.0.1.tgz", + "integrity": "sha512-kXX+5S81lgESA0LsDuGjAlBybImAChYRMT+/uKCEXFBFOeEhS52qUCydGhU3qLRD8D9DVjaUo821WK7DM4iCeg==", "dependencies": { "parse5": "^6.0.1" } }, "node_modules/parseurl": { "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -11843,53 +11698,61 @@ }, "node_modules/path-browserify": { "version": "1.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", "optional": true }, "node_modules/path-exists": { "version": "4.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "engines": { "node": ">=8" } }, "node_modules/path-is-absolute": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "devOptional": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/path-key": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/path-parse": { "version": "1.0.7", - "devOptional": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "devOptional": true }, "node_modules/path-to-regexp": { "version": "0.1.7", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true }, "node_modules/path-type": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/pbkdf2": { "version": "3.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", + "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", "dependencies": { "create-hash": "^1.1.2", "create-hmac": "^1.1.4", @@ -11902,14 +11765,16 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "dev": true, - "license": "ISC" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true }, "node_modules/picomatch": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "devOptional": true, - "license": "MIT", "engines": { "node": ">=8.6" }, @@ -11919,8 +11784,9 @@ }, "node_modules/pidtree": { "version": "0.5.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.5.0.tgz", + "integrity": "sha512-9nxspIM7OpZuhBxPg73Zvyq7j1QMPMPsGKTqRc2XOaFQauDvoNz9fM1Wdkjmeo7l9GXOZiRs97sPkuayl39wjA==", "dev": true, - "license": "MIT", "bin": { "pidtree": "bin/pidtree.js" }, @@ -11930,16 +11796,18 @@ }, "node_modules/pify": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/piscina": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-3.2.0.tgz", + "integrity": "sha512-yn/jMdHRw+q2ZJhFhyqsmANcbF6V2QwmD84c6xRau+QpQOmtrBCoRGdvTfeuFDYXB5W2m6MfLkjkvQa9lUSmIA==", "dev": true, - "license": "MIT", "dependencies": { "eventemitter-asyncresource": "^1.0.0", "hdr-histogram-js": "^2.0.1", @@ -11951,8 +11819,9 @@ }, "node_modules/pkg-dir": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, - "license": "MIT", "dependencies": { "find-up": "^4.0.0" }, @@ -11962,21 +11831,25 @@ }, "node_modules/please-upgrade-node": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", "dev": true, - "license": "MIT", "dependencies": { "semver-compare": "^1.0.0" } }, "node_modules/pngjs": { "version": "5.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", "engines": { "node": ">=10.13.0" } }, "node_modules/postcss": { - "version": "8.4.16", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "funding": [ { @@ -11986,11 +11859,14 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -12000,8 +11876,9 @@ }, "node_modules/postcss-attribute-case-insensitive": { "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz", + "integrity": "sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==", "dev": true, - "license": "MIT", "dependencies": { "postcss-selector-parser": "^6.0.10" }, @@ -12018,8 +11895,9 @@ }, "node_modules/postcss-clamp": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", "dev": true, - "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -12032,8 +11910,9 @@ }, "node_modules/postcss-color-functional-notation": { "version": "4.2.4", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz", + "integrity": "sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==", "dev": true, - "license": "CC0-1.0", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -12050,8 +11929,9 @@ }, "node_modules/postcss-color-hex-alpha": { "version": "8.0.4", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz", + "integrity": "sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==", "dev": true, - "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -12068,8 +11948,9 @@ }, "node_modules/postcss-color-rebeccapurple": { "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz", + "integrity": "sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==", "dev": true, - "license": "CC0-1.0", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -12086,8 +11967,9 @@ }, "node_modules/postcss-custom-media": { "version": "8.0.2", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz", + "integrity": "sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==", "dev": true, - "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -12103,9 +11985,10 @@ } }, "node_modules/postcss-custom-properties": { - "version": "12.1.9", + "version": "12.1.11", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz", + "integrity": "sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ==", "dev": true, - "license": "MIT", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -12122,8 +12005,9 @@ }, "node_modules/postcss-custom-selectors": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz", + "integrity": "sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==", "dev": true, - "license": "MIT", "dependencies": { "postcss-selector-parser": "^6.0.4" }, @@ -12140,8 +12024,9 @@ }, "node_modules/postcss-dir-pseudo-class": { "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz", + "integrity": "sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==", "dev": true, - "license": "CC0-1.0", "dependencies": { "postcss-selector-parser": "^6.0.10" }, @@ -12158,8 +12043,9 @@ }, "node_modules/postcss-double-position-gradients": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz", + "integrity": "sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==", "dev": true, - "license": "CC0-1.0", "dependencies": { "@csstools/postcss-progressive-custom-properties": "^1.1.0", "postcss-value-parser": "^4.2.0" @@ -12177,8 +12063,9 @@ }, "node_modules/postcss-env-function": { "version": "4.0.6", + "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz", + "integrity": "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==", "dev": true, - "license": "CC0-1.0", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -12191,8 +12078,9 @@ }, "node_modules/postcss-focus-visible": { "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz", + "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==", "dev": true, - "license": "CC0-1.0", "dependencies": { "postcss-selector-parser": "^6.0.9" }, @@ -12205,8 +12093,9 @@ }, "node_modules/postcss-focus-within": { "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz", + "integrity": "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==", "dev": true, - "license": "CC0-1.0", "dependencies": { "postcss-selector-parser": "^6.0.9" }, @@ -12219,16 +12108,18 @@ }, "node_modules/postcss-font-variant": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", "dev": true, - "license": "MIT", "peerDependencies": { "postcss": "^8.1.0" } }, "node_modules/postcss-gap-properties": { "version": "3.0.5", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz", + "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==", "dev": true, - "license": "CC0-1.0", "engines": { "node": "^12 || ^14 || >=16" }, @@ -12242,8 +12133,9 @@ }, "node_modules/postcss-image-set-function": { "version": "4.0.7", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz", + "integrity": "sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==", "dev": true, - "license": "CC0-1.0", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -12260,8 +12152,9 @@ }, "node_modules/postcss-import": { "version": "15.0.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.0.0.tgz", + "integrity": "sha512-Y20shPQ07RitgBGv2zvkEAu9bqvrD77C9axhj/aA1BQj4czape2MdClCExvB27EwYEJdGgKZBpKanb0t1rK2Kg==", "dev": true, - "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", @@ -12276,16 +12169,18 @@ }, "node_modules/postcss-initial": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", + "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", "dev": true, - "license": "MIT", "peerDependencies": { "postcss": "^8.0.0" } }, "node_modules/postcss-lab-function": { "version": "4.2.1", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz", + "integrity": "sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==", "dev": true, - "license": "CC0-1.0", "dependencies": { "@csstools/postcss-progressive-custom-properties": "^1.1.0", "postcss-value-parser": "^4.2.0" @@ -12303,8 +12198,9 @@ }, "node_modules/postcss-loader": { "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.0.1.tgz", + "integrity": "sha512-VRviFEyYlLjctSM93gAZtcJJ/iSkPZ79zWbN/1fSH+NisBByEiVLqpdVDrPLVSi8DX0oJo12kL/GppTBdKVXiQ==", "dev": true, - "license": "MIT", "dependencies": { "cosmiconfig": "^7.0.0", "klona": "^2.0.5", @@ -12324,8 +12220,9 @@ }, "node_modules/postcss-logical": { "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", + "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", "dev": true, - "license": "CC0-1.0", "engines": { "node": "^12 || ^14 || >=16" }, @@ -12335,8 +12232,9 @@ }, "node_modules/postcss-media-minmax": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", + "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -12345,9 +12243,10 @@ } }, "node_modules/postcss-modules-extract-imports": { - "version": "3.0.0", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", "dev": true, - "license": "ISC", "engines": { "node": "^10 || ^12 || >= 14" }, @@ -12356,9 +12255,10 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.0.0", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", + "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", "dev": true, - "license": "MIT", "dependencies": { "icss-utils": "^5.0.0", "postcss-selector-parser": "^6.0.2", @@ -12372,9 +12272,10 @@ } }, "node_modules/postcss-modules-scope": { - "version": "3.0.0", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", + "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", "dev": true, - "license": "ISC", "dependencies": { "postcss-selector-parser": "^6.0.4" }, @@ -12387,8 +12288,9 @@ }, "node_modules/postcss-modules-values": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", "dev": true, - "license": "ISC", "dependencies": { "icss-utils": "^5.0.0" }, @@ -12401,8 +12303,9 @@ }, "node_modules/postcss-nesting": { "version": "10.2.0", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.2.0.tgz", + "integrity": "sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==", "dev": true, - "license": "CC0-1.0", "dependencies": { "@csstools/selector-specificity": "^2.0.0", "postcss-selector-parser": "^6.0.10" @@ -12419,7 +12322,9 @@ } }, "node_modules/postcss-opacity-percentage": { - "version": "1.1.2", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz", + "integrity": "sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==", "dev": true, "funding": [ { @@ -12431,15 +12336,18 @@ "url": "https://liberapay.com/mrcgrtz" } ], - "license": "MIT", "engines": { "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" } }, "node_modules/postcss-overflow-shorthand": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz", + "integrity": "sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==", "dev": true, - "license": "CC0-1.0", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -12456,16 +12364,18 @@ }, "node_modules/postcss-page-break": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", "dev": true, - "license": "MIT", "peerDependencies": { "postcss": "^8" } }, "node_modules/postcss-place": { "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.5.tgz", + "integrity": "sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==", "dev": true, - "license": "CC0-1.0", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -12482,8 +12392,9 @@ }, "node_modules/postcss-preset-env": { "version": "7.8.0", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.0.tgz", + "integrity": "sha512-leqiqLOellpLKfbHkD06E04P6d9ZQ24mat6hu4NSqun7WG0UhspHR5Myiv/510qouCjoo4+YJtNOqg5xHaFnCA==", "dev": true, - "license": "CC0-1.0", "dependencies": { "@csstools/postcss-cascade-layers": "^1.0.5", "@csstools/postcss-color-function": "^1.1.1", @@ -12548,8 +12459,9 @@ }, "node_modules/postcss-pseudo-class-any-link": { "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz", + "integrity": "sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==", "dev": true, - "license": "CC0-1.0", "dependencies": { "postcss-selector-parser": "^6.0.10" }, @@ -12566,16 +12478,18 @@ }, "node_modules/postcss-replace-overflow-wrap": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", "dev": true, - "license": "MIT", "peerDependencies": { "postcss": "^8.0.3" } }, "node_modules/postcss-selector-not": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz", + "integrity": "sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==", "dev": true, - "license": "MIT", "dependencies": { "postcss-selector-parser": "^6.0.10" }, @@ -12591,9 +12505,10 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.0.10", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", + "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", "dev": true, - "license": "MIT", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -12604,8 +12519,9 @@ }, "node_modules/postcss-url": { "version": "10.1.3", + "resolved": "https://registry.npmjs.org/postcss-url/-/postcss-url-10.1.3.tgz", + "integrity": "sha512-FUzyxfI5l2tKmXdYc6VTu3TWZsInayEKPbiyW+P6vmmIrrb4I6CGX0BFoewgYHLK+oIL5FECEK02REYRpBvUCw==", "dev": true, - "license": "MIT", "dependencies": { "make-dir": "~3.1.0", "mime": "~2.5.2", @@ -12621,8 +12537,9 @@ }, "node_modules/postcss-url/node_modules/brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -12630,8 +12547,9 @@ }, "node_modules/postcss-url/node_modules/mime": { "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", "dev": true, - "license": "MIT", "bin": { "mime": "cli.js" }, @@ -12641,8 +12559,9 @@ }, "node_modules/postcss-url/node_modules/minimatch": { "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -12652,20 +12571,24 @@ }, "node_modules/postcss-value-parser": { "version": "4.2.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true }, "node_modules/prelude-ls": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", "dev": true, "engines": { "node": ">= 0.8.0" } }, "node_modules/prettier": { - "version": "2.7.1", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, - "license": "MIT", "bin": { "prettier": "bin-prettier.js" }, @@ -12678,8 +12601,9 @@ }, "node_modules/pretty-bytes": { "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" }, @@ -12689,33 +12613,38 @@ }, "node_modules/proc-log": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz", + "integrity": "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw==", "devOptional": true, - "license": "ISC", "engines": { "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/process": { "version": "0.11.10", - "license": "MIT", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", "engines": { "node": ">= 0.6.0" } }, "node_modules/process-nextick-args": { "version": "2.0.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true }, "node_modules/promise-inflight": { "version": "1.0.1", - "devOptional": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "devOptional": true }, "node_modules/promise-retry": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", "devOptional": true, - "license": "MIT", "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" @@ -12726,8 +12655,9 @@ }, "node_modules/proxy-addr": { "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "dev": true, - "license": "MIT", "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -12738,16 +12668,18 @@ }, "node_modules/proxy-addr/node_modules/ipaddr.js": { "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.10" } }, "node_modules/proxy-agent": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-5.0.0.tgz", + "integrity": "sha512-gkH7BkvLVkSfX9Dk27W6TyNOWWZWRilRfk1XxGNWOYJ2TuedAv1yFpCaU9QSBmBe716XOTNpYNOzhysyw8xn7g==", "dev": true, - "license": "MIT", "dependencies": { "agent-base": "^6.0.0", "debug": "4", @@ -12764,16 +12696,18 @@ }, "node_modules/proxy-agent/node_modules/@tootallnate/once": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/proxy-agent/node_modules/http-proxy-agent": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", "dev": true, - "license": "MIT", "dependencies": { "@tootallnate/once": "1", "agent-base": "6", @@ -12783,18 +12717,11 @@ "node": ">= 6" } }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "5.1.1", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, "node_modules/proxy-agent/node_modules/socks-proxy-agent": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz", + "integrity": "sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ==", "dev": true, - "license": "MIT", "dependencies": { "agent-base": "^6.0.2", "debug": "4", @@ -12804,41 +12731,41 @@ "node": ">= 6" } }, - "node_modules/proxy-agent/node_modules/yallist": { - "version": "3.1.1", - "dev": true, - "license": "ISC" - }, "node_modules/proxy-from-env": { "version": "1.1.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true }, "node_modules/prr": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", "dev": true, - "license": "MIT", "optional": true }, "node_modules/pump": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, - "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "node_modules/punycode": { - "version": "2.1.1", - "license": "MIT", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "engines": { "node": ">=6" } }, "node_modules/qrcode": { - "version": "1.5.1", - "license": "MIT", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", + "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", "dependencies": { "dijkstrajs": "^1.0.1", "encode-utf8": "^1.0.3", @@ -12854,7 +12781,8 @@ }, "node_modules/qrcode/node_modules/ansi-styles": { "version": "4.3.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dependencies": { "color-convert": "^2.0.1" }, @@ -12867,7 +12795,8 @@ }, "node_modules/qrcode/node_modules/cliui": { "version": "6.0.0", - "license": "ISC", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -12876,7 +12805,8 @@ }, "node_modules/qrcode/node_modules/color-convert": { "version": "2.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dependencies": { "color-name": "~1.1.4" }, @@ -12886,11 +12816,13 @@ }, "node_modules/qrcode/node_modules/color-name": { "version": "1.1.4", - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/qrcode/node_modules/wrap-ansi": { "version": "6.2.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -12902,11 +12834,13 @@ }, "node_modules/qrcode/node_modules/y18n": { "version": "4.0.3", - "license": "ISC" + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" }, "node_modules/qrcode/node_modules/yargs": { "version": "15.4.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", @@ -12926,7 +12860,8 @@ }, "node_modules/qrcode/node_modules/yargs-parser": { "version": "18.1.3", - "license": "ISC", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" @@ -12936,11 +12871,12 @@ } }, "node_modules/qs": { - "version": "6.11.0", + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.2.tgz", + "integrity": "sha512-x+NLUpx9SYrcwXtX7ob1gnkSems4i/mGZX5SlYxwIau6RrUSODO89TR/XDGGpn5RPWSYIB+aSfuSlV5+CmbTBg==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -12951,6 +12887,8 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "devOptional": true, "funding": [ { @@ -12965,29 +12903,31 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "MIT" + ] }, "node_modules/randombytes": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, - "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } }, "node_modules/range-parser": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/raw-body": { - "version": "2.5.1", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dev": true, - "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -13000,8 +12940,9 @@ }, "node_modules/raw-loader": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz", + "integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==", "dev": true, - "license": "MIT", "dependencies": { "loader-utils": "^2.0.0", "schema-utils": "^3.0.0" @@ -13019,8 +12960,9 @@ }, "node_modules/raw-loader/node_modules/ajv": { "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -13034,21 +12976,24 @@ }, "node_modules/raw-loader/node_modules/ajv-keywords": { "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, - "license": "MIT", "peerDependencies": { "ajv": "^6.9.1" } }, "node_modules/raw-loader/node_modules/json-schema-traverse": { "version": "0.4.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "node_modules/raw-loader/node_modules/loader-utils": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, - "license": "MIT", "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", @@ -13059,9 +13004,10 @@ } }, "node_modules/raw-loader/node_modules/schema-utils": { - "version": "3.1.1", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, - "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -13077,16 +13023,19 @@ }, "node_modules/read-cache": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", "dev": true, - "license": "MIT", "dependencies": { "pify": "^2.3.0" } }, "node_modules/read-package-json": { "version": "5.0.2", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-5.0.2.tgz", + "integrity": "sha512-BSzugrt4kQ/Z0krro8zhTwV1Kd79ue25IhNN/VtHFy1mG/6Tluyi+msc0UpwaoQzxSHa28mntAjIZY6kEgfR9Q==", + "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", "devOptional": true, - "license": "ISC", "dependencies": { "glob": "^8.0.1", "json-parse-even-better-errors": "^2.3.1", @@ -13099,8 +13048,9 @@ }, "node_modules/read-package-json-fast": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-2.0.3.tgz", + "integrity": "sha512-W/BKtbL+dUjTuRL2vziuYhp76s5HZ9qQhd/dKfWIZveD0O40453QNyZhC0e63lqZrAQ4jiOapVoeJ7JrszenQQ==", "devOptional": true, - "license": "ISC", "dependencies": { "json-parse-even-better-errors": "^2.3.0", "npm-normalize-package-bin": "^1.0.1" @@ -13111,15 +13061,17 @@ }, "node_modules/read-package-json/node_modules/npm-normalize-package-bin": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-2.0.0.tgz", + "integrity": "sha512-awzfKUO7v0FscrSpRoogyNm0sajikhBWpU0QMrW09AMi9n1PoKU6WaIqUzuJSQnpciZZmJ/jMZ2Egfmb/9LiWQ==", "devOptional": true, - "license": "ISC", "engines": { "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, "node_modules/readable-stream": { - "version": "3.6.0", - "license": "MIT", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -13131,8 +13083,9 @@ }, "node_modules/readdirp": { "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "devOptional": true, - "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -13141,19 +13094,22 @@ } }, "node_modules/reflect-metadata": { - "version": "0.1.13", - "dev": true, - "license": "Apache-2.0" + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", + "dev": true }, "node_modules/regenerate": { "version": "1.4.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true }, "node_modules/regenerate-unicode-properties": { - "version": "10.1.0", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", "dev": true, - "license": "MIT", "dependencies": { "regenerate": "^1.4.2" }, @@ -13163,47 +13119,47 @@ }, "node_modules/regenerator-runtime": { "version": "0.13.9", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", + "dev": true }, "node_modules/regenerator-transform": { - "version": "0.15.0", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", "dev": true, - "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.4" } }, "node_modules/regex-parser": { - "version": "2.2.11", - "dev": true, - "license": "MIT" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", + "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", + "dev": true }, "node_modules/regexpu-core": { - "version": "5.2.1", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", "dev": true, - "license": "MIT", "dependencies": { + "@babel/regjsgen": "^0.8.0", "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.1.0", - "regjsgen": "^0.7.1", "regjsparser": "^0.9.1", "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.0.0" + "unicode-match-property-value-ecmascript": "^2.1.0" }, "engines": { "node": ">=4" } }, - "node_modules/regjsgen": { - "version": "0.7.1", - "dev": true, - "license": "MIT" - }, "node_modules/regjsparser": { "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "jsesc": "~0.5.0" }, @@ -13213,6 +13169,8 @@ }, "node_modules/regjsparser/node_modules/jsesc": { "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", "dev": true, "bin": { "jsesc": "bin/jsesc" @@ -13220,31 +13178,36 @@ }, "node_modules/require-directory": { "version": "2.1.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "engines": { "node": ">=0.10.0" } }, "node_modules/require-from-string": { "version": "2.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "engines": { "node": ">=0.10.0" } }, "node_modules/require-main-filename": { "version": "2.0.0", - "license": "ISC" + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, "node_modules/requires-port": { "version": "1.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true }, "node_modules/resolve": { "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", "devOptional": true, - "license": "MIT", "dependencies": { "is-core-module": "^2.9.0", "path-parse": "^1.0.7", @@ -13259,16 +13222,18 @@ }, "node_modules/resolve-from": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/resolve-url-loader": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", "dev": true, - "license": "MIT", "dependencies": { "adjust-sourcemap-loader": "^4.0.0", "convert-source-map": "^1.7.0", @@ -13282,8 +13247,9 @@ }, "node_modules/resolve-url-loader/node_modules/loader-utils": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, - "license": "MIT", "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", @@ -13295,15 +13261,17 @@ }, "node_modules/resolve-url-loader/node_modules/source-map": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/restore-cursor": { "version": "3.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" @@ -13314,30 +13282,35 @@ }, "node_modules/retry": { "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "devOptional": true, - "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/reusify": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "devOptional": true, - "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, "node_modules/rfdc": { - "version": "1.3.0", - "dev": true, - "license": "MIT" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true }, "node_modules/rimraf": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "devOptional": true, - "license": "ISC", "dependencies": { "glob": "^7.1.3" }, @@ -13350,8 +13323,9 @@ }, "node_modules/rimraf/node_modules/brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "devOptional": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -13359,8 +13333,10 @@ }, "node_modules/rimraf/node_modules/glob": { "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "devOptional": true, - "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -13378,8 +13354,9 @@ }, "node_modules/rimraf/node_modules/minimatch": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "devOptional": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -13389,16 +13366,18 @@ }, "node_modules/ripemd160": { "version": "2.0.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1" } }, "node_modules/rollup": { - "version": "2.79.0", + "version": "2.79.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", "dev": true, - "license": "MIT", "bin": { "rollup": "dist/bin/rollup" }, @@ -13411,8 +13390,9 @@ }, "node_modules/rollup-plugin-sourcemaps": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/rollup-plugin-sourcemaps/-/rollup-plugin-sourcemaps-0.6.3.tgz", + "integrity": "sha512-paFu+nT1xvuO1tPFYXGe+XnQvg4Hjqv/eIhG8i5EspfYYPBKL57X7iVbfv55aNVASg3dzWvES9dmWsL2KhfByw==", "dev": true, - "license": "MIT", "dependencies": { "@rollup/pluginutils": "^3.0.9", "source-map-resolve": "^0.6.0" @@ -13432,22 +13412,26 @@ }, "node_modules/rsvp": { "version": "3.6.2", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.6.2.tgz", + "integrity": "sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==", "dev": true, - "license": "MIT", "engines": { "node": "0.12.* || 4.* || 6.* || >= 7.*" } }, "node_modules/run-async": { "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", "devOptional": true, - "license": "MIT", "engines": { "node": ">=0.12.0" } }, "node_modules/run-parallel": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "devOptional": true, "funding": [ { @@ -13463,7 +13447,6 @@ "url": "https://feross.org/support" } ], - "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -13477,18 +13460,35 @@ } }, "node_modules/safe-buffer": { - "version": "5.1.2", - "license": "MIT" + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, "node_modules/safer-buffer": { "version": "2.1.2", - "devOptional": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true }, "node_modules/sass": { "version": "1.54.4", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.54.4.tgz", + "integrity": "sha512-3tmF16yvnBwtlPrNBHw/H907j8MlOX8aTBnlNX1yrKx24RKcJGPyLhFUwkoKBKesR3unP93/2z14Ll8NicwQUA==", "dev": true, - "license": "MIT", "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -13503,8 +13503,9 @@ }, "node_modules/sass-loader": { "version": "13.0.2", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.0.2.tgz", + "integrity": "sha512-BbiqbVmbfJaWVeOOAu2o7DhYWtcNmTfvroVgFXa6k2hHheMxNAeDHLNoDy/Q5aoaVlz0LH+MbMktKwm9vN/j8Q==", "dev": true, - "license": "MIT", "dependencies": { "klona": "^2.0.4", "neo-async": "^2.6.2" @@ -13540,13 +13541,15 @@ }, "node_modules/sax": { "version": "1.1.4", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz", + "integrity": "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==", + "dev": true }, "node_modules/schema-utils": { "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", "dev": true, - "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.5", "ajv": "^6.12.4", @@ -13562,8 +13565,9 @@ }, "node_modules/schema-utils/node_modules/ajv": { "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -13577,27 +13581,32 @@ }, "node_modules/schema-utils/node_modules/ajv-keywords": { "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, - "license": "MIT", "peerDependencies": { "ajv": "^6.9.1" } }, "node_modules/schema-utils/node_modules/json-schema-traverse": { "version": "0.4.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "node_modules/select-hose": { "version": "2.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true }, "node_modules/selfsigned": { - "version": "2.1.1", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", "dev": true, - "license": "MIT", "dependencies": { + "@types/node-forge": "^1.3.0", "node-forge": "^1" }, "engines": { @@ -13605,9 +13614,10 @@ } }, "node_modules/semver": { - "version": "7.3.7", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", "devOptional": true, - "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -13620,13 +13630,15 @@ }, "node_modules/semver-compare": { "version": "1.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true }, "node_modules/semver-regex": { "version": "3.1.4", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.4.tgz", + "integrity": "sha512-6IiqeZNgq01qGf0TId0t3NvKzSvUsjcpdEO3AQNeIjR6A2+ckTnQlDpl4qu1bjRv0RzN3FP9hzFmws3lKqRWkA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" }, @@ -13636,8 +13648,9 @@ }, "node_modules/semver/node_modules/lru-cache": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "devOptional": true, - "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -13645,10 +13658,17 @@ "node": ">=10" } }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true + }, "node_modules/send": { "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", "dev": true, - "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -13670,24 +13690,18 @@ }, "node_modules/send/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/send/node_modules/debug/node_modules/ms": { "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/send/node_modules/depd": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true }, "node_modules/send/node_modules/mime": { "version": "1.6.0", @@ -13703,21 +13717,24 @@ }, "node_modules/send/node_modules/ms": { "version": "2.1.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true }, "node_modules/serialize-javascript": { - "version": "6.0.0", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" } }, "node_modules/serve-index": { "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", "dev": true, - "license": "MIT", "dependencies": { "accepts": "~1.3.4", "batch": "0.6.1", @@ -13733,16 +13750,27 @@ }, "node_modules/serve-index/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "license": "MIT", "dependencies": { "ms": "2.0.0" } }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/serve-index/node_modules/http-errors": { "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", "dev": true, - "license": "MIT", "dependencies": { "depd": "~1.1.2", "inherits": "2.0.3", @@ -13755,31 +13783,36 @@ }, "node_modules/serve-index/node_modules/inherits": { "version": "2.0.3", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true }, "node_modules/serve-index/node_modules/ms": { "version": "2.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true }, "node_modules/serve-index/node_modules/setprototypeof": { "version": "1.1.0", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true }, "node_modules/serve-index/node_modules/statuses": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/serve-static": { "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", "dev": true, - "license": "MIT", "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", @@ -13792,16 +13825,36 @@ }, "node_modules/set-blocking": { "version": "2.0.0", - "license": "ISC" + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/setprototypeof": { "version": "1.2.0", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true }, "node_modules/sha.js": { "version": "2.4.11", - "license": "(MIT AND BSD-3-Clause)", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -13812,8 +13865,9 @@ }, "node_modules/shallow-clone": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", "dev": true, - "license": "MIT", "dependencies": { "kind-of": "^6.0.2" }, @@ -13823,8 +13877,9 @@ }, "node_modules/shebang-command": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, - "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -13834,20 +13889,26 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/side-channel": { - "version": "1.0.4", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -13855,16 +13916,18 @@ }, "node_modules/signal-exit": { "version": "3.0.7", - "license": "ISC" + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, "node_modules/sirv": { - "version": "1.0.19", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", "dev": true, - "license": "MIT", "dependencies": { - "@polka/url": "^1.0.0-next.20", - "mrmime": "^1.0.0", - "totalist": "^1.0.0" + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" }, "engines": { "node": ">= 10" @@ -13872,8 +13935,9 @@ }, "node_modules/slash": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -13883,8 +13947,9 @@ }, "node_modules/slice-ansi": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", @@ -13899,8 +13964,9 @@ }, "node_modules/slice-ansi/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -13913,8 +13979,9 @@ }, "node_modules/slice-ansi/node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -13924,13 +13991,15 @@ }, "node_modules/slice-ansi/node_modules/color-name": { "version": "1.1.4", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/smart-buffer": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "devOptional": true, - "license": "MIT", "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" @@ -13938,8 +14007,9 @@ }, "node_modules/sockjs": { "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", "dev": true, - "license": "MIT", "dependencies": { "faye-websocket": "^0.11.3", "uuid": "^8.3.2", @@ -13947,22 +14017,24 @@ } }, "node_modules/socks": { - "version": "2.7.0", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", "devOptional": true, - "license": "MIT", "dependencies": { - "ip": "^2.0.0", + "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 10.13.0", + "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, "node_modules/socks-proxy-agent": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", "devOptional": true, - "license": "MIT", "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", @@ -13974,23 +14046,26 @@ }, "node_modules/source-map": { "version": "0.7.4", - "license": "BSD-3-Clause", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "engines": { "node": ">= 8" } }, "node_modules/source-map-js": { - "version": "1.0.2", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-loader": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-4.0.0.tgz", + "integrity": "sha512-i3KVgM3+QPAHNbGavK+VBq03YoJl24m9JWNbLgsjTj8aJzXG9M61bantBTNBt7CNwY2FYf+RJRYJ3pzalKjIrw==", "dev": true, - "license": "MIT", "dependencies": { "abab": "^2.0.6", "iconv-lite": "^0.6.3", @@ -14009,8 +14084,9 @@ }, "node_modules/source-map-loader/node_modules/iconv-lite": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, - "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -14020,8 +14096,10 @@ }, "node_modules/source-map-resolve": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", + "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", "dev": true, - "license": "MIT", "dependencies": { "atob": "^2.1.2", "decode-uri-component": "^0.2.0" @@ -14029,8 +14107,9 @@ }, "node_modules/source-map-support": { "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, - "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -14038,48 +14117,56 @@ }, "node_modules/source-map-support/node_modules/source-map": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/sourcemap-codec": { "version": "1.4.8", - "license": "MIT" + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead" }, "node_modules/spdx-correct": { - "version": "3.1.1", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "devOptional": true, - "license": "Apache-2.0", "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "node_modules/spdx-exceptions": { - "version": "2.3.0", - "devOptional": true, - "license": "CC-BY-3.0" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "devOptional": true }, "node_modules/spdx-expression-parse": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "devOptional": true, - "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "node_modules/spdx-license-ids": { - "version": "3.0.12", - "devOptional": true, - "license": "CC0-1.0" + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", + "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", + "devOptional": true }, "node_modules/spdy": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^4.1.0", "handle-thing": "^2.0.0", @@ -14093,8 +14180,9 @@ }, "node_modules/spdy-transport": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^4.1.0", "detect-node": "^2.0.4", @@ -14106,30 +14194,35 @@ }, "node_modules/split2": { "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", "dev": true, - "license": "ISC", "dependencies": { "readable-stream": "^3.0.0" } }, "node_modules/sprintf-js": { - "version": "1.0.3", - "dev": true, - "license": "BSD-3-Clause" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "devOptional": true }, "node_modules/ssh-config": { "version": "1.1.6", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/ssh-config/-/ssh-config-1.1.6.tgz", + "integrity": "sha512-ZPO9rECxzs5JIQ6G/2EfL1I9ho/BVZkx9HRKn8+0af7QgwAmumQ7XBFP1ggMyPMo+/tUbmv0HFdv4qifdO/9JA==", + "dev": true }, "node_modules/ssr-window": { "version": "4.0.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/ssr-window/-/ssr-window-4.0.2.tgz", + "integrity": "sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ==" }, "node_modules/ssri": { "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", "devOptional": true, - "license": "ISC", "dependencies": { "minipass": "^3.1.1" }, @@ -14139,25 +14232,28 @@ }, "node_modules/statuses": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/stream-combiner2": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", + "integrity": "sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==", "dev": true, - "license": "MIT", "dependencies": { "duplexer2": "~0.1.0", "readable-stream": "^2.0.2" } }, "node_modules/stream-combiner2/node_modules/readable-stream": { - "version": "2.3.7", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, - "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -14168,50 +14264,42 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/stream-combiner2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, "node_modules/stream-combiner2/node_modules/string_decoder": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, - "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } }, "node_modules/string_decoder": { "version": "1.3.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dependencies": { "safe-buffer": "~5.2.0" } }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/string-argv": { - "version": "0.3.1", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.6.19" } }, "node_modules/string-width": { "version": "4.2.3", - "license": "MIT", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -14223,7 +14311,8 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -14233,16 +14322,18 @@ }, "node_modules/strip-final-newline": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/stylus": { "version": "0.59.0", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.59.0.tgz", + "integrity": "sha512-lQ9w/XIOH5ZHVNuNbWW8D822r+/wBSO/d6XvtyHLF7LW4KaCIDeVbvn5DF8fGCJAUCwVhVi/h6J0NUcnylUEjg==", "dev": true, - "license": "MIT", "dependencies": { "@adobe/css-tools": "^4.0.1", "debug": "^4.3.2", @@ -14262,8 +14353,9 @@ }, "node_modules/stylus-loader": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/stylus-loader/-/stylus-loader-7.0.0.tgz", + "integrity": "sha512-WTbtLrNfOfLgzTaR9Lj/BPhQroKk/LC1hfTXSUbrxmxgfUo3Y3LpmKRVA2R1XbjvTAvOfaian9vOyfv1z99E+A==", "dev": true, - "license": "MIT", "dependencies": { "fast-glob": "^3.2.11", "klona": "^2.0.5", @@ -14283,8 +14375,9 @@ }, "node_modules/stylus/node_modules/brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -14292,8 +14385,10 @@ }, "node_modules/stylus/node_modules/glob": { "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, - "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -14311,8 +14406,9 @@ }, "node_modules/stylus/node_modules/minimatch": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -14322,13 +14418,16 @@ }, "node_modules/stylus/node_modules/sax": { "version": "1.2.4", - "dev": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true }, "node_modules/superagent": { "version": "5.3.1", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-5.3.1.tgz", + "integrity": "sha512-wjJ/MoTid2/RuGCOFtlacyGNxN9QLMgcpYLDQlWFIhhdJ93kNscFonGvrpAHSCVjRVj++DGCglocF7Aej1KHvQ==", + "deprecated": "Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net", "dev": true, - "license": "MIT", "dependencies": { "component-emitter": "^1.3.0", "cookiejar": "^2.1.2", @@ -14348,8 +14447,9 @@ }, "node_modules/superagent-proxy": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/superagent-proxy/-/superagent-proxy-3.0.0.tgz", + "integrity": "sha512-wAlRInOeDFyd9pyonrkJspdRAxdLrcsZ6aSnS+8+nu4x1aXbz6FWSTT9M6Ibze+eG60szlL7JA8wEIV7bPWuyQ==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^4.3.2", "proxy-agent": "^5.0.0" @@ -14363,8 +14463,9 @@ }, "node_modules/superagent/node_modules/mime": { "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true, - "license": "MIT", "bin": { "mime": "cli.js" }, @@ -14374,8 +14475,9 @@ }, "node_modules/supports-color": { "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, - "license": "MIT", "dependencies": { "has-flag": "^3.0.0" }, @@ -14385,8 +14487,9 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "devOptional": true, - "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -14395,7 +14498,9 @@ } }, "node_modules/swiper": { - "version": "8.4.2", + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-8.4.7.tgz", + "integrity": "sha512-VwO/KU3i9IV2Sf+W2NqyzwWob4yX9Qdedq6vBtS0rFqJ6Fa5iLUJwxQkuD4I38w0WDJwmFl8ojkdcRFPHWD+2g==", "funding": [ { "type": "patreon", @@ -14407,7 +14512,6 @@ } ], "hasInstallScript": true, - "license": "MIT", "dependencies": { "dom7": "^4.0.4", "ssr-window": "^4.0.2" @@ -14418,40 +14522,59 @@ }, "node_modules/symbol-observable": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", "devOptional": true, - "license": "MIT", "engines": { "node": ">=0.10" } }, "node_modules/tapable": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/tar": { - "version": "6.1.11", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "devOptional": true, - "license": "ISC", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", + "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" }, "engines": { - "node": ">= 10" + "node": ">=10" } }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "devOptional": true + }, "node_modules/terser": { "version": "5.14.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", @@ -14466,15 +14589,16 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.6", + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", "dev": true, - "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.14", + "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0", - "terser": "^5.14.1" + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" }, "engines": { "node": ">= 10.13.0" @@ -14500,8 +14624,9 @@ }, "node_modules/terser-webpack-plugin/node_modules/ajv": { "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -14515,21 +14640,30 @@ }, "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, - "license": "MIT", "peerDependencies": { "ajv": "^6.9.1" } }, + "node_modules/terser-webpack-plugin/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { "version": "0.4.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.1.1", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, - "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -14543,15 +14677,35 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/terser-webpack-plugin/node_modules/terser": { + "version": "5.31.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.1.tgz", + "integrity": "sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true }, "node_modules/test-exclude": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, - "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", @@ -14563,8 +14717,9 @@ }, "node_modules/test-exclude/node_modules/brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -14572,8 +14727,10 @@ }, "node_modules/test-exclude/node_modules/glob": { "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, - "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -14591,8 +14748,9 @@ }, "node_modules/test-exclude/node_modules/minimatch": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -14602,27 +14760,32 @@ }, "node_modules/text-mask-core": { "version": "5.1.2", - "license": "Unlicense" + "resolved": "https://registry.npmjs.org/text-mask-core/-/text-mask-core-5.1.2.tgz", + "integrity": "sha512-VfkCMdmRRZqXgQZFlDMiavm3hzsMzBM23CxHZsaeAYg66ZhXCNJWrFmnJwNy8KF9f74YvAUAuQenxsMCfuvhUw==" }, "node_modules/text-table": { "version": "0.2.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true }, "node_modules/through": { "version": "2.3.8", - "devOptional": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "devOptional": true }, "node_modules/thunky": { "version": "1.1.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true }, "node_modules/tmp": { "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "devOptional": true, - "license": "MIT", "dependencies": { "os-tmpdir": "~1.0.2" }, @@ -14632,16 +14795,18 @@ }, "node_modules/to-fast-properties": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/to-regex-range": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "devOptional": true, - "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -14651,24 +14816,27 @@ }, "node_modules/toidentifier": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.6" } }, "node_modules/totalist": { - "version": "1.1.0", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/tree-kill": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, - "license": "MIT", "bin": { "tree-kill": "cli.js" } @@ -14679,18 +14847,20 @@ "integrity": "sha512-UFYaKgfqlg9FROK7bdpYqFwG1CJvP4kOJdjXuWoqxo9jCmANoDw1GxkSCpJgoTeIiSTaTH5Qr1klSspb8c+ydg==" }, "node_modules/ts-morph": { - "version": "10.0.2", - "license": "MIT", + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-21.0.1.tgz", + "integrity": "sha512-dbDtVdEAncKctzrVZ+Nr7kHpHkv+0JDJb2MjjpBaj8bFeCkePU9rHfMklmhuLFnpeq/EJZk2IhStY6NzqgjOkg==", "optional": true, "dependencies": { - "@ts-morph/common": "~0.9.0", - "code-block-writer": "^10.1.1" + "@ts-morph/common": "~0.22.0", + "code-block-writer": "^12.0.0" } }, "node_modules/ts-node": { - "version": "10.9.1", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, - "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -14730,13 +14900,16 @@ } }, "node_modules/tslib": { - "version": "2.4.0", - "license": "0BSD" + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, "node_modules/tslint": { "version": "6.1.3", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz", + "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==", + "deprecated": "TSLint has been deprecated in favor of ESLint. Please see https://github.com/palantir/tslint/issues/4534 for more information.", "dev": true, - "license": "Apache-2.0", "dependencies": { "@babel/code-frame": "^7.0.0", "builtin-modules": "^1.1.1", @@ -14764,16 +14937,18 @@ }, "node_modules/tslint/node_modules/argparse": { "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, - "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" } }, "node_modules/tslint/node_modules/brace-expansion": { "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -14781,21 +14956,25 @@ }, "node_modules/tslint/node_modules/builtin-modules": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha512-wxXCdllwGhI2kCC0MnvTGYTMvnVZTvqgypkiTI8Pa5tcz2i6VqsqwYGgqwXji+4RgCzms6EajE4IxiUH6HH8nQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/tslint/node_modules/commander": { "version": "2.20.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true }, "node_modules/tslint/node_modules/glob": { "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, - "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -14813,8 +14992,9 @@ }, "node_modules/tslint/node_modules/js-yaml": { "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -14825,8 +15005,9 @@ }, "node_modules/tslint/node_modules/minimatch": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -14836,8 +15017,9 @@ }, "node_modules/tslint/node_modules/mkdirp": { "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, - "license": "MIT", "dependencies": { "minimist": "^1.2.6" }, @@ -14846,22 +15028,31 @@ } }, "node_modules/tslint/node_modules/semver": { - "version": "5.7.1", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver" } }, + "node_modules/tslint/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, "node_modules/tslint/node_modules/tslib": { "version": "1.14.1", - "dev": true, - "license": "0BSD" + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true }, "node_modules/tsutils": { "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", "dev": true, - "license": "MIT", "dependencies": { "tslib": "^1.8.1" }, @@ -14871,13 +15062,15 @@ }, "node_modules/tsutils/node_modules/tslib": { "version": "1.14.1", - "dev": true, - "license": "0BSD" + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true }, "node_modules/type-check": { "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", "dev": true, - "license": "MIT", "dependencies": { "prelude-ls": "~1.1.2" }, @@ -14887,8 +15080,9 @@ }, "node_modules/type-fest": { "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "devOptional": true, - "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -14898,8 +15092,9 @@ }, "node_modules/type-is": { "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "dev": true, - "license": "MIT", "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -14910,21 +15105,24 @@ }, "node_modules/typed-assert": { "version": "1.0.9", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", "dev": true, - "license": "MIT", "dependencies": { "is-typedarray": "^1.0.0" } }, "node_modules/typescript": { "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", "dev": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14935,16 +15133,18 @@ }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/unicode-match-property-ecmascript": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", "dev": true, - "license": "MIT", "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" @@ -14954,63 +15154,72 @@ } }, "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.0.0", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/unicode-property-aliases-ecmascript": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/unique-filename": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", "devOptional": true, - "license": "ISC", "dependencies": { "unique-slug": "^2.0.0" } }, "node_modules/unique-slug": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", "devOptional": true, - "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4" } }, "node_modules/universalify": { - "version": "2.0.0", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 10.0.0" } }, "node_modules/unpipe": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/untildify": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/update-browserslist-db": { - "version": "1.0.9", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", "dev": true, "funding": [ { @@ -15020,15 +15229,18 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" }, "bin": { - "browserslist-lint": "cli.js" + "update-browserslist-db": "cli.js" }, "peerDependencies": { "browserslist": ">= 4.21.0" @@ -15036,39 +15248,45 @@ }, "node_modules/uri-js": { "version": "4.4.1", - "license": "BSD-2-Clause", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dependencies": { "punycode": "^2.1.0" } }, "node_modules/util-deprecate": { "version": "1.0.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/utils-merge": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.4.0" } }, "node_modules/uuid": { "version": "8.3.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "bin": { "uuid": "dist/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true }, "node_modules/validate-npm-package-license": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "devOptional": true, - "license": "Apache-2.0", "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" @@ -15076,8 +15294,9 @@ }, "node_modules/validate-npm-package-name": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-4.0.0.tgz", + "integrity": "sha512-mzR0L8ZDktZjpX4OB46KT+56MAhl4EIazWP/+G/HPGuvfdaqg4YsCdtOm6U9+LOFyYDoh4dpnpxZRB9MQQns5Q==", "devOptional": true, - "license": "ISC", "dependencies": { "builtins": "^5.0.0" }, @@ -15087,16 +15306,19 @@ }, "node_modules/vary": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/vm2": { - "version": "3.9.11", + "version": "3.9.19", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.19.tgz", + "integrity": "sha512-J637XF0DHDMV57R6JyVsTak7nIL8gy5KH4r1HiwWLf/4GBbb5MKL5y7LpmF4A8E2nR6XmzpmMFQ7V7ppPTmUQg==", + "deprecated": "The library contains critical security issues and should not be used for production! The maintenance of the project has been discontinued. Consider migrating your code to isolated-vm.", "dev": true, - "license": "MIT", "dependencies": { "acorn": "^8.7.0", "acorn-walk": "^8.2.0" @@ -15109,9 +15331,10 @@ } }, "node_modules/watchpack": { - "version": "2.4.0", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", "dev": true, - "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -15122,47 +15345,51 @@ }, "node_modules/wbuf": { "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", "dev": true, - "license": "MIT", "dependencies": { "minimalistic-assert": "^1.0.0" } }, "node_modules/wcwidth": { "version": "1.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", "dependencies": { "defaults": "^1.0.3" } }, "node_modules/webpack": { - "version": "5.74.0", + "version": "5.92.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz", + "integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.7.6", - "browserslist": "^4.14.5", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.10.0", - "es-module-lexer": "^0.9.0", + "enhanced-resolve": "^5.17.0", + "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", + "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", + "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.4.0", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { @@ -15182,19 +15409,22 @@ } }, "node_modules/webpack-bundle-analyzer": { - "version": "4.8.0", + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", + "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", "dev": true, - "license": "MIT", "dependencies": { "@discoveryjs/json-ext": "0.5.7", "acorn": "^8.0.4", "acorn-walk": "^8.0.0", - "chalk": "^4.1.0", "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", "gzip-size": "^6.0.0", - "lodash": "^4.17.20", + "html-escaper": "^2.0.2", "opener": "^1.5.2", - "sirv": "^1.0.7", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", "ws": "^7.3.1" }, "bin": { @@ -15204,102 +15434,32 @@ "node": ">= 10.13.0" } }, - "node_modules/webpack-bundle-analyzer/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, "node_modules/webpack-bundle-analyzer/node_modules/commander": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 10" } }, - "node_modules/webpack-bundle-analyzer/node_modules/has-flag": { + "node_modules/webpack-bundle-analyzer/node_modules/escape-string-regexp": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, - "license": "MIT", "engines": { - "node": ">=8" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" + "node": ">=10" }, - "engines": { - "node": ">=8" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/ws": { - "version": "7.5.9", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/webpack-dev-middleware": { "version": "5.3.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", + "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", "dev": true, - "license": "MIT", "dependencies": { "colorette": "^2.0.10", "memfs": "^3.4.3", @@ -15319,14 +15479,15 @@ } }, "node_modules/webpack-dev-middleware/node_modules/schema-utils": { - "version": "4.0.0", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", "dev": true, - "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", + "ajv": "^8.9.0", "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" + "ajv-keywords": "^5.1.0" }, "engines": { "node": ">= 12.13.0" @@ -15338,8 +15499,9 @@ }, "node_modules/webpack-dev-server": { "version": "4.11.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.11.0.tgz", + "integrity": "sha512-L5S4Q2zT57SK7tazgzjMiSMBdsw+rGYIX27MgPgx7LDhWO0lViPrHKoLS7jo5In06PWYAhlYu3PbyoC6yAThbw==", "dev": true, - "license": "MIT", "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -15391,14 +15553,15 @@ } }, "node_modules/webpack-dev-server/node_modules/schema-utils": { - "version": "4.0.0", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", "dev": true, - "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", + "ajv": "^8.9.0", "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" + "ajv-keywords": "^5.1.0" }, "engines": { "node": ">= 12.13.0" @@ -15408,10 +15571,32 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/webpack-merge": { "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", "dev": true, - "license": "MIT", "dependencies": { "clone-deep": "^4.0.1", "wildcard": "^2.0.0" @@ -15422,16 +15607,18 @@ }, "node_modules/webpack-sources": { "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", "dev": true, - "license": "MIT", "engines": { "node": ">=10.13.0" } }, "node_modules/webpack-subresource-integrity": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", "dev": true, - "license": "MIT", "dependencies": { "typed-assert": "^1.0.8" }, @@ -15448,10 +15635,19 @@ } } }, + "node_modules/webpack/node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true, + "peer": true + }, "node_modules/webpack/node_modules/ajv": { "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -15465,21 +15661,27 @@ }, "node_modules/webpack/node_modules/ajv-keywords": { "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, - "license": "MIT", + "peer": true, "peerDependencies": { "ajv": "^6.9.1" } }, "node_modules/webpack/node_modules/json-schema-traverse": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, - "license": "MIT" + "peer": true }, "node_modules/webpack/node_modules/schema-utils": { - "version": "3.1.1", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, - "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -15495,8 +15697,9 @@ }, "node_modules/websocket-driver": { "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", "dev": true, - "license": "Apache-2.0", "dependencies": { "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", @@ -15508,16 +15711,18 @@ }, "node_modules/websocket-extensions": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=0.8.0" } }, "node_modules/which": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "devOptional": true, - "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -15529,34 +15734,39 @@ } }, "node_modules/which-module": { - "version": "2.0.0", - "license": "ISC" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" }, "node_modules/which-pm-runs": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", + "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/wide-align": { "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", "devOptional": true, - "license": "ISC", "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } }, "node_modules/wildcard": { - "version": "2.0.0", - "dev": true, - "license": "MIT" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true }, "node_modules/windows-release": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-4.0.0.tgz", + "integrity": "sha512-OxmV4wzDKB1x7AZaZgXMVsdJ1qER1ed83ZrTYd5Bwq2HfJVg3DJS8nqlAG4sMoJ7mu8cuRmLEYyU13BKwctRAg==", "dev": true, - "license": "MIT", "dependencies": { "execa": "^4.0.2" }, @@ -15569,8 +15779,9 @@ }, "node_modules/windows-release/node_modules/execa": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", "dev": true, - "license": "MIT", "dependencies": { "cross-spawn": "^7.0.0", "get-stream": "^5.0.0", @@ -15591,8 +15802,9 @@ }, "node_modules/windows-release/node_modules/get-stream": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, - "license": "MIT", "dependencies": { "pump": "^3.0.0" }, @@ -15605,24 +15817,27 @@ }, "node_modules/windows-release/node_modules/human-signals": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", "dev": true, - "license": "Apache-2.0", "engines": { "node": ">=8.12.0" } }, "node_modules/word-wrap": { - "version": "1.2.3", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/wrap-ansi": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "devOptional": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -15637,8 +15852,9 @@ }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "devOptional": true, - "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -15651,8 +15867,9 @@ }, "node_modules/wrap-ansi/node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "devOptional": true, - "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -15662,18 +15879,21 @@ }, "node_modules/wrap-ansi/node_modules/color-name": { "version": "1.1.4", - "devOptional": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true }, "node_modules/wrappy": { "version": "1.0.2", - "devOptional": true, - "license": "ISC" + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "devOptional": true }, "node_modules/write-file-atomic": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", "dev": true, - "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", "is-typedarray": "^1.0.0", @@ -15682,11 +15902,12 @@ } }, "node_modules/ws": { - "version": "8.8.1", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, - "license": "MIT", "engines": { - "node": ">=10.0.0" + "node": ">=8.3.0" }, "peerDependencies": { "bufferutil": "^4.0.1", @@ -15703,42 +15924,51 @@ }, "node_modules/xregexp": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", + "integrity": "sha512-xl/50/Cf32VsGq/1R8jJE5ajH1yMCQkpmoS10QbFZWl2Oor4H0Me64Pu2yxvsRWK3m6soJbmGfzSR7BYmDcWAA==", "dev": true, - "license": "MIT" + "engines": { + "node": "*" + } }, "node_modules/xxhashjs": { "version": "0.2.2", + "resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz", + "integrity": "sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==", "dev": true, - "license": "MIT", "dependencies": { "cuint": "^0.2.2" } }, "node_modules/y18n": { "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "devOptional": true, - "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yallist": { - "version": "4.0.0", - "devOptional": true, - "license": "ISC" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true }, "node_modules/yaml": { "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "dev": true, - "license": "ISC", "engines": { "node": ">= 6" } }, "node_modules/yargs": { "version": "17.5.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", + "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", "devOptional": true, - "license": "MIT", "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -15754,24 +15984,27 @@ }, "node_modules/yargs-parser": { "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "devOptional": true, - "license": "ISC", "engines": { "node": ">=12" } }, "node_modules/yn": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/yocto-queue": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" }, @@ -15781,7 +16014,8 @@ }, "node_modules/zone.js": { "version": "0.11.8", - "license": "MIT", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.11.8.tgz", + "integrity": "sha512-82bctBg2hKcEJ21humWIkXRlLBBmrc3nN7DFh5LGGhcyycO2S7FN8NmdvlcKaGFDNVL4/9kFLmwmInTavdJERA==", "dependencies": { "tslib": "^2.3.0" } diff --git a/web/package.json b/web/package.json index 1db0dead4..e159a2ee5 100644 --- a/web/package.json +++ b/web/package.json @@ -43,20 +43,22 @@ "@angular/service-worker": "^14.2.2", "@ionic/angular": "^6.1.15", "@materia-ui/ngx-monaco-editor": "^6.0.0", - "@ng-web-apis/common": "^2.0.0", - "@ng-web-apis/mutation-observer": "^2.0.0", - "@ng-web-apis/resize-observer": "^2.0.0", + "@ng-web-apis/common": "^3.0.6", + "@ng-web-apis/mutation-observer": "^3.2.1", + "@ng-web-apis/resize-observer": "^3.2.1", "@noble/curves": "^1.4.0", "@noble/hashes": "^1.4.0", "@start9labs/argon2": "^0.2.2", "@start9labs/emver": "^0.1.5", "@start9labs/start-sdk": "file:../sdk/dist", - "@taiga-ui/addon-charts": "3.20.0", - "@taiga-ui/cdk": "3.20.0", - "@taiga-ui/core": "3.20.0", - "@taiga-ui/icons": "3.20.0", - "@taiga-ui/kit": "3.20.0", + "@taiga-ui/addon-charts": "3.84.0", + "@taiga-ui/cdk": "3.84.0", + "@taiga-ui/core": "3.84.0", + "@taiga-ui/experimental": "3.84.0", + "@taiga-ui/icons": "3.84.0", + "@taiga-ui/kit": "3.84.0", "@tinkoff/ng-dompurify": "4.0.0", + "@tinkoff/ng-event-plugins": "3.2.0", "angular-svg-round-progressbar": "^9.0.0", "ansi-to-html": "^0.7.2", "base64-js": "^1.5.1", diff --git a/web/projects/setup-wizard/src/app/app.component.ts b/web/projects/setup-wizard/src/app/app.component.ts index e10d672e5..e07df5870 100644 --- a/web/projects/setup-wizard/src/app/app.component.ts +++ b/web/projects/setup-wizard/src/app/app.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core' import { NavController } from '@ionic/angular' +import { ErrorService } from '@start9labs/shared' import { ApiService } from './services/api/api.service' -import { ErrorToastService } from '@start9labs/shared' @Component({ selector: 'app-root', @@ -11,7 +11,7 @@ import { ErrorToastService } from '@start9labs/shared' export class AppComponent { constructor( private readonly apiService: ApiService, - private readonly errorToastService: ErrorToastService, + private readonly errorService: ErrorService, private readonly navCtrl: NavController, ) {} @@ -26,7 +26,7 @@ export class AppComponent { await this.navCtrl.navigateForward(route) } catch (e: any) { - this.errorToastService.present(e) + this.errorService.handleError(e) } } } diff --git a/web/projects/setup-wizard/src/app/app.module.ts b/web/projects/setup-wizard/src/app/app.module.ts index f2ba59012..973ce35ee 100644 --- a/web/projects/setup-wizard/src/app/app.module.ts +++ b/web/projects/setup-wizard/src/app/app.module.ts @@ -2,7 +2,7 @@ import { NgModule } from '@angular/core' import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { RouteReuseStrategy } from '@angular/router' import { HttpClientModule } from '@angular/common/http' -import { TuiRootModule } from '@taiga-ui/core' +import { TuiAlertModule, TuiRootModule } from '@taiga-ui/core' import { ApiService } from './services/api/api.service' import { MockApiService } from './services/api/mock-api.service' import { LiveApiService } from './services/api/live-api.service' @@ -41,6 +41,7 @@ const { RecoverPageModule, TransferPageModule, TuiRootModule, + TuiAlertModule, ], providers: [ { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, diff --git a/web/projects/setup-wizard/src/app/pages/attach/attach.page.ts b/web/projects/setup-wizard/src/app/pages/attach/attach.page.ts index b4d6eb9f9..3b8323798 100644 --- a/web/projects/setup-wizard/src/app/pages/attach/attach.page.ts +++ b/web/projects/setup-wizard/src/app/pages/attach/attach.page.ts @@ -4,10 +4,10 @@ import { ModalController, NavController, } from '@ionic/angular' -import { ApiService } from 'src/app/services/api/api.service' -import { DiskInfo, ErrorToastService } from '@start9labs/shared' -import { StateService } from 'src/app/services/state.service' +import { DiskInfo, ErrorService } from '@start9labs/shared' import { PasswordPage } from 'src/app/modals/password/password.page' +import { ApiService } from 'src/app/services/api/api.service' +import { StateService } from 'src/app/services/state.service' @Component({ selector: 'app-attach', @@ -21,7 +21,7 @@ export class AttachPage { constructor( private readonly apiService: ApiService, private readonly navCtrl: NavController, - private readonly errToastService: ErrorToastService, + private readonly errorService: ErrorService, private readonly stateService: StateService, private readonly modalCtrl: ModalController, private readonly loadingCtrl: LoadingController, @@ -41,7 +41,7 @@ export class AttachPage { try { this.drives = await this.apiService.getDrives() } catch (e: any) { - this.errToastService.present(e) + this.errorService.handleError(e) } finally { this.loading = false } @@ -70,7 +70,7 @@ export class AttachPage { await this.stateService.importDrive(guid, password) await this.navCtrl.navigateForward(`/loading`) } catch (e: any) { - this.errToastService.present(e) + this.errorService.handleError(e) } finally { loader.dismiss() } diff --git a/web/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts b/web/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts index 7f7ee6241..110768be5 100644 --- a/web/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts +++ b/web/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts @@ -5,8 +5,8 @@ import { ModalController, NavController, } from '@ionic/angular' +import { DiskInfo, ErrorService, GuidPipe } from '@start9labs/shared' import { ApiService } from 'src/app/services/api/api.service' -import { DiskInfo, ErrorToastService, GuidPipe } from '@start9labs/shared' import { StateService } from 'src/app/services/state.service' import { PasswordPage } from '../../modals/password/password.page' @@ -27,7 +27,7 @@ export class EmbassyPage { private readonly alertCtrl: AlertController, private readonly stateService: StateService, private readonly loadingCtrl: LoadingController, - private readonly errorToastService: ErrorToastService, + private readonly errorService: ErrorService, private readonly guidPipe: GuidPipe, ) {} @@ -71,7 +71,7 @@ export class EmbassyPage { }) } } catch (e: any) { - this.errorToastService.present(e) + this.errorService.handleError(e) } finally { this.loading = false } @@ -148,7 +148,7 @@ export class EmbassyPage { await this.stateService.setupEmbassy(logicalname, password) await this.navCtrl.navigateForward(`/loading`) } catch (e: any) { - this.errorToastService.present(e) + this.errorService.handleError(e) } finally { loader.dismiss() } diff --git a/web/projects/setup-wizard/src/app/pages/home/home.page.ts b/web/projects/setup-wizard/src/app/pages/home/home.page.ts index c0e93d18a..f3d165940 100644 --- a/web/projects/setup-wizard/src/app/pages/home/home.page.ts +++ b/web/projects/setup-wizard/src/app/pages/home/home.page.ts @@ -1,9 +1,9 @@ import { Component } from '@angular/core' import { IonicSlides } from '@ionic/angular' +import { ErrorService } from '@start9labs/shared' import { ApiService } from 'src/app/services/api/api.service' -import SwiperCore, { Swiper } from 'swiper' -import { ErrorToastService } from '@start9labs/shared' import { StateService } from 'src/app/services/state.service' +import SwiperCore, { Swiper } from 'swiper' SwiperCore.use([IonicSlides]) @@ -19,7 +19,7 @@ export class HomePage { constructor( private readonly api: ApiService, - private readonly errToastService: ErrorToastService, + private readonly errorService: ErrorService, private readonly stateService: StateService, ) {} @@ -33,7 +33,7 @@ export class HomePage { await this.api.getPubKey() } catch (e: any) { this.error = true - this.errToastService.present(e) + this.errorService.handleError(e) } finally { this.loading = false } diff --git a/web/projects/setup-wizard/src/app/pages/loading/loading.page.ts b/web/projects/setup-wizard/src/app/pages/loading/loading.page.ts index 459be5c7a..2386c09ec 100644 --- a/web/projects/setup-wizard/src/app/pages/loading/loading.page.ts +++ b/web/projects/setup-wizard/src/app/pages/loading/loading.page.ts @@ -1,14 +1,15 @@ import { Component } from '@angular/core' import { NavController } from '@ionic/angular' -import { Pipe, PipeTransform } from '@angular/core' +import { ErrorService } from '@start9labs/shared' +import { T } from '@start9labs/start-sdk' import { - EMPTY, - Observable, catchError, + EMPTY, filter, from, interval, map, + Observable, of, startWith, switchMap, @@ -16,8 +17,6 @@ import { tap, } from 'rxjs' import { ApiService } from 'src/app/services/api/api.service' -import { ErrorToastService } from '@start9labs/shared' -import { T } from '@start9labs/start-sdk' @Component({ selector: 'app-loading', @@ -69,7 +68,7 @@ export class LoadingPage { constructor( private readonly navCtrl: NavController, private readonly api: ApiService, - private readonly errorToastService: ErrorToastService, + private readonly errorService: ErrorService, ) {} private async getStatus(): Promise<{ @@ -96,7 +95,7 @@ export class LoadingPage { return from(this.getStatus()).pipe( filter(Boolean), catchError(e => { - this.errorToastService.present(e) + this.errorService.handleError(e) return of(e) }), take(1), diff --git a/web/projects/setup-wizard/src/app/pages/recover/recover.page.ts b/web/projects/setup-wizard/src/app/pages/recover/recover.page.ts index f9e5adbb1..d47105f24 100644 --- a/web/projects/setup-wizard/src/app/pages/recover/recover.page.ts +++ b/web/projects/setup-wizard/src/app/pages/recover/recover.page.ts @@ -1,8 +1,8 @@ import { Component, Input } from '@angular/core' import { ModalController, NavController } from '@ionic/angular' +import { ErrorService } from '@start9labs/shared' import { CifsModal } from 'src/app/modals/cifs-modal/cifs-modal.page' import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service' -import { ErrorToastService } from '@start9labs/shared' import { StateService } from 'src/app/services/state.service' import { PasswordPage } from '../../modals/password/password.page' @@ -20,7 +20,7 @@ export class RecoverPage { private readonly navCtrl: NavController, private readonly modalCtrl: ModalController, private readonly modalController: ModalController, - private readonly errToastService: ErrorToastService, + private readonly errorService: ErrorService, private readonly stateService: StateService, ) {} @@ -62,7 +62,7 @@ export class RecoverPage { }) }) } catch (e: any) { - this.errToastService.present(e) + this.errorService.handleError(e) } finally { this.loading = false } diff --git a/web/projects/setup-wizard/src/app/pages/success/success.page.ts b/web/projects/setup-wizard/src/app/pages/success/success.page.ts index 71566580a..88262bdf2 100644 --- a/web/projects/setup-wizard/src/app/pages/success/success.page.ts +++ b/web/projects/setup-wizard/src/app/pages/success/success.page.ts @@ -1,6 +1,6 @@ import { DOCUMENT } from '@angular/common' import { Component, ElementRef, Inject, NgZone, ViewChild } from '@angular/core' -import { DownloadHTMLService, ErrorToastService } from '@start9labs/shared' +import { DownloadHTMLService, ErrorService } from '@start9labs/shared' import { ApiService } from 'src/app/services/api/api.service' import { StateService } from 'src/app/services/state.service' @@ -12,7 +12,8 @@ import { StateService } from 'src/app/services/state.service' }) export class SuccessPage { @ViewChild('canvas', { static: true }) - private canvas: ElementRef = {} as ElementRef + private canvas: ElementRef = + {} as ElementRef private ctx: CanvasRenderingContext2D = {} as CanvasRenderingContext2D torAddress?: string @@ -28,7 +29,7 @@ export class SuccessPage { constructor( @Inject(DOCUMENT) private readonly document: Document, - private readonly errCtrl: ErrorToastService, + private readonly errorService: ErrorService, private readonly stateService: StateService, private readonly api: ApiService, private readonly downloadHtml: DownloadHTMLService, @@ -83,7 +84,7 @@ export class SuccessPage { await this.api.exit() } } catch (e: any) { - await this.errCtrl.present(e) + await this.errorService.handleError(e) } } diff --git a/web/projects/setup-wizard/src/app/pages/transfer/transfer.page.ts b/web/projects/setup-wizard/src/app/pages/transfer/transfer.page.ts index 5de21a289..5034a2272 100644 --- a/web/projects/setup-wizard/src/app/pages/transfer/transfer.page.ts +++ b/web/projects/setup-wizard/src/app/pages/transfer/transfer.page.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core' import { AlertController, NavController } from '@ionic/angular' +import { DiskInfo, ErrorService } from '@start9labs/shared' import { ApiService } from 'src/app/services/api/api.service' -import { DiskInfo, ErrorToastService } from '@start9labs/shared' import { StateService } from 'src/app/services/state.service' @Component({ @@ -17,7 +17,7 @@ export class TransferPage { private readonly apiService: ApiService, private readonly navCtrl: NavController, private readonly alertCtrl: AlertController, - private readonly errToastService: ErrorToastService, + private readonly errorService: ErrorService, private readonly stateService: StateService, ) {} @@ -35,7 +35,7 @@ export class TransferPage { try { this.drives = await this.apiService.getDrives() } catch (e: any) { - this.errToastService.present(e) + this.errorService.handleError(e) } finally { this.loading = false } diff --git a/web/projects/shared/assets/img/icon_transparent.png b/web/projects/shared/assets/img/icon_transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..f0aafd15de4c965ad7238b6d89dde4c1124c986f GIT binary patch literal 53685 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

Zb}SFh5?>QaG-qYO`kM}CLNkCD_iBL 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 K9l#MLAz8BRh_ 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 h|8m*Tn!WYe*h^_s|)}D literal 0 HcmV?d00001 diff --git a/web/projects/shared/assets/img/storefront-outline.png b/web/projects/shared/assets/img/storefront-outline.png new file mode 100644 index 0000000000000000000000000000000000000000..aa7bd440493aac3fe53df067f6dc7382e6771c33 GIT binary patch literal 10912 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 Ae*gdg literal 0 HcmV?d00001 diff --git a/web/projects/shared/assets/taiga-ui/icons/tuiIconPaintOutline.svg b/web/projects/shared/assets/taiga-ui/icons/tuiIconPaintOutline.svg new file mode 100644 index 000000000..55450c05c --- /dev/null +++ b/web/projects/shared/assets/taiga-ui/icons/tuiIconPaintOutline.svg @@ -0,0 +1,10 @@ + + + diff --git a/web/projects/shared/src/components/loading/loading.component.scss b/web/projects/shared/src/components/loading/loading.component.scss new file mode 100644 index 000000000..9a7d10100 --- /dev/null +++ b/web/projects/shared/src/components/loading/loading.component.scss @@ -0,0 +1,20 @@ +@import '@taiga-ui/core/styles/taiga-ui-local'; + +:host { + @include shadow(3); + + display: flex; + align-items: center; + max-width: 80%; + margin: auto; + padding: 1.5rem; + background: var(--tui-elevation-01); + border-radius: var(--tui-radius-m); + + --tui-primary: var(--tui-warning-fill); +} + +tui-loader { + flex-shrink: 0; + min-width: 2rem; +} diff --git a/web/projects/shared/src/components/loading/loading.component.ts b/web/projects/shared/src/components/loading/loading.component.ts new file mode 100644 index 000000000..373f013a1 --- /dev/null +++ b/web/projects/shared/src/components/loading/loading.component.ts @@ -0,0 +1,17 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core' +import { + POLYMORPHEUS_CONTEXT, + PolymorpheusContent, +} from '@tinkoff/ng-polymorpheus' + +@Component({ + template: ` + + `, + styleUrls: ['./loading.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class LoadingComponent { + readonly content: PolymorpheusContent = + inject(POLYMORPHEUS_CONTEXT)['content'] +} diff --git a/web/projects/shared/src/components/loading/loading.module.ts b/web/projects/shared/src/components/loading/loading.module.ts new file mode 100644 index 000000000..4a3798041 --- /dev/null +++ b/web/projects/shared/src/components/loading/loading.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core' +import { TuiLoaderModule } from '@taiga-ui/core' +import { tuiAsDialog } from '@taiga-ui/cdk' +import { LoadingComponent } from './loading.component' +import { LoadingService } from './loading.service' + +@NgModule({ + imports: [TuiLoaderModule], + declarations: [LoadingComponent], + exports: [LoadingComponent], + providers: [tuiAsDialog(LoadingService)], +}) +export class LoadingModule {} diff --git a/web/projects/shared/src/components/loading/loading.service.ts b/web/projects/shared/src/components/loading/loading.service.ts new file mode 100644 index 000000000..96ab4301f --- /dev/null +++ b/web/projects/shared/src/components/loading/loading.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@angular/core' +import { AbstractTuiDialogService } from '@taiga-ui/cdk' +import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' +import { LoadingComponent } from './loading.component' + +@Injectable({ providedIn: `root` }) +export class LoadingService extends AbstractTuiDialogService { + protected readonly component = new PolymorpheusComponent(LoadingComponent) + protected readonly defaultOptions = {} +} diff --git a/web/projects/shared/src/components/markdown/markdown.component.ts b/web/projects/shared/src/components/markdown/markdown.component.ts index d560f7537..e3f350e4a 100644 --- a/web/projects/shared/src/components/markdown/markdown.component.ts +++ b/web/projects/shared/src/components/markdown/markdown.component.ts @@ -3,7 +3,7 @@ import { ModalController } from '@ionic/angular' import { defer, isObservable, Observable, of } from 'rxjs' import { catchError, ignoreElements, share } from 'rxjs/operators' -import { getErrorMessage } from '../../services/error-toast.service' +import { getErrorMessage } from '../../services/error.service' @Component({ selector: 'markdown', diff --git a/web/projects/shared/src/public-api.ts b/web/projects/shared/src/public-api.ts index 7b1cd71b8..c83fef860 100644 --- a/web/projects/shared/src/public-api.ts +++ b/web/projects/shared/src/public-api.ts @@ -9,6 +9,9 @@ export * from './components/alert/alert.component' export * from './components/alert/alert.module' export * from './components/alert/alert-button.directive' export * from './components/alert/alert-input.directive' +export * from './components/loading/loading.component' +export * from './components/loading/loading.module' +export * from './components/loading/loading.service' export * from './components/markdown/markdown.component' export * from './components/markdown/markdown.component.module' export * from './components/text-spinner/text-spinner.component' @@ -42,7 +45,7 @@ export * from './pipes/unit-conversion/unit-conversion.pipe' export * from './services/download-html.service' export * from './services/emver.service' -export * from './services/error-toast.service' +export * from './services/error.service' export * from './services/http.service' export * from './themes/dark-theme/dark-theme.component' @@ -63,6 +66,7 @@ export * from './util/base-64' export * from './util/copy-to-clipboard' export * from './util/get-new-entries' export * from './util/get-pkg-id' +export * from './util/invert' export * from './util/misc.util' export * from './util/rpc.util' export * from './util/to-local-iso-string' diff --git a/web/projects/shared/src/services/error-toast.service.ts b/web/projects/shared/src/services/error-toast.service.ts deleted file mode 100644 index fe6607995..000000000 --- a/web/projects/shared/src/services/error-toast.service.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Injectable } from '@angular/core' -import { IonicSafeString, ToastController } from '@ionic/angular' -import { HttpError } from '../classes/http-error' - -@Injectable({ - providedIn: 'root', -}) -export class ErrorToastService { - private toast?: HTMLIonToastElement - - constructor(private readonly toastCtrl: ToastController) {} - - async present(e: HttpError | string, link?: string): Promise { - console.error(e) - - if (this.toast) return - - this.toast = await this.toastCtrl.create({ - header: 'Error', - message: getErrorMessage(e, link), - duration: 0, - position: 'top', - cssClass: 'error-toast', - buttons: [ - { - side: 'end', - icon: 'close', - handler: () => { - this.dismiss() - }, - }, - ], - }) - await this.toast.present() - } - - async dismiss(): Promise { - if (this.toast) { - await this.toast.dismiss() - this.toast = undefined - } - } -} - -export function getErrorMessage( - e: HttpError | string, - link?: string, -): string | IonicSafeString { - let message = '' - - if (typeof e === 'string') { - message = e - } else if (e.code === 0) { - message = - 'Request Error. Your browser blocked the request. This is usually caused by a corrupt browser cache or an overly aggressive ad blocker. Please clear your browser cache and/or adjust your ad blocker and try again' - link = 'https://docs.start9.com/0.3.5.x/support/common-issues#request-error' - } else if (!e.message) { - message = 'Unknown Error' - } else { - message = e.message - } - - if (link) { - return new IonicSafeString( - `${message}

Get Help`, - ) - } - - return message -} diff --git a/web/projects/shared/src/services/error.service.ts b/web/projects/shared/src/services/error.service.ts new file mode 100644 index 000000000..45891e0f4 --- /dev/null +++ b/web/projects/shared/src/services/error.service.ts @@ -0,0 +1,43 @@ +import { ErrorHandler, inject, Injectable } from '@angular/core' +import { TuiAlertService, TuiNotification } from '@taiga-ui/core' +import { HttpError } from '../classes/http-error' + +// TODO: Enable this as ErrorHandler +@Injectable({ + providedIn: 'root', +}) +export class ErrorService extends ErrorHandler { + private readonly alerts = inject(TuiAlertService) + + override handleError(error: HttpError | string, link?: string) { + console.error(error) + + this.alerts + .open(getErrorMessage(error, link), { + label: 'Error', + autoClose: false, + status: TuiNotification.Error, + }) + .subscribe() + } +} + +export function getErrorMessage(e: HttpError | string, link?: string): string { + let message = '' + + if (typeof e === 'string') { + message = e + } else if (e.code === 0) { + message = + 'Request Error. Your browser blocked the request. This is usually caused by a corrupt browser cache or an overly aggressive ad blocker. Please clear your browser cache and/or adjust your ad blocker and try again' + } else if (!e.message) { + message = 'Unknown Error' + link = 'https://docs.start9.com/latest/support/faq' + } else { + message = e.message + } + + return link + ? `${message}

Get Help` + : message +} diff --git a/web/projects/shared/src/util/invert.ts b/web/projects/shared/src/util/invert.ts new file mode 100644 index 000000000..6931b3660 --- /dev/null +++ b/web/projects/shared/src/util/invert.ts @@ -0,0 +1,12 @@ +export function invert< + T extends string | number | symbol, + D extends string | number | symbol, +>(obj: Record): Record { + const result = {} as Record + + for (const key in obj) { + result[obj[key]] = key + } + + return result +} diff --git a/web/projects/ui/src/app/app.module.ts b/web/projects/ui/src/app/app.module.ts index f13b4fbfa..048d81fe0 100644 --- a/web/projects/ui/src/app/app.module.ts +++ b/web/projects/ui/src/app/app.module.ts @@ -14,6 +14,7 @@ import { DarkThemeModule, EnterModule, LightThemeModule, + LoadingModule, MarkdownModule, ResponsiveColModule, SharedPipesModule, @@ -22,7 +23,6 @@ import { import { AppComponent } from './app.component' import { AppRoutingModule } from './app-routing.module' import { OSWelcomePageModule } from './modals/os-welcome/os-welcome.module' -import { GenericInputComponentModule } from './modals/generic-input/generic-input.component.module' import { MarketplaceModule } from './marketplace.module' import { PreloaderModule } from './app/preloader/preloader.module' import { FooterModule } from './app/footer/footer.module' @@ -49,7 +49,7 @@ import { environment } from '../environments/environment' EnterModule, OSWelcomePageModule, MarkdownModule, - GenericInputComponentModule, + LoadingModule, MonacoEditorModule, SharedPipesModule, MarketplaceModule, diff --git a/web/projects/ui/src/app/app.providers.ts b/web/projects/ui/src/app/app.providers.ts index e93c323d0..00ff64690 100644 --- a/web/projects/ui/src/app/app.providers.ts +++ b/web/projects/ui/src/app/app.providers.ts @@ -3,6 +3,7 @@ import { UntypedFormBuilder } from '@angular/forms' import { Router, RouteReuseStrategy } from '@angular/router' import { IonicRouteStrategy, IonNav } from '@ionic/angular' import { RELATIVE_URL, THEME, WorkspaceConfig } from '@start9labs/shared' +import { TUI_ICONS_PATH } from '@taiga-ui/core' import { PatchDB } from 'patch-db-client' import { PATCH_CACHE, @@ -53,6 +54,10 @@ export const APP_PROVIDERS: Provider[] = [ provide: THEME, useExisting: ThemeSwitcherService, }, + { + provide: TUI_ICONS_PATH, + useValue: (name: string) => `/assets/taiga-ui/icons/${name}.svg#${name}`, + }, ] export function appInitializer( diff --git a/web/projects/ui/src/app/app/preloader/preloader.component.html b/web/projects/ui/src/app/app/preloader/preloader.component.html index 94d6d7107..82923250e 100644 --- a/web/projects/ui/src/app/app/preloader/preloader.component.html +++ b/web/projects/ui/src/app/app/preloader/preloader.component.html @@ -8,8 +8,6 @@ - - @@ -19,14 +17,11 @@ - - - - - - - - - load bold font - - @@ -66,10 +53,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +

+
-

a

a

a

diff --git a/web/projects/ui/src/app/app/preloader/preloader.component.ts b/web/projects/ui/src/app/app/preloader/preloader.component.ts index 0b387ac45..f7184008e 100644 --- a/web/projects/ui/src/app/app/preloader/preloader.component.ts +++ b/web/projects/ui/src/app/app/preloader/preloader.component.ts @@ -1,4 +1,10 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' +import { + ActionSheetController, + AlertController, + ModalController, + ToastController, +} from '@ionic/angular' // TODO: Turn into DI token if this is needed someplace else too const ICONS = [ @@ -88,6 +94,26 @@ const ICONS = [ 'wifi', ] +const TAIGA = [ + 'tuiIconPaintOutline', + 'tuiIconTrash', + 'tuiIconTrashOutline', + 'tuiIconChevronDown', + 'tuiIconChevronDownOutline', + 'tuiIconRefreshCcw', + 'tuiIconRefreshCcwOutline', + 'tuiIconEye', + 'tuiIconEyeOutline', + 'tuiIconEyeOff', + 'tuiIconEyeOffOutline', + 'tuiIconPlus', + 'tuiIconMinus', + 'tuiIconCheck', + 'tuiIconClose', + 'tuiIconCalendarLarge', + 'tuiIconHelpCircle', +] + @Component({ selector: 'section[appPreloader]', templateUrl: 'preloader.component.html', @@ -95,4 +121,12 @@ const ICONS = [ }) export class PreloaderComponent { readonly icons = ICONS + readonly taiga = TAIGA + + constructor( + _modals: ModalController, + _alerts: AlertController, + _toasts: ToastController, + _actions: ActionSheetController, + ) {} } diff --git a/web/projects/ui/src/app/app/preloader/preloader.module.ts b/web/projects/ui/src/app/app/preloader/preloader.module.ts index b1496e638..380b70f3a 100644 --- a/web/projects/ui/src/app/app/preloader/preloader.module.ts +++ b/web/projects/ui/src/app/app/preloader/preloader.module.ts @@ -1,11 +1,65 @@ import { CommonModule } from '@angular/common' import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core' import { IonicModule } from '@ionic/angular' +import { + TuiErrorModule, + TuiExpandModule, + TuiLinkModule, + TuiScrollbarModule, + TuiSvgModule, + TuiTooltipModule, +} from '@taiga-ui/core' +import { + TuiButtonModule, + TuiCellModule, + TuiIconModule, +} from '@taiga-ui/experimental' +import { + TuiElasticContainerModule, + TuiInputDateModule, + TuiInputDateTimeModule, + TuiInputFilesModule, + TuiInputModule, + TuiInputNumberModule, + TuiInputTimeModule, + TuiMultiSelectModule, + TuiProgressModule, + TuiRadioListModule, + TuiSelectModule, + TuiTextAreaModule, + TuiToggleModule, +} from '@taiga-ui/kit' import { QrCodeModule } from 'ng-qrcode' import { PreloaderComponent } from './preloader.component' @NgModule({ - imports: [CommonModule, IonicModule, QrCodeModule], + imports: [ + CommonModule, + IonicModule, + QrCodeModule, + TuiTooltipModule, + TuiErrorModule, + TuiInputModule, + TuiSvgModule, + TuiIconModule, + TuiButtonModule, + TuiLinkModule, + TuiInputTimeModule, + TuiInputDateModule, + TuiInputDateTimeModule, + TuiInputFilesModule, + TuiMultiSelectModule, + TuiInputNumberModule, + TuiExpandModule, + TuiSelectModule, + TuiTextAreaModule, + TuiToggleModule, + TuiElasticContainerModule, + TuiCellModule, + TuiProgressModule, + TuiScrollbarModule, + TuiRadioListModule, + ], declarations: [PreloaderComponent], exports: [PreloaderComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA], diff --git a/web/projects/ui/src/app/app/snek/snek.directive.ts b/web/projects/ui/src/app/app/snek/snek.directive.ts index db812ae4a..3e3ad5dc3 100644 --- a/web/projects/ui/src/app/app/snek/snek.directive.ts +++ b/web/projects/ui/src/app/app/snek/snek.directive.ts @@ -1,6 +1,6 @@ import { Directive, HostListener, Input } from '@angular/core' -import { LoadingController, ModalController } from '@ionic/angular' -import { ErrorToastService } from '@start9labs/shared' +import { ModalController } from '@ionic/angular' +import { ErrorService, LoadingService } from '@start9labs/shared' import { SnakePage } from '../../modals/snake/snake.page' import { ApiService } from '../../services/api/embassy-api.service' @@ -13,8 +13,8 @@ export class SnekDirective { constructor( private readonly modalCtrl: ModalController, - private readonly loadingCtrl: LoadingController, - private readonly errToast: ErrorToastService, + private readonly loader: LoadingService, + private readonly errorService: ErrorService, private readonly embassyApi: ApiService, ) {} @@ -30,12 +30,7 @@ export class SnekDirective { modal.onDidDismiss().then(async ({ data }) => { if (data?.highScore <= (this.appSnekHighScore || 0)) return - const loader = await this.loadingCtrl.create({ - message: 'Saving high score...', - backdropDismiss: true, - }) - - await loader.present() + const loader = this.loader.open('Saving high score...').subscribe() try { await this.embassyApi.setDbValue( @@ -43,9 +38,9 @@ export class SnekDirective { data.highScore, ) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - this.loadingCtrl.dismiss() + this.loader.subscribe() } }) diff --git a/web/projects/ui/src/app/components/backup-drives/backup-drives.component.module.ts b/web/projects/ui/src/app/components/backup-drives/backup-drives.component.module.ts index bf7f844e3..60c37d08d 100644 --- a/web/projects/ui/src/app/components/backup-drives/backup-drives.component.module.ts +++ b/web/projects/ui/src/app/components/backup-drives/backup-drives.component.module.ts @@ -10,7 +10,6 @@ import { UnitConversionPipesModule, TextSpinnerComponentModule, } from '@start9labs/shared' -import { GenericFormPageModule } from 'src/app/modals/generic-form/generic-form.module' @NgModule({ declarations: [ @@ -23,7 +22,6 @@ import { GenericFormPageModule } from 'src/app/modals/generic-form/generic-form. IonicModule, UnitConversionPipesModule, TextSpinnerComponentModule, - GenericFormPageModule, ], exports: [ BackupDrivesComponent, diff --git a/web/projects/ui/src/app/components/backup-drives/backup-drives.component.ts b/web/projects/ui/src/app/components/backup-drives/backup-drives.component.ts index fa73b8452..5613ae153 100644 --- a/web/projects/ui/src/app/components/backup-drives/backup-drives.component.ts +++ b/web/projects/ui/src/app/components/backup-drives/backup-drives.component.ts @@ -1,21 +1,18 @@ import { Component, EventEmitter, Input, Output } from '@angular/core' -import { BackupService } from './backup.service' +import { ActionSheetController, AlertController } from '@ionic/angular' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { CB } from '@start9labs/start-sdk' import { CifsBackupTarget, DiskBackupTarget, RR, } from 'src/app/services/api/api.types' -import { - ActionSheetController, - AlertController, - LoadingController, - ModalController, -} from '@ionic/angular' -import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' -import { ConfigSpec } from 'src/app/pkg-config/config-types' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ErrorToastService } from '@start9labs/shared' +import { FormDialogService } from 'src/app/services/form-dialog.service' import { MappedBackupTarget } from 'src/app/types/mapped-backup-target' +import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec' +import { FormComponent } from '../form.component' +import { BackupService } from './backup.service' type BackupType = 'create' | 'restore' @@ -32,13 +29,13 @@ export class BackupDrivesComponent { loadingText = '' constructor( - private readonly loadingCtrl: LoadingController, + private readonly loader: LoadingService, private readonly actionCtrl: ActionSheetController, private readonly alertCtrl: AlertController, - private readonly modalCtrl: ModalController, private readonly embassyApi: ApiService, - private readonly errToast: ErrorToastService, + private readonly errorService: ErrorService, private readonly backupService: BackupService, + private readonly formDialog: FormDialogService, ) {} get loading() { @@ -87,23 +84,19 @@ export class BackupDrivesComponent { } async presentModalAddCifs(): Promise { - const modal = await this.modalCtrl.create({ - component: GenericFormPage, - componentProps: { - title: 'New Network Folder', - spec: CifsSpec, + this.formDialog.open(FormComponent, { + label: 'New Network Folder', + data: { + spec: await configBuilderToSpec(cifsSpec), buttons: [ { - text: 'Connect', - handler: (value: RR.AddBackupTargetReq) => { - return this.addCifs(value) - }, - isSubmit: true, + text: 'Execute', + handler: async (value: RR.AddBackupTargetReq) => + this.addCifs(value), }, ], }, }) - await modal.present() } async presentActionCifs( @@ -151,10 +144,9 @@ export class BackupDrivesComponent { } private async addCifs(value: RR.AddBackupTargetReq): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Testing connectivity to shared folder...', - }) - await loader.present() + const loader = this.loader + .open('Testing connectivity to shared folder...') + .subscribe() try { const res = await this.embassyApi.addBackupTarget(value) @@ -166,10 +158,10 @@ export class BackupDrivesComponent { }) return true } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) return false } finally { - loader.dismiss() + loader.unsubscribe() } } @@ -180,62 +172,57 @@ export class BackupDrivesComponent { ): Promise { const { hostname, path, username } = entry - const modal = await this.modalCtrl.create({ - component: GenericFormPage, - componentProps: { - title: 'Update Shared Folder', - spec: CifsSpec, + this.formDialog.open(FormComponent, { + label: 'Update Network Folder', + data: { + spec: await configBuilderToSpec(cifsSpec), buttons: [ { - text: 'Save', - handler: (value: RR.AddBackupTargetReq) => { - return this.editCifs({ id, ...value }, index) - }, - isSubmit: true, + text: 'Execute', + handler: async (value: RR.AddBackupTargetReq) => + this.editCifs({ id, ...value }, index), }, ], - initialValue: { + value: { hostname, path, username, }, }, }) - await modal.present() } private async editCifs( value: RR.UpdateBackupTargetReq, index: number, - ): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Testing connectivity to shared folder...', - }) - await loader.present() + ): Promise { + const loader = this.loader + .open('Testing connectivity to shared folder...') + .subscribe() try { const res = await this.embassyApi.updateBackupTarget(value) this.backupService.cifs[index].entry = Object.values(res)[0] + + return true } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) + return false } finally { - loader.dismiss() + loader.unsubscribe() } } private async deleteCifs(id: string, index: number): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Removing...', - }) - await loader.present() + const loader = this.loader.open('Removing...').subscribe() try { await this.embassyApi.removeBackupTarget({ id }) this.backupService.cifs.splice(index, 1) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } @@ -274,40 +261,33 @@ export class BackupDrivesStatusComponent { @Input() hasValidBackup!: boolean } -const CifsSpec: ConfigSpec = { - hostname: { - type: 'string', - name: 'Hostname/IP', +const cifsSpec = CB.Config.of({ + hostname: CB.Value.text({ + name: 'Hostname', description: - 'The hostname or IP address of the target device on your Local Area Network.', - placeholder: `e.g. 'MyComputer.local' OR '192.168.1.4'`, - nullable: false, - masked: false, - copyable: false, - }, - path: { - type: 'string', + 'The hostname of your target device on the Local Area Network.', + warning: null, + placeholder: `e.g. 'My Computer' OR 'my-computer.local'`, + required: { default: null }, + patterns: [], + }), + path: CB.Value.text({ name: 'Path', description: `On Windows, this is the fully qualified path to the shared folder, (e.g. /Desktop/my-folder).\n\n On Linux and Mac, this is the literal name of the shared folder (e.g. my-shared-folder).`, placeholder: 'e.g. my-shared-folder or /Desktop/my-folder', - nullable: false, - masked: false, - copyable: false, - }, - username: { - type: 'string', + required: { default: null }, + }), + username: CB.Value.text({ name: 'Username', description: `On Linux, this is the samba username you created when sharing the folder.\n\n On Mac and Windows, this is the username of the user who is sharing the folder.`, - nullable: false, - masked: false, - copyable: false, - }, - password: { - type: 'string', + required: { default: null }, + placeholder: 'My Network Folder', + }), + password: CB.Value.text({ name: 'Password', description: `On Linux, this is the samba password you created when sharing the folder.\n\n On Mac and Windows, this is the password of the user who is sharing the folder.`, - nullable: true, + required: false, masked: true, - copyable: false, - }, -} + placeholder: 'My Network Folder', + }), +}) diff --git a/web/projects/ui/src/app/components/form-object/form-label.component.html b/web/projects/ui/src/app/components/form-object/form-label.component.html deleted file mode 100644 index 69c29f99d..000000000 --- a/web/projects/ui/src/app/components/form-object/form-label.component.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - -{{ data.name }} - - (New) - (New Options) - (Edited) - - * diff --git a/web/projects/ui/src/app/components/form-object/form-object.component.html b/web/projects/ui/src/app/components/form-object/form-object.component.html deleted file mode 100644 index 8bbfc5c78..000000000 --- a/web/projects/ui/src/app/components/form-object/form-object.component.html +++ /dev/null @@ -1,372 +0,0 @@ - -
-
- - - -

- -

- - - - - - - - - - {{ spec.units }} - - -

- - {{ errors | getError: $any(spec)['pattern-description'] }} - -

-
- - - - - - - - {{ spec.name }} - - (New) - - - (Edited) - - - - - - - - - - {{ spec['value-names'][option] }} - - - - - - - - - - - - -
- -
-
-
- - - - - - - - - - - Add - - -

- - {{ errors | getError }} - -

- -
-
- - - - - - - - - - - -
- - - Delete - -
-
-
- -
- - - - - - -

- - {{ errors | getError: $any(spec)['pattern-description'] }} - -

-
-
-
-
-
- - - - -

- -

- - - -

{{ formArr.value | toEnumListDisplay: $any(spec.spec) }}

-
- - - -
-

- - {{ errors | getError }} - -

-
-
-
-
-
diff --git a/web/projects/ui/src/app/components/form-object/form-object.component.module.ts b/web/projects/ui/src/app/components/form-object/form-object.component.module.ts deleted file mode 100644 index 06af388c0..000000000 --- a/web/projects/ui/src/app/components/form-object/form-object.component.module.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { - FormObjectComponent, - FormUnionComponent, - FormLabelComponent, -} from './form-object.component' -import { - GetErrorPipe, - ToWarningTextPipe, - ToElementIdPipe, - GetControlPipe, - ToEnumListDisplayPipe, - ToRangePipe, -} from './form-object.pipes' -import { IonicModule } from '@ionic/angular' -import { FormsModule, ReactiveFormsModule } from '@angular/forms' -import { SharedPipesModule } from '@start9labs/shared' -import { TuiElasticContainerModule } from '@taiga-ui/kit' -import { EnumListPageModule } from 'src/app/modals/enum-list/enum-list.module' -import { TuiExpandModule } from '@taiga-ui/core' - -@NgModule({ - declarations: [ - FormObjectComponent, - FormUnionComponent, - FormLabelComponent, - ToWarningTextPipe, - GetErrorPipe, - ToEnumListDisplayPipe, - ToElementIdPipe, - GetControlPipe, - ToRangePipe, - ], - imports: [ - CommonModule, - IonicModule, - FormsModule, - ReactiveFormsModule, - SharedPipesModule, - EnumListPageModule, - TuiElasticContainerModule, - TuiExpandModule, - ], - exports: [FormObjectComponent, FormLabelComponent], -}) -export class FormObjectComponentModule {} diff --git a/web/projects/ui/src/app/components/form-object/form-object.component.scss b/web/projects/ui/src/app/components/form-object/form-object.component.scss deleted file mode 100644 index a98a0ff72..000000000 --- a/web/projects/ui/src/app/components/form-object/form-object.component.scss +++ /dev/null @@ -1,42 +0,0 @@ -.slot-start { - display: inline-block; - vertical-align: middle; - --padding-start: 0; - --padding-end: 7px; -} - -.error-border { - border-color: var(--ion-color-danger-shade); - --border-color: var(--ion-color-danger-shade); -} - -.redacted { - font-family: 'Redacted' -} - -ion-input { - font-family: 'Courier New'; - font-weight: bold; - --placeholder-font-weight: 400; -} - -ion-item-divider { - text-transform: unset; - --padding-top: 18px; - --padding-start: 0; - border-bottom: 1px solid var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-150, rgba(0, 0, 0, 0.13)))) -} - -.nested-wrapper { - padding: 0 0 16px 24px; -} - -.error-message { - margin-top: 2px; - font-size: small; - color: var(--ion-color-danger); -} - -.indent { - margin-left: 24px; -} \ No newline at end of file diff --git a/web/projects/ui/src/app/components/form-object/form-object.component.ts b/web/projects/ui/src/app/components/form-object/form-object.component.ts deleted file mode 100644 index fcc6a0c61..000000000 --- a/web/projects/ui/src/app/components/form-object/form-object.component.ts +++ /dev/null @@ -1,425 +0,0 @@ -import { - Component, - Input, - Output, - EventEmitter, - ChangeDetectionStrategy, - Inject, - inject, - SimpleChanges, -} from '@angular/core' -import { FormArray, UntypedFormArray, UntypedFormGroup } from '@angular/forms' -import { AlertButton, AlertController, ModalController } from '@ionic/angular' -import { - ConfigSpec, - ListValueSpecOf, - ValueSpec, - ValueSpecBoolean, - ValueSpecEnum, - ValueSpecList, - ValueSpecListOf, - ValueSpecUnion, -} from 'src/app/pkg-config/config-types' -import { FormService } from 'src/app/services/form.service' -import { EnumListPage } from 'src/app/modals/enum-list/enum-list.page' -import { THEME, pauseFor } from '@start9labs/shared' -import { v4 } from 'uuid' -import { DOCUMENT } from '@angular/common' - -const Mustache = require('mustache') - -interface Config { - [key: string]: any -} -@Component({ - selector: 'form-object', - templateUrl: './form-object.component.html', - styleUrls: ['./form-object.component.scss'], -}) -export class FormObjectComponent { - @Input() objectSpec!: ConfigSpec - @Input() formGroup!: UntypedFormGroup - @Input() current?: Config - @Input() original?: Config - @Output() onInputChange = new EventEmitter() - @Output() hasNewOptions = new EventEmitter() - warningAck: { [key: string]: boolean } = {} - unmasked: { [key: string]: boolean } = {} - objectDisplay: { - [key: string]: { expanded: boolean; hasNewOptions: boolean } - } = {} - objectListDisplay: { - [key: string]: { expanded: boolean; displayAs: string }[] - } = {} - objectId = v4() - - readonly theme$ = inject(THEME) - - constructor( - private readonly alertCtrl: AlertController, - private readonly modalCtrl: ModalController, - private readonly formService: FormService, - @Inject(DOCUMENT) private readonly document: Document, - ) {} - - ngOnInit() { - this.setDisplays() - - // setTimeout hack to avoid ExpressionChangedAfterItHasBeenCheckedError - setTimeout(() => { - if ( - this.original && - Object.keys(this.current || {}).some( - key => this.original![key] === undefined, - ) - ) - this.hasNewOptions.emit() - }) - } - - ngOnChanges(changes: SimpleChanges) { - const specChanges = changes['objectSpec'] - - if (!specChanges) return - - if ( - !specChanges.firstChange && - Object.keys({ - ...specChanges.previousValue, - ...specChanges.currentValue, - }).length !== Object.keys(specChanges.previousValue).length - ) { - this.setDisplays() - } - } - - private setDisplays() { - Object.keys(this.objectSpec).forEach(key => { - const spec = this.objectSpec[key] - - if (spec.type === 'list' && ['object', 'union'].includes(spec.subtype)) { - this.objectListDisplay[key] = [] - this.formGroup.get(key)?.value.forEach((obj: any, index: number) => { - const displayAs = (spec.spec as ListValueSpecOf<'object'>)[ - 'display-as' - ] - this.objectListDisplay[key][index] = { - expanded: false, - displayAs: displayAs - ? (Mustache as any).render(displayAs, obj) - : '', - } - }) - } else if (spec.type === 'object') { - this.objectDisplay[key] = { - expanded: false, - hasNewOptions: false, - } - } - }) - } - - addListItemWrapper( - key: string, - spec: T extends ValueSpecUnion ? never : T, - ) { - this.presentAlertChangeWarning(key, spec, () => this.addListItem(key)) - } - - toggleExpandObject(key: string) { - this.objectDisplay[key].expanded = !this.objectDisplay[key].expanded - } - - toggleExpandListObject(key: string, i: number) { - this.objectListDisplay[key][i].expanded = - !this.objectListDisplay[key][i].expanded - } - - updateLabel(key: string, i: number, displayAs: string) { - this.objectListDisplay[key][i].displayAs = displayAs - ? Mustache.render(displayAs, this.formGroup.get(key)?.value[i]) - : '' - } - - handleInputChange() { - this.onInputChange.emit() - } - - setHasNew(key: string) { - this.hasNewOptions.emit() - setTimeout(() => { - this.objectDisplay[key].hasNewOptions = true - }, 100) - } - - handleBooleanChange(key: string, spec: ValueSpecBoolean) { - if (spec.warning) { - const current = this.formGroup.get(key)?.value - const cancelFn = () => this.formGroup.get(key)?.setValue(!current) - this.presentAlertChangeWarning(key, spec, undefined, cancelFn) - } - } - - async presentModalEnumList( - key: string, - spec: ValueSpecListOf<'enum'>, - current: string[], - ) { - const modal = await this.modalCtrl.create({ - componentProps: { - key, - spec, - current, - }, - component: EnumListPage, - }) - - modal.onWillDismiss().then(({ data }) => { - if (!data) return - this.updateEnumList(key, current, data) - }) - - await modal.present() - } - - async presentAlertChangeWarning( - key: string, - spec: T extends ValueSpecUnion ? never : T, - okFn?: Function, - cancelFn?: Function, - ) { - if (!spec.warning || this.warningAck[key]) return okFn ? okFn() : null - this.warningAck[key] = true - - const buttons: AlertButton[] = [ - { - text: 'Ok', - handler: () => { - if (okFn) okFn() - }, - cssClass: 'enter-click', - }, - ] - - if (okFn || cancelFn) { - buttons.unshift({ - text: 'Cancel', - handler: () => { - if (cancelFn) cancelFn() - }, - }) - } - - const alert = await this.alertCtrl.create({ - header: 'Warning', - subHeader: `Editing ${spec.name} has consequences:`, - message: spec.warning, - buttons, - }) - await alert.present() - } - - async presentAlertDelete(key: string, index: number) { - const alert = await this.alertCtrl.create({ - header: 'Confirm', - message: 'Are you sure you want to delete this entry?', - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Delete', - handler: () => { - this.deleteListItem(key, index) - }, - cssClass: 'enter-click', - }, - ], - }) - await alert.present() - } - - async presentAlertBoolEnumDescription( - event: Event, - spec: ValueSpecBoolean | ValueSpecEnum, - ) { - event.stopPropagation() - const { name, description } = spec - - const alert = await this.alertCtrl.create({ - header: name, - message: description, - buttons: [ - { - text: 'OK', - cssClass: 'enter-click', - }, - ], - }) - await alert.present() - } - - private addListItem(key: string): void { - const arr = this.formGroup.get(key) as UntypedFormArray - const listSpec = this.objectSpec[key] as ValueSpecList - const newItem = this.formService.getListItem(listSpec, undefined)! - - const index = arr.length - arr.insert(index, newItem) - - if (['object', 'union'].includes(listSpec.subtype)) { - const displayAs = (listSpec.spec as ListValueSpecOf<'object'>)[ - 'display-as' - ] - this.objectListDisplay[key].push({ - expanded: false, - displayAs: displayAs ? Mustache.render(displayAs, newItem.value) : '', - }) - } - - setTimeout(() => { - const element = this.document.getElementById( - getElementId(this.objectId, key, index), - ) - element?.parentElement?.scrollIntoView({ behavior: 'smooth' }) - - if (['object', 'union'].includes(listSpec.subtype)) { - pauseFor(250).then(() => this.toggleExpandListObject(key, index)) - } - }, 100) - - arr.markAsDirty() - } - - private deleteListItem(key: string, index: number, markDirty = true): void { - // if (this.objectListDisplay[key]) - // this.objectListDisplay[key][index].height = '0px' - const arr = this.formGroup.get(key) as UntypedFormArray - if (markDirty) arr.markAsDirty() - pauseFor(250).then(() => { - if (this.objectListDisplay[key]) - this.objectListDisplay[key].splice(index, 1) - arr.removeAt(index) - }) - } - - private updateEnumList(key: string, current: string[], updated: string[]) { - const arr = this.formGroup.get(key) as FormArray - - for (let i = current.length - 1; i >= 0; i--) { - if (!updated.includes(current[i])) { - arr.removeAt(i) - } - } - - const listSpec = this.objectSpec[key] as ValueSpecList - - updated.forEach(val => { - if (!current.includes(val)) { - const newItem = this.formService.getListItem(listSpec, val)! - arr.insert(arr.length, newItem) - } - }) - - arr.markAsDirty() - } - - asIsOrder() { - return 0 - } -} - -@Component({ - selector: 'form-union', - templateUrl: './form-union.component.html', - styleUrls: ['./form-object.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class FormUnionComponent { - @Input() formGroup!: UntypedFormGroup - @Input() spec!: ValueSpecUnion - @Input() current?: Config - @Input() original?: Config - - get unionValue() { - return this.formGroup.get(this.spec.tag.id)?.value - } - - get isNew() { - return !this.original - } - - get hasNewOptions() { - const tagId = this.spec.tag.id - return ( - this.original?.[tagId] === this.current?.[tagId] && - !!Object.keys(this.current || {}).find( - key => this.original![key] === undefined, - ) - ) - } - - objectId = v4() - - constructor(private readonly formService: FormService) {} - - updateUnion(e: any): void { - const tagId = this.spec.tag.id - - Object.keys(this.formGroup.controls).forEach(control => { - if (control === tagId) return - this.formGroup.removeControl(control) - }) - - const unionGroup = this.formService.getUnionObject( - this.spec as ValueSpecUnion, - e.detail.value, - ) - - Object.keys(unionGroup.controls).forEach(control => { - if (control === tagId) return - this.formGroup.addControl(control, unionGroup.controls[control]) - }) - } -} - -@Component({ - selector: 'form-label', - templateUrl: './form-label.component.html', - styleUrls: ['./form-object.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class FormLabelComponent { - @Input() data!: { - name: string - new: boolean - edited: boolean - description?: string - required?: boolean - newOptions?: boolean - } - - constructor(private readonly alertCtrl: AlertController) {} - - async presentAlertDescription(event: Event) { - event.stopPropagation() - const { name, description } = this.data - - const alert = await this.alertCtrl.create({ - header: name, - message: description, - buttons: [ - { - text: 'OK', - cssClass: 'enter-click', - }, - ], - }) - await alert.present() - } -} - -export function getElementId(objectId: string, key: string, index = 0): string { - return `${key}-${index}-${objectId}` -} diff --git a/web/projects/ui/src/app/components/form-object/form-object.pipes.ts b/web/projects/ui/src/app/components/form-object/form-object.pipes.ts deleted file mode 100644 index 1dc5a18f2..000000000 --- a/web/projects/ui/src/app/components/form-object/form-object.pipes.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core' -import { - AbstractControl, - FormGroup, - UntypedFormArray, - ValidationErrors, -} from '@angular/forms' -import { IonicSafeString } from '@ionic/angular' -import { ListValueSpecOf } from 'src/app/pkg-config/config-types' -import { Range } from 'src/app/pkg-config/config-utilities' -import { getElementId } from './form-object.component' - -@Pipe({ - name: 'getError', -}) -export class GetErrorPipe implements PipeTransform { - transform(errors: ValidationErrors, patternDesc?: string): string { - if (errors['required']) { - return 'Required' - } else if (errors['pattern']) { - return patternDesc || 'Invalid pattern' - } else if (errors['notNumber']) { - return 'Must be a number' - } else if (errors['numberNotInteger']) { - return 'Must be an integer' - } else if (errors['numberNotInRange']) { - return errors['numberNotInRange'].value - } else if (errors['listNotUnique']) { - return errors['listNotUnique'].value - } else if (errors['listNotInRange']) { - return errors['listNotInRange'].value - } else if (errors['listItemIssue']) { - return errors['listItemIssue'].value - } else { - return 'Unknown error' - } - } -} - -@Pipe({ - name: 'toEnumListDisplay', -}) -export class ToEnumListDisplayPipe implements PipeTransform { - transform(arr: string[], spec: ListValueSpecOf<'enum'>): string { - return arr.map((v: string) => spec['value-names'][v]).join(', ') - } -} - -@Pipe({ - name: 'toWarningText', -}) -export class ToWarningTextPipe implements PipeTransform { - transform(text?: string): IonicSafeString | string { - return text - ? new IonicSafeString(`${text}`) - : '' - } -} - -@Pipe({ - name: 'toRange', -}) -export class ToRangePipe implements PipeTransform { - transform(range: string): Range { - return Range.from(range) - } -} - -@Pipe({ - name: 'toElementId', -}) -export class ToElementIdPipe implements PipeTransform { - transform(objectId: string, key: string, index = 0): string { - return getElementId(objectId, key, index) - } -} - -@Pipe({ - name: 'getControl', -}) -export class GetControlPipe implements PipeTransform { - transform( - formGroup: FormGroup, - key: string, - index?: number, - ): AbstractControl { - const abstractControl = formGroup.get(key)! - if (index !== undefined) - return (abstractControl as UntypedFormArray).at(index) - return abstractControl - } -} diff --git a/web/projects/ui/src/app/components/form-object/form-union.component.html b/web/projects/ui/src/app/components/form-object/form-union.component.html deleted file mode 100644 index ed9fc31be..000000000 --- a/web/projects/ui/src/app/components/form-object/form-union.component.html +++ /dev/null @@ -1,42 +0,0 @@ -
- - - - - - - {{ spec.tag['variant-names'][option.key] }} - - - - - - - -
diff --git a/web/projects/ui/src/app/components/form.component.ts b/web/projects/ui/src/app/components/form.component.ts new file mode 100644 index 000000000..830e8de1e --- /dev/null +++ b/web/projects/ui/src/app/components/form.component.ts @@ -0,0 +1,167 @@ +import { CommonModule } from '@angular/common' +import { + ChangeDetectionStrategy, + Component, + inject, + Input, + OnInit, +} from '@angular/core' +import { FormGroup, ReactiveFormsModule } from '@angular/forms' +import { RouterModule } from '@angular/router' +import { CT } from '@start9labs/start-sdk' + +import { + tuiMarkControlAsTouchedAndValidate, + TuiValueChangesModule, +} from '@taiga-ui/cdk' +import { TuiDialogContext, TuiModeModule } from '@taiga-ui/core' +import { TuiButtonModule } from '@taiga-ui/experimental' +import { TuiDialogFormService } from '@taiga-ui/kit' +import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus' +import { compare, Operation } from 'fast-json-patch' +import { FormModule } from 'src/app/components/form/form.module' +import { InvalidService } from 'src/app/components/form/invalid.service' +import { FormService } from 'src/app/services/form.service' + +export interface ActionButton { + text: string + handler?: (value: T) => Promise | void + link?: string +} + +export interface FormContext { + spec: CT.InputSpec + buttons: ActionButton[] + value?: T + patch?: Operation[] +} + +@Component({ + standalone: true, + selector: 'app-form', + template: ` +
+ + +
+ `, + styles: [ + ` + footer { + position: sticky; + bottom: 0; + z-index: 10; + display: flex; + justify-content: flex-end; + padding: 1rem 0; + margin: 1rem 0 -1rem; + gap: 1rem; + background: var(--tui-elevation-01); + border-top: 1px solid var(--tui-base-02); + } + `, + ], + imports: [ + CommonModule, + ReactiveFormsModule, + RouterModule, + TuiValueChangesModule, + TuiButtonModule, + TuiModeModule, + FormModule, + ], + providers: [InvalidService], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FormComponent> implements OnInit { + private readonly dialogFormService = inject(TuiDialogFormService) + private readonly formService = inject(FormService) + private readonly invalidService = inject(InvalidService) + private readonly context = inject>>( + POLYMORPHEUS_CONTEXT, + { optional: true }, + ) + + @Input() spec = this.context?.data.spec || {} + @Input() buttons = this.context?.data.buttons || [] + @Input() patch = this.context?.data.patch || [] + @Input() value?: T = this.context?.data.value + + form = new FormGroup({}) + + ngOnInit() { + this.dialogFormService.markAsPristine() + this.form = this.formService.createForm(this.spec, this.value) + this.process(this.patch) + } + + onReset() { + const { value } = this.form + + this.form = this.formService.createForm(this.spec) + this.process(compare(this.form.value, value)) + tuiMarkControlAsTouchedAndValidate(this.form) + this.markAsDirty() + } + + async onClick(handler: Required>['handler']) { + tuiMarkControlAsTouchedAndValidate(this.form) + this.invalidService.scrollIntoView() + + if (this.form.valid && (await handler(this.form.value as T))) { + this.close() + } + } + + markAsDirty() { + this.dialogFormService.markAsDirty() + } + + close() { + this.context?.$implicit.complete() + } + + private process(patch: Operation[]) { + patch.forEach(({ op, path }) => { + const control = this.form.get(path.substring(1).split('/')) + + if (!control || !control.parent) return + + if (op !== 'remove') { + control.markAsDirty() + control.markAsTouched() + } + + control.parent.markAsDirty() + control.parent.markAsTouched() + }) + } +} diff --git a/web/projects/ui/src/app/components/form/control.directive.ts b/web/projects/ui/src/app/components/form/control.directive.ts new file mode 100644 index 000000000..327eb8255 --- /dev/null +++ b/web/projects/ui/src/app/components/form/control.directive.ts @@ -0,0 +1,30 @@ +import { Directive, ElementRef, inject, OnDestroy, OnInit } from '@angular/core' +import { ControlContainer, NgControl } from '@angular/forms' +import { InvalidService } from './invalid.service' + +@Directive({ + selector: 'form-control, form-array, form-object', +}) +export class ControlDirective implements OnInit, OnDestroy { + private readonly invalidService = inject(InvalidService, { optional: true }) + private readonly element: ElementRef = inject(ElementRef) + private readonly control = + inject(NgControl, { optional: true }) || + inject(ControlContainer, { optional: true }) + + get invalid(): boolean { + return !!this.control?.invalid + } + + scrollIntoView() { + this.element.nativeElement.scrollIntoView({ behavior: 'smooth' }) + } + + ngOnInit() { + this.invalidService?.add(this) + } + + ngOnDestroy() { + this.invalidService?.remove(this) + } +} diff --git a/web/projects/ui/src/app/components/form/control.ts b/web/projects/ui/src/app/components/form/control.ts new file mode 100644 index 000000000..476826194 --- /dev/null +++ b/web/projects/ui/src/app/components/form/control.ts @@ -0,0 +1,35 @@ +import { inject } from '@angular/core' +import { FormControlComponent } from './form-control/form-control.component' +import { CT } from '@start9labs/start-sdk' + +export abstract class Control { + private readonly control: FormControlComponent = + inject(FormControlComponent) + + get invalid(): boolean { + return this.control.touched && this.control.invalid + } + + get spec(): Spec { + return this.control.spec + } + + // TODO: Properly handle already set immutable value + get readOnly(): boolean { + return ( + !!this.value && !!this.control.control?.pristine && this.control.immutable + ) + } + + get value(): Value | null { + return this.control.value + } + + set value(value: Value | null) { + this.control.onInput(value) + } + + onFocus(focused: boolean) { + this.control.onFocus(focused) + } +} diff --git a/web/projects/ui/src/app/components/form/form-array/form-array.component.html b/web/projects/ui/src/app/components/form/form-array/form-array.component.html new file mode 100644 index 000000000..76c67f837 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-array/form-array.component.html @@ -0,0 +1,58 @@ +
+ {{ spec.name }} + + +
+ + + + + {{ item.value | mustache: $any(spec.spec).displayAs }} + + + + + + + + + diff --git a/web/projects/ui/src/app/components/form/form-array/form-array.component.scss b/web/projects/ui/src/app/components/form/form-array/form-array.component.scss new file mode 100644 index 000000000..9b6415ff7 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-array/form-array.component.scss @@ -0,0 +1,50 @@ +@import '@taiga-ui/core/styles/taiga-ui-local'; + +:host { + display: block; + margin: 2rem 0; +} + +.label { + display: flex; + font-size: 1.25rem; + font-weight: bold; +} + +.add { + font-size: 1rem; + padding: 0 1rem; + margin-left: auto; +} + +.object { + display: block; + position: relative; + + &_open::after, + &:last-child::after { + opacity: 0; + } + + &:after { + @include transition(opacity); + + content: ''; + position: absolute; + bottom: -0.5rem; + height: 1px; + left: 3rem; + right: 1rem; + background: var(--tui-clear); + } +} + +.remove { + margin-left: auto; + pointer-events: auto; +} + +.control { + display: block; + margin: 0.5rem 0; +} diff --git a/web/projects/ui/src/app/components/form/form-array/form-array.component.ts b/web/projects/ui/src/app/components/form/form-array/form-array.component.ts new file mode 100644 index 000000000..11495d510 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-array/form-array.component.ts @@ -0,0 +1,91 @@ +import { Component, HostBinding, inject, Input } from '@angular/core' +import { AbstractControl, FormArrayName } from '@angular/forms' +import { TUI_PARENT_ANIMATION, TuiDestroyService } from '@taiga-ui/cdk' +import { + TUI_ANIMATION_OPTIONS, + TuiDialogService, + tuiFadeIn, + tuiHeightCollapse, +} from '@taiga-ui/core' +import { TUI_PROMPT } from '@taiga-ui/kit' +import { CT } from '@start9labs/start-sdk' +import { filter, takeUntil } from 'rxjs' +import { FormService } from 'src/app/services/form.service' +import { ERRORS } from '../form-group/form-group.component' + +@Component({ + selector: 'form-array', + templateUrl: './form-array.component.html', + styleUrls: ['./form-array.component.scss'], + animations: [tuiFadeIn, tuiHeightCollapse, TUI_PARENT_ANIMATION], + providers: [TuiDestroyService], +}) +export class FormArrayComponent { + @Input() + spec!: CT.ValueSpecList + + @HostBinding('@tuiParentAnimation') + readonly animation = { value: '', ...inject(TUI_ANIMATION_OPTIONS) } + readonly order = ERRORS + readonly array = inject(FormArrayName) + readonly open = new Map() + + private warned = false + private readonly formService = inject(FormService) + private readonly dialogs = inject(TuiDialogService) + private readonly destroy$ = inject(TuiDestroyService) + + get canAdd(): boolean { + return ( + !this.spec.disabled && + (!this.spec.maxLength || + this.spec.maxLength >= this.array.control.controls.length) + ) + } + + add() { + if (!this.warned && this.spec.warning) { + this.dialogs + .open(TUI_PROMPT, { + label: 'Warning', + size: 's', + data: { content: this.spec.warning, yes: 'Ok', no: 'Cancel' }, + }) + .pipe(filter(Boolean), takeUntil(this.destroy$)) + .subscribe(() => { + this.addItem() + }) + } else { + this.addItem() + } + + this.warned = true + } + + removeAt(index: number) { + this.dialogs + .open(TUI_PROMPT, { + label: 'Confirm', + size: 's', + data: { + content: 'Are you sure you want to delete this entry?', + yes: 'Delete', + no: 'Cancel', + }, + }) + .pipe(filter(Boolean), takeUntil(this.destroy$)) + .subscribe(() => { + this.removeItem(index) + }) + } + + private removeItem(index: number) { + this.open.delete(this.array.control.at(index)) + this.array.control.removeAt(index) + } + + private addItem() { + this.array.control.insert(0, this.formService.getListItem(this.spec)) + this.open.set(this.array.control.at(0), true) + } +} diff --git a/web/projects/ui/src/app/components/form/form-color/form-color.component.html b/web/projects/ui/src/app/components/form/form-color/form-color.component.html new file mode 100644 index 000000000..19cf4051d --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-color/form-color.component.html @@ -0,0 +1,31 @@ + + {{ spec.name }} + * + + +
+ + +
+
diff --git a/web/projects/ui/src/app/components/form/form-color/form-color.component.scss b/web/projects/ui/src/app/components/form/form-color/form-color.component.scss new file mode 100644 index 000000000..49496946e --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-color/form-color.component.scss @@ -0,0 +1,33 @@ +@import '@taiga-ui/core/styles/taiga-ui-local'; + +.wrapper { + position: relative; + width: 1.5rem; + height: 1.5rem; + pointer-events: auto; + + &::after { + content: ''; + position: absolute; + height: 0.3rem; + width: 1.4rem; + bottom: 0.125rem; + background: currentColor; + border-radius: 0.125rem; + pointer-events: none; + } +} + +.color { + @include fullsize(); + opacity: 0; +} + +.icon { + @include fullsize(); + pointer-events: none; + + input:hover + & { + opacity: 1; + } +} diff --git a/web/projects/ui/src/app/components/form/form-color/form-color.component.ts b/web/projects/ui/src/app/components/form/form-color/form-color.component.ts new file mode 100644 index 000000000..32a7c1c04 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-color/form-color.component.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core' +import { CT } from '@start9labs/start-sdk' +import { Control } from '../control' +import { MaskitoOptions } from '@maskito/core' + +@Component({ + selector: 'form-color', + templateUrl: './form-color.component.html', + styleUrls: ['./form-color.component.scss'], +}) +export class FormColorComponent extends Control { + readonly mask: MaskitoOptions = { + mask: ['#', ...Array(6).fill(/[0-9a-f]/i)], + } +} diff --git a/web/projects/ui/src/app/components/form/form-control/form-control.component.html b/web/projects/ui/src/app/components/form/form-control/form-control.component.html new file mode 100644 index 000000000..614aae05c --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-control/form-control.component.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + {{ spec.warning }} +

This value cannot be changed once set!

+
+ + +
+
diff --git a/web/projects/ui/src/app/components/form/form-control/form-control.component.scss b/web/projects/ui/src/app/components/form/form-control/form-control.component.scss new file mode 100644 index 000000000..844651118 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-control/form-control.component.scss @@ -0,0 +1,11 @@ +:host { + display: block; +} + +.buttons { + margin-top: 0.5rem; + + :first-child { + margin-right: 0.5rem; + } +} diff --git a/web/projects/ui/src/app/components/form/form-control/form-control.component.ts b/web/projects/ui/src/app/components/form/form-control/form-control.component.ts new file mode 100644 index 000000000..ec49bd084 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-control/form-control.component.ts @@ -0,0 +1,71 @@ +import { + ChangeDetectionStrategy, + Component, + inject, + Input, + TemplateRef, + ViewChild, +} from '@angular/core' +import { AbstractTuiNullableControl } from '@taiga-ui/cdk' +import { + TuiAlertService, + TuiDialogContext, + TuiNotification, +} from '@taiga-ui/core' +import { filter, takeUntil } from 'rxjs' +import { CT } from '@start9labs/start-sdk' +import { ERRORS } from '../form-group/form-group.component' +import { FORM_CONTROL_PROVIDERS } from './form-control.providers' + +@Component({ + selector: 'form-control', + templateUrl: './form-control.component.html', + styleUrls: ['./form-control.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: FORM_CONTROL_PROVIDERS, +}) +export class FormControlComponent< + T extends CT.ValueSpec, + V, +> extends AbstractTuiNullableControl { + @Input() + spec!: T + + @ViewChild('warning') + warning?: TemplateRef> + + warned = false + focused = false + readonly order = ERRORS + private readonly alerts = inject(TuiAlertService) + + get immutable(): boolean { + return 'immutable' in this.spec && this.spec.immutable + } + + onFocus(focused: boolean) { + this.focused = focused + this.updateFocused(focused) + } + + onInput(value: V | null) { + const previous = this.value + + if (!this.warned && this.warning) { + this.alerts + .open(this.warning, { + label: 'Warning', + status: TuiNotification.Warning, + hasCloseButton: false, + autoClose: false, + }) + .pipe(filter(Boolean), takeUntil(this.destroy$)) + .subscribe(() => { + this.value = previous + }) + } + + this.warned = true + this.value = value === '' ? null : value + } +} diff --git a/web/projects/ui/src/app/components/form/form-control/form-control.providers.ts b/web/projects/ui/src/app/components/form/form-control/form-control.providers.ts new file mode 100644 index 000000000..f065f86cb --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-control/form-control.providers.ts @@ -0,0 +1,25 @@ +import { forwardRef, Provider } from '@angular/core' +import { TUI_VALIDATION_ERRORS } from '@taiga-ui/kit' +import { CT } from '@start9labs/start-sdk' +import { FormControlComponent } from './form-control.component' + +interface ValidatorsPatternError { + actualValue: string + requiredPattern: string | RegExp +} + +export const FORM_CONTROL_PROVIDERS: Provider[] = [ + { + provide: TUI_VALIDATION_ERRORS, + deps: [forwardRef(() => FormControlComponent)], + useFactory: (control: FormControlComponent) => ({ + required: 'Required', + pattern: ({ requiredPattern }: ValidatorsPatternError) => + ('patterns' in control.spec && + control.spec.patterns.find( + ({ regex }) => String(regex) === String(requiredPattern), + )?.description) || + 'Invalid format', + }), + }, +] diff --git a/web/projects/ui/src/app/components/form/form-datetime/form-datetime.component.html b/web/projects/ui/src/app/components/form/form-datetime/form-datetime.component.html new file mode 100644 index 000000000..05ffa69f3 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-datetime/form-datetime.component.html @@ -0,0 +1,43 @@ + + + {{ spec.name }} + * + + + {{ spec.name }} + * + + + {{ spec.name }} + * + + diff --git a/web/projects/ui/src/app/components/form/form-datetime/form-datetime.component.ts b/web/projects/ui/src/app/components/form/form-datetime/form-datetime.component.ts new file mode 100644 index 000000000..e09b22d24 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-datetime/form-datetime.component.ts @@ -0,0 +1,36 @@ +import { Component } from '@angular/core' +import { + TUI_FIRST_DAY, + TUI_LAST_DAY, + TuiDay, + tuiPure, + TuiTime, +} from '@taiga-ui/cdk' +import { CT } from '@start9labs/start-sdk' +import { Control } from '../control' + +@Component({ + selector: 'form-datetime', + templateUrl: './form-datetime.component.html', +}) +export class FormDatetimeComponent extends Control< + CT.ValueSpecDatetime, + string +> { + readonly min = TUI_FIRST_DAY + readonly max = TUI_LAST_DAY + + @tuiPure + getTime(value: string | null) { + return value ? TuiTime.fromString(value) : null + } + + getLimit(limit: string): [TuiDay, TuiTime] { + return [ + TuiDay.jsonParse(limit.slice(0, 10)), + limit.length === 10 + ? new TuiTime(0, 0) + : TuiTime.fromString(limit.slice(-5)), + ] + } +} diff --git a/web/projects/ui/src/app/components/form/form-file/form-file.component.html b/web/projects/ui/src/app/components/form/form-file/form-file.component.html new file mode 100644 index 000000000..9e9c16613 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-file/form-file.component.html @@ -0,0 +1,31 @@ + + + +
+
+ {{ spec.name }} + * + +
+ + + Click or drop file here + +
+
Drop file here
+
+
diff --git a/web/projects/ui/src/app/components/form/form-file/form-file.component.scss b/web/projects/ui/src/app/components/form/form-file/form-file.component.scss new file mode 100644 index 000000000..2e314972d --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-file/form-file.component.scss @@ -0,0 +1,46 @@ +@import '@taiga-ui/core/styles/taiga-ui-local'; + +.template { + @include transition(opacity); + + width: 100%; + display: flex; + align-items: center; + padding: 0 0.5rem; + font: var(--tui-font-text-m); + font-weight: bold; + + &_hidden { + opacity: 0; + } +} + +.drop { + @include fullsize(); + @include transition(opacity); + display: flex; + align-items: center; + justify-content: space-around; + + &_hidden { + opacity: 0; + } +} + +.label { + display: flex; + align-items: center; + max-width: 50%; +} + +small { + max-width: 50%; + font-weight: normal; + color: var(--tui-text-02); + margin-left: auto; +} + +tui-tag { + z-index: 1; + margin: -0.25rem -0.25rem -0.25rem auto; +} diff --git a/web/projects/ui/src/app/components/form/form-file/form-file.component.ts b/web/projects/ui/src/app/components/form/form-file/form-file.component.ts new file mode 100644 index 000000000..52d340fa6 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-file/form-file.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core' +import { TuiFileLike } from '@taiga-ui/kit' +import { CT } from '@start9labs/start-sdk' +import { Control } from '../control' + +@Component({ + selector: 'form-file', + templateUrl: './form-file.component.html', + styleUrls: ['./form-file.component.scss'], +}) +export class FormFileComponent extends Control {} diff --git a/web/projects/ui/src/app/components/form/form-group/form-group.component.html b/web/projects/ui/src/app/components/form/form-group/form-group.component.html new file mode 100644 index 000000000..d3c769b98 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-group/form-group.component.html @@ -0,0 +1,30 @@ + + + + + + diff --git a/web/projects/ui/src/app/components/form/form-group/form-group.component.scss b/web/projects/ui/src/app/components/form/form-group/form-group.component.scss new file mode 100644 index 000000000..ce5665fc0 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-group/form-group.component.scss @@ -0,0 +1,35 @@ +form-group .g-form-control:not(:first-child) { + margin-top: 1rem; +} + +form-group .g-form-group { + position: relative; + padding-left: var(--tui-height-m); + + &::before, + &::after { + content: ''; + position: absolute; + background: var(--tui-clear); + } + + &::before { + top: 0; + left: calc(1rem - 1px); + bottom: 0.5rem; + width: 2px; + } + + &::after { + left: 0.75rem; + bottom: 0; + width: 0.5rem; + height: 0.5rem; + border-radius: 100%; + } +} + +form-group tui-tooltip { + z-index: 1; + margin-left: 0.25rem; +} diff --git a/web/projects/ui/src/app/components/form/form-group/form-group.component.ts b/web/projects/ui/src/app/components/form/form-group/form-group.component.ts new file mode 100644 index 000000000..d9d28c8df --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-group/form-group.component.ts @@ -0,0 +1,35 @@ +import { + ChangeDetectionStrategy, + Component, + Input, + ViewEncapsulation, +} from '@angular/core' +import { CT } from '@start9labs/start-sdk' +import { FORM_GROUP_PROVIDERS } from './form-group.providers' + +export const ERRORS = [ + 'required', + 'pattern', + 'notNumber', + 'numberNotInteger', + 'numberNotInRange', + 'listNotUnique', + 'listNotInRange', + 'listItemIssue', +] + +@Component({ + selector: 'form-group', + templateUrl: './form-group.component.html', + styleUrls: ['./form-group.component.scss'], + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + viewProviders: [FORM_GROUP_PROVIDERS], +}) +export class FormGroupComponent { + @Input() spec: CT.InputSpec = {} + + asIsOrder() { + return 0 + } +} diff --git a/web/projects/ui/src/app/components/form/form-group/form-group.providers.ts b/web/projects/ui/src/app/components/form/form-group/form-group.providers.ts new file mode 100644 index 000000000..5c9039f40 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-group/form-group.providers.ts @@ -0,0 +1,34 @@ +import { Provider, SkipSelf } from '@angular/core' +import { + TUI_ARROW_MODE, + tuiInputDateOptionsProvider, + tuiInputTimeOptionsProvider, +} from '@taiga-ui/kit' +import { TUI_DEFAULT_ERROR_MESSAGE } from '@taiga-ui/core' +import { ControlContainer } from '@angular/forms' +import { identity, of } from 'rxjs' + +export const FORM_GROUP_PROVIDERS: Provider[] = [ + { + provide: TUI_DEFAULT_ERROR_MESSAGE, + useValue: of('Unknown error'), + }, + { + provide: ControlContainer, + deps: [[new SkipSelf(), ControlContainer]], + useFactory: identity, + }, + { + provide: TUI_ARROW_MODE, + useValue: { + interactive: null, + disabled: null, + }, + }, + tuiInputDateOptionsProvider({ + nativePicker: true, + }), + tuiInputTimeOptionsProvider({ + nativePicker: true, + }), +] diff --git a/web/projects/ui/src/app/components/form/form-multiselect/form-multiselect.component.html b/web/projects/ui/src/app/components/form/form-multiselect/form-multiselect.component.html new file mode 100644 index 000000000..0e2a47cc2 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-multiselect/form-multiselect.component.html @@ -0,0 +1,18 @@ + + {{ spec.name }} + + diff --git a/web/projects/ui/src/app/components/form/form-multiselect/form-multiselect.component.ts b/web/projects/ui/src/app/components/form/form-multiselect/form-multiselect.component.ts new file mode 100644 index 000000000..7134eb1f6 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-multiselect/form-multiselect.component.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core' +import { CT } from '@start9labs/start-sdk' +import { Control } from '../control' +import { tuiPure } from '@taiga-ui/cdk' +import { invert } from '@start9labs/shared' + +@Component({ + selector: 'form-multiselect', + templateUrl: './form-multiselect.component.html', +}) +export class FormMultiselectComponent extends Control< + CT.ValueSpecMultiselect, + readonly string[] +> { + private readonly inverted = invert(this.spec.values) + + private readonly isDisabled = (item: string) => + Array.isArray(this.spec.disabled) && + this.spec.disabled.includes(this.inverted[item]) + + private readonly isExceedingLimit = (item: string) => + !!this.spec.maxLength && + this.selected.length >= this.spec.maxLength && + !this.selected.includes(item) + + readonly disabledItemHandler = (item: string): boolean => + this.isDisabled(item) || this.isExceedingLimit(item) + + readonly items = Object.values(this.spec.values) + + get disabled(): boolean { + return typeof this.spec.disabled === 'string' + } + + get selected(): string[] { + return this.memoize(this.value) + } + + set selected(value: string[]) { + this.value = Object.entries(this.spec.values) + .filter(([_, v]) => value.includes(v)) + .map(([k]) => k) + } + + @tuiPure + private memoize(value: null | readonly string[]): string[] { + return value?.map(key => this.spec.values[key]) || [] + } +} diff --git a/web/projects/ui/src/app/components/form/form-number/form-number.component.html b/web/projects/ui/src/app/components/form/form-number/form-number.component.html new file mode 100644 index 000000000..c205b2bb8 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-number/form-number.component.html @@ -0,0 +1,18 @@ + + {{ spec.name }} + * + + diff --git a/web/projects/ui/src/app/components/form/form-number/form-number.component.ts b/web/projects/ui/src/app/components/form/form-number/form-number.component.ts new file mode 100644 index 000000000..a930b1614 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-number/form-number.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core' +import { CT } from '@start9labs/start-sdk' +import { Control } from '../control' + +@Component({ + selector: 'form-number', + templateUrl: './form-number.component.html', +}) +export class FormNumberComponent extends Control { + protected readonly Infinity = Infinity +} diff --git a/web/projects/ui/src/app/components/form/form-object/form-object.component.html b/web/projects/ui/src/app/components/form/form-object/form-object.component.html new file mode 100644 index 000000000..589019c15 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-object/form-object.component.html @@ -0,0 +1,25 @@ +

+ + + {{ spec.name }} + +

+ + +
+ +
+
diff --git a/web/projects/ui/src/app/components/form/form-object/form-object.component.scss b/web/projects/ui/src/app/components/form/form-object/form-object.component.scss new file mode 100644 index 000000000..c167c89fa --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-object/form-object.component.scss @@ -0,0 +1,41 @@ +@import '@taiga-ui/core/styles/taiga-ui-local'; + +:host { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.title { + position: relative; + height: var(--tui-height-l); + display: flex; + align-items: center; + cursor: pointer; + font: var(--tui-font-text-l); + font-weight: bold; + margin: 0 0 -0.75rem; +} + +.button { + @include transition(transform); + + margin-right: 1rem; + + &_open { + transform: rotate(180deg); + } +} + +.expand { + align-self: stretch; +} + +.g-form-group { + padding-top: 0.75rem; + + &_invalid::before, + &_invalid::after { + background: var(--tui-error-bg); + } +} diff --git a/web/projects/ui/src/app/components/form/form-object/form-object.component.ts b/web/projects/ui/src/app/components/form/form-object/form-object.component.ts new file mode 100644 index 000000000..b1aa507cf --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-object/form-object.component.ts @@ -0,0 +1,38 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + inject, + Input, + Output, +} from '@angular/core' +import { ControlContainer } from '@angular/forms' +import { CT } from '@start9labs/start-sdk' + +@Component({ + selector: 'form-object', + templateUrl: './form-object.component.html', + styleUrls: ['./form-object.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FormObjectComponent { + @Input() + spec!: CT.ValueSpecObject + + @Input() + open = false + + @Output() + readonly openChange = new EventEmitter() + + private readonly container = inject(ControlContainer) + + get invalid() { + return !this.container.valid && this.container.touched + } + + toggle() { + this.open = !this.open + this.openChange.emit(this.open) + } +} diff --git a/web/projects/ui/src/app/components/form/form-select/form-select.component.html b/web/projects/ui/src/app/components/form/form-select/form-select.component.html new file mode 100644 index 000000000..c10e62018 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-select/form-select.component.html @@ -0,0 +1,17 @@ + + {{ spec.name }} + * + + diff --git a/web/projects/ui/src/app/components/form/form-select/form-select.component.ts b/web/projects/ui/src/app/components/form/form-select/form-select.component.ts new file mode 100644 index 000000000..ccbbccae8 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-select/form-select.component.ts @@ -0,0 +1,30 @@ +import { Component } from '@angular/core' +import { CT } from '@start9labs/start-sdk' +import { invert } from '@start9labs/shared' +import { Control } from '../control' + +@Component({ + selector: 'form-select', + templateUrl: './form-select.component.html', +}) +export class FormSelectComponent extends Control { + private readonly inverted = invert(this.spec.values) + + readonly items = Object.values(this.spec.values) + + readonly disabledItemHandler = (item: string) => + Array.isArray(this.spec.disabled) && + this.spec.disabled.includes(this.inverted[item]) + + get disabled(): boolean { + return typeof this.spec.disabled === 'string' + } + + get selected(): string | null { + return (this.value && this.spec.values[this.value]) || null + } + + set selected(value: string | null) { + this.value = (value && this.inverted[value]) || null + } +} diff --git a/web/projects/ui/src/app/components/form/form-text/form-text.component.html b/web/projects/ui/src/app/components/form/form-text/form-text.component.html new file mode 100644 index 000000000..0db900238 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-text/form-text.component.html @@ -0,0 +1,44 @@ + + {{ spec.name }} + * + + + + + + diff --git a/web/projects/ui/src/app/components/form/form-text/form-text.component.scss b/web/projects/ui/src/app/components/form/form-text/form-text.component.scss new file mode 100644 index 000000000..05e47885b --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-text/form-text.component.scss @@ -0,0 +1,8 @@ +.button { + pointer-events: auto; + margin-left: 0.25rem; +} + +.masked { + -webkit-text-security: disc; +} diff --git a/web/projects/ui/src/app/components/form/form-text/form-text.component.ts b/web/projects/ui/src/app/components/form/form-text/form-text.component.ts new file mode 100644 index 000000000..8a5ac86f0 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-text/form-text.component.ts @@ -0,0 +1,16 @@ +import { Component } from '@angular/core' +import { CT, utils } from '@start9labs/start-sdk' +import { Control } from '../control' + +@Component({ + selector: 'form-text', + templateUrl: './form-text.component.html', + styleUrls: ['./form-text.component.scss'], +}) +export class FormTextComponent extends Control { + masked = true + + generate() { + this.value = utils.getDefaultString(this.spec.generate || '') + } +} diff --git a/web/projects/ui/src/app/components/form/form-textarea/form-textarea.component.html b/web/projects/ui/src/app/components/form/form-textarea/form-textarea.component.html new file mode 100644 index 000000000..7a2bf5149 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-textarea/form-textarea.component.html @@ -0,0 +1,15 @@ + + {{ spec.name }} + * + + diff --git a/web/projects/ui/src/app/components/form/form-textarea/form-textarea.component.ts b/web/projects/ui/src/app/components/form/form-textarea/form-textarea.component.ts new file mode 100644 index 000000000..b32685c21 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-textarea/form-textarea.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core' +import { CT } from '@start9labs/start-sdk' +import { Control } from '../control' + +@Component({ + selector: 'form-textarea', + templateUrl: './form-textarea.component.html', +}) +export class FormTextareaComponent extends Control< + CT.ValueSpecTextarea, + string +> {} diff --git a/web/projects/ui/src/app/components/form/form-toggle/form-toggle.component.html b/web/projects/ui/src/app/components/form/form-toggle/form-toggle.component.html new file mode 100644 index 000000000..73d116f92 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-toggle/form-toggle.component.html @@ -0,0 +1,11 @@ +{{ spec.name }} + + diff --git a/web/projects/ui/src/app/components/form/form-toggle/form-toggle.component.ts b/web/projects/ui/src/app/components/form/form-toggle/form-toggle.component.ts new file mode 100644 index 000000000..6a3c0196f --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-toggle/form-toggle.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core' +import { CT } from '@start9labs/start-sdk' +import { Control } from '../control' + +@Component({ + selector: 'form-toggle', + templateUrl: './form-toggle.component.html', + host: { class: 'g-toggle' }, +}) +export class FormToggleComponent extends Control {} diff --git a/web/projects/ui/src/app/components/form/form-union/form-union.component.html b/web/projects/ui/src/app/components/form/form-union/form-union.component.html new file mode 100644 index 000000000..1cb5bfe57 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-union/form-union.component.html @@ -0,0 +1,11 @@ + + + + diff --git a/web/projects/ui/src/app/components/form/form-union/form-union.component.scss b/web/projects/ui/src/app/components/form/form-union/form-union.component.scss new file mode 100644 index 000000000..cfb2f95e8 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-union/form-union.component.scss @@ -0,0 +1,8 @@ +:host { + display: block; +} + +.group { + display: block; + margin-top: 1rem; +} diff --git a/web/projects/ui/src/app/components/form/form-union/form-union.component.ts b/web/projects/ui/src/app/components/form/form-union/form-union.component.ts new file mode 100644 index 000000000..2c164be48 --- /dev/null +++ b/web/projects/ui/src/app/components/form/form-union/form-union.component.ts @@ -0,0 +1,55 @@ +import { + ChangeDetectionStrategy, + Component, + inject, + Input, + OnChanges, +} from '@angular/core' +import { ControlContainer, FormGroupName } from '@angular/forms' +import { CT } from '@start9labs/start-sdk' +import { FormService } from 'src/app/services/form.service' +import { tuiPure } from '@taiga-ui/cdk' + +@Component({ + selector: 'form-union', + templateUrl: './form-union.component.html', + styleUrls: ['./form-union.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + viewProviders: [ + { + provide: ControlContainer, + useExisting: FormGroupName, + }, + ], +}) +export class FormUnionComponent implements OnChanges { + @Input() + spec!: CT.ValueSpecUnion + + selectSpec!: CT.ValueSpecSelect + + private readonly form = inject(FormGroupName) + private readonly formService = inject(FormService) + + get union(): string { + return this.form.value.selection + } + + @tuiPure + onUnion(union: string) { + this.form.control.setControl( + 'value', + this.formService.getFormGroup( + union ? this.spec.variants[union].spec : {}, + ), + { + emitEvent: false, + }, + ) + } + + ngOnChanges() { + this.selectSpec = this.formService.getUnionSelectSpec(this.spec, this.union) + if (this.union) this.onUnion(this.union) + } +} diff --git a/web/projects/ui/src/app/components/form/form.module.ts b/web/projects/ui/src/app/components/form/form.module.ts new file mode 100644 index 000000000..fe16b229f --- /dev/null +++ b/web/projects/ui/src/app/components/form/form.module.ts @@ -0,0 +1,109 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { MaskitoModule } from '@maskito/angular' +import { TuiMapperPipeModule, TuiValueChangesModule } from '@taiga-ui/cdk' +import { + TuiErrorModule, + TuiExpandModule, + TuiHintModule, + TuiLinkModule, + TuiModeModule, + TuiTextfieldControllerModule, + TuiTooltipModule, +} from '@taiga-ui/core' +import { + TuiAppearanceModule, + TuiButtonModule, + TuiIconModule, +} from '@taiga-ui/experimental' +import { + TuiElasticContainerModule, + TuiFieldErrorPipeModule, + TuiInputDateModule, + TuiInputDateTimeModule, + TuiInputFilesModule, + TuiInputModule, + TuiInputNumberModule, + TuiInputTimeModule, + TuiMultiSelectModule, + TuiPromptModule, + TuiSelectModule, + TuiTagModule, + TuiTextAreaModule, + TuiToggleModule, +} from '@taiga-ui/kit' + +import { FormGroupComponent } from './form-group/form-group.component' +import { FormTextComponent } from './form-text/form-text.component' +import { FormToggleComponent } from './form-toggle/form-toggle.component' +import { FormTextareaComponent } from './form-textarea/form-textarea.component' +import { FormNumberComponent } from './form-number/form-number.component' +import { FormSelectComponent } from './form-select/form-select.component' +import { FormFileComponent } from './form-file/form-file.component' +import { FormMultiselectComponent } from './form-multiselect/form-multiselect.component' +import { FormUnionComponent } from './form-union/form-union.component' +import { FormObjectComponent } from './form-object/form-object.component' +import { FormArrayComponent } from './form-array/form-array.component' +import { FormControlComponent } from './form-control/form-control.component' +import { MustachePipe } from './mustache.pipe' +import { ControlDirective } from './control.directive' +import { FormColorComponent } from './form-color/form-color.component' +import { FormDatetimeComponent } from './form-datetime/form-datetime.component' +import { HintPipe } from './hint.pipe' + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TuiInputModule, + TuiInputNumberModule, + TuiInputFilesModule, + TuiTextAreaModule, + TuiSelectModule, + TuiMultiSelectModule, + TuiToggleModule, + TuiTooltipModule, + TuiHintModule, + TuiModeModule, + TuiTagModule, + TuiButtonModule, + TuiExpandModule, + TuiTextfieldControllerModule, + TuiLinkModule, + TuiPromptModule, + TuiErrorModule, + TuiFieldErrorPipeModule, + TuiValueChangesModule, + TuiElasticContainerModule, + MaskitoModule, + TuiIconModule, + TuiAppearanceModule, + TuiInputDateModule, + TuiInputTimeModule, + TuiInputDateTimeModule, + TuiMapperPipeModule, + ], + declarations: [ + FormGroupComponent, + FormControlComponent, + FormColorComponent, + FormDatetimeComponent, + FormTextComponent, + FormToggleComponent, + FormTextareaComponent, + FormNumberComponent, + FormSelectComponent, + FormMultiselectComponent, + FormFileComponent, + FormUnionComponent, + FormObjectComponent, + FormArrayComponent, + MustachePipe, + HintPipe, + ControlDirective, + ], + exports: [FormGroupComponent], +}) +export class FormModule {} diff --git a/web/projects/ui/src/app/components/form/hint.pipe.ts b/web/projects/ui/src/app/components/form/hint.pipe.ts new file mode 100644 index 000000000..b5a730661 --- /dev/null +++ b/web/projects/ui/src/app/components/form/hint.pipe.ts @@ -0,0 +1,21 @@ +import { Pipe, PipeTransform } from '@angular/core' +import { CT } from '@start9labs/start-sdk' + +@Pipe({ + name: 'hint', +}) +export class HintPipe implements PipeTransform { + transform(spec: CT.ValueSpec): string { + const hint = [] + + if (spec.description) { + hint.push(spec.description) + } + + if ('disabled' in spec && typeof spec.disabled === 'string') { + hint.push(`Disabled: ${spec.disabled}`) + } + + return hint.join('\n\n') + } +} diff --git a/web/projects/ui/src/app/components/form/invalid.service.ts b/web/projects/ui/src/app/components/form/invalid.service.ts new file mode 100644 index 000000000..9f474e853 --- /dev/null +++ b/web/projects/ui/src/app/components/form/invalid.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@angular/core' +import { ControlDirective } from './control.directive' + +@Injectable() +export class InvalidService { + private readonly controls: ControlDirective[] = [] + + scrollIntoView() { + this.controls.find(({ invalid }) => invalid)?.scrollIntoView() + } + + add(control: ControlDirective) { + this.controls.push(control) + } + + remove(control: ControlDirective) { + this.controls.splice(this.controls.indexOf(control), 1) + } +} diff --git a/web/projects/ui/src/app/components/form/mustache.pipe.ts b/web/projects/ui/src/app/components/form/mustache.pipe.ts new file mode 100644 index 000000000..ec04b0104 --- /dev/null +++ b/web/projects/ui/src/app/components/form/mustache.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from '@angular/core' + +const Mustache = require('mustache') + +@Pipe({ + name: 'mustache', +}) +export class MustachePipe implements PipeTransform { + transform(value: any, displayAs: string): string { + return displayAs && Mustache.render(displayAs, value) + } +} diff --git a/web/projects/ui/src/app/components/logs/logs.component.ts b/web/projects/ui/src/app/components/logs/logs.component.ts index 3d31313cb..25897c219 100644 --- a/web/projects/ui/src/app/components/logs/logs.component.ts +++ b/web/projects/ui/src/app/components/logs/logs.component.ts @@ -1,5 +1,15 @@ import { Component, Input, ViewChild } from '@angular/core' -import { IonContent, LoadingController } from '@ionic/angular' +import { IonContent } from '@ionic/angular' +import { + DownloadHTMLService, + ErrorService, + LoadingService, + Log, + LogsRes, + ServerLogsReq, + toLocalIsoString, +} from '@start9labs/shared' +import { TuiDestroyService } from '@taiga-ui/cdk' import { bufferTime, catchError, @@ -11,15 +21,6 @@ import { takeUntil, tap, } from 'rxjs' -import { - LogsRes, - ServerLogsReq, - ErrorToastService, - toLocalIsoString, - Log, - DownloadHTMLService, -} from '@start9labs/shared' -import { TuiDestroyService } from '@taiga-ui/cdk' import { RR } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' import { ConnectionService } from 'src/app/services/connection.service' @@ -66,10 +67,10 @@ export class LogsComponent { count = 0 constructor( - private readonly errToast: ErrorToastService, + private readonly errorService: ErrorService, private readonly destroy$: TuiDestroyService, private readonly api: ApiService, - private readonly loadingCtrl: LoadingController, + private readonly loader: LoadingService, private readonly downloadHtml: DownloadHTMLService, private readonly connection$: ConnectionService, ) {} @@ -97,7 +98,7 @@ export class LogsComponent { this.processRes(res) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { e.target.complete() } @@ -119,10 +120,7 @@ export class LogsComponent { } async download() { - const loader = await this.loadingCtrl.create({ - message: 'Processing 10,000 logs...', - }) - await loader.present() + const loader = this.loader.open('Processing 10,000 logs...').subscribe() try { const { entries } = await this.fetchLogs({ @@ -139,9 +137,9 @@ export class LogsComponent { this.downloadHtml.download(`${this.context}-logs.html`, html, styles) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } diff --git a/web/projects/ui/src/app/components/toast-container/refresh-alert/refresh-alert.component.ts b/web/projects/ui/src/app/components/toast-container/refresh-alert/refresh-alert.component.ts index f039841d3..04943eb1a 100644 --- a/web/projects/ui/src/app/components/toast-container/refresh-alert/refresh-alert.component.ts +++ b/web/projects/ui/src/app/components/toast-container/refresh-alert/refresh-alert.component.ts @@ -1,9 +1,9 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' +import { SwUpdate } from '@angular/service-worker' +import { LoadingService } from '@start9labs/shared' import { merge, Observable, Subject } from 'rxjs' import { RefreshAlertService } from './refresh-alert.service' -import { SwUpdate } from '@angular/service-worker' -import { LoadingController } from '@ionic/angular' @Component({ selector: 'refresh-alert', @@ -18,7 +18,7 @@ export class RefreshAlertComponent { constructor( @Inject(RefreshAlertService) private readonly refresh$: Observable, private readonly updates: SwUpdate, - private readonly loadingCtrl: LoadingController, + private readonly loader: LoadingService, ) {} ngOnInit() { @@ -26,17 +26,14 @@ export class RefreshAlertComponent { } async pwaReload() { - const loader = await this.loadingCtrl.create({ - message: 'Reloading PWA...', - }) - await loader.present() + const loader = this.loader.open('Reloading PWA...').subscribe() try { // attempt to update to the latest client version available await this.updates.activateUpdate() } catch (e) { console.error('Error activating update from service worker: ', e) } finally { - loader.dismiss() + loader.unsubscribe() // always reload, as this resolves most out of sync cases window.location.reload() } diff --git a/web/projects/ui/src/app/components/toast-container/update-toast/update-toast.component.ts b/web/projects/ui/src/app/components/toast-container/update-toast/update-toast.component.ts index 72b6956ee..276260ba8 100644 --- a/web/projects/ui/src/app/components/toast-container/update-toast/update-toast.component.ts +++ b/web/projects/ui/src/app/components/toast-container/update-toast/update-toast.component.ts @@ -1,10 +1,9 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' -import { LoadingController } from '@ionic/angular' -import { ErrorToastService } from '@start9labs/shared' -import { Observable, Subject, merge } from 'rxjs' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { merge, Observable, Subject } from 'rxjs' +import { ApiService } from '../../../services/api/embassy-api.service' import { UpdateToastService } from './update-toast.service' -import { ApiService } from '../../../services/api/embassy-api.service' @Component({ selector: 'update-toast', @@ -19,8 +18,8 @@ export class UpdateToastComponent { constructor( @Inject(UpdateToastService) private readonly update$: Observable, private readonly embassyApi: ApiService, - private readonly errToast: ErrorToastService, - private readonly loadingCtrl: LoadingController, + private readonly errorService: ErrorService, + private readonly loader: LoadingService, ) {} onDismiss() { @@ -30,18 +29,14 @@ export class UpdateToastComponent { async restart(): Promise { this.onDismiss() - const loader = await this.loadingCtrl.create({ - message: 'Restarting...', - }) - - await loader.present() + const loader = this.loader.open('Restarting...').subscribe() try { await this.embassyApi.restartServer({}) } catch (e: any) { - await this.errToast.present(e) + await this.errorService.handleError(e) } finally { - await loader.dismiss() + await loader.unsubscribe() } } } diff --git a/web/projects/ui/src/app/modals/app-config/app-config.module.ts b/web/projects/ui/src/app/modals/app-config/app-config.module.ts deleted file mode 100644 index fde422826..000000000 --- a/web/projects/ui/src/app/modals/app-config/app-config.module.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { FormsModule, ReactiveFormsModule } from '@angular/forms' -import { IonicModule } from '@ionic/angular' -import { AppConfigPage } from './app-config.page' -import { TextSpinnerComponentModule } from '@start9labs/shared' -import { FormObjectComponentModule } from 'src/app/components/form-object/form-object.component.module' - -@NgModule({ - declarations: [AppConfigPage], - imports: [ - CommonModule, - FormsModule, - IonicModule, - TextSpinnerComponentModule, - FormObjectComponentModule, - ReactiveFormsModule, - ], - exports: [AppConfigPage], -}) -export class AppConfigPageModule {} diff --git a/web/projects/ui/src/app/modals/app-config/app-config.page.html b/web/projects/ui/src/app/modals/app-config/app-config.page.html deleted file mode 100644 index 42eb6cf39..000000000 --- a/web/projects/ui/src/app/modals/app-config/app-config.page.html +++ /dev/null @@ -1,144 +0,0 @@ - - - Config - - - - - - - - - - - - - - - - - {{ loadingError }} - - - - - - -

- - {{ manifest.title }} has been automatically configured with - recommended defaults. Make whatever changes you want, then click - "Save". - -

-
- -

- - New config options! To accept the default values, click "Save". - You may also customize these new options below. - -

-
-
- - - - -

- - - {{ manifest.title }} - -

-

- - The following modifications have been made to {{ manifest.title }} - to satisfy {{ dependentInfo.title }}: -

    -
  • -
- To accept these modifications, click "Save". - -

-
-
- - - - -

- No config options for {{ manifest.title }} {{ manifest.version }}. -

-
-
- - -
- -
-
-
-
- - - - - - - - Reset Defaults - - - - - Save - - - Close - - - - - diff --git a/web/projects/ui/src/app/modals/app-config/app-config.page.scss b/web/projects/ui/src/app/modals/app-config/app-config.page.scss deleted file mode 100644 index e568528a8..000000000 --- a/web/projects/ui/src/app/modals/app-config/app-config.page.scss +++ /dev/null @@ -1,12 +0,0 @@ -.notifier-item { - margin: 12px; - margin-top: 0px; - border-radius: 12px; - // kills the lines - --border-width: 0; - --inner-border-width: 0; -} - -.header-details { - font-size: 20px; -} \ No newline at end of file diff --git a/web/projects/ui/src/app/modals/app-config/app-config.page.ts b/web/projects/ui/src/app/modals/app-config/app-config.page.ts deleted file mode 100644 index 4bba83acb..000000000 --- a/web/projects/ui/src/app/modals/app-config/app-config.page.ts +++ /dev/null @@ -1,349 +0,0 @@ -import { Component, Input } from '@angular/core' -import { - AlertController, - ModalController, - LoadingController, - IonicSafeString, -} from '@ionic/angular' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { - ErrorToastService, - getErrorMessage, - isEmptyObject, - isObject, -} from '@start9labs/shared' -import { DependentInfo } from 'src/app/types/dependent-info' -import { ConfigSpec } from 'src/app/pkg-config/config-types' -import { - DataModel, - InstalledState, - PackageDataEntry, -} from 'src/app/services/patch-db/data-model' -import { PatchDB } from 'patch-db-client' -import { UntypedFormGroup } from '@angular/forms' -import { - convertValuesRecursive, - FormService, -} from 'src/app/services/form.service' -import { compare, Operation, getValueByPointer } from 'fast-json-patch' -import { hasCurrentDeps } from 'src/app/util/has-deps' -import { - getAllPackages, - getManifest, - getPackage, - isInstalled, -} from 'src/app/util/get-package-data' -import { Breakages } from 'src/app/services/api/api.types' - -@Component({ - selector: 'app-config', - templateUrl: './app-config.page.html', - styleUrls: ['./app-config.page.scss'], -}) -export class AppConfigPage { - @Input() pkgId!: string - - @Input() dependentInfo?: DependentInfo - - pkg!: PackageDataEntry - loadingText = '' - - configSpec?: ConfigSpec - configForm?: UntypedFormGroup - - original?: object // only if existing config - diff?: string[] // only if dependent info - - loading = true - hasNewOptions = false - saving = false - loadingError: string | IonicSafeString = '' - - constructor( - private readonly embassyApi: ApiService, - private readonly errToast: ErrorToastService, - private readonly loadingCtrl: LoadingController, - private readonly alertCtrl: AlertController, - private readonly modalCtrl: ModalController, - private readonly formService: FormService, - private readonly patch: PatchDB, - ) {} - - get hasConfig() { - return this.pkg.stateInfo.manifest.hasConfig - } - - async ngOnInit() { - try { - const pkg = await getPackage(this.patch, this.pkgId) - if (!pkg || !isInstalled(pkg)) return - - this.pkg = pkg - - if (!this.hasConfig) return - - let newConfig: object | undefined - let patch: Operation[] | undefined - - if (this.dependentInfo) { - this.loadingText = `Setting properties to accommodate ${this.dependentInfo.title}` - const { - oldConfig: oc, - newConfig: nc, - spec: s, - } = await this.embassyApi.dryConfigureDependency({ - dependencyId: this.pkgId, - dependentId: this.dependentInfo.id, - }) - this.original = oc - newConfig = nc - this.configSpec = s - patch = compare(this.original, newConfig) - } else { - this.loadingText = 'Loading Config' - const { config: c, spec: s } = await this.embassyApi.getPackageConfig({ - id: this.pkgId, - }) - this.original = c - this.configSpec = s - } - - this.configForm = this.formService.createForm( - this.configSpec, - newConfig || this.original, - ) - - if (patch) { - this.diff = this.getDiff(patch) - this.markDirty(patch) - } - } catch (e: any) { - this.loadingError = getErrorMessage(e) - } finally { - this.loading = false - } - } - - resetDefaults() { - this.configForm = this.formService.createForm(this.configSpec!) - const patch = compare(this.original || {}, this.configForm.value) - this.markDirty(patch) - } - - async dismiss() { - if (this.configForm?.dirty) { - this.presentAlertUnsaved() - } else { - this.modalCtrl.dismiss() - } - } - - async tryConfigure() { - convertValuesRecursive(this.configSpec!, this.configForm!) - - if (this.configForm!.invalid) { - document - .getElementsByClassName('validation-error')[0] - ?.scrollIntoView({ behavior: 'smooth' }) - return - } - - this.saving = true - - if (hasCurrentDeps(this.pkgId, await getAllPackages(this.patch))) { - this.dryConfigure() - } else { - this.configure() - } - } - - private async dryConfigure() { - const loader = await this.loadingCtrl.create({ - message: 'Checking dependent services...', - }) - await loader.present() - - try { - const breakages = await this.embassyApi.drySetPackageConfig({ - id: this.pkgId, - config: this.configForm!.value, - }) - - if (isEmptyObject(breakages)) { - this.configure(loader) - } else { - await loader.dismiss() - const proceed = await this.presentAlertBreakages(breakages) - if (proceed) { - this.configure() - } else { - this.saving = false - } - } - } catch (e: any) { - this.errToast.present(e) - this.saving = false - loader.dismiss() - } - } - - private async configure(loader?: HTMLIonLoadingElement) { - const message = 'Saving...' - if (loader) { - loader.message = message - } else { - loader = await this.loadingCtrl.create({ message }) - await loader.present() - } - - try { - await this.embassyApi.setPackageConfig({ - id: this.pkgId, - config: this.configForm!.value, - }) - this.modalCtrl.dismiss() - } catch (e: any) { - this.errToast.present(e) - } finally { - this.saving = false - loader.dismiss() - } - } - - private async presentAlertBreakages(breakages: Breakages): Promise { - let message: string = - 'As a result of this change, the following services will no longer work properly and may crash:
    ' - const localPkgs = await getAllPackages(this.patch) - const bullets = Object.keys(breakages).map(id => { - const title = getManifest(localPkgs[id]).title - return `
  • ${title}
  • ` - }) - message = `${message}${bullets}
` - - return new Promise(async resolve => { - const alert = await this.alertCtrl.create({ - header: 'Warning', - message, - buttons: [ - { - text: 'Cancel', - role: 'cancel', - handler: () => { - resolve(false) - }, - }, - { - text: 'Continue', - handler: () => { - resolve(true) - }, - cssClass: 'enter-click', - }, - ], - cssClass: 'alert-warning-message', - }) - - await alert.present() - }) - } - - private getDiff(patch: Operation[]): string[] { - return patch.map(op => { - let message: string - switch (op.op) { - case 'add': - message = `Added ${this.getNewValue(op.value)}` - break - case 'remove': - message = `Removed ${this.getOldValue(op.path)}` - break - case 'replace': - message = `Changed from ${this.getOldValue( - op.path, - )} to ${this.getNewValue(op.value)}` - break - default: - message = `Unknown operation` - } - - let displayPath: string - - const arrPath = op.path - .substring(1) - .split('/') - .map(node => { - const num = Number(node) - return isNaN(num) ? node : num - }) - - if (typeof arrPath[arrPath.length - 1] === 'number') { - arrPath.pop() - } - - displayPath = arrPath.join(' → ') - - return `${displayPath}: ${message}` - }) - } - - private getOldValue(path: any): string { - const val = getValueByPointer(this.original, path) - if (['string', 'number', 'boolean'].includes(typeof val)) { - return val - } else if (isObject(val)) { - return 'entry' - } else { - return 'list' - } - } - - private getNewValue(val: any): string { - if (['string', 'number', 'boolean'].includes(typeof val)) { - return val - } else if (isObject(val)) { - return 'new entry' - } else { - return 'new list' - } - } - - private markDirty(patch: Operation[]) { - patch.forEach(op => { - const arrPath = op.path - .substring(1) - .split('/') - .map(node => { - const num = Number(node) - return isNaN(num) ? node : num - }) - - if (op.op !== 'remove') this.configForm!.get(arrPath)?.markAsDirty() - - if (typeof arrPath[arrPath.length - 1] === 'number') { - const prevPath = arrPath.slice(0, arrPath.length - 1) - this.configForm!.get(prevPath)?.markAsDirty() - } - }) - } - - private async presentAlertUnsaved() { - const alert = await this.alertCtrl.create({ - header: 'Unsaved Changes', - message: 'You have unsaved changes. Are you sure you want to leave?', - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: `Leave`, - handler: () => { - this.modalCtrl.dismiss() - }, - cssClass: 'enter-click', - }, - ], - }) - await alert.present() - } -} diff --git a/web/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.ts b/web/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.ts index 15bec3d4f..e1d637ecf 100644 --- a/web/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.ts +++ b/web/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.ts @@ -1,16 +1,12 @@ import { Component, Input } from '@angular/core' -import { - LoadingController, - ModalController, - IonicSafeString, -} from '@ionic/angular' -import { getErrorMessage } from '@start9labs/shared' +import { IonicSafeString, ModalController } from '@ionic/angular' +import { getErrorMessage, LoadingService } from '@start9labs/shared' +import { PatchDB } from 'patch-db-client' +import { take } from 'rxjs' import { BackupInfo } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { PatchDB } from 'patch-db-client' -import { AppRecoverOption } from './to-options.pipe' import { DataModel } from 'src/app/services/patch-db/data-model' -import { take } from 'rxjs' +import { AppRecoverOption } from './to-options.pipe' @Component({ selector: 'app-recover-select', @@ -30,7 +26,7 @@ export class AppRecoverSelectPage { constructor( private readonly modalCtrl: ModalController, - private readonly loadingCtrl: LoadingController, + private readonly loader: LoadingService, private readonly embassyApi: ApiService, private readonly patch: PatchDB, ) {} @@ -45,10 +41,7 @@ export class AppRecoverSelectPage { async restore(options: AppRecoverOption[]): Promise { const ids = options.filter(({ checked }) => !!checked).map(({ id }) => id) - const loader = await this.loadingCtrl.create({ - message: 'Initializing...', - }) - await loader.present() + const loader = this.loader.open('Initializing...').subscribe() try { await this.embassyApi.restorePackages({ @@ -61,7 +54,7 @@ export class AppRecoverSelectPage { } catch (e: any) { this.error = getErrorMessage(e) } finally { - loader.dismiss() + loader.unsubscribe() } } } diff --git a/web/projects/ui/src/app/modals/config-dep.component.ts b/web/projects/ui/src/app/modals/config-dep.component.ts new file mode 100644 index 000000000..14becf2e8 --- /dev/null +++ b/web/projects/ui/src/app/modals/config-dep.component.ts @@ -0,0 +1,104 @@ +import { + ChangeDetectionStrategy, + Component, + Input, + OnChanges, +} from '@angular/core' +import { compare, getValueByPointer, Operation } from 'fast-json-patch' +import { isObject } from '@start9labs/shared' +import { tuiIsNumber } from '@taiga-ui/cdk' +import { CommonModule } from '@angular/common' +import { TuiNotificationModule } from '@taiga-ui/core' + +@Component({ + selector: 'config-dep', + template: ` + +

+ {{ package }} +

+ The following modifications have been made to {{ package }} to satisfy + {{ dep }}: +
    +
  • +
+ To accept these modifications, click "Save". +
+ `, + standalone: true, + imports: [CommonModule, TuiNotificationModule], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ConfigDepComponent implements OnChanges { + @Input() + package = '' + + @Input() + dep = '' + + @Input() + original: object = {} + + @Input() + value: object = {} + + diff: string[] = [] + + ngOnChanges() { + this.diff = compare(this.original, this.value).map( + op => `${this.getPath(op)}: ${this.getMessage(op)}`, + ) + } + + private getPath(operation: Operation): string { + const path = operation.path + .substring(1) + .split('/') + .map(node => { + const num = Number(node) + return isNaN(num) ? node : num + }) + + if (tuiIsNumber(path[path.length - 1])) { + path.pop() + } + + return path.join(' → ') + } + + private getMessage(operation: Operation): string { + switch (operation.op) { + case 'add': + return `Added ${this.getNewValue(operation.value)}` + case 'remove': + return `Removed ${this.getOldValue(operation.path)}` + case 'replace': + return `Changed from ${this.getOldValue( + operation.path, + )} to ${this.getNewValue(operation.value)}` + default: + return `Unknown operation` + } + } + + private getOldValue(path: any): string { + const val = getValueByPointer(this.original, path) + if (['string', 'number', 'boolean'].includes(typeof val)) { + return val + } else if (isObject(val)) { + return 'entry' + } else { + return 'list' + } + } + + private getNewValue(val: any): string { + if (['string', 'number', 'boolean'].includes(typeof val)) { + return val + } else if (isObject(val)) { + return 'new entry' + } else { + return 'new list' + } + } +} diff --git a/web/projects/ui/src/app/modals/config.component.ts b/web/projects/ui/src/app/modals/config.component.ts new file mode 100644 index 000000000..194302f5f --- /dev/null +++ b/web/projects/ui/src/app/modals/config.component.ts @@ -0,0 +1,281 @@ +import { CommonModule } from '@angular/common' +import { Component, Inject, ViewChild } from '@angular/core' +import { + ErrorService, + getErrorMessage, + isEmptyObject, + LoadingService, +} from '@start9labs/shared' +import { CT } from '@start9labs/start-sdk' +import { TuiButtonModule } from '@taiga-ui/experimental' +import { + TuiDialogContext, + TuiDialogService, + TuiLoaderModule, + TuiModeModule, + TuiNotificationModule, +} from '@taiga-ui/core' +import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit' +import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus' +import { compare, Operation } from 'fast-json-patch' +import { PatchDB } from 'patch-db-client' +import { endWith, firstValueFrom, Subscription } from 'rxjs' +import { ActionButton, FormComponent } from 'src/app/components/form.component' +import { InvalidService } from 'src/app/components/form/invalid.service' +import { ConfigDepComponent } from 'src/app/modals/config-dep.component' +import { UiPipeModule } from 'src/app/pipes/ui/ui.module' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { + DataModel, + PackageDataEntry, +} from 'src/app/services/patch-db/data-model' +import { + getAllPackages, + getManifest, + getPackage, +} from 'src/app/util/get-package-data' +import { hasCurrentDeps } from 'src/app/util/has-deps' +import { Breakages } from 'src/app/services/api/api.types' +import { DependentInfo } from 'src/app/types/dependent-info' + +export interface PackageConfigData { + readonly pkgId: string + readonly dependentInfo?: DependentInfo +} + +@Component({ + template: ` + + + +
+
+ + + + {{ manifest.title }} has been automatically configured with recommended + defaults. Make whatever changes you want, then click "Save". + + + + + + No config options for {{ manifest.title }} {{ manifest.version }}. + + + + + + + `, + styles: [ + ` + tui-notification { + font-size: 1rem; + margin-bottom: 1rem; + } + `, + ], + standalone: true, + imports: [ + CommonModule, + FormComponent, + TuiLoaderModule, + TuiNotificationModule, + TuiButtonModule, + TuiModeModule, + ConfigDepComponent, + UiPipeModule, + ], + providers: [InvalidService], +}) +export class ConfigModal { + @ViewChild(FormComponent) + private readonly form?: FormComponent> + + readonly pkgId = this.context.data.pkgId + readonly dependentInfo = this.context.data.dependentInfo + + loadingError = '' + loadingText = this.dependentInfo + ? `Setting properties to accommodate ${this.dependentInfo.title}` + : 'Loading Config' + + pkg?: PackageDataEntry + spec: CT.InputSpec = {} + patch: Operation[] = [] + buttons: ActionButton[] = [ + { + text: 'Save', + handler: value => this.save(value), + }, + ] + + original: object | null = null + value: object | null = null + + constructor( + @Inject(POLYMORPHEUS_CONTEXT) + private readonly context: TuiDialogContext, + private readonly dialogs: TuiDialogService, + private readonly errorService: ErrorService, + private readonly loader: LoadingService, + private readonly embassyApi: ApiService, + private readonly patchDb: PatchDB, + ) {} + + get success(): boolean { + return ( + !!this.form && + !this.form.form.dirty && + !this.original && + !this.pkg?.status?.configured + ) + } + + async ngOnInit() { + try { + this.pkg = await getPackage(this.patchDb, this.pkgId) + + if (!this.pkg) { + this.loadingError = 'This service does not exist' + + return + } + + if (this.dependentInfo) { + const depConfig = await this.embassyApi.dryConfigureDependency({ + dependencyId: this.pkgId, + dependentId: this.dependentInfo.id, + }) + + this.original = depConfig.oldConfig + this.value = depConfig.newConfig || this.original + this.spec = depConfig.spec + this.patch = compare(this.original, this.value) + } else { + const { config, spec } = await this.embassyApi.getPackageConfig({ + id: this.pkgId, + }) + + this.original = config + this.value = config + this.spec = spec + } + } catch (e: any) { + this.loadingError = String(getErrorMessage(e)) + } finally { + this.loadingText = '' + } + } + + private async save(config: any) { + const loader = new Subscription() + + try { + await this.uploadFiles(config, loader) + + if (hasCurrentDeps(this.pkgId, await getAllPackages(this.patchDb))) { + await this.configureDeps(config, loader) + } else { + await this.configure(config, loader) + } + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } + } + + private async uploadFiles(config: Record, loader: Subscription) { + loader.unsubscribe() + loader.closed = false + + // TODO: Could be nested files + const keys = Object.keys(config).filter(key => config[key] instanceof File) + const message = `Uploading File${keys.length > 1 ? 's' : ''}...` + + if (!keys.length) return + + loader.add(this.loader.open(message).subscribe()) + + const hashes = await Promise.all( + keys.map(key => this.embassyApi.uploadFile(config[key])), + ) + keys.forEach((key, i) => (config[key] = hashes[i])) + } + + private async configureDeps( + config: Record, + loader: Subscription, + ) { + loader.unsubscribe() + loader.closed = false + loader.add(this.loader.open('Checking dependent services...').subscribe()) + + const breakages = await this.embassyApi.drySetPackageConfig({ + id: this.pkgId, + config, + }) + + loader.unsubscribe() + loader.closed = false + + if (isEmptyObject(breakages) || (await this.approveBreakages(breakages))) { + await this.configure(config, loader) + } + } + + private async configure(config: Record, loader: Subscription) { + loader.unsubscribe() + loader.closed = false + loader.add(this.loader.open('Saving...').subscribe()) + + await this.embassyApi.setPackageConfig({ id: this.pkgId, config }) + this.context.$implicit.complete() + } + + private async approveBreakages(breakages: Breakages): Promise { + const packages = await getAllPackages(this.patchDb) + const message = + 'As a result of this change, the following services will no longer work properly and may crash:
    ' + const content = `${message}${Object.keys(breakages).map( + id => `
  • ${getManifest(packages[id]).title}
  • `, + )}
` + const data: TuiPromptData = { content, yes: 'Continue', no: 'Cancel' } + + return firstValueFrom( + this.dialogs.open(TUI_PROMPT, { data }).pipe(endWith(false)), + ) + } +} diff --git a/web/projects/ui/src/app/modals/enum-list/enum-list.module.ts b/web/projects/ui/src/app/modals/enum-list/enum-list.module.ts deleted file mode 100644 index a0acb46d5..000000000 --- a/web/projects/ui/src/app/modals/enum-list/enum-list.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { EnumListPage } from './enum-list.page' -import { FormsModule } from '@angular/forms' - -@NgModule({ - declarations: [EnumListPage], - imports: [CommonModule, IonicModule, FormsModule], - exports: [EnumListPage], -}) -export class EnumListPageModule {} diff --git a/web/projects/ui/src/app/modals/enum-list/enum-list.page.html b/web/projects/ui/src/app/modals/enum-list/enum-list.page.html deleted file mode 100644 index 5cc74ba21..000000000 --- a/web/projects/ui/src/app/modals/enum-list/enum-list.page.html +++ /dev/null @@ -1,45 +0,0 @@ - - - {{ spec.name }} - - - - - - - - - - - - - - {{ selectAll ? 'Select All' : 'Deselect All' }} - - - - - {{ spec.spec['value-names'][option.key] }} - - - - - - - - - - Done - - - - diff --git a/web/projects/ui/src/app/modals/enum-list/enum-list.page.scss b/web/projects/ui/src/app/modals/enum-list/enum-list.page.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/projects/ui/src/app/modals/enum-list/enum-list.page.ts b/web/projects/ui/src/app/modals/enum-list/enum-list.page.ts deleted file mode 100644 index e5ddc8ed3..000000000 --- a/web/projects/ui/src/app/modals/enum-list/enum-list.page.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Component, Input } from '@angular/core' -import { ModalController } from '@ionic/angular' -import { ValueSpecListOf } from 'src/app/pkg-config/config-types' - -@Component({ - selector: 'enum-list', - templateUrl: './enum-list.page.html', - styleUrls: ['./enum-list.page.scss'], -}) -export class EnumListPage { - @Input() key!: string - @Input() spec!: ValueSpecListOf<'enum'> - @Input() current: string[] = [] - - options: { [option: string]: boolean } = {} - selectAll = false - - constructor(private readonly modalCtrl: ModalController) {} - - ngOnInit() { - for (let val of this.spec.spec.values || []) { - this.options[val] = this.current.includes(val) - } - // if none are selected, set selectAll to true - this.selectAll = Object.values(this.options).some(k => !k) - } - - dismiss() { - this.modalCtrl.dismiss() - } - - save() { - this.modalCtrl.dismiss( - Object.keys(this.options).filter(key => this.options[key]), - ) - } - - toggleSelectAll() { - Object.keys(this.options).forEach(k => (this.options[k] = this.selectAll)) - this.selectAll = !this.selectAll - } - - toggleSelected(key: string) { - this.options[key] = !this.options[key] - } - - asIsOrder() { - return 0 - } -} diff --git a/web/projects/ui/src/app/modals/generic-form/generic-form.module.ts b/web/projects/ui/src/app/modals/generic-form/generic-form.module.ts deleted file mode 100644 index f278f652b..000000000 --- a/web/projects/ui/src/app/modals/generic-form/generic-form.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { GenericFormPage } from './generic-form.page' -import { FormsModule, ReactiveFormsModule } from '@angular/forms' -import { FormObjectComponentModule } from 'src/app/components/form-object/form-object.component.module' - -@NgModule({ - declarations: [GenericFormPage], - imports: [ - CommonModule, - IonicModule, - FormsModule, - ReactiveFormsModule, - FormObjectComponentModule, - ], - exports: [GenericFormPage], -}) -export class GenericFormPageModule {} diff --git a/web/projects/ui/src/app/modals/generic-form/generic-form.page.html b/web/projects/ui/src/app/modals/generic-form/generic-form.page.html deleted file mode 100644 index 706c8487d..000000000 --- a/web/projects/ui/src/app/modals/generic-form/generic-form.page.html +++ /dev/null @@ -1,35 +0,0 @@ - - - {{ title }} - - - - - - - - - -
- - -
-
- - - - - - {{ button.text }} - - - - diff --git a/web/projects/ui/src/app/modals/generic-form/generic-form.page.scss b/web/projects/ui/src/app/modals/generic-form/generic-form.page.scss deleted file mode 100644 index 0353411b3..000000000 --- a/web/projects/ui/src/app/modals/generic-form/generic-form.page.scss +++ /dev/null @@ -1,9 +0,0 @@ -button:disabled, -button[disabled]{ - border: 1px solid #999999; - background-color: #cccccc; - color: #666666; -} -button { - color: var(--ion-color-primary); -} \ No newline at end of file diff --git a/web/projects/ui/src/app/modals/generic-form/generic-form.page.ts b/web/projects/ui/src/app/modals/generic-form/generic-form.page.ts deleted file mode 100644 index e74c65d47..000000000 --- a/web/projects/ui/src/app/modals/generic-form/generic-form.page.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Component, Input } from '@angular/core' -import { UntypedFormGroup } from '@angular/forms' -import { ModalController } from '@ionic/angular' -import { - convertValuesRecursive, - FormService, -} from 'src/app/services/form.service' -import { ConfigSpec } from 'src/app/pkg-config/config-types' - -export interface ActionButton { - text: string - handler: (value: any) => Promise - isSubmit?: boolean -} - -@Component({ - selector: 'generic-form', - templateUrl: './generic-form.page.html', - styleUrls: ['./generic-form.page.scss'], -}) -export class GenericFormPage { - @Input() title!: string - @Input() spec!: ConfigSpec - @Input() buttons!: ActionButton[] - @Input() initialValue: object = {} - - submitBtn!: ActionButton - formGroup!: UntypedFormGroup - - constructor( - private readonly modalCtrl: ModalController, - private readonly formService: FormService, - ) {} - - ngOnInit() { - this.formGroup = this.formService.createForm(this.spec, this.initialValue) - this.submitBtn = this.buttons.find(btn => btn.isSubmit) || { - text: '', - handler: () => Promise.resolve(true), - } - } - - async dismiss(): Promise { - this.modalCtrl.dismiss() - } - - async handleClick(handler: ActionButton['handler']): Promise { - convertValuesRecursive(this.spec, this.formGroup) - - if (this.formGroup.invalid) { - document - .getElementsByClassName('validation-error')[0] - ?.scrollIntoView({ behavior: 'smooth' }) - return - } - - const success = await handler(this.formGroup.value) - if (success !== false) this.modalCtrl.dismiss() - } -} diff --git a/web/projects/ui/src/app/modals/generic-input/generic-input.component.html b/web/projects/ui/src/app/modals/generic-input/generic-input.component.html deleted file mode 100644 index 8ff7e795f..000000000 --- a/web/projects/ui/src/app/modals/generic-input/generic-input.component.html +++ /dev/null @@ -1,67 +0,0 @@ - -
- - -

{{ options.title }}

-
-

{{ options.message }}

- -
-

- {{ options.warning }} -

-
-
-
- -
-
-

{{ options.label }}

- - - - - - - -

- {{ error }} -

-
- -
- Cancel - - {{ options.buttonText }} - -
-
-
-
diff --git a/web/projects/ui/src/app/modals/generic-input/generic-input.component.module.ts b/web/projects/ui/src/app/modals/generic-input/generic-input.component.module.ts deleted file mode 100644 index d2b1faab4..000000000 --- a/web/projects/ui/src/app/modals/generic-input/generic-input.component.module.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { GenericInputComponent } from './generic-input.component' -import { IonicModule } from '@ionic/angular' -import { RouterModule } from '@angular/router' -import { SharedPipesModule } from '@start9labs/shared' -import { FormsModule } from '@angular/forms' - -@NgModule({ - declarations: [GenericInputComponent], - imports: [ - CommonModule, - IonicModule, - FormsModule, - RouterModule.forChild([]), - SharedPipesModule, - ], - exports: [GenericInputComponent], -}) -export class GenericInputComponentModule {} diff --git a/web/projects/ui/src/app/modals/generic-input/generic-input.component.scss b/web/projects/ui/src/app/modals/generic-input/generic-input.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/projects/ui/src/app/modals/generic-input/generic-input.component.ts b/web/projects/ui/src/app/modals/generic-input/generic-input.component.ts deleted file mode 100644 index 59ebb8c3d..000000000 --- a/web/projects/ui/src/app/modals/generic-input/generic-input.component.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Component, inject, Input, ViewChild } from '@angular/core' -import { ModalController, IonicSafeString, IonInput } from '@ionic/angular' -import { getErrorMessage, THEME } from '@start9labs/shared' -import { MaskPipe } from 'src/app/pipes/mask/mask.pipe' - -@Component({ - selector: 'generic-input', - templateUrl: './generic-input.component.html', - styleUrls: ['./generic-input.component.scss'], - providers: [MaskPipe], -}) -export class GenericInputComponent { - @ViewChild('mainInput') elem?: IonInput - - @Input() options!: GenericInputOptions - - value!: string - masked!: boolean - - maskedValue?: string - - error: string | IonicSafeString = '' - - readonly theme$ = inject(THEME) - - constructor( - private readonly modalCtrl: ModalController, - private readonly mask: MaskPipe, - ) {} - - ngOnInit() { - const defaultOptions: Partial = { - buttonText: 'Submit', - placeholder: 'Enter value', - nullable: false, - useMask: false, - initialValue: '', - } - this.options = { - ...defaultOptions, - ...this.options, - } - - this.masked = !!this.options.useMask - this.value = this.options.initialValue || '' - } - - ngAfterViewInit() { - setTimeout(() => this.elem?.setFocus(), 400) - } - - toggleMask() { - this.masked = !this.masked - } - - cancel() { - this.modalCtrl.dismiss() - } - - transformInput(newValue: string) { - let i = 0 - this.value = newValue - .split('') - .map(x => (x === '●' ? this.value[i++] : x)) - .join('') - this.maskedValue = this.mask.transform(this.value) - } - - async submit() { - const value = this.value.trim() - - if (!value && !this.options.nullable) return - - try { - await this.options.submitFn(value) - this.modalCtrl.dismiss(undefined, 'success') - } catch (e: any) { - this.error = getErrorMessage(e) - } - } -} - -export interface GenericInputOptions { - // required - title: string - message: string - label: string - submitFn: (value: string) => Promise - // optional - warning?: string - buttonText?: string - placeholder?: string - nullable?: boolean - useMask?: boolean - initialValue?: string -} diff --git a/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.module.ts b/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.module.ts deleted file mode 100644 index 096e06add..000000000 --- a/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { MarketplaceSettingsPage } from './marketplace-settings.page' -import { SharedPipesModule } from '@start9labs/shared' -import { StoreIconComponentModule } from 'src/app/components/store-icon/store-icon.component.module' - -@NgModule({ - imports: [ - CommonModule, - IonicModule, - SharedPipesModule, - StoreIconComponentModule, - ], - declarations: [MarketplaceSettingsPage], -}) -export class MarketplaceSettingsPageModule {} diff --git a/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.html b/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.html index 663b022cd..ab3bf8b34 100644 --- a/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.html +++ b/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.html @@ -1,67 +1,30 @@ - - - Change Registry - - - - - - - - - - - Default Registries - +

Default Registries

+ +

Custom Registries

+ +
+ + +
+ diff --git a/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.scss b/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.scss index e69de29bb..310b9ab30 100644 --- a/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.scss +++ b/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.scss @@ -0,0 +1,5 @@ +.connect-container { + display: flex; + flex-direction: row; + align-items: center; +} diff --git a/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.ts b/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.ts index f62331819..dd8fe73eb 100644 --- a/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.ts +++ b/web/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.ts @@ -1,276 +1,230 @@ -import { - ChangeDetectionStrategy, - Component, - Inject, - ViewChild, -} from '@angular/core' -import { - ActionSheetController, - AlertController, - LoadingController, - ModalController, -} from '@ionic/angular' -import { ActionSheetButton } from '@ionic/core' -import { ErrorToastService, sameUrl, toUrl } from '@start9labs/shared' +import { CommonModule } from '@angular/common' +import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { AbstractMarketplaceService } from '@start9labs/marketplace' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ValueSpecObject } from 'src/app/pkg-config/config-types' -import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' +import { + ErrorService, + LoadingService, + sameUrl, + toUrl, +} from '@start9labs/shared' +import { CT } from '@start9labs/start-sdk' +import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core' +import { + TuiButtonModule, + TuiCellModule, + TuiIconModule, + TuiTitleModule, +} from '@taiga-ui/experimental' +import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit' +import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' import { PatchDB } from 'patch-db-client' -import { DataModel, UIStore } from 'src/app/services/patch-db/data-model' -import { MarketplaceService } from 'src/app/services/marketplace.service' +import { combineLatest, filter, firstValueFrom, Subscription } from 'rxjs' import { map } from 'rxjs/operators' -import { combineLatest, firstValueFrom } from 'rxjs' +import { FormComponent } from 'src/app/components/form.component' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { FormDialogService } from 'src/app/services/form-dialog.service' +import { MarketplaceService } from 'src/app/services/marketplace.service' +import { DataModel, UIStore } from 'src/app/services/patch-db/data-model' + +import { MarketplaceRegistryComponent } from './registry.component' @Component({ + standalone: true, + imports: [ + CommonModule, + TuiCellModule, + TuiIconModule, + TuiTitleModule, + TuiButtonModule, + MarketplaceRegistryComponent, + ], selector: 'marketplace-settings', templateUrl: 'marketplace-settings.page.html', styleUrls: ['marketplace-settings.page.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class MarketplaceSettingsPage { - stores$ = combineLatest([ - this.marketplaceService.getKnownHosts$(), - this.marketplaceService.getSelectedHost$(), - ]).pipe( - map(([stores, selected]) => { - const toSlice = stores.map(s => ({ - ...s, - selected: sameUrl(s.url, selected.url), - })) - // 0 and 1 are prod and community - const standard = toSlice.slice(0, 2) - // 2 and beyond are alts - const alt = toSlice.slice(2) - - return { standard, alt } - }), + private readonly api = inject(ApiService) + private readonly loader = inject(LoadingService) + private readonly errorService = inject(ErrorService) + private readonly formDialog = inject(FormDialogService) + private readonly dialogs = inject(TuiDialogService) + private readonly marketplace = inject( + AbstractMarketplaceService, + ) as MarketplaceService + private readonly hosts$ = inject(PatchDB).watch$( + 'ui', + 'marketplace', + 'knownHosts', ) - constructor( - private readonly api: ApiService, - private readonly loadingCtrl: LoadingController, - private readonly modalCtrl: ModalController, - private readonly errToast: ErrorToastService, - private readonly actionCtrl: ActionSheetController, - @Inject(AbstractMarketplaceService) - private readonly marketplaceService: MarketplaceService, - private readonly patch: PatchDB, - private readonly alertCtrl: AlertController, - ) {} + readonly stores$ = combineLatest([ + this.marketplace.getKnownHosts$(), + this.marketplace.getSelectedHost$(), + ]).pipe( + map(([stores, selected]) => + stores.map(s => ({ + ...s, + selected: sameUrl(s.url, selected.url), + })), + ), + // 0 and 1 are prod and community, 2 and beyond are alts + map(stores => ({ standard: stores.slice(0, 2), alt: stores.slice(2) })), + ) - async dismiss() { - this.modalCtrl.dismiss() - } - - async presentModalAdd() { + async add() { const { name, spec } = getMarketplaceValueSpec() - const modal = await this.modalCtrl.create({ - component: GenericFormPage, - componentProps: { - title: name, + + this.formDialog.open(FormComponent, { + label: name, + data: { spec, buttons: [ { text: 'Save for Later', - handler: (value: { url: string }) => { - this.saveOnly(value.url) - }, + handler: async ({ url }: { url: string }) => this.save(url), }, { text: 'Save and Connect', - handler: (value: { url: string }) => { - this.saveAndConnect(value.url) - }, + handler: async ({ url }: { url: string }) => this.save(url, true), isSubmit: true, }, ], }, - cssClass: 'alertlike-modal', }) - - await modal.present() } + delete(url: string, name: string = '') { + this.dialogs + .open(TUI_PROMPT, getPromptOptions(name)) + .pipe(filter(Boolean)) + .subscribe(async () => { + const loader = this.loader.open('Deleting...').subscribe() + const hosts = await firstValueFrom(this.hosts$) + const filtered: { [url: string]: UIStore } = Object.keys(hosts) + .filter(key => !sameUrl(key, url)) + .reduce( + (prev, curr) => ({ + ...prev, + [curr]: hosts[curr], + }), + {}, + ) - async presentAction( - { url, name }: { url: string; name?: string }, - canDelete = false, - ) { - const buttons: ActionSheetButton[] = [ - { - text: 'Connect', - handler: () => { - this.connect(url) - }, - }, - ] - - if (canDelete) { - buttons.unshift({ - text: 'Delete', - role: 'destructive', - handler: () => { - this.presentAlertDelete(url, name!) - }, + try { + await this.api.setDbValue(['marketplace', 'knownHosts'], filtered) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } }) - } - - const action = await this.actionCtrl.create({ - header: name, - mode: 'ios', - buttons, - }) - - await action.present() } - private async presentAlertDelete(url: string, name: string) { - const alert = await this.alertCtrl.create({ - header: 'Confirm', - message: `Are you sure you want to delete ${name}?`, - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Delete', - handler: () => this.delete(url), - cssClass: 'enter-click', - }, - ], - }) - - await alert.present() - } - - private async connect( + async connect( url: string, - loader?: HTMLIonLoadingElement, + loader: Subscription = new Subscription(), ): Promise { - const message = 'Changing Registry...' - if (!loader) { - loader = await this.loadingCtrl.create({ message }) - await loader.present() - } else { - loader.message = message - } + loader.unsubscribe() + loader.closed = false + loader.add(this.loader.open('Changing Registry...').subscribe()) try { - await this.api.setDbValue(['marketplace', 'selected-url'], url) + await this.api.setDbValue(['marketplace', 'selectedUrl'], url) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() - this.dismiss() + loader.unsubscribe() } } - private async saveOnly(rawUrl: string): Promise { - const loader = await this.loadingCtrl.create() + private async save(rawUrl: string, connect = false): Promise { + const loader = this.loader.open('Loading').subscribe() + const url = new URL(rawUrl).toString() try { - const url = new URL(rawUrl).toString() await this.validateAndSave(url, loader) + if (connect) await this.connect(url, loader) + return true } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) + return false } finally { - loader.dismiss() - } - } - - private async saveAndConnect(rawUrl: string): Promise { - const loader = await this.loadingCtrl.create() - - try { - const url = new URL(rawUrl).toString() - await this.validateAndSave(url, loader) - await this.connect(url, loader) - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.dismiss() - this.dismiss() + loader.unsubscribe() } } private async validateAndSave( url: string, - loader: HTMLIonLoadingElement, + loader: Subscription, ): Promise { // Error on duplicates - const hosts = await firstValueFrom( - this.patch.watch$('ui', 'marketplace', 'knownHosts'), - ) + const hosts = await firstValueFrom(this.hosts$) const currentUrls = Object.keys(hosts).map(toUrl) - if (currentUrls.includes(url)) throw new Error('marketplace already added') + if (currentUrls.includes(url)) throw new Error('Marketplace already added') // Validate - loader.message = 'Validating marketplace...' - await loader.present() + loader.unsubscribe() + loader.closed = false + loader.add(this.loader.open('Validating marketplace...').subscribe()) - const { name } = await firstValueFrom( - this.marketplaceService.fetchInfo$(url), - ) + const { name } = await firstValueFrom(this.marketplace.fetchInfo$(url)) // Save - loader.message = 'Saving...' + loader.unsubscribe() + loader.closed = false + loader.add(this.loader.open('Saving...').subscribe()) - await this.api.setDbValue<{ name: string }>( - ['marketplace', 'knownHosts', url], - { name }, - ) - } - - private async delete(url: string): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Deleting...', - }) - await loader.present() - - const hosts = await firstValueFrom( - this.patch.watch$('ui', 'marketplace', 'knownHosts'), - ) - - const filtered: { [url: string]: UIStore } = Object.keys(hosts) - .filter(key => !sameUrl(key, url)) - .reduce((prev, curr) => { - const name = hosts[curr] - return { - ...prev, - [curr]: name, - } - }, {}) - - try { - await this.api.setDbValue<{ [url: string]: UIStore }>( - ['marketplace', 'knownHosts'], - filtered, - ) - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.dismiss() - } + await this.api.setDbValue(['marketplace', 'knownHosts', url], { name }) } } -function getMarketplaceValueSpec(): ValueSpecObject { +export const MARKETPLACE_REGISTRY = new PolymorpheusComponent( + MarketplaceSettingsPage, +) + +function getMarketplaceValueSpec(): CT.ValueSpecObject { return { type: 'object', name: 'Add Custom Registry', + description: null, + warning: null, spec: { url: { - type: 'string', + type: 'text', name: 'URL', description: 'A fully-qualified URL of the custom registry', - nullable: false, + inputmode: 'url', + required: true, masked: false, - copyable: false, - pattern: `https?:\/\/[a-zA-Z0-9][a-zA-Z0-9-\.]+[a-zA-Z0-9]\.[^\s]{2,}`, - 'pattern-description': 'Must be a valid URL', + minLength: null, + maxLength: null, + patterns: [ + { + regex: `https?:\/\/[a-zA-Z0-9][a-zA-Z0-9-\.]+[a-zA-Z0-9]\.[^\s]{2,}`, + description: 'Must be a valid URL', + }, + ], placeholder: 'e.g. https://example.org', + default: null, + warning: null, + disabled: false, + immutable: false, + generate: null, }, }, } } + +function getPromptOptions( + name: string, +): Partial> { + return { + label: 'Confirm', + size: 's', + data: { + content: `Are you sure you want to delete ${name}?`, + yes: 'Delete', + no: 'Cancel', + }, + } +} diff --git a/web/projects/ui/src/app/modals/marketplace-settings/registry.component.ts b/web/projects/ui/src/app/modals/marketplace-settings/registry.component.ts new file mode 100644 index 000000000..0441ae18f --- /dev/null +++ b/web/projects/ui/src/app/modals/marketplace-settings/registry.component.ts @@ -0,0 +1,42 @@ +import { NgIf } from '@angular/common' +import { + ChangeDetectionStrategy, + Component, + inject, + Input, +} from '@angular/core' +import { TuiIconModule, TuiTitleModule } from '@taiga-ui/experimental' +import { ConfigService } from 'src/app/services/config.service' + +import { StoreIconComponent } from './store-icon.component' + +@Component({ + standalone: true, + selector: '[registry]', + template: ` + +
+ {{ registry.name }} +
{{ registry.url }}
+
+ + + `, + styles: [':host { border-radius: 0.25rem; width: stretch; }'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [NgIf, StoreIconComponent, TuiIconModule, TuiTitleModule], +}) +export class MarketplaceRegistryComponent { + readonly marketplace = inject(ConfigService).marketplace + + @Input() + registry!: { url: string; selected: boolean; name?: string } +} diff --git a/web/projects/ui/src/app/modals/marketplace-settings/store-icon.component.ts b/web/projects/ui/src/app/modals/marketplace-settings/store-icon.component.ts new file mode 100644 index 000000000..dcdfe3b2f --- /dev/null +++ b/web/projects/ui/src/app/modals/marketplace-settings/store-icon.component.ts @@ -0,0 +1,46 @@ +import { NgIf } from '@angular/common' +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { sameUrl } from '@start9labs/shared' + +@Component({ + standalone: true, + selector: 'store-icon', + template: ` + Marketplace Icon + + Marketplace Icon + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [NgIf], +}) +export class StoreIconComponent { + @Input() + url = '' + @Input() + size?: string + @Input() + marketplace!: any + + get icon() { + const { start9, community } = this.marketplace + + if (sameUrl(this.url, start9)) { + return 'assets/img/icon_transparent.png' + } else if (sameUrl(this.url, community)) { + return 'assets/img/community-store.png' + } + return null + } +} diff --git a/web/projects/ui/src/app/modals/os-update/os-update.page.ts b/web/projects/ui/src/app/modals/os-update/os-update.page.ts index 9900bbb47..45b9cb009 100644 --- a/web/projects/ui/src/app/modals/os-update/os-update.page.ts +++ b/web/projects/ui/src/app/modals/os-update/os-update.page.ts @@ -1,8 +1,8 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' -import { LoadingController, ModalController } from '@ionic/angular' -import { ApiService } from '../../services/api/embassy-api.service' -import { ErrorToastService } from '@start9labs/shared' +import { ModalController } from '@ionic/angular' +import { ErrorService, LoadingService } from '@start9labs/shared' import { EOSService } from 'src/app/services/eos.service' +import { ApiService } from '../../services/api/embassy-api.service' @Component({ selector: 'os-update', @@ -15,8 +15,8 @@ export class OSUpdatePage { constructor( private readonly modalCtrl: ModalController, - private readonly loadingCtrl: LoadingController, - private readonly errToast: ErrorToastService, + private readonly loader: LoadingService, + private readonly errorService: ErrorService, private readonly embassyApi: ApiService, private readonly eosService: EOSService, ) {} @@ -39,18 +39,15 @@ export class OSUpdatePage { } async updateEOS() { - const loader = await this.loadingCtrl.create({ - message: 'Beginning update...', - }) - await loader.present() + const loader = this.loader.open('Beginning update...').subscribe() try { await this.embassyApi.updateServer() this.dismiss() } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } diff --git a/web/projects/ui/src/app/modals/prompt.component.ts b/web/projects/ui/src/app/modals/prompt.component.ts new file mode 100644 index 000000000..e2a2765f5 --- /dev/null +++ b/web/projects/ui/src/app/modals/prompt.component.ts @@ -0,0 +1,123 @@ +import { CommonModule } from '@angular/common' +import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' +import { FormsModule } from '@angular/forms' +import { TuiAutoFocusModule } from '@taiga-ui/cdk' +import { TuiDialogContext, TuiTextfieldControllerModule } from '@taiga-ui/core' +import { TuiButtonModule } from '@taiga-ui/experimental' +import { TuiInputModule } from '@taiga-ui/kit' +import { + POLYMORPHEUS_CONTEXT, + PolymorpheusComponent, +} from '@tinkoff/ng-polymorpheus' + +@Component({ + standalone: true, + template: ` +

{{ options.message }}

+

{{ options.warning }}

+
+ + {{ options.label }} + * + + +
+ + +
+
+ + + + + `, + styles: [ + ` + .warning { + color: var(--tui-warning-fill); + } + + .button { + pointer-events: auto; + margin-left: 0.25rem; + } + + .masked { + -webkit-text-security: disc; + } + `, + ], + imports: [ + CommonModule, + FormsModule, + TuiInputModule, + TuiButtonModule, + TuiTextfieldControllerModule, + TuiAutoFocusModule, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PromptModal { + masked = this.options.useMask + value = this.options.initialValue || '' + + constructor( + @Inject(POLYMORPHEUS_CONTEXT) + private readonly context: TuiDialogContext, + ) {} + + get options(): PromptOptions { + return this.context.data + } + + cancel() { + this.context.$implicit.complete() + } + + submit(value: string) { + if (value || !this.options.required) { + this.context.$implicit.next(value) + } + } +} + +export const PROMPT = new PolymorpheusComponent(PromptModal) + +export interface PromptOptions { + message: string + label?: string + warning?: string + buttonText?: string + placeholder?: string + required?: boolean + useMask?: boolean + initialValue?: string | null +} diff --git a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.module.ts b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.module.ts index 5f30abc0e..84e52d15f 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.module.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.module.ts @@ -5,7 +5,6 @@ import { IonicModule } from '@ionic/angular' import { AppActionsPage, AppActionsItemComponent } from './app-actions.page' import { QRComponentModule } from 'src/app/components/qr/qr.component.module' import { SharedPipesModule } from '@start9labs/shared' -import { GenericFormPageModule } from 'src/app/modals/generic-form/generic-form.module' import { ActionSuccessPageModule } from 'src/app/modals/action-success/action-success.module' const routes: Routes = [ @@ -22,7 +21,6 @@ const routes: Routes = [ RouterModule.forChild(routes), QRComponentModule, SharedPipesModule, - GenericFormPageModule, ActionSuccessPageModule, ], declarations: [AppActionsPage, AppActionsItemComponent], diff --git a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts index fc008a17a..dd4e4b36e 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts @@ -1,23 +1,24 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ActivatedRoute } from '@angular/router' -import { ApiService } from 'src/app/services/api/embassy-api.service' +import { AlertController, ModalController, NavController } from '@ionic/angular' import { - AlertController, - LoadingController, - ModalController, - NavController, -} from '@ionic/angular' + ErrorService, + getPkgId, + isEmptyObject, + LoadingService, +} from '@start9labs/shared' +import { T } from '@start9labs/start-sdk' import { PatchDB } from 'patch-db-client' +import { FormComponent } from 'src/app/components/form.component' +import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { FormDialogService } from 'src/app/services/form-dialog.service' import { DataModel, PackageDataEntry, } from 'src/app/services/patch-db/data-model' -import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' -import { isEmptyObject, ErrorToastService, getPkgId } from '@start9labs/shared' -import { ActionSuccessPage } from 'src/app/modals/action-success/action-success.page' -import { hasCurrentDeps } from 'src/app/util/has-deps' import { getAllPackages, getManifest } from 'src/app/util/get-package-data' -import { T } from '@start9labs/start-sdk' +import { hasCurrentDeps } from 'src/app/util/has-deps' @Component({ selector: 'app-actions', @@ -34,10 +35,11 @@ export class AppActionsPage { private readonly embassyApi: ApiService, private readonly modalCtrl: ModalController, private readonly alertCtrl: AlertController, - private readonly errToast: ErrorToastService, - private readonly loadingCtrl: LoadingController, + private readonly errorService: ErrorService, + private readonly loader: LoadingService, private readonly navCtrl: NavController, private readonly patch: PatchDB, + private readonly formDialog: FormDialogService, ) {} async handleAction( @@ -46,23 +48,19 @@ export class AppActionsPage { ) { if (status && action.value.allowedStatuses.includes(status.main.status)) { if (!isEmptyObject(action.value.input || {})) { - const modal = await this.modalCtrl.create({ - component: GenericFormPage, - componentProps: { - title: action.value.name, + this.formDialog.open(FormComponent, { + label: action.value.name, + data: { spec: action.value.input, buttons: [ { text: 'Execute', - handler: (value: any) => { - return this.executeAction(action.key, value) - }, - isSubmit: true, + handler: async (value: any) => + this.executeAction(action.key, value), }, ], }, }) - await modal.present() } else { const alert = await this.alertCtrl.create({ header: 'Confirm', @@ -147,10 +145,7 @@ export class AppActionsPage { } private async uninstall() { - const loader = await this.loadingCtrl.create({ - message: `Beginning uninstall...`, - }) - await loader.present() + const loader = this.loader.open(`Beginning uninstall...`).subscribe() try { await this.embassyApi.uninstallPackage({ id: this.pkgId }) @@ -159,9 +154,9 @@ export class AppActionsPage { .catch(e => console.error('Failed to mark instructions as unseen', e)) this.navCtrl.navigateRoot('/services') } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } @@ -169,10 +164,7 @@ export class AppActionsPage { actionId: string, input?: object, ): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Executing action...', - }) - await loader.present() + const loader = this.loader.open('Executing action...').subscribe() try { const res = await this.embassyApi.executePackageAction({ @@ -191,10 +183,10 @@ export class AppActionsPage { setTimeout(() => successModal.present(), 500) return true // needed to dismiss original modal/alert } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) return false // don't dismiss original modal/alert } finally { - loader.dismiss() + loader.unsubscribe() } } diff --git a/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html b/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html index 79b1a1fde..ab7940a06 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-interfaces/app-interfaces.page.html @@ -8,7 +8,9 @@ - + User Interfaces { - const sorted = Object.values(interfaces) - .sort(iface => - iface.name.toLowerCase() > iface.name.toLowerCase() ? -1 : 1, - ) - .map(iface => { - // TODO @Matt - const host = {} as any - return { - ...iface, - addresses: getAddresses(iface, host), - } - }) + private readonly serviceInterfaces$ = this.patch.watch$( + 'packageData', + this.pkgId, + 'serviceInterfaces', + ) + private readonly hosts$ = this.patch.watch$( + 'packageData', + this.pkgId, + 'hosts', + ) - return { - ui: sorted.filter(val => val.type === 'ui'), - api: sorted.filter(val => val.type === 'api'), - p2p: sorted.filter(val => val.type === 'p2p'), - } - }), - ) + readonly serviceInterfacesWithHostInfo$ = combineLatest([ + this.serviceInterfaces$, + this.hosts$, + ]).pipe( + map(([interfaces, hosts]) => { + const sorted = Object.values(interfaces) + .sort(iface => + iface.name.toLowerCase() > iface.name.toLowerCase() ? -1 : 1, + ) + .map(iface => { + return { + ...iface, + addresses: getAddresses( + iface, + hosts[iface.addressInfo.hostId] || {}, + ), + } + }) + + return { + ui: sorted.filter(val => val.type === 'ui'), + api: sorted.filter(val => val.type === 'api'), + p2p: sorted.filter(val => val.type === 'p2p'), + } + }), + ) constructor( private readonly route: ActivatedRoute, @@ -108,20 +120,15 @@ function getAddresses( host: T.Host, ): MappedAddress[] { const addressInfo = serviceInterface.addressInfo - const username = addressInfo.username ? addressInfo.username + '@' : '' - const suffix = addressInfo.suffix || '' const hostnames = - host.kind === 'multi' ? host.hostnameInfo[addressInfo.internalPort] : [] // TODO: non-multi - /* host.hostname - ? [host.hostname] - : [] */ + host.kind === 'multi' ? host.hostnameInfo[addressInfo.internalPort] : [] - return hostnames.flatMap(h => { + const addressesWithNames = hostnames.flatMap(h => { let name = '' if (h.kind === 'onion') { - name = 'Tor' + name = `Tor` } else { const hostnameKind = h.hostname.kind @@ -135,9 +142,23 @@ function getAddresses( } } - return addressHostToUrl(addressInfo, h).map(url => ({ - name, - url, - })) + const addresses = utils.addressHostToUrl(addressInfo, h) + if (addresses.length > 1) { + return utils.addressHostToUrl(addressInfo, h).map(url => ({ + name: `${name} (${new URL(url).protocol + .replace(':', '') + .toUpperCase()})`, + url, + })) + } else { + return utils.addressHostToUrl(addressInfo, h).map(url => ({ + name, + url, + })) + } }) + + return addressesWithNames.filter( + (value, index, self) => index === self.findIndex(t => t.url === value.url), + ) } diff --git a/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts index 8cf576bd7..61b17b669 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-metrics/app-metrics.page.ts @@ -1,8 +1,8 @@ import { Component } from '@angular/core' import { ActivatedRoute } from '@angular/router' +import { ErrorService, getPkgId, pauseFor } from '@start9labs/shared' import { Metric } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { pauseFor, ErrorToastService, getPkgId } from '@start9labs/shared' @Component({ selector: 'app-metrics', @@ -17,7 +17,7 @@ export class AppMetricsPage { constructor( private readonly route: ActivatedRoute, - private readonly errToast: ErrorToastService, + private readonly errorService: ErrorService, private readonly embassyApi: ApiService, ) {} @@ -46,7 +46,7 @@ export class AppMetricsPage { try { this.metrics = await this.embassyApi.getPkgMetrics({ id: this.pkgId }) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) this.stopDaemon() } finally { this.loading = false diff --git a/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts index d9b6bbc3e..af659c32b 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-properties/app-properties.page.ts @@ -1,6 +1,5 @@ import { Component, ViewChild } from '@angular/core' import { ActivatedRoute } from '@angular/router' -import { ApiService } from 'src/app/services/api/embassy-api.service' import { AlertController, IonBackButtonDelegate, @@ -8,18 +7,15 @@ import { NavController, ToastController, } from '@ionic/angular' -import { PackageProperties } from 'src/app/util/properties.util' -import { QRComponent } from 'src/app/components/qr/qr.component' -import { PatchDB } from 'patch-db-client' -import { DataModel } from 'src/app/services/patch-db/data-model' -import { - ErrorToastService, - getPkgId, - copyToClipboard, -} from '@start9labs/shared' +import { copyToClipboard, ErrorService, getPkgId } from '@start9labs/shared' import { TuiDestroyService } from '@taiga-ui/cdk' import { getValueByPointer } from 'fast-json-patch' +import { PatchDB } from 'patch-db-client' import { map, takeUntil } from 'rxjs/operators' +import { QRComponent } from 'src/app/components/qr/qr.component' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { DataModel } from 'src/app/services/patch-db/data-model' +import { PackageProperties } from 'src/app/util/properties.util' @Component({ selector: 'app-properties', @@ -47,7 +43,7 @@ export class AppPropertiesPage { constructor( private readonly route: ActivatedRoute, private readonly embassyApi: ApiService, - private readonly errToast: ErrorToastService, + private readonly errorService: ErrorService, private readonly alertCtrl: AlertController, private readonly toastCtrl: ToastController, private readonly modalCtrl: ModalController, @@ -139,7 +135,7 @@ export class AppPropertiesPage { }) this.node = getValueByPointer(this.properties, this.pointer) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { this.loading = false } diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts index 0b2d82763..86744cc91 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts @@ -4,13 +4,11 @@ import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' import { AppShowPage } from './app-show.page' import { - EmptyPipe, EmverPipesModule, ResponsiveColModule, SharedPipesModule, } from '@start9labs/shared' import { StatusComponentModule } from 'src/app/components/status/status.component.module' -import { AppConfigPageModule } from 'src/app/modals/app-config/app-config.module' import { LaunchablePipeModule } from 'src/app/pipes/launchable/launchable.module' import { UiPipeModule } from 'src/app/pipes/ui/ui.module' import { AppShowHeaderComponent } from './components/app-show-header/app-show-header.component' @@ -51,7 +49,6 @@ const routes: Routes = [ InstallingProgressPipeModule, IonicModule, RouterModule.forChild(routes), - AppConfigPageModule, EmverPipesModule, LaunchablePipeModule, UiPipeModule, diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts index 010a7473c..cb1f6f5f9 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts @@ -11,7 +11,6 @@ import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service' import { map, tap } from 'rxjs/operators' import { ActivatedRoute, NavigationExtras } from '@angular/router' import { getPkgId } from '@start9labs/shared' -import { ModalService } from 'src/app/services/modal.service' import { DependentInfo } from 'src/app/types/dependent-info' import { DepErrorService, @@ -26,6 +25,8 @@ import { isUpdating, } from 'src/app/util/get-package-data' import { T } from '@start9labs/start-sdk' +import { FormDialogService } from 'src/app/services/form-dialog.service' +import { ConfigModal, PackageConfigData } from 'src/app/modals/config.component' export interface DependencyInfo { id: string @@ -68,8 +69,8 @@ export class AppShowPage { private readonly route: ActivatedRoute, private readonly navCtrl: NavController, private readonly patch: PatchDB, - private readonly modalService: ModalService, private readonly depErrorService: DepErrorService, + private readonly formDialog: FormDialogService, ) {} showProgress( @@ -171,7 +172,13 @@ export class AppShowPage { case 'update': return this.installDep(pkg, pkgManifest, id) case 'configure': - return this.configureDep(pkgManifest, id) + return this.formDialog.open(ConfigModal, { + label: `${pkgManifest.title} config`, + data: { + pkgId: id, + dependentInfo: pkgManifest, + }, + }) } } @@ -194,19 +201,4 @@ export class AppShowPage { navigationExtras, ) } - - private async configureDep( - pkgManifest: T.Manifest, - dependencyId: string, - ): Promise { - const dependentInfo: DependentInfo = { - id: pkgManifest.id, - title: pkgManifest.title, - } - - await this.modalService.presentModalConfig({ - pkgId: dependencyId, - dependentInfo, - }) - } } diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.html index 8e4bb760d..b0d65b5fd 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-health-checks/app-show-health-checks.component.html @@ -68,7 +68,7 @@ - + diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts index ba60c331d..30657ae01 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts @@ -1,26 +1,27 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' -import { UiLauncherService } from 'src/app/services/ui-launcher.service' -import { - PackageStatus, - PrimaryRendering, -} from 'src/app/services/pkg-status-rendering.service' +import { AlertController } from '@ionic/angular' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { T } from '@start9labs/start-sdk' +import { PatchDB } from 'patch-db-client' +import { ConfigModal, PackageConfigData } from 'src/app/modals/config.component' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { ConnectionService } from 'src/app/services/connection.service' +import { FormDialogService } from 'src/app/services/form-dialog.service' import { DataModel, PackageDataEntry, } from 'src/app/services/patch-db/data-model' -import { ErrorToastService } from '@start9labs/shared' -import { AlertController, LoadingController } from '@ionic/angular' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ModalService } from 'src/app/services/modal.service' -import { hasCurrentDeps } from 'src/app/util/has-deps' -import { ConnectionService } from 'src/app/services/connection.service' import { - isInstalled, - getManifest, + PackageStatus, + PrimaryRendering, +} from 'src/app/services/pkg-status-rendering.service' +import { UiLauncherService } from 'src/app/services/ui-launcher.service' +import { getAllPackages, + getManifest, + isInstalled, } from 'src/app/util/get-package-data' -import { PatchDB } from 'patch-db-client' -import { T } from '@start9labs/start-sdk' +import { hasCurrentDeps } from 'src/app/util/has-deps' @Component({ selector: 'app-show-status', @@ -41,11 +42,11 @@ export class AppShowStatusComponent { constructor( private readonly alertCtrl: AlertController, - private readonly errToast: ErrorToastService, - private readonly loadingCtrl: LoadingController, + private readonly errorService: ErrorService, + private readonly loader: LoadingService, private readonly embassyApi: ApiService, private readonly launcherService: UiLauncherService, - private readonly modalService: ModalService, + private readonly formDialog: FormDialogService, readonly connection$: ConnectionService, private readonly patch: PatchDB, ) {} @@ -85,8 +86,8 @@ export class AppShowStatusComponent { } async presentModalConfig(): Promise { - return this.modalService.presentModalConfig({ - pkgId: this.manifest.id, + return this.formDialog.open(ConfigModal, { + data: { pkgId: this.manifest.id }, }) } @@ -172,47 +173,38 @@ export class AppShowStatusComponent { } private async start(): Promise { - const loader = await this.loadingCtrl.create({ - message: `Starting...`, - }) - await loader.present() + const loader = this.loader.open(`Starting...`).subscribe() try { await this.embassyApi.startPackage({ id: this.manifest.id }) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } private async stop(): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Stopping...', - }) - await loader.present() + const loader = this.loader.open('Stopping...').subscribe() try { await this.embassyApi.stopPackage({ id: this.manifest.id }) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } private async restart(): Promise { - const loader = await this.loadingCtrl.create({ - message: `Restarting...`, - }) - await loader.present() + const loader = this.loader.open(`Restarting...`).subscribe() try { await this.embassyApi.restartPackage({ id: this.manifest.id }) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } private async presentAlertStart(message: string): Promise { diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts index 692d5d55f..e2a03ecf8 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/pipes/to-buttons.pipe.ts @@ -7,11 +7,12 @@ import { InstalledState, PackageDataEntry, } from 'src/app/services/patch-db/data-model' -import { ModalService } from 'src/app/services/modal.service' import { ApiService } from 'src/app/services/api/embassy-api.service' import { from, map, Observable } from 'rxjs' import { PatchDB } from 'patch-db-client' import { T } from '@start9labs/start-sdk' +import { FormDialogService } from 'src/app/services/form-dialog.service' +import { ConfigModal, PackageConfigData } from 'src/app/modals/config.component' export interface Button { title: string @@ -30,9 +31,9 @@ export class ToButtonsPipe implements PipeTransform { private readonly route: ActivatedRoute, private readonly navCtrl: NavController, private readonly modalCtrl: ModalController, - private readonly modalService: ModalService, private readonly apiService: ApiService, private readonly patch: PatchDB, + private readonly formDialog: FormDialogService, ) {} transform(pkg: PackageDataEntry): Button[] { @@ -52,7 +53,10 @@ export class ToButtonsPipe implements PipeTransform { // config { action: async () => - this.modalService.presentModalConfig({ pkgId: manifest.id }), + this.formDialog.open(ConfigModal, { + label: `${manifest.title} configuration`, + data: { pkgId: manifest.id }, + }), title: 'Config', description: `Customize ${manifest.title}`, icon: 'options-outline', diff --git a/web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.ts b/web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.ts index 9bb7376bc..00ee305bc 100644 --- a/web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.ts +++ b/web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.ts @@ -1,5 +1,6 @@ import { Component } from '@angular/core' -import { AlertController, LoadingController } from '@ionic/angular' +import { AlertController } from '@ionic/angular' +import { LoadingService } from '@start9labs/shared' import { ApiService } from 'src/app/services/api/embassy-api.service' @Component({ @@ -18,7 +19,7 @@ export class HomePage { restarted = false constructor( - private readonly loadingCtrl: LoadingController, + private readonly loader: LoadingService, private readonly api: ApiService, private readonly alertCtrl: AlertController, ) {} @@ -86,10 +87,7 @@ export class HomePage { } async restart(): Promise { - const loader = await this.loadingCtrl.create({ - cssClass: 'loader', - }) - await loader.present() + const loader = this.loader.open('Loading...').subscribe() try { await this.api.diagnosticRestart() @@ -97,15 +95,12 @@ export class HomePage { } catch (e) { console.error(e) } finally { - loader.dismiss() + loader.unsubscribe() } } async forgetDrive(): Promise { - const loader = await this.loadingCtrl.create({ - cssClass: 'loader', - }) - await loader.present() + const loader = this.loader.open('Loading...').subscribe() try { await this.api.diagnosticForgetDrive() @@ -114,7 +109,7 @@ export class HomePage { } catch (e) { console.error(e) } finally { - loader.dismiss() + loader.unsubscribe() } } @@ -149,10 +144,7 @@ export class HomePage { } private async repairDisk(): Promise { - const loader = await this.loadingCtrl.create({ - cssClass: 'loader', - }) - await loader.present() + const loader = this.loader.open('Loading...').subscribe() try { await this.api.diagnosticRepairDisk() @@ -161,7 +153,7 @@ export class HomePage { } catch (e) { console.error(e) } finally { - loader.dismiss() + loader.unsubscribe() } } } diff --git a/web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.page.ts b/web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.page.ts index 5119b9d93..5f98627cb 100644 --- a/web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.page.ts +++ b/web/projects/ui/src/app/pages/diagnostic-routes/logs/logs.page.ts @@ -1,7 +1,7 @@ import { Component, ViewChild } from '@angular/core' import { IonContent } from '@ionic/angular' +import { ErrorService, toLocalIsoString } from '@start9labs/shared' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ErrorToastService, toLocalIsoString } from '@start9labs/shared' var Convert = require('ansi-to-html') var convert = new Convert({ @@ -23,7 +23,7 @@ export class LogsPage { constructor( private readonly api: ApiService, - private readonly errToast: ErrorToastService, + private readonly errorService: ErrorService, ) {} async ngOnInit() { @@ -89,7 +89,7 @@ export class LogsPage { this.needInfinite = false } } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } } } diff --git a/web/projects/ui/src/app/pages/init/init.service.ts b/web/projects/ui/src/app/pages/init/init.service.ts index 7102c9db5..245e1ae24 100644 --- a/web/projects/ui/src/app/pages/init/init.service.ts +++ b/web/projects/ui/src/app/pages/init/init.service.ts @@ -1,5 +1,5 @@ import { inject, Injectable } from '@angular/core' -import { ErrorToastService } from '@start9labs/shared' +import { ErrorService } from '@start9labs/shared' import { T } from '@start9labs/start-sdk' import { catchError, @@ -24,7 +24,7 @@ interface MappedProgress { export class InitService extends Observable { private readonly state = inject(StateService) private readonly api = inject(ApiService) - private readonly errorService = inject(ErrorToastService) + private readonly errorService = inject(ErrorService) private readonly progress$ = defer(() => from(this.api.initGetProgress()), ).pipe( @@ -59,7 +59,7 @@ export class InitService extends Observable { }), catchError(e => { // @TODO this toast is presenting when we navigate away from init page. It seems other websockets exhibit the same behavior, but we never noticed because the error were not being caught and presented in this manner - this.errorService.present(e) + this.errorService.handleError(e) return EMPTY }), diff --git a/web/projects/ui/src/app/pages/login/login.page.ts b/web/projects/ui/src/app/pages/login/login.page.ts index db0d1c2c4..065a4cc7f 100644 --- a/web/projects/ui/src/app/pages/login/login.page.ts +++ b/web/projects/ui/src/app/pages/login/login.page.ts @@ -1,10 +1,11 @@ +import { DOCUMENT } from '@angular/common' import { Component, Inject } from '@angular/core' -import { getPlatforms, LoadingController } from '@ionic/angular' +import { Router } from '@angular/router' +import { getPlatforms } from '@ionic/angular' +import { LoadingService } from '@start9labs/shared' import { ApiService } from 'src/app/services/api/embassy-api.service' import { AuthService } from 'src/app/services/auth.service' -import { Router } from '@angular/router' import { ConfigService } from 'src/app/services/config.service' -import { DOCUMENT } from '@angular/common' @Component({ selector: 'login', @@ -19,7 +20,7 @@ export class LoginPage { constructor( private readonly router: Router, private readonly authService: AuthService, - private readonly loadingCtrl: LoadingController, + private readonly loader: LoadingService, private readonly api: ApiService, public readonly config: ConfigService, @Inject(DOCUMENT) public readonly document: Document, @@ -28,10 +29,7 @@ export class LoginPage { async submit() { this.error = '' - const loader = await this.loadingCtrl.create({ - message: 'Logging in...', - }) - await loader.present() + const loader = this.loader.open('Logging in...').subscribe() try { document.cookie = '' @@ -51,7 +49,7 @@ export class LoginPage { // code 7 is for incorrect password this.error = e.code === 7 ? 'Invalid Password' : e.message } finally { - loader.dismiss() + loader.unsubscribe() } } } diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts index ea1952578..c10ce42aa 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts @@ -18,7 +18,6 @@ import { import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module' import { MarketplaceListPage } from './marketplace-list.page' -import { MarketplaceSettingsPageModule } from 'src/app/modals/marketplace-settings/marketplace-settings.module' import { StoreIconComponentModule } from 'src/app/components/store-icon/store-icon.component.module' const routes: Routes = [ @@ -43,7 +42,6 @@ const routes: Routes = [ CategoriesModule, SearchModule, SkeletonModule, - MarketplaceSettingsPageModule, StoreIconComponentModule, ResponsiveColModule, ], diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts index 6792b917a..e1f13883a 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts @@ -1,10 +1,10 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' import { ActivatedRoute } from '@angular/router' -import { ModalController } from '@ionic/angular' import { AbstractMarketplaceService } from '@start9labs/marketplace' +import { TuiDialogService } from '@taiga-ui/core' import { PatchDB } from 'patch-db-client' import { map } from 'rxjs' -import { MarketplaceSettingsPage } from 'src/app/modals/marketplace-settings/marketplace-settings.page' +import { MARKETPLACE_REGISTRY } from 'src/app/modals/marketplace-settings/marketplace-settings.page' import { ConfigService } from 'src/app/services/config.service' import { MarketplaceService } from 'src/app/services/marketplace.service' import { DataModel } from 'src/app/services/patch-db/data-model' @@ -73,7 +73,7 @@ export class MarketplaceListPage { private readonly patch: PatchDB, @Inject(AbstractMarketplaceService) private readonly marketplaceService: MarketplaceService, - private readonly modalCtrl: ModalController, + private readonly dialogs: TuiDialogService, private readonly config: ConfigService, private readonly route: ActivatedRoute, ) {} @@ -82,10 +82,11 @@ export class MarketplaceListPage { query = '' async presentModalMarketplaceSettings() { - const modal = await this.modalCtrl.create({ - component: MarketplaceSettingsPage, - }) - await modal.present() + this.dialogs + .open(MARKETPLACE_REGISTRY, { + label: 'Change Registry', + }) + .subscribe() } onCategoryChange(category: string): void { diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts index 4e44e2af3..5c83c536f 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts @@ -4,28 +4,29 @@ import { Inject, Input, } from '@angular/core' -import { AlertController, LoadingController } from '@ionic/angular' +import { AlertController } from '@ionic/angular' import { AbstractMarketplaceService, MarketplacePkg, } from '@start9labs/marketplace' import { Emver, - ErrorToastService, + ErrorService, isEmptyObject, + LoadingService, sameUrl, } from '@start9labs/shared' +import { PatchDB } from 'patch-db-client' +import { firstValueFrom } from 'rxjs' +import { ClientStorageService } from 'src/app/services/client-storage.service' +import { MarketplaceService } from 'src/app/services/marketplace.service' import { DataModel, PackageDataEntry, } from 'src/app/services/patch-db/data-model' -import { ClientStorageService } from 'src/app/services/client-storage.service' -import { MarketplaceService } from 'src/app/services/marketplace.service' -import { hasCurrentDeps } from 'src/app/util/has-deps' -import { PatchDB } from 'patch-db-client' -import { getAllPackages, getManifest } from 'src/app/util/get-package-data' -import { firstValueFrom } from 'rxjs' import { dryUpdate } from 'src/app/util/dry-update' +import { getAllPackages, getManifest } from 'src/app/util/get-package-data' +import { hasCurrentDeps } from 'src/app/util/has-deps' @Component({ selector: 'marketplace-show-controls', @@ -50,9 +51,9 @@ export class MarketplaceShowControlsComponent { private readonly ClientStorageService: ClientStorageService, @Inject(AbstractMarketplaceService) private readonly marketplaceService: MarketplaceService, - private readonly loadingCtrl: LoadingController, + private readonly loader: LoadingService, private readonly emver: Emver, - private readonly errToast: ErrorToastService, + private readonly errorService: ErrorService, private readonly patch: PatchDB, ) {} @@ -176,19 +177,16 @@ export class MarketplaceShowControlsComponent { } private async install(url: string) { - const loader = await this.loadingCtrl.create({ - message: 'Beginning Install...', - }) - await loader.present() + const loader = this.loader.open('Beginning Install...').subscribe() const { id, version } = this.pkg.manifest try { await this.marketplaceService.installPackage(id, version, url) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } diff --git a/web/projects/ui/src/app/pages/notifications/notifications.page.ts b/web/projects/ui/src/app/pages/notifications/notifications.page.ts index 933c50e67..69bad6c29 100644 --- a/web/projects/ui/src/app/pages/notifications/notifications.page.ts +++ b/web/projects/ui/src/app/pages/notifications/notifications.page.ts @@ -1,21 +1,17 @@ import { Component } from '@angular/core' -import { ApiService } from 'src/app/services/api/embassy-api.service' +import { ActivatedRoute } from '@angular/router' +import { AlertController, ModalController } from '@ionic/angular' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { PatchDB } from 'patch-db-client' +import { first } from 'rxjs' +import { BackupReportPage } from 'src/app/modals/backup-report/backup-report.page' import { - ServerNotifications, NotificationLevel, ServerNotification, + ServerNotifications, } from 'src/app/services/api/api.types' -import { - AlertController, - LoadingController, - ModalController, -} from '@ionic/angular' -import { ActivatedRoute } from '@angular/router' -import { ErrorToastService } from '@start9labs/shared' -import { BackupReportPage } from 'src/app/modals/backup-report/backup-report.page' -import { PatchDB } from 'patch-db-client' +import { ApiService } from 'src/app/services/api/embassy-api.service' import { DataModel } from 'src/app/services/patch-db/data-model' -import { first } from 'rxjs' @Component({ selector: 'notifications', @@ -34,9 +30,9 @@ export class NotificationsPage { constructor( private readonly embassyApi: ApiService, private readonly alertCtrl: AlertController, - private readonly loadingCtrl: LoadingController, + private readonly loader: LoadingService, private readonly modalCtrl: ModalController, - private readonly errToast: ErrorToastService, + private readonly errorService: ErrorService, private readonly route: ActivatedRoute, private readonly patch: PatchDB, ) {} @@ -66,26 +62,23 @@ export class NotificationsPage { return notifications } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } return [] } async delete(id: number, index: number): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Deleting...', - }) - await loader.present() + const loader = this.loader.open('Deleting...').subscribe() try { await this.embassyApi.deleteNotification({ id }) this.notifications.splice(index, 1) this.beforeCursor = this.notifications[this.notifications.length - 1]?.id } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } @@ -160,10 +153,7 @@ export class NotificationsPage { } private async deleteAll(): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Deleting...', - }) - await loader.present() + const loader = this.loader.open('Deleting...').subscribe() try { await this.embassyApi.deleteAllNotifications({ @@ -172,9 +162,9 @@ export class NotificationsPage { this.notifications = [] this.beforeCursor = undefined } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } } diff --git a/web/projects/ui/src/app/pages/server-routes/restore/restore.component.ts b/web/projects/ui/src/app/pages/server-routes/restore/restore.component.ts index 88e9cb4a8..ae644399f 100644 --- a/web/projects/ui/src/app/pages/server-routes/restore/restore.component.ts +++ b/web/projects/ui/src/app/pages/server-routes/restore/restore.component.ts @@ -1,14 +1,10 @@ import { Component } from '@angular/core' -import { - LoadingController, - ModalController, - NavController, -} from '@ionic/angular' +import { ModalController, NavController } from '@ionic/angular' +import { LoadingService } from '@start9labs/shared' +import { TuiDialogService } from '@taiga-ui/core' +import { take } from 'rxjs/operators' +import { PROMPT, PromptOptions } from 'src/app/modals/prompt.component' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { - GenericInputComponent, - GenericInputOptions, -} from 'src/app/modals/generic-input/generic-input.component' import { MappedBackupTarget } from 'src/app/types/mapped-backup-target' import { BackupInfo, @@ -26,37 +22,35 @@ import * as argon2 from '@start9labs/argon2' export class RestorePage { constructor( private readonly modalCtrl: ModalController, + private readonly dialogs: TuiDialogService, private readonly navCtrl: NavController, private readonly embassyApi: ApiService, - private readonly loadingCtrl: LoadingController, + private readonly loader: LoadingService, ) {} async presentModalPassword( target: MappedBackupTarget, ): Promise { - const options: GenericInputOptions = { - title: 'Password Required', + const options: PromptOptions = { message: 'Enter the master password that was used to encrypt this backup. On the next screen, you will select the individual services you want to restore.', label: 'Master Password', placeholder: 'Enter master password', useMask: true, buttonText: 'Next', - submitFn: async (password: string) => { + } + + this.dialogs + .open(PROMPT, { + label: 'Password Required', + data: options, + }) + .pipe(take(1)) + .subscribe(async (password: string) => { const passwordHash = target.entry.startOs?.passwordHash || '' argon2.verify(passwordHash, password) await this.restoreFromBackup(target, password) - }, - } - - const modal = await this.modalCtrl.create({ - componentProps: { options }, - cssClass: 'alertlike-modal', - presentingElement: await this.modalCtrl.getTop(), - component: GenericInputComponent, - }) - - await modal.present() + }) } private async restoreFromBackup( @@ -64,10 +58,7 @@ export class RestorePage { password: string, oldPassword?: string, ): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Decrypting drive...', - }) - await loader.present() + const loader = this.loader.open('Decrypting drive...').subscribe() try { const backupInfo = await this.embassyApi.getBackupInfo({ @@ -76,7 +67,7 @@ export class RestorePage { }) this.presentModalSelect(target.id, backupInfo, password, oldPassword) } finally { - loader.dismiss() + loader.unsubscribe() } } diff --git a/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts b/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts index 0467df102..804a88fab 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts @@ -1,16 +1,11 @@ import { Component } from '@angular/core' -import { - LoadingController, - ModalController, - NavController, -} from '@ionic/angular' +import { ModalController, NavController } from '@ionic/angular' +import { LoadingService } from '@start9labs/shared' +import { TuiDialogService } from '@taiga-ui/core' +import { PROMPT, PromptOptions } from 'src/app/modals/prompt.component' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { - GenericInputComponent, - GenericInputOptions, -} from 'src/app/modals/generic-input/generic-input.component' import { PatchDB } from 'patch-db-client' -import { skip, takeUntil } from 'rxjs/operators' +import { skip, take, takeUntil } from 'rxjs/operators' import { MappedBackupTarget } from 'src/app/types/mapped-backup-target' import * as argon2 from '@start9labs/argon2' import { TuiDestroyService } from '@taiga-ui/cdk' @@ -35,7 +30,8 @@ export class ServerBackupPage { readonly backingUp$ = this.eosService.backingUp$ constructor( - private readonly loadingCtrl: LoadingController, + private readonly loader: LoadingService, + private readonly dialogs: TuiDialogService, private readonly modalCtrl: ModalController, private readonly embassyApi: ApiService, private readonly navCtrl: NavController, @@ -75,14 +71,21 @@ export class ServerBackupPage { private async presentModalPassword( target: MappedBackupTarget, ): Promise { - const options: GenericInputOptions = { - title: 'Master Password Needed', + const options: PromptOptions = { message: 'Enter your master password to encrypt this backup.', label: 'Master Password', placeholder: 'Enter master password', useMask: true, buttonText: 'Create Backup', - submitFn: async (password: string) => { + } + + this.dialogs + .open(PROMPT, { + label: 'Master Password Needed', + data: options, + }) + .pipe(take(1)) + .subscribe(async (password: string) => { // confirm password matches current master password const { passwordHash } = await getServerInfo(this.patch) argon2.verify(passwordHash, password) @@ -105,45 +108,34 @@ export class ServerBackupPage { } await this.createBackup(target, password) } - }, - } - - const m = await this.modalCtrl.create({ - component: GenericInputComponent, - componentProps: { options }, - cssClass: 'alertlike-modal', - }) - - await m.present() + }) } private async presentModalOldPassword( target: MappedBackupTarget, password: string, ): Promise { - const options: GenericInputOptions = { - title: 'Original Password Needed', + const options: PromptOptions = { message: 'This backup was created with a different password. Enter the ORIGINAL password that was used to encrypt this backup.', label: 'Original Password', placeholder: 'Enter original password', useMask: true, buttonText: 'Create Backup', - submitFn: async (oldPassword: string) => { + } + + this.dialogs + .open(PROMPT, { + label: 'Original Password Needed', + data: options, + }) + .pipe(take(1)) + .subscribe(async (oldPassword: string) => { const passwordHash = target.entry.startOs?.passwordHash || '' argon2.verify(passwordHash, oldPassword) await this.createBackup(target, password, oldPassword) - }, - } - - const m = await this.modalCtrl.create({ - component: GenericInputComponent, - componentProps: { options }, - cssClass: 'alertlike-modal', - }) - - await m.present() + }) } private async createBackup( @@ -151,10 +143,7 @@ export class ServerBackupPage { password: string, oldPassword?: string, ): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Beginning backup...', - }) - await loader.present() + const loader = this.loader.open('Beginning backup...').subscribe() try { await this.embassyApi.createBackup({ @@ -164,7 +153,7 @@ export class ServerBackupPage { password, }) } finally { - loader.dismiss() + loader.unsubscribe() } } } diff --git a/web/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts b/web/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts index 569d34a45..4678fe3ea 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/server-metrics/server-metrics.page.ts @@ -1,9 +1,9 @@ import { Component } from '@angular/core' +import { ErrorService, pauseFor } from '@start9labs/shared' +import { Subject } from 'rxjs' import { Metrics } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' import { TimeService } from 'src/app/services/time-service' -import { pauseFor, ErrorToastService } from '@start9labs/shared' -import { Subject } from 'rxjs' @Component({ selector: 'server-metrics', @@ -18,7 +18,7 @@ export class ServerMetricsPage { readonly uptime$ = this.timeService.uptime$ constructor( - private readonly errToast: ErrorToastService, + private readonly errorService: ErrorService, private readonly embassyApi: ApiService, private readonly timeService: TimeService, ) {} @@ -50,7 +50,7 @@ export class ServerMetricsPage { const metrics = await this.embassyApi.getServerMetrics({}) this.metrics$.next(metrics) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) this.stopDaemon() } } diff --git a/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts b/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts index f375b0e86..5ade9c666 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts @@ -1,32 +1,32 @@ import { Component, Inject } from '@angular/core' +import { ActivatedRoute } from '@angular/router' import { AlertController, - LoadingController, ModalController, NavController, ToastController, } from '@ionic/angular' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ActivatedRoute } from '@angular/router' -import { PatchDB } from 'patch-db-client' -import { firstValueFrom, Observable, of } from 'rxjs' -import { ErrorToastService } from '@start9labs/shared' -import { EOSService } from 'src/app/services/eos.service' -import { ClientStorageService } from 'src/app/services/client-storage.service' -import { OSUpdatePage } from 'src/app/modals/os-update/os-update.page' -import { getAllPackages } from '../../../util/get-package-data' -import { AuthService } from 'src/app/services/auth.service' -import { DataModel } from 'src/app/services/patch-db/data-model' -import { - GenericInputComponent, - GenericInputOptions, -} from 'src/app/modals/generic-input/generic-input.component' -import { ConfigService } from 'src/app/services/config.service' import { WINDOW } from '@ng-web-apis/common' -import { getServerInfo } from 'src/app/util/get-server-info' -import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' -import { ConfigSpec } from 'src/app/pkg-config/config-types' import * as argon2 from '@start9labs/argon2' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { CB } from '@start9labs/start-sdk' +import { TuiAlertService, TuiDialogService } from '@taiga-ui/core' +import { TUI_PROMPT } from '@taiga-ui/kit' +import { PatchDB } from 'patch-db-client' +import { filter, from, Observable, of, switchMap } from 'rxjs' +import { take } from 'rxjs/operators' +import { FormComponent } from 'src/app/components/form.component' +import { OSUpdatePage } from 'src/app/modals/os-update/os-update.page' +import { PROMPT } from 'src/app/modals/prompt.component' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { AuthService } from 'src/app/services/auth.service' +import { ClientStorageService } from 'src/app/services/client-storage.service' +import { ConfigService } from 'src/app/services/config.service' +import { EOSService } from 'src/app/services/eos.service' +import { FormDialogService } from 'src/app/services/form-dialog.service' +import { DataModel } from 'src/app/services/patch-db/data-model' +import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec' +import { getServerInfo } from 'src/app/util/get-server-info' @Component({ selector: 'server-show', @@ -45,8 +45,11 @@ export class ServerShowPage { constructor( private readonly alertCtrl: AlertController, private readonly modalCtrl: ModalController, - private readonly loadingCtrl: LoadingController, - private readonly errToast: ErrorToastService, + private readonly alerts: TuiAlertService, + private readonly dialogs: TuiDialogService, + private readonly formDialog: FormDialogService, + private readonly loader: LoadingService, + private readonly errorService: ErrorService, private readonly embassyApi: ApiService, private readonly navCtrl: NavController, private readonly route: ActivatedRoute, @@ -60,123 +63,110 @@ export class ServerShowPage { ) {} async setBrowserTab(): Promise { - const chosenName = await firstValueFrom(this.patch.watch$('ui', 'name')) + this.patch + .watch$('ui', 'name') + .pipe( + switchMap(initialValue => + this.dialogs.open(PROMPT, { + label: 'Browser Tab Title', + data: { + message: `This value will be displayed as the title of your browser tab.`, + label: 'Device Name', + placeholder: 'StartOS', + required: false, + buttonText: 'Save', + initialValue, + }, + }), + ), + take(1), + ) + .subscribe(async name => { + const loader = this.loader.open('Saving...').subscribe() - const options: GenericInputOptions = { - title: 'Browser Tab Title', - message: `This value will be displayed as the title of your browser tab.`, - label: 'Device Name', - useMask: false, - placeholder: 'StartOS', - nullable: true, - initialValue: chosenName, - buttonText: 'Save', - submitFn: (name: string) => this.setName(name || null), - } - - const modal = await this.modalCtrl.create({ - componentProps: { options }, - cssClass: 'alertlike-modal', - presentingElement: await this.modalCtrl.getTop(), - component: GenericInputComponent, - }) - - await modal.present() + try { + await this.embassyApi.setDbValue( + ['name'], + name || null, + ) + } finally { + loader.unsubscribe() + } + }) } async presentAlertResetPassword() { - const alert = await this.alertCtrl.create({ - header: 'Warning', - message: - 'You will still need your current password to decrypt existing backups!', - buttons: [ - { - text: 'Cancel', - role: 'cancel', + this.dialogs + .open(TUI_PROMPT, { + label: 'Warning', + size: 's', + data: { + content: + 'You will still need your current password to decrypt existing backups!', + yes: 'Continue', + no: 'Cancel', }, - { - text: 'Continue', - handler: () => this.presentModalResetPassword(), - cssClass: 'enter-click', - }, - ], - cssClass: 'alert-warning-message', - }) - - await alert.present() - } - - async presentModalResetPassword(): Promise { - const modal = await this.modalCtrl.create({ - component: GenericFormPage, - componentProps: { - title: 'Change Master Password', - spec: PasswordSpec, - buttons: [ - { - text: 'Save', - handler: (value: any) => { - return this.resetPassword(value) - }, - isSubmit: true, + }) + .pipe( + filter(Boolean), + switchMap(() => from(configBuilderToSpec(passwordSpec))), + ) + .subscribe(spec => { + this.formDialog.open(FormComponent, { + label: 'Change Master Password', + data: { + spec, + buttons: [ + { + text: 'Save', + handler: (value: PasswordSpec) => this.resetPassword(value), + }, + ], }, - ], - }, - }) - await modal.present() + }) + }) } - private async resetPassword(value: { - currPass: string - newPass: string - newPass2: string - }): Promise { + private async resetPassword(value: PasswordSpec): Promise { let err = '' - if (value.newPass !== value.newPass2) { + if (value.newPassword1 !== value.newPassword2) { err = 'New passwords do not match' - } else if (value.newPass.length < 12) { + } else if (value.newPassword1.length < 12) { err = 'New password must be 12 characters or greater' - } else if (value.newPass.length > 64) { + } else if (value.newPassword1.length > 64) { err = 'New password must be less than 65 characters' } // confirm current password is correct const { passwordHash } = await getServerInfo(this.patch) try { - argon2.verify(passwordHash, value.currPass) + argon2.verify(passwordHash, value.currentPassword) } catch (e) { err = 'Current password is invalid' } if (err) { - this.errToast.present(err) + this.errorService.handleError(err) return false } - const loader = await this.loadingCtrl.create({ - message: 'Changing master password...', - }) - await loader.present() + const loader = this.loader.open('Saving...').subscribe() try { await this.embassyApi.resetPassword({ - oldPassword: value.currPass, - newPassword: value.newPass, - }) - const toast = await this.toastCtrl.create({ - header: 'Password changed!', - position: 'bottom', - duration: 2000, + oldPassword: value.currentPassword, + newPassword: value.newPassword1, }) - toast.present() + this.alerts.open('Password changed!').subscribe() + return true } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) return false } finally { - loader.dismiss() + loader.unsubscribe() } } @@ -187,7 +177,7 @@ export class ServerShowPage { const alert = await this.alertCtrl.create({ header: isTor ? 'Warning' : 'Confirm', message: isTor - ? `You are currently connected over Tor. If you reset the Tor daemon, you will loose connectivity until it comes back online.

${shared}` + ? `You are currently connected over Tor. If you reset the Tor daemon, you will lose connectivity until it comes back online.

${shared}` : `Reset Tor?

${shared}`, inputs: [ { @@ -215,10 +205,7 @@ export class ServerShowPage { } private async resetTor(wipeState: boolean) { - const loader = await this.loadingCtrl.create({ - message: 'Resetting Tor...', - }) - await loader.present() + const loader = this.loader.open('Resetting Tor...').subscribe() try { await this.embassyApi.resetTor({ @@ -241,9 +228,9 @@ export class ServerShowPage { }) await toast.present() } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } @@ -336,7 +323,7 @@ export class ServerShowPage { this.restart() }) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } }, cssClass: 'enter-click', @@ -360,19 +347,6 @@ export class ServerShowPage { } } - private async setName(value: string | null): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Saving...', - }) - await loader.present() - - try { - await this.embassyApi.setDbValue(['name'], value) - } finally { - loader.dismiss() - } - } - // should wipe cache independent of actual BE logout private logout() { this.embassyApi.logout({}).catch(e => console.error('Failed to log out', e)) @@ -381,48 +355,37 @@ export class ServerShowPage { private async restart() { const action = 'Restart' - - const loader = await this.loadingCtrl.create({ - message: `Beginning ${action}...`, - }) - await loader.present() + const loader = this.loader.open(`Beginning ${action}...`).subscribe() try { await this.embassyApi.restartServer({}) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } private async shutdown() { const action = 'Shutdown' - - const loader = await this.loadingCtrl.create({ - message: `Beginning ${action}...`, - }) - await loader.present() + const loader = this.loader.open(`Beginning ${action}...`).subscribe() try { await this.embassyApi.shutdownServer({}) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } private async checkForEosUpdate(): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Checking for updates', - }) - await loader.present() + const loader = this.loader.open('Checking for updates').subscribe() try { await this.eosService.loadEos() - await loader.dismiss() + await loader.unsubscribe() if (this.eosService.updateAvailable$.value) { this.updateEos() @@ -430,8 +393,8 @@ export class ServerShowPage { this.presentAlertLatest() } } catch (e: any) { - await loader.dismiss() - this.errToast.present(e) + await loader.unsubscribe() + this.errorService.handleError(e) } } @@ -729,29 +692,28 @@ interface SettingBtn { disabled$: Observable } -const PasswordSpec: ConfigSpec = { - currPass: { - type: 'string', +const passwordSpec = CB.Config.of({ + currentPassword: CB.Value.text({ name: 'Current Password', - placeholder: 'CurrentPass', - nullable: false, + required: { + default: null, + }, masked: true, - copyable: false, - }, - newPass: { - type: 'string', + }), + newPassword1: CB.Value.text({ name: 'New Password', - placeholder: 'NewPass', - nullable: false, + required: { + default: null, + }, masked: true, - copyable: false, - }, - newPass2: { - type: 'string', + }), + newPassword2: CB.Value.text({ name: 'Retype New Password', - placeholder: 'NewPass', - nullable: false, + required: { + default: null, + }, masked: true, - copyable: false, - }, -} + }), +}) + +export type PasswordSpec = typeof passwordSpec.validator._TYPE diff --git a/web/projects/ui/src/app/pages/server-routes/sessions/sessions.page.ts b/web/projects/ui/src/app/pages/server-routes/sessions/sessions.page.ts index d6a1a42e5..9dfdccb66 100644 --- a/web/projects/ui/src/app/pages/server-routes/sessions/sessions.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/sessions/sessions.page.ts @@ -1,8 +1,8 @@ import { Component } from '@angular/core' -import { AlertController, LoadingController } from '@ionic/angular' -import { ErrorToastService } from '@start9labs/shared' -import { ApiService } from 'src/app/services/api/embassy-api.service' +import { AlertController } from '@ionic/angular' +import { ErrorService, LoadingService } from '@start9labs/shared' import { PlatformType, Session } from 'src/app/services/api/api.types' +import { ApiService } from 'src/app/services/api/embassy-api.service' @Component({ selector: 'sessions', @@ -15,8 +15,8 @@ export class SessionsPage { otherSessions: SessionWithId[] = [] constructor( - private readonly loadingCtrl: LoadingController, - private readonly errToast: ErrorToastService, + private readonly loader: LoadingService, + private readonly errorService: ErrorService, private readonly alertCtrl: AlertController, private readonly embassyApi: ApiService, ) {} @@ -39,7 +39,7 @@ export class SessionsPage { ) }) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { this.loading = false } @@ -67,18 +67,17 @@ export class SessionsPage { } async kill(ids: string[]): Promise { - const loader = await this.loadingCtrl.create({ - message: `Terminating session${ids.length > 1 ? 's' : ''}...`, - }) - await loader.present() + const loader = this.loader + .open(`Terminating session${ids.length > 1 ? 's' : ''}...`) + .subscribe() try { await this.embassyApi.killSessions({ ids }) this.otherSessions = this.otherSessions.filter(s => !ids.includes(s.id)) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } diff --git a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts index f6410e748..a0da8609d 100644 --- a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts @@ -1,10 +1,10 @@ import { Component } from '@angular/core' -import { isPlatform, LoadingController, NavController } from '@ionic/angular' +import { isPlatform, NavController } from '@ionic/angular' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { S9pk, T } from '@start9labs/start-sdk' +import cbor from 'cbor' import { ApiService } from 'src/app/services/api/embassy-api.service' import { ConfigService } from 'src/app/services/config.service' -import cbor from 'cbor' -import { ErrorToastService } from '@start9labs/shared' -import { S9pk, T } from '@start9labs/start-sdk' interface Positions { [key: string]: [bigint, bigint] // [position, length] @@ -37,10 +37,10 @@ export class SideloadPage { } constructor( - private readonly loadingCtrl: LoadingController, + private readonly loader: LoadingService, private readonly api: ApiService, private readonly navCtrl: NavController, - private readonly errToast: ErrorToastService, + private readonly errorService: ErrorService, private readonly config: ConfigService, ) {} @@ -111,11 +111,8 @@ export class SideloadPage { } async handleUpload() { - const loader = await this.loadingCtrl.create({ - message: 'Uploading package', - cssClass: 'loader', - }) - await loader.present() + const loader = this.loader.open('Uploading package').subscribe() + try { const res = await this.api.sideloadPackage() this.api @@ -124,9 +121,9 @@ export class SideloadPage { this.navCtrl.navigateRoot('/services') } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() this.clearToUpload() } } diff --git a/web/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.html b/web/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.html index 61737639b..4dd44dfd1 100644 --- a/web/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.html +++ b/web/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.html @@ -24,7 +24,7 @@ Saved Keys - + Add New Key @@ -62,7 +62,7 @@ - +

{{ ssh.hostname }}

@@ -73,7 +73,7 @@ slot="end" fill="clear" color="danger" - (click)="presentAlertDelete(i)" + (click)="delete(ssh)" > Remove diff --git a/web/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.ts b/web/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.ts index 476835566..5e474db0f 100644 --- a/web/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.ts @@ -1,16 +1,12 @@ -import { Component } from '@angular/core' -import { - AlertController, - LoadingController, - ModalController, -} from '@ionic/angular' +import { ChangeDetectorRef, Component } from '@angular/core' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core' +import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit' +import { filter } from 'rxjs' +import { take } from 'rxjs/operators' +import { PROMPT } from 'src/app/modals/prompt.component' import { SSHKey } from 'src/app/services/api/api.types' -import { ErrorToastService } from '@start9labs/shared' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { - GenericInputComponent, - GenericInputOptions, -} from 'src/app/modals/generic-input/generic-input.component' @Component({ selector: 'ssh-keys', @@ -23,10 +19,10 @@ export class SSHKeysPage { readonly docsUrl = 'https://docs.start9.com/0.3.5.x/user-manual/ssh' constructor( - private readonly loadingCtrl: LoadingController, - private readonly modalCtrl: ModalController, - private readonly errToast: ErrorToastService, - private readonly alertCtrl: AlertController, + private readonly cdr: ChangeDetectorRef, + private readonly loader: LoadingService, + private readonly dialogs: TuiDialogService, + private readonly errorService: ErrorService, private readonly embassyApi: ApiService, ) {} @@ -38,89 +34,62 @@ export class SSHKeysPage { try { this.sshKeys = await this.embassyApi.getSshKeys({}) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { this.loading = false } } - async presentModalAdd() { - const { name, description } = sshSpec + add() { + this.dialogs + .open(PROMPT, ADD_OPTIONS) + .pipe(take(1)) + .subscribe(async key => { + const loader = this.loader.open('Saving...').subscribe() - const options: GenericInputOptions = { - title: name, - message: description, - label: name, - submitFn: (pk: string) => this.add(pk), - } - - const modal = await this.modalCtrl.create({ - component: GenericInputComponent, - componentProps: { options }, - cssClass: 'alertlike-modal', - }) - await modal.present() + try { + this.sshKeys?.push(await this.embassyApi.addSshKey({ key })) + } finally { + loader.unsubscribe() + this.cdr.markForCheck() + } + }) } - async add(pubkey: string): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Saving...', - }) - await loader.present() + delete(key: SSHKey) { + this.dialogs + .open(TUI_PROMPT, DELETE_OPTIONS) + .pipe(filter(Boolean)) + .subscribe(async () => { + const loader = this.loader.open('Deleting...').subscribe() - try { - const key = await this.embassyApi.addSshKey({ key: pubkey }) - this.sshKeys.push(key) - } finally { - loader.dismiss() - } - } - - async presentAlertDelete(i: number) { - const alert = await this.alertCtrl.create({ - header: 'Caution', - message: `Are you sure you want to delete this key?`, - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Delete', - handler: () => { - this.delete(i) - }, - cssClass: 'enter-click', - }, - ], - }) - await alert.present() - } - - async delete(i: number): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Deleting...', - }) - await loader.present() - - try { - const entry = this.sshKeys[i] - await this.embassyApi.deleteSshKey({ fingerprint: entry.fingerprint }) - this.sshKeys.splice(i, 1) - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.dismiss() - } + try { + await this.embassyApi.deleteSshKey({ fingerprint: key.fingerprint }) + this.sshKeys?.splice(this.sshKeys?.indexOf(key), 1) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + this.cdr.markForCheck() + } + }) } } -const sshSpec = { - type: 'string', - name: 'SSH Key', - description: - 'Enter the SSH public key you would like to authorize for root access to your server.', - nullable: false, - masked: false, - copyable: false, +const ADD_OPTIONS: Partial> = { + label: 'SSH Key', + data: { + message: + 'Enter the SSH public key you would like to authorize for root access to your Embassy.', + }, +} + +const DELETE_OPTIONS: Partial> = { + label: 'Confirm', + size: 's', + data: { + content: 'Delete key? This action cannot be undone.', + yes: 'Delete', + no: 'Cancel', + }, } diff --git a/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts b/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts index e6d5da252..0a1f6a7d9 100644 --- a/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts @@ -2,19 +2,23 @@ import { Component, Inject } from '@angular/core' import { ActionSheetController, AlertController, - LoadingController, - ModalController, ToastController, } from '@ionic/angular' -import { AlertInput } from '@ionic/core' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ActionSheetButton } from '@ionic/core' -import { ValueSpecObject } from 'src/app/pkg-config/config-types' -import { RR } from 'src/app/services/api/api.types' -import { pauseFor, ErrorToastService } from '@start9labs/shared' -import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' -import { ConfigService } from 'src/app/services/config.service' +import { ActionSheetButton, AlertInput } from '@ionic/core' import { WINDOW } from '@ng-web-apis/common' +import { ErrorService, LoadingService, pauseFor } from '@start9labs/shared' +import { CT } from '@start9labs/start-sdk' +import { TuiDialogOptions } from '@taiga-ui/core' +import { FormComponent, FormContext } from 'src/app/components/form.component' +import { RR } from 'src/app/services/api/api.types' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { ConfigService } from 'src/app/services/config.service' +import { FormDialogService } from 'src/app/services/form-dialog.service' + +export interface WiFiForm { + ssid: string + password: string +} @Component({ selector: 'wifi', @@ -32,9 +36,9 @@ export class WifiPage { private readonly api: ApiService, private readonly toastCtrl: ToastController, private readonly alertCtrl: AlertController, - private readonly loadingCtrl: LoadingController, - private readonly modalCtrl: ModalController, - private readonly errToast: ErrorToastService, + private readonly loader: LoadingService, + private readonly formDialog: FormDialogService, + private readonly errorService: ErrorService, private readonly actionCtrl: ActionSheetController, private readonly config: ConfigService, @Inject(WINDOW) private readonly windowRef: Window, @@ -60,7 +64,7 @@ export class WifiPage { await this.presentAlertCountry() } } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { this.loading = false } @@ -120,30 +124,25 @@ export class WifiPage { async presentModalAdd(ssid?: string, needsPW: boolean = true) { const wifiSpec = getWifiValueSpec(ssid, needsPW) - const modal = await this.modalCtrl.create({ - component: GenericFormPage, - componentProps: { - title: wifiSpec.name, + const options: Partial>> = { + label: wifiSpec.name, + data: { spec: wifiSpec.spec, buttons: [ { text: 'Save for Later', - handler: async (value: { ssid: string; password: string }) => { - await this.save(value.ssid, value.password) - }, + handler: async ({ ssid, password }) => this.save(ssid, password), }, { text: 'Save and Connect', - handler: async (value: { ssid: string; password: string }) => { - await this.saveAndConnect(value.ssid, value.password) - }, - isSubmit: true, + handler: async ({ ssid, password }) => + this.saveAndConnect(ssid, password), }, ], }, - }) + } - await modal.present() + this.formDialog.open(FormComponent, options) } async presentAction(ssid: string) { @@ -179,19 +178,16 @@ export class WifiPage { } private async setCountry(country: string): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Setting country...', - }) - await loader.present() + const loader = this.loader.open('Setting country...').subscribe() try { await this.api.setWifiCountry({ country }) await this.getWifi() this.wifi.country = country } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } @@ -270,43 +266,36 @@ export class WifiPage { } private async connect(ssid: string): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Connecting. This could take a while...', - }) - await loader.present() + const loader = this.loader + .open('Connecting. This could take a while...') + .subscribe() try { await this.api.connectWifi({ ssid }) await this.confirmWifi(ssid) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } private async delete(ssid: string): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Deleting...', - }) - await loader.present() + const loader = this.loader.open('Deleting...').subscribe() try { await this.api.deleteWifi({ ssid }) await this.getWifi() delete this.wifi.ssids[ssid] } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } private async save(ssid: string, password: string): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Saving...', - }) - await loader.present() + const loader = this.loader.open('Saving...').subscribe() try { await this.api.addWifi({ @@ -317,17 +306,16 @@ export class WifiPage { }) await this.getWifi() } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } private async saveAndConnect(ssid: string, password: string): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Connecting. This could take a while...', - }) - await loader.present() + const loader = this.loader + .open('Connecting. This could take a while...') + .subscribe() try { await this.api.addWifi({ @@ -339,39 +327,62 @@ export class WifiPage { await this.confirmWifi(ssid, true) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } } function getWifiValueSpec( - ssid?: string, + ssid: string | null = null, needsPW: boolean = true, -): ValueSpecObject { +): CT.ValueSpecObject { return { + warning: null, type: 'object', name: 'WiFi Credentials', description: 'Enter the network SSID and password. You can connect now or save the network for later.', spec: { ssid: { - type: 'string', + type: 'text', + minLength: null, + maxLength: null, + patterns: [], name: 'Network SSID', - nullable: false, + description: null, + inputmode: 'text', + placeholder: null, + required: true, masked: false, - copyable: false, default: ssid, + warning: null, + disabled: false, + immutable: false, + generate: null, }, password: { - type: 'string', + type: 'text', + minLength: null, + maxLength: null, + patterns: [ + { + regex: '^.{8,}$', + description: 'Must be longer than 8 characters', + }, + ], name: 'Password', - nullable: !needsPW, + description: null, + inputmode: 'text', + placeholder: null, + required: needsPW, masked: true, - copyable: false, - pattern: '^.{8,}$', - 'pattern-description': 'Must be longer than 8 characters', + default: null, + warning: null, + disabled: false, + immutable: false, + generate: null, }, }, } diff --git a/web/projects/ui/src/app/pkg-config/config-types.ts b/web/projects/ui/src/app/pkg-config/config-types.ts deleted file mode 100644 index f73dbc0a4..000000000 --- a/web/projects/ui/src/app/pkg-config/config-types.ts +++ /dev/null @@ -1,159 +0,0 @@ -export type ConfigSpec = Record - -export type ValueType = - | 'string' - | 'number' - | 'boolean' - | 'enum' - | 'list' - | 'object' - | 'pointer' - | 'union' -export type ValueSpec = ValueSpecOf - -// core spec types. These types provide the metadata for performing validations -export type ValueSpecOf = T extends 'string' - ? ValueSpecString - : T extends 'number' - ? ValueSpecNumber - : T extends 'boolean' - ? ValueSpecBoolean - : T extends 'enum' - ? ValueSpecEnum - : T extends 'list' - ? ValueSpecList - : T extends 'object' - ? ValueSpecObject - : T extends 'union' - ? ValueSpecUnion - : never - -export interface ValueSpecString extends ListValueSpecString, WithStandalone { - type: 'string' - default?: DefaultString - nullable: boolean - textarea?: boolean -} - -export interface ValueSpecNumber extends ListValueSpecNumber, WithStandalone { - type: 'number' - nullable: boolean - default?: number -} - -export interface ValueSpecEnum extends ListValueSpecEnum, WithStandalone { - type: 'enum' - default: string -} - -export interface ValueSpecBoolean extends WithStandalone { - type: 'boolean' - default: boolean -} - -export interface ValueSpecUnion { - type: 'union' - tag: UnionTagSpec - variants: { [key: string]: ConfigSpec } - default: string -} - -export interface ValueSpecObject extends WithStandalone { - type: 'object' - spec: ConfigSpec -} - -export interface WithStandalone { - name: string - description?: string - warning?: string -} - -// no lists of booleans, lists, pointers -export type ListValueSpecType = - | 'string' - | 'number' - | 'enum' - | 'object' - | 'union' - -// represents a spec for the values of a list -export type ListValueSpecOf = T extends 'string' - ? ListValueSpecString - : T extends 'number' - ? ListValueSpecNumber - : T extends 'enum' - ? ListValueSpecEnum - : T extends 'object' - ? ListValueSpecObject - : T extends 'union' - ? ListValueSpecUnion - : never - -// represents a spec for a list -export type ValueSpecList = ValueSpecListOf -export interface ValueSpecListOf - extends WithStandalone { - type: 'list' - subtype: T - spec: ListValueSpecOf - range: string // '[0,1]' (inclusive) OR '[0,*)' (right unbounded), normal math rules - default: string[] | number[] | DefaultString[] | object[] -} - -// sometimes the type checker needs just a little bit of help -export function isValueSpecListOf( - t: ValueSpecList, - s: S, -): t is ValueSpecListOf { - return t.subtype === s -} - -export interface ListValueSpecString { - pattern?: string - 'pattern-description'?: string - masked: boolean - copyable: boolean - placeholder?: string -} - -export interface ListValueSpecNumber { - range: string - integral: boolean - units?: string - placeholder?: string -} - -export interface ListValueSpecEnum { - values: string[] - 'value-names': { [value: string]: string } -} - -export interface ListValueSpecObject { - spec: ConfigSpec // this is a mapped type of the config object at this level, replacing the object's values with specs on those values - 'unique-by': UniqueBy // indicates whether duplicates can be permitted in the list - 'display-as'?: string // this should be a handlebars template which can make use of the entire config which corresponds to 'spec' -} - -export type UniqueBy = null | string | { any: UniqueBy[] } | { all: UniqueBy[] } - -export interface ListValueSpecUnion { - tag: UnionTagSpec - variants: { [key: string]: ConfigSpec } - 'display-as'?: string // this may be a handlebars template which can conditionally (on tag.id) make use of each union's entries, or if left blank will display as tag.id - 'unique-by': UniqueBy - default: string // this should be the variantName which one prefers a user to start with by default when creating a new union instance in a list -} - -export interface UnionTagSpec { - id: string // The name of the field containing one of the union variants - 'variant-names': { - // the name of each variant - [variant: string]: string - } - name: string - description?: string - warning?: string -} - -export type DefaultString = string | { charset: string; len: number } diff --git a/web/projects/ui/src/app/pkg-config/config-utilities.ts b/web/projects/ui/src/app/pkg-config/config-utilities.ts deleted file mode 100644 index 431bc1c5d..000000000 --- a/web/projects/ui/src/app/pkg-config/config-utilities.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { ValueSpec, DefaultString } from './config-types' - -export class Range { - min?: number - max?: number - minInclusive!: boolean - maxInclusive!: boolean - - static from(s: string): Range { - const r = new Range() - r.minInclusive = s.startsWith('[') - r.maxInclusive = s.endsWith(']') - const [minStr, maxStr] = s.split(',').map(a => a.trim()) - r.min = minStr === '(*' ? undefined : Number(minStr.slice(1)) - r.max = maxStr === '*)' ? undefined : Number(maxStr.slice(0, -1)) - return r - } - - checkIncludes(n: number) { - if ( - this.hasMin() && - (this.min > n || (!this.minInclusive && this.min == n)) - ) { - throw new Error(this.minMessage()) - } - if ( - this.hasMax() && - (this.max < n || (!this.maxInclusive && this.max == n)) - ) { - throw new Error(this.maxMessage()) - } - } - - hasMin(): this is Range & { min: number } { - return this.min !== undefined - } - - hasMax(): this is Range & { max: number } { - return this.max !== undefined - } - - minMessage(): string { - return `greater than${this.minInclusive ? ' or equal to' : ''} ${this.min}` - } - - maxMessage(): string { - return `less than${this.maxInclusive ? ' or equal to' : ''} ${this.max}` - } - - description(): string { - let message = 'Value can be any number.' - - if (this.hasMin() || this.hasMax()) { - message = 'Value must be' - } - - if (this.hasMin() && this.hasMax()) { - message = `${message} ${this.minMessage()} AND ${this.maxMessage()}.` - } else if (this.hasMin() && !this.hasMax()) { - message = `${message} ${this.minMessage()}.` - } else if (!this.hasMin() && this.hasMax()) { - message = `${message} ${this.maxMessage()}.` - } - - return message - } - - integralMin(): number | undefined { - if (this.min) { - const ceil = Math.ceil(this.min) - if (this.minInclusive) { - return ceil - } else { - if (ceil === this.min) { - return ceil + 1 - } else { - return ceil - } - } - } - } - - integralMax(): number | undefined { - if (this.max) { - const floor = Math.floor(this.max) - if (this.maxInclusive) { - return floor - } else { - if (floor === this.max) { - return floor - 1 - } else { - return floor - } - } - } - } -} - -export function getDefaultDescription(spec: ValueSpec): string { - let toReturn: string | undefined - switch (spec.type) { - case 'string': - if (typeof spec.default === 'string') { - toReturn = spec.default - } else if (typeof spec.default === 'object') { - toReturn = 'random' - } - break - case 'number': - if (typeof spec.default === 'number') { - toReturn = String(spec.default) - } - break - case 'boolean': - toReturn = spec.default === true ? 'True' : 'False' - break - case 'enum': - toReturn = spec['value-names'][spec.default] - break - } - - return toReturn || '' -} - -export function getDefaultString(defaultSpec: DefaultString): string { - if (typeof defaultSpec === 'string') { - return defaultSpec - } else { - let s = '' - for (let i = 0; i < defaultSpec.len; i++) { - s = s + getRandomCharInSet(defaultSpec.charset) - } - - return s - } -} - -// a,g,h,A-Z,,,,- -export function getRandomCharInSet(charset: string): string { - const set = stringToCharSet(charset) - let charIdx = Math.floor(Math.random() * set.len) - for (let range of set.ranges) { - if (range.len > charIdx) { - return String.fromCharCode(range.start.charCodeAt(0) + charIdx) - } - charIdx -= range.len - } - throw new Error('unreachable') -} - -function stringToCharSet(charset: string): CharSet { - let set: CharSet = { ranges: [], len: 0 } - let start: string | null = null - let end: string | null = null - let in_range = false - for (let char of charset) { - switch (char) { - case ',': - if (start !== null && end !== null) { - if (start!.charCodeAt(0) > end!.charCodeAt(0)) { - throw new Error('start > end of charset') - } - const len = end.charCodeAt(0) - start.charCodeAt(0) + 1 - set.ranges.push({ - start, - end, - len, - }) - set.len += len - start = null - end = null - in_range = false - } else if (start !== null && !in_range) { - set.len += 1 - set.ranges.push({ start, end: start, len: 1 }) - start = null - } else if (start !== null && in_range) { - end = ',' - } else if (start === null && end === null && !in_range) { - start = ',' - } else { - throw new Error('unexpected ","') - } - break - case '-': - if (start === null) { - start = '-' - } else if (!in_range) { - in_range = true - } else if (in_range && end === null) { - end = '-' - } else { - throw new Error('unexpected "-"') - } - break - default: - if (start === null) { - start = char - } else if (in_range && end === null) { - end = char - } else { - throw new Error(`unexpected "${char}"`) - } - } - } - if (start !== null && end !== null) { - if (start!.charCodeAt(0) > end!.charCodeAt(0)) { - throw new Error('start > end of charset') - } - const len = end.charCodeAt(0) - start.charCodeAt(0) + 1 - set.ranges.push({ - start, - end, - len, - }) - set.len += len - } else if (start !== null) { - set.len += 1 - set.ranges.push({ - start, - end: start, - len: 1, - }) - } - return set -} - -interface CharSet { - ranges: { - start: string - end: string - len: number - }[] - len: number -} diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index acebdfdfb..e10c682c3 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -6,7 +6,8 @@ import { Metric, NotificationLevel, RR, ServerNotifications } from './api.types' import { BTC_ICON, LND_ICON, PROXY_ICON } from './api-icons' import { DependencyMetadata, MarketplacePkg } from '@start9labs/marketplace' import { Log } from '@start9labs/shared' -import { T } from '@start9labs/start-sdk' +import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec' +import { T, CB } from '@start9labs/start-sdk' export module Mock { export const ServerUpdated: T.ServerStatus = { @@ -716,651 +717,512 @@ export module Mock { }, } - export const ConfigSpec: RR.GetPackageConfigRes['spec'] = { - bitcoin: { - type: 'object', - name: 'Bitcoin Settings', - description: - 'RPC and P2P interface configuration options for Bitcoin Core', - spec: { - 'bitcoind-p2p': { - type: 'union', - tag: { - id: 'type', - name: 'Bitcoin Core P2P', + export const getInputSpec = async (): Promise< + RR.GetPackageConfigRes['spec'] + > => + configBuilderToSpec( + CB.Config.of({ + bitcoin: CB.Value.object( + { + name: 'Bitcoin Settings', description: - '

The Bitcoin Core node to connect to over the peer-to-peer (P2P) interface:

  • Bitcoin Core: The Bitcoin Core service installed on this device
  • External Node: A Bitcoin node running on a different device
', - 'variant-names': { - internal: 'Bitcoin Core', - external: 'External Node', - }, + 'RPC and P2P interface configuration options for Bitcoin Core', }, - default: 'internal', - variants: { - internal: {}, - external: { - 'p2p-host': { - type: 'string', - name: 'Public Address', - description: 'The public address of your Bitcoin Core server', - nullable: false, - masked: false, - copyable: false, - }, - 'p2p-port': { - type: 'number', - name: 'P2P Port', + CB.Config.of({ + 'bitcoind-p2p': CB.Value.union( + { + name: 'P2P Settings', description: - 'The port that your Bitcoin Core P2P server is bound to', - nullable: false, - range: '[0,65535]', - integral: true, - default: 8333, + '

The Bitcoin Core node to connect to over the peer-to-peer (P2P) interface:

  • Bitcoin Core: The Bitcoin Core service installed on this device
  • External Node: A Bitcoin node running on a different device
', + required: { default: 'internal' }, }, - }, - }, - }, - }, - }, - advanced: { - name: 'Advanced', - type: 'object', - description: 'Advanced settings', - spec: { - rpcsettings: { - name: 'RPC Settings', - type: 'object', - description: 'rpc username and password', - warning: - 'Adding RPC users gives them special permissions on your node.', - spec: { - rpcuser2: { - name: 'RPC Username', - type: 'string', - description: 'rpc username', - nullable: false, - default: 'defaultrpcusername', - pattern: '^[a-zA-Z]+$', - 'pattern-description': 'must contain only letters.', - masked: false, - copyable: true, - }, - rpcuser: { - name: 'RPC Username', - type: 'string', - description: 'rpc username', - nullable: false, - default: 'defaultrpcusername', - pattern: '^[a-zA-Z]+$', - 'pattern-description': 'must contain only letters.', - masked: false, - copyable: true, - }, - rpcpass: { - name: 'RPC User Password', - type: 'string', - description: 'rpc password', - nullable: false, - default: { - charset: 'a-z,A-Z,2-9', - len: 20, - }, - masked: true, - copyable: true, - }, - rpcpass2: { - name: 'RPC User Password', - type: 'string', - description: 'rpc password', - nullable: false, - default: { - charset: 'a-z,A-Z,2-9', - len: 20, - }, - masked: true, - copyable: true, - }, - }, - }, - }, - }, - testnet: { - name: 'Testnet', - type: 'boolean', - description: - '
  • determines whether your node is running on testnet or mainnet
', - warning: 'Chain will have to resync!', - default: true, - }, - 'object-list': { - name: 'Object List', - type: 'list', - subtype: 'object', - description: 'This is a list of objects, like users or something', - range: '[0,4]', - default: [ - { - 'first-name': 'Admin', - 'last-name': 'User', - age: 40, - }, - { - 'first-name': 'Admin2', - 'last-name': 'User', - age: 40, - }, - ], - // the outer spec here, at the list level, says that what's inside (the inner spec) pertains to its inner elements. - // it just so happens that ValueSpecObject's have the field { spec: ConfigSpec } - // see 'union-list' below for a different example. - spec: { - 'unique-by': 'last-name', - 'display-as': `I'm {{last-name}}, {{first-name}} {{last-name}}`, - spec: { - 'first-name': { - name: 'First Name', - type: 'string', - description: 'User first name', - nullable: true, - masked: false, - copyable: false, - }, - 'last-name': { - name: 'Last Name', - type: 'string', - description: 'User first name', - nullable: true, - default: { - charset: 'a-g,2-9', - len: 12, - }, - pattern: '^[a-zA-Z]+$', - 'pattern-description': 'must contain only letters.', - masked: false, - copyable: true, - }, - age: { - name: 'Age', - type: 'number', - description: 'The age of the user', - nullable: true, - integral: false, - warning: 'User must be at least 18.', - range: '[18,*)', - }, - }, - }, - }, - 'union-list': { - name: 'Union List', - type: 'list', - subtype: 'union', - description: 'This is a sample list of unions', - warning: 'If you change this, things may work.', - // a list of union selections. e.g. 'summer', 'winter',... - default: ['summer'], - range: '[0, 2]', - spec: { - tag: { - id: 'preference', - 'variant-names': { - summer: 'Summer', - winter: 'Winter', - other: 'Other', - }, - name: 'Preference', - }, - // this default is used to make a union selection when a new list element is first created - default: 'summer', - variants: { - summer: { - 'favorite-tree': { - name: 'Favorite Tree', - type: 'string', - nullable: false, - description: 'What is your favorite tree?', - default: 'Maple', - masked: false, - copyable: false, - }, - 'favorite-flower': { - name: 'Favorite Flower', - type: 'enum', - description: 'Select your favorite flower', - 'value-names': { - none: 'Hate Flowers', - red: 'Red', - blue: 'Blue', - purple: 'Purple', - }, - values: ['none', 'red', 'blue', 'purple'], - default: 'none', - }, - }, - winter: { - 'like-snow': { - name: 'Like Snow?', - type: 'boolean', - description: 'Do you like snow or not?', - default: true, - }, - }, - }, - 'unique-by': 'preference', - }, - }, - 'random-enum': { - name: 'Random Enum', - type: 'enum', - 'value-names': { - null: 'Null', - option1: 'One 1', - option2: 'Two 2', - option3: 'Three 3', - }, - default: 'null', - description: 'This is not even real.', - warning: 'Be careful changing this!', - values: ['null', 'option1', 'option2', 'option3'], - }, - 'favorite-number': { - name: 'Favorite Number', - type: 'number', - integral: false, - description: 'Your favorite number of all time', - warning: - 'Once you set this number, it can never be changed without severe consequences.', - nullable: true, - default: 7, - range: '(-100,100]', - units: 'BTC', - }, - 'unlucky-numbers': { - name: 'Unlucky Numbers', - type: 'list', - subtype: 'number', - description: 'Numbers that you like but are not your top favorite.', - spec: { - integral: false, - range: '[-100,200)', - }, - range: '[0,10]', - default: [2, 3], - }, - rpcsettings: { - name: 'RPC Settings', - type: 'object', - description: 'rpc username and password', - warning: 'Adding RPC users gives them special permissions on your node.', - spec: { - laws: { - name: 'Laws', - type: 'object', - description: 'the law of the realm', - spec: { - law1: { - name: 'First Law', - type: 'string', - description: 'the first law', - nullable: true, - masked: false, - copyable: true, - }, - law2: { - name: 'Second Law', - type: 'string', - description: 'the second law', - nullable: true, - masked: false, - copyable: true, - }, - }, - }, - rulemakers: { - name: 'Rule Makers', - type: 'list', - subtype: 'object', - description: 'the people who make the rules', - range: '[0,2]', + CB.Variants.of({ + internal: { name: 'Bitcoin Core', spec: CB.Config.of({}) }, + external: { + name: 'External Node', + spec: CB.Config.of({ + 'p2p-host': CB.Value.text({ + name: 'Public Address', + required: { + default: null, + }, + description: + 'The public address of your Bitcoin Core server', + }), + 'p2p-port': CB.Value.number({ + name: 'P2P Port', + description: + 'The port that your Bitcoin Core P2P server is bound to', + required: { + default: 8333, + }, + min: 0, + max: 65535, + integer: true, + }), + }), + }, + }), + ), + }), + ), + color: CB.Value.color({ + name: 'Color', + required: false, + }), + datetime: CB.Value.datetime({ + name: 'Datetime', + required: false, + }), + file: CB.Value.file({ + name: 'File', + required: false, + extensions: ['png', 'pdf'], + }), + users: CB.Value.multiselect({ + name: 'Users', default: [], - spec: { - 'unique-by': null, - spec: { - rulemakername: { - name: 'Rulemaker Name', - type: 'string', - description: 'the name of the rule maker', - nullable: false, - default: { - charset: 'a-g,2-9', - len: 12, - }, - masked: false, - copyable: false, + maxLength: 2, + disabled: ['matt'], + values: { + matt: 'Matt Hill', + alex: 'Alex Inkin', + blue: 'Blue J', + lucy: 'Lucy', + }, + }), + advanced: CB.Value.object( + { + name: 'Advanced', + description: 'Advanced settings', + }, + CB.Config.of({ + rpcsettings: CB.Value.object( + { + name: 'RPC Settings', + description: 'rpc username and password', + warning: + 'Adding RPC users gives them special permissions on your node.', }, - rulemakerip: { - name: 'Rulemaker IP', - type: 'string', - description: 'the ip of the rule maker', - nullable: false, - default: '192.168.1.0', - pattern: - '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$', - 'pattern-description': 'may only contain numbers and periods', - masked: false, - copyable: true, - }, - }, - }, - }, - rpcuser: { - name: 'RPC Username', - type: 'string', - description: 'rpc username', - nullable: false, - default: 'defaultrpcusername', - pattern: '^[a-zA-Z]+$', - 'pattern-description': 'must contain only letters.', - masked: false, - copyable: true, - }, - rpcpass: { - name: 'RPC User Password', - type: 'string', - description: 'rpc password', - nullable: false, - default: { - charset: 'a-z,A-Z,2-9', - len: 20, - }, - masked: true, - copyable: true, - }, - }, - }, - 'bitcoin-node': { - type: 'union', - default: 'internal', - tag: { - id: 'type', - 'variant-names': { - internal: 'Internal', - external: 'External', - }, - name: 'Bitcoin Node Settings', - description: 'Options
  • Item 1
  • Item 2
', - warning: 'Careful changing this', - }, - variants: { - internal: {}, - external: { - 'emergency-contact': { - name: 'Emergency Contact', - type: 'object', - description: 'The person to contact in case of emergency.', - spec: { - name: { - type: 'string', - name: 'Name', - nullable: false, - masked: false, - copyable: false, - pattern: '^[a-zA-Z]+$', - 'pattern-description': 'Must contain only letters.', - }, - email: { - type: 'string', - name: 'Email', - nullable: false, - masked: false, - copyable: true, - }, - }, - }, - 'public-domain': { - name: 'Public Domain', - type: 'string', - description: 'the public address of the node', - nullable: false, - default: 'bitcoinnode.com', - pattern: '.*', - 'pattern-description': 'anything', - masked: false, - copyable: true, - }, - 'private-domain': { - name: 'Private Domain', - type: 'string', - description: 'the private address of the node', - nullable: false, - masked: true, - copyable: true, - }, - }, - }, - }, - port: { - name: 'Port', - type: 'number', - integral: true, - description: - 'the default port for your Bitcoin node. default: 8333, testnet: 18333, regtest: 18444', - nullable: false, - default: 8333, - range: '(0, 9998]', - }, - 'favorite-slogan': { - name: 'Favorite Slogan', - type: 'string', - description: - 'You most favorite slogan in the whole world, used for paying you.', - nullable: true, - masked: true, - copyable: true, - }, - rpcallowip: { - name: 'RPC Allowed IPs', - type: 'list', - subtype: 'string', - description: - 'external ip addresses that are authorized to access your Bitcoin node', - warning: - 'Any IP you allow here will have RPC access to your Bitcoin node.', - range: '[1,10]', - default: ['192.168.1.1'], - spec: { - masked: false, - copyable: false, - pattern: - '((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))', - 'pattern-description': 'must be a valid ipv4, ipv6, or domain name', - }, - }, - rpcauth: { - name: 'RPC Auth', - type: 'list', - subtype: 'string', - description: 'api keys that are authorized to access your Bitcoin node.', - range: '[0,*)', - default: [], - spec: { - masked: false, - copyable: false, - }, - }, - 'more-advanced': { - name: 'More Advanced', - type: 'object', - description: 'Advanced settings', - spec: { - notifications: { - name: 'Notification Preferences', - type: 'list', - subtype: 'enum', - description: 'how you want to be notified', - range: '[1,3]', - default: ['email'], - spec: { - 'value-names': { - email: 'EEEEmail', - text: 'Texxxt', - call: 'Ccccall', - push: 'PuuuusH', - webhook: 'WebHooookkeee', - }, - values: ['email', 'text', 'call', 'push', 'webhook'], - }, - }, - rpcsettings: { - name: 'RPC Settings', - type: 'object', - description: 'rpc username and password', - warning: - 'Adding RPC users gives them special permissions on your node.', - spec: { - laws: { - name: 'Laws', - type: 'object', - description: 'the law of the realm', - spec: { - law1: { - name: 'First Law', - type: 'string', - description: 'the first law', - nullable: true, - masked: false, - copyable: true, - }, - law2: { - name: 'Second Law', - type: 'string', - description: 'the second law', - nullable: true, - masked: false, - copyable: true, - }, - law4: { - name: 'Fourth Law', - type: 'string', - description: 'the fourth law', - nullable: true, - masked: false, - copyable: true, - }, - law3: { - name: 'Third Law', - type: 'list', - subtype: 'object', - description: 'the third law', - range: '[0,2]', - default: [], - spec: { - 'unique-by': null, - spec: { - lawname: { - name: 'Law Name', - type: 'string', - description: 'the name of the law maker', - nullable: false, - default: { - charset: 'a-g,2-9', - len: 12, - }, - masked: false, - copyable: false, - }, - lawagency: { - name: 'Law agency', - type: 'string', - description: 'the ip of the law maker', - nullable: false, - default: '192.168.1.0', - pattern: - '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$', - 'pattern-description': - 'may only contain numbers and periods', - masked: false, - copyable: true, - }, + CB.Config.of({ + rpcuser2: CB.Value.text({ + name: 'RPC Username', + required: { + default: 'defaultrpcusername', + }, + description: 'rpc username', + patterns: [ + { + regex: '^[a-zA-Z]+$', + description: 'must contain only letters.', + }, + ], + }), + rpcuser: CB.Value.text({ + name: 'RPC Username', + required: { + default: 'defaultrpcusername', + }, + description: 'rpc username', + patterns: [ + { + regex: '^[a-zA-Z]+$', + description: 'must contain only letters.', + }, + ], + }), + rpcpass: CB.Value.text({ + name: 'RPC User Password', + required: { + default: { + charset: 'a-z,A-Z,2-9', + len: 20, }, }, - }, - law5: { - name: 'Fifth Law', - type: 'string', - description: 'the fifth law', - nullable: true, - masked: false, - copyable: true, - }, - }, + description: 'rpc password', + }), + rpcpass2: CB.Value.text({ + name: 'RPC User Password', + required: { + default: { + charset: 'a-z,A-Z,2-9', + len: 20, + }, + }, + description: 'rpc password', + }), + }), + ), + }), + ), + testnet: CB.Value.toggle({ + name: 'Testnet', + default: true, + description: + '
  • determines whether your node is running on testnet or mainnet
', + warning: 'Chain will have to resync!', + }), + 'object-list': CB.Value.list( + CB.List.obj( + { + name: 'Object List', + minLength: 0, + maxLength: 4, + default: [ + // { 'first-name': 'Admin', 'last-name': 'User', age: 40 }, + // { 'first-name': 'Admin2', 'last-name': 'User', age: 40 }, + ], + description: 'This is a list of objects, like users or something', }, - rulemakers: { - name: 'Rule Makers', - type: 'list', - subtype: 'object', - description: 'the people who make the rules', - range: '[0,2]', - default: [], - spec: { - 'unique-by': null, - spec: { - rulemakername: { - name: 'Rulemaker Name', - type: 'string', - description: 'the name of the rule maker', - nullable: false, + { + spec: CB.Config.of({ + 'first-name': CB.Value.text({ + name: 'First Name', + required: false, + description: 'User first name', + }), + 'last-name': CB.Value.text({ + name: 'Last Name', + required: { default: { charset: 'a-g,2-9', len: 12, }, - masked: false, - copyable: false, }, - rulemakerip: { - name: 'Rulemaker IP', - type: 'string', - description: 'the ip of the rule maker', - nullable: false, - default: '192.168.1.0', - pattern: - '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$', - 'pattern-description': - 'may only contain numbers and periods', - masked: false, - copyable: true, + description: 'User first name', + patterns: [ + { + regex: '^[a-zA-Z]+$', + description: 'must contain only letters.', + }, + ], + }), + age: CB.Value.number({ + name: 'Age', + description: 'The age of the user', + warning: 'User must be at least 18.', + required: false, + min: 18, + integer: false, + }), + }), + displayAs: `I'm {{last-name}}, {{first-name}} {{last-name}}`, + uniqueBy: 'last-name', + }, + ), + ), + 'union-list': CB.Value.list( + CB.List.obj( + { + name: 'Union List', + minLength: 0, + maxLength: 2, + default: [], + description: 'This is a sample list of unions', + warning: 'If you change this, things may work.', + }, + { + spec: CB.Config.of({ + /* TODO: Convert range for this value ([0, 2])*/ + union: CB.Value.union( + { + name: 'Preference', + description: null, + warning: null, + required: { default: 'summer' }, }, + CB.Variants.of({ + summer: { + name: 'summer', + spec: CB.Config.of({ + 'favorite-tree': CB.Value.text({ + name: 'Favorite Tree', + required: { + default: 'Maple', + }, + description: 'What is your favorite tree?', + }), + 'favorite-flower': CB.Value.select({ + name: 'Favorite Flower', + description: 'Select your favorite flower', + required: { + default: 'none', + }, + values: { + none: 'none', + red: 'red', + blue: 'blue', + purple: 'purple', + }, + }), + }), + }, + winter: { + name: 'winter', + spec: CB.Config.of({ + 'like-snow': CB.Value.toggle({ + name: 'Like Snow?', + default: true, + description: 'Do you like snow or not?', + }), + }), + }, + }), + ), + }), + uniqueBy: 'preference', + }, + ), + ), + 'random-select': CB.Value.select({ + name: 'Random select', + description: 'This is not even real.', + warning: 'Be careful changing this!', + required: { + default: null, + }, + values: { + option1: 'option1', + option2: 'option2', + option3: 'option3', + }, + disabled: ['option2'], + }), + 'favorite-number': + /* TODO: Convert range for this value ((-100,100])*/ CB.Value.number({ + name: 'Favorite Number', + description: 'Your favorite number of all time', + warning: + 'Once you set this number, it can never be changed without severe consequences.', + required: { + default: 7, + }, + integer: false, + units: 'BTC', + }), + rpcsettings: CB.Value.object( + { + name: 'RPC Settings', + description: 'rpc username and password', + warning: + 'Adding RPC users gives them special permissions on your node.', + }, + CB.Config.of({ + laws: CB.Value.object( + { + name: 'Laws', + description: 'the law of the realm', + }, + CB.Config.of({ + law1: CB.Value.text({ + name: 'First Law', + required: false, + description: 'the first law', + }), + law2: CB.Value.text({ + name: 'Second Law', + required: false, + description: 'the second law', + }), + }), + ), + rulemakers: CB.Value.list( + CB.List.obj( + { + name: 'Rule Makers', + minLength: 0, + maxLength: 2, + description: 'the people who make the rules', + }, + { + spec: CB.Config.of({ + rulemakername: CB.Value.text({ + name: 'Rulemaker Name', + required: { + default: { + charset: 'a-g,2-9', + len: 12, + }, + }, + description: 'the name of the rule maker', + }), + rulemakerip: CB.Value.text({ + name: 'Rulemaker IP', + required: { + default: '192.168.1.0', + }, + description: 'the ip of the rule maker', + patterns: [ + { + regex: + '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$', + description: 'may only contain numbers and periods', + }, + ], + }), + }), + }, + ), + ), + rpcuser: CB.Value.text({ + name: 'RPC Username', + required: { + default: 'defaultrpcusername', + }, + description: 'rpc username', + patterns: [ + { + regex: '^[a-zA-Z]+$', + description: 'must contain only letters.', + }, + ], + }), + rpcpass: CB.Value.text({ + name: 'RPC User Password', + required: { + default: { + charset: 'a-z,A-Z,2-9', + len: 20, }, }, - }, - rpcuser: { - name: 'RPC Username', - type: 'string', - description: 'rpc username', - nullable: false, - default: 'defaultrpcusername', - pattern: '^[a-zA-Z]+$', - 'pattern-description': 'must contain only letters.', - masked: false, - copyable: true, - }, - rpcpass: { - name: 'RPC User Password', - type: 'string', description: 'rpc password', - nullable: false, - default: { - charset: 'a-z,A-Z,2-9', - len: 20, - }, masked: true, - copyable: true, - }, + }), + }), + ), + 'bitcoin-node': CB.Value.union( + { + name: 'Bitcoin Node', + description: 'Options
  • Item 1
  • Item 2
', + warning: 'Careful changing this', + required: { default: 'internal' }, + disabled: ['fake'], }, - }, - }, - }, - } + CB.Variants.of({ + fake: { + name: 'Fake', + spec: CB.Config.of({}), + }, + internal: { + name: 'Internal', + spec: CB.Config.of({}), + }, + external: { + name: 'External', + spec: CB.Config.of({ + 'emergency-contact': CB.Value.object( + { + name: 'Emergency Contact', + description: 'The person to contact in case of emergency.', + }, + CB.Config.of({ + name: CB.Value.text({ + name: 'Name', + required: { + default: null, + }, + patterns: [ + { + regex: '^[a-zA-Z]+$', + description: 'Must contain only letters.', + }, + ], + }), + email: CB.Value.text({ + name: 'Email', + inputmode: 'email', + required: { + default: null, + }, + }), + }), + ), + 'public-domain': CB.Value.text({ + name: 'Public Domain', + required: { + default: 'bitcoinnode.com', + }, + description: 'the public address of the node', + patterns: [ + { + regex: '.*', + description: 'anything', + }, + ], + }), + 'private-domain': CB.Value.text({ + name: 'Private Domain', + required: { + default: null, + }, + description: 'the private address of the node', + masked: true, + inputmode: 'url', + }), + }), + }, + }), + ), + port: CB.Value.number({ + name: 'Port', + description: + 'the default port for your Bitcoin node. default: 8333, testnet: 18333, regtest: 18444', + required: { + default: 8333, + }, + min: 1, + max: 9998, + step: 1, + integer: true, + }), + 'favorite-slogan': CB.Value.text({ + name: 'Favorite Slogan', + generate: { + charset: 'a-z,A-Z,2-9', + len: 20, + }, + required: false, + description: + 'You most favorite slogan in the whole world, used for paying you.', + masked: true, + }), + rpcallowip: CB.Value.list( + CB.List.text( + { + name: 'RPC Allowed IPs', + minLength: 1, + maxLength: 10, + default: ['192.168.1.1'], + description: + 'external ip addresses that are authorized to access your Bitcoin node', + warning: + 'Any IP you allow here will have RPC access to your Bitcoin node.', + }, + { + patterns: [ + { + regex: + '((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))', + description: 'must be a valid ipv4, ipv6, or domain name', + }, + ], + }, + ), + ), + rpcauth: CB.Value.list( + CB.List.text( + { + name: 'RPC Auth', + description: + 'api keys that are authorized to access your Bitcoin node.', + }, + { + patterns: [], + }, + ), + ), + }), + ) export const MockConfig = { testnet: undefined, @@ -1381,8 +1243,7 @@ export module Mock { age: 60, }, ], - 'union-list': undefined, - 'random-enum': 'option2', + 'random-select': ['goodbye'], 'favorite-number': 0, rpcsettings: { laws: { @@ -1394,7 +1255,7 @@ export module Mock { rulemakers: [], }, 'bitcoin-node': { - type: 'internal', + selection: 'internal', }, port: 20, rpcallowip: undefined, @@ -1477,7 +1338,107 @@ export module Mock { }, }, currentDependencies: {}, - hosts: {}, + hosts: { + abcdefg: { + kind: 'multi', + bindings: [], + addresses: [], + hostnameInfo: { + 80: [ + { + kind: 'ip', + networkInterfaceId: 'eth0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: null, + sslPort: 1234, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'wlan0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: null, + sslPort: 1234, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'eth0', + public: false, + hostname: { + kind: 'ipv4', + value: '192.168.10.11', + port: null, + sslPort: 1234, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'wlan0', + public: false, + hostname: { + kind: 'ipv4', + value: '10.0.0.2', + port: null, + sslPort: 1234, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'eth0', + public: false, + hostname: { + kind: 'ipv6', + value: '[FE80:CD00:0000:0CDE:1257:0000:211E:729CD]', + port: null, + sslPort: 1234, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'wlan0', + public: false, + hostname: { + kind: 'ipv6', + value: '[FE80:CD00:0000:0CDE:1257:0000:211E:1234]', + port: null, + sslPort: 1234, + }, + }, + { + kind: 'onion', + hostname: { + value: 'bitcoin-p2p.onion', + port: 80, + sslPort: 443, + }, + }, + ], + }, + }, + bcdefgh: { + kind: 'multi', + bindings: [], + addresses: [], + hostnameInfo: { + 8332: [], + }, + }, + cdefghi: { + kind: 'multi', + bindings: [], + addresses: [], + hostnameInfo: { + 8333: [], + }, + }, + }, storeExposedDependents: [], registry: 'https://registry.start9.com/', developerKey: 'developer-key', diff --git a/web/projects/ui/src/app/services/api/api.types.ts b/web/projects/ui/src/app/services/api/api.types.ts index 96be4850b..10e8892fb 100644 --- a/web/projects/ui/src/app/services/api/api.types.ts +++ b/web/projects/ui/src/app/services/api/api.types.ts @@ -1,10 +1,9 @@ import { Dump } from 'patch-db-client' import { MarketplacePkg, StoreInfo } from '@start9labs/marketplace' import { PackagePropertiesVersioned } from 'src/app/util/properties.util' -import { ConfigSpec } from 'src/app/pkg-config/config-types' import { DataModel } from 'src/app/services/patch-db/data-model' import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared' -import { T } from '@start9labs/start-sdk' +import { CT, T } from '@start9labs/start-sdk' import { WebSocketSubjectConfig } from 'rxjs/webSocket' export module RR { @@ -223,7 +222,7 @@ export module RR { export type InstallPackageRes = null export type GetPackageConfigReq = { id: string } // package.config.get - export type GetPackageConfigRes = { spec: ConfigSpec; config: object } + export type GetPackageConfigRes = { spec: CT.InputSpec; config: object } export type DrySetPackageConfigReq = { id: string; config: object } // package.config.set.dry export type DrySetPackageConfigRes = Breakages @@ -266,7 +265,7 @@ export module RR { export type DryConfigureDependencyRes = { oldConfig: object newConfig: object - spec: ConfigSpec + spec: CT.InputSpec } export type SideloadPackageReq = { diff --git a/web/projects/ui/src/app/services/api/embassy-api.service.ts b/web/projects/ui/src/app/services/api/embassy-api.service.ts index 735b8f1d1..a5bce8c62 100644 --- a/web/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-api.service.ts @@ -10,6 +10,8 @@ export abstract class ApiService { // for sideloading packages abstract uploadPackage(guid: string, body: Blob): Promise + abstract uploadFile(body: Blob): Promise + // websocket abstract openWebsocket$( diff --git a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts index 8cdd0d5e3..f25ee86ca 100644 --- a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -29,7 +29,7 @@ export class LiveApiService extends ApiService { @Inject(PATCH_CACHE) private readonly cache$: Observable>, ) { super() - ;(window as any).rpcClient = this + ; (window as any).rpcClient = this } // for getting static files: ex icons, instructions, licenses @@ -53,6 +53,15 @@ export class LiveApiService extends ApiService { }) } + async uploadFile(body: Blob): Promise { + return this.httpRequest({ + method: Method.POST, + body, + url: `/rest/upload`, + responseType: 'text', + }) + } + // websocket openWebsocket$( diff --git a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts index 0a82ac850..284e3bab8 100644 --- a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -768,7 +768,7 @@ export class MockApiService extends ApiService { await pauseFor(2000) return { config: Mock.MockConfig, - spec: Mock.ConfigSpec, + spec: await Mock.getInputSpec(), } } @@ -1058,7 +1058,7 @@ export class MockApiService extends ApiService { return { oldConfig: Mock.MockConfig, newConfig: Mock.MockDependencyConfig, - spec: Mock.ConfigSpec, + spec: await Mock.getInputSpec(), } } @@ -1070,6 +1070,11 @@ export class MockApiService extends ApiService { } } + async uploadFile(body: Blob): Promise { + await pauseFor(2000) + return 'returnedhash' + } + private async initProgress(): Promise { const progress = JSON.parse(JSON.stringify(PROGRESS)) diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 755799522..e24556771 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -185,7 +185,107 @@ export const mockPatchData: DataModel = { }, }, currentDependencies: {}, - hosts: {}, + hosts: { + abcdefg: { + kind: 'multi', + bindings: [], + addresses: [], + hostnameInfo: { + 80: [ + { + kind: 'ip', + networkInterfaceId: 'eth0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: null, + sslPort: 1234, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'wlan0', + public: false, + hostname: { + kind: 'local', + value: 'adjective-noun.local', + port: null, + sslPort: 1234, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'eth0', + public: false, + hostname: { + kind: 'ipv4', + value: '10.0.0.1', + port: null, + sslPort: 1234, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'wlan0', + public: false, + hostname: { + kind: 'ipv4', + value: '10.0.0.2', + port: null, + sslPort: 1234, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'eth0', + public: false, + hostname: { + kind: 'ipv6', + value: '[FE80:CD00:0000:0CDE:1257:0000:211E:729CD]', + port: null, + sslPort: 1234, + }, + }, + { + kind: 'ip', + networkInterfaceId: 'wlan0', + public: false, + hostname: { + kind: 'ipv6', + value: '[FE80:CD00:0000:0CDE:1257:0000:211E:1234]', + port: null, + sslPort: 1234, + }, + }, + { + kind: 'onion', + hostname: { + value: 'bitcoin-p2p.onion', + port: 80, + sslPort: 443, + }, + }, + ], + }, + }, + bcdefgh: { + kind: 'multi', + bindings: [], + addresses: [], + hostnameInfo: { + 8332: [], + }, + }, + cdefghi: { + kind: 'multi', + bindings: [], + addresses: [], + hostnameInfo: { + 8333: [], + }, + }, + }, storeExposedDependents: [], registry: 'https://registry.start9.com/', developerKey: 'developer-key', diff --git a/web/projects/ui/src/app/services/form-dialog.service.ts b/web/projects/ui/src/app/services/form-dialog.service.ts new file mode 100644 index 000000000..69df946bb --- /dev/null +++ b/web/projects/ui/src/app/services/form-dialog.service.ts @@ -0,0 +1,41 @@ +import { inject, Injectable, Injector, Type } from '@angular/core' +import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core' +import { TuiDialogFormService, TuiPromptData } from '@taiga-ui/kit' +import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' + +const PROMPT: Partial> = { + label: 'Unsaved Changes', + data: { + content: 'You have unsaved changes. Are you sure you want to leave?', + yes: 'Leave', + no: 'Cancel', + }, +} + +@Injectable({ providedIn: 'root' }) +export class FormDialogService { + private readonly dialogs = inject(TuiDialogService) + private readonly formService = new TuiDialogFormService(this.dialogs) + private readonly prompt = this.formService.withPrompt(PROMPT) + private readonly injector = Injector.create({ + parent: inject(Injector), + providers: [ + { + provide: TuiDialogFormService, + useValue: this.formService, + }, + ], + }) + + open(component: Type, options: Partial> = {}) { + this.dialogs + .open(new PolymorpheusComponent(component, this.injector), { + closeable: this.prompt, + dismissible: this.prompt, + ...options, + }) + .subscribe({ + complete: () => this.formService.markAsPristine(), + }) + } +} diff --git a/web/projects/ui/src/app/services/form.service.ts b/web/projects/ui/src/app/services/form.service.ts index ee8f0f136..3cd0ee591 100644 --- a/web/projects/ui/src/app/services/form.service.ts +++ b/web/projects/ui/src/app/services/form.service.ts @@ -7,24 +7,7 @@ import { ValidatorFn, Validators, } from '@angular/forms' -import { - ConfigSpec, - isValueSpecListOf, - ListValueSpecNumber, - ListValueSpecObject, - ListValueSpecOf, - ListValueSpecString, - ListValueSpecUnion, - UniqueBy, - ValueSpec, - ValueSpecEnum, - ValueSpecList, - ValueSpecNumber, - ValueSpecObject, - ValueSpecString, - ValueSpecUnion, -} from 'src/app/pkg-config/config-types' -import { getDefaultString, Range } from '../pkg-config/config-utilities' +import { CT, utils } from '@start9labs/start-sdk' const Mustache = require('mustache') @Injectable({ @@ -34,55 +17,54 @@ export class FormService { constructor(private readonly formBuilder: UntypedFormBuilder) {} createForm( - spec: ConfigSpec, - current: { [key: string]: any } = {}, + spec: CT.InputSpec, + current: Record = {}, ): UntypedFormGroup { return this.getFormGroup(spec, [], current) } - getUnionObject( - spec: ValueSpecUnion | ListValueSpecUnion, - selection: string, - current?: { [key: string]: any } | null, - ): UntypedFormGroup { - const { variants, tag } = spec - const { name, description, warning, 'variant-names': variantNames } = tag - - const enumSpec: ValueSpecEnum = { - type: 'enum', - name, - description, - warning, + getUnionSelectSpec( + spec: CT.ValueSpecUnion, + selection: string | null, + ): CT.ValueSpecSelect { + return { + ...spec, + type: 'select', default: selection, - values: Object.keys(variants), - 'value-names': variantNames, + values: Object.fromEntries( + Object.entries(spec.variants).map(([key, { name }]) => [key, name]), + ), } - return this.getFormGroup( - { [spec.tag.id]: enumSpec, ...spec.variants[selection] }, - [], - current, + } + + getUnionObject( + spec: CT.ValueSpecUnion, + selected: string | null, + ): UntypedFormGroup { + const group = this.getFormGroup({ + selection: this.getUnionSelectSpec(spec, selected), + }) + + group.setControl( + 'value', + this.getFormGroup(selected ? spec.variants[selected].spec : {}), ) + + return group } - getListItem(spec: ValueSpecList, entry: any) { - const listItemValidators = getListItemValidators(spec) - if (isValueSpecListOf(spec, 'string')) { - return this.formBuilder.control(entry, listItemValidators) - } else if (isValueSpecListOf(spec, 'number')) { - return this.formBuilder.control(entry, listItemValidators) - } else if (isValueSpecListOf(spec, 'enum')) { - return this.formBuilder.control(entry) - } else if (isValueSpecListOf(spec, 'object')) { - return this.getFormGroup(spec.spec.spec, listItemValidators, entry) - } else if (isValueSpecListOf(spec, 'union')) { - return this.getUnionObject(spec.spec, spec.spec.default, entry) + getListItem(spec: CT.ValueSpecList, entry?: any) { + if (CT.isValueSpecListOf(spec, 'text')) { + return this.formBuilder.control(entry, stringValidators(spec.spec)) + } else if (CT.isValueSpecListOf(spec, 'object')) { + return this.getFormGroup(spec.spec.spec, [], entry) } } - private getFormGroup( - config: ConfigSpec, + getFormGroup( + config: CT.InputSpec, validators: ValidatorFn[] = [], - current?: { [key: string]: any } | null, + current?: Record | null, ): UntypedFormGroup { let group: Record< string, @@ -95,150 +77,281 @@ export class FormService { } private getFormEntry( - spec: ValueSpec, + spec: CT.ValueSpec, currentValue?: any, ): UntypedFormGroup | UntypedFormArray | UntypedFormControl { - let validators: ValidatorFn[] let value: any switch (spec.type) { - case 'string': - validators = stringValidators(spec) + case 'text': if (currentValue !== undefined) { value = currentValue } else { - value = spec.default ? getDefaultString(spec.default) : null + value = spec.default ? utils.getDefaultString(spec.default) : null } - return this.formBuilder.control(value, validators) + return this.formBuilder.control(value, stringValidators(spec)) + case 'textarea': + value = currentValue || null + return this.formBuilder.control(value, textareaValidators(spec)) case 'number': - validators = numberValidators(spec) if (currentValue !== undefined) { value = currentValue } else { value = spec.default || null } - return this.formBuilder.control(value, validators) + return this.formBuilder.control(value, numberValidators(spec)) + case 'color': + if (currentValue !== undefined) { + value = currentValue + } else { + value = spec.default || null + } + return this.formBuilder.control(value, colorValidators(spec)) + case 'datetime': + if (currentValue !== undefined) { + value = currentValue + } else { + value = spec.default || null + } + return this.formBuilder.control(value, datetimeValidators(spec)) case 'object': return this.getFormGroup(spec.spec, [], currentValue) case 'list': - validators = listValidators(spec) const mapped = ( Array.isArray(currentValue) ? currentValue : (spec.default as any[]) ).map(entry => { return this.getListItem(spec, entry) }) - return this.formBuilder.array(mapped, validators) + return this.formBuilder.array(mapped, listValidators(spec)) + case 'file': + return this.formBuilder.control( + currentValue || null, + fileValidators(spec), + ) case 'union': - const currentSelection = currentValue?.[spec.tag.id] + const currentSelection = currentValue?.selection const isValid = !!spec.variants[currentSelection] return this.getUnionObject( spec, isValid ? currentSelection : spec.default, - isValid ? currentValue : undefined, ) - case 'boolean': - case 'enum': + case 'toggle': value = currentValue === undefined ? spec.default : currentValue return this.formBuilder.control(value) + case 'select': + value = currentValue === undefined ? spec.default : currentValue + return this.formBuilder.control(value, selectValidators(spec)) + case 'multiselect': + value = currentValue === undefined ? spec.default : currentValue + return this.formBuilder.control(value, multiselectValidators(spec)) default: return this.formBuilder.control(null) } } } -function getListItemValidators(spec: ValueSpecList) { - if (isValueSpecListOf(spec, 'string')) { - return stringValidators(spec.spec) - } else if (isValueSpecListOf(spec, 'number')) { - return numberValidators(spec.spec) - } -} +// function getListItemValidators(spec: CT.ValueSpecList) { +// if (CT.isValueSpecListOf(spec, 'text')) { +// return stringValidators(spec.spec) +// } +// } function stringValidators( - spec: ValueSpecString | ListValueSpecString, + spec: CT.ValueSpecText | CT.ListValueSpecText, ): ValidatorFn[] { const validators: ValidatorFn[] = [] - if (!(spec as ValueSpecString).nullable) { + if ((spec as CT.ValueSpecText).required) { validators.push(Validators.required) } - if (spec.pattern) { - validators.push(Validators.pattern(spec.pattern)) + validators.push(textLengthInRange(spec.minLength, spec.maxLength)) + + if (spec.patterns.length) { + spec.patterns.forEach(p => validators.push(Validators.pattern(p.regex))) } return validators } -function numberValidators( - spec: ValueSpecNumber | ListValueSpecNumber, -): ValidatorFn[] { +function textareaValidators(spec: CT.ValueSpecTextarea): ValidatorFn[] { + const validators: ValidatorFn[] = [] + + if (spec.required) { + validators.push(Validators.required) + } + + validators.push(textLengthInRange(spec.minLength, spec.maxLength)) + + return validators +} + +function colorValidators({ required }: CT.ValueSpecColor): ValidatorFn[] { + const validators: ValidatorFn[] = [Validators.pattern(/^#[0-9a-f]{6}$/i)] + + if (required) { + validators.push(Validators.required) + } + + return validators +} + +function datetimeValidators({ + required, + min, + max, +}: CT.ValueSpecDatetime): ValidatorFn[] { + const validators: ValidatorFn[] = [] + + if (required) { + validators.push(Validators.required) + } + + if (min) { + validators.push(datetimeMin(min)) + } + + if (max) { + validators.push(datetimeMax(max)) + } + + return validators +} + +function numberValidators(spec: CT.ValueSpecNumber): ValidatorFn[] { const validators: ValidatorFn[] = [] validators.push(isNumber()) - if (!(spec as ValueSpecNumber).nullable) { + if ((spec as CT.ValueSpecNumber).required) { validators.push(Validators.required) } - if (spec.integral) { + if (spec.integer) { validators.push(isInteger()) } - validators.push(numberInRange(spec.range)) + validators.push(numberInRange(spec.min, spec.max)) return validators } -function listValidators(spec: ValueSpecList): ValidatorFn[] { +function selectValidators(spec: CT.ValueSpecSelect): ValidatorFn[] { const validators: ValidatorFn[] = [] - validators.push(listInRange(spec.range)) - - validators.push(listItemIssue()) - - if (!isValueSpecListOf(spec, 'enum')) { - validators.push(listUnique(spec)) + if (spec.required) { + validators.push(Validators.required) } return validators } -export function numberInRange(stringRange: string): ValidatorFn { +function multiselectValidators(spec: CT.ValueSpecMultiselect): ValidatorFn[] { + const validators: ValidatorFn[] = [] + validators.push(listInRange(spec.minLength, spec.maxLength)) + return validators +} + +function listValidators(spec: CT.ValueSpecList): ValidatorFn[] { + const validators: ValidatorFn[] = [] + validators.push(listInRange(spec.minLength, spec.maxLength)) + validators.push(listItemIssue()) + return validators +} + +function fileValidators(spec: CT.ValueSpecFile): ValidatorFn[] { + const validators: ValidatorFn[] = [] + + if (spec.required) { + validators.push(Validators.required) + } + + return validators +} + +export function numberInRange( + min: number | null, + max: number | null, +): ValidatorFn { return control => { const value = control.value - if (!value) return null - try { - Range.from(stringRange).checkIncludes(value) - return null - } catch (e: any) { - return { numberNotInRange: { value: `Number must be ${e.message}` } } - } + if (typeof value !== 'number') return null + if (min && value < min) + return { + numberNotInRange: `Number must be greater than or equal to ${min}`, + } + if (max && value > max) + return { numberNotInRange: `Number must be less than or equal to ${max}` } + return null } } export function isNumber(): ValidatorFn { - return control => - !control.value || control.value == Number(control.value) - ? null - : { notNumber: { value: control.value } } + return ({ value }) => + !value || value == Number(value) ? null : { notNumber: 'Must be a number' } } export function isInteger(): ValidatorFn { - return control => - !control.value || control.value == Math.trunc(control.value) + return ({ value }) => + !value || value == Math.trunc(value) ? null - : { numberNotInteger: { value: control.value } } + : { numberNotInteger: 'Must be an integer' } } -export function listInRange(stringRange: string): ValidatorFn { +export function listInRange( + minLength: number | null, + maxLength: number | null, +): ValidatorFn { return control => { - try { - Range.from(stringRange).checkIncludes(control.value.length) - return null - } catch (e: any) { - return { listNotInRange: { value: `List must be ${e.message}` } } - } + const length = control.value.length + if (minLength && length < minLength) + return { + listNotInRange: `List must contain at least ${minLength} entries`, + } + if (maxLength && length > maxLength) + return { + listNotInRange: `List cannot contain more than ${maxLength} entries`, + } + return null + } +} + +export function datetimeMin(min: string): ValidatorFn { + return ({ value }) => { + if (!value) return null + + const date = new Date(value.length === 5 ? `2000-01-01T${value}` : value) + const minDate = new Date(min.length === 5 ? `2000-01-01T${min}` : min) + + return date < minDate ? { datetimeMin: `Minimum is ${min}` } : null + } +} + +export function datetimeMax(max: string): ValidatorFn { + return ({ value }) => { + if (!value) return null + + const date = new Date(value.length === 5 ? `2000-01-01T${value}` : value) + const maxDate = new Date(max.length === 5 ? `2000-01-01T${max}` : max) + + return date > maxDate ? { datetimeMin: `Maximum is ${max}` } : null + } +} + +export function textLengthInRange( + minLength: number | null, + maxLength: number | null, +): ValidatorFn { + return control => { + const value = control.value + if (value === null || value === undefined) return null + + const length = value.length + if (minLength && length < minLength) + return { listNotInRange: `Must be at least ${minLength} characters` } + if (maxLength && length > maxLength) + return { listNotInRange: `Cannot be great than ${maxLength} characters` } + return null } } @@ -247,36 +360,33 @@ export function listItemIssue(): ValidatorFn { const { controls } = parentControl as UntypedFormArray const problemChild = controls.find(c => c.invalid) if (problemChild) { - return { listItemIssue: { value: 'Invalid entries' } } + return { listItemIssue: 'Invalid entries' } } else { return null } } } -export function listUnique(spec: ValueSpecList): ValidatorFn { +export function listUnique(spec: CT.ValueSpecList): ValidatorFn { return control => { const list = control.value for (let idx = 0; idx < list.length; idx++) { for (let idx2 = idx + 1; idx2 < list.length; idx2++) { if (listItemEquals(spec, list[idx], list[idx2])) { + const objSpec = spec.spec let display1: string let display2: string - let uniqueMessage = isObjectOrUnion(spec.spec) - ? uniqueByMessageWrapper( - spec.spec['unique-by'], - spec.spec, - list[idx], - ) + let uniqueMessage = isObject(objSpec) + ? uniqueByMessageWrapper(objSpec.uniqueBy, objSpec) : '' - if (isObjectOrUnion(spec.spec) && spec.spec['display-as']) { + if (isObject(objSpec) && objSpec.displayAs) { display1 = `"${(Mustache as any).render( - spec.spec['display-as'], + objSpec.displayAs, list[idx], )}"` display2 = `"${(Mustache as any).render( - spec.spec['display-as'], + objSpec.displayAs, list[idx2], )}"` } else { @@ -285,9 +395,7 @@ export function listUnique(spec: ValueSpecList): ValidatorFn { } return { - listNotUnique: { - value: `${display1} and ${display2} are not unique.${uniqueMessage}`, - }, + listNotUnique: `${display1} and ${display2} are not unique.${uniqueMessage}`, } } } @@ -296,46 +404,40 @@ export function listUnique(spec: ValueSpecList): ValidatorFn { } } -function listItemEquals(spec: ValueSpecList, val1: any, val2: any): boolean { +function listItemEquals(spec: CT.ValueSpecList, val1: any, val2: any): boolean { // TODO: fix types - switch (spec.subtype) { - case 'string': - case 'number': - case 'enum': + switch (spec.spec.type) { + case 'text': return val1 == val2 case 'object': - const obj: ListValueSpecObject = spec.spec as any - - return listObjEquals(obj['unique-by'], obj, val1, val2) - case 'union': - const union: ListValueSpecUnion = spec.spec as any - - return unionEquals(union['unique-by'], union, val1, val2) + const obj = spec.spec + return listObjEquals(obj.uniqueBy, obj, val1, val2) default: return false } } -function itemEquals(spec: ValueSpec, val1: any, val2: any): boolean { +function itemEquals(spec: CT.ValueSpec, val1: any, val2: any): boolean { switch (spec.type) { - case 'string': + case 'text': + case 'textarea': case 'number': - case 'boolean': - case 'enum': + case 'toggle': + case 'select': return val1 == val2 case 'object': // TODO: 'unique-by' does not exist on ValueSpecObject, fix types return objEquals( (spec as any)['unique-by'], - spec as ValueSpecObject, + spec as CT.ValueSpecObject, val1, val2, ) case 'union': - // TODO: 'unique-by' does not exist on ValueSpecUnion, fix types + // TODO: 'unique-by' does not exist on CT.ValueSpecUnion, fix types return unionEquals( (spec as any)['unique-by'], - spec as ValueSpecUnion, + spec as CT.ValueSpecUnion, val1, val2, ) @@ -355,12 +457,12 @@ function itemEquals(spec: ValueSpec, val1: any, val2: any): boolean { } function listObjEquals( - uniqueBy: UniqueBy, - spec: ListValueSpecObject, + uniqueBy: CT.UniqueBy, + spec: CT.ListValueSpecObject, val1: any, val2: any, ): boolean { - if (uniqueBy === null) { + if (!uniqueBy) { return false } else if (typeof uniqueBy === 'string') { return itemEquals(spec.spec[uniqueBy], val1[uniqueBy], val2[uniqueBy]) @@ -383,12 +485,12 @@ function listObjEquals( } function objEquals( - uniqueBy: UniqueBy, - spec: ValueSpecObject, + uniqueBy: CT.UniqueBy, + spec: CT.ValueSpecObject, val1: any, val2: any, ): boolean { - if (uniqueBy === null) { + if (!uniqueBy) { return false } else if (typeof uniqueBy === 'string') { // TODO: fix types @@ -412,20 +514,19 @@ function objEquals( } function unionEquals( - uniqueBy: UniqueBy, - spec: ValueSpecUnion | ListValueSpecUnion, + uniqueBy: CT.UniqueBy, + spec: CT.ValueSpecUnion, val1: any, val2: any, ): boolean { - const tagId = spec.tag.id - const variant = spec.variants[val1[tagId]] - if (uniqueBy === null) { + const variantSpec = spec.variants[val1.selection].spec + if (!uniqueBy) { return false } else if (typeof uniqueBy === 'string') { - if (uniqueBy === tagId) { - return val1[tagId] === val2[tagId] + if (uniqueBy === 'selection') { + return val1.selection === val2.selection } else { - return itemEquals(variant[uniqueBy], val1[uniqueBy], val2[uniqueBy]) + return itemEquals(variantSpec[uniqueBy], val1[uniqueBy], val2[uniqueBy]) } } else if ('any' in uniqueBy) { for (let subSpec of uniqueBy.any) { @@ -446,20 +547,10 @@ function unionEquals( } function uniqueByMessageWrapper( - uniqueBy: UniqueBy, - spec: ListValueSpecObject | ListValueSpecUnion, - obj: Record, + uniqueBy: CT.UniqueBy, + spec: CT.ListValueSpecObject, ) { - let configSpec: ConfigSpec - if (isUnion(spec)) { - const tagId = spec.tag.id - configSpec = { - [tagId]: { name: spec.tag.name } as ValueSpec, - ...spec.variants[obj[tagId]], - } - } else { - configSpec = spec.spec - } + let configSpec = spec.spec const message = uniqueByMessage(uniqueBy, configSpec) if (message) { @@ -468,17 +559,17 @@ function uniqueByMessageWrapper( } function uniqueByMessage( - uniqueBy: UniqueBy, - configSpec: ConfigSpec, + uniqueBy: CT.UniqueBy, + configSpec: CT.InputSpec, outermost = true, ): string { let joinFunc const subSpecs: string[] = [] - if (uniqueBy === null) { + if (!uniqueBy) { return '' } else if (typeof uniqueBy === 'string') { return configSpec[uniqueBy] - ? (configSpec[uniqueBy] as ValueSpecObject).name + ? (configSpec[uniqueBy] as CT.ValueSpecObject).name : uniqueBy } else if ('any' in uniqueBy) { joinFunc = ' OR ' @@ -497,20 +588,15 @@ function uniqueByMessage( : '(' + ret + ')' } -function isObjectOrUnion( - spec: ListValueSpecOf, -): spec is ListValueSpecObject | ListValueSpecUnion { - // only lists of objects and unions have unique-by - return 'unique-by' in spec -} - -function isUnion(spec: any): spec is ListValueSpecUnion { - // only unions have tag - return !!spec.tag +function isObject( + spec: CT.ListValueSpecOf, +): spec is CT.ListValueSpecObject { + // only lists of objects have uniqueBy + return 'uniqueBy' in spec } export function convertValuesRecursive( - configSpec: ConfigSpec, + configSpec: CT.InputSpec, group: UntypedFormGroup, ) { Object.entries(configSpec).forEach(([key, valueSpec]) => { @@ -522,40 +608,27 @@ export function convertValuesRecursive( control.setValue( control.value || control.value === 0 ? Number(control.value) : null, ) - } else if (valueSpec.type === 'string') { + } else if (valueSpec.type === 'text' || valueSpec.type === 'textarea') { if (!control.value) control.setValue(null) } else if (valueSpec.type === 'object') { convertValuesRecursive(valueSpec.spec, group.get(key) as UntypedFormGroup) } else if (valueSpec.type === 'union') { const formGr = group.get(key) as UntypedFormGroup - const spec = valueSpec.variants[formGr.controls[valueSpec.tag.id].value] + const spec = valueSpec.variants[formGr.controls['selection'].value].spec convertValuesRecursive(spec, formGr) } else if (valueSpec.type === 'list') { const formArr = group.get(key) as UntypedFormArray const { controls } = formArr - if (valueSpec.subtype === 'number') { - controls.forEach(control => { - control.setValue(control.value ? Number(control.value) : null) - }) - } else if (valueSpec.subtype === 'string') { + if (valueSpec.spec.type === 'text') { controls.forEach(control => { if (!control.value) control.setValue(null) }) - } else if (valueSpec.subtype === 'object') { + } else if (valueSpec.spec.type === 'object') { controls.forEach(formGroup => { - const objectSpec = valueSpec.spec as ListValueSpecObject + const objectSpec = valueSpec.spec as CT.ListValueSpecObject convertValuesRecursive(objectSpec.spec, formGroup as UntypedFormGroup) }) - } else if (valueSpec.subtype === 'union') { - controls.forEach(formGroup => { - const unionSpec = valueSpec.spec as ListValueSpecUnion - const spec = - unionSpec.variants[ - (formGroup as UntypedFormGroup).controls[unionSpec.tag.id].value - ] - convertValuesRecursive(spec, formGroup as UntypedFormGroup) - }) } } }) diff --git a/web/projects/ui/src/app/services/modal.service.ts b/web/projects/ui/src/app/services/modal.service.ts deleted file mode 100644 index c34fce9a2..000000000 --- a/web/projects/ui/src/app/services/modal.service.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Injectable } from '@angular/core' -import { ModalController } from '@ionic/angular' -import { DependentInfo } from 'src/app/types/dependent-info' -import { AppConfigPage } from 'src/app/modals/app-config/app-config.page' - -@Injectable({ - providedIn: 'root', -}) -export class ModalService { - constructor(private readonly modalCtrl: ModalController) {} - - async presentModalConfig(componentProps: ComponentProps): Promise { - const modal = await this.modalCtrl.create({ - component: AppConfigPage, - componentProps, - }) - await modal.present() - } -} - -interface ComponentProps { - pkgId: string - dependentInfo?: DependentInfo -} diff --git a/web/projects/ui/src/app/util/configBuilderToSpec.ts b/web/projects/ui/src/app/util/configBuilderToSpec.ts new file mode 100644 index 000000000..1f75329c5 --- /dev/null +++ b/web/projects/ui/src/app/util/configBuilderToSpec.ts @@ -0,0 +1,9 @@ +import { CB } from '@start9labs/start-sdk' + +export async function configBuilderToSpec( + builder: + | CB.Config, unknown> + | CB.Config, never>, +) { + return builder.build({} as any) +} diff --git a/web/projects/ui/src/index.html b/web/projects/ui/src/index.html index 0248b7462..9c0cc2e89 100644 --- a/web/projects/ui/src/index.html +++ b/web/projects/ui/src/index.html @@ -21,10 +21,36 @@ /> + - + + Start OS +

Loading

+ +
diff --git a/web/projects/ui/src/styles.scss b/web/projects/ui/src/styles.scss index a00ca4ae2..fa4a6598e 100644 --- a/web/projects/ui/src/styles.scss +++ b/web/projects/ui/src/styles.scss @@ -347,4 +347,15 @@ p { svg:not(:root) { overflow: auto; -} \ No newline at end of file +} + +tui-dialog { + transform: translate3d(0, 0, 0); +} + +.g-buttons { + display: flex; + justify-content: flex-end; + gap: 16px; + margin-top: 24px; +} From e6cedc257e629da1ed1a7541bd8173ebf4fab072 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Wed, 10 Jul 2024 13:00:02 -0600 Subject: [PATCH 055/125] add boot param to logs request --- core/startos/src/logs.rs | 127 ++++++++++++++++++++++++++++++++++-- core/startos/src/net/tor.rs | 10 ++- 2 files changed, 130 insertions(+), 7 deletions(-) diff --git a/core/startos/src/logs.rs b/core/startos/src/logs.rs index bb1198451..751dd243a 100644 --- a/core/startos/src/logs.rs +++ b/core/startos/src/logs.rs @@ -1,9 +1,12 @@ +use std::convert::Infallible; use std::ops::{Deref, DerefMut}; use std::process::Stdio; +use std::str::FromStr; use std::time::{Duration, UNIX_EPOCH}; use axum::extract::ws::{self, WebSocket}; use chrono::{DateTime, Utc}; +use clap::builder::ValueParserFactory; use clap::{Args, FromArgMatches, Parser}; use color_eyre::eyre::eyre; use futures::stream::BoxStream; @@ -14,7 +17,7 @@ use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::{ from_fn_async, CallRemote, Context, Empty, HandlerArgs, HandlerExt, HandlerFor, ParentHandler, }; -use serde::de::DeserializeOwned; +use serde::de::{self, DeserializeOwned}; use serde::{Deserialize, Serialize}; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::{Child, Command}; @@ -27,6 +30,7 @@ use crate::error::ResultExt; use crate::lxc::ContainerId; use crate::prelude::*; use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations}; +use crate::util::clap::FromStrParser; use crate::util::serde::Reversible; use crate::util::Invoke; @@ -227,6 +231,91 @@ pub struct PackageIdParams { id: PackageId, } +#[derive(Debug, Clone)] +pub enum BootIdentifier { + Index(i32), + Id(String), +} +impl FromStr for BootIdentifier { + type Err = Infallible; + fn from_str(s: &str) -> Result { + Ok(match s.parse() { + Ok(i) => Self::Index(i), + Err(_) => Self::Id(s.to_owned()), + }) + } +} +impl ValueParserFactory for BootIdentifier { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + Self::Parser::new() + } +} +impl Serialize for BootIdentifier { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::Index(i) => serializer.serialize_i32(*i), + Self::Id(i) => serializer.serialize_str(i), + } + } +} +impl<'de> Deserialize<'de> for BootIdentifier { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct Visitor; + impl<'de> de::Visitor<'de> for Visitor { + type Value = BootIdentifier; + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "a string or integer") + } + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + Ok(Self::Value::Id(v.to_owned())) + } + fn visit_string(self, v: String) -> Result + where + E: de::Error, + { + Ok(Self::Value::Id(v)) + } + fn visit_i64(self, v: i64) -> Result + where + E: de::Error, + { + Ok(Self::Value::Index(v as i32)) + } + fn visit_f64(self, v: f64) -> Result + where + E: de::Error, + { + Ok(Self::Value::Index(v as i32)) + } + fn visit_u64(self, v: u64) -> Result + where + E: de::Error, + { + Ok(Self::Value::Index(v as i32)) + } + } + deserializer.deserialize_any(Visitor) + } +} +impl From for String { + fn from(value: BootIdentifier) -> Self { + match value { + BootIdentifier::Index(i) => i.to_string(), + BootIdentifier::Id(i) => i, + } + } +} + #[derive(Deserialize, Serialize, Parser)] #[serde(rename_all = "camelCase")] #[command(rename_all = "kebab-case")] @@ -238,6 +327,9 @@ pub struct LogsParams { limit: Option, #[arg(short = 'c', long = "cursor", conflicts_with = "follow")] cursor: Option, + #[arg(short = 'b', long = "boot")] + #[serde(default)] + boot: Option, #[arg(short = 'B', long = "before", conflicts_with = "follow")] #[serde(default)] before: bool, @@ -352,12 +444,22 @@ where extra, limit, cursor, + boot, before, }, .. }: HandlerArgs>| { let f = f.clone(); - async move { fetch_logs(f.call(&context, extra).await?, limit, cursor, before).await } + async move { + fetch_logs( + f.call(&context, extra).await?, + limit, + cursor, + boot.map(String::from), + before, + ) + .await + } }, ) } @@ -377,13 +479,16 @@ fn logs_follow< from_fn_async( move |HandlerArgs { context, - inherited_params: LogsParams { extra, limit, .. }, + inherited_params: + LogsParams { + extra, limit, boot, .. + }, .. }: HandlerArgs>| { let f = f.clone(); async move { let src = f.call(&context, extra).await?; - follow_logs(context, src, limit).await + follow_logs(context, src, limit, boot.map(String::from)).await } }, ) @@ -416,6 +521,7 @@ pub async fn journalctl( id: LogSource, limit: usize, cursor: Option<&str>, + boot: Option<&str>, before: bool, follow: bool, ) -> Result { @@ -431,6 +537,12 @@ pub async fn journalctl( } } + if let Some(boot) = boot { + cmd.arg(format!("--boot={boot}")); + } else { + cmd.arg("--boot=all"); + } + let deserialized_entries = String::from_utf8(cmd.invoke(ErrorKind::Journald).await?)? .lines() .map(serde_json::from_str::) @@ -516,10 +628,12 @@ pub async fn fetch_logs( id: LogSource, limit: Option, cursor: Option, + boot: Option, before: bool, ) -> Result { let limit = limit.unwrap_or(50); - let mut stream = journalctl(id, limit, cursor.as_deref(), before, false).await?; + let mut stream = + journalctl(id, limit, cursor.as_deref(), boot.as_deref(), before, false).await?; let mut entries = Vec::with_capacity(limit); let mut start_cursor = None; @@ -563,9 +677,10 @@ pub async fn follow_logs>( ctx: Context, id: LogSource, limit: Option, + boot: Option, ) -> Result { let limit = limit.unwrap_or(50); - let mut stream = journalctl(id, limit, None, false, true).await?; + let mut stream = journalctl(id, limit, None, boot.as_deref(), false, true).await?; let mut start_cursor = None; let mut first_entry = None; diff --git a/core/startos/src/net/tor.rs b/core/startos/src/net/tor.rs index 31eae5fab..24b8ddb02 100644 --- a/core/startos/src/net/tor.rs +++ b/core/startos/src/net/tor.rs @@ -305,7 +305,15 @@ async fn torctl( .invoke(ErrorKind::Tor) .await?; - let logs = journalctl(LogSource::Unit(SYSTEMD_UNIT), 0, None, false, true).await?; + let logs = journalctl( + LogSource::Unit(SYSTEMD_UNIT), + 0, + None, + Some("0"), + false, + true, + ) + .await?; let mut tcp_stream = None; for _ in 0..60 { From 87322744d4efaeec8168bbe73c3b4f3aca92cb83 Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Thu, 11 Jul 2024 11:32:46 -0600 Subject: [PATCH 056/125] Feature/backup fs (#2665) * port 040 config, WIP * update fixtures * use taiga modal for backups too * fix: update Taiga UI and refactor everything to work * chore: package-lock * fix interfaces and mocks for interfaces * better mocks * function to transform old spec to new * delete unused fns * delete unused FE config utils * fix exports from sdk * reorganize exports * functions to translate config * rename unionSelectKey and unionValueKey * new backup fs * update sdk types * change types, include fuse module * fix casing * rework setup wiz * rework UI * only fuse3 * fix arm build * misc fixes * fix duplicate server select * fix: fix throwing inside dialog --------- Co-authored-by: Matt Hill Co-authored-by: waterplea Co-authored-by: Matt Hill --- Makefile | 10 +- build-cargo-dep.sh | 15 ++- build/dpkg-deps/depends | 1 + core/startos/src/backup/backup_bulk.rs | 24 ++-- core/startos/src/backup/restore.rs | 25 +++-- core/startos/src/backup/target/cifs.rs | 6 +- core/startos/src/backup/target/mod.rs | 16 +++ core/startos/src/disk/mod.rs | 16 ++- core/startos/src/disk/mount/backup.rs | 51 +++++---- .../src/disk/mount/filesystem/backupfs.rs | 55 ++++++++++ core/startos/src/disk/mount/filesystem/mod.rs | 3 + core/startos/src/disk/util.rs | 71 +++++++----- core/startos/src/hostname.rs | 2 +- core/startos/src/setup.rs | 82 ++++++++------ core/startos/src/util/serde.rs | 78 +++---------- foo | 1 - sdk/lib/osBindings/RecoverySource.ts | 9 +- sdk/lib/osBindings/SetupExecuteParams.ts | 3 +- .../src/app/services/api/mock-api.service.ts | 39 ++++--- .../modals/cifs-modal/cifs-modal.module.ts | 3 +- .../app/modals/cifs-modal/cifs-modal.page.ts | 25 ++--- .../src/app/modals/password/password.page.ts | 15 +-- .../server-backup-select.module.ts | 13 +++ .../server-backup-select.page.html | 24 ++++ .../server-backup-select.page.scss | 0 .../server-backup-select.page.ts | 44 ++++++++ .../src/app/pages/embassy/embassy.page.ts | 38 ++++--- .../pages/recover/drive-status.component.html | 14 --- .../src/app/pages/recover/recover.module.ts | 4 +- .../src/app/pages/recover/recover.page.html | 36 +++--- .../src/app/pages/recover/recover.page.ts | 101 ++++++----------- .../src/app/pages/success/success.module.ts | 1 - .../src/app/pages/success/success.page.ts | 1 - .../src/app/services/api/api.service.ts | 36 +++--- .../src/app/services/api/live-api.service.ts | 8 +- .../src/app/services/api/mock-api.service.ts | 56 ++++++---- .../src/app/services/state.service.ts | 13 ++- .../src/services/download-html.service.ts | 4 +- web/projects/shared/src/types/api.ts | 5 +- .../app/preloader/preloader.component.html | 28 ++--- .../app/app/preloader/preloader.component.ts | 2 + .../src/app/app/preloader/preloader.module.ts | 6 +- .../backup-drives-status.component.html | 14 +-- .../backup-drives.component.html | 4 +- .../backup-drives/backup-drives.component.ts | 8 +- .../backup-drives/backup.service.ts | 18 ++- .../form/form-group/form-group.component.html | 2 +- .../form-textarea.component.html | 4 +- .../ui/src/app/components/form/form.module.ts | 4 +- .../src/app/components/logs/logs.component.ts | 2 +- .../app-recover-select.page.html | 8 +- .../app-recover-select.page.ts | 8 +- .../app-recover-select/to-options.pipe.ts | 4 +- .../backup-server-select.module.ts | 13 +++ .../backup-server-select.page.html | 35 ++++++ .../backup-server-select.page.scss | 0 .../backup-server-select.page.ts | 103 ++++++++++++++++++ .../password-prompt.modal.ts | 69 ++++++++++++ .../restore/restore.component.html | 2 +- .../restore/restore.component.module.ts | 4 +- .../restore/restore.component.ts | 82 +------------- .../server-backup/server-backup.page.ts | 16 +-- .../ui/src/app/services/api/api.fixures.ts | 40 ++++--- .../ui/src/app/services/api/api.types.ts | 13 ++- .../services/api/embassy-mock-api.service.ts | 2 +- .../ui/src/app/services/api/mock-patch.ts | 2 +- .../ui/src/app/types/mapped-backup-target.ts | 2 +- 67 files changed, 880 insertions(+), 563 deletions(-) create mode 100644 core/startos/src/disk/mount/filesystem/backupfs.rs delete mode 120000 foo create mode 100644 web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.module.ts create mode 100644 web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.page.html create mode 100644 web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.page.scss create mode 100644 web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.page.ts delete mode 100644 web/projects/setup-wizard/src/app/pages/recover/drive-status.component.html create mode 100644 web/projects/ui/src/app/modals/backup-server-select/backup-server-select.module.ts create mode 100644 web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.html create mode 100644 web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.scss create mode 100644 web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.ts create mode 100644 web/projects/ui/src/app/modals/backup-server-select/password-prompt.modal.ts diff --git a/Makefile b/Makefile index 9a9a001b7..c0492ba55 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ PATCH_DB_CLIENT_SRC := $(shell git ls-files --recurse-submodules patch-db/client GZIP_BIN := $(shell which pigz || which gzip) TAR_BIN := $(shell which gtar || which tar) COMPILED_TARGETS := $(BINS) system-images/compat/docker-images/$(ARCH).tar system-images/utils/docker-images/$(ARCH).tar system-images/binfmt/docker-images/$(ARCH).tar container-runtime/rootfs.$(ARCH).squashfs -ALL_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo cargo-deps/aarch64-unknown-linux-musl/release/pi-beep; fi) $(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]; then echo cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console; fi') $(PLATFORM_FILE) +ALL_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) cargo-deps/$(ARCH)-unknown-linux-musl/release/startos-backup-fs $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo cargo-deps/aarch64-unknown-linux-musl/release/pi-beep; fi) $(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]; then echo cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console; fi') $(PLATFORM_FILE) ifeq ($(REMOTE),) mkdir = mkdir -p $1 @@ -115,12 +115,15 @@ results/$(BASENAME).$(IMAGE_TYPE) results/$(BASENAME).squashfs: $(IMAGE_RECIPE_S # For creating os images. DO NOT USE install: $(ALL_TARGETS) $(call mkdir,$(DESTDIR)/usr/bin) + $(call mkdir,$(DESTDIR)/usr/sbin) $(call cp,core/target/$(ARCH)-unknown-linux-musl/release/startbox,$(DESTDIR)/usr/bin/startbox) $(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/startd) $(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-cli) $(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-sdk) if [ "$(PLATFORM)" = "raspberrypi" ]; then $(call cp,cargo-deps/aarch64-unknown-linux-musl/release/pi-beep,$(DESTDIR)/usr/bin/pi-beep); fi if /bin/bash -c '[[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]'; then $(call cp,cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console,$(DESTDIR)/usr/bin/tokio-console); fi + $(call cp,cargo-deps/$(ARCH)-unknown-linux-musl/release/startos-backup-fs,$(DESTDIR)/usr/bin/startos-backup-fs) + $(call ln,/usr/bin/startos-backup-fs,$(DESTDIR)/usr/sbin/mount.backup-fs) $(call mkdir,$(DESTDIR)/lib/systemd/system) $(call cp,core/startos/startd.service,$(DESTDIR)/lib/systemd/system/startd.service) @@ -310,4 +313,7 @@ cargo-deps/aarch64-unknown-linux-musl/release/pi-beep: ARCH=aarch64 ./build-cargo-dep.sh pi-beep cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console: - ARCH=$(ARCH) ./build-cargo-dep.sh tokio-console \ No newline at end of file + ARCH=$(ARCH) PREINSTALL="apk add musl-dev pkgconfig" ./build-cargo-dep.sh tokio-console + +cargo-deps/$(ARCH)-unknown-linux-musl/release/startos-backup-fs: + ARCH=$(ARCH) PREINSTALL="apk add fuse3 fuse3-dev fuse3-static musl-dev pkgconfig" ./build-cargo-dep.sh --git https://github.com/Start9Labs/start-fs.git startos-backup-fs \ No newline at end of file diff --git a/build-cargo-dep.sh b/build-cargo-dep.sh index 9e20f0caf..c32e4f8ae 100755 --- a/build-cargo-dep.sh +++ b/build-cargo-dep.sh @@ -17,9 +17,18 @@ if [ -z "$ARCH" ]; then ARCH=$(uname -m) fi -mkdir -p cargo-deps -alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)"/cargo-deps:/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl' +DOCKER_PLATFORM="linux/${ARCH}" +if [ "$ARCH" = aarch64 ]; then + DOCKER_PLATFORM="linux/arm64" +elif [ "$ARCH" = x86_64 ]; then + DOCKER_PLATFORM="linux/amd64" +fi -rust-musl-builder cargo install "$1" --target-dir /home/rust/src --target=$ARCH-unknown-linux-musl +mkdir -p cargo-deps +alias 'rust-musl-builder'='docker run $USE_TTY --platform=${DOCKER_PLATFORM} --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)"/cargo-deps:/home/rust/src -w /home/rust/src -P rust:alpine' + +PREINSTALL=${PREINSTALL:-true} + +rust-musl-builder sh -c "$PREINSTALL && cargo install $* --target-dir /home/rust/src --target=$ARCH-unknown-linux-musl" sudo chown -R $USER cargo-deps sudo chown -R $USER ~/.cargo \ No newline at end of file diff --git a/build/dpkg-deps/depends b/build/dpkg-deps/depends index 7519cff1b..33b7be2ad 100644 --- a/build/dpkg-deps/depends +++ b/build/dpkg-deps/depends @@ -14,6 +14,7 @@ e2fsprogs ecryptfs-utils exfatprogs flashrom +fuse3 grub-common htop httpdirfs diff --git a/core/startos/src/backup/backup_bulk.rs b/core/startos/src/backup/backup_bulk.rs index 79cb3b98a..a52c97f9b 100644 --- a/core/startos/src/backup/backup_bulk.rs +++ b/core/startos/src/backup/backup_bulk.rs @@ -164,7 +164,7 @@ pub async fn backup_all( .decrypt(&ctx)?; let password = password.decrypt(&ctx)?; - let ((fs, package_ids), status_guard) = ( + let ((fs, package_ids, server_id), status_guard) = ( ctx.db .mutate(|db| { check_password_against_db(db, &password)?; @@ -181,7 +181,11 @@ pub async fn backup_all( .collect() }; assure_backing_up(db, &package_ids)?; - Ok((fs, package_ids)) + Ok(( + fs, + package_ids, + db.as_public().as_server_info().as_id().de()?, + )) }) .await?, BackupStatusGuard::new(ctx.db.clone()), @@ -189,6 +193,7 @@ pub async fn backup_all( let mut backup_guard = BackupMountGuard::mount( TmpMountGuard::mount(&fs, ReadWrite).await?, + &server_id, &old_password_decrypted, ) .await?; @@ -298,11 +303,11 @@ async fn perform_backup( let ui = ctx.db.peek().await.into_public().into_ui().de()?; let mut os_backup_file = - AtomicFile::new(backup_guard.path().join("os-backup.cbor"), None::) + AtomicFile::new(backup_guard.path().join("os-backup.json"), None::) .await .with_kind(ErrorKind::Filesystem)?; os_backup_file - .write_all(&IoFormat::Cbor.to_vec(&OsBackup { + .write_all(&IoFormat::Json.to_vec(&OsBackup { account: ctx.account.read().await.clone(), ui, })?) @@ -325,22 +330,23 @@ async fn perform_backup( dir_copy(luks_folder, &luks_folder_bak, None).await?; } - let timestamp = Some(Utc::now()); + let timestamp = Utc::now(); backup_guard.unencrypted_metadata.version = crate::version::Current::new().semver().into(); - backup_guard.unencrypted_metadata.full = true; + backup_guard.unencrypted_metadata.hostname = ctx.account.read().await.hostname.clone(); + backup_guard.unencrypted_metadata.timestamp = timestamp.clone(); backup_guard.metadata.version = crate::version::Current::new().semver().into(); - backup_guard.metadata.timestamp = timestamp; + backup_guard.metadata.timestamp = Some(timestamp); backup_guard.metadata.package_backups = package_backups; - backup_guard.save().await?; + backup_guard.save_and_unmount().await?; ctx.db .mutate(|v| { v.as_public_mut() .as_server_info_mut() .as_last_backup_mut() - .ser(×tamp) + .ser(&Some(timestamp)) }) .await?; diff --git a/core/startos/src/backup/restore.rs b/core/startos/src/backup/restore.rs index 3f0aacbe8..23e0c8ac1 100644 --- a/core/startos/src/backup/restore.rs +++ b/core/startos/src/backup/restore.rs @@ -44,9 +44,14 @@ pub async fn restore_packages_rpc( password, }: RestorePackageParams, ) -> Result<(), Error> { - let fs = target_id.load(&ctx.db.peek().await)?; - let backup_guard = - BackupMountGuard::mount(TmpMountGuard::mount(&fs, ReadWrite).await?, &password).await?; + let peek = ctx.db.peek().await; + let fs = target_id.load(&peek)?; + let backup_guard = BackupMountGuard::mount( + TmpMountGuard::mount(&fs, ReadWrite).await?, + &peek.as_public().as_server_info().as_id().de()?, + &password, + ) + .await?; let tasks = restore_packages(&ctx, backup_guard, ids).await?; @@ -73,7 +78,8 @@ pub async fn recover_full_embassy( disk_guid: Arc, start_os_password: String, recovery_source: TmpMountGuard, - recovery_password: Option, + server_id: &str, + recovery_password: &str, SetupExecuteProgress { init_phases, restore_phase, @@ -82,14 +88,11 @@ pub async fn recover_full_embassy( ) -> Result<(SetupResult, RpcContext), Error> { let mut restore_phase = restore_phase.or_not_found("restore progress")?; - let backup_guard = BackupMountGuard::mount( - recovery_source, - recovery_password.as_deref().unwrap_or_default(), - ) - .await?; + let backup_guard = + BackupMountGuard::mount(recovery_source, server_id, recovery_password).await?; - let os_backup_path = backup_guard.path().join("os-backup.cbor"); - let mut os_backup: OsBackup = IoFormat::Cbor.from_slice( + let os_backup_path = backup_guard.path().join("os-backup.json"); + let mut os_backup: OsBackup = IoFormat::Json.from_slice( &tokio::fs::read(&os_backup_path) .await .with_ctx(|_| (ErrorKind::Filesystem, os_backup_path.display().to_string()))?, diff --git a/core/startos/src/backup/target/cifs.rs b/core/startos/src/backup/target/cifs.rs index ad4f81aa0..e83f4e981 100644 --- a/core/startos/src/backup/target/cifs.rs +++ b/core/startos/src/backup/target/cifs.rs @@ -14,7 +14,7 @@ use crate::db::model::DatabaseModel; use crate::disk::mount::filesystem::cifs::Cifs; use crate::disk::mount::filesystem::ReadOnly; use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; -use crate::disk::util::{recovery_info, EmbassyOsRecoveryInfo}; +use crate::disk::util::{recovery_info, StartOsRecoveryInfo}; use crate::prelude::*; use crate::util::serde::KeyVal; @@ -43,7 +43,7 @@ pub struct CifsBackupTarget { path: PathBuf, username: String, mountable: bool, - start_os: Option, + start_os: BTreeMap, } pub fn cifs() -> ParentHandler { @@ -239,7 +239,7 @@ pub async fn list(db: &DatabaseModel) -> Result, Er path: mount_info.path, username: mount_info.username, mountable: start_os.is_ok(), - start_os: start_os.ok().and_then(|a| a), + start_os: start_os.ok().unwrap_or_default(), }, )); } diff --git a/core/startos/src/backup/target/mod.rs b/core/startos/src/backup/target/mod.rs index f1273aa6a..f3b6b5f5c 100644 --- a/core/startos/src/backup/target/mod.rs +++ b/core/startos/src/backup/target/mod.rs @@ -157,6 +157,16 @@ pub fn target() -> ParentHandler { }) .with_call_remote::(), ) + .subcommand( + "mount", + from_fn_async(mount).with_call_remote::(), + ) + .subcommand( + "umount", + from_fn_async(umount) + .no_display() + .with_call_remote::(), + ) } // #[command(display(display_serializable))] @@ -250,6 +260,7 @@ fn display_backup_info(params: WithIoFormat, info: BackupInfo) { #[command(rename_all = "kebab-case")] pub struct InfoParams { target_id: BackupTargetId, + server_id: String, password: String, } @@ -258,11 +269,13 @@ pub async fn info( ctx: RpcContext, InfoParams { target_id, + server_id, password, }: InfoParams, ) -> Result { let guard = BackupMountGuard::mount( TmpMountGuard::mount(&target_id.load(&ctx.db.peek().await)?, ReadWrite).await?, + &server_id, &password, ) .await?; @@ -284,6 +297,7 @@ lazy_static::lazy_static! { #[command(rename_all = "kebab-case")] pub struct MountParams { target_id: BackupTargetId, + server_id: String, password: String, } @@ -292,6 +306,7 @@ pub async fn mount( ctx: RpcContext, MountParams { target_id, + server_id, password, }: MountParams, ) -> Result { @@ -303,6 +318,7 @@ pub async fn mount( let guard = BackupMountGuard::mount( TmpMountGuard::mount(&target_id.clone().load(&ctx.db.peek().await)?, ReadWrite).await?, + &server_id, &password, ) .await?; diff --git a/core/startos/src/disk/mod.rs b/core/startos/src/disk/mod.rs index d7ce5f766..c0a701fc9 100644 --- a/core/startos/src/disk/mod.rs +++ b/core/startos/src/disk/mod.rs @@ -1,5 +1,7 @@ use std::path::{Path, PathBuf}; +use itertools::Itertools; +use lazy_format::lazy_format; use rpc_toolkit::{from_fn_async, CallRemoteHandler, Context, Empty, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; @@ -102,10 +104,18 @@ fn display_disk_info(params: WithIoFormat, args: Vec) { } else { "N/A" }, - &if let Some(eos) = part.start_os.as_ref() { - eos.version.to_string() - } else { + &if part.start_os.is_empty() { "N/A".to_owned() + } else if part.start_os.len() == 1 { + part.start_os + .first_key_value() + .map(|(_, info)| info.version.to_string()) + .unwrap() + } else { + part.start_os + .iter() + .map(|(id, info)| lazy_format!("{} ({})", info.version, id)) + .join(", ") }, ]; table.add_row(row); diff --git a/core/startos/src/disk/mount/backup.rs b/core/startos/src/disk/mount/backup.rs index ad9f5090b..67f88d21f 100644 --- a/core/startos/src/disk/mount/backup.rs +++ b/core/startos/src/disk/mount/backup.rs @@ -11,9 +11,10 @@ use super::filesystem::ecryptfs::EcryptFS; use super::guard::{GenericMountGuard, TmpMountGuard}; use crate::auth::check_password; use crate::backup::target::BackupInfo; +use crate::disk::mount::filesystem::backupfs::BackupFS; use crate::disk::mount::filesystem::ReadWrite; use crate::disk::mount::guard::SubPath; -use crate::disk::util::EmbassyOsRecoveryInfo; +use crate::disk::util::StartOsRecoveryInfo; use crate::util::crypto::{decrypt_slice, encrypt_slice}; use crate::util::serde::IoFormat; use crate::{Error, ErrorKind, ResultExt}; @@ -23,29 +24,27 @@ pub struct BackupMountGuard { backup_disk_mount_guard: Option, encrypted_guard: Option, enc_key: String, - pub unencrypted_metadata: EmbassyOsRecoveryInfo, + unencrypted_metadata_path: PathBuf, + pub unencrypted_metadata: StartOsRecoveryInfo, pub metadata: BackupInfo, } impl BackupMountGuard { - fn backup_disk_path(&self) -> &Path { - if let Some(guard) = &self.backup_disk_mount_guard { - guard.path() - } else { - unreachable!() - } - } - #[instrument(skip_all)] - pub async fn mount(backup_disk_mount_guard: G, password: &str) -> Result { + pub async fn mount( + backup_disk_mount_guard: G, + server_id: &str, + password: &str, + ) -> Result { let backup_disk_path = backup_disk_mount_guard.path(); - let unencrypted_metadata_path = - backup_disk_path.join("EmbassyBackups/unencrypted-metadata.cbor"); - let mut unencrypted_metadata: EmbassyOsRecoveryInfo = + let backup_dir = backup_disk_path.join("StartOSBackups").join(server_id); + let unencrypted_metadata_path = backup_dir.join("unencrypted-metadata.json"); + let crypt_path = backup_dir.join("crypt"); + let mut unencrypted_metadata: StartOsRecoveryInfo = if tokio::fs::metadata(&unencrypted_metadata_path) .await .is_ok() { - IoFormat::Cbor.from_slice( + IoFormat::Json.from_slice( &tokio::fs::read(&unencrypted_metadata_path) .await .with_ctx(|_| { @@ -56,6 +55,9 @@ impl BackupMountGuard { })?, )? } else { + if tokio::fs::metadata(&crypt_path).await.is_ok() { + tokio::fs::remove_dir_all(&crypt_path).await?; + } Default::default() }; let enc_key = if let (Some(hash), Some(wrapped_key)) = ( @@ -96,7 +98,6 @@ impl BackupMountGuard { )); } - let crypt_path = backup_disk_path.join("EmbassyBackups/crypt"); if tokio::fs::metadata(&crypt_path).await.is_err() { tokio::fs::create_dir_all(&crypt_path).await.with_ctx(|_| { ( @@ -106,11 +107,11 @@ impl BackupMountGuard { })?; } let encrypted_guard = - TmpMountGuard::mount(&EcryptFS::new(&crypt_path, &enc_key), ReadWrite).await?; + TmpMountGuard::mount(&BackupFS::new(&crypt_path, &enc_key), ReadWrite).await?; - let metadata_path = encrypted_guard.path().join("metadata.cbor"); + let metadata_path = encrypted_guard.path().join("metadata.json"); let metadata: BackupInfo = if tokio::fs::metadata(&metadata_path).await.is_ok() { - IoFormat::Cbor.from_slice(&tokio::fs::read(&metadata_path).await.with_ctx(|_| { + IoFormat::Json.from_slice(&tokio::fs::read(&metadata_path).await.with_ctx(|_| { ( crate::ErrorKind::Filesystem, metadata_path.display().to_string(), @@ -124,6 +125,7 @@ impl BackupMountGuard { backup_disk_mount_guard: Some(backup_disk_mount_guard), encrypted_guard: Some(encrypted_guard), enc_key, + unencrypted_metadata_path, unencrypted_metadata, metadata, }) @@ -152,20 +154,17 @@ impl BackupMountGuard { #[instrument(skip_all)] pub async fn save(&self) -> Result<(), Error> { - let metadata_path = self.path().join("metadata.cbor"); - let backup_disk_path = self.backup_disk_path(); + let metadata_path = self.path().join("metadata.json"); let mut file = AtomicFile::new(&metadata_path, None::) .await .with_kind(ErrorKind::Filesystem)?; - file.write_all(&IoFormat::Cbor.to_vec(&self.metadata)?) + file.write_all(&IoFormat::Json.to_vec(&self.metadata)?) .await?; file.save().await.with_kind(ErrorKind::Filesystem)?; - let unencrypted_metadata_path = - backup_disk_path.join("EmbassyBackups/unencrypted-metadata.cbor"); - let mut file = AtomicFile::new(&unencrypted_metadata_path, None::) + let mut file = AtomicFile::new(&self.unencrypted_metadata_path, None::) .await .with_kind(ErrorKind::Filesystem)?; - file.write_all(&IoFormat::Cbor.to_vec(&self.unencrypted_metadata)?) + file.write_all(&IoFormat::Json.to_vec(&self.unencrypted_metadata)?) .await?; file.save().await.with_kind(ErrorKind::Filesystem)?; Ok(()) diff --git a/core/startos/src/disk/mount/filesystem/backupfs.rs b/core/startos/src/disk/mount/filesystem/backupfs.rs new file mode 100644 index 000000000..9ef258f34 --- /dev/null +++ b/core/startos/src/disk/mount/filesystem/backupfs.rs @@ -0,0 +1,55 @@ +use std::fmt::{self, Display}; +use std::os::unix::ffi::OsStrExt; +use std::path::Path; + +use digest::generic_array::GenericArray; +use digest::{Digest, OutputSizeUser}; +use sha2::Sha256; + +use super::FileSystem; +use crate::prelude::*; + +pub struct BackupFS, Password: fmt::Display> { + data_dir: DataDir, + password: Password, +} +impl, Password: fmt::Display> BackupFS { + pub fn new(data_dir: DataDir, password: Password) -> Self { + BackupFS { data_dir, password } + } +} +impl + Send + Sync, Password: fmt::Display + Send + Sync> FileSystem + for BackupFS +{ + fn mount_type(&self) -> Option> { + Some("backup-fs") + } + fn mount_options(&self) -> impl IntoIterator { + [ + format!("password={}", self.password), + format!("file-size-padding=0.05"), + ] + } + async fn source(&self) -> Result>, Error> { + Ok(Some(&self.data_dir)) + } + async fn source_hash( + &self, + ) -> Result::OutputSize>, Error> { + let mut sha = Sha256::new(); + sha.update("BackupFS"); + sha.update( + tokio::fs::canonicalize(self.data_dir.as_ref()) + .await + .with_ctx(|_| { + ( + crate::ErrorKind::Filesystem, + self.data_dir.as_ref().display().to_string(), + ) + })? + .as_os_str() + .as_bytes(), + ); + Ok(sha.finalize()) + } +} diff --git a/core/startos/src/disk/mount/filesystem/mod.rs b/core/startos/src/disk/mount/filesystem/mod.rs index 53157937c..818549a0a 100644 --- a/core/startos/src/disk/mount/filesystem/mod.rs +++ b/core/startos/src/disk/mount/filesystem/mod.rs @@ -1,6 +1,7 @@ use std::ffi::OsStr; use std::fmt::{Display, Write}; use std::path::Path; +use std::time::Duration; use digest::generic_array::GenericArray; use digest::OutputSizeUser; @@ -11,6 +12,7 @@ use tokio::process::Command; use crate::prelude::*; use crate::util::Invoke; +pub mod backupfs; pub mod bind; pub mod block_dev; pub mod cifs; @@ -71,6 +73,7 @@ pub(self) async fn default_mount_impl( fs.pre_mount().await?; tokio::fs::create_dir_all(mountpoint.as_ref()).await?; Command::from(default_mount_command(fs, mountpoint, mount_type).await?) + .capture(false) .invoke(ErrorKind::Filesystem) .await?; diff --git a/core/startos/src/disk/util.rs b/core/startos/src/disk/util.rs index d3f2a8d27..c64ea40ae 100644 --- a/core/startos/src/disk/util.rs +++ b/core/startos/src/disk/util.rs @@ -1,6 +1,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::path::{Path, PathBuf}; +use chrono::{DateTime, Utc}; use color_eyre::eyre::{self, eyre}; use futures::TryStreamExt; use nom::bytes::complete::{tag, take_till1}; @@ -19,6 +20,7 @@ use super::mount::filesystem::ReadOnly; use super::mount::guard::TmpMountGuard; use crate::disk::mount::guard::GenericMountGuard; use crate::disk::OsPartitionInfo; +use crate::hostname::Hostname; use crate::util::serde::IoFormat; use crate::util::Invoke; use crate::{Error, ResultExt as _}; @@ -49,15 +51,16 @@ pub struct PartitionInfo { pub label: Option, pub capacity: u64, pub used: Option, - pub start_os: Option, + pub start_os: BTreeMap, pub guid: Option, } #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct EmbassyOsRecoveryInfo { +pub struct StartOsRecoveryInfo { + pub hostname: Hostname, pub version: exver::Version, - pub full: bool, + pub timestamp: DateTime, pub password_hash: Option, pub wrapped_key: Option, } @@ -223,29 +226,38 @@ pub async fn pvscan() -> Result>, Error> { pub async fn recovery_info( mountpoint: impl AsRef, -) -> Result, Error> { - let backup_unencrypted_metadata_path = mountpoint - .as_ref() - .join("EmbassyBackups/unencrypted-metadata.cbor"); - if tokio::fs::metadata(&backup_unencrypted_metadata_path) - .await - .is_ok() - { - return Ok(Some( - IoFormat::Cbor.from_slice( - &tokio::fs::read(&backup_unencrypted_metadata_path) - .await - .with_ctx(|_| { - ( - crate::ErrorKind::Filesystem, - backup_unencrypted_metadata_path.display().to_string(), - ) - })?, - )?, - )); +) -> Result, Error> { + let backup_root = mountpoint.as_ref().join("StartOSBackups"); + let mut res = BTreeMap::new(); + if tokio::fs::metadata(&backup_root).await.is_ok() { + let mut dir = tokio::fs::read_dir(&backup_root).await?; + while let Some(entry) = dir.next_entry().await? { + let server_id = entry.file_name().to_string_lossy().into_owned(); + let backup_unencrypted_metadata_path = backup_root + .join(&server_id) + .join("unencrypted-metadata.json"); + if tokio::fs::metadata(&backup_unencrypted_metadata_path) + .await + .is_ok() + { + res.insert( + server_id, + IoFormat::Json.from_slice( + &tokio::fs::read(&backup_unencrypted_metadata_path) + .await + .with_ctx(|_| { + ( + crate::ErrorKind::Filesystem, + backup_unencrypted_metadata_path.display().to_string(), + ) + })?, + )?, + ); + } + } } - Ok(None) + Ok(res) } #[instrument(skip_all)] @@ -390,7 +402,7 @@ async fn disk_info(disk: PathBuf) -> DiskInfo { } async fn part_info(part: PathBuf) -> PartitionInfo { - let mut start_os = None; + let mut start_os = BTreeMap::new(); let label = get_label(&part) .await .map_err(|e| tracing::warn!("Could not get label of {}: {}", part.display(), e.source)) @@ -410,14 +422,13 @@ async fn part_info(part: PathBuf) -> PartitionInfo { tracing::warn!("Could not get usage of {}: {}", part.display(), e.source) }) .ok(); - if let Some(recovery_info) = match recovery_info(mount_guard.path()).await { - Ok(a) => a, + match recovery_info(mount_guard.path()).await { + Ok(a) => { + start_os = a; + } Err(e) => { tracing::error!("Error fetching unencrypted backup metadata: {}", e); - None } - } { - start_os = Some(recovery_info) } if let Err(e) = mount_guard.unmount().await { tracing::error!("Error unmounting partition {}: {}", part.display(), e); diff --git a/core/startos/src/hostname.rs b/core/startos/src/hostname.rs index f68d5c9d8..c4332354c 100644 --- a/core/startos/src/hostname.rs +++ b/core/startos/src/hostname.rs @@ -4,7 +4,7 @@ use tracing::instrument; use crate::util::Invoke; use crate::{Error, ErrorKind}; -#[derive(Clone, serde::Deserialize, serde::Serialize, Debug)] +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] pub struct Hostname(pub String); lazy_static::lazy_static! { diff --git a/core/startos/src/setup.rs b/core/startos/src/setup.rs index 8fb7cf7c1..642dd5476 100644 --- a/core/startos/src/setup.rs +++ b/core/startos/src/setup.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; @@ -25,7 +26,7 @@ use crate::disk::main::DEFAULT_PASSWORD; use crate::disk::mount::filesystem::cifs::Cifs; use crate::disk::mount::filesystem::ReadWrite; use crate::disk::mount::guard::{GenericMountGuard, TmpMountGuard}; -use crate::disk::util::{pvscan, recovery_info, DiskInfo, EmbassyOsRecoveryInfo}; +use crate::disk::util::{pvscan, recovery_info, DiskInfo, StartOsRecoveryInfo}; use crate::disk::REPAIR_DISK_PATH; use crate::init::{init, InitPhases, InitResult}; use crate::net::net_controller::PreInitNetController; @@ -237,7 +238,7 @@ pub async fn verify_cifs( username, password, }: VerifyCifsParams, -) -> Result { +) -> Result, Error> { let password: Option = password.map(|x| x.decrypt(&ctx)).flatten(); let guard = TmpMountGuard::mount( &Cifs { @@ -251,15 +252,28 @@ pub async fn verify_cifs( .await?; let start_os = recovery_info(guard.path()).await?; guard.unmount().await?; - start_os.ok_or_else(|| Error::new(eyre!("No Backup Found"), crate::ErrorKind::NotFound)) + if start_os.is_empty() { + return Err(Error::new( + eyre!("No Backup Found"), + crate::ErrorKind::NotFound, + )); + } + Ok(start_os) } #[derive(Debug, Deserialize, Serialize, TS)] #[serde(tag = "type")] #[serde(rename_all = "camelCase")] -pub enum RecoverySource { - Migrate { guid: String }, - Backup { target: BackupTargetFS }, +#[serde(rename_all_fields = "camelCase")] +pub enum RecoverySource { + Migrate { + guid: String, + }, + Backup { + target: BackupTargetFS, + password: Password, + server_id: String, + }, } #[derive(Deserialize, Serialize, TS)] @@ -268,8 +282,7 @@ pub enum RecoverySource { pub struct SetupExecuteParams { start_os_logicalname: PathBuf, start_os_password: EncryptedWire, - recovery_source: Option, - recovery_password: Option, + recovery_source: Option>, } // #[command(rpc_only)] @@ -279,7 +292,6 @@ pub async fn execute( start_os_logicalname, start_os_password, recovery_source, - recovery_password, }: SetupExecuteParams, ) -> Result { let start_os_password = match start_os_password.decrypt(&ctx) { @@ -291,29 +303,27 @@ pub async fn execute( )) } }; - let recovery_password: Option = match recovery_password { - Some(a) => match a.decrypt(&ctx) { - Some(a) => Some(a), - None => { - return Err(Error::new( + let recovery = match recovery_source { + Some(RecoverySource::Backup { + target, + password, + server_id, + }) => Some(RecoverySource::Backup { + target, + password: password.decrypt(&ctx).ok_or_else(|| { + Error::new( color_eyre::eyre::eyre!("Couldn't decode recoveryPassword"), crate::ErrorKind::Unknown, - )) - } - }, + ) + })?, + server_id, + }), + Some(RecoverySource::Migrate { guid }) => Some(RecoverySource::Migrate { guid }), None => None, }; let setup_ctx = ctx.clone(); - ctx.run_setup(|| { - execute_inner( - setup_ctx, - start_os_logicalname, - start_os_password, - recovery_source, - recovery_password, - ) - })?; + ctx.run_setup(|| execute_inner(setup_ctx, start_os_logicalname, start_os_password, recovery))?; Ok(ctx.progress().await) } @@ -348,12 +358,11 @@ pub async fn execute_inner( ctx: SetupContext, start_os_logicalname: PathBuf, start_os_password: String, - recovery_source: Option, - recovery_password: Option, + recovery_source: Option>, ) -> Result<(SetupResult, RpcContext), Error> { let progress = &ctx.progress; let mut disk_phase = progress.add_phase("Formatting data drive".into(), Some(10)); - let restore_phase = match &recovery_source { + let restore_phase = match recovery_source.as_ref() { Some(RecoverySource::Backup { .. }) => { Some(progress.add_phase("Restoring backup".into(), Some(100))) } @@ -396,13 +405,18 @@ pub async fn execute_inner( }; match recovery_source { - Some(RecoverySource::Backup { target }) => { + Some(RecoverySource::Backup { + target, + password, + server_id, + }) => { recover( &ctx, guid, start_os_password, target, - recovery_password, + server_id, + password, progress, ) .await @@ -448,7 +462,8 @@ async fn recover( guid: Arc, start_os_password: String, recovery_source: BackupTargetFS, - recovery_password: Option, + server_id: String, + recovery_password: String, progress: SetupExecuteProgress, ) -> Result<(SetupResult, RpcContext), Error> { let recovery_source = TmpMountGuard::mount(&recovery_source, ReadWrite).await?; @@ -457,7 +472,8 @@ async fn recover( guid.clone(), start_os_password, recovery_source, - recovery_password, + &server_id, + &recovery_password, progress, ) .await diff --git a/core/startos/src/util/serde.rs b/core/startos/src/util/serde.rs index b3b2dea6a..049fad2d2 100644 --- a/core/startos/src/util/serde.rs +++ b/core/startos/src/util/serde.rs @@ -107,64 +107,22 @@ pub fn serialize_display_opt( Option::::serialize(&t.as_ref().map(|t| t.to_string()), serializer) } -pub mod ed25519_pubkey { - use ed25519_dalek::VerifyingKey; - use serde::de::{Error, Unexpected, Visitor}; - use serde::{Deserializer, Serializer}; - - pub fn serialize( - pubkey: &VerifyingKey, - serializer: S, - ) -> Result { - serializer.serialize_str(&base32::encode( - base32::Alphabet::RFC4648 { padding: true }, - pubkey.as_bytes(), - )) - } - pub fn deserialize<'de, D: Deserializer<'de>>( - deserializer: D, - ) -> Result { - struct PubkeyVisitor; - impl<'de> Visitor<'de> for PubkeyVisitor { - type Value = ed25519_dalek::VerifyingKey; - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(formatter, "an RFC4648 encoded string") - } - fn visit_str(self, v: &str) -> Result - where - E: Error, - { - VerifyingKey::from_bytes( - &<[u8; 32]>::try_from( - base32::decode(base32::Alphabet::RFC4648 { padding: true }, v).ok_or( - Error::invalid_value(Unexpected::Str(v), &"an RFC4648 encoded string"), - )?, - ) - .map_err(|e| Error::invalid_length(e.len(), &"32 bytes"))?, - ) - .map_err(Error::custom) - } - } - deserializer.deserialize_str(PubkeyVisitor) - } -} - #[derive(Debug, Serialize)] #[serde(untagged)] -pub enum ValuePrimative { +pub enum ValuePrimitive { Null, Boolean(bool), String(String), Number(serde_json::Number), } -impl<'de> serde::de::Deserialize<'de> for ValuePrimative { +impl<'de> serde::de::Deserialize<'de> for ValuePrimitive { fn deserialize(deserializer: D) -> Result where D: serde::de::Deserializer<'de>, { struct Visitor; impl<'de> serde::de::Visitor<'de> for Visitor { - type Value = ValuePrimative; + type Value = ValuePrimitive; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { write!(formatter, "a JSON primative value") } @@ -172,37 +130,37 @@ impl<'de> serde::de::Deserialize<'de> for ValuePrimative { where E: serde::de::Error, { - Ok(ValuePrimative::Null) + Ok(ValuePrimitive::Null) } fn visit_none(self) -> Result where E: serde::de::Error, { - Ok(ValuePrimative::Null) + Ok(ValuePrimitive::Null) } fn visit_bool(self, v: bool) -> Result where E: serde::de::Error, { - Ok(ValuePrimative::Boolean(v)) + Ok(ValuePrimitive::Boolean(v)) } fn visit_str(self, v: &str) -> Result where E: serde::de::Error, { - Ok(ValuePrimative::String(v.to_owned())) + Ok(ValuePrimitive::String(v.to_owned())) } fn visit_string(self, v: String) -> Result where E: serde::de::Error, { - Ok(ValuePrimative::String(v)) + Ok(ValuePrimitive::String(v)) } fn visit_f32(self, v: f32) -> Result where E: serde::de::Error, { - Ok(ValuePrimative::Number( + Ok(ValuePrimitive::Number( serde_json::Number::from_f64(v as f64).ok_or_else(|| { serde::de::Error::invalid_value( serde::de::Unexpected::Float(v as f64), @@ -215,7 +173,7 @@ impl<'de> serde::de::Deserialize<'de> for ValuePrimative { where E: serde::de::Error, { - Ok(ValuePrimative::Number( + Ok(ValuePrimitive::Number( serde_json::Number::from_f64(v).ok_or_else(|| { serde::de::Error::invalid_value( serde::de::Unexpected::Float(v), @@ -228,49 +186,49 @@ impl<'de> serde::de::Deserialize<'de> for ValuePrimative { where E: serde::de::Error, { - Ok(ValuePrimative::Number(v.into())) + Ok(ValuePrimitive::Number(v.into())) } fn visit_u16(self, v: u16) -> Result where E: serde::de::Error, { - Ok(ValuePrimative::Number(v.into())) + Ok(ValuePrimitive::Number(v.into())) } fn visit_u32(self, v: u32) -> Result where E: serde::de::Error, { - Ok(ValuePrimative::Number(v.into())) + Ok(ValuePrimitive::Number(v.into())) } fn visit_u64(self, v: u64) -> Result where E: serde::de::Error, { - Ok(ValuePrimative::Number(v.into())) + Ok(ValuePrimitive::Number(v.into())) } fn visit_i8(self, v: i8) -> Result where E: serde::de::Error, { - Ok(ValuePrimative::Number(v.into())) + Ok(ValuePrimitive::Number(v.into())) } fn visit_i16(self, v: i16) -> Result where E: serde::de::Error, { - Ok(ValuePrimative::Number(v.into())) + Ok(ValuePrimitive::Number(v.into())) } fn visit_i32(self, v: i32) -> Result where E: serde::de::Error, { - Ok(ValuePrimative::Number(v.into())) + Ok(ValuePrimitive::Number(v.into())) } fn visit_i64(self, v: i64) -> Result where E: serde::de::Error, { - Ok(ValuePrimative::Number(v.into())) + Ok(ValuePrimitive::Number(v.into())) } } deserializer.deserialize_any(Visitor) diff --git a/foo b/foo deleted file mode 120000 index 5cea28a80..000000000 --- a/foo +++ /dev/null @@ -1 +0,0 @@ -firmware/x86_64/foo.romr.gz \ No newline at end of file diff --git a/sdk/lib/osBindings/RecoverySource.ts b/sdk/lib/osBindings/RecoverySource.ts index c40ec5132..40061f215 100644 --- a/sdk/lib/osBindings/RecoverySource.ts +++ b/sdk/lib/osBindings/RecoverySource.ts @@ -1,6 +1,11 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { BackupTargetFS } from "./BackupTargetFS" -export type RecoverySource = +export type RecoverySource = | { type: "migrate"; guid: string } - | { type: "backup"; target: BackupTargetFS } + | { + type: "backup" + target: BackupTargetFS + password: Password + serverId: string + } diff --git a/sdk/lib/osBindings/SetupExecuteParams.ts b/sdk/lib/osBindings/SetupExecuteParams.ts index 4593e7667..17c35c346 100644 --- a/sdk/lib/osBindings/SetupExecuteParams.ts +++ b/sdk/lib/osBindings/SetupExecuteParams.ts @@ -5,6 +5,5 @@ import type { RecoverySource } from "./RecoverySource" export type SetupExecuteParams = { startOsLogicalname: string startOsPassword: EncryptedWire - recoverySource: RecoverySource | null - recoveryPassword: EncryptedWire | null + recoverySource: RecoverySource | null } diff --git a/web/projects/install-wizard/src/app/services/api/mock-api.service.ts b/web/projects/install-wizard/src/app/services/api/mock-api.service.ts index 5be394c67..6b94f18db 100644 --- a/web/projects/install-wizard/src/app/services/api/mock-api.service.ts +++ b/web/projects/install-wizard/src/app/services/api/mock-api.service.ts @@ -18,11 +18,14 @@ export class MockApiService implements ApiService { capacity: 73264762332, used: null, startOs: { - version: '0.2.17', - full: true, - passwordHash: - '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', - wrappedKey: null, + '1234-5678-9876-5432': { + hostname: 'adjective-noun', + timestamp: new Date().toISOString(), + version: '0.2.17', + passwordHash: + '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', + wrappedKey: null, + }, }, guid: null, }, @@ -41,11 +44,14 @@ export class MockApiService implements ApiService { capacity: 73264762332, used: null, startOs: { - version: '0.3.3', - full: true, - passwordHash: - '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', - wrappedKey: null, + '1234-5678-9876-5432': { + hostname: 'adjective-noun', + timestamp: new Date().toISOString(), + version: '0.2.17', + passwordHash: + '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', + wrappedKey: null, + }, }, guid: null, }, @@ -64,11 +70,14 @@ export class MockApiService implements ApiService { capacity: 73264762332, used: null, startOs: { - version: '0.3.2', - full: true, - passwordHash: - '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', - wrappedKey: null, + '1234-5678-9876-5432': { + hostname: 'adjective-noun', + timestamp: new Date().toISOString(), + version: '0.2.17', + passwordHash: + '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', + wrappedKey: null, + }, }, guid: 'guid-guid-guid-guid', }, diff --git a/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.module.ts b/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.module.ts index b5c07d37c..78c09de0e 100644 --- a/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.module.ts +++ b/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.module.ts @@ -3,10 +3,11 @@ import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { FormsModule } from '@angular/forms' import { CifsModal } from './cifs-modal.page' +import { ServerBackupSelectModule } from '../server-backup-select/server-backup-select.module' @NgModule({ declarations: [CifsModal], - imports: [CommonModule, FormsModule, IonicModule], + imports: [CommonModule, FormsModule, IonicModule, ServerBackupSelectModule], exports: [CifsModal], }) export class CifsModalModule {} diff --git a/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts b/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts index 0664be38f..f6ce0e5bc 100644 --- a/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts +++ b/web/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts @@ -4,9 +4,9 @@ import { LoadingController, ModalController, } from '@ionic/angular' -import { ApiService, CifsBackupTarget } from 'src/app/services/api/api.service' +import { ApiService } from 'src/app/services/api/api.service' import { StartOSDiskInfo } from '@start9labs/shared' -import { PasswordPage } from '../password/password.page' +import { ServerBackupSelectModal } from '../server-backup-select/server-backup-select.page' @Component({ selector: 'cifs-modal', @@ -50,30 +50,29 @@ export class CifsModal { await loader.dismiss() - this.presentModalPassword(diskInfo) + this.presentModalSelectServer(diskInfo) } catch (e) { await loader.dismiss() this.presentAlertFailed() } } - private async presentModalPassword(diskInfo: StartOSDiskInfo): Promise { - const target: CifsBackupTarget = { - ...this.cifs, - mountable: true, - startOs: diskInfo, - } - + private async presentModalSelectServer( + servers: Record, + ): Promise { const modal = await this.modalController.create({ - component: PasswordPage, - componentProps: { target }, + component: ServerBackupSelectModal, + componentProps: { + servers: Object.keys(servers).map(id => ({ id, ...servers[id] })), + }, }) modal.onDidDismiss().then(res => { if (res.role === 'success') { this.modalController.dismiss( { cifs: this.cifs, - recoveryPassword: res.data.password, + serverId: res.data.serverId, + recoveryPassword: res.data.recoveryPassword, }, 'success', ) diff --git a/web/projects/setup-wizard/src/app/modals/password/password.page.ts b/web/projects/setup-wizard/src/app/modals/password/password.page.ts index 8ea00ce0c..f99607002 100644 --- a/web/projects/setup-wizard/src/app/modals/password/password.page.ts +++ b/web/projects/setup-wizard/src/app/modals/password/password.page.ts @@ -1,9 +1,5 @@ import { Component, Input, ViewChild } from '@angular/core' import { IonInput, ModalController } from '@ionic/angular' -import { - CifsBackupTarget, - DiskBackupTarget, -} from 'src/app/services/api/api.service' import * as argon2 from '@start9labs/argon2' @Component({ @@ -13,7 +9,7 @@ import * as argon2 from '@start9labs/argon2' }) export class PasswordPage { @ViewChild('focusInput') elem?: IonInput - @Input() target?: CifsBackupTarget | DiskBackupTarget + @Input() passwordHash = '' @Input() storageDrive = false pwError = '' @@ -31,13 +27,8 @@ export class PasswordPage { } async verifyPw() { - if (!this.target || !this.target.startOs) - this.pwError = 'No recovery target' // unreachable - try { - const passwordHash = this.target!.startOs?.passwordHash || '' - - argon2.verify(passwordHash, this.password) + argon2.verify(this.passwordHash, this.password) this.modalController.dismiss({ password: this.password }, 'success') } catch (e) { this.pwError = 'Incorrect password provided' @@ -55,7 +46,7 @@ export class PasswordPage { } validate() { - if (!!this.target) return (this.pwError = '') + if (!!this.passwordHash) return (this.pwError = '') if (this.passwordVer) { this.checkVer() diff --git a/web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.module.ts b/web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.module.ts new file mode 100644 index 000000000..8305cd2a6 --- /dev/null +++ b/web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { FormsModule } from '@angular/forms' +import { ServerBackupSelectModal } from './server-backup-select.page' +import { PasswordPageModule } from '../password/password.module' + +@NgModule({ + declarations: [ServerBackupSelectModal], + imports: [CommonModule, FormsModule, IonicModule, PasswordPageModule], + exports: [ServerBackupSelectModal], +}) +export class ServerBackupSelectModule {} diff --git a/web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.page.html b/web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.page.html new file mode 100644 index 000000000..37736d78e --- /dev/null +++ b/web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.page.html @@ -0,0 +1,24 @@ + + + Select Server to Restore + + + + + + +

+ Local Hostname + : {{ server.hostname }}.local +

+

+ StartOS Version + : {{ server.version }} +

+

+ Created + : {{ server.timestamp | date : 'medium' }} +

+
+
+
diff --git a/web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.page.scss b/web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.page.ts b/web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.page.ts new file mode 100644 index 000000000..9b0d8cf8b --- /dev/null +++ b/web/projects/setup-wizard/src/app/modals/server-backup-select/server-backup-select.page.ts @@ -0,0 +1,44 @@ +import { Component, Input } from '@angular/core' +import { ModalController } from '@ionic/angular' +import { StartOSDiskInfoWithId } from 'src/app/services/api/api.service' +import { PasswordPage } from '../password/password.page' + +@Component({ + selector: 'server-backup-select', + templateUrl: 'server-backup-select.page.html', + styleUrls: ['server-backup-select.page.scss'], +}) +export class ServerBackupSelectModal { + @Input() servers: StartOSDiskInfoWithId[] = [] + + constructor(private readonly modalController: ModalController) {} + + cancel() { + this.modalController.dismiss() + } + + async select(server: StartOSDiskInfoWithId): Promise { + this.presentModalPassword(server) + } + + private async presentModalPassword( + server: StartOSDiskInfoWithId, + ): Promise { + const modal = await this.modalController.create({ + component: PasswordPage, + componentProps: { passwordHash: server.passwordHash }, + }) + modal.onDidDismiss().then(res => { + if (res.role === 'success') { + this.modalController.dismiss( + { + serverId: server.id, + recoveryPassword: res.data.password, + }, + 'success', + ) + } + }) + await modal.present() + } +} diff --git a/web/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts b/web/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts index 110768be5..2961daf63 100644 --- a/web/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts +++ b/web/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts @@ -9,6 +9,7 @@ import { DiskInfo, ErrorService, GuidPipe } from '@start9labs/shared' import { ApiService } from 'src/app/services/api/api.service' import { StateService } from 'src/app/services/state.service' import { PasswordPage } from '../../modals/password/password.page' +import { T } from '@start9labs/start-sdk' @Component({ selector: 'app-embassy', @@ -50,15 +51,19 @@ export class EmbassyPage { const disks = await this.apiService.getDrives() if (this.stateService.setupType === 'fresh') { this.storageDrives = disks - } else if (this.stateService.setupType === 'restore') { - this.storageDrives = disks.filter( - d => - this.stateService.recoverySource?.type === 'backup' && - this.stateService.recoverySource.target?.type === 'disk' && - !d.partitions - .map(p => p.logicalname) - .includes(this.stateService.recoverySource.target.logicalname), - ) + } else if ( + this.stateService.setupType === 'restore' && + this.stateService.recoverySource?.type === 'backup' + ) { + if (this.stateService.recoverySource.target.type === 'disk') { + const logicalname = + this.stateService.recoverySource.target.logicalname + this.storageDrives = disks.filter( + d => !d.partitions.map(p => p.logicalname).includes(logicalname), + ) + } else { + this.storageDrives = disks + } } else if ( this.stateService.setupType === 'transfer' && this.stateService.recoverySource?.type === 'migrate' @@ -95,10 +100,10 @@ export class EmbassyPage { text: 'Continue', handler: () => { // for backup recoveries - if (this.stateService.recoveryPassword) { + if (this.stateService.recoverySource?.type === 'backup') { this.setupEmbassy( drive.logicalname, - this.stateService.recoveryPassword, + this.stateService.recoverySource.password, ) } else { // for migrations and fresh setups @@ -111,8 +116,11 @@ export class EmbassyPage { await alert.present() } else { // for backup recoveries - if (this.stateService.recoveryPassword) { - this.setupEmbassy(drive.logicalname, this.stateService.recoveryPassword) + if (this.stateService.recoverySource?.type === 'backup') { + this.setupEmbassy( + drive.logicalname, + this.stateService.recoverySource.password, + ) } else { // for migrations and fresh setups this.presentModalPassword(drive.logicalname) @@ -154,3 +162,7 @@ export class EmbassyPage { } } } + +function isDiskRecovery(source: T.RecoverySource): source is any { + return source.type === 'backup' && source.target.type === 'disk' +} diff --git a/web/projects/setup-wizard/src/app/pages/recover/drive-status.component.html b/web/projects/setup-wizard/src/app/pages/recover/drive-status.component.html deleted file mode 100644 index 7f4a4e5bd..000000000 --- a/web/projects/setup-wizard/src/app/pages/recover/drive-status.component.html +++ /dev/null @@ -1,14 +0,0 @@ -
- -

- - StartOS backup detected -

- - -

- - No StartOS backup -

-
-
diff --git a/web/projects/setup-wizard/src/app/pages/recover/recover.module.ts b/web/projects/setup-wizard/src/app/pages/recover/recover.module.ts index eaf04f506..7ecda048a 100644 --- a/web/projects/setup-wizard/src/app/pages/recover/recover.module.ts +++ b/web/projects/setup-wizard/src/app/pages/recover/recover.module.ts @@ -3,13 +3,13 @@ import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { FormsModule } from '@angular/forms' import { UnitConversionPipesModule } from '@start9labs/shared' -import { DriveStatusComponent, RecoverPage } from './recover.page' +import { RecoverPage } from './recover.page' import { PasswordPageModule } from '../../modals/password/password.module' import { RecoverPageRoutingModule } from './recover-routing.module' import { CifsModalModule } from 'src/app/modals/cifs-modal/cifs-modal.module' @NgModule({ - declarations: [RecoverPage, DriveStatusComponent], + declarations: [RecoverPage], imports: [ CommonModule, FormsModule, diff --git a/web/projects/setup-wizard/src/app/pages/recover/recover.page.html b/web/projects/setup-wizard/src/app/pages/recover/recover.page.html index 32f71a3ad..8b92b36d3 100644 --- a/web/projects/setup-wizard/src/app/pages/recover/recover.page.html +++ b/web/projects/setup-wizard/src/app/pages/recover/recover.page.html @@ -54,29 +54,21 @@
- - - + + -

{{ drive.label || drive.logicalname }}

- -

- {{ drive.vendor || 'Unknown Vendor' }} - {{ drive.model || - 'Unknown Model' }} -

-

Capacity: {{ drive.capacity | convertBytes }}

+

+ Local Hostname + : {{ server.hostname }}.local +

+

+ StartOS Version + : {{ server.version }} +

+

+ Created + : {{ server.timestamp | date : 'medium' }} +

diff --git a/web/projects/setup-wizard/src/app/pages/recover/recover.page.ts b/web/projects/setup-wizard/src/app/pages/recover/recover.page.ts index d47105f24..dd1dae9ce 100644 --- a/web/projects/setup-wizard/src/app/pages/recover/recover.page.ts +++ b/web/projects/setup-wizard/src/app/pages/recover/recover.page.ts @@ -1,8 +1,11 @@ -import { Component, Input } from '@angular/core' +import { Component } from '@angular/core' import { ModalController, NavController } from '@ionic/angular' import { ErrorService } from '@start9labs/shared' import { CifsModal } from 'src/app/modals/cifs-modal/cifs-modal.page' -import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service' +import { + ApiService, + StartOSDiskInfoWithId, +} from 'src/app/services/api/api.service' import { StateService } from 'src/app/services/state.service' import { PasswordPage } from '../../modals/password/password.page' @@ -13,7 +16,7 @@ import { PasswordPage } from '../../modals/password/password.page' }) export class RecoverPage { loading = true - mappedDrives: MappedDisk[] = [] + servers: StartOSDiskInfoWithId[] = [] constructor( private readonly apiService: ApiService, @@ -34,33 +37,19 @@ export class RecoverPage { await this.getDrives() } - driveClickable(mapped: MappedDisk) { - return mapped.drive.startOs?.full - } - async getDrives() { - this.mappedDrives = [] try { - const disks = await this.apiService.getDrives() - disks - .filter(d => d.partitions.length) - .forEach(d => { - d.partitions.forEach(p => { - const drive: DiskBackupTarget = { - vendor: d.vendor, - model: d.model, - logicalname: p.logicalname, - label: p.label, - capacity: p.capacity, - used: p.used, - startOs: p.startOs, - } - this.mappedDrives.push({ - hasValidBackup: !!p.startOs?.full, - drive, - }) - }) - }) + const drives = await this.apiService.getDrives() + this.servers = drives.flatMap(drive => + drive.partitions.flatMap(partition => + Object.entries(partition.startOs).map(([id, val]) => ({ + id, + ...val, + partition, + drive, + })), + ), + ) } catch (e: any) { this.errorService.handleError(e) } finally { @@ -74,65 +63,41 @@ export class RecoverPage { }) modal.onDidDismiss().then(res => { if (res.role === 'success') { - const { hostname, path, username, password } = res.data.cifs this.stateService.recoverySource = { type: 'backup', target: { type: 'cifs', - hostname, - path, - username, - password, + ...res.data.cifs, }, + serverId: res.data.serverId, + password: res.data.recoveryPassword, } - this.stateService.recoveryPassword = res.data.recoveryPassword this.navCtrl.navigateForward('/storage') } }) await modal.present() } - async select(target: DiskBackupTarget) { - const { logicalname } = target - - if (!logicalname) return - + async select(server: StartOSDiskInfoWithId) { const modal = await this.modalController.create({ component: PasswordPage, - componentProps: { target }, + componentProps: { passwordHash: server.passwordHash }, cssClass: 'alertlike-modal', }) modal.onDidDismiss().then(res => { - if (res.data?.password) { - this.selectRecoverySource(logicalname, res.data.password) + if (res.role === 'success') { + this.stateService.recoverySource = { + type: 'backup', + target: { + type: 'disk', + logicalname: res.data.logicalname, + }, + serverId: server.id, + password: res.data.password, + } + this.navCtrl.navigateForward(`/storage`) } }) await modal.present() } - - private async selectRecoverySource(logicalname: string, password?: string) { - this.stateService.recoverySource = { - type: 'backup', - target: { - type: 'disk', - logicalname, - }, - } - this.stateService.recoveryPassword = password - this.navCtrl.navigateForward(`/storage`) - } -} - -@Component({ - selector: 'drive-status', - templateUrl: './drive-status.component.html', - styleUrls: ['./recover.page.scss'], -}) -export class DriveStatusComponent { - @Input() hasValidBackup!: boolean -} - -interface MappedDisk { - hasValidBackup: boolean - drive: DiskBackupTarget } diff --git a/web/projects/setup-wizard/src/app/pages/success/success.module.ts b/web/projects/setup-wizard/src/app/pages/success/success.module.ts index c0a7a0ec2..7abb5d2d8 100644 --- a/web/projects/setup-wizard/src/app/pages/success/success.module.ts +++ b/web/projects/setup-wizard/src/app/pages/success/success.module.ts @@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { FormsModule } from '@angular/forms' import { ResponsiveColModule } from '@start9labs/shared' - import { SuccessPage } from './success.page' import { PasswordPageModule } from '../../modals/password/password.module' import { SuccessPageRoutingModule } from './success-routing.module' diff --git a/web/projects/setup-wizard/src/app/pages/success/success.page.ts b/web/projects/setup-wizard/src/app/pages/success/success.page.ts index 88262bdf2..dab0b44a6 100644 --- a/web/projects/setup-wizard/src/app/pages/success/success.page.ts +++ b/web/projects/setup-wizard/src/app/pages/success/success.page.ts @@ -8,7 +8,6 @@ import { StateService } from 'src/app/services/state.service' selector: 'success', templateUrl: 'success.page.html', styleUrls: ['success.page.scss'], - providers: [DownloadHTMLService], }) export class SuccessPage { @ViewChild('canvas', { static: true }) diff --git a/web/projects/setup-wizard/src/app/services/api/api.service.ts b/web/projects/setup-wizard/src/app/services/api/api.service.ts index 6719ce859..882d656ae 100644 --- a/web/projects/setup-wizard/src/app/services/api/api.service.ts +++ b/web/projects/setup-wizard/src/app/services/api/api.service.ts @@ -1,5 +1,10 @@ import * as jose from 'node-jose' -import { DiskListResponse, StartOSDiskInfo } from '@start9labs/shared' +import { + DiskInfo, + DiskListResponse, + PartitionInfo, + StartOSDiskInfo, +} from '@start9labs/shared' import { T } from '@start9labs/start-sdk' import { WebSocketSubjectConfig } from 'rxjs/webSocket' import { Observable } from 'rxjs' @@ -10,14 +15,16 @@ export abstract class ApiService { abstract getStatus(): Promise // setup.status abstract getPubKey(): Promise // setup.get-pubkey abstract getDrives(): Promise // setup.disk.list - abstract verifyCifs(cifs: T.VerifyCifsParams): Promise // setup.cifs.verify + abstract verifyCifs( + cifs: T.VerifyCifsParams, + ): Promise> // setup.cifs.verify abstract attach(importInfo: T.AttachParams): Promise // setup.attach abstract execute(setupInfo: T.SetupExecuteParams): Promise // setup.execute abstract complete(): Promise // setup.complete abstract exit(): Promise // setup.exit abstract openProgressWebsocket$(guid: string): Observable - async encrypt(toEncrypt: string): Promise { + async encrypt(toEncrypt: string): Promise { if (!this.pubkey) throw new Error('No pubkey found!') const encrypted = await jose.JWE.createEncrypt(this.pubkey!) .update(toEncrypt) @@ -28,26 +35,13 @@ export abstract class ApiService { } } -type Encrypted = { - encrypted: string -} - export type WebsocketConfig = Omit, 'url'> -export type DiskBackupTarget = { - vendor: string | null - model: string | null - logicalname: string | null - label: string | null - capacity: number - used: number | null - startOs: StartOSDiskInfo | null +export type StartOSDiskInfoWithId = StartOSDiskInfo & { + id: string } -export type CifsBackupTarget = { - hostname: string - path: string - username: string - mountable: boolean - startOs: StartOSDiskInfo | null +export type StartOSDiskInfoFull = StartOSDiskInfoWithId & { + partition: PartitionInfo + drive: DiskInfo } diff --git a/web/projects/setup-wizard/src/app/services/api/live-api.service.ts b/web/projects/setup-wizard/src/app/services/api/live-api.service.ts index f431f5151..0245bf554 100644 --- a/web/projects/setup-wizard/src/app/services/api/live-api.service.ts +++ b/web/projects/setup-wizard/src/app/services/api/live-api.service.ts @@ -9,7 +9,7 @@ import { RPCOptions, } from '@start9labs/shared' import { T } from '@start9labs/start-sdk' -import { ApiService, WebsocketConfig } from './api.service' +import { ApiService } from './api.service' import * as jose from 'node-jose' import { Observable } from 'rxjs' import { DOCUMENT } from '@angular/common' @@ -65,9 +65,11 @@ export class LiveApiService extends ApiService { }) } - async verifyCifs(source: T.VerifyCifsParams): Promise { + async verifyCifs( + source: T.VerifyCifsParams, + ): Promise> { source.path = source.path.replace('/\\/g', '/') - return this.rpcRequest({ + return this.rpcRequest>({ method: 'setup.cifs.verify', params: source, }) diff --git a/web/projects/setup-wizard/src/app/services/api/mock-api.service.ts b/web/projects/setup-wizard/src/app/services/api/mock-api.service.ts index 0a1c221f7..a5757b010 100644 --- a/web/projects/setup-wizard/src/app/services/api/mock-api.service.ts +++ b/web/projects/setup-wizard/src/app/services/api/mock-api.service.ts @@ -175,11 +175,14 @@ export class MockApiService extends ApiService { capacity: 1979120929996, used: null, startOs: { - version: '0.2.17', - full: true, - passwordHash: - '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', - wrappedKey: null, + '1234-5678-9876-5432': { + hostname: 'adjective-noun', + version: '0.2.17', + timestamp: new Date().toISOString(), + passwordHash: + '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', + wrappedKey: null, + }, }, guid: null, }, @@ -198,11 +201,14 @@ export class MockApiService extends ApiService { capacity: 73264762332, used: null, startOs: { - version: '0.3.3', - full: true, - passwordHash: - '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', - wrappedKey: null, + '1234-5678-9876-5432': { + hostname: 'adjective-noun', + version: '0.2.17', + timestamp: new Date().toISOString(), + passwordHash: + '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', + wrappedKey: null, + }, }, guid: null, }, @@ -221,11 +227,14 @@ export class MockApiService extends ApiService { capacity: 73264762332, used: null, startOs: { - version: '0.3.2', - full: true, - passwordHash: - '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', - wrappedKey: null, + '1234-5678-9876-5432': { + hostname: 'adjective-noun', + version: '0.2.17', + timestamp: new Date().toISOString(), + passwordHash: + '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', + wrappedKey: null, + }, }, guid: 'guid-guid-guid-guid', }, @@ -236,14 +245,19 @@ export class MockApiService extends ApiService { ] } - async verifyCifs(params: T.VerifyCifsParams): Promise { + async verifyCifs( + params: T.VerifyCifsParams, + ): Promise> { await pauseFor(1000) return { - version: '0.3.0', - full: true, - passwordHash: - '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', - wrappedKey: '', + '9876-5432-1234-5678': { + hostname: 'adjective-noun', + version: '0.3.6', + timestamp: new Date().toISOString(), + passwordHash: + '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', + wrappedKey: '', + }, } } diff --git a/web/projects/setup-wizard/src/app/services/state.service.ts b/web/projects/setup-wizard/src/app/services/state.service.ts index 8c653a088..8f4290ac0 100644 --- a/web/projects/setup-wizard/src/app/services/state.service.ts +++ b/web/projects/setup-wizard/src/app/services/state.service.ts @@ -7,8 +7,7 @@ import { T } from '@start9labs/start-sdk' }) export class StateService { setupType?: 'fresh' | 'restore' | 'attach' | 'transfer' - recoverySource?: T.RecoverySource - recoveryPassword?: string + recoverySource?: T.RecoverySource constructor(private readonly api: ApiService) {} @@ -26,9 +25,13 @@ export class StateService { await this.api.execute({ startOsLogicalname: storageLogicalname, startOsPassword: await this.api.encrypt(password), - recoverySource: this.recoverySource || null, - recoveryPassword: this.recoveryPassword - ? await this.api.encrypt(this.recoveryPassword) + recoverySource: this.recoverySource + ? this.recoverySource.type === 'migrate' + ? this.recoverySource + : { + ...this.recoverySource, + password: await this.api.encrypt(this.recoverySource.password), + } : null, }) } diff --git a/web/projects/shared/src/services/download-html.service.ts b/web/projects/shared/src/services/download-html.service.ts index 81f7b945b..13a146186 100644 --- a/web/projects/shared/src/services/download-html.service.ts +++ b/web/projects/shared/src/services/download-html.service.ts @@ -1,7 +1,9 @@ import { DOCUMENT } from '@angular/common' import { Inject, Injectable } from '@angular/core' -@Injectable() +@Injectable({ + providedIn: 'root', +}) export class DownloadHTMLService { constructor(@Inject(DOCUMENT) private readonly document: Document) {} diff --git a/web/projects/shared/src/types/api.ts b/web/projects/shared/src/types/api.ts index 743ea6ac8..c53ed5e33 100644 --- a/web/projects/shared/src/types/api.ts +++ b/web/projects/shared/src/types/api.ts @@ -32,13 +32,14 @@ export interface PartitionInfo { label: string | null capacity: number used: number | null - startOs: StartOSDiskInfo | null + startOs: Record guid: string | null } export type StartOSDiskInfo = { + hostname: string version: string - full: boolean + timestamp: string passwordHash: string | null wrappedKey: string | null } diff --git a/web/projects/ui/src/app/app/preloader/preloader.component.html b/web/projects/ui/src/app/app/preloader/preloader.component.html index 82923250e..b94b3fa0c 100644 --- a/web/projects/ui/src/app/app/preloader/preloader.component.html +++ b/web/projects/ui/src/app/app/preloader/preloader.component.html @@ -3,7 +3,7 @@ - + @@ -58,21 +58,21 @@ - - - - - - - - - + + + + + + + + + + + - - - - + + diff --git a/web/projects/ui/src/app/app/preloader/preloader.component.ts b/web/projects/ui/src/app/app/preloader/preloader.component.ts index f7184008e..177362222 100644 --- a/web/projects/ui/src/app/app/preloader/preloader.component.ts +++ b/web/projects/ui/src/app/app/preloader/preloader.component.ts @@ -1,4 +1,5 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' +import { FormControl } from '@angular/forms' import { ActionSheetController, AlertController, @@ -122,6 +123,7 @@ const TAIGA = [ export class PreloaderComponent { readonly icons = ICONS readonly taiga = TAIGA + readonly control = new FormControl() constructor( _modals: ModalController, diff --git a/web/projects/ui/src/app/app/preloader/preloader.module.ts b/web/projects/ui/src/app/app/preloader/preloader.module.ts index 380b70f3a..0f22efb25 100644 --- a/web/projects/ui/src/app/app/preloader/preloader.module.ts +++ b/web/projects/ui/src/app/app/preloader/preloader.module.ts @@ -1,5 +1,6 @@ import { CommonModule } from '@angular/common' import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core' +import { ReactiveFormsModule } from '@angular/forms' import { IonicModule } from '@ionic/angular' import { TuiErrorModule, @@ -26,7 +27,7 @@ import { TuiProgressModule, TuiRadioListModule, TuiSelectModule, - TuiTextAreaModule, + TuiTextareaModule, TuiToggleModule, } from '@taiga-ui/kit' import { QrCodeModule } from 'ng-qrcode' @@ -35,6 +36,7 @@ import { PreloaderComponent } from './preloader.component' @NgModule({ imports: [ CommonModule, + ReactiveFormsModule, IonicModule, QrCodeModule, TuiTooltipModule, @@ -52,7 +54,7 @@ import { PreloaderComponent } from './preloader.component' TuiInputNumberModule, TuiExpandModule, TuiSelectModule, - TuiTextAreaModule, + TuiTextareaModule, TuiToggleModule, TuiElasticContainerModule, TuiCellModule, diff --git a/web/projects/ui/src/app/components/backup-drives/backup-drives-status.component.html b/web/projects/ui/src/app/components/backup-drives/backup-drives-status.component.html index e0437cd1d..ad422a8f8 100644 --- a/web/projects/ui/src/app/components/backup-drives/backup-drives-status.component.html +++ b/web/projects/ui/src/app/components/backup-drives/backup-drives-status.component.html @@ -1,20 +1,16 @@

- {{ - hasValidBackup - ? 'Available, contains existing backup' - : 'Available for fresh backup' - }} + Available for backup

-

+

- StartOS backup detected + StartOS backups detected

-

+

- No StartOS backup + No StartOS backups

diff --git a/web/projects/ui/src/app/components/backup-drives/backup-drives.component.html b/web/projects/ui/src/app/components/backup-drives/backup-drives.component.html index 141872084..82e508420 100644 --- a/web/projects/ui/src/app/components/backup-drives/backup-drives.component.html +++ b/web/projects/ui/src/app/components/backup-drives/backup-drives.component.html @@ -73,7 +73,7 @@

@@ -155,7 +155,7 @@

{{ drive.label || drive.logicalname }}

{{ drive.vendor || 'Unknown Vendor' }} - diff --git a/web/projects/ui/src/app/components/backup-drives/backup-drives.component.ts b/web/projects/ui/src/app/components/backup-drives/backup-drives.component.ts index 5613ae153..a4b272c46 100644 --- a/web/projects/ui/src/app/components/backup-drives/backup-drives.component.ts +++ b/web/projects/ui/src/app/components/backup-drives/backup-drives.component.ts @@ -72,10 +72,10 @@ export class BackupDrivesComponent { return } - if (this.type === 'restore' && !target.hasValidBackup) { + if (this.type === 'restore' && !target.hasAnyBackup) { const message = `${ target.entry.type === 'cifs' ? 'Network Folder' : 'Drive partition' - } does not contain a valid Start9 Server backup.` + } does not contain a valid backup.` this.presentAlertError(message) return } @@ -153,7 +153,7 @@ export class BackupDrivesComponent { const [id, entry] = Object.entries(res)[0] this.backupService.cifs.unshift({ id, - hasValidBackup: this.backupService.hasValidBackup(entry), + hasAnyBackup: this.backupService.hasAnyBackup(entry), entry, }) return true @@ -258,7 +258,7 @@ export class BackupDrivesHeaderComponent { }) export class BackupDrivesStatusComponent { @Input() type!: BackupType - @Input() hasValidBackup!: boolean + @Input() hasAnyBackup!: boolean } const cifsSpec = CB.Config.of({ diff --git a/web/projects/ui/src/app/components/backup-drives/backup.service.ts b/web/projects/ui/src/app/components/backup-drives/backup.service.ts index 6beec7e2d..cc1898e7e 100644 --- a/web/projects/ui/src/app/components/backup-drives/backup.service.ts +++ b/web/projects/ui/src/app/components/backup-drives/backup.service.ts @@ -34,7 +34,7 @@ export class BackupService { .map(([id, cifs]) => { return { id, - hasValidBackup: this.hasValidBackup(cifs), + hasAnyBackup: this.hasAnyBackup(cifs), entry: cifs as CifsBackupTarget, } }) @@ -44,7 +44,7 @@ export class BackupService { .map(([id, drive]) => { return { id, - hasValidBackup: this.hasValidBackup(drive), + hasAnyBackup: this.hasAnyBackup(drive), entry: drive as DiskBackupTarget, } }) @@ -55,8 +55,16 @@ export class BackupService { } } - hasValidBackup(target: BackupTarget): boolean { - const backup = target.startOs - return !!backup && this.emver.compare(backup.version, '0.3.0') !== -1 + hasAnyBackup(target: BackupTarget): boolean { + return Object.values(target.startOs).some( + s => this.emver.compare(s.version, '0.3.6') !== -1, + ) + } + + async hasThisBackup(target: BackupTarget, id: string): Promise { + return ( + target.startOs[id] && + this.emver.compare(target.startOs[id].version, '0.3.6') !== -1 + ) } } diff --git a/web/projects/ui/src/app/components/form/form-group/form-group.component.html b/web/projects/ui/src/app/components/form/form-group/form-group.component.html index d3c769b98..1c4f8301a 100644 --- a/web/projects/ui/src/app/components/form/form-group/form-group.component.html +++ b/web/projects/ui/src/app/components/form/form-group/form-group.component.html @@ -1,5 +1,5 @@ * - + diff --git a/web/projects/ui/src/app/components/form/form.module.ts b/web/projects/ui/src/app/components/form/form.module.ts index fe16b229f..b7a36cd1f 100644 --- a/web/projects/ui/src/app/components/form/form.module.ts +++ b/web/projects/ui/src/app/components/form/form.module.ts @@ -30,7 +30,7 @@ import { TuiPromptModule, TuiSelectModule, TuiTagModule, - TuiTextAreaModule, + TuiTextareaModule, TuiToggleModule, } from '@taiga-ui/kit' @@ -60,7 +60,7 @@ import { HintPipe } from './hint.pipe' TuiInputModule, TuiInputNumberModule, TuiInputFilesModule, - TuiTextAreaModule, + TuiTextareaModule, TuiSelectModule, TuiMultiSelectModule, TuiToggleModule, diff --git a/web/projects/ui/src/app/components/logs/logs.component.ts b/web/projects/ui/src/app/components/logs/logs.component.ts index c1ed61f09..59d78dac7 100644 --- a/web/projects/ui/src/app/components/logs/logs.component.ts +++ b/web/projects/ui/src/app/components/logs/logs.component.ts @@ -39,7 +39,7 @@ var convert = new Convert({ selector: 'logs', templateUrl: './logs.component.html', styleUrls: ['./logs.component.scss'], - providers: [TuiDestroyService, DownloadHTMLService], + providers: [TuiDestroyService], }) export class LogsComponent { @ViewChild(IonContent) diff --git a/web/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.html b/web/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.html index 9dfb72da9..f1372f468 100644 --- a/web/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.html +++ b/web/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.html @@ -18,8 +18,8 @@

{{ option.title }}

Version {{ option.version }}

-

Backup made: {{ option.timestamp | date : 'medium' }}

-

+

Created: {{ option.timestamp | date : 'medium' }}

+

Ready to restore

@@ -27,7 +27,7 @@ Unavailable. {{ option.title }} is already installed.

-

+

Unavailable. Backup was made on a newer version of StartOS. @@ -36,7 +36,7 @@ diff --git a/web/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.ts b/web/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.ts index e1d637ecf..c9869bce6 100644 --- a/web/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.ts +++ b/web/projects/ui/src/app/modals/app-recover-select/app-recover-select.page.ts @@ -14,10 +14,10 @@ import { AppRecoverOption } from './to-options.pipe' styleUrls: ['./app-recover-select.page.scss'], }) export class AppRecoverSelectPage { - @Input() id!: string + @Input() targetId!: string + @Input() serverId!: string @Input() backupInfo!: BackupInfo @Input() password!: string - @Input() oldPassword?: string readonly packageData$ = this.patch.watch$('packageData').pipe(take(1)) @@ -46,8 +46,8 @@ export class AppRecoverSelectPage { try { await this.embassyApi.restorePackages({ ids, - targetId: this.id, - oldPassword: this.oldPassword || null, + targetId: this.targetId, + serverId: this.serverId, password: this.password, }) this.modalCtrl.dismiss(undefined, 'success') diff --git a/web/projects/ui/src/app/modals/app-recover-select/to-options.pipe.ts b/web/projects/ui/src/app/modals/app-recover-select/to-options.pipe.ts index 6a81df4bc..8a607896e 100644 --- a/web/projects/ui/src/app/modals/app-recover-select/to-options.pipe.ts +++ b/web/projects/ui/src/app/modals/app-recover-select/to-options.pipe.ts @@ -10,7 +10,7 @@ export interface AppRecoverOption extends PackageBackupInfo { id: string checked: boolean installed: boolean - 'newer-eos': boolean + newerOS: boolean } @Pipe({ @@ -34,7 +34,7 @@ export class ToOptionsPipe implements PipeTransform { id, installed: !!packageData[id], checked: false, - 'newer-eos': this.compare(packageBackups[id].osVersion), + newerOS: this.compare(packageBackups[id].osVersion), })) .sort((a, b) => b.title.toLowerCase() > a.title.toLowerCase() ? -1 : 1, diff --git a/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.module.ts b/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.module.ts new file mode 100644 index 000000000..958b98dff --- /dev/null +++ b/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { FormsModule } from '@angular/forms' +import { BackupServerSelectModal } from './backup-server-select.page' +import { AppRecoverSelectPageModule } from 'src/app/modals/app-recover-select/app-recover-select.module' + +@NgModule({ + declarations: [BackupServerSelectModal], + imports: [CommonModule, FormsModule, IonicModule, AppRecoverSelectPageModule], + exports: [BackupServerSelectModal], +}) +export class BackupServerSelectModule {} diff --git a/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.html b/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.html new file mode 100644 index 000000000..e5b2369e5 --- /dev/null +++ b/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.html @@ -0,0 +1,35 @@ + + + Select Server Backup + + + + + + + + + + + + +

+ Local Hostname + : {{ server.value.hostname }}.local +

+

+ StartOS Version + : {{ server.value.version }} +

+

+ Created + : {{ server.value.timestamp | date : 'medium' }} +

+ +
+ + diff --git a/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.scss b/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.scss new file mode 100644 index 000000000..e69de29bb diff --git a/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.ts b/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.ts new file mode 100644 index 000000000..e1f4f8961 --- /dev/null +++ b/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.ts @@ -0,0 +1,103 @@ +import { Component, Input } from '@angular/core' +import { ModalController, NavController } from '@ionic/angular' +import * as argon2 from '@start9labs/argon2' +import { + ErrorService, + LoadingService, + StartOSDiskInfo, +} from '@start9labs/shared' +import { + BackupInfo, + CifsBackupTarget, + DiskBackupTarget, +} from 'src/app/services/api/api.types' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { MappedBackupTarget } from 'src/app/types/mapped-backup-target' +import { AppRecoverSelectPage } from '../app-recover-select/app-recover-select.page' +import { PasswordPromptModal } from './password-prompt.modal' + +@Component({ + selector: 'backup-server-select', + templateUrl: 'backup-server-select.page.html', + styleUrls: ['backup-server-select.page.scss'], +}) +export class BackupServerSelectModal { + @Input() target!: MappedBackupTarget + + constructor( + private readonly modalCtrl: ModalController, + private readonly loader: LoadingService, + private readonly api: ApiService, + private readonly navCtrl: NavController, + private readonly errorService: ErrorService, + ) {} + + dismiss() { + this.modalCtrl.dismiss() + } + + async presentModalPassword( + serverId: string, + server: StartOSDiskInfo, + ): Promise { + const modal = await this.modalCtrl.create({ + component: PasswordPromptModal, + }) + modal.present() + + const { data, role } = await modal.onWillDismiss() + + if (role === 'confirm') { + try { + argon2.verify(server.passwordHash!, data) + await this.restoreFromBackup(serverId, data) + } catch (e: any) { + this.errorService.handleError(e) + } + } + } + + private async restoreFromBackup( + serverId: string, + password: string, + ): Promise { + const loader = this.loader.open('Decrypting drive...').subscribe() + + try { + const backupInfo = await this.api.getBackupInfo({ + targetId: this.target.id, + serverId, + password, + }) + this.presentModalSelect(serverId, backupInfo, password) + } finally { + loader.unsubscribe() + } + } + + private async presentModalSelect( + serverId: string, + backupInfo: BackupInfo, + password: string, + ): Promise { + const modal = await this.modalCtrl.create({ + componentProps: { + targetId: this.target.id, + serverId, + backupInfo, + password, + }, + presentingElement: await this.modalCtrl.getTop(), + component: AppRecoverSelectPage, + }) + + modal.onDidDismiss().then(res => { + if (res.role === 'success') { + this.modalCtrl.dismiss(undefined, 'success') + this.navCtrl.navigateRoot('/services') + } + }) + + await modal.present() + } +} diff --git a/web/projects/ui/src/app/modals/backup-server-select/password-prompt.modal.ts b/web/projects/ui/src/app/modals/backup-server-select/password-prompt.modal.ts new file mode 100644 index 000000000..2af89e3d0 --- /dev/null +++ b/web/projects/ui/src/app/modals/backup-server-select/password-prompt.modal.ts @@ -0,0 +1,69 @@ +import { Component } from '@angular/core' +import { FormsModule } from '@angular/forms' +import { IonicModule, ModalController } from '@ionic/angular' +import { TuiInputPasswordModule } from '@taiga-ui/kit' + +@Component({ + standalone: true, + template: ` + + + Decrypt Backup + + + + + + + + + +

+ Enter the password that was used to encrypt this backup. On the next + screen, you will select the individual services you want to restore. +

+

+ + Enter password + +

+
+ + + + + Cancel + + + Next + + + + `, + imports: [IonicModule, FormsModule, TuiInputPasswordModule], +}) +export class PasswordPromptModal { + password = '' + + constructor(private modalCtrl: ModalController) {} + + cancel() { + return this.modalCtrl.dismiss(null, 'cancel') + } + + confirm() { + return this.modalCtrl.dismiss(this.password, 'confirm') + } +} diff --git a/web/projects/ui/src/app/pages/server-routes/restore/restore.component.html b/web/projects/ui/src/app/pages/server-routes/restore/restore.component.html index 7440c9a77..ef22a587b 100644 --- a/web/projects/ui/src/app/pages/server-routes/restore/restore.component.html +++ b/web/projects/ui/src/app/pages/server-routes/restore/restore.component.html @@ -1,5 +1,5 @@ diff --git a/web/projects/ui/src/app/pages/server-routes/restore/restore.component.module.ts b/web/projects/ui/src/app/pages/server-routes/restore/restore.component.module.ts index 8cb4f6916..da99e66a2 100644 --- a/web/projects/ui/src/app/pages/server-routes/restore/restore.component.module.ts +++ b/web/projects/ui/src/app/pages/server-routes/restore/restore.component.module.ts @@ -5,7 +5,7 @@ import { IonicModule } from '@ionic/angular' import { RestorePage } from './restore.component' import { SharedPipesModule } from '@start9labs/shared' import { BackupDrivesComponentModule } from 'src/app/components/backup-drives/backup-drives.component.module' -import { AppRecoverSelectPageModule } from 'src/app/modals/app-recover-select/app-recover-select.module' +import { BackupServerSelectModule } from 'src/app/modals/backup-server-select/backup-server-select.module' const routes: Routes = [ { @@ -21,7 +21,7 @@ const routes: Routes = [ RouterModule.forChild(routes), SharedPipesModule, BackupDrivesComponentModule, - AppRecoverSelectPageModule, + BackupServerSelectModule, ], declarations: [RestorePage], }) diff --git a/web/projects/ui/src/app/pages/server-routes/restore/restore.component.ts b/web/projects/ui/src/app/pages/server-routes/restore/restore.component.ts index ae644399f..58f679908 100644 --- a/web/projects/ui/src/app/pages/server-routes/restore/restore.component.ts +++ b/web/projects/ui/src/app/pages/server-routes/restore/restore.component.ts @@ -1,18 +1,11 @@ import { Component } from '@angular/core' -import { ModalController, NavController } from '@ionic/angular' -import { LoadingService } from '@start9labs/shared' -import { TuiDialogService } from '@taiga-ui/core' -import { take } from 'rxjs/operators' -import { PROMPT, PromptOptions } from 'src/app/modals/prompt.component' -import { ApiService } from 'src/app/services/api/embassy-api.service' +import { ModalController } from '@ionic/angular' import { MappedBackupTarget } from 'src/app/types/mapped-backup-target' import { - BackupInfo, CifsBackupTarget, DiskBackupTarget, } from 'src/app/services/api/api.types' -import { AppRecoverSelectPage } from 'src/app/modals/app-recover-select/app-recover-select.page' -import * as argon2 from '@start9labs/argon2' +import { BackupServerSelectModal } from 'src/app/modals/backup-server-select/backup-server-select.page' @Component({ selector: 'restore', @@ -20,78 +13,15 @@ import * as argon2 from '@start9labs/argon2' styleUrls: ['./restore.component.scss'], }) export class RestorePage { - constructor( - private readonly modalCtrl: ModalController, - private readonly dialogs: TuiDialogService, - private readonly navCtrl: NavController, - private readonly embassyApi: ApiService, - private readonly loader: LoadingService, - ) {} + constructor(private readonly modalCtrl: ModalController) {} - async presentModalPassword( + async presentModalSelectServer( target: MappedBackupTarget, - ): Promise { - const options: PromptOptions = { - message: - 'Enter the master password that was used to encrypt this backup. On the next screen, you will select the individual services you want to restore.', - label: 'Master Password', - placeholder: 'Enter master password', - useMask: true, - buttonText: 'Next', - } - - this.dialogs - .open(PROMPT, { - label: 'Password Required', - data: options, - }) - .pipe(take(1)) - .subscribe(async (password: string) => { - const passwordHash = target.entry.startOs?.passwordHash || '' - argon2.verify(passwordHash, password) - await this.restoreFromBackup(target, password) - }) - } - - private async restoreFromBackup( - target: MappedBackupTarget, - password: string, - oldPassword?: string, - ): Promise { - const loader = this.loader.open('Decrypting drive...').subscribe() - - try { - const backupInfo = await this.embassyApi.getBackupInfo({ - targetId: target.id, - password, - }) - this.presentModalSelect(target.id, backupInfo, password, oldPassword) - } finally { - loader.unsubscribe() - } - } - - private async presentModalSelect( - id: string, - backupInfo: BackupInfo, - password: string, - oldPassword?: string, ): Promise { const modal = await this.modalCtrl.create({ - componentProps: { - id, - backupInfo, - password, - oldPassword, - }, + componentProps: { target }, presentingElement: await this.modalCtrl.getTop(), - component: AppRecoverSelectPage, - }) - - modal.onWillDismiss().then(res => { - if (res.role === 'success') { - this.navCtrl.navigateRoot('/services') - } + component: BackupServerSelectModal, }) await modal.present() diff --git a/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts b/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts index 804a88fab..b570b1603 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts @@ -17,6 +17,7 @@ import { BackupSelectPage } from 'src/app/modals/backup-select/backup-select.pag import { EOSService } from 'src/app/services/eos.service' import { getServerInfo } from 'src/app/util/get-server-info' import { DataModel } from 'src/app/services/patch-db/data-model' +import { BackupService } from 'src/app/components/backup-drives/backup.service' @Component({ selector: 'server-backup', @@ -38,6 +39,7 @@ export class ServerBackupPage { private readonly destroy$: TuiDestroyService, private readonly eosService: EOSService, private readonly patch: PatchDB, + private readonly backupService: BackupService, ) {} ngOnInit() { @@ -86,19 +88,18 @@ export class ServerBackupPage { }) .pipe(take(1)) .subscribe(async (password: string) => { + const { passwordHash, id } = await getServerInfo(this.patch) + // confirm password matches current master password - const { passwordHash } = await getServerInfo(this.patch) argon2.verify(passwordHash, password) // first time backup - if (!target.hasValidBackup) { + if (!this.backupService.hasThisBackup(target.entry, id)) { await this.createBackup(target, password) // existing backup } else { try { - const passwordHash = target.entry.startOs?.passwordHash || '' - - argon2.verify(passwordHash, password) + argon2.verify(target.entry.startOs[id].passwordHash!, password) } catch { setTimeout( () => this.presentModalOldPassword(target, password), @@ -124,6 +125,8 @@ export class ServerBackupPage { buttonText: 'Create Backup', } + const { id } = await getServerInfo(this.patch) + this.dialogs .open(PROMPT, { label: 'Original Password Needed', @@ -131,8 +134,7 @@ export class ServerBackupPage { }) .pipe(take(1)) .subscribe(async (oldPassword: string) => { - const passwordHash = target.entry.startOs?.passwordHash || '' - + const passwordHash = target.entry.startOs[id].passwordHash! argon2.verify(passwordHash, oldPassword) await this.createBackup(target, password, oldPassword) }) diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index abddbcf90..a1f81fd95 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -600,12 +600,15 @@ export module Mock { username: 'TestUser', mountable: false, startOs: { - version: '0.3.0', - full: true, - passwordHash: - // password is asdfasdf - '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', - wrappedKey: '', + '1234-5678-9876-5432': { + hostname: 'adjective-noun', + timestamp: new Date().toISOString(), + version: '0.3.6', + passwordHash: + // password is asdfasdf + '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', + wrappedKey: '', + }, }, }, // 'ftcvewdnkemfksdm': { @@ -616,7 +619,7 @@ export module Mock { // used: 0, // model: 'Evo SATA 2.5', // vendor: 'Samsung', - // startOs: null, + // startOs: {}, // }, csgashbdjkasnd: { type: 'cifs', @@ -624,7 +627,7 @@ export module Mock { path: '/Desktop/startos-backups-2', username: 'TestUser', mountable: true, - startOs: null, + startOs: {}, }, powjefhjbnwhdva: { type: 'disk', @@ -635,30 +638,33 @@ export module Mock { model: null, vendor: 'SSK', startOs: { - version: '0.3.0', - full: true, - // password is asdfasdf - passwordHash: - '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', - wrappedKey: '', + '1234-5678-9876-5432': { + hostname: 'adjective-noun', + timestamp: new Date().toISOString(), + version: '0.3.6', + passwordHash: + // password is asdfasdf + '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', + wrappedKey: '', + }, }, }, } export const BackupInfo: RR.GetBackupInfoRes = { - version: '0.3.0', + version: '0.3.6', timestamp: new Date().toISOString(), packageBackups: { bitcoind: { title: 'Bitcoin Core', version: '0.21.0', - osVersion: '0.3.0', + osVersion: '0.3.6', timestamp: new Date().toISOString(), }, 'btc-rpc-proxy': { title: 'Bitcoin Proxy', version: '0.2.2', - osVersion: '0.3.0', + osVersion: '0.3.6', timestamp: new Date().toISOString(), }, }, diff --git a/web/projects/ui/src/app/services/api/api.types.ts b/web/projects/ui/src/app/services/api/api.types.ts index 7ca97db53..5ada03cad 100644 --- a/web/projects/ui/src/app/services/api/api.types.ts +++ b/web/projects/ui/src/app/services/api/api.types.ts @@ -191,7 +191,12 @@ export module RR { export type RemoveBackupTargetReq = { id: string } // backup.target.cifs.remove export type RemoveBackupTargetRes = null - export type GetBackupInfoReq = { targetId: string; password: string } // backup.target.info + export type GetBackupInfoReq = { + // backup.target.info + targetId: string + serverId: string + password: string + } export type GetBackupInfoRes = BackupInfo export type CreateBackupReq = { @@ -239,7 +244,7 @@ export module RR { // package.backup.restore ids: string[] targetId: string - oldPassword: string | null + serverId: string password: string } export type RestorePackagesRes = null @@ -403,7 +408,7 @@ export interface DiskBackupTarget { label: string | null capacity: number used: number | null - startOs: StartOSDiskInfo | null + startOs: Record } export interface CifsBackupTarget { @@ -412,7 +417,7 @@ export interface CifsBackupTarget { path: string username: string mountable: boolean - startOs: StartOSDiskInfo | null + startOs: Record } export type RecoverySource = DiskRecoverySource | CifsRecoverySource diff --git a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts index 284e3bab8..21491dc52 100644 --- a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -582,7 +582,7 @@ export class MockApiService extends ApiService { path: path.replace(/\\/g, '/'), username, mountable: true, - startOs: null, + startOs: {}, }, } } diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 92fd979ef..8411b7b7c 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -59,7 +59,7 @@ export const mockPatchData: DataModel = { // password is asdfasdf passwordHash: '$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ', - eosVersionCompat: '>=0.3.0 <=0.3.0.1', + eosVersionCompat: '>=0.3.0 <=0.3.6', statusInfo: { backupProgress: null, updated: false, diff --git a/web/projects/ui/src/app/types/mapped-backup-target.ts b/web/projects/ui/src/app/types/mapped-backup-target.ts index 13b51d4b5..4b3610bec 100644 --- a/web/projects/ui/src/app/types/mapped-backup-target.ts +++ b/web/projects/ui/src/app/types/mapped-backup-target.ts @@ -1,5 +1,5 @@ export interface MappedBackupTarget { id: string - hasValidBackup: boolean + hasAnyBackup: boolean entry: T } From 6def083b4f8507a9cd64f4a99cc97a2c2ae658ce Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Thu, 11 Jul 2024 14:55:13 -0600 Subject: [PATCH 057/125] fix deadlock on install (#2667) * fix deadlock on install * improve pruning script * bump tokio --- build/dpkg-deps/depends | 1 + build/lib/scripts/chroot-and-upgrade | 2 +- build/lib/scripts/prune-images | 2 +- core/Cargo.lock | 344 ++++++++++++------ core/startos/Cargo.toml | 25 +- core/startos/src/disk/main.rs | 2 +- core/startos/src/disk/mount/backup.rs | 8 +- core/startos/src/disk/mount/guard.rs | 2 +- core/startos/src/init.rs | 5 + core/startos/src/middleware/auth.rs | 4 +- core/startos/src/net/static_server.rs | 7 +- core/startos/src/progress.rs | 2 +- core/startos/src/s9pk/v1/reader.rs | 2 +- core/startos/src/s9pk/v2/pack.rs | 6 +- core/startos/src/service/mod.rs | 2 + core/startos/src/service/service_map.rs | 3 + .../startos/src/service/transition/restart.rs | 2 - core/startos/src/util/io.rs | 2 +- core/startos/src/util/mod.rs | 2 +- core/startos/src/util/serde.rs | 4 +- 20 files changed, 273 insertions(+), 154 deletions(-) diff --git a/build/dpkg-deps/depends b/build/dpkg-deps/depends index 33b7be2ad..3ccaee4d6 100644 --- a/build/dpkg-deps/depends +++ b/build/dpkg-deps/depends @@ -1,5 +1,6 @@ avahi-daemon avahi-utils +b3sum bash-completion beep bmon diff --git a/build/lib/scripts/chroot-and-upgrade b/build/lib/scripts/chroot-and-upgrade index 59581fbc6..3af93f5ef 100755 --- a/build/lib/scripts/chroot-and-upgrade +++ b/build/lib/scripts/chroot-and-upgrade @@ -85,7 +85,7 @@ if [ "$CHROOT_RES" -eq 0 ]; then echo 'Upgrading...' time mksquashfs /media/startos/next /media/startos/images/next.squashfs -b 4096 -comp gzip - hash=$(start-cli util b3sum /media/startos/images/next.squashfs | head -c 32) + hash=$(b3sum /media/startos/images/next.squashfs | head -c 32) mv /media/startos/images/next.squashfs /media/startos/images/${hash}.rootfs ln -rsf /media/startos/images/${hash}.rootfs /media/startos/config/current.rootfs diff --git a/build/lib/scripts/prune-images b/build/lib/scripts/prune-images index 21861467f..20356a28c 100755 --- a/build/lib/scripts/prune-images +++ b/build/lib/scripts/prune-images @@ -33,7 +33,7 @@ if [ -h /media/startos/config/current.rootfs ] && [ -e /media/startos/config/cur echo 'Pruning...' current="$(readlink -f /media/startos/config/current.rootfs)" while [[ "$(df -B1 --output=avail --sync /media/startos/images | tail -n1)" -lt "$needed" ]]; do - to_prune="$(ls -t1 /media/startos/images/*.rootfs | grep -v "$current" | tail -n1)" + to_prune="$(ls -t1 /media/startos/images/*.rootfs /media/startos/images/*.squashfs | grep -v "$current" | tail -n1)" if [ -e "$to_prune" ]; then echo " Pruning $to_prune" rm -rf "$to_prune" diff --git a/core/Cargo.lock b/core/Cargo.lock index 80c2dcc0e..9f7face93 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -260,23 +260,39 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - [[package]] name = "autocfg" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "aws-lc-rs" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a47f2fb521b70c11ce7369a6c5fa4bd6af7e5d62ec06303875bafe7c6ba245" +dependencies = [ + "aws-lc-sys", + "mirai-annotations", + "paste", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2927c7af777b460b7ccd95f8b67acd7b4c04ec8896bf0c8e80ba30523cffc057" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "libc", + "paste", +] + [[package]] name = "axum" version = "0.6.20" @@ -335,7 +351,7 @@ dependencies = [ "sha1", "sync_wrapper 1.0.1", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.21.0", "tower", "tower-layer", "tower-service", @@ -453,6 +469,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" +[[package]] +name = "base32" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1ce0365f4d5fb6646220bb52fe547afd51796d90f914d4063cb0b032ebee088" + [[package]] name = "base64" version = "0.21.7" @@ -491,6 +513,29 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.69.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.68", + "which", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -651,12 +696,27 @@ dependencies = [ "once_cell", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.38" @@ -727,6 +787,17 @@ dependencies = [ "inout", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.7" @@ -767,6 +838,15 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + [[package]] name = "color-eyre" version = "0.6.3" @@ -833,9 +913,9 @@ dependencies = [ [[package]] name = "console-api" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd326812b3fd01da5bb1af7d340d0d555fd3d4b641e7f1dfcf5962a902952787" +checksum = "a257c22cd7e487dd4a13d413beabc512c5052f0bc048db0da6a84c3d8a6142fd" dependencies = [ "futures-core", "prost", @@ -846,9 +926,9 @@ dependencies = [ [[package]] name = "console-subscriber" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7481d4c57092cd1c19dd541b92bdce883de840df30aa5d03fd48a3935c01842e" +checksum = "31c4cc54bae66f7d9188996404abdf7fdfa23034ef8e43478c8810828abad758" dependencies = [ "console-api", "crossbeam-channel", @@ -856,6 +936,7 @@ dependencies = [ "futures-task", "hdrhistogram", "humantime", + "prost", "prost-types", "serde", "serde_json", @@ -915,17 +996,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "cookie" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - [[package]] name = "cookie" version = "0.18.1" @@ -937,30 +1007,13 @@ dependencies = [ "version_check", ] -[[package]] -name = "cookie_store" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "387461abbc748185c3a6e1673d826918b450b87ff22639429c694619a83b6cf6" -dependencies = [ - "cookie 0.17.0", - "idna 0.3.0", - "log", - "publicsuffix", - "serde", - "serde_derive", - "serde_json", - "time", - "url", -] - [[package]] name = "cookie_store" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4934e6b7e8419148b6ef56950d277af8561060b56afd59e2aadf98b59fce6baa" dependencies = [ - "cookie 0.18.1", + "cookie", "idna 0.5.0", "log", "publicsuffix", @@ -1362,6 +1415,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + [[package]] name = "dyn-clone" version = "1.0.17" @@ -1685,6 +1744,12 @@ dependencies = [ "itertools 0.8.2", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "funty" version = "1.1.0" @@ -1836,6 +1901,12 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "gpt" version = "3.1.0" @@ -1992,15 +2063,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - [[package]] name = "hermit-abi" version = "0.3.9" @@ -2178,7 +2240,7 @@ dependencies = [ "rustls 0.23.10", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.0", + "tokio-rustls", "tower-service", ] @@ -2447,7 +2509,7 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi", "libc", "windows-sys 0.52.0", ] @@ -2689,6 +2751,12 @@ dependencies = [ "spin", ] +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "lexical-core" version = "0.7.6" @@ -2708,6 +2776,16 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libloading" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" +dependencies = [ + "cfg-if", + "windows-targets 0.52.5", +] + [[package]] name = "libm" version = "0.2.8" @@ -2878,6 +2956,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mirai-annotations" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" + [[package]] name = "models" version = "0.1.0" @@ -2969,12 +3053,13 @@ dependencies = [ [[package]] name = "nix" -version = "0.27.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.6.0", "cfg-if", + "cfg_aliases", "libc", ] @@ -3123,7 +3208,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi", "libc", ] @@ -3540,6 +3625,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "prettyplease" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" +dependencies = [ + "proc-macro2", + "syn 2.0.68", +] + [[package]] name = "prettytable-rs" version = "0.10.0" @@ -3603,13 +3698,13 @@ dependencies = [ [[package]] name = "proptest-derive" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf16337405ca084e9c78985114633b6827711d22b9e6ef6c6c0d665eb3f0b6e" +checksum = "6ff7ff745a347b87471d859a377a9a404361e7efc2a971d73424a6d183c0fc77" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.68", ] [[package]] @@ -3892,8 +3987,8 @@ checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" dependencies = [ "base64 0.22.1", "bytes", - "cookie 0.18.1", - "cookie_store 0.21.0", + "cookie", + "cookie_store", "encoding_rs", "futures-core", "futures-util", @@ -3934,12 +4029,12 @@ dependencies = [ [[package]] name = "reqwest_cookie_store" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ea5c6f30c19d766efe8d823c88f9abd1c56516648a0d4264ab2dc04cc19472" +checksum = "a0b36498c7452f11b1833900f31fbb01fc46be20992a50269c88cf59d79f54e9" dependencies = [ "bytes", - "cookie_store 0.20.0", + "cookie_store", "reqwest", "url", ] @@ -4095,26 +4190,14 @@ dependencies = [ "sct", ] -[[package]] -name = "rustls" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" -dependencies = [ - "log", - "ring", - "rustls-pki-types", - "rustls-webpki 0.102.4", - "subtle", - "zeroize", -] - [[package]] name = "rustls" version = "0.23.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" dependencies = [ + "aws-lc-rs", + "log", "once_cell", "rustls-pki-types", "rustls-webpki 0.102.4", @@ -4163,6 +4246,7 @@ version = "0.102.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -4475,6 +4559,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook" version = "0.3.17" @@ -4874,8 +4964,8 @@ dependencies = [ "axum-server", "backhand", "barrage", - "base32", - "base64 0.21.7", + "base32 0.5.0", + "base64 0.22.1", "base64ct", "basic-cookies", "blake3", @@ -4886,8 +4976,8 @@ dependencies = [ "color-eyre", "console", "console-subscriber", - "cookie 0.18.1", - "cookie_store 0.20.0", + "cookie", + "cookie_store", "der", "digest 0.10.7", "divrem", @@ -4914,7 +5004,7 @@ dependencies = [ "ipnet", "iprange", "isocountry", - "itertools 0.12.1", + "itertools 0.13.0", "jaq-core", "jaq-std", "josekit", @@ -4927,7 +5017,7 @@ dependencies = [ "mbrman", "models", "new_mime_guess", - "nix 0.27.1", + "nix 0.29.0", "nom 7.1.3", "num", "num_enum", @@ -4963,15 +5053,14 @@ dependencies = [ "sqlx", "sscanf", "ssh-key", - "stderrlog", "tar", "thiserror", "tokio", - "tokio-rustls 0.25.0", + "tokio-rustls", "tokio-socks", "tokio-stream", "tokio-tar", - "tokio-tungstenite", + "tokio-tungstenite 0.23.1", "tokio-util", "toml 0.8.14", "torut", @@ -4996,19 +5085,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "stderrlog" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69a26bbf6de627d389164afa9783739b56746c6c72c4ed16539f4ff54170327b" -dependencies = [ - "atty", - "chrono", - "log", - "termcolor", - "thread_local", -] - [[package]] name = "string_cache" version = "0.8.7" @@ -5312,17 +5388,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-rustls" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" -dependencies = [ - "rustls 0.22.4", - "rustls-pki-types", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.0" @@ -5377,13 +5442,25 @@ name = "tokio-tungstenite" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.21.0", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" dependencies = [ "futures-util", "log", "native-tls", "tokio", "tokio-native-tls", - "tungstenite", + "tungstenite 0.23.0", ] [[package]] @@ -5471,9 +5548,9 @@ dependencies = [ [[package]] name = "tonic" -version = "0.10.2" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" +checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13" dependencies = [ "async-stream", "async-trait", @@ -5501,7 +5578,7 @@ name = "torut" version = "0.2.1" source = "git+https://github.com/Start9Labs/torut.git?branch=update/dependencies#cc7a1425a01214465e106975e6690794d8551bdb" dependencies = [ - "base32", + "base32 0.4.0", "base64 0.21.7", "derive_more", "ed25519-dalek 1.0.1", @@ -5728,6 +5805,25 @@ name = "tungstenite" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand 0.8.5", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "tungstenite" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" dependencies = [ "byteorder", "bytes", @@ -6045,6 +6141,18 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "whoami" version = "1.5.1" diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index d2304fedb..842ef44ef 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -61,8 +61,8 @@ axum = { version = "0.7.3", features = ["ws"] } axum-server = "0.6.0" barrage = "0.2.3" backhand = "0.18.0" -base32 = "0.4.0" -base64 = "0.21.4" +base32 = "0.5.0" +base64 = "0.22.1" base64ct = "1.6.0" basic-cookies = "0.1.4" blake3 = { version = "1.5.0", features = ["mmap", "rayon"] } @@ -71,9 +71,9 @@ chrono = { version = "0.4.31", features = ["serde"] } clap = "4.4.12" color-eyre = "0.6.2" console = "0.15.7" -console-subscriber = { version = "0.2", optional = true } +console-subscriber = { version = "0.3.0", optional = true } cookie = "0.18.0" -cookie_store = "0.20.0" +cookie_store = "0.21.0" der = { version = "0.7.9", features = ["derive", "pem"] } digest = "0.10.7" divrem = "1.0.0" @@ -102,7 +102,7 @@ id-pool = { version = "0.2.2", default-features = false, features = [ "serde", "u16", ] } -imbl = "2.0.2" +imbl = "2.0.3" imbl-value = { git = "https://github.com/Start9Labs/imbl-value.git" } include_dir = { version = "0.7.3", features = ["metadata"] } indexmap = { version = "2.0.2", features = ["serde"] } @@ -111,7 +111,7 @@ integer-encoding = { version = "4.0.0", features = ["tokio_async"] } ipnet = { version = "2.8.0", features = ["serde"] } iprange = { version = "0.6.7", features = ["serde"] } isocountry = "0.3.2" -itertools = "0.12.0" +itertools = "0.13.0" jaq-core = "0.10.1" jaq-std = "0.10.0" josekit = "0.8.4" @@ -124,7 +124,7 @@ log = "0.4.20" mbrman = "0.5.2" models = { version = "*", path = "../models" } new_mime_guess = "4" -nix = { version = "0.27.1", features = ["user", "process", "signal", "fs"] } +nix = { version = "0.29.0", features = ["user", "process", "signal", "fs"] } nom = "7.1.3" num = "0.4.1" num_enum = "0.7.0" @@ -140,11 +140,11 @@ pin-project = "1.1.3" pkcs8 = { version = "0.10.2", features = ["std"] } prettytable-rs = "0.10.0" proptest = "1.3.1" -proptest-derive = "0.4.0" +proptest-derive = "0.5.0" rand = { version = "0.8.5", features = ["std"] } regex = "1.10.2" reqwest = { version = "0.12.4", features = ["stream", "json", "socks"] } -reqwest_cookie_store = "0.7.0" +reqwest_cookie_store = "0.8.0" rpassword = "7.2.0" rpc-toolkit = { git = "https://github.com/Start9Labs/rpc-toolkit.git", branch = "refactor/no-dyn-ctx" } rust-argon2 = "2.0.0" @@ -168,15 +168,14 @@ sqlx = { version = "0.7.2", features = [ ] } sscanf = "0.4.1" ssh-key = { version = "0.6.2", features = ["ed25519"] } -stderrlog = "0.5.4" tar = "0.4.40" thiserror = "1.0.49" -tokio = { version = "1.37", features = ["full"] } -tokio-rustls = "0.25.0" +tokio = { version = "1.38.0", features = ["full"] } +tokio-rustls = "0.26.0" tokio-socks = "0.5.1" tokio-stream = { version = "0.1.14", features = ["io-util", "sync", "net"] } tokio-tar = { git = "https://github.com/dr-bonez/tokio-tar.git" } -tokio-tungstenite = { version = "0.21.0", features = ["native-tls"] } +tokio-tungstenite = { version = "0.23.1", features = ["native-tls", "url"] } tokio-util = { version = "0.7.9", features = ["io"] } torut = { git = "https://github.com/Start9Labs/torut.git", branch = "update/dependencies", features = [ "serialize", diff --git a/core/startos/src/disk/main.rs b/core/startos/src/disk/main.rs index ee807a938..3a13c5dca 100644 --- a/core/startos/src/disk/main.rs +++ b/core/startos/src/disk/main.rs @@ -66,7 +66,7 @@ where let mut guid = format!( "STARTOS_{}", base32::encode( - base32::Alphabet::RFC4648 { padding: false }, + base32::Alphabet::Rfc4648 { padding: false }, &rand::random::<[u8; 20]>(), ) ); diff --git a/core/startos/src/disk/mount/backup.rs b/core/startos/src/disk/mount/backup.rs index 67f88d21f..142301a74 100644 --- a/core/startos/src/disk/mount/backup.rs +++ b/core/startos/src/disk/mount/backup.rs @@ -65,7 +65,7 @@ impl BackupMountGuard { unencrypted_metadata.wrapped_key.as_ref(), ) { let wrapped_key = - base32::decode(base32::Alphabet::RFC4648 { padding: true }, wrapped_key) + base32::decode(base32::Alphabet::Rfc4648 { padding: true }, wrapped_key) .ok_or_else(|| { Error::new( eyre!("failed to decode wrapped key"), @@ -76,7 +76,7 @@ impl BackupMountGuard { String::from_utf8(decrypt_slice(wrapped_key, password))? } else { base32::encode( - base32::Alphabet::RFC4648 { padding: false }, + base32::Alphabet::Rfc4648 { padding: false }, &rand::random::<[u8; 32]>()[..], ) }; @@ -93,7 +93,7 @@ impl BackupMountGuard { } if unencrypted_metadata.wrapped_key.is_none() { unencrypted_metadata.wrapped_key = Some(base32::encode( - base32::Alphabet::RFC4648 { padding: true }, + base32::Alphabet::Rfc4648 { padding: true }, &encrypt_slice(&enc_key, password), )); } @@ -141,7 +141,7 @@ impl BackupMountGuard { .with_kind(crate::ErrorKind::PasswordHashGeneration)?, ); self.unencrypted_metadata.wrapped_key = Some(base32::encode( - base32::Alphabet::RFC4648 { padding: false }, + base32::Alphabet::Rfc4648 { padding: false }, &encrypt_slice(&self.enc_key, new_password), )); Ok(()) diff --git a/core/startos/src/disk/mount/guard.rs b/core/startos/src/disk/mount/guard.rs index 4686a10b8..d6e7e3da1 100644 --- a/core/startos/src/disk/mount/guard.rs +++ b/core/startos/src/disk/mount/guard.rs @@ -111,7 +111,7 @@ impl GenericMountGuard for MountGuard { async fn tmp_mountpoint(source: &impl FileSystem) -> Result { Ok(Path::new(TMP_MOUNTPOINT).join(base32::encode( - base32::Alphabet::RFC4648 { padding: false }, + base32::Alphabet::Rfc4648 { padding: false }, &source.source_hash().await?[0..20], ))) } diff --git a/core/startos/src/init.rs b/core/startos/src/init.rs index fdc38b60d..48cb24c9a 100644 --- a/core/startos/src/init.rs +++ b/core/startos/src/init.rs @@ -30,6 +30,7 @@ use crate::progress::{ FullProgress, FullProgressTracker, PhaseProgressTrackerHandle, PhasedProgressBar, }; use crate::rpc_continuations::{Guid, RpcContinuation}; +use crate::s9pk::v2::pack::{CONTAINER_DATADIR, CONTAINER_TOOL}; use crate::ssh::SSH_AUTHORIZED_KEYS_FILE; use crate::util::io::{create_file, IOHook}; use crate::util::net::WebSocketExt; @@ -421,6 +422,10 @@ pub async fn init( tokio::fs::remove_dir_all(&tmp_var).await?; } crate::disk::mount::util::bind(&tmp_var, "/var/tmp", false).await?; + let tmp_docker = cfg + .datadir() + .join(format!("package-data/tmp/{CONTAINER_TOOL}")); + crate::disk::mount::util::bind(&tmp_docker, CONTAINER_DATADIR, false).await?; init_tmp.complete(); set_governor.start(); diff --git a/core/startos/src/middleware/auth.rs b/core/startos/src/middleware/auth.rs index ecf88dba7..fd60894db 100644 --- a/core/startos/src/middleware/auth.rs +++ b/core/startos/src/middleware/auth.rs @@ -151,7 +151,7 @@ impl HashSessionToken { pub fn new() -> Self { Self::from_token(InternedString::intern( base32::encode( - base32::Alphabet::RFC4648 { padding: false }, + base32::Alphabet::Rfc4648 { padding: false }, &rand::random::<[u8; 16]>(), ) .to_lowercase(), @@ -200,7 +200,7 @@ impl HashSessionToken { hasher.update(token.as_bytes()); InternedString::intern( base32::encode( - base32::Alphabet::RFC4648 { padding: false }, + base32::Alphabet::Rfc4648 { padding: false }, hasher.finalize().as_slice(), ) .to_lowercase(), diff --git a/core/startos/src/net/static_server.rs b/core/startos/src/net/static_server.rs index 79adad504..3e5f0997d 100644 --- a/core/startos/src/net/static_server.rs +++ b/core/startos/src/net/static_server.rs @@ -253,7 +253,7 @@ fn cert_send(cert: &X509, hostname: &Hostname) -> Result { .header( http::header::ETAG, base32::encode( - base32::Alphabet::RFC4648 { padding: false }, + base32::Alphabet::Rfc4648 { padding: false }, &*cert.digest(MessageDigest::sha256())?, ) .to_lowercase(), @@ -338,8 +338,7 @@ impl FileData { .any(|e| e == "gzip") .then_some("gzip"); - let file = open_file(path) - .await?; + let file = open_file(path).await?; let metadata = file .metadata() .await @@ -439,6 +438,6 @@ fn e_tag(path: &Path, modified: impl AsRef<[u8]>) -> String { let res = hasher.finalize(); format!( "\"{}\"", - base32::encode(base32::Alphabet::RFC4648 { padding: false }, res.as_slice()).to_lowercase() + base32::encode(base32::Alphabet::Rfc4648 { padding: false }, res.as_slice()).to_lowercase() ) } diff --git a/core/startos/src/progress.rs b/core/startos/src/progress.rs index f70a5adbc..5a2e5ef27 100644 --- a/core/startos/src/progress.rs +++ b/core/startos/src/progress.rs @@ -279,7 +279,7 @@ impl FullProgressTracker { .mutate(|v| { if let Some(p) = deref(v) { p.ser(&progress)?; - Ok(false) + Ok(progress.overall.is_complete()) } else { Ok(true) } diff --git a/core/startos/src/s9pk/v1/reader.rs b/core/startos/src/s9pk/v1/reader.rs index f2bbf578e..05f351343 100644 --- a/core/startos/src/s9pk/v1/reader.rs +++ b/core/startos/src/s9pk/v1/reader.rs @@ -206,7 +206,7 @@ impl S9pkReader { ( Some(hash), Some(base32::encode( - base32::Alphabet::RFC4648 { padding: false }, + base32::Alphabet::Rfc4648 { padding: false }, hash.as_slice(), )), ) diff --git a/core/startos/src/s9pk/v2/pack.rs b/core/startos/src/s9pk/v2/pack.rs index fb271e785..ae6807c23 100644 --- a/core/startos/src/s9pk/v2/pack.rs +++ b/core/startos/src/s9pk/v2/pack.rs @@ -32,10 +32,14 @@ use crate::util::Invoke; #[cfg(not(feature = "docker"))] pub const CONTAINER_TOOL: &str = "podman"; - #[cfg(feature = "docker")] pub const CONTAINER_TOOL: &str = "docker"; +#[cfg(feature = "docker")] +pub const CONTAINER_DATADIR: &str = "/var/lib/docker"; +#[cfg(not(feature = "docker"))] +pub const CONTAINER_DATADIR: &str = "/var/lib/containers"; + pub struct SqfsDir { path: PathBuf, tmpdir: Arc, diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index b85bd8d6d..581d2ac0d 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -374,9 +374,11 @@ impl Service { entry.as_icon_mut().ser(&icon)?; // TODO: marketplace url // TODO: dependency info + Ok(()) }) .await?; + Ok(service) } diff --git a/core/startos/src/service/service_map.rs b/core/startos/src/service/service_map.rs index 02244169c..af10a065c 100644 --- a/core/startos/src/service/service_map.rs +++ b/core/startos/src/service/service_map.rs @@ -294,9 +294,12 @@ impl ServiceMap { .into(), ); } + drop(service); + sync_progress_task.await.map_err(|_| { Error::new(eyre!("progress sync task panicked"), ErrorKind::Unknown) })??; + Ok(()) }) .boxed()) diff --git a/core/startos/src/service/transition/restart.rs b/core/startos/src/service/transition/restart.rs index a39291621..108e232ad 100644 --- a/core/startos/src/service/transition/restart.rs +++ b/core/startos/src/service/transition/restart.rs @@ -20,7 +20,6 @@ impl Handler for ServiceActor { .except::() } async fn handle(&mut self, _: Guid, _: Restart, jobs: &BackgroundJobQueue) -> Self::Response { - dbg!("here"); // So Need a handle to just a single field in the state let temp = TempDesiredRestore::new(&self.0.persistent_container.state); let mut current = self.0.persistent_container.state.subscribe(); @@ -77,7 +76,6 @@ impl Handler for ServiceActor { impl Service { #[instrument(skip_all)] pub async fn restart(&self, id: Guid) -> Result<(), Error> { - dbg!("here"); self.actor.send(id, Restart).await } } diff --git a/core/startos/src/util/io.rs b/core/startos/src/util/io.rs index 1ec789f23..bba45fa69 100644 --- a/core/startos/src/util/io.rs +++ b/core/startos/src/util/io.rs @@ -781,7 +781,7 @@ pub struct TmpDir { impl TmpDir { pub async fn new() -> Result { let path = Path::new("/var/tmp/startos").join(base32::encode( - base32::Alphabet::RFC4648 { padding: false }, + base32::Alphabet::Rfc4648 { padding: false }, &rand::random::<[u8; 8]>(), )); if tokio::fs::metadata(&path).await.is_ok() { diff --git a/core/startos/src/util/mod.rs b/core/startos/src/util/mod.rs index 7200eaaba..0c9e5c5f9 100644 --- a/core/startos/src/util/mod.rs +++ b/core/startos/src/util/mod.rs @@ -644,7 +644,7 @@ pub fn new_guid() -> InternedString { let mut buf = [0; 20]; rand::thread_rng().fill_bytes(&mut buf); InternedString::intern(base32::encode( - base32::Alphabet::RFC4648 { padding: false }, + base32::Alphabet::Rfc4648 { padding: false }, &buf, )) } diff --git a/core/startos/src/util/serde.rs b/core/startos/src/util/serde.rs index 049fad2d2..44a69165e 100644 --- a/core/startos/src/util/serde.rs +++ b/core/startos/src/util/serde.rs @@ -958,7 +958,7 @@ impl> std::fmt::Display for Base16 { pub struct Base32(pub T); impl> std::fmt::Display for Base32 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - base32::encode(base32::Alphabet::RFC4648 { padding: true }, self.0.as_ref()).fmt(f) + base32::encode(base32::Alphabet::Rfc4648 { padding: true }, self.0.as_ref()).fmt(f) } } impl<'de, T: TryFrom>> Deserialize<'de> for Base32 { @@ -967,7 +967,7 @@ impl<'de, T: TryFrom>> Deserialize<'de> for Base32 { D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; - base32::decode(base32::Alphabet::RFC4648 { padding: true }, &s) + base32::decode(base32::Alphabet::Rfc4648 { padding: true }, &s) .ok_or_else(|| { serde::de::Error::invalid_value( serde::de::Unexpected::Str(&s), From d235ebaac989d46a2692433c575af28a86bdd2eb Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 11 Jul 2024 17:58:07 -0600 Subject: [PATCH 058/125] solve infinite recursion and promise returning true --- .../src/app/components/backup-drives/backup.service.ts | 2 +- .../backup-server-select/backup-server-select.page.ts | 1 + .../server-routes/server-backup/server-backup.page.ts | 9 +++++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/web/projects/ui/src/app/components/backup-drives/backup.service.ts b/web/projects/ui/src/app/components/backup-drives/backup.service.ts index cc1898e7e..7e05178bf 100644 --- a/web/projects/ui/src/app/components/backup-drives/backup.service.ts +++ b/web/projects/ui/src/app/components/backup-drives/backup.service.ts @@ -61,7 +61,7 @@ export class BackupService { ) } - async hasThisBackup(target: BackupTarget, id: string): Promise { + hasThisBackup(target: BackupTarget, id: string): boolean { return ( target.startOs[id] && this.emver.compare(target.startOs[id].version, '0.3.6') !== -1 diff --git a/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.ts b/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.ts index e1f4f8961..0f1c47276 100644 --- a/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.ts +++ b/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.ts @@ -49,6 +49,7 @@ export class BackupServerSelectModal { if (role === 'confirm') { try { + // @TODO Alex if invalid password, we should tell the user "Invalid password" and halt execution of this function. The modal should remain so the user can try again. Correct password is asdfasdf argon2.verify(server.passwordHash!, data) await this.restoreFromBackup(serverId, data) } catch (e: any) { diff --git a/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts b/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts index b570b1603..25532c256 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts @@ -60,7 +60,7 @@ export class ServerBackupPage { component: BackupSelectPage, }) - modal.onWillDismiss().then(res => { + modal.onDidDismiss().then(res => { if (res.data) { this.serviceIds = res.data this.presentModalPassword(target) @@ -90,6 +90,7 @@ export class ServerBackupPage { .subscribe(async (password: string) => { const { passwordHash, id } = await getServerInfo(this.patch) + // @TODO Alex if invalid password, we should tell the user "Invalid password" and halt execution of this function. The modal should remain so the user can try again. Correct password is asdfasdf // confirm password matches current master password argon2.verify(passwordHash, password) @@ -103,7 +104,7 @@ export class ServerBackupPage { } catch { setTimeout( () => this.presentModalOldPassword(target, password), - 500, + 250, ) return } @@ -134,8 +135,8 @@ export class ServerBackupPage { }) .pipe(take(1)) .subscribe(async (oldPassword: string) => { - const passwordHash = target.entry.startOs[id].passwordHash! - argon2.verify(passwordHash, oldPassword) + // @TODO Alex if invalid password, we should tell the user "Invalid password" and halt execution of this function. The modal should remain so the user can try again. Correct password is asdfasdf + argon2.verify(target.entry.startOs[id].passwordHash!, oldPassword) await this.createBackup(target, password, oldPassword) }) } From 0f5cec0a604f4d6714e0640a5684abcaa58e9d9c Mon Sep 17 00:00:00 2001 From: waterplea Date: Fri, 12 Jul 2024 11:14:45 +0500 Subject: [PATCH 059/125] fix: fix wrong password messaging --- web/package-lock.json | 94 +++++++++---------- web/package.json | 12 +-- .../shared/src/services/error.service.ts | 1 - .../badge-menu-button/badge-menu.component.ts | 4 +- .../backup-server-select.page.ts | 44 ++++++--- ....modal.ts => password-prompt.component.ts} | 45 +++++++-- .../server-backup/server-backup.page.scss | 0 .../server-backup/server-backup.page.ts | 81 +++++++++------- .../ui/src/app/pages/widgets/widgets.page.ts | 4 +- 9 files changed, 170 insertions(+), 115 deletions(-) rename web/projects/ui/src/app/modals/{backup-server-select/password-prompt.modal.ts => password-prompt.component.ts} (59%) delete mode 100644 web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.scss diff --git a/web/package-lock.json b/web/package-lock.json index e07531a96..cf32bbc01 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -28,12 +28,12 @@ "@start9labs/argon2": "^0.2.2", "@start9labs/emver": "^0.1.5", "@start9labs/start-sdk": "file:../sdk/dist", - "@taiga-ui/addon-charts": "3.84.0", - "@taiga-ui/cdk": "3.84.0", - "@taiga-ui/core": "3.84.0", - "@taiga-ui/experimental": "3.84.0", - "@taiga-ui/icons": "3.84.0", - "@taiga-ui/kit": "3.84.0", + "@taiga-ui/addon-charts": "3.86.0", + "@taiga-ui/cdk": "3.86.0", + "@taiga-ui/core": "3.86.0", + "@taiga-ui/experimental": "3.86.0", + "@taiga-ui/icons": "3.86.0", + "@taiga-ui/kit": "3.86.0", "@tinkoff/ng-dompurify": "4.0.0", "@tinkoff/ng-event-plugins": "3.2.0", "angular-svg-round-progressbar": "^9.0.0", @@ -4128,9 +4128,9 @@ } }, "node_modules/@taiga-ui/addon-charts": { - "version": "3.84.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-3.84.0.tgz", - "integrity": "sha512-XR7UFywnrv4NRLHOCbba63gXDYYDL4Rt0MbjnF54p5U2EXnbt2of7VbjlB6cPx40XkQqfqa3CNayYxWZP82Ijg==", + "version": "3.86.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-3.86.0.tgz", + "integrity": "sha512-Du/85qqaj8hpFSI6hPuFeIhtE93Z6WSkYZLt0gvnsaCb2qSAg8D4oHSogrtF1rsWGGoM+fvXjD7UEUw9GzFIPg==", "dependencies": { "tslib": "^2.6.2" }, @@ -4138,15 +4138,15 @@ "@angular/common": ">=12.0.0", "@angular/core": ">=12.0.0", "@ng-web-apis/common": "^3.0.6", - "@taiga-ui/cdk": "^3.84.0", - "@taiga-ui/core": "^3.84.0", + "@taiga-ui/cdk": "^3.86.0", + "@taiga-ui/core": "^3.86.0", "@tinkoff/ng-polymorpheus": "^4.3.0" } }, "node_modules/@taiga-ui/addon-commerce": { - "version": "3.84.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-3.84.0.tgz", - "integrity": "sha512-1zqLwnZLAYYcHvjH89d7JmtV2+QeZ2YnSJ3YWEMNLjGPzpev4RvQXtDfglIyu0LCyTxqpXmuzes9v/cgq2P5TQ==", + "version": "3.86.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-3.86.0.tgz", + "integrity": "sha512-8QSB490ckI4jnU+1sQ3x8os2GVE162hbvzPVYIZ0TruoeXl076dAz6PT2WRaFwjcaCAIGsuaQgQ4Cv02NjkiYQ==", "peer": true, "dependencies": { "tslib": "^2.6.2" @@ -4159,18 +4159,18 @@ "@maskito/core": "^1.9.0", "@maskito/kit": "^1.9.0", "@ng-web-apis/common": "^3.0.6", - "@taiga-ui/cdk": "^3.84.0", - "@taiga-ui/core": "^3.84.0", - "@taiga-ui/i18n": "^3.84.0", - "@taiga-ui/kit": "^3.84.0", + "@taiga-ui/cdk": "^3.86.0", + "@taiga-ui/core": "^3.86.0", + "@taiga-ui/i18n": "^3.86.0", + "@taiga-ui/kit": "^3.86.0", "@tinkoff/ng-polymorpheus": "^4.3.0", "rxjs": ">=6.0.0" } }, "node_modules/@taiga-ui/cdk": { - "version": "3.84.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-3.84.0.tgz", - "integrity": "sha512-0umw/CUmYNEYOCUNQVTQS53zXzxZsH/6+lj1mFVzocvfJFJWAUT6ltCH9QvxYmxSDDGWwNGg16AaVo2K+aGL0w==", + "version": "3.86.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-3.86.0.tgz", + "integrity": "sha512-aVbnW01Oh0Er1sHKVGHP8W05mOSKxjSzFE3Qx4iF4T6KW7Rlz9HZoNx5ADMg0TATYChtWh9Kwjo8I4LSVj2ZUw==", "dependencies": { "@ng-web-apis/common": "3.0.6", "@ng-web-apis/mutation-observer": "3.1.0", @@ -4221,11 +4221,11 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/@taiga-ui/core": { - "version": "3.84.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-3.84.0.tgz", - "integrity": "sha512-FZy77z0E4qjYcszVcp+qPFkPwJPl8qXZb7t2P+juUtJvSmSn2foQHHdyhbIYN808H26tqCdgkTMG1BWQxVuDSg==", + "version": "3.86.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-3.86.0.tgz", + "integrity": "sha512-diQKOnPtDDfxPOMk6wLRq8nyDVfNSPSNy+1TeyqzUgOvJ6XAjfaBXGsL3iuR7AN8+sz/b3rJmBce+vdw6FjMLQ==", "dependencies": { - "@taiga-ui/i18n": "^3.84.0", + "@taiga-ui/i18n": "^3.86.0", "tslib": "^2.6.2" }, "peerDependencies": { @@ -4237,35 +4237,35 @@ "@angular/router": ">=12.0.0", "@ng-web-apis/common": "^3.0.6", "@ng-web-apis/mutation-observer": "^3.1.0", - "@taiga-ui/cdk": "^3.84.0", - "@taiga-ui/i18n": "^3.84.0", + "@taiga-ui/cdk": "^3.86.0", + "@taiga-ui/i18n": "^3.86.0", "@tinkoff/ng-event-plugins": "^3.2.0", "@tinkoff/ng-polymorpheus": "^4.3.0", "rxjs": ">=6.0.0" } }, "node_modules/@taiga-ui/experimental": { - "version": "3.84.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/experimental/-/experimental-3.84.0.tgz", - "integrity": "sha512-q0hNVy+EmywCG8hpZlg/+haKIFhnmxicQiSeV/D1P7CHO10safjGo0ptT6e1hYMFa5/cJZOM4OwDPen2xs17Wg==", + "version": "3.86.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/experimental/-/experimental-3.86.0.tgz", + "integrity": "sha512-ACjoRVeX5MgsNJsiu2ukliXLD2mfEWm8Vtmk78vqcnkyPUmy1ZWK4sG3p5ybFN8AdIMHkblVq0l+x2qAwr/+LQ==", "dependencies": { "tslib": "^2.6.2" }, "peerDependencies": { "@angular/common": ">=12.0.0", "@angular/core": ">=12.0.0", - "@taiga-ui/addon-commerce": "^3.84.0", - "@taiga-ui/cdk": "^3.84.0", - "@taiga-ui/core": "^3.84.0", - "@taiga-ui/kit": "^3.84.0", + "@taiga-ui/addon-commerce": "^3.86.0", + "@taiga-ui/cdk": "^3.86.0", + "@taiga-ui/core": "^3.86.0", + "@taiga-ui/kit": "^3.86.0", "@tinkoff/ng-polymorpheus": "^4.3.0", "rxjs": ">=6.0.0" } }, "node_modules/@taiga-ui/i18n": { - "version": "3.85.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-3.85.0.tgz", - "integrity": "sha512-CGoxfq9WY+psX5ZOfWmuQZ6OA/0CAPYJTlbHkw5sRKAyhEQ3NM/Wbx3xcwrcYRRJDnt9yOlfibz+3a+WDF2bFA==", + "version": "3.86.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-3.86.0.tgz", + "integrity": "sha512-8zkNhMo/QtxZ2Zp6EP/nxo4SOLwaIrX+P3X/Wt+1cjFNZUYWWfdvfHLLdNviKFPVl4RAOxvkhDfza/wkrwv+iQ==", "dependencies": { "tslib": "^2.6.2" }, @@ -4276,20 +4276,20 @@ } }, "node_modules/@taiga-ui/icons": { - "version": "3.84.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-3.84.0.tgz", - "integrity": "sha512-KiH7BJRZ6wbkOHlJAS0XHq2gYnQTpRgdEogKW+GoD0da/4trCdM66vhDk2j0DwDFdBGq5U0inHJCjnskBI1nSQ==", + "version": "3.86.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-3.86.0.tgz", + "integrity": "sha512-jVBEbvE/r9JG+knmXMTn/l/js3JjYi8nSGbrLCryJZZoS2izRnQARN2txABieUJm8H463CoF0rcdXlHKRuA4Ew==", "dependencies": { "tslib": "^2.6.2" }, "peerDependencies": { - "@taiga-ui/cdk": "^3.84.0" + "@taiga-ui/cdk": "^3.86.0" } }, "node_modules/@taiga-ui/kit": { - "version": "3.84.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-3.84.0.tgz", - "integrity": "sha512-lSUPDco5FeBYK3ESnXeEPLCdMCmNXwcdHNK/we+0ZoH4VPx/OGg2hpEP0Fej7jfGHwXFTzDbufQD0hT6WlfTAw==", + "version": "3.86.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-3.86.0.tgz", + "integrity": "sha512-naAy4pyhCaQ9+vWxqSMjbV+9KwnMxT5ybrw+MAJgMn2evzRq0FjqzyFZFog7oiRbRvgVdoWPQfBNKaaLhJcpsw==", "dependencies": { "@maskito/angular": "1.9.0", "@maskito/core": "1.9.0", @@ -4306,9 +4306,9 @@ "@ng-web-apis/common": "3.0.6", "@ng-web-apis/mutation-observer": "^3.1.0", "@ng-web-apis/resize-observer": "^3.0.6", - "@taiga-ui/cdk": "^3.84.0", - "@taiga-ui/core": "^3.84.0", - "@taiga-ui/i18n": "^3.84.0", + "@taiga-ui/cdk": "^3.86.0", + "@taiga-ui/core": "^3.86.0", + "@taiga-ui/i18n": "^3.86.0", "@tinkoff/ng-polymorpheus": "^4.3.0", "rxjs": ">=6.0.0" } diff --git a/web/package.json b/web/package.json index 69c3585ce..a75701333 100644 --- a/web/package.json +++ b/web/package.json @@ -51,12 +51,12 @@ "@start9labs/argon2": "^0.2.2", "@start9labs/emver": "^0.1.5", "@start9labs/start-sdk": "file:../sdk/dist", - "@taiga-ui/addon-charts": "3.84.0", - "@taiga-ui/cdk": "3.84.0", - "@taiga-ui/core": "3.84.0", - "@taiga-ui/experimental": "3.84.0", - "@taiga-ui/icons": "3.84.0", - "@taiga-ui/kit": "3.84.0", + "@taiga-ui/addon-charts": "3.86.0", + "@taiga-ui/cdk": "3.86.0", + "@taiga-ui/core": "3.86.0", + "@taiga-ui/experimental": "3.86.0", + "@taiga-ui/icons": "3.86.0", + "@taiga-ui/kit": "3.86.0", "@tinkoff/ng-dompurify": "4.0.0", "@tinkoff/ng-event-plugins": "3.2.0", "angular-svg-round-progressbar": "^9.0.0", diff --git a/web/projects/shared/src/services/error.service.ts b/web/projects/shared/src/services/error.service.ts index 45891e0f4..383b3fc97 100644 --- a/web/projects/shared/src/services/error.service.ts +++ b/web/projects/shared/src/services/error.service.ts @@ -15,7 +15,6 @@ export class ErrorService extends ErrorHandler { this.alerts .open(getErrorMessage(error, link), { label: 'Error', - autoClose: false, status: TuiNotification.Error, }) .subscribe() diff --git a/web/projects/ui/src/app/components/badge-menu-button/badge-menu.component.ts b/web/projects/ui/src/app/components/badge-menu-button/badge-menu.component.ts index 5fe9b621e..0d7f3ec66 100644 --- a/web/projects/ui/src/app/components/badge-menu-button/badge-menu.component.ts +++ b/web/projects/ui/src/app/components/badge-menu-button/badge-menu.component.ts @@ -32,7 +32,7 @@ export class BadgeMenuComponent { constructor( private readonly splitPane: SplitPaneTracker, private readonly patch: PatchDB, - private readonly dialog: TuiDialogService, + private readonly dialogs: TuiDialogService, private readonly clientStorageService: ClientStorageService, ) {} @@ -44,6 +44,6 @@ export class BadgeMenuComponent { } onWidgets() { - this.dialog.open(WIDGETS_COMPONENT, { label: 'Widgets' }).subscribe() + this.dialogs.open(WIDGETS_COMPONENT, { label: 'Widgets' }).subscribe() } } diff --git a/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.ts b/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.ts index 0f1c47276..a10067044 100644 --- a/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.ts +++ b/web/projects/ui/src/app/modals/backup-server-select/backup-server-select.page.ts @@ -6,6 +6,10 @@ import { LoadingService, StartOSDiskInfo, } from '@start9labs/shared' +import { + PasswordPromptComponent, + PromptOptions, +} from 'src/app/modals/password-prompt.component' import { BackupInfo, CifsBackupTarget, @@ -14,7 +18,6 @@ import { import { ApiService } from 'src/app/services/api/embassy-api.service' import { MappedBackupTarget } from 'src/app/types/mapped-backup-target' import { AppRecoverSelectPage } from '../app-recover-select/app-recover-select.page' -import { PasswordPromptModal } from './password-prompt.modal' @Component({ selector: 'backup-server-select', @@ -38,24 +41,35 @@ export class BackupServerSelectModal { async presentModalPassword( serverId: string, - server: StartOSDiskInfo, + { passwordHash }: StartOSDiskInfo, ): Promise { + const options: PromptOptions = { + title: 'Password Required', + message: + 'Enter the password that was used to encrypt this backup. On the next screen, you will select the individual services you want to restore.', + label: 'Decrypt Backup', + placeholder: 'Enter password', + buttonText: 'Next', + } const modal = await this.modalCtrl.create({ - component: PasswordPromptModal, + component: PasswordPromptComponent, + componentProps: { options }, + canDismiss: async password => { + if (password === null) { + return true + } + + try { + argon2.verify(passwordHash!, password) + await this.restoreFromBackup(serverId, password) + return true + } catch (e: any) { + this.errorService.handleError(e) + return false + } + }, }) modal.present() - - const { data, role } = await modal.onWillDismiss() - - if (role === 'confirm') { - try { - // @TODO Alex if invalid password, we should tell the user "Invalid password" and halt execution of this function. The modal should remain so the user can try again. Correct password is asdfasdf - argon2.verify(server.passwordHash!, data) - await this.restoreFromBackup(serverId, data) - } catch (e: any) { - this.errorService.handleError(e) - } - } } private async restoreFromBackup( diff --git a/web/projects/ui/src/app/modals/backup-server-select/password-prompt.modal.ts b/web/projects/ui/src/app/modals/password-prompt.component.ts similarity index 59% rename from web/projects/ui/src/app/modals/backup-server-select/password-prompt.modal.ts rename to web/projects/ui/src/app/modals/password-prompt.component.ts index 2af89e3d0..a9fec75cd 100644 --- a/web/projects/ui/src/app/modals/backup-server-select/password-prompt.modal.ts +++ b/web/projects/ui/src/app/modals/password-prompt.component.ts @@ -1,14 +1,29 @@ -import { Component } from '@angular/core' +import { + AfterViewInit, + Component, + ElementRef, + Input, + ViewChild, +} from '@angular/core' import { FormsModule } from '@angular/forms' import { IonicModule, ModalController } from '@ionic/angular' +import { TuiTextfieldComponent } from '@taiga-ui/core' import { TuiInputPasswordModule } from '@taiga-ui/kit' +export interface PromptOptions { + title: string + message: string + label: string + placeholder: string + buttonText: string +} + @Component({ standalone: true, template: ` - Decrypt Backup + {{ options.title }} @@ -18,13 +33,11 @@ import { TuiInputPasswordModule } from '@taiga-ui/kit' +

{{ options.message }}

- Enter the password that was used to encrypt this backup. On the next - screen, you will select the individual services you want to restore. -

-

- - Enter password + + {{ options.label }} +

@@ -47,18 +60,30 @@ import { TuiInputPasswordModule } from '@taiga-ui/kit' [disabled]="!password" (click)="confirm()" > - Next + {{ options.buttonText }} `, imports: [IonicModule, FormsModule, TuiInputPasswordModule], }) -export class PasswordPromptModal { +export class PasswordPromptComponent implements AfterViewInit { + @ViewChild(TuiTextfieldComponent, { read: ElementRef }) + input?: ElementRef + + @Input() + options!: PromptOptions + password = '' constructor(private modalCtrl: ModalController) {} + ngAfterViewInit() { + setTimeout(() => { + this.input?.nativeElement.focus({ preventScroll: true }) + }, 300) + } + cancel() { return this.modalCtrl.dismiss(null, 'cancel') } diff --git a/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.scss b/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts b/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts index 25532c256..9a067bc1e 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/server-backup/server-backup.page.ts @@ -1,11 +1,13 @@ import { Component } from '@angular/core' import { ModalController, NavController } from '@ionic/angular' -import { LoadingService } from '@start9labs/shared' -import { TuiDialogService } from '@taiga-ui/core' -import { PROMPT, PromptOptions } from 'src/app/modals/prompt.component' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { + PasswordPromptComponent, + PromptOptions, +} from 'src/app/modals/password-prompt.component' import { ApiService } from 'src/app/services/api/embassy-api.service' import { PatchDB } from 'patch-db-client' -import { skip, take, takeUntil } from 'rxjs/operators' +import { skip, takeUntil } from 'rxjs/operators' import { MappedBackupTarget } from 'src/app/types/mapped-backup-target' import * as argon2 from '@start9labs/argon2' import { TuiDestroyService } from '@taiga-ui/cdk' @@ -22,7 +24,6 @@ import { BackupService } from 'src/app/components/backup-drives/backup.service' @Component({ selector: 'server-backup', templateUrl: './server-backup.page.html', - styleUrls: ['./server-backup.page.scss'], providers: [TuiDestroyService], }) export class ServerBackupPage { @@ -31,8 +32,8 @@ export class ServerBackupPage { readonly backingUp$ = this.eosService.backingUp$ constructor( + private readonly errorService: ErrorService, private readonly loader: LoadingService, - private readonly dialogs: TuiDialogService, private readonly modalCtrl: ModalController, private readonly embassyApi: ApiService, private readonly navCtrl: NavController, @@ -74,29 +75,35 @@ export class ServerBackupPage { target: MappedBackupTarget, ): Promise { const options: PromptOptions = { + title: 'Master Password Needed', message: 'Enter your master password to encrypt this backup.', label: 'Master Password', placeholder: 'Enter master password', - useMask: true, buttonText: 'Create Backup', } - this.dialogs - .open(PROMPT, { - label: 'Master Password Needed', - data: options, - }) - .pipe(take(1)) - .subscribe(async (password: string) => { + const modal = await this.modalCtrl.create({ + component: PasswordPromptComponent, + componentProps: { options }, + canDismiss: async password => { + if (password === null) { + return true + } + const { passwordHash, id } = await getServerInfo(this.patch) - // @TODO Alex if invalid password, we should tell the user "Invalid password" and halt execution of this function. The modal should remain so the user can try again. Correct password is asdfasdf // confirm password matches current master password - argon2.verify(passwordHash, password) + try { + argon2.verify(passwordHash, password) + } catch (e: any) { + this.errorService.handleError(e) + return false + } // first time backup if (!this.backupService.hasThisBackup(target.entry, id)) { - await this.createBackup(target, password) + this.createBackup(target, password) + return true // existing backup } else { try { @@ -106,39 +113,49 @@ export class ServerBackupPage { () => this.presentModalOldPassword(target, password), 250, ) - return + return true } await this.createBackup(target, password) + return true } - }) + }, + }) + modal.present() } private async presentModalOldPassword( target: MappedBackupTarget, password: string, ): Promise { + const { id } = await getServerInfo(this.patch) const options: PromptOptions = { + title: 'Original Password Needed', message: 'This backup was created with a different password. Enter the ORIGINAL password that was used to encrypt this backup.', label: 'Original Password', placeholder: 'Enter original password', - useMask: true, buttonText: 'Create Backup', } - const { id } = await getServerInfo(this.patch) + const modal = await this.modalCtrl.create({ + component: PasswordPromptComponent, + componentProps: { options }, + canDismiss: async oldPassword => { + if (oldPassword === null) { + return true + } - this.dialogs - .open(PROMPT, { - label: 'Original Password Needed', - data: options, - }) - .pipe(take(1)) - .subscribe(async (oldPassword: string) => { - // @TODO Alex if invalid password, we should tell the user "Invalid password" and halt execution of this function. The modal should remain so the user can try again. Correct password is asdfasdf - argon2.verify(target.entry.startOs[id].passwordHash!, oldPassword) - await this.createBackup(target, password, oldPassword) - }) + try { + argon2.verify(target.entry.startOs[id].passwordHash!, oldPassword) + await this.createBackup(target, password, oldPassword) + return true + } catch (e: any) { + this.errorService.handleError(e) + return false + } + }, + }) + modal.present() } private async createBackup( diff --git a/web/projects/ui/src/app/pages/widgets/widgets.page.ts b/web/projects/ui/src/app/pages/widgets/widgets.page.ts index ca1541269..30d605bda 100644 --- a/web/projects/ui/src/app/pages/widgets/widgets.page.ts +++ b/web/projects/ui/src/app/pages/widgets/widgets.page.ts @@ -56,7 +56,7 @@ export class WidgetsPage { @Optional() @Inject(POLYMORPHEUS_CONTEXT) readonly context: TuiDialogContext | null, - private readonly dialog: TuiDialogService, + private readonly dialogs: TuiDialogService, private readonly patch: PatchDB, private readonly cdr: ChangeDetectorRef, private readonly api: ApiService, @@ -83,7 +83,7 @@ export class WidgetsPage { } add() { - this.dialog.open(ADD_WIDGET, { label: 'Add widget' }).subscribe(widget => { + this.dialogs.open(ADD_WIDGET, { label: 'Add widget' }).subscribe(widget => { this.addWidget(widget!) }) } From 62fc6afd8a0220d4782691acdf24917d80dff977 Mon Sep 17 00:00:00 2001 From: waterplea Date: Fri, 12 Jul 2024 12:46:07 +0500 Subject: [PATCH 060/125] fix: fix select on mobile --- .../app/components/form/form-select/form-select.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/web/projects/ui/src/app/components/form/form-select/form-select.component.html b/web/projects/ui/src/app/components/form/form-select/form-select.component.html index c10e62018..fe2b561c7 100644 --- a/web/projects/ui/src/app/components/form/form-select/form-select.component.html +++ b/web/projects/ui/src/app/components/form/form-select/form-select.component.html @@ -11,6 +11,7 @@ * From 8f0bdcd1720939b9135242ee53628c8e69cdfee0 Mon Sep 17 00:00:00 2001 From: Jade <2364004+Blu-J@users.noreply.github.com> Date: Wed, 17 Jul 2024 15:46:27 -0600 Subject: [PATCH 061/125] Fix/backups (#2659) * fix master build (#2639) * feat: Change ts to use rsync Chore: Update the ts to use types over interface * feat: Get the rust and the js to do a backup * Wip: Got the backup working? * fix permissions * remove trixie list * update tokio to fix timer bug * fix error handling on backup * wip * remove idmap * run restore before init, and init with own version on restore --------- Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Co-authored-by: Aiden McClelland --- .../src/Adapters/Systems/SystemForStartOs.ts | 12 +- container-runtime/src/Interfaces/System.ts | 2 +- core/Cargo.lock | 354 +++++++++--------- core/startos/Cargo.toml | 3 +- core/startos/src/backup/backup_bulk.rs | 2 +- core/startos/src/backup/restore.rs | 2 +- core/startos/src/bins/startd.rs | 2 + core/startos/src/disk/mount/backup.rs | 26 +- .../src/disk/mount/filesystem/backupfs.rs | 21 +- core/startos/src/service/mod.rs | 76 ++-- .../src/service/persistent_container.rs | 12 +- core/startos/src/service/service_map.rs | 43 +-- core/startos/src/service/transition/backup.rs | 68 ++-- core/startos/src/service/transition/mod.rs | 5 +- core/startos/src/sound.rs | 8 +- core/startos/src/util/mod.rs | 2 +- image-recipe/build.sh | 3 + sdk/lib/backup/Backups.ts | 165 ++++---- sdk/lib/backup/setupBackups.ts | 6 +- sdk/lib/config/configTypes.ts | 1 - sdk/lib/manifest/ManifestTypes.ts | 4 +- sdk/lib/types.ts | 7 +- .../ui/src/app/services/api/api.fixures.ts | 1 + 23 files changed, 445 insertions(+), 380 deletions(-) diff --git a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts index e7f09952a..84bf57302 100644 --- a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts +++ b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts @@ -7,6 +7,7 @@ import { RpcResult, matchRpcResult } from "../RpcListener" import { duration } from "../../Models/Duration" import { T } from "@start9labs/start-sdk" import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk" +import { Volume } from "../../Models/Volume" export const STARTOS_JS_LOCATION = "/usr/lib/startos/package/index.js" export class SystemForStartOs implements System { private onTerm: (() => Promise) | undefined @@ -151,8 +152,17 @@ export class SystemForStartOs implements System { return this.abi.getConfig({ effects }) } case "/backup/create": + return this.abi.createBackup({ + effects, + pathMaker: ((options) => + new Volume(options.volume, options.path).path) as T.PathMaker, + }) case "/backup/restore": - throw new Error("this should be called with the init/unit") + return this.abi.restoreBackup({ + effects, + pathMaker: ((options) => + new Volume(options.volume, options.path).path) as T.PathMaker, + }) case "/actions/metadata": { return this.abi.actionsMetadata({ effects }) } diff --git a/container-runtime/src/Interfaces/System.ts b/container-runtime/src/Interfaces/System.ts index 986288411..58e045356 100644 --- a/container-runtime/src/Interfaces/System.ts +++ b/container-runtime/src/Interfaces/System.ts @@ -5,7 +5,7 @@ import { hostSystemStartOs } from "../Adapters/HostSystemStartOs" export type ExecuteResult = | { ok: unknown } | { err: { code: number; message: string } } -export interface System { +export type System = { // init(effects: Effects): Promise // exit(effects: Effects): Promise // start(effects: Effects): Promise diff --git a/core/Cargo.lock b/core/Cargo.lock index 9f7face93..1072f4203 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -231,18 +231,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] name = "async-trait" -version = "0.1.80" +version = "0.1.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -306,7 +306,7 @@ dependencies = [ "futures-util", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.29", + "hyper 0.14.30", "itoa", "matchit", "memchr", @@ -333,9 +333,9 @@ dependencies = [ "bytes", "futures-util", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "http-body-util", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-util", "itoa", "matchit", @@ -385,7 +385,7 @@ dependencies = [ "bytes", "futures-util", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", @@ -405,9 +405,9 @@ dependencies = [ "bytes", "futures-util", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "http-body-util", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-util", "pin-project-lite", "tokio", @@ -532,7 +532,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.68", + "syn 2.0.71", "which", ] @@ -609,9 +609,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.5.1" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52" +checksum = "e9ec96fe9a81b5e365f9db71fe00edc4fe4ca2cc7dcb7861f0603012a7caa210" dependencies = [ "arrayref", "arrayvec 0.7.4", @@ -619,7 +619,7 @@ dependencies = [ "cfg-if", "constant_time_eq", "memmap2", - "rayon", + "rayon-core", ] [[package]] @@ -675,9 +675,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" [[package]] name = "cache-padded" @@ -687,13 +687,12 @@ checksum = "981520c98f422fcc584dc1a95c334e6953900b9106bc47a9839b81790009eb21" [[package]] name = "cc" -version = "1.0.101" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac367972e516d45567c7eafc73d24e1c193dcf200a8d94e9db7b3d38b349572d" +checksum = "324c74f2155653c90b04f25b2a47a8a631360cb908f92a772695f430c7e31052" dependencies = [ "jobserver", "libc", - "once_cell", ] [[package]] @@ -729,7 +728,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -800,9 +799,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.7" +version = "4.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" +checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" dependencies = [ "clap_builder", "clap_derive", @@ -810,9 +809,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.7" +version = "4.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" +checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" dependencies = [ "anstream", "anstyle", @@ -822,14 +821,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.5" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c780290ccf4fb26629baa7a1081e68ced113f1d3ec302fa5948f1c381ebf06c6" +checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -1237,14 +1236,14 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] name = "darling" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ "darling_core", "darling_macro", @@ -1252,27 +1251,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] name = "darling_macro" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -1303,7 +1302,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -1320,13 +1319,13 @@ dependencies = [ [[package]] name = "der_derive" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fe87ce4529967e0ba1dcf8450bab64d97dfd5010a6256187ffe2e43e6f0e049" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -1349,7 +1348,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -1571,7 +1570,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -1829,7 +1828,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -2143,9 +2142,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http 1.1.0", @@ -2160,7 +2159,7 @@ dependencies = [ "bytes", "futures-util", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "pin-project-lite", ] @@ -2184,9 +2183,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.29" +version = "0.14.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" dependencies = [ "bytes", "futures-channel", @@ -2208,16 +2207,16 @@ dependencies = [ [[package]] name = "hyper" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", "futures-util", "h2 0.4.5", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "httparse", "httpdate", "itoa", @@ -2235,9 +2234,9 @@ checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-util", - "rustls 0.23.10", + "rustls 0.23.11", "rustls-pki-types", "tokio", "tokio-rustls", @@ -2250,7 +2249,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper 0.14.29", + "hyper 0.14.30", "pin-project-lite", "tokio", "tokio-io-timeout", @@ -2264,7 +2263,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-util", "native-tls", "tokio", @@ -2274,16 +2273,16 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" +checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" dependencies = [ "bytes", "futures-channel", "futures-util", "http 1.1.0", - "http-body 1.0.0", - "hyper 1.3.1", + "http-body 1.0.1", + "hyper 1.4.1", "pin-project-lite", "socket2", "tokio", @@ -2783,7 +2782,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" dependencies = [ "cfg-if", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -2837,9 +2836,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lzma-sys" @@ -3230,7 +3229,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -3262,9 +3261,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssh-keys" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9939566c441a6542e87c74310d4ac713c17679c76f296932e732f405bc25e3d" +checksum = "abb830a82898b2ac17c9620ddce839ac3b34b9cb8a1a037cbdbfb9841c756c3e" dependencies = [ "base64 0.21.7", "byteorder", @@ -3296,7 +3295,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -3395,9 +3394,9 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.2", + "redox_syscall 0.5.3", "smallvec", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -3474,9 +3473,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.10" +version = "2.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" +checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" dependencies = [ "memchr", "thiserror", @@ -3485,9 +3484,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.10" +version = "2.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" +checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a" dependencies = [ "pest", "pest_generator", @@ -3495,22 +3494,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.10" +version = "2.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" +checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] name = "pest_meta" -version = "2.7.10" +version = "2.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" +checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f" dependencies = [ "once_cell", "pest", @@ -3559,7 +3558,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -3632,7 +3631,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -3704,7 +3703,7 @@ checksum = "6ff7ff745a347b87471d859a377a9a404361e7efc2a971d73424a6d183c0fc77" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -3727,7 +3726,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -3871,16 +3870,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "rayon" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" -dependencies = [ - "either", - "rayon-core", -] - [[package]] name = "rayon-core" version = "1.12.1" @@ -3917,9 +3906,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ "bitflags 2.6.0", ] @@ -3994,9 +3983,9 @@ dependencies = [ "futures-util", "h2 0.4.5", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "http-body-util", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-rustls", "hyper-tls", "hyper-util", @@ -4192,15 +4181,15 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.10" +version = "0.23.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" +checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" dependencies = [ "aws-lc-rs", "log", "once_cell", "rustls-pki-types", - "rustls-webpki 0.102.4", + "rustls-webpki 0.102.5", "subtle", "zeroize", ] @@ -4242,9 +4231,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.4" +version = "0.102.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" dependencies = [ "aws-lc-rs", "ring", @@ -4342,9 +4331,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.6.0", "core-foundation", @@ -4355,9 +4344,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" dependencies = [ "core-foundation-sys", "libc", @@ -4374,9 +4363,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] @@ -4400,20 +4389,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] name = "serde_json" -version = "1.0.118" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ "indexmap 2.2.6", "itoa", @@ -4454,9 +4443,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.8.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" dependencies = [ "base64 0.22.1", "chrono", @@ -4472,14 +4461,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.8.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" +checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -4906,7 +4895,7 @@ dependencies = [ "quote", "regex-syntax 0.6.29", "strsim 0.10.0", - "syn 2.0.68", + "syn 2.0.71", "unicode-width", ] @@ -5020,6 +5009,7 @@ dependencies = [ "nix 0.29.0", "nom 7.1.3", "num", + "num_cpus", "num_enum", "once_cell", "openssh-keys", @@ -5140,9 +5130,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.68" +version = "2.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" dependencies = [ "proc-macro2", "quote", @@ -5224,9 +5214,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.1.3" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] @@ -5243,22 +5233,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -5324,9 +5314,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55115c6fbe2d2bef26eb09ad74bde02d8255476fc0c7b515ef09fbb35742d82" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -5339,9 +5329,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" dependencies = [ "backtrace", "bytes", @@ -5375,7 +5365,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -5394,7 +5384,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.10", + "rustls 0.23.11", "rustls-pki-types", "tokio", ] @@ -5497,7 +5487,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.14", + "toml_edit 0.22.15", ] [[package]] @@ -5535,9 +5525,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.14" +version = "0.22.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" +checksum = "d59a3a72298453f564e2b111fa896f8d07fabb36f51f06d7e875fc5e0b5a3ef1" dependencies = [ "indexmap 2.2.6", "serde", @@ -5560,7 +5550,7 @@ dependencies = [ "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.29", + "hyper 0.14.30", "hyper-timeout", "percent-encoding", "pin-project", @@ -5644,7 +5634,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -5796,7 +5786,7 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", "termcolor", ] @@ -5856,7 +5846,7 @@ checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -5975,9 +5965,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.9.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom 0.2.15", ] @@ -6067,7 +6057,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", "wasm-bindgen-shared", ] @@ -6101,7 +6091,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6200,7 +6190,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -6218,7 +6208,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -6238,18 +6228,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -6260,9 +6250,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -6272,9 +6262,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -6284,15 +6274,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -6302,9 +6292,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -6314,9 +6304,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -6326,9 +6316,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -6338,9 +6328,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" @@ -6440,22 +6430,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] @@ -6475,32 +6465,32 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.68", + "syn 2.0.71", ] [[package]] name = "zstd" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "7.1.0" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" +checksum = "fa556e971e7b568dc775c136fc9de8c779b1c2fc3a63defaafadffdbd3181afa" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.11+zstd.1.5.6" +version = "2.0.12+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75652c55c0b6f3e6f12eb786fe1bc960396bf05a1eb3bf1f3691c3610ac2e6d4" +checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13" dependencies = [ "cc", "pkg-config", diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index 842ef44ef..2695c7813 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -128,6 +128,7 @@ nix = { version = "0.29.0", features = ["user", "process", "signal", "fs"] } nom = "7.1.3" num = "0.4.1" num_enum = "0.7.0" +num_cpus = "1.16.0" once_cell = "1.19.0" openssh-keys = "0.6.2" openssl = { version = "0.10.57", features = ["vendored"] } @@ -170,7 +171,7 @@ sscanf = "0.4.1" ssh-key = { version = "0.6.2", features = ["ed25519"] } tar = "0.4.40" thiserror = "1.0.49" -tokio = { version = "1.38.0", features = ["full"] } +tokio = { version = "1.38.1", features = ["full"] } tokio-rustls = "0.26.0" tokio-socks = "0.5.1" tokio-stream = { version = "0.1.14", features = ["io-util", "sync", "net"] } diff --git a/core/startos/src/backup/backup_bulk.rs b/core/startos/src/backup/backup_bulk.rs index a52c97f9b..b4419e88e 100644 --- a/core/startos/src/backup/backup_bulk.rs +++ b/core/startos/src/backup/backup_bulk.rs @@ -260,7 +260,7 @@ async fn perform_backup( for id in package_ids { if let Some(service) = &*ctx.services.get(id).await { let backup_result = service - .backup(backup_guard.package_backup(id)) + .backup(backup_guard.package_backup(id).await?) .await .err() .map(|e| e.to_string()); diff --git a/core/startos/src/backup/restore.rs b/core/startos/src/backup/restore.rs index 23e0c8ac1..28d70653f 100644 --- a/core/startos/src/backup/restore.rs +++ b/core/startos/src/backup/restore.rs @@ -158,7 +158,7 @@ async fn restore_packages( let backup_guard = Arc::new(backup_guard); let mut tasks = BTreeMap::new(); for id in ids { - let backup_dir = backup_guard.clone().package_backup(&id); + let backup_dir = backup_guard.clone().package_backup(&id).await?; let s9pk_path = backup_dir.path().join(&id).with_extension("s9pk"); let task = ctx .services diff --git a/core/startos/src/bins/startd.rs b/core/startos/src/bins/startd.rs index 7576c41e9..d383f3091 100644 --- a/core/startos/src/bins/startd.rs +++ b/core/startos/src/bins/startd.rs @@ -1,3 +1,4 @@ +use std::cmp::max; use std::ffi::OsString; use std::net::{Ipv6Addr, SocketAddr}; use std::sync::Arc; @@ -136,6 +137,7 @@ pub fn main(args: impl IntoIterator) { let res = { let rt = tokio::runtime::Builder::new_multi_thread() + .worker_threads(max(4, num_cpus::get())) .enable_all() .build() .expect("failed to initialize runtime"); diff --git a/core/startos/src/disk/mount/backup.rs b/core/startos/src/disk/mount/backup.rs index 142301a74..8f45b0d4f 100644 --- a/core/startos/src/disk/mount/backup.rs +++ b/core/startos/src/disk/mount/backup.rs @@ -106,8 +106,11 @@ impl BackupMountGuard { ) })?; } - let encrypted_guard = - TmpMountGuard::mount(&BackupFS::new(&crypt_path, &enc_key), ReadWrite).await?; + let encrypted_guard = TmpMountGuard::mount( + &BackupFS::new(&crypt_path, &enc_key, vec![(100000, 65536)]), + ReadWrite, + ) + .await?; let metadata_path = encrypted_guard.path().join("metadata.json"); let metadata: BackupInfo = if tokio::fs::metadata(&metadata_path).await.is_ok() { @@ -148,8 +151,23 @@ impl BackupMountGuard { } #[instrument(skip_all)] - pub fn package_backup(self: &Arc, id: &PackageId) -> SubPath> { - SubPath::new(self.clone(), id) + pub async fn package_backup( + self: &Arc, + id: &PackageId, + ) -> Result>, Error> { + let package_guard = SubPath::new(self.clone(), id); + let package_path = package_guard.path(); + if tokio::fs::metadata(&package_path).await.is_err() { + tokio::fs::create_dir_all(&package_path) + .await + .with_ctx(|_| { + ( + crate::ErrorKind::Filesystem, + package_path.display().to_string(), + ) + })?; + } + Ok(package_guard) } #[instrument(skip_all)] diff --git a/core/startos/src/disk/mount/filesystem/backupfs.rs b/core/startos/src/disk/mount/filesystem/backupfs.rs index 9ef258f34..254abde20 100644 --- a/core/startos/src/disk/mount/filesystem/backupfs.rs +++ b/core/startos/src/disk/mount/filesystem/backupfs.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::fmt::{self, Display}; use std::os::unix::ffi::OsStrExt; use std::path::Path; @@ -12,10 +13,15 @@ use crate::prelude::*; pub struct BackupFS, Password: fmt::Display> { data_dir: DataDir, password: Password, + idmapped_root: Vec<(u32, u32)>, } impl, Password: fmt::Display> BackupFS { - pub fn new(data_dir: DataDir, password: Password) -> Self { - BackupFS { data_dir, password } + pub fn new(data_dir: DataDir, password: Password, idmapped_root: Vec<(u32, u32)>) -> Self { + BackupFS { + data_dir, + password, + idmapped_root, + } } } impl + Send + Sync, Password: fmt::Display + Send + Sync> FileSystem @@ -26,9 +32,16 @@ impl + Send + Sync, Password: fmt::Display + Send + Sync> F } fn mount_options(&self) -> impl IntoIterator { [ - format!("password={}", self.password), - format!("file-size-padding=0.05"), + Cow::Owned(format!("password={}", self.password)), + Cow::Borrowed("file-size-padding=0.05"), + Cow::Borrowed("allow_other"), ] + .into_iter() + .chain( + self.idmapped_root + .iter() + .map(|(root, range)| Cow::Owned(format!("idmapped-root={root}:{range}"))), + ) } async fn source(&self) -> Result>, Error> { Ok(Some(&self.data_dir)) diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index 581d2ac0d..d78df6c79 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -34,6 +34,7 @@ use crate::util::actor::concurrent::ConcurrentActor; use crate::util::actor::Actor; use crate::util::io::create_file; use crate::util::serde::Pem; +use crate::util::Never; use crate::volume::data_dir; mod action; @@ -220,12 +221,13 @@ impl Service { tracing::error!("Error opening s9pk for install: {e}"); tracing::debug!("{e:?}") }) { - if let Ok(service) = Self::install(ctx.clone(), s9pk, None, None) - .await - .map_err(|e| { - tracing::error!("Error installing service: {e}"); - tracing::debug!("{e:?}") - }) + if let Ok(service) = + Self::install(ctx.clone(), s9pk, None, None::, None) + .await + .map_err(|e| { + tracing::error!("Error installing service: {e}"); + tracing::debug!("{e:?}") + }) { return Ok(Some(service)); } @@ -257,6 +259,7 @@ impl Service { ctx.clone(), s9pk, Some(s.as_manifest().as_version().de()?), + None::, None, ) .await @@ -334,13 +337,35 @@ impl Service { pub async fn install( ctx: RpcContext, s9pk: S9pk, - src_version: Option, + mut src_version: Option, + recovery_source: Option, progress: Option, ) -> Result { let manifest = s9pk.as_manifest().clone(); let developer_key = s9pk.as_archive().signer(); let icon = s9pk.icon_data_url().await?; let service = Self::new(ctx.clone(), s9pk, StartStop::Stop).await?; + if let Some(recovery_source) = recovery_source { + service + .actor + .send( + Guid::new(), + transition::restore::Restore { + path: recovery_source.path().to_path_buf(), + }, + ) + .await??; + recovery_source.unmount().await?; + src_version = Some( + service + .seed + .persistent_container + .s9pk + .as_manifest() + .version + .clone(), + ); + } service .seed .persistent_container @@ -382,26 +407,6 @@ impl Service { Ok(service) } - pub async fn restore( - ctx: RpcContext, - s9pk: S9pk, - backup_source: impl GenericMountGuard, - progress: Option, - ) -> Result { - let service = Service::install(ctx.clone(), s9pk, None, progress).await?; - - service - .actor - .send( - Guid::new(), - transition::restore::Restore { - path: backup_source.path().to_path_buf(), - }, - ) - .await??; - Ok(service) - } - #[instrument(skip_all)] pub async fn backup(&self, guard: impl GenericMountGuard) -> Result<(), Error> { let id = &self.seed.id; @@ -417,10 +422,11 @@ impl Service { .send( Guid::new(), transition::backup::Backup { - path: guard.path().to_path_buf(), + path: guard.path().join("data"), }, ) - .await??; + .await?? + .await?; Ok(()) } @@ -505,13 +511,21 @@ impl Actor for ServiceActor { } (Some(TransitionKind::Restarting), _, _) => MainStatus::Restarting, (Some(TransitionKind::Restoring), _, _) => MainStatus::Restoring, - (Some(TransitionKind::BackingUp), _, Some(status)) => { + (Some(TransitionKind::BackingUp), StartStop::Stop, Some(status)) => { + seed.persistent_container.stop().await?; MainStatus::BackingUp { started: Some(status.started), health: status.health.clone(), } } - (Some(TransitionKind::BackingUp), _, None) => MainStatus::BackingUp { + (Some(TransitionKind::BackingUp), StartStop::Start, _) => { + seed.persistent_container.start().await?; + MainStatus::BackingUp { + started: None, + health: OrdMap::new(), + } + } + (Some(TransitionKind::BackingUp), _, _) => MainStatus::BackingUp { started: None, health: OrdMap::new(), }, diff --git a/core/startos/src/service/persistent_container.rs b/core/startos/src/service/persistent_container.rs index e0b31ea97..11cc237d3 100644 --- a/core/startos/src/service/persistent_container.rs +++ b/core/startos/src/service/persistent_container.rs @@ -1,5 +1,5 @@ use std::collections::BTreeMap; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::sync::{Arc, Weak}; use std::time::Duration; @@ -277,7 +277,7 @@ impl PersistentContainer { backup_path: impl AsRef, mount_type: MountType, ) -> Result { - let backup_path: PathBuf = backup_path.as_ref().to_path_buf(); + let backup_path = backup_path.as_ref(); let mountpoint = self .lxc_container .get() @@ -295,14 +295,14 @@ impl PersistentContainer { .arg(mountpoint.as_os_str()) .invoke(ErrorKind::Filesystem) .await?; - let bind = Bind::new(&backup_path); - let mount_guard = MountGuard::mount(&bind, &mountpoint, mount_type).await; + tokio::fs::create_dir_all(backup_path).await?; Command::new("chown") .arg("100000:100000") - .arg(backup_path.as_os_str()) + .arg(backup_path) .invoke(ErrorKind::Filesystem) .await?; - mount_guard + let bind = Bind::new(backup_path); + MountGuard::mount(&bind, &mountpoint, mount_type).await } #[instrument(skip_all)] diff --git a/core/startos/src/service/service_map.rs b/core/startos/src/service/service_map.rs index af10a065c..90223216c 100644 --- a/core/startos/src/service/service_map.rs +++ b/core/startos/src/service/service_map.rs @@ -265,35 +265,20 @@ impl ServiceMap { } else { None }; - if let Some(recovery_source) = recovery_source { - *service = Some( - Service::restore( - ctx, - s9pk, - recovery_source, - Some(InstallProgressHandles { - finalization_progress, - progress, - }), - ) - .await? - .into(), - ); - } else { - *service = Some( - Service::install( - ctx, - s9pk, - prev, - Some(InstallProgressHandles { - finalization_progress, - progress, - }), - ) - .await? - .into(), - ); - } + *service = Some( + Service::install( + ctx, + s9pk, + prev, + recovery_source, + Some(InstallProgressHandles { + finalization_progress, + progress, + }), + ) + .await? + .into(), + ); drop(service); sync_progress_task.await.map_err(|_| { diff --git a/core/startos/src/service/transition/backup.rs b/core/startos/src/service/transition/backup.rs index f7591f0d9..d8606f534 100644 --- a/core/startos/src/service/transition/backup.rs +++ b/core/startos/src/service/transition/backup.rs @@ -1,5 +1,7 @@ use std::path::PathBuf; +use std::sync::Arc; +use futures::future::BoxFuture; use futures::FutureExt; use models::ProcedureName; @@ -19,7 +21,7 @@ pub(in crate::service) struct Backup { pub path: PathBuf, } impl Handler for ServiceActor { - type Response = Result<(), Error>; + type Response = Result>, Error>; fn conflicts_with(_: &Backup) -> ConflictBuilder { ConflictBuilder::everything() .except::() @@ -37,43 +39,31 @@ impl Handler for ServiceActor { let path = backup.path.clone(); let seed = self.0.clone(); - let state = self.0.persistent_container.state.clone(); - let transition = RemoteCancellable::new( - async move { - temp.stop(); + let transition = RemoteCancellable::new(async move { + temp.stop(); + current + .wait_for(|s| s.running_status.is_none()) + .await + .with_kind(ErrorKind::Unknown)?; + + let backup_guard = seed + .persistent_container + .mount_backup(path, ReadWrite) + .await?; + seed.persistent_container + .execute(id, ProcedureName::CreateBackup, Value::Null, None) + .await?; + backup_guard.unmount(true).await?; + + if temp.restore().is_start() { current - .wait_for(|s| s.running_status.is_none()) + .wait_for(|s| s.running_status.is_some()) .await .with_kind(ErrorKind::Unknown)?; - - let backup_guard = seed - .persistent_container - .mount_backup(path, ReadWrite) - .await?; - seed.persistent_container - .execute(id, ProcedureName::CreateBackup, Value::Null, None) - .await?; - backup_guard.unmount(true).await?; - - if temp.restore().is_start() { - current - .wait_for(|s| s.running_status.is_some()) - .await - .with_kind(ErrorKind::Unknown)?; - } - drop(temp); - state.send_modify(|s| { - s.transition_state.take(); - }); - Ok::<_, Error>(()) } - .map(|x| { - if let Err(err) = dbg!(x) { - tracing::debug!("{:?}", err); - tracing::warn!("{}", err); - } - }), - ); + drop(temp); + Ok::<_, Arc>(()) + }); let cancel_handle = transition.cancellation_handle(); let transition = transition.shared(); let job_transition = transition.clone(); @@ -92,9 +82,11 @@ impl Handler for ServiceActor { if let Some(t) = old { t.abort().await; } - match transition.await { - None => Err(Error::new(eyre!("Backup canceled"), ErrorKind::Unknown)), - Some(x) => Ok(x), - } + Ok(transition + .map(|r| { + r.ok_or_else(|| Error::new(eyre!("Backup canceled"), ErrorKind::Cancelled))? + .map_err(|e| e.clone_output()) + }) + .boxed()) } } diff --git a/core/startos/src/service/transition/mod.rs b/core/startos/src/service/transition/mod.rs index 7b7f10f2a..a6a41073b 100644 --- a/core/startos/src/service/transition/mod.rs +++ b/core/startos/src/service/transition/mod.rs @@ -79,7 +79,10 @@ impl TempDesiredRestore { } impl Drop for TempDesiredRestore { fn drop(&mut self) { - self.0.send_modify(|s| s.temp_desired_state = None); + self.0.send_modify(|s| { + s.temp_desired_state.take(); + s.transition_state.take(); + }); } } // impl Deref for TempDesiredState { diff --git a/core/startos/src/sound.rs b/core/startos/src/sound.rs index 8dc78357c..8cedd78ce 100644 --- a/core/startos/src/sound.rs +++ b/core/startos/src/sound.rs @@ -10,12 +10,12 @@ use crate::util::{FileLock, Invoke}; use crate::{Error, ErrorKind}; lazy_static::lazy_static! { - static ref SEMITONE_K: f64 = 2f64.powf(1f64 / 12f64); - static ref A_4: f64 = 440f64; - static ref C_0: f64 = *A_4 / SEMITONE_K.powf(9f64) / 2f64.powf(4f64); + static ref SEMITONE_K: f64 = 2f64.powf(1.0 / 12.0); + static ref A_4: f64 = 440.0; + static ref C_0: f64 = *A_4 / SEMITONE_K.powf(9.0) / 2_f64.powf(4.0); } -pub const SOUND_LOCK_FILE: &str = "/etc/embassy/sound.lock"; +pub const SOUND_LOCK_FILE: &str = "/run/startos/sound.lock"; struct SoundInterface { guard: Option, diff --git a/core/startos/src/util/mod.rs b/core/startos/src/util/mod.rs index 0c9e5c5f9..f591eaaf3 100644 --- a/core/startos/src/util/mod.rs +++ b/core/startos/src/util/mod.rs @@ -555,7 +555,7 @@ impl T, T> Drop for GeneralGuard { } } -pub struct FileLock(OwnedMutexGuard<()>, Option>); +pub struct FileLock(#[allow(unused)] OwnedMutexGuard<()>, Option>); impl Drop for FileLock { fn drop(&mut self) { if let Some(fd_lock) = self.1.take() { diff --git a/image-recipe/build.sh b/image-recipe/build.sh index 5ec500ce3..5635e94f3 100755 --- a/image-recipe/build.sh +++ b/image-recipe/build.sh @@ -166,6 +166,9 @@ fi curl -fsSL https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc > config/archives/tor.key echo "deb [arch=${IB_TARGET_ARCH} signed-by=/etc/apt/trusted.gpg.d/tor.key.gpg] https://deb.torproject.org/torproject.org ${IB_SUITE} main" > config/archives/tor.list +curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o config/archives/docker.key +echo "deb [arch=${IB_TARGET_ARCH} signed-by=/etc/apt/trusted.gpg.d/docker.key.gpg] https://download.docker.com/linux/debian ${IB_SUITE} stable" > config/archives/docker.list + # Dependencies ## Base dependencies diff --git a/sdk/lib/backup/Backups.ts b/sdk/lib/backup/Backups.ts index 20099c86d..6ffe74b70 100644 --- a/sdk/lib/backup/Backups.ts +++ b/sdk/lib/backup/Backups.ts @@ -1,6 +1,10 @@ +import { recursive } from "ts-matches" import { SDKManifest } from "../manifest/ManifestTypes" import * as T from "../types" +import * as child_process from "child_process" +import { promises as fsPromises } from "fs" + export type BACKUP = "BACKUP" export const DEFAULT_OPTIONS: T.BackupOptions = { delete: true, @@ -91,58 +95,22 @@ export class Backups { ) return this } - build() { + build(pathMaker: T.PathMaker) { const createBackup: T.ExpectedExports.createBackup = async ({ effects, }) => { - // const previousItems = ( - // await effects - // .readDir({ - // volumeId: Backups.BACKUP, - // path: ".", - // }) - // .catch(() => []) - // ).map((x) => `${x}`) - // const backupPaths = this.backupSet - // .filter((x) => x.dstVolume === Backups.BACKUP) - // .map((x) => x.dstPath) - // .map((x) => x.replace(/\.\/([^]*)\//, "$1")) - // const filteredItems = previousItems.filter( - // (x) => backupPaths.indexOf(x) === -1, - // ) - // for (const itemToRemove of filteredItems) { - // effects.console.error(`Trying to remove ${itemToRemove}`) - // await effects - // .removeDir({ - // volumeId: Backups.BACKUP, - // path: itemToRemove, - // }) - // .catch(() => - // effects.removeFile({ - // volumeId: Backups.BACKUP, - // path: itemToRemove, - // }), - // ) - // .catch(() => { - // console.warn(`Failed to remove ${itemToRemove} from backup volume`) - // }) - // } for (const item of this.backupSet) { - // if (notEmptyPath(item.dstPath)) { - // await effects.createDir({ - // volumeId: item.dstVolume, - // path: item.dstPath, - // }) - // } - // await effects - // .runRsync({ - // ...item, - // options: { - // ...this.options, - // ...item.options, - // }, - // }) - // .wait() + const rsyncResults = await runRsync( + { + dstPath: item.dstPath, + dstVolume: item.dstVolume, + options: { ...this.options, ...item.options }, + srcPath: item.srcPath, + srcVolume: item.srcVolume, + }, + pathMaker, + ) + await rsyncResults.wait() } return } @@ -150,26 +118,17 @@ export class Backups { effects, }) => { for (const item of this.backupSet) { - // if (notEmptyPath(item.srcPath)) { - // await new Promise((resolve, reject) => fs.mkdir(items.src)).createDir( - // { - // volumeId: item.srcVolume, - // path: item.srcPath, - // }, - // ) - // } - // await effects - // .runRsync({ - // options: { - // ...this.options, - // ...item.options, - // }, - // srcVolume: item.dstVolume, - // dstVolume: item.srcVolume, - // srcPath: item.dstPath, - // dstPath: item.srcPath, - // }) - // .wait() + const rsyncResults = await runRsync( + { + dstPath: item.dstPath, + dstVolume: item.dstVolume, + options: { ...this.options, ...item.options }, + srcPath: item.srcPath, + srcVolume: item.srcVolume, + }, + pathMaker, + ) + await rsyncResults.wait() } return } @@ -179,3 +138,73 @@ export class Backups { function notEmptyPath(file: string) { return ["", ".", "./"].indexOf(file) === -1 } +async function runRsync( + rsyncOptions: { + srcVolume: string + dstVolume: string + srcPath: string + dstPath: string + options: T.BackupOptions + }, + pathMaker: T.PathMaker, +): Promise<{ + id: () => Promise + wait: () => Promise + progress: () => Promise +}> { + const { srcVolume, dstVolume, srcPath, dstPath, options } = rsyncOptions + + const command = "rsync" + const args: string[] = [] + if (options.delete) { + args.push("--delete") + } + if (options.force) { + args.push("--force") + } + if (options.ignoreExisting) { + args.push("--ignore-existing") + } + for (const exclude of options.exclude) { + args.push(`--exclude=${exclude}`) + } + args.push("-actAXH") + args.push("--info=progress2") + args.push("--no-inc-recursive") + args.push(pathMaker({ volume: srcVolume, path: srcPath })) + args.push(pathMaker({ volume: dstVolume, path: dstPath })) + const spawned = child_process.spawn(command, args, { detached: true }) + let percentage = 0.0 + spawned.stdout.on("data", (data: unknown) => { + const lines = String(data).replace("\r", "\n").split("\n") + for (const line of lines) { + const parsed = /$([0-9.]+)%/.exec(line)?.[1] + if (!parsed) continue + percentage = Number.parseFloat(parsed) + } + }) + + spawned.stderr.on("data", (data: unknown) => { + console.error(String(data)) + }) + + const id = async () => { + const pid = spawned.pid + if (pid === undefined) { + throw new Error("rsync process has no pid") + } + return String(pid) + } + const waitPromise = new Promise((resolve, reject) => { + spawned.on("exit", (code: any) => { + if (code === 0) { + resolve(null) + } else { + reject(new Error(`rsync exited with code ${code}`)) + } + }) + }) + const wait = () => waitPromise + const progress = () => Promise.resolve(percentage) + return { id, wait, progress } +} diff --git a/sdk/lib/backup/setupBackups.ts b/sdk/lib/backup/setupBackups.ts index af2d08410..69d1bf7df 100644 --- a/sdk/lib/backup/setupBackups.ts +++ b/sdk/lib/backup/setupBackups.ts @@ -1,6 +1,6 @@ import { Backups } from "./Backups" import { SDKManifest } from "../manifest/ManifestTypes" -import { ExpectedExports } from "../types" +import { ExpectedExports, PathMaker } from "../types" import { _ } from "../util" export type SetupBackupsParams = Array< @@ -27,14 +27,14 @@ export function setupBackups( get createBackup() { return (async (options) => { for (const backup of backups) { - await backup.build().createBackup(options) + await backup.build(options.pathMaker).createBackup(options) } }) as ExpectedExports.createBackup }, get restoreBackup() { return (async (options) => { for (const backup of backups) { - await backup.build().restoreBackup(options) + await backup.build(options.pathMaker).restoreBackup(options) } }) as ExpectedExports.restoreBackup }, diff --git a/sdk/lib/config/configTypes.ts b/sdk/lib/config/configTypes.ts index 14d857433..0179e531e 100644 --- a/sdk/lib/config/configTypes.ts +++ b/sdk/lib/config/configTypes.ts @@ -240,7 +240,6 @@ export type ListValueSpecText = { inputmode: "text" | "email" | "tel" | "url" placeholder: string | null } - export type ListValueSpecObject = { type: "object" /** this is a mapped type of the config object at this level, replacing the object's values with specs on those values */ diff --git a/sdk/lib/manifest/ManifestTypes.ts b/sdk/lib/manifest/ManifestTypes.ts index c820930c8..8e6b572d3 100644 --- a/sdk/lib/manifest/ManifestTypes.ts +++ b/sdk/lib/manifest/ManifestTypes.ts @@ -1,7 +1,7 @@ import { ValidEmVer } from "../emverLite/mod" import { ActionMetadata, ImageConfig, ImageId } from "../types" -export interface Container { +export type Container = { /** This should be pointing to a docker container name */ image: string /** These should match the manifest data volumes */ @@ -72,7 +72,7 @@ export type SDKManifest = { readonly dependencies: Readonly> } -export interface ManifestDependency { +export type ManifestDependency = { /** * A human readable explanation on what the dependency is used for */ diff --git a/sdk/lib/types.ts b/sdk/lib/types.ts index 1f1245adc..686cb4750 100644 --- a/sdk/lib/types.ts +++ b/sdk/lib/types.ts @@ -26,6 +26,7 @@ export * from "./osBindings" export { SDKManifest } from "./manifest/ManifestTypes" export { HealthReceipt } from "./health/HealthReceipt" +export type PathMaker = (options: { volume: string; path: string }) => string export type ExportedAction = (options: { effects: Effects input?: Record @@ -43,10 +44,14 @@ export namespace ExpectedExports { // /** These are how we make sure the our dependency configurations are valid and if not how to fix them. */ // export type dependencies = Dependencies; /** For backing up service data though the startOS UI */ - export type createBackup = (options: { effects: Effects }) => Promise + export type createBackup = (options: { + effects: Effects + pathMaker: PathMaker + }) => Promise /** For restoring service data that was previously backed up using the startOS UI create backup flow. Backup restores are also triggered via the startOS UI, or doing a system restore flow during setup. */ export type restoreBackup = (options: { effects: Effects + pathMaker: PathMaker }) => Promise // /** Health checks are used to determine if the service is working properly after starting diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index a1f81fd95..c1f98dcd4 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -22,6 +22,7 @@ export module Mock { headline: 'Our biggest release ever.', releaseNotes: { '0.3.6': 'Some **Markdown** release _notes_ for 0.3.6', + '0.3.5.2': 'Some **Markdown** release _notes_ for 0.3.5.2', '0.3.5.1': 'Some **Markdown** release _notes_ for 0.3.5.1', '0.3.4.4': 'Some **Markdown** release _notes_ for 0.3.4.4', '0.3.4.3': 'Some **Markdown** release _notes_ for 0.3.4.3', From 196561fed2cc1df198022b8e218e7f8c9b5a68d1 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 18 Jul 2024 21:49:31 -0600 Subject: [PATCH 062/125] init UI increase logs buffer and don't throw on websocket unsubscribe (#2669) * init UI increase logs buffer and don't throw on websocket unsubscribe * fix: remove smooth scroll for logs --------- Co-authored-by: waterplea --- web/projects/ui/src/app/pages/init/init.service.ts | 4 ++-- web/projects/ui/src/app/pages/init/logs/logs.component.ts | 6 +++--- web/projects/ui/src/app/pages/init/logs/logs.service.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/web/projects/ui/src/app/pages/init/init.service.ts b/web/projects/ui/src/app/pages/init/init.service.ts index 245e1ae24..96bf656d3 100644 --- a/web/projects/ui/src/app/pages/init/init.service.ts +++ b/web/projects/ui/src/app/pages/init/init.service.ts @@ -58,8 +58,8 @@ export class InitService extends Observable { } }), catchError(e => { - // @TODO this toast is presenting when we navigate away from init page. It seems other websockets exhibit the same behavior, but we never noticed because the error were not being caught and presented in this manner - this.errorService.handleError(e) + // @TODO Alex this toast is presenting when we navigate away from init page. It seems other websockets exhibit the same behavior, but we never noticed because the error were not being caught and presented in this manner. It seems odd that unsubscribing from a websocket subject would be treated as an error. + // this.errorService.handleError(e) return EMPTY }), diff --git a/web/projects/ui/src/app/pages/init/logs/logs.component.ts b/web/projects/ui/src/app/pages/init/logs/logs.component.ts index edce1f282..de05d33bd 100644 --- a/web/projects/ui/src/app/pages/init/logs/logs.component.ts +++ b/web/projects/ui/src/app/pages/init/logs/logs.component.ts @@ -24,10 +24,10 @@ export class LogsComponent { scroll = true scrollTo(bottom: HTMLElement) { - if (this.scroll) bottom.scrollIntoView({ behavior: 'smooth' }) + if (this.scroll) bottom.scrollIntoView() } - onBottom([{ isIntersecting }]: readonly IntersectionObserverEntry[]) { - this.scroll = isIntersecting + onBottom(entries: readonly IntersectionObserverEntry[]) { + this.scroll = entries[entries.length - 1].isIntersecting } } diff --git a/web/projects/ui/src/app/pages/init/logs/logs.service.ts b/web/projects/ui/src/app/pages/init/logs/logs.service.ts index 5fe550d45..2553e9872 100644 --- a/web/projects/ui/src/app/pages/init/logs/logs.service.ts +++ b/web/projects/ui/src/app/pages/init/logs/logs.service.ts @@ -39,7 +39,7 @@ export class LogsService extends Observable { this.api.initFollowLogs({ boot: 0 }), ).pipe( switchMap(({ guid }) => this.api.openWebsocket$(guid, {})), - bufferTime(250), + bufferTime(500), filter(logs => !!logs.length), map(convertAnsi), scan((logs: readonly string[], log) => [...logs, log], []), From 3eb0093d2a9bbcc7f05e5d4347c885b989123659 Mon Sep 17 00:00:00 2001 From: Jade <2364004+Blu-J@users.noreply.github.com> Date: Mon, 22 Jul 2024 11:40:12 -0600 Subject: [PATCH 063/125] feature: Adding in the stopping state (#2677) * feature: Adding in the stopping state * chore: Deal with timeout in the sigterm for main * chore: Update the timeout * Update web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.ts Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> * Update web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> --------- Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> --- .../Systems/SystemForEmbassy/MainLoop.ts | 5 +- .../Systems/SystemForEmbassy/index.ts | 26 +-- .../src/Adapters/Systems/SystemForStartOs.ts | 11 +- core/startos/src/service/mod.rs | 120 +------------ .../src/service/persistent_container.rs | 4 +- core/startos/src/service/service_actor.rs | 159 ++++++++++++++++++ core/startos/src/status/mod.rs | 9 +- sdk/lib/mainFn/CommandController.ts | 14 +- sdk/lib/mainFn/Daemon.ts | 1 + sdk/lib/mainFn/Daemons.ts | 2 + sdk/lib/mainFn/HealthDaemon.ts | 9 +- sdk/lib/mainFn/index.ts | 1 + sdk/lib/osBindings/MainStatus.ts | 3 +- sdk/lib/util/inMs.test.ts | 34 ++++ sdk/lib/util/inMs.ts | 31 ++++ sdk/lib/util/index.ts | 1 + .../app-list-pkg/app-list-pkg.component.ts | 4 +- .../app-show-status.component.ts | 4 +- 18 files changed, 282 insertions(+), 156 deletions(-) create mode 100644 core/startos/src/service/service_actor.rs create mode 100644 sdk/lib/util/inMs.test.ts create mode 100644 sdk/lib/util/inMs.ts diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts index 975bb52ca..f6e7614ed 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts @@ -2,7 +2,7 @@ import { polyfillEffects } from "./polyfillEffects" import { DockerProcedureContainer } from "./DockerProcedureContainer" import { SystemForEmbassy } from "." import { hostSystemStartOs } from "../../HostSystemStartOs" -import { Daemons, T, daemons } from "@start9labs/start-sdk" +import { Daemons, T, daemons, utils } from "@start9labs/start-sdk" import { Daemon } from "@start9labs/start-sdk/cjs/lib/mainFn/Daemon" import { Effects } from "../../../Models/Effects" @@ -58,6 +58,9 @@ export class MainLoop { currentCommand, { overlay: dockerProcedureContainer.overlay, + sigtermTimeout: utils.inMs( + this.system.manifest.main["sigterm-timeout"], + ), }, ) daemon.start() diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index 3a8d4f4b2..dd2ccbab3 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -481,20 +481,20 @@ export class SystemForEmbassy implements System { private async mainStop( effects: Effects, timeoutMs: number | null, - ): Promise { - const { currentRunning } = this - this.currentRunning?.clean() - delete this.currentRunning - if (currentRunning) { - await currentRunning.clean({ - timeout: fromDuration(this.manifest.main["sigterm-timeout"]), - }) + ): Promise { + try { + const { currentRunning } = this + this.currentRunning?.clean() + delete this.currentRunning + if (currentRunning) { + await currentRunning.clean({ + timeout: utils.inMs(this.manifest.main["sigterm-timeout"]), + }) + } + return + } finally { + await effects.setMainStatus({ status: "stopped" }) } - const durationValue = duration( - fromDuration(this.manifest.main["sigterm-timeout"]), - "s", - ) - return durationValue } private async createBackup( effects: Effects, diff --git a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts index 84bf57302..2c455dc42 100644 --- a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts +++ b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts @@ -139,10 +139,13 @@ export class SystemForStartOs implements System { return } case "/main/stop": { - if (this.onTerm) await this.onTerm() - await effects.setMainStatus({ status: "stopped" }) - delete this.onTerm - return duration(30, "s") + try { + if (this.onTerm) await this.onTerm() + delete this.onTerm + return + } finally { + await effects.setMainStatus({ status: "stopped" }) + } } case "/config/set": { const input = options.input as any // TODO diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index d78df6c79..9103b70a1 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -10,6 +10,7 @@ use models::{HealthCheckId, PackageId, ProcedureName}; use persistent_container::PersistentContainer; use rpc_toolkit::{from_fn_async, CallRemoteHandler, Empty, HandlerArgs, HandlerFor}; use serde::{Deserialize, Serialize}; +use service_actor::ServiceActor; use start_stop::StartStop; use tokio::sync::Notify; use ts_rs::TS; @@ -45,6 +46,7 @@ mod dependencies; pub mod persistent_container; mod properties; mod rpc; +mod service_actor; pub mod service_effect_handler; pub mod service_map; mod start_stop; @@ -482,124 +484,6 @@ impl ServiceActorSeed { }); } } -#[derive(Clone)] -struct ServiceActor(Arc); - -impl Actor for ServiceActor { - fn init(&mut self, jobs: &BackgroundJobQueue) { - let seed = self.0.clone(); - jobs.add_job(async move { - let id = seed.id.clone(); - let mut current = seed.persistent_container.state.subscribe(); - - loop { - let kinds = current.borrow().kinds(); - - if let Err(e) = async { - let main_status = match ( - kinds.transition_state, - kinds.desired_state, - kinds.running_status, - ) { - (Some(TransitionKind::Restarting), StartStop::Stop, Some(_)) => { - seed.persistent_container.stop().await?; - MainStatus::Restarting - } - (Some(TransitionKind::Restarting), StartStop::Start, _) => { - seed.persistent_container.start().await?; - MainStatus::Restarting - } - (Some(TransitionKind::Restarting), _, _) => MainStatus::Restarting, - (Some(TransitionKind::Restoring), _, _) => MainStatus::Restoring, - (Some(TransitionKind::BackingUp), StartStop::Stop, Some(status)) => { - seed.persistent_container.stop().await?; - MainStatus::BackingUp { - started: Some(status.started), - health: status.health.clone(), - } - } - (Some(TransitionKind::BackingUp), StartStop::Start, _) => { - seed.persistent_container.start().await?; - MainStatus::BackingUp { - started: None, - health: OrdMap::new(), - } - } - (Some(TransitionKind::BackingUp), _, _) => MainStatus::BackingUp { - started: None, - health: OrdMap::new(), - }, - (None, StartStop::Stop, None) => MainStatus::Stopped, - (None, StartStop::Stop, Some(_)) => MainStatus::Stopping { - timeout: seed.persistent_container.stop().await?.into(), - }, - (None, StartStop::Start, Some(status)) => MainStatus::Running { - started: status.started, - health: status.health.clone(), - }, - (None, StartStop::Start, None) => { - seed.persistent_container.start().await?; - MainStatus::Starting - } - }; - seed.ctx - .db - .mutate(|d| { - if let Some(i) = d.as_public_mut().as_package_data_mut().as_idx_mut(&id) - { - let previous = i.as_status().as_main().de()?; - let previous_health = previous.health(); - let previous_started = previous.started(); - let mut main_status = main_status; - match &mut main_status { - &mut MainStatus::Running { ref mut health, .. } - | &mut MainStatus::BackingUp { ref mut health, .. } => { - *health = previous_health.unwrap_or(health).clone(); - } - _ => (), - }; - match &mut main_status { - MainStatus::Running { - ref mut started, .. - } => { - *started = previous_started.unwrap_or(*started); - } - MainStatus::BackingUp { - ref mut started, .. - } => { - *started = previous_started.map(Some).unwrap_or(*started); - } - _ => (), - }; - i.as_status_mut().as_main_mut().ser(&main_status)?; - } - Ok(()) - }) - .await?; - - Ok::<_, Error>(()) - } - .await - { - tracing::error!("error synchronizing state of service: {e}"); - tracing::debug!("{e:?}"); - - seed.synchronized.notify_waiters(); - - tracing::error!("Retrying in {}s...", SYNC_RETRY_COOLDOWN_SECONDS); - tokio::time::sleep(Duration::from_secs(SYNC_RETRY_COOLDOWN_SECONDS)).await; - continue; - } - - seed.synchronized.notify_waiters(); - - tokio::select! { - _ = current.changed() => (), - } - } - }) - } -} #[derive(Deserialize, Serialize, Parser, TS)] pub struct ConnectParams { diff --git a/core/startos/src/service/persistent_container.rs b/core/startos/src/service/persistent_container.rs index 11cc237d3..e8c504a92 100644 --- a/core/startos/src/service/persistent_container.rs +++ b/core/startos/src/service/persistent_container.rs @@ -441,11 +441,11 @@ impl PersistentContainer { } #[instrument(skip_all)] - pub async fn stop(&self) -> Result { + pub async fn stop(&self) -> Result<(), Error> { let timeout: Option = self .execute(Guid::new(), ProcedureName::StopMain, Value::Null, None) .await?; - Ok(timeout.map(|a| *a).unwrap_or(Duration::from_secs(30))) + Ok(()) } #[instrument(skip_all)] diff --git a/core/startos/src/service/service_actor.rs b/core/startos/src/service/service_actor.rs new file mode 100644 index 000000000..e6e1c4ede --- /dev/null +++ b/core/startos/src/service/service_actor.rs @@ -0,0 +1,159 @@ +use std::sync::Arc; +use std::time::Duration; + +use imbl::OrdMap; +use models::PackageId; + +use super::start_stop::StartStop; + +use crate::prelude::*; +use crate::service::transition::TransitionKind; +use crate::service::SYNC_RETRY_COOLDOWN_SECONDS; +use crate::status::MainStatus; +use crate::util::actor::background::BackgroundJobQueue; +use crate::util::actor::Actor; + +use super::ServiceActorSeed; + +#[derive(Clone)] +pub(super) struct ServiceActor(pub(super) Arc); + +enum ServiceActorLoopNext { + Wait, + DontWait, +} + +impl Actor for ServiceActor { + fn init(&mut self, jobs: &BackgroundJobQueue) { + let seed = self.0.clone(); + jobs.add_job(async move { + let id = seed.id.clone(); + let mut current = seed.persistent_container.state.subscribe(); + + loop { + match service_actor_loop(¤t, &seed, &id).await { + ServiceActorLoopNext::Wait => tokio::select! { + _ = current.changed() => (), + }, + ServiceActorLoopNext::DontWait => (), + } + } + }) + } +} + +async fn service_actor_loop( + current: &tokio::sync::watch::Receiver, + seed: &Arc, + id: &PackageId, +) -> ServiceActorLoopNext { + let kinds = current.borrow().kinds(); + if let Err(e) = async { + let main_status = match ( + kinds.transition_state, + kinds.desired_state, + kinds.running_status, + ) { + (Some(TransitionKind::Restarting), StartStop::Stop, Some(_)) => { + seed.persistent_container.stop().await?; + MainStatus::Restarting + } + (Some(TransitionKind::Restarting), StartStop::Start, _) => { + seed.persistent_container.start().await?; + MainStatus::Restarting + } + (Some(TransitionKind::Restarting), _, _) => MainStatus::Restarting, + (Some(TransitionKind::Restoring), _, _) => MainStatus::Restoring, + (Some(TransitionKind::BackingUp), StartStop::Stop, Some(status)) => { + seed.persistent_container.stop().await?; + MainStatus::BackingUp { + started: Some(status.started), + health: status.health.clone(), + } + } + (Some(TransitionKind::BackingUp), StartStop::Start, _) => { + seed.persistent_container.start().await?; + MainStatus::BackingUp { + started: None, + health: OrdMap::new(), + } + } + (Some(TransitionKind::BackingUp), _, _) => MainStatus::BackingUp { + started: None, + health: OrdMap::new(), + }, + (None, StartStop::Stop, None) => MainStatus::Stopped, + (None, StartStop::Stop, Some(_)) => { + let task_seed = seed.clone(); + seed.ctx + .db + .mutate(|d| { + if let Some(i) = d.as_public_mut().as_package_data_mut().as_idx_mut(&id) { + i.as_status_mut().as_main_mut().ser(&MainStatus::Stopping)?; + } + Ok(()) + }) + .await?; + task_seed.persistent_container.stop().await?; + MainStatus::Stopped + } + (None, StartStop::Start, Some(status)) => MainStatus::Running { + started: status.started, + health: status.health.clone(), + }, + (None, StartStop::Start, None) => { + seed.persistent_container.start().await?; + MainStatus::Starting + } + }; + seed.ctx + .db + .mutate(|d| { + if let Some(i) = d.as_public_mut().as_package_data_mut().as_idx_mut(&id) { + let previous = i.as_status().as_main().de()?; + let previous_health = previous.health(); + let previous_started = previous.started(); + let mut main_status = main_status; + match &mut main_status { + &mut MainStatus::Running { ref mut health, .. } + | &mut MainStatus::BackingUp { ref mut health, .. } => { + *health = previous_health.unwrap_or(health).clone(); + } + _ => (), + }; + match &mut main_status { + MainStatus::Running { + ref mut started, .. + } => { + *started = previous_started.unwrap_or(*started); + } + MainStatus::BackingUp { + ref mut started, .. + } => { + *started = previous_started.map(Some).unwrap_or(*started); + } + _ => (), + }; + i.as_status_mut().as_main_mut().ser(&main_status)?; + } + Ok(()) + }) + .await?; + + Ok::<_, Error>(()) + } + .await + { + tracing::error!("error synchronizing state of service: {e}"); + tracing::debug!("{e:?}"); + + seed.synchronized.notify_waiters(); + + tracing::error!("Retrying in {}s...", SYNC_RETRY_COOLDOWN_SECONDS); + tokio::time::sleep(Duration::from_secs(SYNC_RETRY_COOLDOWN_SECONDS)).await; + return ServiceActorLoopNext::DontWait; + } + seed.synchronized.notify_waiters(); + + ServiceActorLoopNext::Wait +} diff --git a/core/startos/src/status/mod.rs b/core/startos/src/status/mod.rs index 520fe5089..c1d3a36ad 100644 --- a/core/startos/src/status/mod.rs +++ b/core/startos/src/status/mod.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::{collections::BTreeMap, sync::Arc}; use chrono::{DateTime, Utc}; use imbl::OrdMap; @@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize}; use ts_rs::TS; use self::health_check::HealthCheckId; -use crate::prelude::*; use crate::status::health_check::HealthCheckResult; +use crate::{prelude::*, util::GeneralGuard}; pub mod health_check; #[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS)] @@ -26,10 +26,7 @@ pub enum MainStatus { Stopped, Restarting, Restoring, - #[serde(rename_all = "camelCase")] - Stopping { - timeout: crate::util::serde::Duration, - }, + Stopping, Starting, #[serde(rename_all = "camelCase")] Running { diff --git a/sdk/lib/mainFn/CommandController.ts b/sdk/lib/mainFn/CommandController.ts index 264574f7c..3af01e915 100644 --- a/sdk/lib/mainFn/CommandController.ts +++ b/sdk/lib/mainFn/CommandController.ts @@ -1,3 +1,4 @@ +import { DEFAULT_SIGTERM_TIMEOUT } from "." import { NO_TIMEOUT, SIGKILL, SIGTERM } from "../StartSdk" import { SDKManifest } from "../manifest/ManifestTypes" import { Effects, ImageId, ValidIfNoStupidEscape } from "../types" @@ -10,6 +11,7 @@ export class CommandController { readonly runningAnswer: Promise, readonly overlay: Overlay, readonly pid: number | undefined, + readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT, ) {} static of() { return async ( @@ -20,6 +22,8 @@ export class CommandController { }, command: ValidIfNoStupidEscape | [string, ...string[]], options: { + // Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms + sigtermTimeout?: number mounts?: { path: string; options: MountOptions }[] overlay?: Overlay env?: @@ -67,10 +71,14 @@ export class CommandController { const pid = childProcess.pid - return new CommandController(answer, overlay, pid) + return new CommandController(answer, overlay, pid, options.sigtermTimeout) } } - async wait() { + async wait(timeout: number = NO_TIMEOUT) { + if (timeout > 0) + setTimeout(() => { + this.term() + }, timeout) try { return await this.runningAnswer } finally { @@ -82,7 +90,7 @@ export class CommandController { await this.overlay.destroy().catch((_) => {}) } } - async term({ signal = SIGTERM, timeout = NO_TIMEOUT } = {}) { + async term({ signal = SIGTERM, timeout = this.sigtermTimeout } = {}) { if (this.pid === undefined) return try { await cpExecFile("pkill", [ diff --git a/sdk/lib/mainFn/Daemon.ts b/sdk/lib/mainFn/Daemon.ts index c48865f94..90882418c 100644 --- a/sdk/lib/mainFn/Daemon.ts +++ b/sdk/lib/mainFn/Daemon.ts @@ -34,6 +34,7 @@ export class Daemon { user?: string | undefined onStdout?: (x: Buffer) => void onStderr?: (x: Buffer) => void + sigtermTimeout?: number }, ) => { const startCommand = () => diff --git a/sdk/lib/mainFn/Daemons.ts b/sdk/lib/mainFn/Daemons.ts index 059d148ab..fbda547aa 100644 --- a/sdk/lib/mainFn/Daemons.ts +++ b/sdk/lib/mainFn/Daemons.ts @@ -44,6 +44,7 @@ type DaemonsParams< env?: Record ready: Ready requires: Exclude[] + sigtermTimeout?: number } type ErrorDuplicateId = `The id '${Id}' is already used` @@ -136,6 +137,7 @@ export class Daemons { this.ids, options.ready, this.effects, + options.sigtermTimeout, ) const daemons = this.daemons.concat(daemon) const ids = [...this.ids, id] as (Ids | Id)[] diff --git a/sdk/lib/mainFn/HealthDaemon.ts b/sdk/lib/mainFn/HealthDaemon.ts index 48f3fab55..84865e59b 100644 --- a/sdk/lib/mainFn/HealthDaemon.ts +++ b/sdk/lib/mainFn/HealthDaemon.ts @@ -3,6 +3,7 @@ import { defaultTrigger } from "../trigger/defaultTrigger" import { Ready } from "./Daemons" import { Daemon } from "./Daemon" import { Effects } from "../types" +import { DEFAULT_SIGTERM_TIMEOUT } from "." const oncePromise = () => { let resolve: (value: T) => void @@ -32,6 +33,7 @@ export class HealthDaemon { readonly ids: string[], readonly ready: Ready, readonly effects: Effects, + readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT, ) { this.updateStatus() this.dependencies.forEach((d) => d.addWatcher(() => this.updateStatus())) @@ -46,7 +48,12 @@ export class HealthDaemon { this.#running = false this.#healthCheckCleanup?.() - await this.daemon.then((d) => d.stop(termOptions)) + await this.daemon.then((d) => + d.stop({ + timeout: this.sigtermTimeout, + ...termOptions, + }), + ) } /** Want to add another notifier that the health might have changed */ diff --git a/sdk/lib/mainFn/index.ts b/sdk/lib/mainFn/index.ts index 3da57d32f..c4f74764c 100644 --- a/sdk/lib/mainFn/index.ts +++ b/sdk/lib/mainFn/index.ts @@ -7,6 +7,7 @@ import "./Daemons" import { SDKManifest } from "../manifest/ManifestTypes" import { MainEffects } from "../StartSdk" +export const DEFAULT_SIGTERM_TIMEOUT = 30_000 /** * Used to ensure that the main function is running with the valid proofs. * We first do the folowing order of things diff --git a/sdk/lib/osBindings/MainStatus.ts b/sdk/lib/osBindings/MainStatus.ts index 1b9d8482e..6acdce14a 100644 --- a/sdk/lib/osBindings/MainStatus.ts +++ b/sdk/lib/osBindings/MainStatus.ts @@ -1,5 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Duration } from "./Duration" import type { HealthCheckId } from "./HealthCheckId" import type { HealthCheckResult } from "./HealthCheckResult" @@ -7,7 +6,7 @@ export type MainStatus = | { status: "stopped" } | { status: "restarting" } | { status: "restoring" } - | { status: "stopping"; timeout: Duration } + | { status: "stopping" } | { status: "starting" } | { status: "running" diff --git a/sdk/lib/util/inMs.test.ts b/sdk/lib/util/inMs.test.ts new file mode 100644 index 000000000..fbf71bf2c --- /dev/null +++ b/sdk/lib/util/inMs.test.ts @@ -0,0 +1,34 @@ +import { inMs } from "./inMs" + +describe("inMs", () => { + test("28.001s", () => { + expect(inMs("28.001s")).toBe(28001) + }) + test("28.123s", () => { + expect(inMs("28.123s")).toBe(28123) + }) + test(".123s", () => { + expect(inMs(".123s")).toBe(123) + }) + test("123ms", () => { + expect(inMs("123ms")).toBe(123) + }) + test("1h", () => { + expect(inMs("1h")).toBe(3600000) + }) + test("1m", () => { + expect(inMs("1m")).toBe(60000) + }) + test("1m", () => { + expect(inMs("1d")).toBe(1000 * 60 * 60 * 24) + }) + test("123", () => { + expect(() => inMs("123")).toThrowError("Invalid time format: 123") + }) + test("123 as number", () => { + expect(inMs(123)).toBe(123) + }) + test.only("undefined", () => { + expect(inMs(undefined)).toBe(undefined) + }) +}) diff --git a/sdk/lib/util/inMs.ts b/sdk/lib/util/inMs.ts new file mode 100644 index 000000000..547fb8bea --- /dev/null +++ b/sdk/lib/util/inMs.ts @@ -0,0 +1,31 @@ +import { DEFAULT_SIGTERM_TIMEOUT } from "../mainFn" + +const matchTimeRegex = /^\s*(\d+)?(\.\d+)?\s*(ms|s|m|h|d)/ + +const unitMultiplier = (unit?: string) => { + if (!unit) return 1 + if (unit === "ms") return 1 + if (unit === "s") return 1000 + if (unit === "m") return 1000 * 60 + if (unit === "h") return 1000 * 60 * 60 + if (unit === "d") return 1000 * 60 * 60 * 24 + throw new Error(`Invalid unit: ${unit}`) +} +const digitsMs = (digits: string | null, multiplier: number) => { + if (!digits) return 0 + const value = parseInt(digits.slice(1)) + const divideBy = multiplier / Math.pow(10, digits.length - 1) + return Math.round(value * divideBy) +} +export const inMs = (time?: string | number) => { + if (typeof time === "number") return time + if (!time) return undefined + const matches = time.match(matchTimeRegex) + if (!matches) throw new Error(`Invalid time format: ${time}`) + const [_, leftHandSide, digits, unit] = matches + const multiplier = unitMultiplier(unit) + const firstValue = parseInt(leftHandSide || "0") * multiplier + const secondValue = digitsMs(digits, multiplier) + + return firstValue + secondValue +} diff --git a/sdk/lib/util/index.ts b/sdk/lib/util/index.ts index 63629dfaa..d4427adb0 100644 --- a/sdk/lib/util/index.ts +++ b/sdk/lib/util/index.ts @@ -12,3 +12,4 @@ export { addressHostToUrl } from "./getServiceInterface" export { hostnameInfoToAddress } from "./Hostname" export * from "./typeHelpers" export { getDefaultString } from "./getDefaultString" +export { inMs } from "./inMs" diff --git a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.ts b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.ts index 35bf99f8f..4df818956 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.ts @@ -24,9 +24,7 @@ export class AppListPkgComponent { } get sigtermTimeout(): string | null { - return this.pkgMainStatus.status === 'stopping' - ? this.pkgMainStatus.timeout - : null + return this.pkgMainStatus.status === 'stopping' ? '30s' : null // @dr-bonez TODO } launchUi(e: Event, interfaces: PackageDataEntry['serviceInterfaces']): void { diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts index 30657ae01..186bbe39a 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts @@ -76,9 +76,7 @@ export class AppShowStatusComponent { } get sigtermTimeout(): string | null { - return this.pkgStatus?.main.status === 'stopping' - ? this.pkgStatus.main.timeout - : null + return this.pkgStatus?.main.status === 'stopping' ? '30s' : null // @dr-bonez TODO } launchUi(interfaces: PackageDataEntry['serviceInterfaces']): void { From a535fc17c320bfa778315eabbe670ee4c0a8f72e Mon Sep 17 00:00:00 2001 From: Lucy <12953208+elvece@users.noreply.github.com> Date: Mon, 22 Jul 2024 20:48:12 -0400 Subject: [PATCH 064/125] Feature/fe new registry (#2647) * bugfixes * update fe types * implement new registry types in marketplace and ui * fix marketplace types to have default params * add alt implementation toggle * merge cleanup * more cleanup and notes * fix build * cleanup sync with next/minor * add exver JS parser * parse ValidExVer to string * update types to interface * add VersionRange and comparative functions * Parse ExtendedVersion from string * add conjunction, disjunction, and inversion logic * consider flavor in satisfiedBy fn * consider prerelease for ordering * add compare fn for sorting * rename fns for consistency * refactoring * update compare fn to return null if flavors don't match * begin simplifying dependencies * under construction * wip * add dependency metadata to CurrentDependencyInfo * ditch inheritance for recursive VersionRange constructor. Recursive 'satisfiedBy' fn wip * preprocess manifest * misc fixes * use sdk version as osVersion in manifest * chore: Change the type to just validate and not generate all solutions. * add publishedAt * fix pegjs exports * integrate exver into sdk * misc fixes * complete satisfiedBy fn * refactor - use greaterThanOrEqual and lessThanOrEqual fns * fix tests * update dependency details * update types * remove interim types * rename alt implementation to flavor * cleanup os update * format exver.ts * add s9pk parsing endpoints * fix build * update to exver * exver and bug fixes * update static endpoints + cleanup * cleanup * update static proxy verification * make mocks more robust; fix dep icon fallback; cleanup * refactor alert versions and update fixtures * registry bugfixes * misc fixes * cleanup unused * convert patchdb ui seed to camelCase * update otherVersions type * change otherVersions: null to 'none' * refactor and complete feature * improve static endpoints * fix install params * mask systemd-networkd-wait-online * fix static file fetching * include non-matching versions in otherVersions * convert release notes to modal and clean up displayExver * alert for no other versions * Fix ack-instructions casing * fix indeterminate loader on service install --------- Co-authored-by: Aiden McClelland Co-authored-by: Shadowy Super Coder Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Co-authored-by: J H Co-authored-by: Matt Hill --- .../Systems/SystemForEmbassy/index.ts | 20 +- core/Cargo.lock | 51 +- core/startos/Cargo.toml | 2 + core/startos/src/backup/target/mod.rs | 3 +- core/startos/src/bins/start_init.rs | 85 +- core/startos/src/context/init.rs | 3 + core/startos/src/db/model/package.rs | 9 +- core/startos/src/dependencies.rs | 13 + core/startos/src/init.rs | 55 +- core/startos/src/install/mod.rs | 1 + core/startos/src/middleware/db.rs | 1 - core/startos/src/net/static_server.rs | 459 ++- core/startos/src/registry/admin.rs | 25 +- core/startos/src/registry/asset.rs | 3 + core/startos/src/registry/auth.rs | 12 +- core/startos/src/registry/context.rs | 30 +- core/startos/src/registry/device_info.rs | 8 +- core/startos/src/registry/mod.rs | 29 +- core/startos/src/registry/os/asset/add.rs | 10 +- core/startos/src/registry/os/asset/sign.rs | 6 +- core/startos/src/registry/os/version/mod.rs | 8 +- core/startos/src/registry/package/add.rs | 20 +- core/startos/src/registry/package/get.rs | 81 +- core/startos/src/registry/package/index.rs | 46 +- core/startos/src/registry/package/mod.rs | 11 +- .../signer/commitment/merkle_archive.rs | 29 + .../src/registry/signer/commitment/request.rs | 5 +- .../src/s9pk/merkle_archive/expected.rs | 48 +- core/startos/src/s9pk/merkle_archive/mod.rs | 4 + .../src/s9pk/merkle_archive/source/mod.rs | 52 +- core/startos/src/s9pk/rpc.rs | 36 + core/startos/src/s9pk/v2/compat.rs | 6 +- core/startos/src/s9pk/v2/manifest.rs | 15 +- core/startos/src/s9pk/v2/mod.rs | 108 +- core/startos/src/s9pk/v2/pack.rs | 224 +- .../src/service/service_effect_handler.rs | 156 +- core/startos/src/util/mod.rs | 51 + core/startos/src/util/net.rs | 51 +- core/startos/src/util/rpc.rs | 31 +- core/startos/src/util/serde.rs | 18 +- debian/postinst | 2 +- sdk/.prettierignore | 1 + sdk/Makefile | 5 +- sdk/jest.config.js | 2 +- sdk/lib/Dependency.ts | 6 +- sdk/lib/StartSdk.ts | 29 +- sdk/lib/actions/createAction.ts | 9 +- sdk/lib/actions/setupActions.ts | 4 +- sdk/lib/backup/Backups.ts | 10 +- sdk/lib/backup/setupBackups.ts | 16 +- sdk/lib/config/configDependencies.ts | 15 +- sdk/lib/config/setupConfig.ts | 16 +- sdk/lib/dependencies/DependencyConfig.ts | 17 +- sdk/lib/dependencies/dependencies.ts | 44 +- sdk/lib/dependencies/setupDependencyConfig.ts | 8 +- sdk/lib/emverLite/mod.ts | 323 --- sdk/lib/exver/exver.pegjs | 99 + sdk/lib/exver/exver.ts | 2507 +++++++++++++++++ sdk/lib/exver/index.ts | 443 +++ sdk/lib/health/HealthCheck.ts | 9 +- sdk/lib/index.browser.ts | 6 +- sdk/lib/index.ts | 2 +- sdk/lib/inits/migrations/Migration.ts | 28 +- sdk/lib/inits/migrations/setupMigrations.ts | 38 +- sdk/lib/inits/setupInit.ts | 12 +- sdk/lib/inits/setupInstall.ts | 15 +- sdk/lib/inits/setupUninstall.ts | 15 +- sdk/lib/interfaces/setupInterfaces.ts | 12 +- sdk/lib/mainFn/CommandController.ts | 12 +- sdk/lib/mainFn/Daemon.ts | 11 +- sdk/lib/mainFn/Daemons.ts | 25 +- sdk/lib/mainFn/Mounts.ts | 9 +- sdk/lib/mainFn/index.ts | 8 +- sdk/lib/manifest/ManifestTypes.ts | 74 +- sdk/lib/manifest/setupManifest.ts | 64 +- sdk/lib/osBindings/CheckDependenciesResult.ts | 4 +- sdk/lib/osBindings/CurrentDependencyInfo.ts | 7 +- sdk/lib/osBindings/DepInfo.ts | 7 +- sdk/lib/osBindings/DependencyMetadata.ts | 9 + sdk/lib/osBindings/DependencyRequirement.ts | 5 +- sdk/lib/osBindings/FullIndex.ts | 1 + ...VersionParams.ts => GetOsVersionParams.ts} | 2 +- sdk/lib/osBindings/GetPackageParams.ts | 2 +- sdk/lib/osBindings/HardwareRequirements.ts | 2 +- sdk/lib/osBindings/InstallParams.ts | 9 + sdk/lib/osBindings/Manifest.ts | 1 + sdk/lib/osBindings/PackageDetailLevel.ts | 2 +- sdk/lib/osBindings/PackageVersionInfo.ts | 6 + sdk/lib/osBindings/PathOrUrl.ts | 3 + sdk/lib/osBindings/RegistryAsset.ts | 1 + sdk/lib/osBindings/RegistryInfo.ts | 9 + sdk/lib/osBindings/index.ts | 6 +- sdk/lib/test/configBuilder.test.ts | 5 +- sdk/lib/test/emverList.test.ts | 253 -- sdk/lib/test/exverList.test.ts | 355 +++ sdk/lib/test/output.sdk.ts | 5 +- sdk/lib/test/setupDependencyConfig.test.ts | 2 +- sdk/lib/test/utils.splitCommand.test.ts | 42 - sdk/lib/types.ts | 24 +- sdk/lib/util/splitCommand.ts | 11 +- sdk/package-lock.json | 295 ++ sdk/package.json | 2 + web/angular.json | 3 +- web/package-lock.json | 4 + web/package.json | 1 + web/patchdb-ui-seed.json | 18 +- .../release-notes.component.html | 17 +- .../release-notes.component.scss | 1 + .../release-notes/release-notes.component.ts | 69 + .../release-notes/release-notes.module.ts | 7 +- .../list/categories/categories.component.html | 8 +- .../list/categories/categories.component.ts | 7 +- .../src/pages/list/item/item.component.html | 10 +- .../release-notes/release-notes.component.ts | 39 - .../src/pages/show/about/about.component.html | 12 +- .../src/pages/show/about/about.component.ts | 13 + .../src/pages/show/about/about.module.ts | 7 +- .../show/additional/additional.component.html | 20 +- .../show/additional/additional.component.ts | 81 +- .../dependencies/dependencies.component.html | 8 +- .../dependencies/dependencies.component.ts | 3 +- .../show/dependencies/dependencies.module.ts | 7 +- .../pages/show/flavors/flavors.component.html | 21 + .../pages/show/flavors/flavors.component.ts | 12 + .../src/pages/show/flavors/flavors.module.ts | 19 + .../pages/show/package/package.component.html | 8 +- .../src/pages/show/package/package.module.ts | 4 +- .../src/pipes/filter-packages.pipe.ts | 15 +- web/projects/marketplace/src/public-api.ts | 4 +- .../src/services/marketplace.service.ts | 15 +- web/projects/marketplace/src/types.ts | 72 +- .../assets/img/service-icons/fallback.png | Bin 0 -> 30439 bytes .../shared/src/pipes/emver/emver.module.ts | 12 - .../shared/src/pipes/emver/emver.pipe.ts | 52 - .../shared/src/pipes/exver/exver.module.ts | 8 + .../shared/src/pipes/exver/exver.pipe.ts | 35 + web/projects/shared/src/public-api.ts | 6 +- .../shared/src/services/emver.service.ts | 18 - .../shared/src/services/exver.service.ts | 43 + web/projects/shared/src/types/http.types.ts | 6 +- .../ui/src/app/app/menu/menu.component.ts | 8 +- .../backup-drives/backup.service.ts | 10 +- .../form/form-array/form-array.component.html | 2 +- .../form-datetime.component.html | 8 +- .../refresh-alert/refresh-alert.service.ts | 9 +- .../app-recover-select/to-options.pipe.ts | 10 +- .../app-actions/app-actions.page.ts | 2 +- .../app-list-pkg/app-list-pkg.component.html | 4 +- .../apps-routes/app-list/app-list.module.ts | 4 +- .../apps-routes/app-show/app-show.module.ts | 4 +- .../apps-routes/app-show/app-show.page.html | 7 +- .../apps-routes/app-show/app-show.page.ts | 51 +- .../app-show-additional.component.html | 32 +- .../app-show-additional.component.ts | 15 +- .../app-show-dependencies.component.html | 2 +- .../app-show-header.component.html | 2 +- .../app-show-progress.component.html | 10 +- .../app-show-progress.component.ts | 2 +- .../app-show/pipes/to-buttons.pipe.ts | 15 +- .../login/ca-wizard/ca-wizard.component.html | 5 +- .../marketplace-list.module.ts | 4 +- .../marketplace-list.page.html | 5 +- .../marketplace-list/marketplace-list.page.ts | 20 +- .../marketplace-routing.module.ts | 7 - .../marketplace-show-controls.component.html | 24 +- .../marketplace-show-controls.component.ts | 17 +- .../marketplace-show-dependent.component.html | 8 +- .../marketplace-show-dependent.component.ts | 4 +- .../marketplace-show.module.ts | 6 +- .../marketplace-show.page.html | 22 +- .../marketplace-show/marketplace-show.page.ts | 68 +- .../marketplace-status.component.html | 6 +- .../marketplace-status.component.ts | 15 +- .../marketplace-status.module.ts | 4 +- .../app/pages/server-routes/lan/lan.page.html | 2 +- .../server-specs/server-specs.module.ts | 4 +- .../server-specs/server-specs.page.html | 2 +- .../server-routes/sideload/sideload.module.ts | 4 +- .../server-routes/sideload/sideload.page.html | 2 +- .../src/app/pages/updates/updates.module.ts | 4 +- .../src/app/pages/updates/updates.page.html | 30 +- .../ui/src/app/pages/updates/updates.page.ts | 34 +- .../ui/src/app/services/api/api-icons.ts | 10 +- .../ui/src/app/services/api/api.fixures.ts | 660 ++++- .../ui/src/app/services/api/api.types.ts | 27 +- .../app/services/api/embassy-api.service.ts | 38 +- .../services/api/embassy-live-api.service.ts | 137 +- .../services/api/embassy-mock-api.service.ts | 96 +- .../ui/src/app/services/api/mock-patch.ts | 10 +- .../ui/src/app/services/dep-error.service.ts | 20 +- .../ui/src/app/services/eos.service.ts | 6 +- .../src/app/services/marketplace.service.ts | 171 +- .../src/app/services/patch-db/data-model.ts | 4 + web/projects/ui/src/app/util/dry-update.ts | 7 +- web/projects/ui/src/manifest.webmanifest | 4 +- web/projects/ui/src/polyfills.ts | 4 +- 196 files changed, 7002 insertions(+), 2162 deletions(-) create mode 100644 sdk/.prettierignore delete mode 100644 sdk/lib/emverLite/mod.ts create mode 100644 sdk/lib/exver/exver.pegjs create mode 100644 sdk/lib/exver/exver.ts create mode 100644 sdk/lib/exver/index.ts create mode 100644 sdk/lib/osBindings/DependencyMetadata.ts rename sdk/lib/osBindings/{GetVersionParams.ts => GetOsVersionParams.ts} (85%) create mode 100644 sdk/lib/osBindings/InstallParams.ts create mode 100644 sdk/lib/osBindings/PathOrUrl.ts create mode 100644 sdk/lib/osBindings/RegistryInfo.ts delete mode 100644 sdk/lib/test/emverList.test.ts create mode 100644 sdk/lib/test/exverList.test.ts delete mode 100644 sdk/lib/test/utils.splitCommand.test.ts rename web/projects/marketplace/src/{pages => modals}/release-notes/release-notes.component.html (63%) rename web/projects/marketplace/src/{pages => modals}/release-notes/release-notes.component.scss (91%) create mode 100644 web/projects/marketplace/src/modals/release-notes/release-notes.component.ts rename web/projects/marketplace/src/{pages => modals}/release-notes/release-notes.module.ts (86%) delete mode 100644 web/projects/marketplace/src/pages/release-notes/release-notes.component.ts create mode 100644 web/projects/marketplace/src/pages/show/flavors/flavors.component.html create mode 100644 web/projects/marketplace/src/pages/show/flavors/flavors.component.ts create mode 100644 web/projects/marketplace/src/pages/show/flavors/flavors.module.ts create mode 100644 web/projects/shared/assets/img/service-icons/fallback.png delete mode 100644 web/projects/shared/src/pipes/emver/emver.module.ts delete mode 100644 web/projects/shared/src/pipes/emver/emver.pipe.ts create mode 100644 web/projects/shared/src/pipes/exver/exver.module.ts create mode 100644 web/projects/shared/src/pipes/exver/exver.pipe.ts delete mode 100644 web/projects/shared/src/services/emver.service.ts create mode 100644 web/projects/shared/src/services/exver.service.ts diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index dd2ccbab3..be8a4d163 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -1,4 +1,4 @@ -import { types as T, utils, EmVer } from "@start9labs/start-sdk" +import { ExtendedVersion, types as T, utils } from "@start9labs/start-sdk" import * as fs from "fs/promises" import { polyfillEffects } from "./polyfillEffects" @@ -647,13 +647,13 @@ export class SystemForEmbassy implements System { dependencies: Object.entries(dependsOn).flatMap(([key, value]) => { const dependency = this.manifest.dependencies?.[key] if (!dependency) return [] - const versionSpec = dependency.version + const versionRange = dependency.version const registryUrl = DEFAULT_REGISTRY const kind = "running" return [ { id: key, - versionSpec, + versionRange, registryUrl, kind, healthChecks: [...value], @@ -668,18 +668,24 @@ export class SystemForEmbassy implements System { fromVersion: string, timeoutMs: number | null, ): Promise { - const fromEmver = EmVer.from(fromVersion) - const currentEmver = EmVer.from(this.manifest.version) + const fromEmver = ExtendedVersion.parseEmver(fromVersion) + const currentEmver = ExtendedVersion.parseEmver(this.manifest.version) if (!this.manifest.migrations) return { configured: true } const fromMigration = Object.entries(this.manifest.migrations.from) - .map(([version, procedure]) => [EmVer.from(version), procedure] as const) + .map( + ([version, procedure]) => + [ExtendedVersion.parseEmver(version), procedure] as const, + ) .find( ([versionEmver, procedure]) => versionEmver.greaterThan(fromEmver) && versionEmver.lessThanOrEqual(currentEmver), ) const toMigration = Object.entries(this.manifest.migrations.to) - .map(([version, procedure]) => [EmVer.from(version), procedure] as const) + .map( + ([version, procedure]) => + [ExtendedVersion.parseEmver(version), procedure] as const, + ) .find( ([versionEmver, procedure]) => versionEmver.greaterThan(fromEmver) && diff --git a/core/Cargo.lock b/core/Cargo.lock index 1072f4203..e3576a8d8 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -471,9 +471,9 @@ checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" [[package]] name = "base32" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1ce0365f4d5fb6646220bb52fe547afd51796d90f914d4063cb0b032ebee088" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" [[package]] name = "base64" @@ -4632,6 +4632,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + [[package]] name = "socket2" version = "0.5.7" @@ -4953,7 +4959,7 @@ dependencies = [ "axum-server", "backhand", "barrage", - "base32 0.5.0", + "base32 0.5.1", "base64 0.22.1", "base64ct", "basic-cookies", @@ -4975,6 +4981,7 @@ dependencies = [ "ed25519-dalek 2.1.1", "exver", "fd-lock-rs", + "form_urlencoded", "futures", "gpt", "helpers", @@ -5044,6 +5051,7 @@ dependencies = [ "sscanf", "ssh-key", "tar", + "textwrap", "thiserror", "tokio", "tokio-rustls", @@ -5052,7 +5060,7 @@ dependencies = [ "tokio-tar", "tokio-tungstenite 0.23.1", "tokio-util", - "toml 0.8.14", + "toml 0.8.15", "torut", "tower-service", "tracing", @@ -5221,6 +5229,17 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thingbuf" version = "0.1.6" @@ -5233,18 +5252,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.62" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.62" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", @@ -5480,14 +5499,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.14" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" +checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.15", + "toml_edit 0.22.16", ] [[package]] @@ -5525,9 +5544,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.15" +version = "0.22.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59a3a72298453f564e2b111fa896f8d07fabb36f51f06d7e875fc5e0b5a3ef1" +checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788" dependencies = [ "indexmap 2.2.6", "serde", @@ -5888,6 +5907,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + [[package]] name = "unicode-normalization" version = "0.1.23" diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index 2695c7813..a531477b2 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -90,6 +90,7 @@ exver = { version = "0.2.0", git = "https://github.com/Start9Labs/exver-rs.git", "serde", ] } fd-lock-rs = "0.1.4" +form_urlencoded = "1.2.1" futures = "0.3.28" gpt = "3.1.0" helpers = { path = "../helpers" } @@ -171,6 +172,7 @@ sscanf = "0.4.1" ssh-key = { version = "0.6.2", features = ["ed25519"] } tar = "0.4.40" thiserror = "1.0.49" +textwrap = "0.16.1" tokio = { version = "1.38.1", features = ["full"] } tokio-rustls = "0.26.0" tokio-socks = "0.5.1" diff --git a/core/startos/src/backup/target/mod.rs b/core/startos/src/backup/target/mod.rs index f3b6b5f5c..032f70848 100644 --- a/core/startos/src/backup/target/mod.rs +++ b/core/startos/src/backup/target/mod.rs @@ -8,6 +8,7 @@ use color_eyre::eyre::eyre; use digest::generic_array::GenericArray; use digest::OutputSizeUser; use exver::Version; +use imbl_value::InternedString; use models::PackageId; use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; @@ -213,7 +214,7 @@ pub struct BackupInfo { #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct PackageBackupInfo { - pub title: String, + pub title: InternedString, pub version: VersionString, pub os_version: Version, pub timestamp: DateTime, diff --git a/core/startos/src/bins/start_init.rs b/core/startos/src/bins/start_init.rs index f4aa411b5..394d42c8d 100644 --- a/core/startos/src/bins/start_init.rs +++ b/core/startos/src/bins/start_init.rs @@ -141,6 +141,7 @@ async fn setup_or_init( } else { let init_ctx = InitContext::init(config).await?; let handle = init_ctx.progress.clone(); + let err_channel = init_ctx.error.clone(); let mut disk_phase = handle.add_phase("Opening data drive".into(), Some(10)); let init_phases = InitPhases::new(&handle); @@ -148,47 +149,55 @@ async fn setup_or_init( server.serve_init(init_ctx); - disk_phase.start(); - let guid_string = tokio::fs::read_to_string("/media/startos/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy + async { + disk_phase.start(); + let guid_string = tokio::fs::read_to_string("/media/startos/config/disk.guid") // unique identifier for volume group - keeps track of the disk that goes with your embassy + .await?; + let disk_guid = Arc::new(String::from(guid_string.trim())); + let requires_reboot = crate::disk::main::import( + &**disk_guid, + config.datadir(), + if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() { + RepairStrategy::Aggressive + } else { + RepairStrategy::Preen + }, + if disk_guid.ends_with("_UNENC") { + None + } else { + Some(DEFAULT_PASSWORD) + }, + ) .await?; - let disk_guid = Arc::new(String::from(guid_string.trim())); - let requires_reboot = crate::disk::main::import( - &**disk_guid, - config.datadir(), if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() { - RepairStrategy::Aggressive - } else { - RepairStrategy::Preen - }, - if disk_guid.ends_with("_UNENC") { - None - } else { - Some(DEFAULT_PASSWORD) - }, - ) - .await?; - if tokio::fs::metadata(REPAIR_DISK_PATH).await.is_ok() { - tokio::fs::remove_file(REPAIR_DISK_PATH) - .await - .with_ctx(|_| (crate::ErrorKind::Filesystem, REPAIR_DISK_PATH))?; + tokio::fs::remove_file(REPAIR_DISK_PATH) + .await + .with_ctx(|_| (crate::ErrorKind::Filesystem, REPAIR_DISK_PATH))?; + } + disk_phase.complete(); + tracing::info!("Loaded Disk"); + + if requires_reboot.0 { + let mut reboot_phase = handle.add_phase("Rebooting".into(), Some(1)); + reboot_phase.start(); + return Ok(Err(Shutdown { + export_args: Some((disk_guid, config.datadir().to_owned())), + restart: true, + })); + } + + let InitResult { net_ctrl } = crate::init::init(config, init_phases).await?; + + let rpc_ctx = + RpcContext::init(config, disk_guid, Some(net_ctrl), rpc_ctx_phases).await?; + + Ok::<_, Error>(Ok((rpc_ctx, handle))) } - disk_phase.complete(); - tracing::info!("Loaded Disk"); - - if requires_reboot.0 { - let mut reboot_phase = handle.add_phase("Rebooting".into(), Some(1)); - reboot_phase.start(); - return Ok(Err(Shutdown { - export_args: Some((disk_guid, config.datadir().to_owned())), - restart: true, - })); - } - - let InitResult { net_ctrl } = crate::init::init(config, init_phases).await?; - - let rpc_ctx = RpcContext::init(config, disk_guid, Some(net_ctrl), rpc_ctx_phases).await?; - - Ok(Ok((rpc_ctx, handle))) + .await + .map_err(|e| { + err_channel.send_replace(Some(e.clone_output())); + e + }) } } diff --git a/core/startos/src/context/init.rs b/core/startos/src/context/init.rs index f5f4a5430..566457a9c 100644 --- a/core/startos/src/context/init.rs +++ b/core/startos/src/context/init.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use rpc_toolkit::Context; use tokio::sync::broadcast::Sender; +use tokio::sync::watch; use tracing::instrument; use crate::context::config::ServerConfig; @@ -12,6 +13,7 @@ use crate::Error; pub struct InitContextSeed { pub config: ServerConfig, + pub error: watch::Sender>, pub progress: FullProgressTracker, pub shutdown: Sender<()>, pub rpc_continuations: RpcContinuations, @@ -25,6 +27,7 @@ impl InitContext { let (shutdown, _) = tokio::sync::broadcast::channel(1); Ok(Self(Arc::new(InitContextSeed { config: cfg.clone(), + error: watch::channel(None).0, progress: FullProgressTracker::new(), shutdown, rpc_continuations: RpcContinuations::new(), diff --git a/core/startos/src/db/model/package.rs b/core/startos/src/db/model/package.rs index 22d6440bb..e3b6e613b 100644 --- a/core/startos/src/db/model/package.rs +++ b/core/startos/src/db/model/package.rs @@ -372,14 +372,13 @@ impl Map for CurrentDependencies { #[derive(Clone, Debug, Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] pub struct CurrentDependencyInfo { + #[ts(type = "string | null")] + pub title: Option, + pub icon: Option>, #[serde(flatten)] pub kind: CurrentDependencyKind, - pub title: String, - pub icon: DataUrl<'static>, #[ts(type = "string")] - pub registry_url: Url, - #[ts(type = "string")] - pub version_spec: VersionRange, + pub version_range: VersionRange, pub config_satisfied: bool, } diff --git a/core/startos/src/dependencies.rs b/core/startos/src/dependencies.rs index f6ccc53ad..54e38f299 100644 --- a/core/startos/src/dependencies.rs +++ b/core/startos/src/dependencies.rs @@ -2,18 +2,21 @@ use std::collections::BTreeMap; use std::time::Duration; use clap::Parser; +use imbl_value::InternedString; use models::PackageId; use patch_db::json_patch::merge; use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use tracing::instrument; use ts_rs::TS; +use url::Url; use crate::config::{Config, ConfigSpec, ConfigureContext}; use crate::context::RpcContext; use crate::db::model::package::CurrentDependencies; use crate::prelude::*; use crate::rpc_continuations::Guid; +use crate::util::PathOrUrl; use crate::Error; pub fn dependency() -> ParentHandler { @@ -42,6 +45,16 @@ impl Map for Dependencies { pub struct DepInfo { pub description: Option, pub optional: bool, + pub s9pk: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct DependencyMetadata { + #[ts(type = "string")] + pub title: InternedString, } #[derive(Deserialize, Serialize, Parser, TS)] diff --git a/core/startos/src/init.rs b/core/startos/src/init.rs index 48cb24c9a..735ca85a5 100644 --- a/core/startos/src/init.rs +++ b/core/startos/src/init.rs @@ -554,33 +554,54 @@ pub struct InitProgressRes { pub async fn init_progress(ctx: InitContext) -> Result { let progress_tracker = ctx.progress.clone(); let progress = progress_tracker.snapshot(); + let mut error = ctx.error.subscribe(); let guid = Guid::new(); ctx.rpc_continuations .add( guid.clone(), RpcContinuation::ws( |mut ws| async move { - if let Err(e) = async { - let mut stream = progress_tracker.stream(Some(Duration::from_millis(100))); - while let Some(progress) = stream.next().await { - ws.send(ws::Message::Text( - serde_json::to_string(&progress) - .with_kind(ErrorKind::Serialization)?, - )) - .await - .with_kind(ErrorKind::Network)?; - if progress.overall.is_complete() { - break; + let res = tokio::try_join!( + async { + let mut stream = + progress_tracker.stream(Some(Duration::from_millis(100))); + while let Some(progress) = stream.next().await { + ws.send(ws::Message::Text( + serde_json::to_string(&progress) + .with_kind(ErrorKind::Serialization)?, + )) + .await + .with_kind(ErrorKind::Network)?; + if progress.overall.is_complete() { + break; + } + } + + Ok::<_, Error>(()) + }, + async { + if let Some(e) = error + .wait_for(|e| e.is_some()) + .await + .ok() + .and_then(|e| e.as_ref().map(|e| e.clone_output())) + { + Err::<(), _>(e) + } else { + Ok(()) } } + ); - ws.normal_close("complete").await?; - - Ok::<_, Error>(()) - } - .await + if let Err(e) = ws + .close_result(res.map(|_| "complete").map_err(|e| { + tracing::error!("error in init progress websocket: {e}"); + tracing::debug!("{e:?}"); + e + })) + .await { - tracing::error!("error in init progress websocket: {e}"); + tracing::error!("error closing init progress websocket: {e}"); tracing::debug!("{e:?}"); } }, diff --git a/core/startos/src/install/mod.rs b/core/startos/src/install/mod.rs index 18591dc8d..394136024 100644 --- a/core/startos/src/install/mod.rs +++ b/core/startos/src/install/mod.rs @@ -111,6 +111,7 @@ impl std::fmt::Display for MinMax { #[derive(Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] +#[ts(export)] pub struct InstallParams { #[ts(type = "string")] registry: Url, diff --git a/core/startos/src/middleware/db.rs b/core/startos/src/middleware/db.rs index dca0418e5..e8b4f8887 100644 --- a/core/startos/src/middleware/db.rs +++ b/core/startos/src/middleware/db.rs @@ -7,7 +7,6 @@ use serde::Deserialize; use crate::context::RpcContext; #[derive(Deserialize)] -#[serde(rename_all = "camelCase")] pub struct Metadata { #[serde(default)] sync_db: bool, diff --git a/core/startos/src/net/static_server.rs b/core/startos/src/net/static_server.rs index 3e5f0997d..cd93a9d65 100644 --- a/core/startos/src/net/static_server.rs +++ b/core/startos/src/net/static_server.rs @@ -1,5 +1,8 @@ +use std::cmp::min; use std::future::Future; +use std::io::Cursor; use std::path::{Path, PathBuf}; +use std::sync::Arc; use std::time::UNIX_EPOCH; use async_compression::tokio::bufread::GzipEncoder; @@ -8,36 +11,51 @@ use axum::extract::{self as x, Request}; use axum::response::Response; use axum::routing::{any, get, post}; use axum::Router; +use base64::display::Base64Display; use digest::Digest; use futures::future::ready; -use http::header::ACCEPT_ENCODING; +use http::header::{ + ACCEPT_ENCODING, ACCEPT_RANGES, CACHE_CONTROL, CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, + CONTENT_RANGE, CONTENT_TYPE, ETAG, RANGE, +}; use http::request::Parts as RequestParts; -use http::{Method, StatusCode}; +use http::{HeaderValue, Method, StatusCode}; use imbl_value::InternedString; use include_dir::Dir; use new_mime_guess::MimeGuess; use openssl::hash::MessageDigest; use openssl::x509::X509; use rpc_toolkit::{Context, HttpServer, Server}; -use tokio::io::BufReader; +use sqlx::query; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeekExt, BufReader}; use tokio_util::io::ReaderStream; +use url::Url; use crate::context::{DiagnosticContext, InitContext, InstallContext, RpcContext, SetupContext}; use crate::hostname::Hostname; +use crate::install::PKG_ARCHIVE_DIR; use crate::middleware::auth::{Auth, HasValidSession}; use crate::middleware::cors::Cors; use crate::middleware::db::SyncDb; +use crate::prelude::*; +use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; use crate::rpc_continuations::{Guid, RpcContinuations}; +use crate::s9pk::merkle_archive::source::http::HttpSource; +use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; +use crate::s9pk::merkle_archive::source::FileSource; +use crate::s9pk::S9pk; use crate::util::io::open_file; -use crate::{ - diagnostic_api, init_api, install_api, main_api, setup_api, Error, ErrorKind, ResultExt, -}; +use crate::util::net::SyncBody; +use crate::util::serde::BASE64; +use crate::{diagnostic_api, init_api, install_api, main_api, setup_api}; const NOT_FOUND: &[u8] = b"Not Found"; const METHOD_NOT_ALLOWED: &[u8] = b"Method Not Allowed"; const NOT_AUTHORIZED: &[u8] = b"Not Authorized"; const INTERNAL_SERVER_ERROR: &[u8] = b"Internal Server Error"; +const PROXY_STRIP_HEADERS: &[&str] = &["cookie", "host", "origin", "referer", "user-agent"]; + #[cfg(all(feature = "daemon", not(feature = "test")))] const EMBEDDED_UIS: Dir<'_> = include_dir::include_dir!("$CARGO_MANIFEST_DIR/../../web/dist/static"); @@ -97,7 +115,7 @@ pub fn rpc_router>( fn serve_ui(req: Request, ui_mode: UiMode) -> Result { let (request_parts, _body) = req.into_parts(); match &request_parts.method { - &Method::GET => { + &Method::GET | &Method::HEAD => { let uri_path = ui_mode.path( request_parts .uri @@ -111,7 +129,7 @@ fn serve_ui(req: Request, ui_mode: UiMode) -> Result { .or_else(|| EMBEDDED_UIS.get_file(&*ui_mode.path("index.html"))); if let Some(file) = file { - FileData::from_embedded(&request_parts, file).into_response(&request_parts) + FileData::from_embedded(&request_parts, file)?.into_response(&request_parts) } else { Ok(not_found()) } @@ -161,14 +179,35 @@ pub fn init_ui_router(ctx: InitContext) -> Router { } pub fn main_ui_router(ctx: RpcContext) -> Router { - rpc_router( - ctx.clone(), + rpc_router(ctx.clone(), { + let ctx = ctx.clone(); Server::new(move || ready(Ok(ctx.clone())), main_api::()) .middleware(Cors::new()) .middleware(Auth::new()) - .middleware(SyncDb::new()), + .middleware(SyncDb::new()) + }) + .route("/proxy/:url", { + let ctx = ctx.clone(); + any(move |x::Path(url): x::Path, request: Request| { + let ctx = ctx.clone(); + async move { + proxy_request(ctx, request, url) + .await + .unwrap_or_else(server_error) + } + }) + }) + .nest("/s9pk", s9pk_router(ctx.clone())) + .route( + "/static/local-root-ca.crt", + get(move || { + let ctx = ctx.clone(); + async move { + let account = ctx.account.read().await; + cert_send(&account.root_ca_cert, &account.hostname) + } + }), ) - // TODO: cert .fallback(any(|request: Request| async move { serve_ui(request, UiMode::Main).unwrap_or_else(server_error) })) @@ -179,29 +218,133 @@ pub fn refresher() -> Router { let res = include_bytes!("./refresher.html"); FileData { data: Body::from(&res[..]), + content_range: None, e_tag: None, encoding: None, len: Some(res.len() as u64), mime: Some("text/html".into()), + digest: None, } .into_response(&request.into_parts().0) .unwrap_or_else(server_error) })) } +async fn proxy_request(ctx: RpcContext, request: Request, url: String) -> Result { + if_authorized(&ctx, request, |mut request| async { + for header in PROXY_STRIP_HEADERS { + request.headers_mut().remove(*header); + } + *request.uri_mut() = url.parse()?; + let request = request.map(|b| reqwest::Body::wrap_stream(SyncBody::from(b))); + let response = ctx.client.execute(request.try_into()?).await?; + Ok(Response::from(response).map(|b| Body::new(b))) + }) + .await +} + +fn s9pk_router(ctx: RpcContext) -> Router { + Router::new() + .route("/installed/:s9pk", { + let ctx = ctx.clone(); + any( + |x::Path(s9pk): x::Path, request: Request| async move { + if_authorized(&ctx, request, |request| async { + let (parts, _) = request.into_parts(); + match FileData::from_path( + &parts, + &ctx.datadir + .join(PKG_ARCHIVE_DIR) + .join("installed") + .join(s9pk), + ) + .await? + { + Some(file) => file.into_response(&parts), + None => Ok(not_found()), + } + }) + .await + .unwrap_or_else(server_error) + }, + ) + }) + .route("/installed/:s9pk/*path", { + let ctx = ctx.clone(); + any( + |x::Path(s9pk): x::Path, + x::Path(path): x::Path, + x::Query(commitment): x::Query>, + request: Request| async move { + if_authorized(&ctx, request, |request| async { + let s9pk = S9pk::deserialize( + &MultiCursorFile::from( + open_file( + ctx.datadir + .join(PKG_ARCHIVE_DIR) + .join("installed") + .join(s9pk), + ) + .await?, + ), + commitment.as_ref(), + ) + .await?; + let (parts, _) = request.into_parts(); + match FileData::from_s9pk(&parts, &s9pk, &path).await? { + Some(file) => file.into_response(&parts), + None => Ok(not_found()), + } + }) + .await + .unwrap_or_else(server_error) + }, + ) + }) + .route( + "/proxy/:url/*path", + any( + |x::Path((url, path)): x::Path<(Url, PathBuf)>, + x::RawQuery(query): x::RawQuery, + request: Request| async move { + if_authorized(&ctx, request, |request| async { + let s9pk = S9pk::deserialize( + &Arc::new(HttpSource::new(ctx.client.clone(), url).await?), + query + .as_deref() + .map(MerkleArchiveCommitment::from_query) + .and_then(|a| a.transpose()) + .transpose()? + .as_ref(), + ) + .await?; + let (parts, _) = request.into_parts(); + match FileData::from_s9pk(&parts, &s9pk, &path).await? { + Some(file) => file.into_response(&parts), + None => Ok(not_found()), + } + }) + .await + .unwrap_or_else(server_error) + }, + ), + ) +} + async fn if_authorized< - F: FnOnce() -> Fut, - Fut: Future> + Send + Sync, + F: FnOnce(Request) -> Fut, + Fut: Future> + Send, >( ctx: &RpcContext, - parts: &RequestParts, + request: Request, f: F, ) -> Result { - if let Err(e) = HasValidSession::from_header(parts.headers.get(http::header::COOKIE), ctx).await + if let Err(e) = + HasValidSession::from_header(request.headers().get(http::header::COOKIE), ctx).await { - Ok(unauthorized(e, parts.uri.path())) + Ok(unauthorized(e, request.uri().path())) } else { - f().await + f(request).await } } @@ -268,44 +411,117 @@ fn cert_send(cert: &X509, hostname: &Hostname) -> Result { .with_kind(ErrorKind::Network) } +fn parse_range(header: &HeaderValue, len: u64) -> Result<(u64, u64, u64), Error> { + let r = header + .to_str() + .with_kind(ErrorKind::Network)? + .trim() + .strip_prefix("bytes=") + .ok_or_else(|| Error::new(eyre!("invalid range units"), ErrorKind::InvalidRequest))?; + + if r.contains(",") { + return Err(Error::new( + eyre!("multi-range requests are unsupported"), + ErrorKind::InvalidRequest, + )); + } + if let Some((start, end)) = r.split_once("-").map(|(s, e)| (s.trim(), e.trim())) { + Ok(( + if start.is_empty() { + 0u64 + } else { + start.parse()? + }, + if end.is_empty() { + len - 1 + } else { + min(end.parse()?, len - 1) + }, + len, + )) + } else { + Ok((len - r.trim().parse::()?, len - 1, len)) + } +} + struct FileData { data: Body, len: Option, + content_range: Option<(u64, u64, u64)>, encoding: Option<&'static str>, e_tag: Option, mime: Option, + digest: Option<(&'static str, Vec)>, } impl FileData { - fn from_embedded(req: &RequestParts, file: &'static include_dir::File<'static>) -> Self { + fn from_embedded( + req: &RequestParts, + file: &'static include_dir::File<'static>, + ) -> Result { let path = file.path(); - let (encoding, data) = req - .headers - .get_all(ACCEPT_ENCODING) - .into_iter() - .filter_map(|h| h.to_str().ok()) - .flat_map(|s| s.split(",")) - .filter_map(|s| s.split(";").next()) - .map(|s| s.trim()) - .fold((None, file.contents()), |acc, e| { - if let Some(file) = (e == "br") - .then_some(()) - .and_then(|_| EMBEDDED_UIS.get_file(format!("{}.br", path.display()))) - { - (Some("br"), file.contents()) - } else if let Some(file) = (e == "gzip" && acc.0 != Some("br")) - .then_some(()) - .and_then(|_| EMBEDDED_UIS.get_file(format!("{}.gz", path.display()))) - { - (Some("gzip"), file.contents()) - } else { - acc - } - }); + let (encoding, data, len, content_range) = if let Some(range) = req.headers.get(RANGE) { + let data = file.contents(); + let (start, end, size) = parse_range(range, data.len() as u64)?; + let encoding = req + .headers + .get_all(ACCEPT_ENCODING) + .into_iter() + .filter_map(|h| h.to_str().ok()) + .flat_map(|s| s.split(",")) + .filter_map(|s| s.split(";").next()) + .map(|s| s.trim()) + .any(|e| e == "gzip") + .then_some("gzip"); + let data = if start > end { + &[] + } else { + &data[(start as usize)..=(end as usize)] + }; + let (len, data) = if encoding == Some("gzip") { + ( + None, + Body::from_stream(ReaderStream::new(GzipEncoder::new(Cursor::new(data)))), + ) + } else { + (Some(data.len() as u64), Body::from(data)) + }; + (encoding, data, len, Some((start, end, size))) + } else { + let (encoding, data) = req + .headers + .get_all(ACCEPT_ENCODING) + .into_iter() + .filter_map(|h| h.to_str().ok()) + .flat_map(|s| s.split(",")) + .filter_map(|s| s.split(";").next()) + .map(|s| s.trim()) + .fold((None, file.contents()), |acc, e| { + if let Some(file) = (e == "br") + .then_some(()) + .and_then(|_| EMBEDDED_UIS.get_file(format!("{}.br", path.display()))) + { + (Some("br"), file.contents()) + } else if let Some(file) = (e == "gzip" && acc.0 != Some("br")) + .then_some(()) + .and_then(|_| EMBEDDED_UIS.get_file(format!("{}.gz", path.display()))) + { + (Some("gzip"), file.contents()) + } else { + acc + } + }); + (encoding, Body::from(data), Some(data.len() as u64), None) + }; - Self { - len: Some(data.len() as u64), + Ok(Self { + len, encoding, - data: data.into(), + content_range, + data: if req.method == Method::HEAD { + Body::empty() + } else { + data + }, e_tag: file.metadata().map(|metadata| { e_tag( path, @@ -323,11 +539,28 @@ impl FileData { mime: MimeGuess::from_path(path) .first() .map(|m| m.essence_str().into()), + digest: None, + }) + } + + fn encode( + encoding: &mut Option<&str>, + data: R, + len: u64, + ) -> (Option, Body) { + if *encoding == Some("gzip") { + ( + None, + Body::from_stream(ReaderStream::new(GzipEncoder::new(BufReader::new(data)))), + ) + } else { + *encoding = None; + (Some(len), Body::from_stream(ReaderStream::new(data))) } } - async fn from_path(req: &RequestParts, path: &Path) -> Result { - let encoding = req + async fn from_path(req: &RequestParts, path: &Path) -> Result, Error> { + let mut encoding = req .headers .get_all(ACCEPT_ENCODING) .into_iter() @@ -338,12 +571,23 @@ impl FileData { .any(|e| e == "gzip") .then_some("gzip"); - let file = open_file(path).await?; + if tokio::fs::metadata(path).await.is_err() { + return Ok(None); + } + + let mut file = open_file(path).await?; + let metadata = file .metadata() .await .with_ctx(|_| (ErrorKind::Filesystem, path.display().to_string()))?; + let content_range = req + .headers + .get(RANGE) + .map(|r| parse_range(r, metadata.len())) + .transpose()?; + let e_tag = Some(e_tag( path, format!( @@ -357,51 +601,123 @@ impl FileData { .as_bytes(), )); - let (len, data) = if encoding == Some("gzip") { - ( - None, - Body::from_stream(ReaderStream::new(GzipEncoder::new(BufReader::new(file)))), - ) + let (len, data) = if let Some((start, end, _)) = content_range { + let len = end + 1 - start; + file.seek(std::io::SeekFrom::Start(start)).await?; + Self::encode(&mut encoding, file.take(len), len) } else { - ( - Some(metadata.len()), - Body::from_stream(ReaderStream::new(file)), - ) + Self::encode(&mut encoding, file, metadata.len()) }; - Ok(Self { - data, + Ok(Some(Self { + data: if req.method == Method::HEAD { + Body::empty() + } else { + data + }, len, + content_range, encoding, e_tag, mime: MimeGuess::from_path(path) .first() .map(|m| m.essence_str().into()), - }) + digest: None, + })) + } + + async fn from_s9pk( + req: &RequestParts, + s9pk: &S9pk, + path: &Path, + ) -> Result, Error> { + let mut encoding = req + .headers + .get_all(ACCEPT_ENCODING) + .into_iter() + .filter_map(|h| h.to_str().ok()) + .flat_map(|s| s.split(",")) + .filter_map(|s| s.split(";").next()) + .map(|s| s.trim()) + .any(|e| e == "gzip") + .then_some("gzip"); + + let Some(file) = s9pk.as_archive().contents().get_path(path) else { + return Ok(None); + }; + let Some(contents) = file.as_file() else { + return Ok(None); + }; + let (digest, len) = if let Some((hash, len)) = file.hash() { + (Some(("blake3", hash.as_bytes().to_vec())), len) + } else { + (None, contents.size().await?) + }; + + let content_range = req + .headers + .get(RANGE) + .map(|r| parse_range(r, len)) + .transpose()?; + + let (len, data) = if let Some((start, end, _)) = content_range { + let len = end + 1 - start; + Self::encode(&mut encoding, contents.slice(start, len).await?, len) + } else { + Self::encode(&mut encoding, contents.reader().await?.take(len), len) + }; + + Ok(Some(Self { + data: if req.method == Method::HEAD { + Body::empty() + } else { + data + }, + len, + content_range, + encoding, + e_tag: None, + mime: MimeGuess::from_path(path) + .first() + .map(|m| m.essence_str().into()), + digest, + })) } fn into_response(self, req: &RequestParts) -> Result { let mut builder = Response::builder(); if let Some(mime) = self.mime { - builder = builder.header(http::header::CONTENT_TYPE, &*mime); + builder = builder.header(CONTENT_TYPE, &*mime); } if let Some(e_tag) = &self.e_tag { - builder = builder.header(http::header::ETAG, &**e_tag); + builder = builder + .header(ETAG, &**e_tag) + .header(CACHE_CONTROL, "public, max-age=21000000, immutable"); + } + + builder = builder.header(ACCEPT_RANGES, "bytes"); + if let Some((start, end, size)) = self.content_range { + builder = builder + .header(CONTENT_RANGE, format!("bytes {start}-{end}/{size}")) + .status(StatusCode::PARTIAL_CONTENT); + } + + if let Some((algorithm, digest)) = self.digest { + builder = builder.header( + "Repr-Digest", + format!("{algorithm}=:{}:", Base64Display::new(&digest, &BASE64)), + ); } - builder = builder.header( - http::header::CACHE_CONTROL, - "public, max-age=21000000, immutable", - ); if req .headers - .get_all(http::header::CONNECTION) + .get_all(CONNECTION) .iter() .flat_map(|s| s.to_str().ok()) .flat_map(|s| s.split(",")) .any(|s| s.trim() == "keep-alive") { - builder = builder.header(http::header::CONNECTION, "keep-alive"); + builder = builder.header(CONNECTION, "keep-alive"); } if self.e_tag.is_some() @@ -411,14 +727,13 @@ impl FileData { .and_then(|h| h.to_str().ok()) == self.e_tag.as_deref() { - builder = builder.status(StatusCode::NOT_MODIFIED); - builder.body(Body::empty()) + builder.status(StatusCode::NOT_MODIFIED).body(Body::empty()) } else { if let Some(len) = self.len { - builder = builder.header(http::header::CONTENT_LENGTH, len); + builder = builder.header(CONTENT_LENGTH, len); } if let Some(encoding) = self.encoding { - builder = builder.header(http::header::CONTENT_ENCODING, encoding); + builder = builder.header(CONTENT_ENCODING, encoding); } builder.body(self.data) diff --git a/core/startos/src/registry/admin.rs b/core/startos/src/registry/admin.rs index cd795e5cd..8125580a4 100644 --- a/core/startos/src/registry/admin.rs +++ b/core/startos/src/registry/admin.rs @@ -46,7 +46,7 @@ fn signers_api() -> ParentHandler { .with_metadata("admin", Value::Bool(true)) .no_cli(), ) - .subcommand("add", from_fn_async(cli_add_signer).no_display()) + .subcommand("add", from_fn_async(cli_add_signer)) } impl Model> { @@ -71,7 +71,7 @@ impl Model> { .ok_or_else(|| Error::new(eyre!("unknown signer"), ErrorKind::Authorization)) } - pub fn add_signer(&mut self, signer: &SignerInfo) -> Result<(), Error> { + pub fn add_signer(&mut self, signer: &SignerInfo) -> Result { if let Some((guid, s)) = self .as_entries()? .into_iter() @@ -89,7 +89,9 @@ impl Model> { ErrorKind::InvalidRequest, )); } - self.insert(&Guid::new(), signer) + let id = Guid::new(); + self.insert(&id, signer)?; + Ok(id) } } @@ -122,7 +124,7 @@ pub fn display_signers(params: WithIoFormat, signers: BTreeMap Result<(), Error> { +pub async fn add_signer(ctx: RegistryContext, signer: SignerInfo) -> Result { ctx.db .mutate(|db| db.as_index_mut().as_signers_mut().add_signer(&signer)) .await @@ -155,7 +157,7 @@ pub async fn cli_add_signer( }, .. }: HandlerArgs, -) -> Result<(), Error> { +) -> Result { let signer = SignerInfo { name, contact, @@ -165,15 +167,16 @@ pub async fn cli_add_signer( TypedPatchDb::::load(PatchDb::open(database).await?) .await? .mutate(|db| db.as_index_mut().as_signers_mut().add_signer(&signer)) - .await?; + .await } else { - ctx.call_remote::( - &parent_method.into_iter().chain(method).join("."), - to_value(&signer)?, + from_value( + ctx.call_remote::( + &parent_method.into_iter().chain(method).join("."), + to_value(&signer)?, + ) + .await?, ) - .await?; } - Ok(()) } #[derive(Debug, Deserialize, Serialize, TS)] diff --git a/core/startos/src/registry/asset.rs b/core/startos/src/registry/asset.rs index ab251d2aa..7697a0c99 100644 --- a/core/startos/src/registry/asset.rs +++ b/core/startos/src/registry/asset.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::sync::Arc; +use chrono::{DateTime, Utc}; use reqwest::Client; use serde::{Deserialize, Serialize}; use tokio::io::AsyncWrite; @@ -20,6 +21,8 @@ use crate::s9pk::S9pk; #[serde(rename_all = "camelCase")] #[ts(export)] pub struct RegistryAsset { + #[ts(type = "string")] + pub published_at: DateTime, #[ts(type = "string")] pub url: Url, pub commitment: Commitment, diff --git a/core/startos/src/registry/auth.rs b/core/startos/src/registry/auth.rs index 27655c4a8..4707bf809 100644 --- a/core/startos/src/registry/auth.rs +++ b/core/startos/src/registry/auth.rs @@ -27,7 +27,6 @@ use crate::util::serde::Base64; pub const AUTH_SIG_HEADER: &str = "X-StartOS-Registry-Auth-Sig"; #[derive(Deserialize)] -#[serde(rename_all = "camelCase")] pub struct Metadata { #[serde(default)] admin: bool, @@ -75,9 +74,7 @@ pub struct RegistryAdminLogRecord { pub key: AnyVerifyingKey, } -#[derive(Serialize, Deserialize)] pub struct SignatureHeader { - #[serde(flatten)] pub commitment: RequestCommitment, pub signer: AnyVerifyingKey, pub signature: AnySignature, @@ -93,14 +90,9 @@ impl SignatureHeader { HeaderValue::from_str(url.query().unwrap_or_default()).unwrap() } pub fn from_header(header: &HeaderValue) -> Result { - let url: Url = format!( - "http://localhost/?{}", - header.to_str().with_kind(ErrorKind::Utf8)? - ) - .parse()?; - let query: BTreeMap<_, _> = url.query_pairs().collect(); + let query: BTreeMap<_, _> = form_urlencoded::parse(header.as_bytes()).collect(); Ok(Self { - commitment: RequestCommitment::from_query(&url)?, + commitment: RequestCommitment::from_query(&header)?, signer: query.get("signer").or_not_found("signer")?.parse()?, signature: query.get("signature").or_not_found("signature")?.parse()?, }) diff --git a/core/startos/src/registry/context.rs b/core/startos/src/registry/context.rs index 64a157073..d3eaf3691 100644 --- a/core/startos/src/registry/context.rs +++ b/core/startos/src/registry/context.rs @@ -200,6 +200,19 @@ impl CallRemote for CliContext { .send() .await?; + if !res.status().is_success() { + let status = res.status(); + let txt = res.text().await?; + let mut res = Err(Error::new( + eyre!("{}", status.canonical_reason().unwrap_or(status.as_str())), + ErrorKind::Network, + )); + if !txt.is_empty() { + res = res.with_ctx(|_| (ErrorKind::Network, txt)); + } + return res.map_err(From::from); + } + match res .headers() .get(CONTENT_TYPE) @@ -210,7 +223,7 @@ impl CallRemote for CliContext { .with_kind(ErrorKind::Deserialization)? .result } - _ => Err(Error::new(eyre!("missing content type"), ErrorKind::Network).into()), + _ => Err(Error::new(eyre!("unknown content type"), ErrorKind::Network).into()), } } } @@ -247,6 +260,19 @@ impl CallRemote for RpcContext { .send() .await?; + if !res.status().is_success() { + let status = res.status(); + let txt = res.text().await?; + let mut res = Err(Error::new( + eyre!("{}", status.canonical_reason().unwrap_or(status.as_str())), + ErrorKind::Network, + )); + if !txt.is_empty() { + res = res.with_ctx(|_| (ErrorKind::Network, txt)); + } + return res.map_err(From::from); + } + match res .headers() .get(CONTENT_TYPE) @@ -257,7 +283,7 @@ impl CallRemote for RpcContext { .with_kind(ErrorKind::Deserialization)? .result } - _ => Err(Error::new(eyre!("missing content type"), ErrorKind::Network).into()), + _ => Err(Error::new(eyre!("unknown content type"), ErrorKind::Network).into()), } } } diff --git a/core/startos/src/registry/device_info.rs b/core/startos/src/registry/device_info.rs index 9a357358a..172348a10 100644 --- a/core/startos/src/registry/device_info.rs +++ b/core/startos/src/registry/device_info.rs @@ -54,12 +54,7 @@ impl DeviceInfo { HeaderValue::from_str(url.query().unwrap_or_default()).unwrap() } pub fn from_header_value(header: &HeaderValue) -> Result { - let url: Url = format!( - "http://localhost/?{}", - header.to_str().with_kind(ErrorKind::ParseUrl)? - ) - .parse()?; - let query: BTreeMap<_, _> = url.query_pairs().collect(); + let query: BTreeMap<_, _> = form_urlencoded::parse(header.as_bytes()).collect(); Ok(Self { os: OsInfo { version: query @@ -151,7 +146,6 @@ impl From<&RpcContext> for HardwareInfo { } #[derive(Deserialize)] -#[serde(rename_all = "camelCase")] pub struct Metadata { #[serde(default)] get_device_info: bool, diff --git a/core/startos/src/registry/mod.rs b/core/startos/src/registry/mod.rs index 1039264df..d34ebb841 100644 --- a/core/startos/src/registry/mod.rs +++ b/core/startos/src/registry/mod.rs @@ -2,6 +2,7 @@ use std::collections::{BTreeMap, BTreeSet}; use axum::Router; use futures::future::ready; +use imbl_value::InternedString; use models::DataUrl; use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler, Server}; use serde::{Deserialize, Serialize}; @@ -16,7 +17,7 @@ use crate::registry::auth::Auth; use crate::registry::context::RegistryContext; use crate::registry::device_info::DeviceInfoMiddleware; use crate::registry::os::index::OsIndex; -use crate::registry::package::index::PackageIndex; +use crate::registry::package::index::{Category, PackageIndex}; use crate::registry::signer::SignerInfo; use crate::rpc_continuations::Guid; use crate::util::serde::HandlerExtSerde; @@ -45,6 +46,7 @@ impl RegistryDatabase {} #[model = "Model"] #[ts(export)] pub struct FullIndex { + pub name: Option, pub icon: Option>, pub package: PackageIndex, pub os: OsIndex, @@ -55,6 +57,25 @@ pub async fn get_full_index(ctx: RegistryContext) -> Result { ctx.db.peek().await.into_index().de() } +#[derive(Debug, Default, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct RegistryInfo { + pub name: Option, + pub icon: Option>, + #[ts(as = "BTreeMap::")] + pub categories: BTreeMap, +} + +pub async fn get_info(ctx: RegistryContext) -> Result { + let peek = ctx.db.peek().await.into_index(); + Ok(RegistryInfo { + name: peek.as_name().de()?, + icon: peek.as_icon().de()?, + categories: peek.as_package().as_categories().de()?, + }) +} + pub fn registry_api() -> ParentHandler { ParentHandler::new() .subcommand( @@ -63,6 +84,12 @@ pub fn registry_api() -> ParentHandler { .with_display_serializable() .with_call_remote::(), ) + .subcommand( + "info", + from_fn_async(get_info) + .with_display_serializable() + .with_call_remote::(), + ) .subcommand("os", os::os_api::()) .subcommand("package", package::package_api::()) .subcommand("admin", admin::admin_api::()) diff --git a/core/startos/src/registry/os/asset/add.rs b/core/startos/src/registry/os/asset/add.rs index 6108dd5bc..c18d05b8f 100644 --- a/core/startos/src/registry/os/asset/add.rs +++ b/core/startos/src/registry/os/asset/add.rs @@ -2,6 +2,7 @@ use std::collections::{BTreeMap, HashMap}; use std::panic::UnwindSafe; use std::path::PathBuf; +use chrono::Utc; use clap::Parser; use imbl_value::InternedString; use itertools::Itertools; @@ -12,7 +13,7 @@ use url::Url; use crate::context::CliContext; use crate::prelude::*; -use crate::progress::{FullProgressTracker}; +use crate::progress::FullProgressTracker; use crate::registry::asset::RegistryAsset; use crate::registry::context::RegistryContext; use crate::registry::os::index::OsVersionInfo; @@ -33,19 +34,19 @@ pub fn add_api() -> ParentHandler { .subcommand( "iso", from_fn_async(add_iso) - .with_metadata("getSigner", Value::Bool(true)) + .with_metadata("get_signer", Value::Bool(true)) .no_cli(), ) .subcommand( "img", from_fn_async(add_img) - .with_metadata("getSigner", Value::Bool(true)) + .with_metadata("get_signer", Value::Bool(true)) .no_cli(), ) .subcommand( "squashfs", from_fn_async(add_squashfs) - .with_metadata("getSigner", Value::Bool(true)) + .with_metadata("get_signer", Value::Bool(true)) .no_cli(), ) } @@ -107,6 +108,7 @@ async fn add_asset( ) .upsert(&platform, || { Ok(RegistryAsset { + published_at: Utc::now(), url, commitment: commitment.clone(), signatures: HashMap::new(), diff --git a/core/startos/src/registry/os/asset/sign.rs b/core/startos/src/registry/os/asset/sign.rs index 8bf1cfeb5..12903d8a1 100644 --- a/core/startos/src/registry/os/asset/sign.rs +++ b/core/startos/src/registry/os/asset/sign.rs @@ -30,19 +30,19 @@ pub fn sign_api() -> ParentHandler { .subcommand( "iso", from_fn_async(sign_iso) - .with_metadata("getSigner", Value::Bool(true)) + .with_metadata("get_signer", Value::Bool(true)) .no_cli(), ) .subcommand( "img", from_fn_async(sign_img) - .with_metadata("getSigner", Value::Bool(true)) + .with_metadata("get_signer", Value::Bool(true)) .no_cli(), ) .subcommand( "squashfs", from_fn_async(sign_squashfs) - .with_metadata("getSigner", Value::Bool(true)) + .with_metadata("get_signer", Value::Bool(true)) .no_cli(), ) } diff --git a/core/startos/src/registry/os/version/mod.rs b/core/startos/src/registry/os/version/mod.rs index 9ebe8a696..242ae28a7 100644 --- a/core/startos/src/registry/os/version/mod.rs +++ b/core/startos/src/registry/os/version/mod.rs @@ -25,7 +25,7 @@ pub fn version_api() -> ParentHandler { "add", from_fn_async(add_version) .with_metadata("admin", Value::Bool(true)) - .with_metadata("getSigner", Value::Bool(true)) + .with_metadata("get_signer", Value::Bool(true)) .no_display() .with_call_remote::(), ) @@ -121,7 +121,7 @@ pub async fn remove_version( #[command(rename_all = "kebab-case")] #[serde(rename_all = "camelCase")] #[ts(export)] -pub struct GetVersionParams { +pub struct GetOsVersionParams { #[ts(type = "string | null")] #[arg(long = "src")] pub source: Option, @@ -138,12 +138,12 @@ pub struct GetVersionParams { pub async fn get_version( ctx: RegistryContext, - GetVersionParams { + GetOsVersionParams { source, target, server_id, arch, - }: GetVersionParams, + }: GetOsVersionParams, ) -> Result, Error> { if let (Some(pool), Some(server_id), Some(arch)) = (&ctx.pool, server_id, arch) { let created_at = Utc::now(); diff --git a/core/startos/src/registry/package/add.rs b/core/startos/src/registry/package/add.rs index 9bc772f78..c52f06ac0 100644 --- a/core/startos/src/registry/package/add.rs +++ b/core/startos/src/registry/package/add.rs @@ -11,13 +11,14 @@ use url::Url; use crate::context::CliContext; use crate::prelude::*; -use crate::progress::FullProgressTracker; +use crate::progress::{FullProgressTracker, ProgressTrackerWriter}; use crate::registry::context::RegistryContext; use crate::registry::package::index::PackageVersionInfo; use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; use crate::registry::signer::sign::ed25519::Ed25519; use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey, SignatureScheme}; use crate::s9pk::merkle_archive::source::http::HttpSource; +use crate::s9pk::merkle_archive::source::ArchiveSource; use crate::s9pk::v2::SIG_CONTEXT; use crate::s9pk::S9pk; use crate::util::io::TrackingIO; @@ -126,13 +127,16 @@ pub async fn cli_add_package( sign_phase.complete(); verify_phase.start(); - let mut src = S9pk::deserialize( - &Arc::new(HttpSource::new(ctx.client.clone(), url.clone()).await?), - Some(&commitment), - ) - .await?; - src.serialize(&mut TrackingIO::new(0, tokio::io::sink()), true) + let source = HttpSource::new(ctx.client.clone(), url.clone()).await?; + let len = source.size().await; + let mut src = S9pk::deserialize(&Arc::new(source), Some(&commitment)).await?; + if let Some(len) = len { + verify_phase.set_total(len); + } + let mut verify_writer = ProgressTrackerWriter::new(tokio::io::sink(), verify_phase); + src.serialize(&mut TrackingIO::new(0, &mut verify_writer), true) .await?; + let (_, mut verify_phase) = verify_writer.into_inner(); verify_phase.complete(); index_phase.start(); @@ -140,7 +144,7 @@ pub async fn cli_add_package( &parent_method.into_iter().chain(method).join("."), imbl_value::json!({ "url": &url, - "signature": signature, + "signature": AnySignature::Ed25519(signature), "commitment": commitment, }), ) diff --git a/core/startos/src/registry/package/get.rs b/core/startos/src/registry/package/get.rs index fb63be1bc..cae1289a9 100644 --- a/core/startos/src/registry/package/get.rs +++ b/core/startos/src/registry/package/get.rs @@ -21,6 +21,7 @@ use crate::util::VersionString; #[serde(rename_all = "camelCase")] #[ts(export)] pub enum PackageDetailLevel { + None, Short, Full, } @@ -50,7 +51,9 @@ pub struct GetPackageParams { #[arg(skip)] #[serde(rename = "__device_info")] pub device_info: Option, - pub other_versions: Option, + #[serde(default)] + #[arg(default_value = "none")] + pub other_versions: PackageDetailLevel, } #[derive(Debug, Deserialize, Serialize, TS)] @@ -126,7 +129,6 @@ fn get_matching_models<'a>( db: &'a Model, GetPackageParams { id, - version, source_version, device_info, .. @@ -148,22 +150,18 @@ fn get_matching_models<'a>( .into_iter() .map(|(v, info)| { Ok::<_, Error>( - if version + if source_version.as_ref().map_or(Ok(true), |source_version| { + Ok::<_, Error>( + source_version.satisfies( + &info + .as_source_version() + .de()? + .unwrap_or(VersionRange::any()), + ), + ) + })? && device_info .as_ref() - .map_or(true, |version| v.satisfies(version)) - && source_version.as_ref().map_or(Ok(true), |source_version| { - Ok::<_, Error>( - source_version.satisfies( - &info - .as_source_version() - .de()? - .unwrap_or(VersionRange::any()), - ), - ) - })? - && device_info - .as_ref() - .map_or(Ok(true), |device_info| info.works_for_device(device_info))? + .map_or(Ok(true), |device_info| info.works_for_device(device_info))? { Some((k.clone(), ExtendedVersion::from(v), info)) } else { @@ -187,24 +185,27 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu let mut other: BTreeMap>> = Default::default(); for (id, version, info) in get_matching_models(&peek.as_index().as_package(), ¶ms)? { - let mut package_best = best.remove(&id).unwrap_or_default(); - let mut package_other = other.remove(&id).unwrap_or_default(); - for worse_version in package_best - .keys() - .filter(|k| ***k < version) - .cloned() - .collect_vec() + let package_best = best.entry(id.clone()).or_default(); + let package_other = other.entry(id.clone()).or_default(); + if params + .version + .as_ref() + .map_or(true, |v| version.satisfies(v)) + && package_best.keys().all(|k| !(**k > version)) { - if let Some(info) = package_best.remove(&worse_version) { - package_other.insert(worse_version, info); + for worse_version in package_best + .keys() + .filter(|k| ***k < version) + .cloned() + .collect_vec() + { + if let Some(info) = package_best.remove(&worse_version) { + package_other.insert(worse_version, info); + } } - } - if package_best.keys().all(|k| !(**k > version)) { package_best.insert(version.into(), info); - } - best.insert(id.clone(), package_best); - if params.other_versions.is_some() { - other.insert(id.clone(), package_other); + } else { + package_other.insert(version.into(), info); } } if let Some(id) = params.id { @@ -224,12 +225,12 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu .try_collect()?; let other = other.remove(&id).unwrap_or_default(); match params.other_versions { - None => to_value(&GetPackageResponse { + PackageDetailLevel::None => to_value(&GetPackageResponse { categories, best, other_versions: None, }), - Some(PackageDetailLevel::Short) => to_value(&GetPackageResponse { + PackageDetailLevel::Short => to_value(&GetPackageResponse { categories, best, other_versions: Some( @@ -239,7 +240,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu .try_collect()?, ), }), - Some(PackageDetailLevel::Full) => to_value(&GetPackageResponseFull { + PackageDetailLevel::Full => to_value(&GetPackageResponseFull { categories, best, other_versions: other @@ -250,7 +251,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu } } else { match params.other_versions { - None => to_value( + PackageDetailLevel::None => to_value( &best .into_iter() .map(|(id, best)| { @@ -276,7 +277,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu }) .try_collect::<_, GetPackagesResponse, _>()?, ), - Some(PackageDetailLevel::Short) => to_value( + PackageDetailLevel::Short => to_value( &best .into_iter() .map(|(id, best)| { @@ -310,7 +311,7 @@ pub async fn get_package(ctx: RegistryContext, params: GetPackageParams) -> Resu }) .try_collect::<_, GetPackagesResponse, _>()?, ), - Some(PackageDetailLevel::Full) => to_value( + PackageDetailLevel::Full => to_value( &best .into_iter() .map(|(id, best)| { @@ -354,7 +355,7 @@ pub fn display_package_info( } if let Some(_) = params.rest.id { - if params.rest.other_versions == Some(PackageDetailLevel::Full) { + if params.rest.other_versions == PackageDetailLevel::Full { for table in from_value::(info)?.tables() { table.print_tty(false)?; println!(); @@ -366,7 +367,7 @@ pub fn display_package_info( } } } else { - if params.rest.other_versions == Some(PackageDetailLevel::Full) { + if params.rest.other_versions == PackageDetailLevel::Full { for (_, package) in from_value::(info)? { for table in package.tables() { table.print_tty(false)?; diff --git a/core/startos/src/registry/package/index.rs b/core/startos/src/registry/package/index.rs index 80055f06d..12a17f634 100644 --- a/core/startos/src/registry/package/index.rs +++ b/core/startos/src/registry/package/index.rs @@ -1,5 +1,6 @@ use std::collections::{BTreeMap, BTreeSet}; +use chrono::Utc; use exver::{Version, VersionRange}; use imbl_value::InternedString; use models::{DataUrl, PackageId, VersionString}; @@ -15,7 +16,7 @@ use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey}; use crate::rpc_continuations::Guid; use crate::s9pk::git_hash::GitHash; -use crate::s9pk::manifest::{Description, HardwareRequirements}; +use crate::s9pk::manifest::{Alerts, Description, HardwareRequirements}; use crate::s9pk::merkle_archive::source::FileSource; use crate::s9pk::S9pk; @@ -49,12 +50,25 @@ pub struct Category { pub description: Description, } +#[derive(Debug, Deserialize, Serialize, HasModel, TS)] +#[serde(rename_all = "camelCase")] +#[model = "Model"] +#[ts(export)] +pub struct DependencyMetadata { + #[ts(type = "string | null")] + pub title: Option, + pub icon: Option>, + pub description: Option, + pub optional: bool, +} + #[derive(Debug, Deserialize, Serialize, HasModel, TS)] #[serde(rename_all = "camelCase")] #[model = "Model"] #[ts(export)] pub struct PackageVersionInfo { - pub title: String, + #[ts(type = "string")] + pub title: InternedString, pub icon: DataUrl<'static>, pub description: Description, pub release_notes: String, @@ -70,6 +84,10 @@ pub struct PackageVersionInfo { pub support_site: Url, #[ts(type = "string")] pub marketing_site: Url, + #[ts(type = "string | null")] + pub donation_url: Option, + pub alerts: Alerts, + pub dependency_metadata: BTreeMap, #[ts(type = "string")] pub os_version: Version, pub hardware_requirements: HardwareRequirements, @@ -80,6 +98,19 @@ pub struct PackageVersionInfo { impl PackageVersionInfo { pub async fn from_s9pk(s9pk: &S9pk, url: Url) -> Result { let manifest = s9pk.as_manifest(); + let mut dependency_metadata = BTreeMap::new(); + for (id, info) in &manifest.dependencies.0 { + let metadata = s9pk.dependency_metadata(id).await?; + dependency_metadata.insert( + id.clone(), + DependencyMetadata { + title: metadata.map(|m| m.title), + icon: s9pk.dependency_icon_data_url(id).await?, + description: info.description.clone(), + optional: info.optional, + }, + ); + } Ok(Self { title: manifest.title.clone(), icon: s9pk.icon_data_url().await?, @@ -91,10 +122,14 @@ impl PackageVersionInfo { upstream_repo: manifest.upstream_repo.clone(), support_site: manifest.support_site.clone(), marketing_site: manifest.marketing_site.clone(), + donation_url: manifest.donation_url.clone(), + alerts: manifest.alerts.clone(), + dependency_metadata, os_version: manifest.os_version.clone(), hardware_requirements: manifest.hardware_requirements.clone(), source_version: None, // TODO s9pk: RegistryAsset { + published_at: Utc::now(), url, commitment: s9pk.as_archive().commitment().await?, signatures: [( @@ -114,8 +149,11 @@ impl PackageVersionInfo { table.add_row(row![bc => &self.title]); table.add_row(row![br -> "VERSION", AsRef::::as_ref(version)]); table.add_row(row![br -> "RELEASE NOTES", &self.release_notes]); - table.add_row(row![br -> "ABOUT", &self.description.short]); - table.add_row(row![br -> "DESCRIPTION", &self.description.long]); + table.add_row(row![br -> "ABOUT", &textwrap::wrap(&self.description.short, 80).join("\n")]); + table.add_row(row![ + br -> "DESCRIPTION", + &textwrap::wrap(&self.description.long, 80).join("\n") + ]); table.add_row(row![br -> "GIT HASH", AsRef::::as_ref(&self.git_hash)]); table.add_row(row![br -> "LICENSE", &self.license]); table.add_row(row![br -> "PACKAGE REPO", &self.wrapper_repo.to_string()]); diff --git a/core/startos/src/registry/package/mod.rs b/core/startos/src/registry/package/mod.rs index ac09afbb1..cb2d317f9 100644 --- a/core/startos/src/registry/package/mod.rs +++ b/core/startos/src/registry/package/mod.rs @@ -16,14 +16,21 @@ pub fn package_api() -> ParentHandler { .with_display_serializable() .with_call_remote::(), ) - .subcommand("add", from_fn_async(add::add_package).no_cli()) + .subcommand( + "add", + from_fn_async(add::add_package) + .with_metadata("get_signer", Value::Bool(true)) + .no_cli(), + ) .subcommand("add", from_fn_async(add::cli_add_package).no_display()) .subcommand( "get", from_fn_async(get::get_package) + .with_metadata("get_device_info", Value::Bool(true)) .with_display_serializable() .with_custom_display_fn(|handle, result| { get::display_package_info(handle.params, result) - }), + }) + .with_call_remote::(), ) } diff --git a/core/startos/src/registry/signer/commitment/merkle_archive.rs b/core/startos/src/registry/signer/commitment/merkle_archive.rs index 0b61734b4..1b9d7d1e0 100644 --- a/core/startos/src/registry/signer/commitment/merkle_archive.rs +++ b/core/startos/src/registry/signer/commitment/merkle_archive.rs @@ -20,6 +20,35 @@ pub struct MerkleArchiveCommitment { #[ts(type = "number")] pub root_maxsize: u64, } +impl MerkleArchiveCommitment { + pub fn from_query(query: &str) -> Result, Error> { + let mut root_sighash = None; + let mut root_maxsize = None; + for (k, v) in form_urlencoded::parse(dbg!(query).as_bytes()) { + match &*k { + "rootSighash" => { + root_sighash = Some(dbg!(v).parse()?); + } + "rootMaxsize" => { + root_maxsize = Some(v.parse()?); + } + _ => (), + } + } + if root_sighash.is_some() || root_maxsize.is_some() { + Ok(Some(Self { + root_sighash: root_sighash + .or_not_found("rootSighash required if rootMaxsize specified") + .with_kind(ErrorKind::InvalidRequest)?, + root_maxsize: root_maxsize + .or_not_found("rootMaxsize required if rootSighash specified") + .with_kind(ErrorKind::InvalidRequest)?, + })) + } else { + Ok(None) + } + } +} impl Digestable for MerkleArchiveCommitment { fn update(&self, digest: &mut D) { digest.update(&*self.root_sighash); diff --git a/core/startos/src/registry/signer/commitment/request.rs b/core/startos/src/registry/signer/commitment/request.rs index ce60b7f88..e5bb776bf 100644 --- a/core/startos/src/registry/signer/commitment/request.rs +++ b/core/startos/src/registry/signer/commitment/request.rs @@ -5,6 +5,7 @@ use axum::body::Body; use axum::extract::Request; use digest::Update; use futures::TryStreamExt; +use http::HeaderValue; use serde::{Deserialize, Serialize}; use tokio::io::AsyncWrite; use tokio_util::io::StreamReader; @@ -37,8 +38,8 @@ impl RequestCommitment { .append_pair("size", &self.size.to_string()) .append_pair("blake3", &self.blake3.to_string()); } - pub fn from_query(url: &Url) -> Result { - let query: BTreeMap<_, _> = url.query_pairs().collect(); + pub fn from_query(query: &HeaderValue) -> Result { + let query: BTreeMap<_, _> = form_urlencoded::parse(query.as_bytes()).collect(); Ok(Self { timestamp: query.get("timestamp").or_not_found("timestamp")?.parse()?, nonce: query.get("nonce").or_not_found("nonce")?.parse()?, diff --git a/core/startos/src/s9pk/merkle_archive/expected.rs b/core/startos/src/s9pk/merkle_archive/expected.rs index a0f095b7a..c9a2fd31b 100644 --- a/core/startos/src/s9pk/merkle_archive/expected.rs +++ b/core/startos/src/s9pk/merkle_archive/expected.rs @@ -1,4 +1,3 @@ - use std::ffi::OsStr; use std::path::Path; @@ -7,16 +6,16 @@ use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; use crate::s9pk::merkle_archive::source::FileSource; use crate::s9pk::merkle_archive::Entry; -/// An object for tracking the files expected to be in an s9pk +/// An object for tracking the files expected to be in an s9pk pub struct Expected<'a, T> { keep: DirectoryContents<()>, dir: &'a DirectoryContents, } impl<'a, T> Expected<'a, T> { - pub fn new(dir: &'a DirectoryContents,) -> Self { + pub fn new(dir: &'a DirectoryContents) -> Self { Self { keep: DirectoryContents::new(), - dir + dir, } } } @@ -42,22 +41,23 @@ impl<'a, T: Clone> Expected<'a, T> { path: impl AsRef, mut valid_extension: impl FnMut(Option<&OsStr>) -> bool, ) -> Result<(), Error> { - let (dir, stem) = if let Some(parent) = path.as_ref().parent().filter(|p| *p != Path::new("")) { - ( - self.dir - .get_path(parent) - .and_then(|e| e.as_directory()) - .ok_or_else(|| { - Error::new( - eyre!("directory {} missing from archive", parent.display()), - ErrorKind::ParseS9pk, - ) - })?, - path.as_ref().strip_prefix(parent).unwrap(), - ) - } else { - (self.dir, path.as_ref()) - }; + let (dir, stem) = + if let Some(parent) = path.as_ref().parent().filter(|p| *p != Path::new("")) { + ( + self.dir + .get_path(parent) + .and_then(|e| e.as_directory()) + .ok_or_else(|| { + Error::new( + eyre!("directory {} missing from archive", parent.display()), + ErrorKind::ParseS9pk, + ) + })?, + path.as_ref().strip_prefix(parent).unwrap(), + ) + } else { + (self.dir, path.as_ref()) + }; let name = dir .with_stem(&stem.as_os_str().to_string_lossy()) .filter(|(_, e)| e.as_file().is_some()) @@ -69,7 +69,7 @@ impl<'a, T: Clone> Expected<'a, T> { ), ErrorKind::ParseS9pk, )), - |acc, (name, _)| + |acc, (name, _)| if valid_extension(Path::new(&*name).extension()) { match acc { Ok(_) => Err(Error::new( @@ -96,8 +96,10 @@ impl<'a, T: Clone> Expected<'a, T> { pub struct Filter(DirectoryContents<()>); impl Filter { - pub fn keep_checked(&self, dir: &mut DirectoryContents) -> Result<(), Error> { + pub fn keep_checked( + &self, + dir: &mut DirectoryContents, + ) -> Result<(), Error> { dir.filter(|path| self.0.get_path(path).is_some()) } } - diff --git a/core/startos/src/s9pk/merkle_archive/mod.rs b/core/startos/src/s9pk/merkle_archive/mod.rs index 977e5ebb2..3f30a4ce1 100644 --- a/core/startos/src/s9pk/merkle_archive/mod.rs +++ b/core/startos/src/s9pk/merkle_archive/mod.rs @@ -233,6 +233,10 @@ impl Entry { _ => None, } } + pub fn expect_file(&self) -> Result<&FileContents, Error> { + self.as_file() + .ok_or_else(|| Error::new(eyre!("not a file"), ErrorKind::ParseS9pk)) + } pub fn as_directory(&self) -> Option<&DirectoryContents> { match self.as_contents() { EntryContents::Directory(d) => Some(d), diff --git a/core/startos/src/s9pk/merkle_archive/source/mod.rs b/core/startos/src/s9pk/merkle_archive/source/mod.rs index 6b7459787..cc9623ab6 100644 --- a/core/startos/src/s9pk/merkle_archive/source/mod.rs +++ b/core/startos/src/s9pk/merkle_archive/source/mod.rs @@ -1,3 +1,5 @@ +use std::cmp::min; +use std::io::SeekFrom; use std::ops::Deref; use std::path::PathBuf; use std::sync::Arc; @@ -6,7 +8,7 @@ use blake3::Hash; use futures::future::BoxFuture; use futures::{Future, FutureExt}; use tokio::fs::File; -use tokio::io::{AsyncRead, AsyncWrite}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeekExt, AsyncWrite, Take}; use crate::prelude::*; use crate::s9pk::merkle_archive::hash::VerifyingWriter; @@ -17,8 +19,14 @@ pub mod multi_cursor_file; pub trait FileSource: Send + Sync + Sized + 'static { type Reader: AsyncRead + Unpin + Send; + type SliceReader: AsyncRead + Unpin + Send; fn size(&self) -> impl Future> + Send; fn reader(&self) -> impl Future> + Send; + fn slice( + &self, + position: u64, + size: u64, + ) -> impl Future> + Send; fn copy( &self, w: &mut W, @@ -65,12 +73,16 @@ pub trait FileSource: Send + Sync + Sized + 'static { impl FileSource for Arc { type Reader = T::Reader; + type SliceReader = T::SliceReader; async fn size(&self) -> Result { self.deref().size().await } async fn reader(&self) -> Result { self.deref().reader().await } + async fn slice(&self, position: u64, size: u64) -> Result { + self.deref().slice(position, size).await + } async fn copy(&self, w: &mut W) -> Result<(), Error> { self.deref().copy(w).await } @@ -95,12 +107,16 @@ impl DynFileSource { } impl FileSource for DynFileSource { type Reader = Box; + type SliceReader = Box; async fn size(&self) -> Result { self.0.size().await } async fn reader(&self) -> Result { self.0.reader().await } + async fn slice(&self, position: u64, size: u64) -> Result { + self.0.slice(position, size).await + } async fn copy( &self, mut w: &mut W, @@ -123,6 +139,11 @@ impl FileSource for DynFileSource { trait DynableFileSource: Send + Sync + 'static { async fn size(&self) -> Result; async fn reader(&self) -> Result, Error>; + async fn slice( + &self, + position: u64, + size: u64, + ) -> Result, Error>; async fn copy(&self, w: &mut (dyn AsyncWrite + Unpin + Send)) -> Result<(), Error>; async fn copy_verify( &self, @@ -139,6 +160,13 @@ impl DynableFileSource for T { async fn reader(&self) -> Result, Error> { Ok(Box::new(FileSource::reader(self).await?)) } + async fn slice( + &self, + position: u64, + size: u64, + ) -> Result, Error> { + Ok(Box::new(FileSource::slice(self, position, size).await?)) + } async fn copy(&self, w: &mut (dyn AsyncWrite + Unpin + Send)) -> Result<(), Error> { FileSource::copy(self, w).await } @@ -156,22 +184,34 @@ impl DynableFileSource for T { impl FileSource for PathBuf { type Reader = File; + type SliceReader = Take; async fn size(&self) -> Result { Ok(tokio::fs::metadata(self).await?.len()) } async fn reader(&self) -> Result { Ok(open_file(self).await?) } + async fn slice(&self, position: u64, size: u64) -> Result { + let mut r = FileSource::reader(self).await?; + r.seek(SeekFrom::Start(position)).await?; + Ok(r.take(size)) + } } impl FileSource for Arc<[u8]> { type Reader = std::io::Cursor; + type SliceReader = Take; async fn size(&self) -> Result { Ok(self.len() as u64) } async fn reader(&self) -> Result { Ok(std::io::Cursor::new(self.clone())) } + async fn slice(&self, position: u64, size: u64) -> Result { + let mut r = FileSource::reader(self).await?; + r.seek(SeekFrom::Start(position)).await?; + Ok(r.take(size)) + } async fn copy(&self, w: &mut W) -> Result<(), Error> { use tokio::io::AsyncWriteExt; @@ -272,12 +312,18 @@ pub struct Section { } impl FileSource for Section { type Reader = S::FetchReader; + type SliceReader = S::FetchReader; async fn size(&self) -> Result { Ok(self.size) } async fn reader(&self) -> Result { self.source.fetch(self.position, self.size).await } + async fn slice(&self, position: u64, size: u64) -> Result { + self.source + .fetch(self.position + position, min(size, self.size)) + .await + } async fn copy(&self, w: &mut W) -> Result<(), Error> { self.source.copy_to(self.position, self.size, w).await } @@ -342,12 +388,16 @@ impl From> for DynFileSource { impl FileSource for TmpSource { type Reader = ::Reader; + type SliceReader = ::SliceReader; async fn size(&self) -> Result { self.source.size().await } async fn reader(&self) -> Result { self.source.reader().await } + async fn slice(&self, position: u64, size: u64) -> Result { + self.source.slice(position, size).await + } async fn copy( &self, mut w: &mut W, diff --git a/core/startos/src/s9pk/rpc.rs b/core/startos/src/s9pk/rpc.rs index 83b78dad7..92f952077 100644 --- a/core/startos/src/s9pk/rpc.rs +++ b/core/startos/src/s9pk/rpc.rs @@ -15,14 +15,36 @@ use crate::s9pk::v2::pack::ImageConfig; use crate::s9pk::v2::SIG_CONTEXT; use crate::util::io::{create_file, open_file, TmpDir}; use crate::util::serde::{apply_expr, HandlerExtSerde}; +use crate::util::Apply; pub const SKIP_ENV: &[&str] = &["TERM", "container", "HOME", "HOSTNAME"]; pub fn s9pk() -> ParentHandler { ParentHandler::new() .subcommand("pack", from_fn_async(super::v2::pack::pack).no_display()) + .subcommand( + "list-ingredients", + from_fn_async(super::v2::pack::list_ingredients).with_custom_display_fn( + |_, ingredients| { + ingredients + .into_iter() + .map(Some) + .apply(|i| itertools::intersperse(i, None)) + .for_each(|i| { + if let Some(p) = i { + print!("{}", p.display()) + } else { + print!(" ") + } + }); + println!(); + Ok(()) + }, + ), + ) .subcommand("edit", edit()) .subcommand("inspect", inspect()) + .subcommand("convert", from_fn_async(convert).no_display()) } #[derive(Deserialize, Serialize, Parser)] @@ -193,3 +215,17 @@ async fn inspect_manifest( .await?; Ok(s9pk.as_manifest().clone()) } + +async fn convert(ctx: CliContext, S9pkPath { s9pk: s9pk_path }: S9pkPath) -> Result<(), Error> { + let mut s9pk = super::load( + MultiCursorFile::from(open_file(&s9pk_path).await?), + || ctx.developer_key().cloned(), + None, + ) + .await?; + let tmp_path = s9pk_path.with_extension("s9pk.tmp"); + s9pk.serialize(&mut create_file(&tmp_path).await?, true) + .await?; + tokio::fs::rename(tmp_path, s9pk_path).await?; + Ok(()) +} diff --git a/core/startos/src/s9pk/v2/compat.rs b/core/startos/src/s9pk/v2/compat.rs index 914d2e5aa..22250419a 100644 --- a/core/startos/src/s9pk/v2/compat.rs +++ b/core/startos/src/s9pk/v2/compat.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::path::Path; use std::sync::Arc; @@ -199,8 +199,9 @@ impl From for Manifest { let default_url = value.upstream_repo.clone(); Self { id: value.id, - title: value.title, + title: value.title.into(), version: ExtendedVersion::from(value.version).into(), + satisfies: BTreeSet::new(), release_notes: value.release_notes, license: value.license.into(), wrapper_repo: value.wrapper_repo, @@ -233,6 +234,7 @@ impl From for Manifest { DepInfo { description: value.description, optional: !value.requirement.required(), + s9pk: None, }, ) }) diff --git a/core/startos/src/s9pk/v2/manifest.rs b/core/startos/src/s9pk/v2/manifest.rs index 9607bb654..a10a65ddb 100644 --- a/core/startos/src/s9pk/v2/manifest.rs +++ b/core/startos/src/s9pk/v2/manifest.rs @@ -31,8 +31,10 @@ fn current_version() -> Version { #[ts(export)] pub struct Manifest { pub id: PackageId, - pub title: String, + #[ts(type = "string")] + pub title: InternedString, pub version: VersionString, + pub satisfies: BTreeSet, pub release_notes: String, #[ts(type = "string")] pub license: InternedString, // type of license @@ -81,6 +83,15 @@ impl Manifest { expected.check_file("LICENSE.md")?; expected.check_file("instructions.md")?; expected.check_file("javascript.squashfs")?; + for (dependency, _) in &self.dependencies.0 { + let dep_path = Path::new("dependencies").join(dependency); + let _ = expected.check_file(dep_path.join("metadata.json")); + let _ = expected.check_stem(dep_path.join("icon"), |ext| { + ext.and_then(|e| e.to_str()) + .and_then(mime) + .map_or(false, |mime| mime.starts_with("image/")) + }); + } for assets in &self.assets { expected.check_file(Path::new("assets").join(assets).with_extension("squashfs"))?; } @@ -148,7 +159,7 @@ impl Manifest { #[ts(export)] pub struct HardwareRequirements { #[serde(default)] - #[ts(type = "{ [key: string]: string }")] // TODO more specific key + #[ts(type = "{ device?: string, processor?: string }")] pub device: BTreeMap, #[ts(type = "number | null")] pub ram: Option, diff --git a/core/startos/src/s9pk/v2/mod.rs b/core/startos/src/s9pk/v2/mod.rs index 2477e63a0..e012480af 100644 --- a/core/startos/src/s9pk/v2/mod.rs +++ b/core/startos/src/s9pk/v2/mod.rs @@ -6,10 +6,10 @@ use imbl_value::InternedString; use models::{mime, DataUrl, PackageId}; use tokio::fs::File; +use crate::dependencies::DependencyMetadata; use crate::prelude::*; use crate::registry::signer::commitment::merkle_archive::MerkleArchiveCommitment; use crate::s9pk::manifest::Manifest; -use crate::s9pk::merkle_archive::file_contents::FileContents; use crate::s9pk::merkle_archive::sink::Sink; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::{ @@ -18,6 +18,7 @@ use crate::s9pk::merkle_archive::source::{ use crate::s9pk::merkle_archive::{Entry, MerkleArchive}; use crate::s9pk::v2::pack::{ImageSource, PackSource}; use crate::util::io::{open_file, TmpDir}; +use crate::util::serde::IoFormat; const MAGIC_AND_VERSION: &[u8] = &[0x3b, 0x3b, 0x02]; @@ -33,6 +34,10 @@ pub mod pack; ├── icon. ├── LICENSE.md ├── instructions.md + ├── dependencies + │ └── + │ ├── metadata.json + │ └── icon. ├── javascript.squashfs ├── assets │ └── .squashfs (xN) @@ -52,9 +57,10 @@ fn priority(s: &str) -> Option { a if Path::new(a).file_stem() == Some(OsStr::new("icon")) => Some(1), "LICENSE.md" => Some(2), "instructions.md" => Some(3), - "javascript.squashfs" => Some(4), - "assets" => Some(5), - "images" => Some(6), + "dependencies" => Some(4), + "javascript.squashfs" => Some(5), + "assets" => Some(6), + "images" => Some(7), _ => None, } } @@ -101,22 +107,16 @@ impl S9pk { filter.keep_checked(self.archive.contents_mut()) } - pub async fn icon(&self) -> Result<(InternedString, FileContents), Error> { + pub async fn icon(&self) -> Result<(InternedString, Entry), Error> { let mut best_icon = None; - for (path, icon) in self - .archive - .contents() - .with_stem("icon") - .filter(|(p, _)| { - Path::new(&*p) - .extension() - .and_then(|e| e.to_str()) - .and_then(mime) - .map_or(false, |e| e.starts_with("image/")) - }) - .filter_map(|(k, v)| v.into_file().map(|f| (k, f))) - { - let size = icon.size().await?; + for (path, icon) in self.archive.contents().with_stem("icon").filter(|(p, v)| { + Path::new(&*p) + .extension() + .and_then(|e| e.to_str()) + .and_then(mime) + .map_or(false, |e| e.starts_with("image/") && v.as_file().is_some()) + }) { + let size = icon.expect_file()?.size().await?; best_icon = match best_icon { Some((s, a)) if s >= size => Some((s, a)), _ => Some((size, (path, icon))), @@ -134,7 +134,75 @@ impl S9pk { .and_then(|e| e.to_str()) .and_then(mime) .unwrap_or("image/png"); - DataUrl::from_reader(mime, contents.reader().await?, Some(contents.size().await?)).await + Ok(DataUrl::from_vec( + mime, + contents.expect_file()?.to_vec(contents.hash()).await?, + )) + } + + pub async fn dependency_icon( + &self, + id: &PackageId, + ) -> Result)>, Error> { + let mut best_icon = None; + for (path, icon) in self + .archive + .contents() + .get_path(Path::new("dependencies").join(id)) + .and_then(|p| p.as_directory()) + .into_iter() + .flat_map(|d| { + d.with_stem("icon").filter(|(p, v)| { + Path::new(&*p) + .extension() + .and_then(|e| e.to_str()) + .and_then(mime) + .map_or(false, |e| e.starts_with("image/") && v.as_file().is_some()) + }) + }) + { + let size = icon.expect_file()?.size().await?; + best_icon = match best_icon { + Some((s, a)) if s >= size => Some((s, a)), + _ => Some((size, (path, icon))), + }; + } + Ok(best_icon.map(|(_, a)| a)) + } + + pub async fn dependency_icon_data_url( + &self, + id: &PackageId, + ) -> Result>, Error> { + let Some((name, contents)) = self.dependency_icon(id).await? else { + return Ok(None); + }; + let mime = Path::new(&*name) + .extension() + .and_then(|e| e.to_str()) + .and_then(mime) + .unwrap_or("image/png"); + Ok(Some(DataUrl::from_vec( + mime, + contents.expect_file()?.to_vec(contents.hash()).await?, + ))) + } + + pub async fn dependency_metadata( + &self, + id: &PackageId, + ) -> Result, Error> { + if let Some(entry) = self + .archive + .contents() + .get_path(Path::new("dependencies").join(id).join("metadata.json")) + { + Ok(Some(IoFormat::Json.from_slice( + &entry.expect_file()?.to_vec(entry.hash()).await?, + )?)) + } else { + Ok(None) + } } pub async fn serialize(&mut self, w: &mut W, verify: bool) -> Result<(), Error> { diff --git a/core/startos/src/s9pk/v2/pack.rs b/core/startos/src/s9pk/v2/pack.rs index ae6807c23..06a47b9d0 100644 --- a/core/startos/src/s9pk/v2/pack.rs +++ b/core/startos/src/s9pk/v2/pack.rs @@ -1,6 +1,4 @@ use std::collections::BTreeSet; -use std::ffi::OsStr; -use std::io::Cursor; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -10,25 +8,29 @@ use futures::{FutureExt, TryStreamExt}; use imbl_value::InternedString; use models::{ImageId, PackageId, VersionString}; use serde::{Deserialize, Serialize}; -use tokio::io::AsyncRead; use tokio::process::Command; use tokio::sync::OnceCell; use tokio_stream::wrappers::ReadDirStream; +use tracing::{debug, warn}; use ts_rs::TS; use crate::context::CliContext; +use crate::dependencies::DependencyMetadata; use crate::prelude::*; use crate::rpc_continuations::Guid; +use crate::s9pk::manifest::Manifest; use crate::s9pk::merkle_archive::directory_contents::DirectoryContents; +use crate::s9pk::merkle_archive::source::http::HttpSource; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::{ - into_dyn_read, ArchiveSource, DynFileSource, FileSource, TmpSource, + into_dyn_read, ArchiveSource, DynFileSource, DynRead, FileSource, TmpSource, }; use crate::s9pk::merkle_archive::{Entry, MerkleArchive}; use crate::s9pk::v2::SIG_CONTEXT; use crate::s9pk::S9pk; use crate::util::io::{create_file, open_file, TmpDir}; -use crate::util::Invoke; +use crate::util::serde::IoFormat; +use crate::util::{new_guid, Invoke, PathOrUrl}; #[cfg(not(feature = "docker"))] pub const CONTAINER_TOOL: &str = "podman"; @@ -83,7 +85,8 @@ pub enum PackSource { Squashfs(Arc), } impl FileSource for PackSource { - type Reader = Box; + type Reader = DynRead; + type SliceReader = DynRead; async fn size(&self) -> Result { match self { Self::Buffered(a) => Ok(a.len() as u64), @@ -102,11 +105,23 @@ impl FileSource for PackSource { } async fn reader(&self) -> Result { match self { - Self::Buffered(a) => Ok(into_dyn_read(Cursor::new(a.clone()))), - Self::File(f) => Ok(into_dyn_read(open_file(f).await?)), + Self::Buffered(a) => Ok(into_dyn_read(FileSource::reader(a).await?)), + Self::File(f) => Ok(into_dyn_read(FileSource::reader(f).await?)), Self::Squashfs(dir) => dir.file().await?.fetch_all().await.map(into_dyn_read), } } + async fn slice(&self, position: u64, size: u64) -> Result { + match self { + Self::Buffered(a) => Ok(into_dyn_read(FileSource::slice(a, position, size).await?)), + Self::File(f) => Ok(into_dyn_read(FileSource::slice(f, position, size).await?)), + Self::Squashfs(dir) => dir + .file() + .await? + .fetch(position, size) + .await + .map(into_dyn_read), + } + } } impl From for DynFileSource { fn from(value: PackSource) -> Self { @@ -150,24 +165,71 @@ impl PackParams { if let Some(icon) = &self.icon { Ok(icon.clone()) } else { - ReadDirStream::new(tokio::fs::read_dir(self.path()).await?).try_filter(|x| ready(x.path().file_stem() == Some(OsStr::new("icon")))).map_err(Error::from).try_fold(Err(Error::new(eyre!("icon not found"), ErrorKind::NotFound)), |acc, x| async move { match acc { - Ok(_) => Err(Error::new(eyre!("multiple icons found in working directory, please specify which to use with `--icon`"), ErrorKind::InvalidRequest)), - Err(e) => Ok({ - let path = x.path(); - if path.file_stem().and_then(|s| s.to_str()) == Some("icon") { - Ok(path) - } else { - Err(e) - } + ReadDirStream::new(tokio::fs::read_dir(self.path()).await?) + .try_filter(|x| { + ready( + x.path() + .file_stem() + .map_or(false, |s| s.eq_ignore_ascii_case("icon")), + ) }) - }}).await? + .map_err(Error::from) + .try_fold( + Err(Error::new(eyre!("icon not found"), ErrorKind::NotFound)), + |acc, x| async move { + match acc { + Ok(_) => Err(Error::new(eyre!("multiple icons found in working directory, please specify which to use with `--icon`"), ErrorKind::InvalidRequest)), + Err(e) => Ok({ + let path = x.path(); + if path + .file_stem() + .map_or(false, |s| s.eq_ignore_ascii_case("icon")) + { + Ok(path) + } else { + Err(e) + } + }), + } + }, + ) + .await? } } - fn license(&self) -> PathBuf { - self.license - .as_ref() - .cloned() - .unwrap_or_else(|| self.path().join("LICENSE.md")) + async fn license(&self) -> Result { + if let Some(license) = &self.license { + Ok(license.clone()) + } else { + ReadDirStream::new(tokio::fs::read_dir(self.path()).await?) + .try_filter(|x| { + ready( + x.path() + .file_stem() + .map_or(false, |s| s.eq_ignore_ascii_case("license")), + ) + }) + .map_err(Error::from) + .try_fold( + Err(Error::new(eyre!("icon not found"), ErrorKind::NotFound)), + |acc, x| async move { + match acc { + Ok(_) => Err(Error::new(eyre!("multiple licenses found in working directory, please specify which to use with `--license`"), ErrorKind::InvalidRequest)), + Err(e) => Ok({ + let path = x.path(); + if path + .file_stem() + .map_or(false, |s| s.eq_ignore_ascii_case("license")) + { + Ok(path) + } else { + Err(e) + } + }), + } + }, + ) + .await? + } } fn instructions(&self) -> PathBuf { self.instructions @@ -282,6 +344,15 @@ pub enum ImageSource { DockerTag(String), } impl ImageSource { + pub fn ingredients(&self) -> Vec { + match self { + Self::Packed => Vec::new(), + Self::DockerBuild { dockerfile, .. } => { + vec![dockerfile.clone().unwrap_or_else(|| "Dockerfile".into())] + } + Self::DockerTag(_) => Vec::new(), + } + } #[instrument(skip_all)] pub fn load<'a, S: From> + FileSource + Clone>( &'a self, @@ -320,7 +391,7 @@ impl ImageSource { format!("--platform=linux/{arch}") }; // docker buildx build ${path} -o type=image,name=start9/${id} - let tag = format!("start9/{id}/{image_id}:{version}"); + let tag = format!("start9/{id}/{image_id}:{}", new_guid()); Command::new(CONTAINER_TOOL) .arg("build") .arg(workdir) @@ -501,7 +572,7 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> { "LICENSE.md".into(), Entry::file(TmpSource::new( tmp_dir.clone(), - PackSource::File(params.license()), + PackSource::File(params.license().await?), )), ); files.insert( @@ -541,6 +612,54 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> { s9pk.load_images(tmp_dir.clone()).await?; + let mut to_insert = Vec::new(); + for (id, dependency) in &mut s9pk.as_manifest_mut().dependencies.0 { + if let Some(s9pk) = dependency.s9pk.take() { + let s9pk = match s9pk { + PathOrUrl::Path(path) => { + S9pk::deserialize(&MultiCursorFile::from(open_file(path).await?), None) + .await? + .into_dyn() + } + PathOrUrl::Url(url) => { + if url.scheme() == "http" || url.scheme() == "https" { + S9pk::deserialize( + &Arc::new(HttpSource::new(ctx.client.clone(), url).await?), + None, + ) + .await? + .into_dyn() + } else { + return Err(Error::new( + eyre!("unknown scheme: {}", url.scheme()), + ErrorKind::InvalidRequest, + )); + } + } + }; + let dep_path = Path::new("dependencies").join(id); + to_insert.push(( + dep_path.join("metadata.json"), + Entry::file(PackSource::Buffered( + IoFormat::Json + .to_vec(&DependencyMetadata { + title: s9pk.as_manifest().title.clone(), + })? + .into(), + )), + )); + let icon = s9pk.icon().await?; + to_insert.push(( + dep_path.join(&*icon.0), + Entry::file(PackSource::Buffered( + icon.1.expect_file()?.to_vec(icon.1.hash()).await?.into(), + )), + )); + } else { + warn!("no s9pk specified for {id}, leaving metadata empty"); + } + } + s9pk.validate_and_filter(None)?; s9pk.serialize( @@ -555,3 +674,58 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> { Ok(()) } + +#[instrument(skip_all)] +pub async fn list_ingredients(_: CliContext, params: PackParams) -> Result, Error> { + let js_path = params.javascript().join("index.js"); + let manifest: Manifest = match async { + serde_json::from_slice( + &Command::new("node") + .arg("-e") + .arg(format!( + "console.log(JSON.stringify(require('{}').manifest))", + js_path.display() + )) + .invoke(ErrorKind::Javascript) + .await?, + ) + .with_kind(ErrorKind::Deserialization) + } + .await + { + Ok(m) => m, + Err(e) => { + warn!("failed to load manifest: {e}"); + debug!("{e:?}"); + return Ok(vec![ + js_path, + params.icon().await?, + params.license().await?, + params.instructions(), + ]); + } + }; + let mut ingredients = vec![ + js_path, + params.icon().await?, + params.license().await?, + params.instructions(), + ]; + + for (_, dependency) in manifest.dependencies.0 { + if let Some(PathOrUrl::Path(p)) = dependency.s9pk { + ingredients.push(p); + } + } + + let assets_dir = params.assets(); + for assets in manifest.assets { + ingredients.push(assets_dir.join(assets)); + } + + for image in manifest.images.values() { + ingredients.extend(image.source.ingredients()); + } + + Ok(ingredients) +} diff --git a/core/startos/src/service/service_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs index 7547584e0..79ca4dc42 100644 --- a/core/startos/src/service/service_effect_handler.rs +++ b/core/startos/src/service/service_effect_handler.rs @@ -1147,18 +1147,14 @@ enum DependencyRequirement { #[ts(type = "string[]")] health_checks: BTreeSet, #[ts(type = "string")] - version_spec: VersionRange, - #[ts(type = "string")] - registry_url: Url, + version_range: VersionRange, }, #[serde(rename_all = "camelCase")] Exists { #[ts(type = "string")] id: PackageId, #[ts(type = "string")] - version_spec: VersionRange, - #[ts(type = "string")] - registry_url: Url, + version_range: VersionRange, }, } // filebrowser:exists,bitcoind:running:foo+bar+baz @@ -1168,8 +1164,7 @@ impl FromStr for DependencyRequirement { match s.split_once(':') { Some((id, "e")) | Some((id, "exists")) => Ok(Self::Exists { id: id.parse()?, - registry_url: "".parse()?, // TODO - version_spec: "*".parse()?, // TODO + version_range: "*".parse()?, // TODO }), Some((id, rest)) => { let health_checks = match rest.split_once(':') { @@ -1192,15 +1187,13 @@ impl FromStr for DependencyRequirement { Ok(Self::Running { id: id.parse()?, health_checks, - registry_url: "".parse()?, // TODO - version_spec: "*".parse()?, // TODO + version_range: "*".parse()?, // TODO }) } None => Ok(Self::Running { id: s.parse()?, health_checks: BTreeSet::new(), - registry_url: "".parse()?, // TODO - version_spec: "*".parse()?, // TODO + version_range: "*".parse()?, // TODO }), } } @@ -1234,59 +1227,20 @@ async fn set_dependencies( let mut deps = BTreeMap::new(); for dependency in dependencies { - let (dep_id, kind, registry_url, version_spec) = match dependency { - DependencyRequirement::Exists { - id, - registry_url, - version_spec, - } => ( - id, - CurrentDependencyKind::Exists, - registry_url, - version_spec, - ), + let (dep_id, kind, version_range) = match dependency { + DependencyRequirement::Exists { id, version_range } => { + (id, CurrentDependencyKind::Exists, version_range) + } DependencyRequirement::Running { id, health_checks, - registry_url, - version_spec, + version_range, } => ( id, CurrentDependencyKind::Running { health_checks }, - registry_url, - version_spec, + version_range, ), }; - let (icon, title) = match async { - let remote_s9pk = S9pk::deserialize( - &Arc::new( - HttpSource::new( - context.seed.ctx.client.clone(), - registry_url - .join(&format!("package/v2/{}.s9pk?spec={}", dep_id, version_spec))?, - ) - .await?, - ), - None, // TODO - ) - .await?; - - let icon = remote_s9pk.icon_data_url().await?; - - Ok::<_, Error>((icon, remote_s9pk.as_manifest().title.clone())) - } - .await - { - Ok(a) => a, - Err(e) => { - tracing::error!("Error fetching remote s9pk: {e}"); - tracing::debug!("{e:?}"); - ( - DataUrl::from_slice("image/png", include_bytes!("../install/package-icon.png")), - dep_id.to_string(), - ) - } - }; let config_satisfied = if let Some(dep_service) = &*context.seed.ctx.services.get(&dep_id).await { context @@ -1300,17 +1254,25 @@ async fn set_dependencies( } else { true }; - deps.insert( - dep_id, - CurrentDependencyInfo { - kind, - registry_url, - version_spec, - icon, - title, - config_satisfied, - }, - ); + let info = CurrentDependencyInfo { + title: context + .seed + .persistent_container + .s9pk + .dependency_metadata(&dep_id) + .await? + .map(|m| m.title), + icon: context + .seed + .persistent_container + .s9pk + .dependency_icon_data_url(&dep_id) + .await?, + kind, + version_range, + config_satisfied, + }; + deps.insert(dep_id, info); } context .seed @@ -1343,23 +1305,19 @@ async fn get_dependencies(context: EffectContext) -> Result(match kind { - CurrentDependencyKind::Exists => DependencyRequirement::Exists { - id, - registry_url, - version_spec, - }, + CurrentDependencyKind::Exists => { + DependencyRequirement::Exists { id, version_range } + } CurrentDependencyKind::Running { health_checks } => { DependencyRequirement::Running { id, health_checks, - version_spec, - registry_url, + version_range, } } }) @@ -1381,7 +1339,8 @@ struct CheckDependenciesResult { package_id: PackageId, is_installed: bool, is_running: bool, - health_checks: Vec, + config_satisfied: bool, + health_checks: BTreeMap, #[ts(type = "string | null")] version: Option, } @@ -1415,24 +1374,27 @@ async fn check_dependencies( package_id, is_installed: false, is_running: false, - health_checks: vec![], + config_satisfied: false, + health_checks: Default::default(), version: None, }); continue; }; - let installed_version = package - .as_state_info() - .as_manifest(ManifestPreference::New) - .as_version() - .de()? - .into_version(); + let manifest = package.as_state_info().as_manifest(ManifestPreference::New); + let installed_version = manifest.as_version().de()?.into_version(); + let satisfies = manifest.as_satisfies().de()?; let version = Some(installed_version.clone()); - if !installed_version.satisfies(&dependency_info.version_spec) { + if ![installed_version] + .into_iter() + .chain(satisfies.into_iter().map(|v| v.into_version())) + .any(|v| v.satisfies(&dependency_info.version_range)) + { results.push(CheckDependenciesResult { package_id, is_installed: false, is_running: false, - health_checks: vec![], + config_satisfied: false, + health_checks: Default::default(), version, }); continue; @@ -1444,17 +1406,23 @@ async fn check_dependencies( } else { false }; - let health_checks = status - .health() - .cloned() - .unwrap_or_default() - .into_iter() - .map(|(_, val)| val) - .collect(); + let health_checks = + if let CurrentDependencyKind::Running { health_checks } = &dependency_info.kind { + status + .health() + .cloned() + .unwrap_or_default() + .into_iter() + .filter(|(id, _)| health_checks.contains(id)) + .collect() + } else { + Default::default() + }; results.push(CheckDependenciesResult { package_id, is_installed, is_running, + config_satisfied: dependency_info.config_satisfied, health_checks, version, }); diff --git a/core/startos/src/util/mod.rs b/core/startos/src/util/mod.rs index f591eaaf3..3b66d7507 100644 --- a/core/startos/src/util/mod.rs +++ b/core/startos/src/util/mod.rs @@ -1,13 +1,16 @@ use std::collections::{BTreeMap, VecDeque}; +use std::fmt; use std::future::Future; use std::marker::PhantomData; use std::path::{Path, PathBuf}; use std::pin::Pin; use std::process::Stdio; +use std::str::FromStr; use std::sync::Arc; use std::task::{Context, Poll}; use std::time::Duration; +use ::serde::{Deserialize, Serialize}; use async_trait::async_trait; use color_eyre::eyre::{self, eyre}; use fd_lock_rs::FdLock; @@ -24,9 +27,12 @@ use tokio::fs::File; use tokio::io::{AsyncRead, AsyncReadExt, BufReader}; use tokio::sync::{oneshot, Mutex, OwnedMutexGuard, RwLock}; use tracing::instrument; +use ts_rs::TS; +use url::Url; use crate::shutdown::Shutdown; use crate::util::io::create_file; +use crate::util::serde::{deserialize_from_str, serialize_display}; use crate::{Error, ErrorKind, ResultExt as _}; pub mod actor; pub mod clap; @@ -648,3 +654,48 @@ pub fn new_guid() -> InternedString { &buf, )) } + +#[derive(Debug, Clone, TS)] +#[ts(type = "string")] +pub enum PathOrUrl { + Path(PathBuf), + Url(Url), +} +impl FromStr for PathOrUrl { + type Err = ::Err; + fn from_str(s: &str) -> Result { + if let Ok(url) = s.parse::() { + if url.scheme() == "file" { + Ok(Self::Path(url.path().parse()?)) + } else { + Ok(Self::Url(url)) + } + } else { + Ok(Self::Path(s.parse()?)) + } + } +} +impl fmt::Display for PathOrUrl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Path(p) => write!(f, "file://{}", p.display()), + Self::Url(u) => write!(f, "{u}"), + } + } +} +impl<'de> Deserialize<'de> for PathOrUrl { + fn deserialize(deserializer: D) -> Result + where + D: ::serde::Deserializer<'de>, + { + deserialize_from_str(deserializer) + } +} +impl Serialize for PathOrUrl { + fn serialize(&self, serializer: S) -> Result + where + S: ::serde::Serializer, + { + serialize_display(self, serializer) + } +} diff --git a/core/startos/src/util/net.rs b/core/startos/src/util/net.rs index 93131f16e..9e1beeaba 100644 --- a/core/startos/src/util/net.rs +++ b/core/startos/src/util/net.rs @@ -1,19 +1,25 @@ +use core::fmt; use std::borrow::Cow; +use std::sync::Mutex; use axum::extract::ws::{self, CloseFrame}; -use futures::Future; +use futures::{Future, Stream, StreamExt}; use crate::prelude::*; pub trait WebSocketExt { fn normal_close( self, - msg: impl Into>, - ) -> impl Future>; + msg: impl Into> + Send, + ) -> impl Future> + Send; + fn close_result( + self, + result: Result> + Send, impl fmt::Display + Send>, + ) -> impl Future> + Send; } impl WebSocketExt for ws::WebSocket { - async fn normal_close(mut self, msg: impl Into>) -> Result<(), Error> { + async fn normal_close(mut self, msg: impl Into> + Send) -> Result<(), Error> { self.send(ws::Message::Close(Some(CloseFrame { code: 1000, reason: msg.into(), @@ -21,4 +27,41 @@ impl WebSocketExt for ws::WebSocket { .await .with_kind(ErrorKind::Network) } + async fn close_result( + mut self, + result: Result> + Send, impl fmt::Display + Send>, + ) -> Result<(), Error> { + match result { + Ok(msg) => self + .send(ws::Message::Close(Some(CloseFrame { + code: 1000, + reason: msg.into(), + }))) + .await + .with_kind(ErrorKind::Network), + Err(e) => self + .send(ws::Message::Close(Some(CloseFrame { + code: 1011, + reason: e.to_string().into(), + }))) + .await + .with_kind(ErrorKind::Network), + } + } +} + +pub struct SyncBody(Mutex); +impl From for SyncBody { + fn from(value: axum::body::Body) -> Self { + SyncBody(Mutex::new(value.into_data_stream())) + } +} +impl Stream for SyncBody { + type Item = ::Item; + fn poll_next( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + self.0.lock().unwrap().poll_next_unpin(cx) + } } diff --git a/core/startos/src/util/rpc.rs b/core/startos/src/util/rpc.rs index 54664833b..80d6d9251 100644 --- a/core/startos/src/util/rpc.rs +++ b/core/startos/src/util/rpc.rs @@ -12,7 +12,7 @@ use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::ArchiveSource; use crate::util::io::{open_file, ParallelBlake3Writer}; use crate::util::serde::Base16; -use crate::util::Apply; +use crate::util::{Apply, PathOrUrl}; use crate::CAP_10_MiB; pub fn util() -> ParentHandler { @@ -45,21 +45,20 @@ pub async fn b3sum( } b3sum_source(file).await } - if let Ok(url) = file.parse::() { - if url.scheme() == "file" { - b3sum_file(url.path(), allow_mmap).await - } else if url.scheme() == "http" || url.scheme() == "https" { - HttpSource::new(ctx.client.clone(), url) - .await? - .apply(b3sum_source) - .await - } else { - return Err(Error::new( - eyre!("unknown scheme: {}", url.scheme()), - ErrorKind::InvalidRequest, - )); + match file.parse::()? { + PathOrUrl::Path(path) => b3sum_file(path, allow_mmap).await, + PathOrUrl::Url(url) => { + if url.scheme() == "http" || url.scheme() == "https" { + HttpSource::new(ctx.client.clone(), url) + .await? + .apply(b3sum_source) + .await + } else { + Err(Error::new( + eyre!("unknown scheme: {}", url.scheme()), + ErrorKind::InvalidRequest, + )) + } } - } else { - b3sum_file(file, allow_mmap).await } } diff --git a/core/startos/src/util/serde.rs b/core/startos/src/util/serde.rs index 44a69165e..bac2c524a 100644 --- a/core/startos/src/util/serde.rs +++ b/core/startos/src/util/serde.rs @@ -1,8 +1,10 @@ +use std::any::Any; use std::collections::VecDeque; use std::marker::PhantomData; use std::ops::Deref; use std::str::FromStr; +use base64::Engine; use clap::builder::ValueParserFactory; use clap::{ArgMatches, CommandFactory, FromArgMatches}; use color_eyre::eyre::eyre; @@ -37,7 +39,11 @@ pub fn deserialize_from_str< { type Value = T; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(formatter, "a parsable string") + write!( + formatter, + "a string that can be parsed as a {}", + std::any::type_name::() + ) } fn visit_str(self, v: &str) -> Result where @@ -988,18 +994,24 @@ impl> Serialize for Base32 { } } +pub const BASE64: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new( + &base64::alphabet::STANDARD, + base64::engine::GeneralPurposeConfig::new(), +); + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, TS)] #[ts(type = "string", concrete(T = Vec))] pub struct Base64(pub T); impl> std::fmt::Display for Base64 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&base64::encode(self.0.as_ref())) + f.write_str(&BASE64.encode(self.0.as_ref())) } } impl>> FromStr for Base64 { type Err = Error; fn from_str(s: &str) -> Result { - base64::decode(&s) + BASE64 + .decode(&s) .with_kind(ErrorKind::Deserialization)? .apply(TryFrom::try_from) .map(Self) diff --git a/debian/postinst b/debian/postinst index 96e392fc8..cafa691e0 100755 --- a/debian/postinst +++ b/debian/postinst @@ -49,9 +49,9 @@ managed=true EOF $SYSTEMCTL enable startd.service $SYSTEMCTL enable systemd-resolved.service -$SYSTEMCTL enable systemd-networkd-wait-online.service $SYSTEMCTL enable ssh.service $SYSTEMCTL disable wpa_supplicant.service +$SYSTEMCTL mask systemd-networkd-wait-online.service # currently use `NetworkManager-wait-online.service` $SYSTEMCTL disable docker.service $SYSTEMCTL disable postgresql.service diff --git a/sdk/.prettierignore b/sdk/.prettierignore new file mode 100644 index 000000000..19b24bbe8 --- /dev/null +++ b/sdk/.prettierignore @@ -0,0 +1 @@ +/lib/exver/exver.ts \ No newline at end of file diff --git a/sdk/Makefile b/sdk/Makefile index 4d01fa3d7..ef1c886f5 100644 --- a/sdk/Makefile +++ b/sdk/Makefile @@ -17,6 +17,9 @@ lib/test/output.ts: node_modules lib/test/makeOutput.ts scripts/oldSpecToBuilder bundle: dist | test fmt touch dist +lib/exver/exver.ts: node_modules lib/exver/exver.pegjs + npx peggy --allowed-start-rules '*' --plugin ./node_modules/ts-pegjs/dist/tspegjs -o lib/exver/exver.ts lib/exver/exver.pegjs + dist: $(TS_FILES) package.json node_modules README.md LICENSE npx tsc npx tsc --project tsconfig-cjs.json @@ -31,7 +34,7 @@ check: npm run check fmt: node_modules - npx prettier --write "**/*.ts" + npx prettier . "**/*.ts" --write node_modules: package.json npm ci diff --git a/sdk/jest.config.js b/sdk/jest.config.js index c6aed8f3d..c38fa5062 100644 --- a/sdk/jest.config.js +++ b/sdk/jest.config.js @@ -5,4 +5,4 @@ module.exports = { testEnvironment: "node", rootDir: "./lib/", modulePathIgnorePatterns: ["./dist/"], -}; +} diff --git a/sdk/lib/Dependency.ts b/sdk/lib/Dependency.ts index 1e70629da..067ed653e 100644 --- a/sdk/lib/Dependency.ts +++ b/sdk/lib/Dependency.ts @@ -1,17 +1,17 @@ -import { Checker } from "./emverLite/mod" +import { VersionRange } from "./exver" export class Dependency { constructor( readonly data: | { type: "running" - versionSpec: Checker + versionRange: VersionRange registryUrl: string healthChecks: string[] } | { type: "exists" - versionSpec: Checker + versionRange: VersionRange registryUrl: string }, ) {} diff --git a/sdk/lib/StartSdk.ts b/sdk/lib/StartSdk.ts index fac78a4af..f09d83750 100644 --- a/sdk/lib/StartSdk.ts +++ b/sdk/lib/StartSdk.ts @@ -1,4 +1,3 @@ -import { ManifestVersion, SDKManifest } from "./manifest/ManifestTypes" import { RequiredDefault, Value } from "./config/builder/value" import { Config, ExtractConfigType, LazyBuild } from "./config/builder/config" import { @@ -21,7 +20,6 @@ import { MaybePromise, ServiceInterfaceId, PackageId, - ValidIfNoStupidEscape, } from "./types" import * as patterns from "./util/patterns" import { DependencyConfig, Update } from "./dependencies/DependencyConfig" @@ -74,12 +72,14 @@ import { splitCommand } from "./util/splitCommand" import { Mounts } from "./mainFn/Mounts" import { Dependency } from "./Dependency" import * as T from "./types" -import { Checker, EmVer } from "./emverLite/mod" +import { testTypeVersion, ValidateExVer } from "./exver" import { ExposedStorePaths } from "./store/setupExposeStore" import { PathBuilder, extractJsonPath, pathBuilder } from "./store/PathBuilder" import { checkAllDependencies } from "./dependencies/dependencies" import { health } from "." +export const SDKVersion = testTypeVersion("0.3.6") + // prettier-ignore type AnyNeverCond = T extends [] ? Else : @@ -98,12 +98,12 @@ function removeConstType() { return (t: T) => t as T & (E extends MainEffects ? {} : { const: never }) } -export class StartSdk { +export class StartSdk { private constructor(readonly manifest: Manifest) {} static of() { return new StartSdk(null as never) } - withManifest(manifest: Manifest) { + withManifest(manifest: Manifest) { return new StartSdk(manifest) } withStore>() { @@ -191,7 +191,7 @@ export class StartSdk { id: keyof Manifest["images"] & T.ImageId sharedRun?: boolean }, - command: ValidIfNoStupidEscape | [string, ...string[]], + command: T.CommandType, options: CommandOptions & { mounts?: { path: string; options: MountOptions }[] }, @@ -335,7 +335,7 @@ export class StartSdk { ([ id, { - data: { versionSpec, ...x }, + data: { versionRange, ...x }, }, ]) => ({ id, @@ -348,7 +348,7 @@ export class StartSdk { : { kind: "exists", }), - versionSpec: versionSpec.range, + versionRange: versionRange.toString(), }), ), }) @@ -432,9 +432,6 @@ export class StartSdk { spec: Spec, ) => Config.of(spec), }, - Checker: { - parse: Checker.parse, - }, Daemons: { of(config: { effects: Effects @@ -474,10 +471,6 @@ export class StartSdk { >(dependencyConfig, update) }, }, - EmVer: { - from: EmVer.from, - parse: EmVer.parse, - }, List: { text: List.text, obj: >( @@ -524,8 +517,8 @@ export class StartSdk { ) => List.dynamicText(getA), }, Migration: { - of: (options: { - version: Version + of: (options: { + version: Version & ValidateExVer up: (opts: { effects: Effects }) => Promise down: (opts: { effects: Effects }) => Promise }) => Migration.of(options), @@ -720,7 +713,7 @@ export class StartSdk { } } -export async function runCommand( +export async function runCommand( effects: Effects, image: { id: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean }, command: string | [string, ...string[]], diff --git a/sdk/lib/actions/createAction.ts b/sdk/lib/actions/createAction.ts index 2fe4dfa74..4fa858d56 100644 --- a/sdk/lib/actions/createAction.ts +++ b/sdk/lib/actions/createAction.ts @@ -1,12 +1,13 @@ +import * as T from "../types" import { Config, ExtractConfigType } from "../config/builder/config" -import { SDKManifest } from "../manifest/ManifestTypes" + import { ActionMetadata, ActionResult, Effects, ExportedAction } from "../types" -export type MaybeFn = +export type MaybeFn = | Value | ((options: { effects: Effects }) => Promise | Value) export class CreatedAction< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, ConfigType extends | Record @@ -30,7 +31,7 @@ export class CreatedAction< ) {} static of< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, ConfigType extends | Record diff --git a/sdk/lib/actions/setupActions.ts b/sdk/lib/actions/setupActions.ts index 9dd9937b4..07b4e2606 100644 --- a/sdk/lib/actions/setupActions.ts +++ b/sdk/lib/actions/setupActions.ts @@ -1,8 +1,8 @@ -import { SDKManifest } from "../manifest/ManifestTypes" +import * as T from "../types" import { Effects, ExpectedExports } from "../types" import { CreatedAction } from "./createAction" -export function setupActions( +export function setupActions( ...createdActions: CreatedAction[] ) { const myActions = async (options: { effects: Effects }) => { diff --git a/sdk/lib/backup/Backups.ts b/sdk/lib/backup/Backups.ts index 6ffe74b70..6751b1910 100644 --- a/sdk/lib/backup/Backups.ts +++ b/sdk/lib/backup/Backups.ts @@ -1,5 +1,3 @@ -import { recursive } from "ts-matches" -import { SDKManifest } from "../manifest/ManifestTypes" import * as T from "../types" import * as child_process from "child_process" @@ -41,14 +39,14 @@ export type BackupSet = { * ).build()q * ``` */ -export class Backups { +export class Backups { static BACKUP: BACKUP = "BACKUP" private constructor( private options = DEFAULT_OPTIONS, private backupSet = [] as BackupSet[], ) {} - static volumes( + static volumes( ...volumeNames: Array ): Backups { return new Backups().addSets( @@ -60,12 +58,12 @@ export class Backups { })), ) } - static addSets( + static addSets( ...options: BackupSet[] ) { return new Backups().addSets(...options) } - static with_options( + static with_options( options?: Partial, ) { return new Backups({ ...DEFAULT_OPTIONS, ...options }) diff --git a/sdk/lib/backup/setupBackups.ts b/sdk/lib/backup/setupBackups.ts index 69d1bf7df..40be01829 100644 --- a/sdk/lib/backup/setupBackups.ts +++ b/sdk/lib/backup/setupBackups.ts @@ -1,13 +1,13 @@ import { Backups } from "./Backups" -import { SDKManifest } from "../manifest/ManifestTypes" -import { ExpectedExports, PathMaker } from "../types" + +import * as T from "../types" import { _ } from "../util" -export type SetupBackupsParams = Array< +export type SetupBackupsParams = Array< M["volumes"][number] | Backups > -export function setupBackups( +export function setupBackups( ...args: _> ) { const backups = Array>() @@ -21,22 +21,22 @@ export function setupBackups( } backups.push(Backups.volumes(...volumes)) const answer: { - createBackup: ExpectedExports.createBackup - restoreBackup: ExpectedExports.restoreBackup + createBackup: T.ExpectedExports.createBackup + restoreBackup: T.ExpectedExports.restoreBackup } = { get createBackup() { return (async (options) => { for (const backup of backups) { await backup.build(options.pathMaker).createBackup(options) } - }) as ExpectedExports.createBackup + }) as T.ExpectedExports.createBackup }, get restoreBackup() { return (async (options) => { for (const backup of backups) { await backup.build(options.pathMaker).restoreBackup(options) } - }) as ExpectedExports.restoreBackup + }) as T.ExpectedExports.restoreBackup }, } return answer diff --git a/sdk/lib/config/configDependencies.ts b/sdk/lib/config/configDependencies.ts index 2ab091e18..d9865f25c 100644 --- a/sdk/lib/config/configDependencies.ts +++ b/sdk/lib/config/configDependencies.ts @@ -1,22 +1,21 @@ -import { SDKManifest } from "../manifest/ManifestTypes" -import { Dependencies } from "../types" +import * as T from "../types" -export type ConfigDependencies = { - exists(id: keyof T["dependencies"]): Dependencies[number] +export type ConfigDependencies = { + exists(id: keyof T["dependencies"]): T.Dependencies[number] running( id: keyof T["dependencies"], healthChecks: string[], - ): Dependencies[number] + ): T.Dependencies[number] } export const configDependenciesSet = < - T extends SDKManifest, + T extends T.Manifest, >(): ConfigDependencies => ({ exists(id: keyof T["dependencies"]) { return { id, kind: "exists", - } as Dependencies[number] + } as T.Dependencies[number] }, running(id: keyof T["dependencies"], healthChecks: string[]) { @@ -24,6 +23,6 @@ export const configDependenciesSet = < id, kind: "running", healthChecks, - } as Dependencies[number] + } as T.Dependencies[number] }, }) diff --git a/sdk/lib/config/setupConfig.ts b/sdk/lib/config/setupConfig.ts index ba82dbad6..8a1550d57 100644 --- a/sdk/lib/config/setupConfig.ts +++ b/sdk/lib/config/setupConfig.ts @@ -1,5 +1,5 @@ -import { Effects, ExpectedExports } from "../types" -import { SDKManifest } from "../manifest/ManifestTypes" +import * as T from "../types" + import * as D from "./configDependencies" import { Config, ExtractConfigType } from "./builder/config" import nullIfEmpty from "../util/nullIfEmpty" @@ -16,7 +16,7 @@ export type Save< | Config, any> | Config, never>, > = (options: { - effects: Effects + effects: T.Effects input: ExtractConfigType & Record }) => Promise<{ dependenciesReceipt: DependenciesReceipt @@ -24,14 +24,14 @@ export type Save< restart: boolean }> export type Read< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, A extends | Record | Config, any> | Config, never>, > = (options: { - effects: Effects + effects: T.Effects }) => Promise & Record)> /** * We want to setup a config export with a get and set, this @@ -46,7 +46,7 @@ export function setupConfig< | Record | Config | Config, - Manifest extends SDKManifest, + Manifest extends T.Manifest, Type extends Record = ExtractConfigType, >( spec: Config | Config, @@ -69,7 +69,7 @@ export function setupConfig< if (restart) { await effects.restart() } - }) as ExpectedExports.setConfig, + }) as T.ExpectedExports.setConfig, getConfig: (async ({ effects }) => { const configValue = nullIfEmpty((await read({ effects })) || null) return { @@ -78,7 +78,7 @@ export function setupConfig< }), config: configValue, } - }) as ExpectedExports.getConfig, + }) as T.ExpectedExports.getConfig, } } diff --git a/sdk/lib/dependencies/DependencyConfig.ts b/sdk/lib/dependencies/DependencyConfig.ts index d7ce435ad..b48bf56d3 100644 --- a/sdk/lib/dependencies/DependencyConfig.ts +++ b/sdk/lib/dependencies/DependencyConfig.ts @@ -1,11 +1,6 @@ -import { - DependencyConfig as DependencyConfigType, - DeepPartial, - Effects, -} from "../types" +import * as T from "../types" import { deepEqual } from "../util/deepEqual" import { deepMerge } from "../util/deepMerge" -import { SDKManifest } from "../manifest/ManifestTypes" export type Update = (options: { remoteConfig: RemoteConfig @@ -13,7 +8,7 @@ export type Update = (options: { }) => Promise export class DependencyConfig< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, Input extends Record, RemoteConfig extends Record, @@ -26,16 +21,16 @@ export class DependencyConfig< } constructor( readonly dependencyConfig: (options: { - effects: Effects + effects: T.Effects localConfig: Input - }) => Promise>, + }) => Promise>, readonly update: Update< - void | DeepPartial, + void | T.DeepPartial, RemoteConfig > = DependencyConfig.defaultUpdate as any, ) {} - async query(options: { effects: Effects; localConfig: unknown }) { + async query(options: { effects: T.Effects; localConfig: unknown }) { return this.dependencyConfig({ localConfig: options.localConfig as Input, effects: options.effects, diff --git a/sdk/lib/dependencies/dependencies.ts b/sdk/lib/dependencies/dependencies.ts index c074d2ad7..28b04a07b 100644 --- a/sdk/lib/dependencies/dependencies.ts +++ b/sdk/lib/dependencies/dependencies.ts @@ -3,20 +3,23 @@ import { PackageId, DependencyRequirement, SetHealth, - CheckDependencyResult, + CheckDependenciesResult, } from "../types" export type CheckAllDependencies = { - notRunning: () => Promise - - notInstalled: () => Promise - + notInstalled: () => Promise + notRunning: () => Promise + configNotSatisfied: () => Promise healthErrors: () => Promise<{ [id: string]: SetHealth[] }> - throwIfNotRunning: () => Promise - throwIfNotValid: () => Promise - throwIfNotInstalled: () => Promise - throwIfError: () => Promise + isValid: () => Promise + + throwIfNotRunning: () => Promise + throwIfNotInstalled: () => Promise + throwIfConfigNotSatisfied: () => Promise + throwIfHealthError: () => Promise + + throwIfNotValid: () => Promise } export function checkAllDependencies(effects: Effects): CheckAllDependencies { const dependenciesPromise = effects.getDependencies() @@ -45,14 +48,16 @@ export function checkAllDependencies(effects: Effects): CheckAllDependencies { if (!dependency) continue if (dependency.kind !== "running") continue - const healthChecks = result.healthChecks - .filter((x) => dependency.healthChecks.includes(x.id)) + const healthChecks = Object.entries(result.healthChecks) + .map(([id, hc]) => ({ ...hc, id })) .filter((x) => !!x.message) if (healthChecks.length === 0) continue answer[result.packageId] = healthChecks } return answer } + const configNotSatisfied = () => + resultsPromise.then((x) => x.filter((x) => !x.configSatisfied)) const notInstalled = () => resultsPromise.then((x) => x.filter((x) => !x.isInstalled)) const notRunning = async () => { @@ -68,7 +73,7 @@ export function checkAllDependencies(effects: Effects): CheckAllDependencies { const entries = (x: { [k: string]: B }) => Object.entries(x) const first = (x: A[]): A | undefined => x[0] const sinkVoid = (x: A) => void 0 - const throwIfError = () => + const throwIfHealthError = () => healthErrors() .then(entries) .then(first) @@ -78,6 +83,14 @@ export function checkAllDependencies(effects: Effects): CheckAllDependencies { if (healthChecks.length > 0) throw `Package ${id} has the following errors: ${healthChecks.map((x) => x.message).join(", ")}` }) + + const throwIfConfigNotSatisfied = () => + configNotSatisfied().then((results) => { + throw new Error( + `Package ${results[0].packageId} does not have a valid configuration`, + ) + }) + const throwIfNotRunning = () => notRunning().then((results) => { if (results[0]) @@ -93,7 +106,8 @@ export function checkAllDependencies(effects: Effects): CheckAllDependencies { Promise.all([ throwIfNotRunning(), throwIfNotInstalled(), - throwIfError(), + throwIfConfigNotSatisfied(), + throwIfHealthError(), ]).then(sinkVoid) const isValid = () => @@ -105,11 +119,13 @@ export function checkAllDependencies(effects: Effects): CheckAllDependencies { return { notRunning, notInstalled, + configNotSatisfied, healthErrors, throwIfNotRunning, + throwIfConfigNotSatisfied, throwIfNotValid, throwIfNotInstalled, - throwIfError, + throwIfHealthError, isValid, } } diff --git a/sdk/lib/dependencies/setupDependencyConfig.ts b/sdk/lib/dependencies/setupDependencyConfig.ts index c67c46a44..2fde4bce5 100644 --- a/sdk/lib/dependencies/setupDependencyConfig.ts +++ b/sdk/lib/dependencies/setupDependencyConfig.ts @@ -1,12 +1,12 @@ import { Config } from "../config/builder/config" -import { SDKManifest } from "../manifest/ManifestTypes" -import { ExpectedExports } from "../types" + +import * as T from "../types" import { DependencyConfig } from "./DependencyConfig" export function setupDependencyConfig< Store, Input extends Record, - Manifest extends SDKManifest, + Manifest extends T.Manifest, >( _config: Config | Config, autoConfigs: { @@ -17,6 +17,6 @@ export function setupDependencyConfig< any > | null }, -): ExpectedExports.dependencyConfig { +): T.ExpectedExports.dependencyConfig { return autoConfigs } diff --git a/sdk/lib/emverLite/mod.ts b/sdk/lib/emverLite/mod.ts deleted file mode 100644 index 52fb4e347..000000000 --- a/sdk/lib/emverLite/mod.ts +++ /dev/null @@ -1,323 +0,0 @@ -import * as matches from "ts-matches" - -const starSub = /((\d+\.)*\d+)\.\*/ -// prettier-ignore -export type ValidEmVer = string; -// prettier-ignore -export type ValidEmVerRange = string; - -function incrementLastNumber(list: number[]) { - const newList = [...list] - newList[newList.length - 1]++ - return newList -} -/** - * Will take in a range, like `>1.2` or `<1.2.3.4` or `=1.2` or `1.*` - * and return a checker, that has the check function for checking that a version is in the valid - * @param range - * @returns - */ -export function rangeOf(range: string | Checker): Checker { - return Checker.parse(range) -} - -/** - * Used to create a checker that will `and` all the ranges passed in - * @param ranges - * @returns - */ -export function rangeAnd(...ranges: (string | Checker)[]): Checker { - if (ranges.length === 0) { - throw new Error("No ranges given") - } - const [firstCheck, ...rest] = ranges - return Checker.parse(firstCheck).and(...rest) -} - -/** - * Used to create a checker that will `or` all the ranges passed in - * @param ranges - * @returns - */ -export function rangeOr(...ranges: (string | Checker)[]): Checker { - if (ranges.length === 0) { - throw new Error("No ranges given") - } - const [firstCheck, ...rest] = ranges - return Checker.parse(firstCheck).or(...rest) -} - -/** - * This will negate the checker, so given a checker that checks for >= 1.0.0, it will check for < 1.0.0 - * @param range - * @returns - */ -export function notRange(range: string | Checker): Checker { - return rangeOf(range).not() -} - -/** - * EmVer is a set of versioning of any pattern like 1 or 1.2 or 1.2.3 or 1.2.3.4 or .. - */ -export class EmVer { - /** - * Convert the range, should be 1.2.* or * into a emver - * Or an already made emver - * IsUnsafe - */ - static from(range: string | EmVer): EmVer { - if (range instanceof EmVer) { - return range - } - return EmVer.parse(range) - } - /** - * Convert the range, should be 1.2.* or * into a emver - * IsUnsafe - */ - static parse(rangeExtra: string): EmVer { - const [range, extra] = rangeExtra.split("-") - const values = range.split(".").map((x) => parseInt(x)) - for (const value of values) { - if (isNaN(value)) { - throw new Error(`Couldn't parse range: ${range}`) - } - } - return new EmVer(values, extra) - } - private constructor( - public readonly values: number[], - readonly extra: string | null, - ) {} - - /** - * Used when we need a new emver that has the last number incremented, used in the 1.* like things - */ - public withLastIncremented() { - return new EmVer(incrementLastNumber(this.values), null) - } - - public greaterThan(other: EmVer): boolean { - for (const i in this.values) { - if (other.values[i] == null) { - return true - } - if (this.values[i] > other.values[i]) { - return true - } - - if (this.values[i] < other.values[i]) { - return false - } - } - return false - } - - public equals(other: EmVer): boolean { - if (other.values.length !== this.values.length) { - return false - } - for (const i in this.values) { - if (this.values[i] !== other.values[i]) { - return false - } - } - return true - } - public greaterThanOrEqual(other: EmVer): boolean { - return this.greaterThan(other) || this.equals(other) - } - public lessThanOrEqual(other: EmVer): boolean { - return !this.greaterThan(other) - } - public lessThan(other: EmVer): boolean { - return !this.greaterThanOrEqual(other) - } - /** - * Return a enum string that describes (used for switching/iffs) - * to know comparison - * @param other - * @returns - */ - public compare(other: EmVer) { - if (this.equals(other)) { - return "equal" as const - } else if (this.greaterThan(other)) { - return "greater" as const - } else { - return "less" as const - } - } - /** - * Used when sorting emver's in a list using the sort method - * @param other - * @returns - */ - public compareForSort(other: EmVer) { - return matches - .matches(this.compare(other)) - .when("equal", () => 0 as const) - .when("greater", () => 1 as const) - .when("less", () => -1 as const) - .unwrap() - } - - toString() { - return `${this.values.join(".")}${this.extra ? `-${this.extra}` : ""}` as ValidEmVer - } -} - -/** - * A checker is a function that takes a version and returns true if the version matches the checker. - * Used when we are doing range checking, like saying ">=1.0.0".check("1.2.3") will be true - */ -export class Checker { - /** - * Will take in a range, like `>1.2` or `<1.2.3.4` or `=1.2` or `1.*` - * and return a checker, that has the check function for checking that a version is in the valid - * @param range - * @returns - */ - static parse(range: string | Checker): Checker { - if (range instanceof Checker) { - return range - } - range = range.trim() - if (range.indexOf("||") !== -1) { - return rangeOr(...range.split("||").map((x) => Checker.parse(x))) - } - if (range.indexOf("&&") !== -1) { - return rangeAnd(...range.split("&&").map((x) => Checker.parse(x))) - } - if (range === "*") { - return new Checker((version) => { - EmVer.from(version) - return true - }, range) - } - if (range.startsWith("!!")) return Checker.parse(range.substring(2)) - if (range.startsWith("!")) { - const tempValue = Checker.parse(range.substring(1)) - return new Checker((x) => !tempValue.check(x), range) - } - const starSubMatches = starSub.exec(range) - if (starSubMatches != null) { - const emVarLower = EmVer.parse(starSubMatches[1]) - const emVarUpper = emVarLower.withLastIncremented() - - return new Checker((version) => { - const v = EmVer.from(version) - return ( - (v.greaterThan(emVarLower) || v.equals(emVarLower)) && - !v.greaterThan(emVarUpper) && - !v.equals(emVarUpper) - ) - }, range) - } - - switch (range.substring(0, 2)) { - case ">=": { - const emVar = EmVer.parse(range.substring(2)) - return new Checker((version) => { - const v = EmVer.from(version) - return v.greaterThanOrEqual(emVar) - }, range) - } - case "<=": { - const emVar = EmVer.parse(range.substring(2)) - return new Checker((version) => { - const v = EmVer.from(version) - return v.lessThanOrEqual(emVar) - }, range) - } - } - - switch (range.substring(0, 1)) { - case ">": { - const emVar = EmVer.parse(range.substring(1)) - return new Checker((version) => { - const v = EmVer.from(version) - return v.greaterThan(emVar) - }, range) - } - case "<": { - const emVar = EmVer.parse(range.substring(1)) - return new Checker((version) => { - const v = EmVer.from(version) - return v.lessThan(emVar) - }, range) - } - case "=": { - const emVar = EmVer.parse(range.substring(1)) - return new Checker((version) => { - const v = EmVer.from(version) - return v.equals(emVar) - }, `=${emVar.toString()}`) - } - } - throw new Error("Couldn't parse range: " + range) - } - constructor( - /** - * Check is the function that will be given a emver or unparsed emver and should give if it follows - * a pattern - */ - public readonly check: (value: ValidEmVer | EmVer) => boolean, - private readonly _range: string, - ) {} - - get range() { - return this._range as ValidEmVerRange - } - - /** - * Used when we want the `and` condition with another checker - */ - public and(...others: (Checker | string)[]): Checker { - const othersCheck = others.map(Checker.parse) - return new Checker( - (value) => { - if (!this.check(value)) { - return false - } - for (const other of othersCheck) { - if (!other.check(value)) { - return false - } - } - return true - }, - othersCheck.map((x) => x._range).join(" && "), - ) - } - - /** - * Used when we want the `or` condition with another checker - */ - public or(...others: (Checker | string)[]): Checker { - const othersCheck = others.map(Checker.parse) - return new Checker( - (value) => { - if (this.check(value)) { - return true - } - for (const other of othersCheck) { - if (other.check(value)) { - return true - } - } - return false - }, - othersCheck.map((x) => x._range).join(" || "), - ) - } - - /** - * A useful example is making sure we don't match an exact version, like !=1.2.3 - * @returns - */ - public not(): Checker { - let newRange = `!${this._range}` - return Checker.parse(newRange) - } -} diff --git a/sdk/lib/exver/exver.pegjs b/sdk/lib/exver/exver.pegjs new file mode 100644 index 000000000..3045b9224 --- /dev/null +++ b/sdk/lib/exver/exver.pegjs @@ -0,0 +1,99 @@ +// #flavor:0.1.2-beta.1:0 +// !( >=1:1 && <= 2:2) + +VersionRange + = first:VersionRangeAtom rest:(_ ((Or / And) _)? VersionRangeAtom)* + +Or = "||" + +And = "&&" + +VersionRangeAtom + = Parens + / Anchor + / Not + / Any + / None + +Parens + = "(" _ expr:VersionRange _ ")" { return { type: "Parens", expr } } + +Anchor + = operator:CmpOp? _ version:VersionSpec { return { type: "Anchor", operator, version } } + +VersionSpec + = flavor:Flavor? upstream:Version downstream:( ":" Version )? { return { flavor: flavor || null, upstream, downstream: downstream ? downstream[1] : { number: [0], prerelease: [] } } } + +Not = "!" _ value:VersionRangeAtom { return { type: "Not", value: value }} + +Any = "*" { return { type: "Any" } } + +None = "!" { return { type: "None" } } + +CmpOp + = ">=" { return ">="; } + / "<=" { return "<="; } + / ">" { return ">"; } + / "<" { return "<"; } + / "=" { return "="; } + / "!=" { return "!="; } + / "^" { return "^"; } + / "~" { return "~"; } + +ExtendedVersion + = flavor:Flavor? upstream:Version ":" downstream:Version { + return { flavor: flavor || null, upstream, downstream } + } + +EmVer + = major:Digit "." minor:Digit "." patch:Digit ("." revision:Digit)? { + return { + flavor: null, + upstream: { + number: [major, minor, patch], + prerelease: [], + }, + downstream: { + number: [revision || 0], + prerelease: [], + }, + } + } + +Flavor + = "#" flavor:Lowercase ":" { return flavor } + +Lowercase + = [a-z]+ { return text() } + +String + = [a-zA-Z]+ { return text(); } + +Version + = number:VersionNumber prerelease: PreRelease? { + return { + number, + prerelease: prerelease || [] + }; + } + +PreRelease + = "-" first:PreReleaseSegment rest:("." PreReleaseSegment)* { + return [first].concat(rest.map(r => r[1])); + } + +PreReleaseSegment + = "."? segment:(Digit / String) { + return segment; + } + +VersionNumber + = first:Digit rest:("." Digit)* { + return [first].concat(rest.map(r => r[1])); + } + +Digit + = [0-9]+ { return parseInt(text(), 10); } + +_ "whitespace" + = [ \t\n\r]* \ No newline at end of file diff --git a/sdk/lib/exver/exver.ts b/sdk/lib/exver/exver.ts new file mode 100644 index 000000000..be9ea3e0e --- /dev/null +++ b/sdk/lib/exver/exver.ts @@ -0,0 +1,2507 @@ +/* eslint-disable */ + + + +const peggyParser: {parse: any, SyntaxError: any, DefaultTracer?: any} = // Generated by Peggy 3.0.2. +// +// https://peggyjs.org/ +// @ts-ignore +(function() { +// @ts-ignore + "use strict"; + +// @ts-ignore +function peg$subclass(child, parent) { +// @ts-ignore + function C() { this.constructor = child; } +// @ts-ignore + C.prototype = parent.prototype; +// @ts-ignore + child.prototype = new C(); +} + +// @ts-ignore +function peg$SyntaxError(message, expected, found, location) { +// @ts-ignore + var self = Error.call(this, message); + // istanbul ignore next Check is a necessary evil to support older environments +// @ts-ignore + if (Object.setPrototypeOf) { +// @ts-ignore + Object.setPrototypeOf(self, peg$SyntaxError.prototype); + } +// @ts-ignore + self.expected = expected; +// @ts-ignore + self.found = found; +// @ts-ignore + self.location = location; +// @ts-ignore + self.name = "SyntaxError"; +// @ts-ignore + return self; +} + +// @ts-ignore +peg$subclass(peg$SyntaxError, Error); + +// @ts-ignore +function peg$padEnd(str, targetLength, padString) { +// @ts-ignore + padString = padString || " "; +// @ts-ignore + if (str.length > targetLength) { return str; } +// @ts-ignore + targetLength -= str.length; +// @ts-ignore + padString += padString.repeat(targetLength); +// @ts-ignore + return str + padString.slice(0, targetLength); +} + +// @ts-ignore +peg$SyntaxError.prototype.format = function(sources) { +// @ts-ignore + var str = "Error: " + this.message; +// @ts-ignore + if (this.location) { +// @ts-ignore + var src = null; +// @ts-ignore + var k; +// @ts-ignore + for (k = 0; k < sources.length; k++) { +// @ts-ignore + if (sources[k].source === this.location.source) { +// @ts-ignore + src = sources[k].text.split(/\r\n|\n|\r/g); +// @ts-ignore + break; + } + } +// @ts-ignore + var s = this.location.start; +// @ts-ignore + var offset_s = (this.location.source && (typeof this.location.source.offset === "function")) +// @ts-ignore + ? this.location.source.offset(s) +// @ts-ignore + : s; +// @ts-ignore + var loc = this.location.source + ":" + offset_s.line + ":" + offset_s.column; +// @ts-ignore + if (src) { +// @ts-ignore + var e = this.location.end; +// @ts-ignore + var filler = peg$padEnd("", offset_s.line.toString().length, ' '); +// @ts-ignore + var line = src[s.line - 1]; +// @ts-ignore + var last = s.line === e.line ? e.column : line.length + 1; +// @ts-ignore + var hatLen = (last - s.column) || 1; +// @ts-ignore + str += "\n --> " + loc + "\n" +// @ts-ignore + + filler + " |\n" +// @ts-ignore + + offset_s.line + " | " + line + "\n" +// @ts-ignore + + filler + " | " + peg$padEnd("", s.column - 1, ' ') +// @ts-ignore + + peg$padEnd("", hatLen, "^"); +// @ts-ignore + } else { +// @ts-ignore + str += "\n at " + loc; + } + } +// @ts-ignore + return str; +}; + +// @ts-ignore +peg$SyntaxError.buildMessage = function(expected, found) { +// @ts-ignore + var DESCRIBE_EXPECTATION_FNS = { +// @ts-ignore + literal: function(expectation) { +// @ts-ignore + return "\"" + literalEscape(expectation.text) + "\""; + }, + +// @ts-ignore + class: function(expectation) { +// @ts-ignore + var escapedParts = expectation.parts.map(function(part) { +// @ts-ignore + return Array.isArray(part) +// @ts-ignore + ? classEscape(part[0]) + "-" + classEscape(part[1]) +// @ts-ignore + : classEscape(part); + }); + +// @ts-ignore + return "[" + (expectation.inverted ? "^" : "") + escapedParts.join("") + "]"; + }, + +// @ts-ignore + any: function() { +// @ts-ignore + return "any character"; + }, + +// @ts-ignore + end: function() { +// @ts-ignore + return "end of input"; + }, + +// @ts-ignore + other: function(expectation) { +// @ts-ignore + return expectation.description; + } + }; + +// @ts-ignore + function hex(ch) { +// @ts-ignore + return ch.charCodeAt(0).toString(16).toUpperCase(); + } + +// @ts-ignore + function literalEscape(s) { +// @ts-ignore + return s +// @ts-ignore + .replace(/\\/g, "\\\\") +// @ts-ignore + .replace(/"/g, "\\\"") +// @ts-ignore + .replace(/\0/g, "\\0") +// @ts-ignore + .replace(/\t/g, "\\t") +// @ts-ignore + .replace(/\n/g, "\\n") +// @ts-ignore + .replace(/\r/g, "\\r") +// @ts-ignore + .replace(/[\x00-\x0F]/g, function(ch) { return "\\x0" + hex(ch); }) +// @ts-ignore + .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return "\\x" + hex(ch); }); + } + +// @ts-ignore + function classEscape(s) { +// @ts-ignore + return s +// @ts-ignore + .replace(/\\/g, "\\\\") +// @ts-ignore + .replace(/\]/g, "\\]") +// @ts-ignore + .replace(/\^/g, "\\^") +// @ts-ignore + .replace(/-/g, "\\-") +// @ts-ignore + .replace(/\0/g, "\\0") +// @ts-ignore + .replace(/\t/g, "\\t") +// @ts-ignore + .replace(/\n/g, "\\n") +// @ts-ignore + .replace(/\r/g, "\\r") +// @ts-ignore + .replace(/[\x00-\x0F]/g, function(ch) { return "\\x0" + hex(ch); }) +// @ts-ignore + .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return "\\x" + hex(ch); }); + } + +// @ts-ignore + function describeExpectation(expectation) { +// @ts-ignore + return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation); + } + +// @ts-ignore + function describeExpected(expected) { +// @ts-ignore + var descriptions = expected.map(describeExpectation); +// @ts-ignore + var i, j; + +// @ts-ignore + descriptions.sort(); + +// @ts-ignore + if (descriptions.length > 0) { +// @ts-ignore + for (i = 1, j = 1; i < descriptions.length; i++) { +// @ts-ignore + if (descriptions[i - 1] !== descriptions[i]) { +// @ts-ignore + descriptions[j] = descriptions[i]; +// @ts-ignore + j++; + } + } +// @ts-ignore + descriptions.length = j; + } + +// @ts-ignore + switch (descriptions.length) { +// @ts-ignore + case 1: +// @ts-ignore + return descriptions[0]; + +// @ts-ignore + case 2: +// @ts-ignore + return descriptions[0] + " or " + descriptions[1]; + +// @ts-ignore + default: +// @ts-ignore + return descriptions.slice(0, -1).join(", ") +// @ts-ignore + + ", or " +// @ts-ignore + + descriptions[descriptions.length - 1]; + } + } + +// @ts-ignore + function describeFound(found) { +// @ts-ignore + return found ? "\"" + literalEscape(found) + "\"" : "end of input"; + } + +// @ts-ignore + return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found."; +}; + +// @ts-ignore +function peg$parse(input, options) { +// @ts-ignore + options = options !== undefined ? options : {}; + +// @ts-ignore + var peg$FAILED = {}; +// @ts-ignore + var peg$source = options.grammarSource; + +// @ts-ignore + var peg$startRuleFunctions = { VersionRange: peg$parseVersionRange, Or: peg$parseOr, And: peg$parseAnd, VersionRangeAtom: peg$parseVersionRangeAtom, Parens: peg$parseParens, Anchor: peg$parseAnchor, VersionSpec: peg$parseVersionSpec, Not: peg$parseNot, Any: peg$parseAny, None: peg$parseNone, CmpOp: peg$parseCmpOp, ExtendedVersion: peg$parseExtendedVersion, EmVer: peg$parseEmVer, Flavor: peg$parseFlavor, Lowercase: peg$parseLowercase, String: peg$parseString, Version: peg$parseVersion, PreRelease: peg$parsePreRelease, PreReleaseSegment: peg$parsePreReleaseSegment, VersionNumber: peg$parseVersionNumber, Digit: peg$parseDigit, _: peg$parse_ }; +// @ts-ignore + var peg$startRuleFunction = peg$parseVersionRange; + +// @ts-ignore + var peg$c0 = "||"; + var peg$c1 = "&&"; + var peg$c2 = "("; + var peg$c3 = ")"; + var peg$c4 = ":"; + var peg$c5 = "!"; + var peg$c6 = "*"; + var peg$c7 = ">="; + var peg$c8 = "<="; + var peg$c9 = ">"; + var peg$c10 = "<"; + var peg$c11 = "="; + var peg$c12 = "!="; + var peg$c13 = "^"; + var peg$c14 = "~"; + var peg$c15 = "."; + var peg$c16 = "#"; + var peg$c17 = "-"; + + var peg$r0 = /^[a-z]/; + var peg$r1 = /^[a-zA-Z]/; + var peg$r2 = /^[0-9]/; + var peg$r3 = /^[ \t\n\r]/; + + var peg$e0 = peg$literalExpectation("||", false); + var peg$e1 = peg$literalExpectation("&&", false); + var peg$e2 = peg$literalExpectation("(", false); + var peg$e3 = peg$literalExpectation(")", false); + var peg$e4 = peg$literalExpectation(":", false); + var peg$e5 = peg$literalExpectation("!", false); + var peg$e6 = peg$literalExpectation("*", false); + var peg$e7 = peg$literalExpectation(">=", false); + var peg$e8 = peg$literalExpectation("<=", false); + var peg$e9 = peg$literalExpectation(">", false); + var peg$e10 = peg$literalExpectation("<", false); + var peg$e11 = peg$literalExpectation("=", false); + var peg$e12 = peg$literalExpectation("!=", false); + var peg$e13 = peg$literalExpectation("^", false); + var peg$e14 = peg$literalExpectation("~", false); + var peg$e15 = peg$literalExpectation(".", false); + var peg$e16 = peg$literalExpectation("#", false); + var peg$e17 = peg$classExpectation([["a", "z"]], false, false); + var peg$e18 = peg$classExpectation([["a", "z"], ["A", "Z"]], false, false); + var peg$e19 = peg$literalExpectation("-", false); + var peg$e20 = peg$classExpectation([["0", "9"]], false, false); + var peg$e21 = peg$otherExpectation("whitespace"); + var peg$e22 = peg$classExpectation([" ", "\t", "\n", "\r"], false, false); +// @ts-ignore + + var peg$f0 = function(expr) {// @ts-ignore + return { type: "Parens", expr } };// @ts-ignore + + var peg$f1 = function(operator, version) {// @ts-ignore + return { type: "Anchor", operator, version } };// @ts-ignore + + var peg$f2 = function(flavor, upstream, downstream) {// @ts-ignore + return { flavor: flavor || null, upstream, downstream: downstream ? downstream[1] : { number: [0], prerelease: [] } } };// @ts-ignore + + var peg$f3 = function(value) {// @ts-ignore + return { type: "Not", value: value }};// @ts-ignore + + var peg$f4 = function() {// @ts-ignore + return { type: "Any" } };// @ts-ignore + + var peg$f5 = function() {// @ts-ignore + return { type: "None" } };// @ts-ignore + + var peg$f6 = function() {// @ts-ignore + return ">="; };// @ts-ignore + + var peg$f7 = function() {// @ts-ignore + return "<="; };// @ts-ignore + + var peg$f8 = function() {// @ts-ignore + return ">"; };// @ts-ignore + + var peg$f9 = function() {// @ts-ignore + return "<"; };// @ts-ignore + + var peg$f10 = function() {// @ts-ignore + return "="; };// @ts-ignore + + var peg$f11 = function() {// @ts-ignore + return "!="; };// @ts-ignore + + var peg$f12 = function() {// @ts-ignore + return "^"; };// @ts-ignore + + var peg$f13 = function() {// @ts-ignore + return "~"; };// @ts-ignore + + var peg$f14 = function(flavor, upstream, downstream) { +// @ts-ignore + return { flavor: flavor || null, upstream, downstream } + };// @ts-ignore + + var peg$f15 = function(major, minor, patch) { +// @ts-ignore + return { +// @ts-ignore + flavor: null, +// @ts-ignore + upstream: { +// @ts-ignore + number: [major, minor, patch], +// @ts-ignore + prerelease: [], + }, +// @ts-ignore + downstream: { +// @ts-ignore + number: [revision || 0], +// @ts-ignore + prerelease: [], + }, + } + };// @ts-ignore + + var peg$f16 = function(flavor) {// @ts-ignore + return flavor };// @ts-ignore + + var peg$f17 = function() {// @ts-ignore + return text() };// @ts-ignore + + var peg$f18 = function() {// @ts-ignore + return text(); };// @ts-ignore + + var peg$f19 = function(number, prerelease) { +// @ts-ignore + return { +// @ts-ignore + number, +// @ts-ignore + prerelease: prerelease || [] + }; + };// @ts-ignore + + var peg$f20 = function(first, rest) { +// @ts-ignore + return [first].concat(rest.map(r => r[1])); + };// @ts-ignore + + var peg$f21 = function(segment) { +// @ts-ignore + return segment; + };// @ts-ignore + + var peg$f22 = function(first, rest) { +// @ts-ignore + return [first].concat(rest.map(r => r[1])); + };// @ts-ignore + + var peg$f23 = function() {// @ts-ignore + return parseInt(text(), 10); }; +// @ts-ignore + var peg$currPos = 0; +// @ts-ignore + var peg$savedPos = 0; +// @ts-ignore + var peg$posDetailsCache = [{ line: 1, column: 1 }]; +// @ts-ignore + var peg$maxFailPos = 0; +// @ts-ignore + var peg$maxFailExpected = []; +// @ts-ignore + var peg$silentFails = 0; + +// @ts-ignore + var peg$result; + +// @ts-ignore + if ("startRule" in options) { +// @ts-ignore + if (!(options.startRule in peg$startRuleFunctions)) { +// @ts-ignore + throw new Error("Can't start parsing from rule \"" + options.startRule + "\"."); + } + +// @ts-ignore + peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; + } + +// @ts-ignore + function text() { +// @ts-ignore + return input.substring(peg$savedPos, peg$currPos); + } + +// @ts-ignore + function offset() { +// @ts-ignore + return peg$savedPos; + } + +// @ts-ignore + function range() { +// @ts-ignore + return { +// @ts-ignore + source: peg$source, +// @ts-ignore + start: peg$savedPos, +// @ts-ignore + end: peg$currPos + }; + } + +// @ts-ignore + function location() { +// @ts-ignore + return peg$computeLocation(peg$savedPos, peg$currPos); + } + +// @ts-ignore + function expected(description, location) { +// @ts-ignore + location = location !== undefined +// @ts-ignore + ? location +// @ts-ignore + : peg$computeLocation(peg$savedPos, peg$currPos); + +// @ts-ignore + throw peg$buildStructuredError( +// @ts-ignore + [peg$otherExpectation(description)], +// @ts-ignore + input.substring(peg$savedPos, peg$currPos), +// @ts-ignore + location + ); + } + +// @ts-ignore + function error(message, location) { +// @ts-ignore + location = location !== undefined +// @ts-ignore + ? location +// @ts-ignore + : peg$computeLocation(peg$savedPos, peg$currPos); + +// @ts-ignore + throw peg$buildSimpleError(message, location); + } + +// @ts-ignore + function peg$literalExpectation(text, ignoreCase) { +// @ts-ignore + return { type: "literal", text: text, ignoreCase: ignoreCase }; + } + +// @ts-ignore + function peg$classExpectation(parts, inverted, ignoreCase) { +// @ts-ignore + return { type: "class", parts: parts, inverted: inverted, ignoreCase: ignoreCase }; + } + +// @ts-ignore + function peg$anyExpectation() { +// @ts-ignore + return { type: "any" }; + } + +// @ts-ignore + function peg$endExpectation() { +// @ts-ignore + return { type: "end" }; + } + +// @ts-ignore + function peg$otherExpectation(description) { +// @ts-ignore + return { type: "other", description: description }; + } + +// @ts-ignore + function peg$computePosDetails(pos) { +// @ts-ignore + var details = peg$posDetailsCache[pos]; +// @ts-ignore + var p; + +// @ts-ignore + if (details) { +// @ts-ignore + return details; +// @ts-ignore + } else { +// @ts-ignore + p = pos - 1; +// @ts-ignore + while (!peg$posDetailsCache[p]) { +// @ts-ignore + p--; + } + +// @ts-ignore + details = peg$posDetailsCache[p]; +// @ts-ignore + details = { +// @ts-ignore + line: details.line, +// @ts-ignore + column: details.column + }; + +// @ts-ignore + while (p < pos) { +// @ts-ignore + if (input.charCodeAt(p) === 10) { +// @ts-ignore + details.line++; +// @ts-ignore + details.column = 1; +// @ts-ignore + } else { +// @ts-ignore + details.column++; + } + +// @ts-ignore + p++; + } + +// @ts-ignore + peg$posDetailsCache[pos] = details; + +// @ts-ignore + return details; + } + } + +// @ts-ignore + function peg$computeLocation(startPos, endPos, offset) { +// @ts-ignore + var startPosDetails = peg$computePosDetails(startPos); +// @ts-ignore + var endPosDetails = peg$computePosDetails(endPos); + +// @ts-ignore + var res = { +// @ts-ignore + source: peg$source, +// @ts-ignore + start: { +// @ts-ignore + offset: startPos, +// @ts-ignore + line: startPosDetails.line, +// @ts-ignore + column: startPosDetails.column + }, +// @ts-ignore + end: { +// @ts-ignore + offset: endPos, +// @ts-ignore + line: endPosDetails.line, +// @ts-ignore + column: endPosDetails.column + } + }; +// @ts-ignore + if (offset && peg$source && (typeof peg$source.offset === "function")) { +// @ts-ignore + res.start = peg$source.offset(res.start); +// @ts-ignore + res.end = peg$source.offset(res.end); + } +// @ts-ignore + return res; + } + +// @ts-ignore + function peg$fail(expected) { +// @ts-ignore + if (peg$currPos < peg$maxFailPos) { return; } + +// @ts-ignore + if (peg$currPos > peg$maxFailPos) { +// @ts-ignore + peg$maxFailPos = peg$currPos; +// @ts-ignore + peg$maxFailExpected = []; + } + +// @ts-ignore + peg$maxFailExpected.push(expected); + } + +// @ts-ignore + function peg$buildSimpleError(message, location) { +// @ts-ignore + return new peg$SyntaxError(message, null, null, location); + } + +// @ts-ignore + function peg$buildStructuredError(expected, found, location) { +// @ts-ignore + return new peg$SyntaxError( +// @ts-ignore + peg$SyntaxError.buildMessage(expected, found), +// @ts-ignore + expected, +// @ts-ignore + found, +// @ts-ignore + location + ); + } + +// @ts-ignore + function // @ts-ignore +peg$parseVersionRange() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5, s6, s7; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseVersionRangeAtom(); +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = []; +// @ts-ignore + s3 = peg$currPos; +// @ts-ignore + s4 = peg$parse_(); +// @ts-ignore + s5 = peg$currPos; +// @ts-ignore + s6 = peg$parseOr(); +// @ts-ignore + if (s6 === peg$FAILED) { +// @ts-ignore + s6 = peg$parseAnd(); + } +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s7 = peg$parse_(); +// @ts-ignore + s6 = [s6, s7]; +// @ts-ignore + s5 = s6; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s5; +// @ts-ignore + s5 = peg$FAILED; + } +// @ts-ignore + if (s5 === peg$FAILED) { +// @ts-ignore + s5 = null; + } +// @ts-ignore + s6 = peg$parseVersionRangeAtom(); +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s4 = [s4, s5, s6]; +// @ts-ignore + s3 = s4; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + while (s3 !== peg$FAILED) { +// @ts-ignore + s2.push(s3); +// @ts-ignore + s3 = peg$currPos; +// @ts-ignore + s4 = peg$parse_(); +// @ts-ignore + s5 = peg$currPos; +// @ts-ignore + s6 = peg$parseOr(); +// @ts-ignore + if (s6 === peg$FAILED) { +// @ts-ignore + s6 = peg$parseAnd(); + } +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s7 = peg$parse_(); +// @ts-ignore + s6 = [s6, s7]; +// @ts-ignore + s5 = s6; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s5; +// @ts-ignore + s5 = peg$FAILED; + } +// @ts-ignore + if (s5 === peg$FAILED) { +// @ts-ignore + s5 = null; + } +// @ts-ignore + s6 = peg$parseVersionRangeAtom(); +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s4 = [s4, s5, s6]; +// @ts-ignore + s3 = s4; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } + } +// @ts-ignore + s1 = [s1, s2]; +// @ts-ignore + s0 = s1; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseOr() { +// @ts-ignore + var s0; + +// @ts-ignore + if (input.substr(peg$currPos, 2) === peg$c0) { +// @ts-ignore + s0 = peg$c0; +// @ts-ignore + peg$currPos += 2; +// @ts-ignore + } else { +// @ts-ignore + s0 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e0); } + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseAnd() { +// @ts-ignore + var s0; + +// @ts-ignore + if (input.substr(peg$currPos, 2) === peg$c1) { +// @ts-ignore + s0 = peg$c1; +// @ts-ignore + peg$currPos += 2; +// @ts-ignore + } else { +// @ts-ignore + s0 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e1); } + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseVersionRangeAtom() { +// @ts-ignore + var s0; + +// @ts-ignore + s0 = peg$parseParens(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseAnchor(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseNot(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseAny(); +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$parseNone(); + } + } + } + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseParens() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 40) { +// @ts-ignore + s1 = peg$c2; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e2); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parse_(); +// @ts-ignore + s3 = peg$parseVersionRange(); +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + s4 = peg$parse_(); +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 41) { +// @ts-ignore + s5 = peg$c3; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s5 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e3); } + } +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f0(s3); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseAnchor() { +// @ts-ignore + var s0, s1, s2, s3; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseCmpOp(); +// @ts-ignore + if (s1 === peg$FAILED) { +// @ts-ignore + s1 = null; + } +// @ts-ignore + s2 = peg$parse_(); +// @ts-ignore + s3 = peg$parseVersionSpec(); +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f1(s1, s3); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseVersionSpec() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseFlavor(); +// @ts-ignore + if (s1 === peg$FAILED) { +// @ts-ignore + s1 = null; + } +// @ts-ignore + s2 = peg$parseVersion(); +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + s3 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 58) { +// @ts-ignore + s4 = peg$c4; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s4 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e4); } + } +// @ts-ignore + if (s4 !== peg$FAILED) { +// @ts-ignore + s5 = peg$parseVersion(); +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s4 = [s4, s5]; +// @ts-ignore + s3 = s4; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + if (s3 === peg$FAILED) { +// @ts-ignore + s3 = null; + } +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f2(s1, s2, s3); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseNot() { +// @ts-ignore + var s0, s1, s2, s3; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 33) { +// @ts-ignore + s1 = peg$c5; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e5); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parse_(); +// @ts-ignore + s3 = peg$parseVersionRangeAtom(); +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f3(s3); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseAny() { +// @ts-ignore + var s0, s1; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 42) { +// @ts-ignore + s1 = peg$c6; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e6); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f4(); + } +// @ts-ignore + s0 = s1; + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseNone() { +// @ts-ignore + var s0, s1; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 33) { +// @ts-ignore + s1 = peg$c5; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e5); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f5(); + } +// @ts-ignore + s0 = s1; + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseCmpOp() { +// @ts-ignore + var s0, s1; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.substr(peg$currPos, 2) === peg$c7) { +// @ts-ignore + s1 = peg$c7; +// @ts-ignore + peg$currPos += 2; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e7); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f6(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.substr(peg$currPos, 2) === peg$c8) { +// @ts-ignore + s1 = peg$c8; +// @ts-ignore + peg$currPos += 2; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e8); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f7(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 62) { +// @ts-ignore + s1 = peg$c9; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e9); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f8(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 60) { +// @ts-ignore + s1 = peg$c10; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e10); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f9(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 61) { +// @ts-ignore + s1 = peg$c11; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e11); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f10(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.substr(peg$currPos, 2) === peg$c12) { +// @ts-ignore + s1 = peg$c12; +// @ts-ignore + peg$currPos += 2; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e12); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f11(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 94) { +// @ts-ignore + s1 = peg$c13; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e13); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f12(); + } +// @ts-ignore + s0 = s1; +// @ts-ignore + if (s0 === peg$FAILED) { +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 126) { +// @ts-ignore + s1 = peg$c14; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e14); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f13(); + } +// @ts-ignore + s0 = s1; + } + } + } + } + } + } + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseExtendedVersion() { +// @ts-ignore + var s0, s1, s2, s3, s4; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseFlavor(); +// @ts-ignore + if (s1 === peg$FAILED) { +// @ts-ignore + s1 = null; + } +// @ts-ignore + s2 = peg$parseVersion(); +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 58) { +// @ts-ignore + s3 = peg$c4; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s3 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e4); } + } +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + s4 = peg$parseVersion(); +// @ts-ignore + if (s4 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f14(s1, s2, s4); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseEmVer() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5, s6, s7, s8; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseDigit(); +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s2 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + s3 = peg$parseDigit(); +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s4 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s4 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s4 !== peg$FAILED) { +// @ts-ignore + s5 = peg$parseDigit(); +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s6 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s7 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s7 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s7 !== peg$FAILED) { +// @ts-ignore + s8 = peg$parseDigit(); +// @ts-ignore + if (s8 !== peg$FAILED) { +// @ts-ignore + s7 = [s7, s8]; +// @ts-ignore + s6 = s7; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s6; +// @ts-ignore + s6 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s6; +// @ts-ignore + s6 = peg$FAILED; + } +// @ts-ignore + if (s6 === peg$FAILED) { +// @ts-ignore + s6 = null; + } +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f15(s1, s3, s5); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseFlavor() { +// @ts-ignore + var s0, s1, s2, s3; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 35) { +// @ts-ignore + s1 = peg$c16; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e16); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parseLowercase(); +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 58) { +// @ts-ignore + s3 = peg$c4; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s3 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e4); } + } +// @ts-ignore + if (s3 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f16(s2); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseLowercase() { +// @ts-ignore + var s0, s1, s2; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = []; +// @ts-ignore + if (peg$r0.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e17); } + } +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + while (s2 !== peg$FAILED) { +// @ts-ignore + s1.push(s2); +// @ts-ignore + if (peg$r0.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e17); } + } + } +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f17(); + } +// @ts-ignore + s0 = s1; + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseString() { +// @ts-ignore + var s0, s1, s2; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = []; +// @ts-ignore + if (peg$r1.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e18); } + } +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + while (s2 !== peg$FAILED) { +// @ts-ignore + s1.push(s2); +// @ts-ignore + if (peg$r1.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e18); } + } + } +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f18(); + } +// @ts-ignore + s0 = s1; + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseVersion() { +// @ts-ignore + var s0, s1, s2; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseVersionNumber(); +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parsePreRelease(); +// @ts-ignore + if (s2 === peg$FAILED) { +// @ts-ignore + s2 = null; + } +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f19(s1, s2); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parsePreRelease() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5, s6; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 45) { +// @ts-ignore + s1 = peg$c17; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e19); } + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = peg$parsePreReleaseSegment(); +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + s3 = []; +// @ts-ignore + s4 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s5 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s5 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s6 = peg$parsePreReleaseSegment(); +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s5 = [s5, s6]; +// @ts-ignore + s4 = s5; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s4; +// @ts-ignore + s4 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s4; +// @ts-ignore + s4 = peg$FAILED; + } +// @ts-ignore + while (s4 !== peg$FAILED) { +// @ts-ignore + s3.push(s4); +// @ts-ignore + s4 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s5 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s5 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s6 = peg$parsePreReleaseSegment(); +// @ts-ignore + if (s6 !== peg$FAILED) { +// @ts-ignore + s5 = [s5, s6]; +// @ts-ignore + s4 = s5; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s4; +// @ts-ignore + s4 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s4; +// @ts-ignore + s4 = peg$FAILED; + } + } +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f20(s2, s3); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parsePreReleaseSegment() { +// @ts-ignore + var s0, s1, s2; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s1 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s1 === peg$FAILED) { +// @ts-ignore + s1 = null; + } +// @ts-ignore + s2 = peg$parseDigit(); +// @ts-ignore + if (s2 === peg$FAILED) { +// @ts-ignore + s2 = peg$parseString(); + } +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f21(s2); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseVersionNumber() { +// @ts-ignore + var s0, s1, s2, s3, s4, s5; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = peg$parseDigit(); +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + s2 = []; +// @ts-ignore + s3 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s4 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s4 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s4 !== peg$FAILED) { +// @ts-ignore + s5 = peg$parseDigit(); +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s4 = [s4, s5]; +// @ts-ignore + s3 = s4; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + while (s3 !== peg$FAILED) { +// @ts-ignore + s2.push(s3); +// @ts-ignore + s3 = peg$currPos; +// @ts-ignore + if (input.charCodeAt(peg$currPos) === 46) { +// @ts-ignore + s4 = peg$c15; +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s4 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e15); } + } +// @ts-ignore + if (s4 !== peg$FAILED) { +// @ts-ignore + s5 = peg$parseDigit(); +// @ts-ignore + if (s5 !== peg$FAILED) { +// @ts-ignore + s4 = [s4, s5]; +// @ts-ignore + s3 = s4; +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s3; +// @ts-ignore + s3 = peg$FAILED; + } + } +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s0 = peg$f22(s1, s2); +// @ts-ignore + } else { +// @ts-ignore + peg$currPos = s0; +// @ts-ignore + s0 = peg$FAILED; + } + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parseDigit() { +// @ts-ignore + var s0, s1, s2; + +// @ts-ignore + s0 = peg$currPos; +// @ts-ignore + s1 = []; +// @ts-ignore + if (peg$r2.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e20); } + } +// @ts-ignore + if (s2 !== peg$FAILED) { +// @ts-ignore + while (s2 !== peg$FAILED) { +// @ts-ignore + s1.push(s2); +// @ts-ignore + if (peg$r2.test(input.charAt(peg$currPos))) { +// @ts-ignore + s2 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s2 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e20); } + } + } +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; + } +// @ts-ignore + if (s1 !== peg$FAILED) { +// @ts-ignore + peg$savedPos = s0; +// @ts-ignore + s1 = peg$f23(); + } +// @ts-ignore + s0 = s1; + +// @ts-ignore + return s0; + } + +// @ts-ignore + function // @ts-ignore +peg$parse_() { +// @ts-ignore + var s0, s1; + +// @ts-ignore + peg$silentFails++; +// @ts-ignore + s0 = []; +// @ts-ignore + if (peg$r3.test(input.charAt(peg$currPos))) { +// @ts-ignore + s1 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e22); } + } +// @ts-ignore + while (s1 !== peg$FAILED) { +// @ts-ignore + s0.push(s1); +// @ts-ignore + if (peg$r3.test(input.charAt(peg$currPos))) { +// @ts-ignore + s1 = input.charAt(peg$currPos); +// @ts-ignore + peg$currPos++; +// @ts-ignore + } else { +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e22); } + } + } +// @ts-ignore + peg$silentFails--; +// @ts-ignore + s1 = peg$FAILED; +// @ts-ignore + if (peg$silentFails === 0) { peg$fail(peg$e21); } + +// @ts-ignore + return s0; + } + +// @ts-ignore + peg$result = peg$startRuleFunction(); + +// @ts-ignore + if (peg$result !== peg$FAILED && peg$currPos === input.length) { +// @ts-ignore + return peg$result; +// @ts-ignore + } else { +// @ts-ignore + if (peg$result !== peg$FAILED && peg$currPos < input.length) { +// @ts-ignore + peg$fail(peg$endExpectation()); + } + +// @ts-ignore + throw peg$buildStructuredError( +// @ts-ignore + peg$maxFailExpected, +// @ts-ignore + peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null, +// @ts-ignore + peg$maxFailPos < input.length +// @ts-ignore + ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1) +// @ts-ignore + : peg$computeLocation(peg$maxFailPos, peg$maxFailPos) + ); + } +} + +// @ts-ignore + return { + SyntaxError: peg$SyntaxError, + parse: peg$parse + }; +})() + +export interface FilePosition { + offset: number; + line: number; + column: number; +} + +export interface FileRange { + start: FilePosition; + end: FilePosition; + source: string; +} + +export interface LiteralExpectation { + type: "literal"; + text: string; + ignoreCase: boolean; +} + +export interface ClassParts extends Array {} + +export interface ClassExpectation { + type: "class"; + parts: ClassParts; + inverted: boolean; + ignoreCase: boolean; +} + +export interface AnyExpectation { + type: "any"; +} + +export interface EndExpectation { + type: "end"; +} + +export interface OtherExpectation { + type: "other"; + description: string; +} + +export type Expectation = LiteralExpectation | ClassExpectation | AnyExpectation | EndExpectation | OtherExpectation; + +declare class _PeggySyntaxError extends Error { + public static buildMessage(expected: Expectation[], found: string | null): string; + public message: string; + public expected: Expectation[]; + public found: string | null; + public location: FileRange; + public name: string; + constructor(message: string, expected: Expectation[], found: string | null, location: FileRange); + format(sources: { + source?: any; + text: string; + }[]): string; +} + +export interface TraceEvent { + type: string; + rule: string; + result?: any; + location: FileRange; + } + +declare class _DefaultTracer { + private indentLevel: number; + public trace(event: TraceEvent): void; +} + +peggyParser.SyntaxError.prototype.name = "PeggySyntaxError"; + +export interface ParseOptions { + filename?: string; + startRule?: "VersionRange" | "Or" | "And" | "VersionRangeAtom" | "Parens" | "Anchor" | "VersionSpec" | "Not" | "Any" | "None" | "CmpOp" | "ExtendedVersion" | "EmVer" | "Flavor" | "Lowercase" | "String" | "Version" | "PreRelease" | "PreReleaseSegment" | "VersionNumber" | "Digit" | "_"; + tracer?: any; + [key: string]: any; +} +export type ParseFunction = ( + input: string, + options?: Options + ) => Options extends { startRule: infer StartRule } ? + StartRule extends "VersionRange" ? VersionRange : + StartRule extends "Or" ? Or : + StartRule extends "And" ? And : + StartRule extends "VersionRangeAtom" ? VersionRangeAtom : + StartRule extends "Parens" ? Parens : + StartRule extends "Anchor" ? Anchor : + StartRule extends "VersionSpec" ? VersionSpec : + StartRule extends "Not" ? Not : + StartRule extends "Any" ? Any : + StartRule extends "None" ? None : + StartRule extends "CmpOp" ? CmpOp : + StartRule extends "ExtendedVersion" ? ExtendedVersion : + StartRule extends "EmVer" ? EmVer : + StartRule extends "Flavor" ? Flavor : + StartRule extends "Lowercase" ? Lowercase_1 : + StartRule extends "String" ? String_1 : + StartRule extends "Version" ? Version : + StartRule extends "PreRelease" ? PreRelease : + StartRule extends "PreReleaseSegment" ? PreReleaseSegment : + StartRule extends "VersionNumber" ? VersionNumber : + StartRule extends "Digit" ? Digit : + StartRule extends "_" ? _ : VersionRange + : VersionRange; +export const parse: ParseFunction = peggyParser.parse; + +export const PeggySyntaxError = peggyParser.SyntaxError as typeof _PeggySyntaxError; + +export type PeggySyntaxError = _PeggySyntaxError; + +// These types were autogenerated by ts-pegjs +export type VersionRange = [ + VersionRangeAtom, + [_, [Or | And, _] | null, VersionRangeAtom][] +]; +export type Or = "||"; +export type And = "&&"; +export type VersionRangeAtom = Parens | Anchor | Not | Any | None; +export type Parens = { type: "Parens"; expr: VersionRange }; +export type Anchor = { + type: "Anchor"; + operator: CmpOp | null; + version: VersionSpec; +}; +export type VersionSpec = { + flavor: NonNullable | null; + upstream: Version; + downstream: any; +}; +export type Not = { type: "Not"; value: VersionRangeAtom }; +export type Any = { type: "Any" }; +export type None = { type: "None" }; +export type CmpOp = ">=" | "<=" | ">" | "<" | "=" | "!=" | "^" | "~"; +export type ExtendedVersion = { + flavor: NonNullable | null; + upstream: Version; + downstream: Version; +}; +export type EmVer = { + flavor: null; + upstream: { number: [Digit, Digit, Digit]; prerelease: [] }; + downstream: { number: [any]; prerelease: [] }; +}; +export type Flavor = Lowercase_1; +export type Lowercase_1 = string; +export type String_1 = string; +export type Version = { + number: VersionNumber; + prerelease: never[] | NonNullable; +}; +export type PreRelease = PreReleaseSegment[]; +export type PreReleaseSegment = Digit | String_1; +export type VersionNumber = Digit[]; +export type Digit = number; +export type _ = string[]; diff --git a/sdk/lib/exver/index.ts b/sdk/lib/exver/index.ts new file mode 100644 index 000000000..913194875 --- /dev/null +++ b/sdk/lib/exver/index.ts @@ -0,0 +1,443 @@ +import * as P from "./exver" + +// prettier-ignore +export type ValidateVersion = +T extends `-${infer A}` ? never : +T extends `${infer A}-${infer B}` ? ValidateVersion & ValidateVersion : + T extends `${bigint}` ? unknown : + T extends `${bigint}.${infer A}` ? ValidateVersion : + never + +// prettier-ignore +export type ValidateExVer = + T extends `#${string}:${infer A}:${infer B}` ? ValidateVersion & ValidateVersion : + T extends `${infer A}:${infer B}` ? ValidateVersion & ValidateVersion : + never + +// prettier-ignore +export type ValidateExVers = + T extends [] ? unknown : + T extends [infer A, ...infer B] ? ValidateExVer & ValidateExVers : + never + +type Anchor = { + type: "Anchor" + operator: P.CmpOp + version: ExtendedVersion +} + +type And = { + type: "And" + left: VersionRange + right: VersionRange +} + +type Or = { + type: "Or" + left: VersionRange + right: VersionRange +} + +type Not = { + type: "Not" + value: VersionRange +} + +export class VersionRange { + private constructor(private atom: Anchor | And | Or | Not | P.Any | P.None) {} + + toString(): string { + switch (this.atom.type) { + case "Anchor": + return `${this.atom.operator}${this.atom.version}` + case "And": + return `(${this.atom.left.toString()}) && (${this.atom.right.toString()})` + case "Or": + return `(${this.atom.left.toString()}) || (${this.atom.right.toString()})` + case "Not": + return `!(${this.atom.value.toString()})` + case "Any": + return "*" + case "None": + return "!" + } + } + + /** + * Returns a boolean indicating whether a given version satisfies the VersionRange + * !( >= 1:1 <= 2:2) || <=#bitcoin:1.2.0-alpha:0 + */ + satisfiedBy(version: ExtendedVersion): boolean { + switch (this.atom.type) { + case "Anchor": + const otherVersion = this.atom.version + switch (this.atom.operator) { + case "=": + return version.equals(otherVersion) + case ">": + return version.greaterThan(otherVersion) + case "<": + return version.lessThan(otherVersion) + case ">=": + return version.greaterThanOrEqual(otherVersion) + case "<=": + return version.lessThanOrEqual(otherVersion) + case "!=": + return !version.equals(otherVersion) + case "^": + const nextMajor = this.atom.version.incrementMajor() + if ( + version.greaterThanOrEqual(otherVersion) && + version.lessThan(nextMajor) + ) { + return true + } else { + return false + } + case "~": + const nextMinor = this.atom.version.incrementMinor() + if ( + version.greaterThanOrEqual(otherVersion) && + version.lessThan(nextMinor) + ) { + return true + } else { + return false + } + } + case "And": + return ( + this.atom.left.satisfiedBy(version) && + this.atom.right.satisfiedBy(version) + ) + case "Or": + return ( + this.atom.left.satisfiedBy(version) || + this.atom.right.satisfiedBy(version) + ) + case "Not": + return !this.atom.value.satisfiedBy(version) + case "Any": + return true + case "None": + return false + } + } + + private static parseAtom(atom: P.VersionRangeAtom): VersionRange { + switch (atom.type) { + case "Not": + return new VersionRange({ + type: "Not", + value: VersionRange.parseAtom(atom.value), + }) + case "Parens": + return VersionRange.parseRange(atom.expr) + case "Anchor": + return new VersionRange({ + type: "Anchor", + operator: atom.operator || "^", + version: new ExtendedVersion( + atom.version.flavor, + new Version( + atom.version.upstream.number, + atom.version.upstream.prerelease, + ), + new Version( + atom.version.downstream.number, + atom.version.downstream.prerelease, + ), + ), + }) + default: + return new VersionRange(atom) + } + } + + private static parseRange(range: P.VersionRange): VersionRange { + let result = VersionRange.parseAtom(range[0]) + for (const next of range[1]) { + switch (next[1]?.[0]) { + case "||": + result = new VersionRange({ + type: "Or", + left: result, + right: VersionRange.parseAtom(next[2]), + }) + break + case "&&": + default: + result = new VersionRange({ + type: "And", + left: result, + right: VersionRange.parseAtom(next[2]), + }) + break + } + } + return result + } + + static parse(range: string): VersionRange { + return VersionRange.parseRange( + P.parse(range, { startRule: "VersionRange" }), + ) + } + + and(right: VersionRange) { + return new VersionRange({ type: "And", left: this, right }) + } + + or(right: VersionRange) { + return new VersionRange({ type: "Or", left: this, right }) + } + + not() { + return new VersionRange({ type: "Not", value: this }) + } + + static anchor(operator: P.CmpOp, version: ExtendedVersion) { + return new VersionRange({ type: "Anchor", operator, version }) + } + + static any() { + return new VersionRange({ type: "Any" }) + } + + static none() { + return new VersionRange({ type: "None" }) + } +} + +export class Version { + constructor( + public number: number[], + public prerelease: (string | number)[], + ) {} + + toString(): string { + return `${this.number.join(".")}${this.prerelease.length > 0 ? `-${this.prerelease.join(".")}` : ""}` + } + + compare(other: Version): "greater" | "equal" | "less" { + const numLen = Math.max(this.number.length, other.number.length) + for (let i = 0; i < numLen; i++) { + if ((this.number[i] || 0) > (other.number[i] || 0)) { + return "greater" + } else if ((this.number[i] || 0) < (other.number[i] || 0)) { + return "less" + } + } + + if (this.prerelease.length === 0 && other.prerelease.length !== 0) { + return "greater" + } else if (this.prerelease.length !== 0 && other.prerelease.length === 0) { + return "less" + } + + const prereleaseLen = Math.max(this.number.length, other.number.length) + for (let i = 0; i < prereleaseLen; i++) { + if (typeof this.prerelease[i] === typeof other.prerelease[i]) { + if (this.prerelease[i] > other.prerelease[i]) { + return "greater" + } else if (this.prerelease[i] < other.prerelease[i]) { + return "less" + } + } else { + switch (`${typeof this.prerelease[1]}:${typeof other.prerelease[i]}`) { + case "number:string": + return "less" + case "string:number": + return "greater" + case "number:undefined": + case "string:undefined": + return "greater" + case "undefined:number": + case "undefined:string": + return "less" + } + } + } + + return "equal" + } + + static parse(version: string): Version { + const parsed = P.parse(version, { startRule: "Version" }) + return new Version(parsed.number, parsed.prerelease) + } +} + +// #flavor:0.1.2-beta.1:0 +export class ExtendedVersion { + constructor( + public flavor: string | null, + public upstream: Version, + public downstream: Version, + ) {} + + toString(): string { + return `${this.flavor ? `#${this.flavor}:` : ""}${this.upstream.toString()}:${this.downstream.toString()}` + } + + compare(other: ExtendedVersion): "greater" | "equal" | "less" | null { + if (this.flavor !== other.flavor) { + return null + } + const upstreamCmp = this.upstream.compare(other.upstream) + if (upstreamCmp !== "equal") { + return upstreamCmp + } + return this.downstream.compare(other.downstream) + } + + compareLexicographic(other: ExtendedVersion): "greater" | "equal" | "less" { + if ((this.flavor || "") > (other.flavor || "")) { + return "greater" + } else if ((this.flavor || "") > (other.flavor || "")) { + return "less" + } else { + return this.compare(other)! + } + } + + compareForSort(other: ExtendedVersion): 1 | 0 | -1 { + switch (this.compareLexicographic(other)) { + case "greater": + return 1 + case "equal": + return 0 + case "less": + return -1 + } + } + + greaterThan(other: ExtendedVersion): boolean { + return this.compare(other) === "greater" + } + + greaterThanOrEqual(other: ExtendedVersion): boolean { + return ["greater", "equal"].includes(this.compare(other) as string) + } + + equals(other: ExtendedVersion): boolean { + return this.compare(other) === "equal" + } + + lessThan(other: ExtendedVersion): boolean { + return this.compare(other) === "less" + } + + lessThanOrEqual(other: ExtendedVersion): boolean { + return ["less", "equal"].includes(this.compare(other) as string) + } + + static parse(extendedVersion: string): ExtendedVersion { + const parsed = P.parse(extendedVersion, { startRule: "ExtendedVersion" }) + return new ExtendedVersion( + parsed.flavor, + new Version(parsed.upstream.number, parsed.upstream.prerelease), + new Version(parsed.downstream.number, parsed.downstream.prerelease), + ) + } + + static parseEmver(extendedVersion: string): ExtendedVersion { + const parsed = P.parse(extendedVersion, { startRule: "EmVer" }) + return new ExtendedVersion( + parsed.flavor, + new Version(parsed.upstream.number, parsed.upstream.prerelease), + new Version(parsed.downstream.number, parsed.downstream.prerelease), + ) + } + + /** + * Returns an ExtendedVersion with the Upstream major version version incremented by 1 + * and sets subsequent digits to zero. + * If no non-zero upstream digit can be found the last upstream digit will be incremented. + */ + incrementMajor(): ExtendedVersion { + const majorIdx = this.upstream.number.findIndex((num: number) => num !== 0) + + const majorNumber = this.upstream.number.map((num, idx): number => { + if (idx > majorIdx) { + return 0 + } else if (idx === majorIdx) { + return num + 1 + } + return num + }) + + const incrementedUpstream = new Version(majorNumber, []) + const updatedDownstream = new Version([0], []) + + return new ExtendedVersion( + this.flavor, + incrementedUpstream, + updatedDownstream, + ) + } + + /** + * Returns an ExtendedVersion with the Upstream minor version version incremented by 1 + * also sets subsequent digits to zero. + * If no non-zero upstream digit can be found the last digit will be incremented. + */ + incrementMinor(): ExtendedVersion { + const majorIdx = this.upstream.number.findIndex((num: number) => num !== 0) + let minorIdx = majorIdx === -1 ? majorIdx : majorIdx + 1 + + const majorNumber = this.upstream.number.map((num, idx): number => { + if (idx > minorIdx) { + return 0 + } else if (idx === minorIdx) { + return num + 1 + } + return num + }) + + const incrementedUpstream = new Version(majorNumber, []) + const updatedDownstream = new Version([0], []) + + return new ExtendedVersion( + this.flavor, + incrementedUpstream, + updatedDownstream, + ) + } +} + +export const testTypeExVer = (t: T & ValidateExVer) => t + +export const testTypeVersion = (t: T & ValidateVersion) => + t +function tests() { + testTypeVersion("1.2.3") + testTypeVersion("1") + testTypeVersion("12.34.56") + testTypeVersion("1.2-3") + testTypeVersion("1-3") + // @ts-expect-error + testTypeVersion("-3") + // @ts-expect-error + testTypeVersion("1.2.3:1") + // @ts-expect-error + testTypeVersion("#cat:1:1") + + testTypeExVer("1.2.3:1.2.3") + testTypeExVer("1.2.3.4.5.6.7.8.9.0:1") + testTypeExVer("100:1") + testTypeExVer("#cat:1:1") + testTypeExVer("1.2.3.4.5.6.7.8.9.11.22.33:1") + testTypeExVer("1-0:1") + testTypeExVer("1-0:1") + // @ts-expect-error + testTypeExVer("1.2-3") + // @ts-expect-error + testTypeExVer("1-3") + // @ts-expect-error + testTypeExVer("1.2.3.4.5.6.7.8.9.0.10:1" as string) + // @ts-expect-error + testTypeExVer("1.-2:1") + // @ts-expect-error + testTypeExVer("1..2.3:3") +} diff --git a/sdk/lib/health/HealthCheck.ts b/sdk/lib/health/HealthCheck.ts index 1ed8652bf..4b72dbf61 100644 --- a/sdk/lib/health/HealthCheck.ts +++ b/sdk/lib/health/HealthCheck.ts @@ -1,5 +1,4 @@ -import { InterfaceReceipt } from "../interfaces/interfaceReceipt" -import { Daemon, Effects, SDKManifest } from "../types" +import { Effects } from "../types" import { CheckResult } from "./checkFns/CheckResult" import { HealthReceipt } from "./HealthReceipt" import { Trigger } from "../trigger" @@ -8,9 +7,9 @@ import { defaultTrigger } from "../trigger/defaultTrigger" import { once } from "../util/once" import { Overlay } from "../util/Overlay" import { object, unknown } from "ts-matches" -import { T } from ".." +import * as T from "../types" -export type HealthCheckParams = { +export type HealthCheckParams = { effects: Effects name: string image: { @@ -22,7 +21,7 @@ export type HealthCheckParams = { onFirstSuccess?: () => unknown | Promise } -export function healthCheck( +export function healthCheck( o: HealthCheckParams, ) { new Promise(async () => { diff --git a/sdk/lib/index.browser.ts b/sdk/lib/index.browser.ts index c7ab45e60..f7d645133 100644 --- a/sdk/lib/index.browser.ts +++ b/sdk/lib/index.browser.ts @@ -1,12 +1,10 @@ -export { EmVer } from "./emverLite/mod" -export { setupManifest } from "./manifest/setupManifest" -export { setupExposeStore } from "./store/setupExposeStore" export { S9pk } from "./s9pk" +export { VersionRange, ExtendedVersion, Version } from "./exver" + export * as config from "./config" export * as CB from "./config/builder" export * as CT from "./config/configTypes" export * as dependencyConfig from "./dependencies" -export * as manifest from "./manifest" export * as types from "./types" export * as T from "./types" export * as yaml from "yaml" diff --git a/sdk/lib/index.ts b/sdk/lib/index.ts index c99798d72..935ffc023 100644 --- a/sdk/lib/index.ts +++ b/sdk/lib/index.ts @@ -1,5 +1,4 @@ export { Daemons } from "./mainFn/Daemons" -export { EmVer } from "./emverLite/mod" export { Overlay } from "./util/Overlay" export { StartSdk } from "./StartSdk" export { setupManifest } from "./manifest/setupManifest" @@ -7,6 +6,7 @@ export { FileHelper } from "./util/fileHelper" export { setupExposeStore } from "./store/setupExposeStore" export { pathBuilder } from "./store/PathBuilder" export { S9pk } from "./s9pk" +export { VersionRange, ExtendedVersion, Version } from "./exver" export * as actions from "./actions" export * as backup from "./backup" diff --git a/sdk/lib/inits/migrations/Migration.ts b/sdk/lib/inits/migrations/Migration.ts index 119271aea..16be93dbd 100644 --- a/sdk/lib/inits/migrations/Migration.ts +++ b/sdk/lib/inits/migrations/Migration.ts @@ -1,35 +1,35 @@ -import { ManifestVersion, SDKManifest } from "../../manifest/ManifestTypes" -import { Effects } from "../../types" +import { ValidateExVer } from "../../exver" +import * as T from "../../types" export class Migration< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, - Version extends ManifestVersion, + Version extends string, > { constructor( readonly options: { - version: Version - up: (opts: { effects: Effects }) => Promise - down: (opts: { effects: Effects }) => Promise + version: Version & ValidateExVer + up: (opts: { effects: T.Effects }) => Promise + down: (opts: { effects: T.Effects }) => Promise }, ) {} static of< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, - Version extends ManifestVersion, + Version extends string, >(options: { - version: Version - up: (opts: { effects: Effects }) => Promise - down: (opts: { effects: Effects }) => Promise + version: Version & ValidateExVer + up: (opts: { effects: T.Effects }) => Promise + down: (opts: { effects: T.Effects }) => Promise }) { return new Migration(options) } - async up(opts: { effects: Effects }) { + async up(opts: { effects: T.Effects }) { this.up(opts) } - async down(opts: { effects: Effects }) { + async down(opts: { effects: T.Effects }) { this.down(opts) } } diff --git a/sdk/lib/inits/migrations/setupMigrations.ts b/sdk/lib/inits/migrations/setupMigrations.ts index 288b2b9d7..6d690b239 100644 --- a/sdk/lib/inits/migrations/setupMigrations.ts +++ b/sdk/lib/inits/migrations/setupMigrations.ts @@ -1,27 +1,31 @@ -import { EmVer } from "../../emverLite/mod" -import { SDKManifest } from "../../manifest/ManifestTypes" -import { ExpectedExports } from "../../types" +import { ExtendedVersion } from "../../exver" + +import * as T from "../../types" import { once } from "../../util/once" import { Migration } from "./Migration" -export class Migrations { +export class Migrations { private constructor( - readonly manifest: SDKManifest, + readonly manifest: T.Manifest, readonly migrations: Array>, ) {} private sortedMigrations = once(() => { const migrationsAsVersions = ( this.migrations as Array> - ).map((x) => [EmVer.parse(x.options.version), x] as const) + ) + .map((x) => [ExtendedVersion.parse(x.options.version), x] as const) + .filter(([v, _]) => v.flavor === this.currentVersion().flavor) migrationsAsVersions.sort((a, b) => a[0].compareForSort(b[0])) return migrationsAsVersions }) - private currentVersion = once(() => EmVer.parse(this.manifest.version)) + private currentVersion = once(() => + ExtendedVersion.parse(this.manifest.version), + ) static of< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, Migrations extends Array>, - >(manifest: SDKManifest, ...migrations: EnsureUniqueId) { + >(manifest: T.Manifest, ...migrations: EnsureUniqueId) { return new Migrations( manifest, migrations as Array>, @@ -30,11 +34,11 @@ export class Migrations { async init({ effects, previousVersion, - }: Parameters[0]) { + }: Parameters[0]) { if (!!previousVersion) { - const previousVersionEmVer = EmVer.parse(previousVersion) + const previousVersionExVer = ExtendedVersion.parse(previousVersion) for (const [_, migration] of this.sortedMigrations() - .filter((x) => x[0].greaterThan(previousVersionEmVer)) + .filter((x) => x[0].greaterThan(previousVersionExVer)) .filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) { await migration.up({ effects }) } @@ -43,12 +47,12 @@ export class Migrations { async uninit({ effects, nextVersion, - }: Parameters[0]) { + }: Parameters[0]) { if (!!nextVersion) { - const nextVersionEmVer = EmVer.parse(nextVersion) + const nextVersionExVer = ExtendedVersion.parse(nextVersion) const reversed = [...this.sortedMigrations()].reverse() for (const [_, migration] of reversed - .filter((x) => x[0].greaterThan(nextVersionEmVer)) + .filter((x) => x[0].greaterThan(nextVersionExVer)) .filter((x) => x[0].lessThanOrEqual(this.currentVersion()))) { await migration.down({ effects }) } @@ -57,10 +61,10 @@ export class Migrations { } export function setupMigrations< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, Migrations extends Array>, ->(manifest: SDKManifest, ...migrations: EnsureUniqueId) { +>(manifest: T.Manifest, ...migrations: EnsureUniqueId) { return Migrations.of(manifest, ...migrations) } diff --git a/sdk/lib/inits/setupInit.ts b/sdk/lib/inits/setupInit.ts index 03a7085c5..5718caa58 100644 --- a/sdk/lib/inits/setupInit.ts +++ b/sdk/lib/inits/setupInit.ts @@ -1,25 +1,25 @@ import { DependenciesReceipt } from "../config/setupConfig" import { SetInterfaces } from "../interfaces/setupInterfaces" -import { SDKManifest } from "../manifest/ManifestTypes" + import { ExposedStorePaths } from "../store/setupExposeStore" -import { Effects, ExpectedExports } from "../types" +import * as T from "../types" import { Migrations } from "./migrations/setupMigrations" import { Install } from "./setupInstall" import { Uninstall } from "./setupUninstall" -export function setupInit( +export function setupInit( migrations: Migrations, install: Install, uninstall: Uninstall, setInterfaces: SetInterfaces, setDependencies: (options: { - effects: Effects + effects: T.Effects input: any }) => Promise, exposedStore: ExposedStorePaths, ): { - init: ExpectedExports.init - uninit: ExpectedExports.uninit + init: T.ExpectedExports.init + uninit: T.ExpectedExports.uninit } { return { init: async (opts) => { diff --git a/sdk/lib/inits/setupInstall.ts b/sdk/lib/inits/setupInstall.ts index 3990be0ca..7b51a22ea 100644 --- a/sdk/lib/inits/setupInstall.ts +++ b/sdk/lib/inits/setupInstall.ts @@ -1,12 +1,11 @@ -import { SDKManifest } from "../manifest/ManifestTypes" -import { Effects, ExpectedExports } from "../types" +import * as T from "../types" -export type InstallFn = (opts: { - effects: Effects +export type InstallFn = (opts: { + effects: T.Effects }) => Promise -export class Install { +export class Install { private constructor(readonly fn: InstallFn) {} - static of( + static of( fn: InstallFn, ) { return new Install(fn) @@ -15,7 +14,7 @@ export class Install { async init({ effects, previousVersion, - }: Parameters[0]) { + }: Parameters[0]) { if (!previousVersion) await this.fn({ effects, @@ -23,7 +22,7 @@ export class Install { } } -export function setupInstall( +export function setupInstall( fn: InstallFn, ) { return Install.of(fn) diff --git a/sdk/lib/inits/setupUninstall.ts b/sdk/lib/inits/setupUninstall.ts index 812848c8f..c8c3e490f 100644 --- a/sdk/lib/inits/setupUninstall.ts +++ b/sdk/lib/inits/setupUninstall.ts @@ -1,12 +1,11 @@ -import { SDKManifest } from "../manifest/ManifestTypes" -import { Effects, ExpectedExports } from "../types" +import * as T from "../types" -export type UninstallFn = (opts: { - effects: Effects +export type UninstallFn = (opts: { + effects: T.Effects }) => Promise -export class Uninstall { +export class Uninstall { private constructor(readonly fn: UninstallFn) {} - static of( + static of( fn: UninstallFn, ) { return new Uninstall(fn) @@ -15,7 +14,7 @@ export class Uninstall { async uninit({ effects, nextVersion, - }: Parameters[0]) { + }: Parameters[0]) { if (!nextVersion) await this.fn({ effects, @@ -23,7 +22,7 @@ export class Uninstall { } } -export function setupUninstall( +export function setupUninstall( fn: UninstallFn, ) { return Uninstall.of(fn) diff --git a/sdk/lib/interfaces/setupInterfaces.ts b/sdk/lib/interfaces/setupInterfaces.ts index 5ad8d8a7d..c82b69e0b 100644 --- a/sdk/lib/interfaces/setupInterfaces.ts +++ b/sdk/lib/interfaces/setupInterfaces.ts @@ -1,17 +1,17 @@ import { Config } from "../config/builder/config" -import { SDKManifest } from "../manifest/ManifestTypes" -import { AddressInfo, Effects } from "../types" + +import * as T from "../types" import { AddressReceipt } from "./AddressReceipt" -export type InterfacesReceipt = Array +export type InterfacesReceipt = Array export type SetInterfaces< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, ConfigInput extends Record, Output extends InterfacesReceipt, -> = (opts: { effects: Effects; input: null | ConfigInput }) => Promise +> = (opts: { effects: T.Effects; input: null | ConfigInput }) => Promise export type SetupInterfaces = < - Manifest extends SDKManifest, + Manifest extends T.Manifest, Store, ConfigInput extends Record, Output extends InterfacesReceipt, diff --git a/sdk/lib/mainFn/CommandController.ts b/sdk/lib/mainFn/CommandController.ts index 3af01e915..40f787f86 100644 --- a/sdk/lib/mainFn/CommandController.ts +++ b/sdk/lib/mainFn/CommandController.ts @@ -1,7 +1,7 @@ import { DEFAULT_SIGTERM_TIMEOUT } from "." import { NO_TIMEOUT, SIGKILL, SIGTERM } from "../StartSdk" -import { SDKManifest } from "../manifest/ManifestTypes" -import { Effects, ImageId, ValidIfNoStupidEscape } from "../types" + +import * as T from "../types" import { MountOptions, Overlay } from "../util/Overlay" import { splitCommand } from "../util/splitCommand" import { cpExecFile, cpExec } from "./Daemons" @@ -13,14 +13,14 @@ export class CommandController { readonly pid: number | undefined, readonly sigtermTimeout: number = DEFAULT_SIGTERM_TIMEOUT, ) {} - static of() { + static of() { return async ( - effects: Effects, + effects: T.Effects, imageId: { - id: keyof Manifest["images"] & ImageId + id: keyof Manifest["images"] & T.ImageId sharedRun?: boolean }, - command: ValidIfNoStupidEscape | [string, ...string[]], + command: T.CommandType, options: { // Defaults to the DEFAULT_SIGTERM_TIMEOUT = 30_000ms sigtermTimeout?: number diff --git a/sdk/lib/mainFn/Daemon.ts b/sdk/lib/mainFn/Daemon.ts index 90882418c..6dceda951 100644 --- a/sdk/lib/mainFn/Daemon.ts +++ b/sdk/lib/mainFn/Daemon.ts @@ -1,5 +1,4 @@ -import { SDKManifest } from "../manifest/ManifestTypes" -import { Effects, ImageId, ValidIfNoStupidEscape } from "../types" +import * as T from "../types" import { MountOptions, Overlay } from "../util/Overlay" import { CommandController } from "./CommandController" @@ -14,14 +13,14 @@ export class Daemon { private commandController: CommandController | null = null private shouldBeRunning = false private constructor(private startCommand: () => Promise) {} - static of() { + static of() { return async ( - effects: Effects, + effects: T.Effects, imageId: { - id: keyof Manifest["images"] & ImageId + id: keyof Manifest["images"] & T.ImageId sharedRun?: boolean }, - command: ValidIfNoStupidEscape | [string, ...string[]], + command: T.CommandType, options: { mounts?: { path: string; options: MountOptions }[] overlay?: Overlay diff --git a/sdk/lib/mainFn/Daemons.ts b/sdk/lib/mainFn/Daemons.ts index fbda547aa..c766e2f2e 100644 --- a/sdk/lib/mainFn/Daemons.ts +++ b/sdk/lib/mainFn/Daemons.ts @@ -1,16 +1,11 @@ import { NO_TIMEOUT, SIGKILL, SIGTERM, Signals } from "../StartSdk" import { HealthReceipt } from "../health/HealthReceipt" import { CheckResult } from "../health/checkFns" -import { SDKManifest } from "../manifest/ManifestTypes" + import { Trigger } from "../trigger" import { TriggerInput } from "../trigger/TriggerInput" import { defaultTrigger } from "../trigger/defaultTrigger" -import { - DaemonReturned, - Effects, - ImageId, - ValidIfNoStupidEscape, -} from "../types" +import * as T from "../types" import { Mounts } from "./Mounts" import { CommandOptions, MountOptions, Overlay } from "../util/Overlay" import { splitCommand } from "../util/splitCommand" @@ -33,13 +28,13 @@ export type Ready = { } type DaemonsParams< - Manifest extends SDKManifest, + Manifest extends T.Manifest, Ids extends string, Command extends string, Id extends string, > = { - command: ValidIfNoStupidEscape | [string, ...string[]] - image: { id: keyof Manifest["images"] & ImageId; sharedRun?: boolean } + command: T.CommandType + image: { id: keyof Manifest["images"] & T.ImageId; sharedRun?: boolean } mounts: Mounts env?: Record ready: Ready @@ -49,7 +44,7 @@ type DaemonsParams< type ErrorDuplicateId = `The id '${Id}' is already used` -export const runCommand = () => +export const runCommand = () => CommandController.of() /** @@ -75,9 +70,9 @@ Daemons.of({ }) ``` */ -export class Daemons { +export class Daemons { private constructor( - readonly effects: Effects, + readonly effects: T.Effects, readonly started: (onTerm: () => PromiseLike) => PromiseLike, readonly daemons: Promise[], readonly ids: Ids[], @@ -93,8 +88,8 @@ export class Daemons { * @param config * @returns */ - static of(config: { - effects: Effects + static of(config: { + effects: T.Effects started: (onTerm: () => PromiseLike) => PromiseLike healthReceipts: HealthReceipt[] }) { diff --git a/sdk/lib/mainFn/Mounts.ts b/sdk/lib/mainFn/Mounts.ts index eeedc79c6..968b77b4e 100644 --- a/sdk/lib/mainFn/Mounts.ts +++ b/sdk/lib/mainFn/Mounts.ts @@ -1,10 +1,9 @@ -import { SDKManifest } from "../manifest/ManifestTypes" -import { Effects } from "../types" +import * as T from "../types" import { MountOptions } from "../util/Overlay" type MountArray = { path: string; options: MountOptions }[] -export class Mounts { +export class Mounts { private constructor( readonly volumes: { id: Manifest["volumes"][number] @@ -26,7 +25,7 @@ export class Mounts { }[], ) {} - static of() { + static of() { return new Mounts([], [], []) } @@ -58,7 +57,7 @@ export class Mounts { return this } - addDependency( + addDependency( dependencyId: keyof Manifest["dependencies"] & string, volumeId: DependencyManifest["volumes"][number], subpath: string | null, diff --git a/sdk/lib/mainFn/index.ts b/sdk/lib/mainFn/index.ts index c4f74764c..7a094a31a 100644 --- a/sdk/lib/mainFn/index.ts +++ b/sdk/lib/mainFn/index.ts @@ -1,10 +1,10 @@ -import { ExpectedExports } from "../types" +import * as T from "../types" import { Daemons } from "./Daemons" import "../interfaces/ServiceInterfaceBuilder" import "../interfaces/Origin" import "./Daemons" -import { SDKManifest } from "../manifest/ManifestTypes" + import { MainEffects } from "../StartSdk" export const DEFAULT_SIGTERM_TIMEOUT = 30_000 @@ -18,12 +18,12 @@ export const DEFAULT_SIGTERM_TIMEOUT = 30_000 * @param fn * @returns */ -export const setupMain = ( +export const setupMain = ( fn: (o: { effects: MainEffects started(onTerm: () => PromiseLike): PromiseLike }) => Promise>, -): ExpectedExports.main => { +): T.ExpectedExports.main => { return async (options) => { const result = await fn(options) return result diff --git a/sdk/lib/manifest/ManifestTypes.ts b/sdk/lib/manifest/ManifestTypes.ts index 8e6b572d3..ea349710f 100644 --- a/sdk/lib/manifest/ManifestTypes.ts +++ b/sdk/lib/manifest/ManifestTypes.ts @@ -1,20 +1,16 @@ -import { ValidEmVer } from "../emverLite/mod" -import { ActionMetadata, ImageConfig, ImageId } from "../types" +import { ValidateExVer, ValidateExVers } from "../exver" +import { + ActionMetadata, + HardwareRequirements, + ImageConfig, + ImageId, + ImageSource, +} from "../types" -export type Container = { - /** This should be pointing to a docker container name */ - image: string - /** These should match the manifest data volumes */ - mounts: Record - /** Default is 64mb */ - shmSizeMb?: `${number}${"mb" | "gb" | "b" | "kb"}` - /** if more than 30s to shutdown */ - sigtermTimeout?: `${number}${"s" | "m" | "h"}` -} - -export type ManifestVersion = ValidEmVer - -export type SDKManifest = { +export type SDKManifest< + Version extends string, + Satisfies extends string[] = [], +> = { /** The package identifier used by the OS. This must be unique amongst all other known packages */ readonly id: string /** A human readable service title */ @@ -23,7 +19,8 @@ export type SDKManifest = { * - see documentation: https://github.com/Start9Labs/emver-rs. This value will change with each release of * the service */ - readonly version: ManifestVersion + readonly version: Version & ValidateExVer + readonly satisfies?: Satisfies & ValidateExVers /** Release notes for the update - can be a string, paragraph or URL */ readonly releaseNotes: string /** The type of license for the project. Include the LICENSE in the root of the project directory. A license is required for a Start9 package.*/ @@ -50,36 +47,49 @@ export type SDKManifest = { } /** Defines the os images needed to run the container processes */ - readonly images: Record + readonly images: Record /** This denotes readonly asset directories that should be available to mount to the container. - * Assuming that there will be three files with names along the lines: - * icon.* : the icon that will be this packages icon on the ui - * LICENSE : What the license is for this service - * Instructions : to be seen in the ui section of the package - * */ + * These directories are expected to be found in `assets/` at pack time. + **/ readonly assets: string[] /** This denotes any data volumes that should be available to mount to the container */ readonly volumes: string[] - readonly alerts: { - readonly install: string | null - readonly update: string | null - readonly uninstall: string | null - readonly restore: string | null - readonly start: string | null - readonly stop: string | null + readonly alerts?: { + readonly install?: string | null + readonly update?: string | null + readonly uninstall?: string | null + readonly restore?: string | null + readonly start?: string | null + readonly stop?: string | null } + readonly hasConfig?: boolean readonly dependencies: Readonly> + readonly hardwareRequirements?: { + readonly device?: { display?: RegExp; processor?: RegExp } + readonly ram?: number | null + readonly arch?: string[] | null + } +} + +export type SDKImageConfig = { + source: Exclude + arch?: string[] + emulateMissingAs?: string | null } export type ManifestDependency = { /** * A human readable explanation on what the dependency is used for */ - description: string | null + readonly description: string | null /** * Determines if the dependency is optional or not. Times that optional that are good include such situations * such as being able to toggle other services or to use a different service for the same purpose. */ - optional: boolean + readonly optional: boolean + /** + * A url or local path for an s9pk that satisfies this dependency + */ + readonly s9pk: string } diff --git a/sdk/lib/manifest/setupManifest.ts b/sdk/lib/manifest/setupManifest.ts index 8bd39a7aa..e3b746874 100644 --- a/sdk/lib/manifest/setupManifest.ts +++ b/sdk/lib/manifest/setupManifest.ts @@ -1,21 +1,71 @@ +import * as T from "../types" import { ImageConfig, ImageId, VolumeId } from "../osBindings" -import { SDKManifest, ManifestVersion } from "./ManifestTypes" +import { SDKManifest, SDKImageConfig } from "./ManifestTypes" +import { SDKVersion } from "../StartSdk" export function setupManifest< Id extends string, - Version extends ManifestVersion, + Version extends string, Dependencies extends Record, VolumesTypes extends VolumeId, AssetTypes extends VolumeId, ImagesTypes extends ImageId, - Manifest extends SDKManifest & { + Manifest extends SDKManifest & { dependencies: Dependencies id: Id - version: Version assets: AssetTypes[] - images: Record + images: Record volumes: VolumesTypes[] }, ->(manifest: Manifest): Manifest { - return manifest + Satisfies extends string[] = [], +>(manifest: Manifest & { version: Version }): Manifest & T.Manifest { + const images = Object.entries(manifest.images).reduce( + (images, [k, v]) => { + v.arch = v.arch || ["aarch64", "x86_64"] + if (v.emulateMissingAs === undefined) + v.emulateMissingAs = v.arch[0] || null + images[k] = v as ImageConfig + return images + }, + {} as { [k: string]: ImageConfig }, + ) + return { + ...manifest, + gitHash: null, + osVersion: SDKVersion, + satisfies: manifest.satisfies || [], + images, + alerts: { + install: manifest.alerts?.install || null, + update: manifest.alerts?.update || null, + uninstall: manifest.alerts?.uninstall || null, + restore: manifest.alerts?.restore || null, + start: manifest.alerts?.start || null, + stop: manifest.alerts?.stop || null, + }, + hasConfig: manifest.hasConfig === undefined ? true : manifest.hasConfig, + hardwareRequirements: { + device: Object.fromEntries( + Object.entries(manifest.hardwareRequirements?.device || {}).map( + ([k, v]) => [k, v.source], + ), + ), + ram: manifest.hardwareRequirements?.ram || null, + arch: + manifest.hardwareRequirements?.arch === undefined + ? Object.values(images).reduce( + (arch, config) => { + if (config.emulateMissingAs) { + return arch + } + if (arch === null) { + return config.arch + } + return arch.filter((a) => config.arch.includes(a)) + }, + null as string[] | null, + ) + : manifest.hardwareRequirements?.arch, + }, + } } diff --git a/sdk/lib/osBindings/CheckDependenciesResult.ts b/sdk/lib/osBindings/CheckDependenciesResult.ts index c102c733a..d349bdf18 100644 --- a/sdk/lib/osBindings/CheckDependenciesResult.ts +++ b/sdk/lib/osBindings/CheckDependenciesResult.ts @@ -1,4 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HealthCheckId } from "./HealthCheckId" import type { HealthCheckResult } from "./HealthCheckResult" import type { PackageId } from "./PackageId" @@ -6,6 +7,7 @@ export type CheckDependenciesResult = { packageId: PackageId isInstalled: boolean isRunning: boolean - healthChecks: Array + configSatisfied: boolean + healthChecks: { [key: HealthCheckId]: HealthCheckResult } version: string | null } diff --git a/sdk/lib/osBindings/CurrentDependencyInfo.ts b/sdk/lib/osBindings/CurrentDependencyInfo.ts index de46e4b52..2096a0113 100644 --- a/sdk/lib/osBindings/CurrentDependencyInfo.ts +++ b/sdk/lib/osBindings/CurrentDependencyInfo.ts @@ -2,9 +2,8 @@ import type { DataUrl } from "./DataUrl" export type CurrentDependencyInfo = { - title: string - icon: DataUrl - registryUrl: string - versionSpec: string + title: string | null + icon: DataUrl | null + versionRange: string configSatisfied: boolean } & ({ kind: "exists" } | { kind: "running"; healthChecks: string[] }) diff --git a/sdk/lib/osBindings/DepInfo.ts b/sdk/lib/osBindings/DepInfo.ts index 20b9eb4cd..d635cca3b 100644 --- a/sdk/lib/osBindings/DepInfo.ts +++ b/sdk/lib/osBindings/DepInfo.ts @@ -1,3 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PathOrUrl } from "./PathOrUrl" -export type DepInfo = { description: string | null; optional: boolean } +export type DepInfo = { + description: string | null + optional: boolean + s9pk: PathOrUrl | null +} diff --git a/sdk/lib/osBindings/DependencyMetadata.ts b/sdk/lib/osBindings/DependencyMetadata.ts new file mode 100644 index 000000000..3d56ef052 --- /dev/null +++ b/sdk/lib/osBindings/DependencyMetadata.ts @@ -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 { DataUrl } from "./DataUrl" + +export type DependencyMetadata = { + title: string | null + icon: DataUrl | null + description: string | null + optional: boolean +} diff --git a/sdk/lib/osBindings/DependencyRequirement.ts b/sdk/lib/osBindings/DependencyRequirement.ts index d0415bee9..01b8e12ce 100644 --- a/sdk/lib/osBindings/DependencyRequirement.ts +++ b/sdk/lib/osBindings/DependencyRequirement.ts @@ -5,7 +5,6 @@ export type DependencyRequirement = kind: "running" id: string healthChecks: string[] - versionSpec: string - registryUrl: string + versionRange: string } - | { kind: "exists"; id: string; versionSpec: string; registryUrl: string } + | { kind: "exists"; id: string; versionRange: string } diff --git a/sdk/lib/osBindings/FullIndex.ts b/sdk/lib/osBindings/FullIndex.ts index 4d9914015..c7889760a 100644 --- a/sdk/lib/osBindings/FullIndex.ts +++ b/sdk/lib/osBindings/FullIndex.ts @@ -6,6 +6,7 @@ import type { PackageIndex } from "./PackageIndex" import type { SignerInfo } from "./SignerInfo" export type FullIndex = { + name: string | null icon: DataUrl | null package: PackageIndex os: OsIndex diff --git a/sdk/lib/osBindings/GetVersionParams.ts b/sdk/lib/osBindings/GetOsVersionParams.ts similarity index 85% rename from sdk/lib/osBindings/GetVersionParams.ts rename to sdk/lib/osBindings/GetOsVersionParams.ts index 853d76022..de0458645 100644 --- a/sdk/lib/osBindings/GetVersionParams.ts +++ b/sdk/lib/osBindings/GetOsVersionParams.ts @@ -1,6 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type GetVersionParams = { +export type GetOsVersionParams = { source: string | null target: string | null serverId: string | null diff --git a/sdk/lib/osBindings/GetPackageParams.ts b/sdk/lib/osBindings/GetPackageParams.ts index 8b852c35c..3dde55b28 100644 --- a/sdk/lib/osBindings/GetPackageParams.ts +++ b/sdk/lib/osBindings/GetPackageParams.ts @@ -7,5 +7,5 @@ export type GetPackageParams = { id: PackageId | null version: string | null sourceVersion: Version | null - otherVersions: PackageDetailLevel | null + otherVersions: PackageDetailLevel } diff --git a/sdk/lib/osBindings/HardwareRequirements.ts b/sdk/lib/osBindings/HardwareRequirements.ts index 0e1da1f36..3579e9524 100644 --- a/sdk/lib/osBindings/HardwareRequirements.ts +++ b/sdk/lib/osBindings/HardwareRequirements.ts @@ -1,7 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. export type HardwareRequirements = { - device: { [key: string]: string } + device: { device?: string; processor?: string } ram: number | null arch: string[] | null } diff --git a/sdk/lib/osBindings/InstallParams.ts b/sdk/lib/osBindings/InstallParams.ts new file mode 100644 index 000000000..2b70ad593 --- /dev/null +++ b/sdk/lib/osBindings/InstallParams.ts @@ -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 InstallParams = { + registry: string + id: PackageId + version: Version +} diff --git a/sdk/lib/osBindings/Manifest.ts b/sdk/lib/osBindings/Manifest.ts index d808f47a2..51f14935a 100644 --- a/sdk/lib/osBindings/Manifest.ts +++ b/sdk/lib/osBindings/Manifest.ts @@ -13,6 +13,7 @@ export type Manifest = { id: PackageId title: string version: Version + satisfies: Array releaseNotes: string license: string wrapperRepo: string diff --git a/sdk/lib/osBindings/PackageDetailLevel.ts b/sdk/lib/osBindings/PackageDetailLevel.ts index b5e1ae42b..f2016f632 100644 --- a/sdk/lib/osBindings/PackageDetailLevel.ts +++ b/sdk/lib/osBindings/PackageDetailLevel.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type PackageDetailLevel = "short" | "full" +export type PackageDetailLevel = "none" | "short" | "full" diff --git a/sdk/lib/osBindings/PackageVersionInfo.ts b/sdk/lib/osBindings/PackageVersionInfo.ts index 364c530f2..80481acb3 100644 --- a/sdk/lib/osBindings/PackageVersionInfo.ts +++ b/sdk/lib/osBindings/PackageVersionInfo.ts @@ -1,8 +1,11 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Alerts } from "./Alerts" import type { DataUrl } from "./DataUrl" +import type { DependencyMetadata } from "./DependencyMetadata" import type { Description } from "./Description" import type { HardwareRequirements } from "./HardwareRequirements" import type { MerkleArchiveCommitment } from "./MerkleArchiveCommitment" +import type { PackageId } from "./PackageId" import type { RegistryAsset } from "./RegistryAsset" export type PackageVersionInfo = { @@ -16,6 +19,9 @@ export type PackageVersionInfo = { upstreamRepo: string supportSite: string marketingSite: string + donationUrl: string | null + alerts: Alerts + dependencyMetadata: { [key: PackageId]: DependencyMetadata } osVersion: string hardwareRequirements: HardwareRequirements sourceVersion: string | null diff --git a/sdk/lib/osBindings/PathOrUrl.ts b/sdk/lib/osBindings/PathOrUrl.ts new file mode 100644 index 000000000..9c4ff1e28 --- /dev/null +++ b/sdk/lib/osBindings/PathOrUrl.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PathOrUrl = string diff --git a/sdk/lib/osBindings/RegistryAsset.ts b/sdk/lib/osBindings/RegistryAsset.ts index 3eb13e8a4..41f09431f 100644 --- a/sdk/lib/osBindings/RegistryAsset.ts +++ b/sdk/lib/osBindings/RegistryAsset.ts @@ -3,6 +3,7 @@ import type { AnySignature } from "./AnySignature" import type { AnyVerifyingKey } from "./AnyVerifyingKey" export type RegistryAsset = { + publishedAt: string url: string commitment: Commitment signatures: { [key: AnyVerifyingKey]: AnySignature } diff --git a/sdk/lib/osBindings/RegistryInfo.ts b/sdk/lib/osBindings/RegistryInfo.ts new file mode 100644 index 000000000..f9265fdec --- /dev/null +++ b/sdk/lib/osBindings/RegistryInfo.ts @@ -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 { Category } from "./Category" +import type { DataUrl } from "./DataUrl" + +export type RegistryInfo = { + name: string | null + icon: DataUrl | null + categories: { [key: string]: Category } +} diff --git a/sdk/lib/osBindings/index.ts b/sdk/lib/osBindings/index.ts index 32e57956a..70ce65f29 100644 --- a/sdk/lib/osBindings/index.ts +++ b/sdk/lib/osBindings/index.ts @@ -37,6 +37,7 @@ export { CurrentDependencyInfo } from "./CurrentDependencyInfo" export { DataUrl } from "./DataUrl" export { Dependencies } from "./Dependencies" export { DependencyKind } from "./DependencyKind" +export { DependencyMetadata } from "./DependencyMetadata" export { DependencyRequirement } from "./DependencyRequirement" export { DepInfo } from "./DepInfo" export { Description } from "./Description" @@ -51,6 +52,7 @@ export { FullIndex } from "./FullIndex" export { FullProgress } from "./FullProgress" export { GetHostInfoParams } from "./GetHostInfoParams" export { GetOsAssetParams } from "./GetOsAssetParams" +export { GetOsVersionParams } from "./GetOsVersionParams" export { GetPackageParams } from "./GetPackageParams" export { GetPackageResponseFull } from "./GetPackageResponseFull" export { GetPackageResponse } from "./GetPackageResponse" @@ -61,7 +63,6 @@ export { GetSslCertificateParams } from "./GetSslCertificateParams" export { GetSslKeyParams } from "./GetSslKeyParams" export { GetStoreParams } from "./GetStoreParams" export { GetSystemSmtpParams } from "./GetSystemSmtpParams" -export { GetVersionParams } from "./GetVersionParams" export { Governor } from "./Governor" export { Guid } from "./Guid" export { HardwareRequirements } from "./HardwareRequirements" @@ -82,6 +83,7 @@ export { InstalledState } from "./InstalledState" export { InstalledVersionParams } from "./InstalledVersionParams" export { InstallingInfo } from "./InstallingInfo" export { InstallingState } from "./InstallingState" +export { InstallParams } from "./InstallParams" export { IpHostname } from "./IpHostname" export { IpInfo } from "./IpInfo" export { LanInfo } from "./LanInfo" @@ -109,11 +111,13 @@ export { PackageVersionInfo } from "./PackageVersionInfo" export { ParamsMaybePackageId } from "./ParamsMaybePackageId" export { ParamsPackageId } from "./ParamsPackageId" export { PasswordType } from "./PasswordType" +export { PathOrUrl } from "./PathOrUrl" export { ProcedureId } from "./ProcedureId" export { Progress } from "./Progress" export { Public } from "./Public" export { RecoverySource } from "./RecoverySource" export { RegistryAsset } from "./RegistryAsset" +export { RegistryInfo } from "./RegistryInfo" export { RemoveActionParams } from "./RemoveActionParams" export { RemoveAddressParams } from "./RemoveAddressParams" export { RemoveVersionParams } from "./RemoveVersionParams" diff --git a/sdk/lib/test/configBuilder.test.ts b/sdk/lib/test/configBuilder.test.ts index c5003c55b..a413d76b8 100644 --- a/sdk/lib/test/configBuilder.test.ts +++ b/sdk/lib/test/configBuilder.test.ts @@ -369,7 +369,7 @@ describe("values", () => { setupManifest({ id: "testOutput", title: "", - version: "1.0", + version: "1.0.0:0", releaseNotes: "", license: "", replaces: [], @@ -395,9 +395,10 @@ describe("values", () => { stop: null, }, dependencies: { - remoteTest: { + "remote-test": { description: "", optional: true, + s9pk: "https://example.com/remote-test.s9pk", }, }, }), diff --git a/sdk/lib/test/emverList.test.ts b/sdk/lib/test/emverList.test.ts deleted file mode 100644 index 07dbb5aaf..000000000 --- a/sdk/lib/test/emverList.test.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { EmVer, notRange, rangeAnd, rangeOf, rangeOr } from "../emverLite/mod" -describe("EmVer", () => { - { - { - const checker = rangeOf("*") - test("rangeOf('*')", () => { - checker.check("1") - checker.check("1.2") - checker.check("1.2.3") - checker.check("1.2.3.4") - checker.check("1.2.3.4.5") - checker.check("1.2.3.4.5.6") - expect(checker.check("1")).toEqual(true) - expect(checker.check("1.2")).toEqual(true) - expect(checker.check("1.2.3.4")).toEqual(true) - }) - test("rangeOf('*') invalid", () => { - expect(() => checker.check("a")).toThrow() - expect(() => checker.check("")).toThrow() - expect(() => checker.check("1..3")).toThrow() - }) - } - - { - const checker = rangeOf(">1.2.3.4") - test(`rangeOf(">1.2.3.4") valid`, () => { - expect(checker.check("2-beta123")).toEqual(true) - expect(checker.check("2")).toEqual(true) - expect(checker.check("1.2.3.5")).toEqual(true) - expect(checker.check("1.2.3.4.1")).toEqual(true) - }) - - test(`rangeOf(">1.2.3.4") invalid`, () => { - expect(checker.check("1.2.3.4")).toEqual(false) - expect(checker.check("1.2.3")).toEqual(false) - expect(checker.check("1")).toEqual(false) - }) - } - { - const checker = rangeOf("=1.2.3") - test(`rangeOf("=1.2.3") valid`, () => { - expect(checker.check("1.2.3")).toEqual(true) - }) - - test(`rangeOf("=1.2.3") invalid`, () => { - expect(checker.check("2")).toEqual(false) - expect(checker.check("1.2.3.1")).toEqual(false) - expect(checker.check("1.2")).toEqual(false) - }) - } - { - const checker = rangeOf(">=1.2.3.4") - test(`rangeOf(">=1.2.3.4") valid`, () => { - expect(checker.check("2")).toEqual(true) - expect(checker.check("1.2.3.5")).toEqual(true) - expect(checker.check("1.2.3.4.1")).toEqual(true) - expect(checker.check("1.2.3.4")).toEqual(true) - }) - - test(`rangeOf(">=1.2.3.4") invalid`, () => { - expect(checker.check("1.2.3")).toEqual(false) - expect(checker.check("1")).toEqual(false) - }) - } - { - const checker = rangeOf("<1.2.3.4") - test(`rangeOf("<1.2.3.4") invalid`, () => { - expect(checker.check("2")).toEqual(false) - expect(checker.check("1.2.3.5")).toEqual(false) - expect(checker.check("1.2.3.4.1")).toEqual(false) - expect(checker.check("1.2.3.4")).toEqual(false) - }) - - test(`rangeOf("<1.2.3.4") valid`, () => { - expect(checker.check("1.2.3")).toEqual(true) - expect(checker.check("1")).toEqual(true) - }) - } - { - const checker = rangeOf("<=1.2.3.4") - test(`rangeOf("<=1.2.3.4") invalid`, () => { - expect(checker.check("2")).toEqual(false) - expect(checker.check("1.2.3.5")).toEqual(false) - expect(checker.check("1.2.3.4.1")).toEqual(false) - }) - - test(`rangeOf("<=1.2.3.4") valid`, () => { - expect(checker.check("1.2.3")).toEqual(true) - expect(checker.check("1")).toEqual(true) - expect(checker.check("1.2.3.4")).toEqual(true) - }) - } - - { - const checkA = rangeOf(">1") - const checkB = rangeOf("<=2") - - const checker = rangeAnd(checkA, checkB) - test(`simple and(checkers) valid`, () => { - expect(checker.check("2")).toEqual(true) - - expect(checker.check("1.1")).toEqual(true) - }) - test(`simple and(checkers) invalid`, () => { - expect(checker.check("2.1")).toEqual(false) - expect(checker.check("1")).toEqual(false) - expect(checker.check("0")).toEqual(false) - }) - } - { - const checkA = rangeOf("<1") - const checkB = rangeOf("=2") - - const checker = rangeOr(checkA, checkB) - test(`simple or(checkers) valid`, () => { - expect(checker.check("2")).toEqual(true) - expect(checker.check("0.1")).toEqual(true) - }) - test(`simple or(checkers) invalid`, () => { - expect(checker.check("2.1")).toEqual(false) - expect(checker.check("1")).toEqual(false) - expect(checker.check("1.1")).toEqual(false) - }) - } - - { - const checker = rangeOf("1.2.*") - test(`rangeOf(1.2.*) valid`, () => { - expect(checker.check("1.2")).toEqual(true) - expect(checker.check("1.2.1")).toEqual(true) - }) - test(`rangeOf(1.2.*) invalid`, () => { - expect(checker.check("1.3")).toEqual(false) - expect(checker.check("1.3.1")).toEqual(false) - - expect(checker.check("1.1.1")).toEqual(false) - expect(checker.check("1.1")).toEqual(false) - expect(checker.check("1")).toEqual(false) - - expect(checker.check("2")).toEqual(false) - }) - } - - { - const checker = notRange(rangeOf("1.2.*")) - test(`notRange(rangeOf(1.2.*)) valid`, () => { - expect(checker.check("1.3")).toEqual(true) - expect(checker.check("1.3.1")).toEqual(true) - - expect(checker.check("1.1.1")).toEqual(true) - expect(checker.check("1.1")).toEqual(true) - expect(checker.check("1")).toEqual(true) - - expect(checker.check("2")).toEqual(true) - }) - test(`notRange(rangeOf(1.2.*)) invalid `, () => { - expect(checker.check("1.2")).toEqual(false) - expect(checker.check("1.2.1")).toEqual(false) - }) - } - { - const checker = rangeOf("!1.2.*") - test(`!(rangeOf(1.2.*)) valid`, () => { - expect(checker.check("1.3")).toEqual(true) - expect(checker.check("1.3.1")).toEqual(true) - - expect(checker.check("1.1.1")).toEqual(true) - expect(checker.check("1.1")).toEqual(true) - expect(checker.check("1")).toEqual(true) - - expect(checker.check("2")).toEqual(true) - }) - test(`!(rangeOf(1.2.*)) invalid `, () => { - expect(checker.check("1.2")).toEqual(false) - expect(checker.check("1.2.1")).toEqual(false) - }) - } - { - test(`no and ranges`, () => { - expect(() => rangeAnd()).toThrow() - }) - test(`no or ranges`, () => { - expect(() => rangeOr()).toThrow() - }) - } - { - const checker = rangeOf("!>1.2.3.4") - test(`rangeOf("!>1.2.3.4") invalid`, () => { - expect(checker.check("2")).toEqual(false) - expect(checker.check("1.2.3.5")).toEqual(false) - expect(checker.check("1.2.3.4.1")).toEqual(false) - }) - - test(`rangeOf("!>1.2.3.4") valid`, () => { - expect(checker.check("1.2.3.4")).toEqual(true) - expect(checker.check("1.2.3")).toEqual(true) - expect(checker.check("1")).toEqual(true) - }) - } - - { - test(">1 && =1.2", () => { - const checker = rangeOf(">1 && =1.2") - - expect(checker.check("1.2")).toEqual(true) - expect(checker.check("1.2.1")).toEqual(false) - }) - test("=1 || =2", () => { - const checker = rangeOf("=1 || =2") - - expect(checker.check("1")).toEqual(true) - expect(checker.check("2")).toEqual(true) - expect(checker.check("3")).toEqual(false) - }) - - test(">1 && =1.2 || =2", () => { - const checker = rangeOf(">1 && =1.2 || =2") - - expect(checker.check("1.2")).toEqual(true) - expect(checker.check("1")).toEqual(false) - expect(checker.check("2")).toEqual(true) - expect(checker.check("3")).toEqual(false) - }) - - test("&& before || order of operationns: <1.5 && >1 || >1.5 && <3", () => { - const checker = rangeOf("<1.5 && >1 || >1.5 && <3") - expect(checker.check("1.1")).toEqual(true) - expect(checker.check("2")).toEqual(true) - - expect(checker.check("1.5")).toEqual(false) - expect(checker.check("1")).toEqual(false) - expect(checker.check("3")).toEqual(false) - }) - - test("Compare function on the emver", () => { - const a = EmVer.from("1.2.3") - const b = EmVer.from("1.2.4") - - expect(a.compare(b)).toEqual("less") - expect(b.compare(a)).toEqual("greater") - expect(a.compare(a)).toEqual("equal") - }) - test("Compare for sort function on the emver", () => { - const a = EmVer.from("1.2.3") - const b = EmVer.from("1.2.4") - - expect(a.compareForSort(b)).toEqual(-1) - expect(b.compareForSort(a)).toEqual(1) - expect(a.compareForSort(a)).toEqual(0) - }) - } - } -}) diff --git a/sdk/lib/test/exverList.test.ts b/sdk/lib/test/exverList.test.ts new file mode 100644 index 000000000..e29a9f0d1 --- /dev/null +++ b/sdk/lib/test/exverList.test.ts @@ -0,0 +1,355 @@ +import { VersionRange, ExtendedVersion } from "../exver" +describe("ExVer", () => { + { + { + const checker = VersionRange.parse("*") + test("VersionRange.parse('*')", () => { + checker.satisfiedBy(ExtendedVersion.parse("1:0")) + checker.satisfiedBy(ExtendedVersion.parse("1.2:0")) + checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0")) + checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4")) + checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.5")) + checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.5.6")) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))).toEqual( + true, + ) + }) + test("VersionRange.parse('*') invalid", () => { + expect(() => checker.satisfiedBy(ExtendedVersion.parse("a"))).toThrow() + expect(() => checker.satisfiedBy(ExtendedVersion.parse(""))).toThrow() + expect(() => + checker.satisfiedBy(ExtendedVersion.parse("1..3")), + ).toThrow() + }) + } + + { + const checker = VersionRange.parse(">1.2.3:4") + test(`VersionRange.parse(">1.2.3:4") valid`, () => { + expect( + checker.satisfiedBy(ExtendedVersion.parse("2-beta.123:0")), + ).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:5"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.1"))).toEqual( + true, + ) + }) + + test(`VersionRange.parse(">1.2.3:4") invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false) + }) + } + { + const checker = VersionRange.parse("=1.2.3") + test(`VersionRange.parse("=1.2.3") valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual( + true, + ) + }) + + test(`VersionRange.parse("=1.2.3") invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:1"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual( + false, + ) + }) + } + { + const checker = VersionRange.parse(">=1.2.3:4") + test(`VersionRange.parse(">=1.2.3:4") valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:5"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.1"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))).toEqual( + true, + ) + }) + + test(`VersionRange.parse(">=1.2.3:4") invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false) + }) + } + { + const checker = VersionRange.parse("<1.2.3:4") + test(`VersionRange.parse("<1.2.3:4") invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:5"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.1"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))).toEqual( + false, + ) + }) + + test(`VersionRange.parse("<1.2.3:4") valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true) + }) + } + { + const checker = VersionRange.parse("<=1.2.3:4") + test(`VersionRange.parse("<=1.2.3:4") invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:5"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.1"))).toEqual( + false, + ) + }) + + test(`VersionRange.parse("<=1.2.3:4") valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))).toEqual( + true, + ) + }) + } + + { + const checkA = VersionRange.parse(">1") + const checkB = VersionRange.parse("<=2") + + const checker = checkA.and(checkB) + test(`simple and(checkers) valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1:0"))).toEqual( + true, + ) + }) + test(`simple and(checkers) invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2.1:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false) + }) + } + { + const checkA = VersionRange.parse("<1") + const checkB = VersionRange.parse("=2") + + const checker = checkA.or(checkB) + test(`simple or(checkers) valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("0.1:0"))).toEqual( + true, + ) + }) + test(`simple or(checkers) invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2.1:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1:0"))).toEqual( + false, + ) + }) + } + + { + const checker = VersionRange.parse("~1.2") + test(`VersionRange.parse(~1.2) valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.1:0"))).toEqual( + true, + ) + }) + test(`VersionRange.parse(~1.2) invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.3:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.3.1:0"))).toEqual( + false, + ) + + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1.1:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false) + + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(false) + }) + } + + { + const checker = VersionRange.parse("~1.2").not() + test(`VersionRange.parse(~1.2).not() valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.3:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.3.1:0"))).toEqual( + true, + ) + + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1.1:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true) + + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + }) + test(`VersionRange.parse(~1.2).not() invalid `, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.1:0"))).toEqual( + false, + ) + }) + } + { + const checker = VersionRange.parse("!~1.2") + test(`!(VersionRange.parse(~1.2)) valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.3:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.3.1:0"))).toEqual( + true, + ) + + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1.1:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true) + + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + }) + test(`!(VersionRange.parse(~1.2)) invalid `, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.1:0"))).toEqual( + false, + ) + }) + } + { + const checker = VersionRange.parse("!>1.2.3:4") + test(`VersionRange.parse("!>1.2.3:4") invalid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:5"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4.1"))).toEqual( + false, + ) + }) + + test(`VersionRange.parse("!>1.2.3:4") valid`, () => { + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:4"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.3:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true) + }) + } + + { + test(">1 && =1.2", () => { + const checker = VersionRange.parse(">1 && =1.2") + + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2.1:0"))).toEqual( + false, + ) + }) + test("=1 || =2", () => { + const checker = VersionRange.parse("=1 || =2") + + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("3:0"))).toEqual(false) + }) + + test(">1 && =1.2 || =2", () => { + const checker = VersionRange.parse(">1 && =1.2 || =2") + + expect(checker.satisfiedBy(ExtendedVersion.parse("1.2:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + expect(checker.satisfiedBy(ExtendedVersion.parse("3:0"))).toEqual(false) + }) + + test("&& before || order of operationns: <1.5 && >1 || >1.5 && <3", () => { + const checker = VersionRange.parse("<1.5 && >1 || >1.5 && <3") + expect(checker.satisfiedBy(ExtendedVersion.parse("1.1:0"))).toEqual( + true, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("2:0"))).toEqual(true) + + expect(checker.satisfiedBy(ExtendedVersion.parse("1.5:0"))).toEqual( + false, + ) + expect(checker.satisfiedBy(ExtendedVersion.parse("1:0"))).toEqual(false) + expect(checker.satisfiedBy(ExtendedVersion.parse("3:0"))).toEqual(false) + }) + + test("Compare function on the emver", () => { + const a = ExtendedVersion.parse("1.2.3:0") + const b = ExtendedVersion.parse("1.2.4:0") + + expect(a.compare(b)).toEqual("less") + expect(b.compare(a)).toEqual("greater") + expect(a.compare(a)).toEqual("equal") + }) + test("Compare for sort function on the emver", () => { + const a = ExtendedVersion.parse("1.2.3:0") + const b = ExtendedVersion.parse("1.2.4:0") + + expect(a.compareForSort(b)).toEqual(-1) + expect(b.compareForSort(a)).toEqual(1) + expect(a.compareForSort(a)).toEqual(0) + }) + } + } +}) diff --git a/sdk/lib/test/output.sdk.ts b/sdk/lib/test/output.sdk.ts index 189491be5..c56e05e60 100644 --- a/sdk/lib/test/output.sdk.ts +++ b/sdk/lib/test/output.sdk.ts @@ -7,7 +7,7 @@ export const sdk = StartSdk.of() setupManifest({ id: "testOutput", title: "", - version: "1.0", + version: "1.0:0", releaseNotes: "", license: "", replaces: [], @@ -33,9 +33,10 @@ export const sdk = StartSdk.of() stop: null, }, dependencies: { - remoteTest: { + "remote-test": { description: "", optional: false, + s9pk: "https://example.com/remote-test.s9pk", }, }, }), diff --git a/sdk/lib/test/setupDependencyConfig.test.ts b/sdk/lib/test/setupDependencyConfig.test.ts index 5fa4a0ddf..622559eb6 100644 --- a/sdk/lib/test/setupDependencyConfig.test.ts +++ b/sdk/lib/test/setupDependencyConfig.test.ts @@ -21,7 +21,7 @@ describe("setupDependencyConfig", () => { dependencyConfig: async ({}) => {}, }) sdk.setupDependencyConfig(testConfig, { - remoteTest, + "remote-test": remoteTest, }) }) }) diff --git a/sdk/lib/test/utils.splitCommand.test.ts b/sdk/lib/test/utils.splitCommand.test.ts deleted file mode 100644 index aafddb177..000000000 --- a/sdk/lib/test/utils.splitCommand.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { getHostname } from "../util/getServiceInterface" -import { splitCommand } from "../util/splitCommand" - -describe("splitCommand ", () => { - const inputToExpected = [ - ["cat", ["cat"]], - [["cat"], ["cat"]], - [ - ["cat", "hello all my homies"], - ["cat", "hello all my homies"], - ], - ["cat hello world", ["cat", "hello", "world"]], - ["cat hello 'big world'", ["cat", "hello", "big world"]], - [`cat hello "big world"`, ["cat", "hello", "big world"]], - [ - `cat hello "big world's are the greatest"`, - ["cat", "hello", "big world's are the greatest"], - ], - // Too many spaces - ["cat ", ["cat"]], - [["cat "], ["cat "]], - [ - ["cat ", "hello all my homies "], - ["cat ", "hello all my homies "], - ], - ["cat hello world ", ["cat", "hello", "world"]], - [ - " cat hello 'big world' ", - ["cat", "hello", "big world"], - ], - [ - ` cat hello "big world" `, - ["cat", "hello", "big world"], - ], - ] - - for (const [input, expectValue] of inputToExpected) { - test(`should return ${expectValue} for ${input}`, () => { - expect(splitCommand(input as any)).toEqual(expectValue) - }) - } -}) diff --git a/sdk/lib/types.ts b/sdk/lib/types.ts index 686cb4750..e9c766448 100644 --- a/sdk/lib/types.ts +++ b/sdk/lib/types.ts @@ -12,6 +12,7 @@ import { LanInfo, BindParams, Manifest, + CheckDependenciesResult, } from "./osBindings" import { MainEffects, ServiceInterfaceType, Signals } from "./StartSdk" @@ -154,14 +155,6 @@ export type DependencyConfig = { }): Promise } -export type ValidIfNoStupidEscape = A extends - | `${string}'"'"'${string}` - | `${string}\\"${string}` - ? never - : "" extends A & "" - ? never - : A - export type ConfigRes = { /** This should be the previous config, that way during set config we start with the previous */ config?: null | Record @@ -188,9 +181,7 @@ export type SmtpValue = { password: string | null | undefined } -export type CommandType = - | ValidIfNoStupidEscape - | [string, ...string[]] +export type CommandType = string | [string, ...string[]] export type DaemonReturned = { wait(): Promise @@ -470,7 +461,7 @@ export type Effects = { */ checkDependencies(options: { packageIds: PackageId[] | null - }): Promise + }): Promise /** Exists could be useful during the runtime to know if some service exists, option dep */ exists(options: { packageId: PackageId }): Promise /** Exists could be useful during the runtime to know if some service is running, option dep */ @@ -554,12 +545,3 @@ export type Dependencies = Array export type DeepPartial = T extends {} ? { [P in keyof T]?: DeepPartial } : T - -export type CheckDependencyResult = { - packageId: PackageId - isInstalled: boolean - isRunning: boolean - healthChecks: SetHealth[] - version: string | null -} -export type CheckResults = CheckDependencyResult[] diff --git a/sdk/lib/util/splitCommand.ts b/sdk/lib/util/splitCommand.ts index 69f00a5a7..ac1237574 100644 --- a/sdk/lib/util/splitCommand.ts +++ b/sdk/lib/util/splitCommand.ts @@ -1,17 +1,8 @@ import { arrayOf, string } from "ts-matches" -import { ValidIfNoStupidEscape } from "../types" export const splitCommand = ( command: string | [string, ...string[]], ): string[] => { if (arrayOf(string).test(command)) return command - return String(command) - .split('"') - .flatMap((x, i) => - i % 2 !== 0 - ? [x] - : x.split("'").flatMap((x, i) => (i % 2 !== 0 ? [x] : x.split(" "))), - ) - .map((x) => x.trim()) - .filter(Boolean) + return ["sh", "-c", command] } diff --git a/sdk/package-lock.json b/sdk/package-lock.json index 0a3655a7d..06d456019 100644 --- a/sdk/package-lock.json +++ b/sdk/package-lock.json @@ -22,9 +22,11 @@ "@types/jest": "^29.4.0", "@types/lodash.merge": "^4.6.2", "jest": "^29.4.3", + "peggy": "^3.0.2", "prettier": "^3.2.5", "ts-jest": "^29.0.5", "ts-node": "^10.9.1", + "ts-pegjs": "^4.2.1", "tsx": "^4.7.1", "typescript": "^5.0.4" } @@ -1030,6 +1032,41 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@sinclair/typebox": { "version": "0.25.24", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", @@ -1054,6 +1091,42 @@ "@sinonjs/commons": "^2.0.0" } }, + "node_modules/@ts-morph/common": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.19.0.tgz", + "integrity": "sha512-Unz/WHmd4pGax91rdIKWi51wnVUW11QttMEPpBiBgIewnc9UQIX7UDLxr5vRlqeByXCwhkF6VabSsI0raWcyAQ==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.12", + "minimatch": "^7.4.3", + "mkdirp": "^2.1.6", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@ts-morph/common/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@ts-morph/common/node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -1605,6 +1678,12 @@ "node": ">= 0.12.0" } }, + "node_modules/code-block-writer": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz", + "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==", + "dev": true + }, "node_modules/collect-v8-coverage": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", @@ -1629,6 +1708,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1826,12 +1914,37 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -1963,6 +2076,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -2076,6 +2201,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-generator-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", @@ -2085,6 +2219,18 @@ "node": ">=6" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -2938,6 +3084,15 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -2986,6 +3141,21 @@ "node": "*" } }, + "node_modules/mkdirp": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", + "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", + "dev": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -3124,6 +3294,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3157,6 +3333,22 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/peggy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/peggy/-/peggy-3.0.2.tgz", + "integrity": "sha512-n7chtCbEoGYRwZZ0i/O3t1cPr6o+d9Xx4Zwy2LYfzv0vjchMBU0tO+qYYyvZloBPcgRgzYvALzGWHe609JjEpg==", + "dev": true, + "dependencies": { + "commander": "^10.0.0", + "source-map-generator": "0.8.0" + }, + "bin": { + "peggy": "bin/peggy.js" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -3266,6 +3458,26 @@ } ] }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -3337,6 +3549,39 @@ "node": ">=10" } }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -3397,6 +3642,15 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-generator": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/source-map-generator/-/source-map-generator-0.8.0.tgz", + "integrity": "sha512-psgxdGMwl5MZM9S3FWee4EgsEaIjahYV5AzGnwUvPhWeITz/j6rKpysQHlQ4USdxvINlb8lKfWGIXwfkrgtqkA==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/source-map-support": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", @@ -3631,6 +3885,16 @@ "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-5.5.1.tgz", "integrity": "sha512-UFYaKgfqlg9FROK7bdpYqFwG1CJvP4kOJdjXuWoqxo9jCmANoDw1GxkSCpJgoTeIiSTaTH5Qr1klSspb8c+ydg==" }, + "node_modules/ts-morph": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-18.0.0.tgz", + "integrity": "sha512-Kg5u0mk19PIIe4islUI/HWRvm9bC1lHejK4S0oh1zaZ77TMZAEmQC0sHQYiu2RgCQFZKXz1fMVi/7nOOeirznA==", + "dev": true, + "dependencies": { + "@ts-morph/common": "~0.19.0", + "code-block-writer": "^12.0.0" + } + }, "node_modules/ts-node": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", @@ -3674,6 +3938,37 @@ } } }, + "node_modules/ts-pegjs": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ts-pegjs/-/ts-pegjs-4.2.1.tgz", + "integrity": "sha512-mK/O2pu6lzWUeKpEMA/wsa0GdYblfjJI1y0s0GqH6xCTvugQDOWPJbm5rY6AHivpZICuXIriCb+a7Cflbdtc2w==", + "dev": true, + "dependencies": { + "prettier": "^2.8.8", + "ts-morph": "^18.0.0" + }, + "bin": { + "tspegjs": "dist/cli.mjs" + }, + "peerDependencies": { + "peggy": "^3.0.2" + } + }, + "node_modules/ts-pegjs/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/tsx": { "version": "4.7.1", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.7.1.tgz", diff --git a/sdk/package.json b/sdk/package.json index 8ed984a82..e03153722 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -49,9 +49,11 @@ "@types/jest": "^29.4.0", "@types/lodash.merge": "^4.6.2", "jest": "^29.4.3", + "peggy": "^3.0.2", "prettier": "^3.2.5", "ts-jest": "^29.0.5", "ts-node": "^10.9.1", + "ts-pegjs": "^4.2.1", "tsx": "^4.7.1", "typescript": "^5.0.4" } diff --git a/web/angular.json b/web/angular.json index cf6c113a4..e3153139b 100644 --- a/web/angular.json +++ b/web/angular.json @@ -74,7 +74,8 @@ "with": "projects/ui/src/environments/environment.prod.ts" } ], - "outputHashing": "all" + "outputHashing": "all", + "extractLicenses": false }, "development": { "buildOptimizer": false, diff --git a/web/package-lock.json b/web/package-lock.json index cf32bbc01..ac1f77a6b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "startos-ui", "version": "0.3.6", + "license": "MIT", "dependencies": { "@angular/animations": "^14.1.0", "@angular/common": "^14.1.0", @@ -114,6 +115,7 @@ } }, "../sdk/dist": { + "name": "@start9labs/start-sdk", "version": "0.3.6-alpha5", "license": "MIT", "dependencies": { @@ -130,9 +132,11 @@ "@types/jest": "^29.4.0", "@types/lodash.merge": "^4.6.2", "jest": "^29.4.3", + "peggy": "^3.0.2", "prettier": "^3.2.5", "ts-jest": "^29.0.5", "ts-node": "^10.9.1", + "ts-pegjs": "^4.2.1", "tsx": "^4.7.1", "typescript": "^5.0.4" } diff --git a/web/package.json b/web/package.json index a75701333..b304d4f94 100644 --- a/web/package.json +++ b/web/package.json @@ -3,6 +3,7 @@ "version": "0.3.6", "author": "Start9 Labs, Inc", "homepage": "https://start9.com/", + "license": "MIT", "scripts": { "ng": "ng", "check": "npm run check:shared && npm run check:marketplace && npm run check:ui && npm run check:install-wiz && npm run check:setup && npm run check:dui", diff --git a/web/patchdb-ui-seed.json b/web/patchdb-ui-seed.json index 7bea3ea14..6236275cd 100644 --- a/web/patchdb-ui-seed.json +++ b/web/patchdb-ui-seed.json @@ -1,20 +1,24 @@ { "name": null, - "ack-welcome": "0.3.6", + "ackWelcome": "0.3.6", "marketplace": { - "selected-url": "https://registry.start9.com/", - "known-hosts": { - "https://registry.start9.com/": {}, - "https://community-registry.start9.com/": {} + "selectedUrl": "https://registry.start9.com/", + "knownHosts": { + "https://registry.start9.com/": { + "name": "Start9 Registry" + }, + "https://community-registry.start9.com/": { + "name": "Community Registry" + } } }, "dev": {}, "gaming": { "snake": { - "high-score": 0 + "highScore": 0 } }, - "ack-instructions": {}, + "ackInstructions": {}, "theme": "Dark", "widgets": [] } diff --git a/web/projects/marketplace/src/pages/release-notes/release-notes.component.html b/web/projects/marketplace/src/modals/release-notes/release-notes.component.html similarity index 63% rename from web/projects/marketplace/src/pages/release-notes/release-notes.component.html rename to web/projects/marketplace/src/modals/release-notes/release-notes.component.html index 74e34c88f..74661827d 100644 --- a/web/projects/marketplace/src/pages/release-notes/release-notes.component.html +++ b/web/projects/marketplace/src/modals/release-notes/release-notes.component.html @@ -1,6 +1,17 @@ - + + + Past Release Notes + + + + + + + + + -
+
-

{{ note.key | displayEmver }}

+

{{ note.key }}

{ + return Object.entries(this.pkg.otherVersions) + .filter( + ([v, _]) => + this.exver.getFlavor(v) === this.pkg.flavor && + this.exver.compareExver(this.pkg.version, v) === 1, + ) + .reduce( + (obj, [version, info]) => ({ + ...obj, + [version]: info.releaseNotes, + }), + { + [`${this.pkg.version} (current)`]: this.pkg.releaseNotes, + }, + ) + }), + ) + + constructor( + private readonly marketplaceService: AbstractMarketplaceService, + private readonly exver: Exver, + private readonly modalCtrl: ModalController, + ) {} + + async dismiss() { + return this.modalCtrl.dismiss() + } + + isSelected(key: string): boolean { + return this.selected === key + } + + setSelected(selected: string) { + this.selected = this.isSelected(selected) ? null : selected + } + + getDocSize(key: string, { nativeElement }: ElementRef) { + return this.isSelected(key) ? nativeElement.scrollHeight : 0 + } + + asIsOrder(a: any, b: any) { + return 0 + } +} diff --git a/web/projects/marketplace/src/pages/release-notes/release-notes.module.ts b/web/projects/marketplace/src/modals/release-notes/release-notes.module.ts similarity index 86% rename from web/projects/marketplace/src/pages/release-notes/release-notes.module.ts rename to web/projects/marketplace/src/modals/release-notes/release-notes.module.ts index 583631dc4..41055a314 100644 --- a/web/projects/marketplace/src/pages/release-notes/release-notes.module.ts +++ b/web/projects/marketplace/src/modals/release-notes/release-notes.module.ts @@ -2,12 +2,11 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { - EmverPipesModule, + ExverPipesModule, MarkdownPipeModule, TextSpinnerComponentModule, } from '@start9labs/shared' import { TuiElementModule } from '@taiga-ui/cdk' - import { ReleaseNotesComponent } from './release-notes.component' @NgModule({ @@ -15,11 +14,11 @@ import { ReleaseNotesComponent } from './release-notes.component' CommonModule, IonicModule, TextSpinnerComponentModule, - EmverPipesModule, + ExverPipesModule, MarkdownPipeModule, TuiElementModule, ], declarations: [ReleaseNotesComponent], exports: [ReleaseNotesComponent], }) -export class ReleaseNotesModule {} +export class ReleaseNotesComponentModule {} diff --git a/web/projects/marketplace/src/pages/list/categories/categories.component.html b/web/projects/marketplace/src/pages/list/categories/categories.component.html index 4e99a21c2..29613813b 100644 --- a/web/projects/marketplace/src/pages/list/categories/categories.component.html +++ b/web/projects/marketplace/src/pages/list/categories/categories.component.html @@ -1,9 +1,9 @@ - {{ cat }} + {{ cat.value.name }} diff --git a/web/projects/marketplace/src/pages/list/categories/categories.component.ts b/web/projects/marketplace/src/pages/list/categories/categories.component.ts index b34761079..349a3bd18 100644 --- a/web/projects/marketplace/src/pages/list/categories/categories.component.ts +++ b/web/projects/marketplace/src/pages/list/categories/categories.component.ts @@ -5,6 +5,7 @@ import { Input, Output, } from '@angular/core' +import { T } from '@start9labs/start-sdk' @Component({ selector: 'marketplace-categories', @@ -17,7 +18,7 @@ import { }) export class CategoriesComponent { @Input() - categories: readonly string[] = [] + categories!: Map @Input() category = '' @@ -29,4 +30,8 @@ export class CategoriesComponent { this.category = category this.categoryChange.emit(category) } + + originalOrder() { + return 0 + } } diff --git a/web/projects/marketplace/src/pages/list/item/item.component.html b/web/projects/marketplace/src/pages/list/item/item.component.html index 9a88d0869..0a22b8699 100644 --- a/web/projects/marketplace/src/pages/list/item/item.component.html +++ b/web/projects/marketplace/src/pages/list/item/item.component.html @@ -1,12 +1,16 @@ - +

- {{ pkg.manifest.title }} + {{ pkg.title }}

-

{{ pkg.manifest.description.short }}

+

{{ pkg.description.short }}

diff --git a/web/projects/marketplace/src/pages/release-notes/release-notes.component.ts b/web/projects/marketplace/src/pages/release-notes/release-notes.component.ts deleted file mode 100644 index 49da475d9..000000000 --- a/web/projects/marketplace/src/pages/release-notes/release-notes.component.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { ChangeDetectionStrategy, Component, ElementRef } from '@angular/core' -import { ActivatedRoute } from '@angular/router' -import { getPkgId } from '@start9labs/shared' -import { AbstractMarketplaceService } from '../../services/marketplace.service' - -@Component({ - selector: 'release-notes', - templateUrl: './release-notes.component.html', - styleUrls: ['./release-notes.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ReleaseNotesComponent { - private readonly pkgId = getPkgId(this.route) - - private selected: string | null = null - - readonly notes$ = this.marketplaceService.fetchReleaseNotes$(this.pkgId) - - constructor( - private readonly route: ActivatedRoute, - private readonly marketplaceService: AbstractMarketplaceService, - ) {} - - isSelected(key: string): boolean { - return this.selected === key - } - - setSelected(selected: string) { - this.selected = this.isSelected(selected) ? null : selected - } - - getDocSize(key: string, { nativeElement }: ElementRef) { - return this.isSelected(key) ? nativeElement.scrollHeight : 0 - } - - asIsOrder(a: any, b: any) { - return 0 - } -} diff --git a/web/projects/marketplace/src/pages/show/about/about.component.html b/web/projects/marketplace/src/pages/show/about/about.component.html index 1126f8b54..56d363071 100644 --- a/web/projects/marketplace/src/pages/show/about/about.component.html +++ b/web/projects/marketplace/src/pages/show/about/about.component.html @@ -1,13 +1,11 @@ - - New in {{ pkg.manifest.version | displayEmver }} - +New in {{ pkg.version }} -
+
- + Past Release Notes @@ -15,10 +13,10 @@ Description -

{{ pkg.manifest.description.long }}

+

{{ pkg.description.long }}

-
+
View website diff --git a/web/projects/marketplace/src/pages/show/about/about.component.ts b/web/projects/marketplace/src/pages/show/about/about.component.ts index 6626d4fbe..e9d69ebb3 100644 --- a/web/projects/marketplace/src/pages/show/about/about.component.ts +++ b/web/projects/marketplace/src/pages/show/about/about.component.ts @@ -1,5 +1,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { MarketplacePkg } from '../../../types' +import { ModalController } from '@ionic/angular' +import { ReleaseNotesComponent } from '../../../modals/release-notes/release-notes.component' @Component({ selector: 'marketplace-about', @@ -10,4 +12,15 @@ import { MarketplacePkg } from '../../../types' export class AboutComponent { @Input() pkg!: MarketplacePkg + + constructor(private readonly modalCtrl: ModalController) {} + + async presentModalNotes() { + const modal = await this.modalCtrl.create({ + componentProps: { pkg: this.pkg }, + component: ReleaseNotesComponent, + }) + + await modal.present() + } } diff --git a/web/projects/marketplace/src/pages/show/about/about.module.ts b/web/projects/marketplace/src/pages/show/about/about.module.ts index b48bbcbaa..a110cd300 100644 --- a/web/projects/marketplace/src/pages/show/about/about.module.ts +++ b/web/projects/marketplace/src/pages/show/about/about.module.ts @@ -2,9 +2,9 @@ import { CommonModule } from '@angular/common' import { NgModule } from '@angular/core' import { RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' -import { EmverPipesModule, MarkdownPipeModule } from '@start9labs/shared' - +import { ExverPipesModule, MarkdownPipeModule } from '@start9labs/shared' import { AboutComponent } from './about.component' +import { ReleaseNotesComponentModule } from '../../../modals/release-notes/release-notes.module' @NgModule({ imports: [ @@ -12,7 +12,8 @@ import { AboutComponent } from './about.component' RouterModule, IonicModule, MarkdownPipeModule, - EmverPipesModule, + ExverPipesModule, + ReleaseNotesComponentModule, ], declarations: [AboutComponent], exports: [AboutComponent], diff --git a/web/projects/marketplace/src/pages/show/additional/additional.component.html b/web/projects/marketplace/src/pages/show/additional/additional.component.html index a75daa3db..03a52cc94 100644 --- a/web/projects/marketplace/src/pages/show/additional/additional.component.html +++ b/web/projects/marketplace/src/pages/show/additional/additional.component.html @@ -1,10 +1,10 @@ Additional Info - +

License

-

{{ manifest.license }}

+

{{ pkg.license }}

@@ -53,39 +53,39 @@

Source Repository

-

{{ manifest.upstreamRepo }}

+

{{ pkg.upstreamRepo }}

Wrapper Repository

-

{{ manifest.wrapperRepo }}

+

{{ pkg.wrapperRepo }}

Support Site

-

{{ manifest.supportSite || 'Not provided' }}

+

{{ pkg.supportSite || 'Not provided' }}

diff --git a/web/projects/marketplace/src/pages/show/additional/additional.component.ts b/web/projects/marketplace/src/pages/show/additional/additional.component.ts index 778ea6c54..92ad42bff 100644 --- a/web/projects/marketplace/src/pages/show/additional/additional.component.ts +++ b/web/projects/marketplace/src/pages/show/additional/additional.component.ts @@ -10,15 +10,9 @@ import { ModalController, ToastController, } from '@ionic/angular' -import { - copyToClipboard, - displayEmver, - Emver, - MarkdownComponent, -} from '@start9labs/shared' +import { copyToClipboard, Exver, MarkdownComponent } from '@start9labs/shared' import { MarketplacePkg } from '../../../types' import { AbstractMarketplaceService } from '../../../services/marketplace.service' -import { ActivatedRoute } from '@angular/router' @Component({ selector: 'marketplace-additional', @@ -32,15 +26,12 @@ export class AdditionalComponent { @Output() version = new EventEmitter() - readonly url = this.route.snapshot.queryParamMap.get('url') || undefined - constructor( private readonly alertCtrl: AlertController, private readonly modalCtrl: ModalController, - private readonly emver: Emver, + private readonly exver: Exver, private readonly marketplaceService: AbstractMarketplaceService, private readonly toastCtrl: ToastController, - private readonly route: ActivatedRoute, ) {} async copy(address: string): Promise { @@ -58,41 +49,53 @@ export class AdditionalComponent { } async presentAlertVersions() { - const alert = await this.alertCtrl.create({ - header: 'Versions', - inputs: this.pkg.versions - .sort((a, b) => -1 * (this.emver.compare(a, b) || 0)) - .map(v => ({ - name: v, // for CSS - type: 'radio', - label: displayEmver(v), // appearance on screen - value: v, // literal SEM version value - checked: this.pkg.manifest.version === v, - })), - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Ok', - handler: (version: string) => this.version.emit(version), - }, - ], - }) + const versions = Object.keys(this.pkg.otherVersions).filter( + v => this.exver.getFlavor(v) === this.pkg.flavor, + ) - await alert.present() + if (!versions.length) { + const alert = await this.alertCtrl.create({ + header: 'Versions', + message: 'No other versions', + }) + + await alert.present() + } else { + const alert = await this.alertCtrl.create({ + header: 'Versions', + inputs: versions + .sort((a, b) => -1 * (this.exver.compareExver(a, b) || 0)) + .map(v => ({ + name: v, // for CSS + type: 'radio', + label: v, // appearance on screen + value: v, // literal SEM version value + checked: this.pkg.version === v, + })), + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'Ok', + handler: (version: string) => this.version.emit(version), + }, + ], + }) + + await alert.present() + } } - async presentModalMd(title: string) { + async presentModalMd(asset: 'license' | 'instructions') { const content = this.marketplaceService.fetchStatic$( - this.pkg.manifest.id, - title, - this.url, + this.pkg, + asset === 'license' ? 'LICENSE.md' : 'instructions.md', ) const modal = await this.modalCtrl.create({ - componentProps: { title, content }, + componentProps: { title: asset, content }, component: MarkdownComponent, }) diff --git a/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.html b/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.html index 4c1bfcc65..b5d3fb4db 100644 --- a/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.html +++ b/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.html @@ -2,7 +2,7 @@

- {{ pkg.dependencyMetadata[dep.key].title }} + {{ + pkg.dependencyMetadata[dep.key].title + ? pkg.dependencyMetadata[dep.key].title + : dep.key + }} (optional) (Required) diff --git a/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.ts b/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.ts index a1fed6cca..7b7f2efe7 100644 --- a/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.ts +++ b/web/projects/marketplace/src/pages/show/dependencies/dependencies.component.ts @@ -11,6 +11,7 @@ export class DependenciesComponent { pkg!: MarketplacePkg getImg(key: string): string { - return this.pkg.dependencyMetadata[key].icon + const icon = this.pkg.dependencyMetadata[key]?.icon + return icon ? icon : 'assets/img/service-icons/fallback.png' } } diff --git a/web/projects/marketplace/src/pages/show/dependencies/dependencies.module.ts b/web/projects/marketplace/src/pages/show/dependencies/dependencies.module.ts index abb3032e9..55b220d08 100644 --- a/web/projects/marketplace/src/pages/show/dependencies/dependencies.module.ts +++ b/web/projects/marketplace/src/pages/show/dependencies/dependencies.module.ts @@ -2,11 +2,7 @@ import { CommonModule } from '@angular/common' import { NgModule } from '@angular/core' import { RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' -import { - EmverPipesModule, - ResponsiveColModule, - SharedPipesModule, -} from '@start9labs/shared' +import { ResponsiveColModule, SharedPipesModule } from '@start9labs/shared' import { DependenciesComponent } from './dependencies.component' @@ -16,7 +12,6 @@ import { DependenciesComponent } from './dependencies.component' RouterModule, IonicModule, SharedPipesModule, - EmverPipesModule, ResponsiveColModule, ], declarations: [DependenciesComponent], diff --git a/web/projects/marketplace/src/pages/show/flavors/flavors.component.html b/web/projects/marketplace/src/pages/show/flavors/flavors.component.html new file mode 100644 index 000000000..7a0f64aff --- /dev/null +++ b/web/projects/marketplace/src/pages/show/flavors/flavors.component.html @@ -0,0 +1,21 @@ +Alternative Implementations + + + + + + + + +

+ {{ pkg.title }} +

+

{{ pkg.version }}

+
+
+
+
+
diff --git a/web/projects/marketplace/src/pages/show/flavors/flavors.component.ts b/web/projects/marketplace/src/pages/show/flavors/flavors.component.ts new file mode 100644 index 000000000..4927f47a1 --- /dev/null +++ b/web/projects/marketplace/src/pages/show/flavors/flavors.component.ts @@ -0,0 +1,12 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { MarketplacePkg } from '../../../types' + +@Component({ + selector: 'marketplace-flavors', + templateUrl: 'flavors.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FlavorsComponent { + @Input() + pkgs!: MarketplacePkg[] +} diff --git a/web/projects/marketplace/src/pages/show/flavors/flavors.module.ts b/web/projects/marketplace/src/pages/show/flavors/flavors.module.ts new file mode 100644 index 000000000..662a914fd --- /dev/null +++ b/web/projects/marketplace/src/pages/show/flavors/flavors.module.ts @@ -0,0 +1,19 @@ +import { CommonModule } from '@angular/common' +import { NgModule } from '@angular/core' +import { RouterModule } from '@angular/router' +import { IonicModule } from '@ionic/angular' +import { ResponsiveColModule, SharedPipesModule } from '@start9labs/shared' +import { FlavorsComponent } from './flavors.component' + +@NgModule({ + imports: [ + CommonModule, + RouterModule, + IonicModule, + SharedPipesModule, + ResponsiveColModule, + ], + declarations: [FlavorsComponent], + exports: [FlavorsComponent], +}) +export class FlavorsModule {} diff --git a/web/projects/marketplace/src/pages/show/package/package.component.html b/web/projects/marketplace/src/pages/show/package/package.component.html index 94e7006b1..aa5fcf0d0 100644 --- a/web/projects/marketplace/src/pages/show/package/package.component.html +++ b/web/projects/marketplace/src/pages/show/package/package.component.html @@ -1,9 +1,11 @@
-

{{ pkg.manifest.title }}

-

{{ pkg.manifest.version | displayEmver }}

-

Released: {{ pkg.publishedAt | date: 'medium' }}

+

{{ pkg.title }}

+

{{ pkg.version }}

+

+ Released: {{ pkg.s9pk.publishedAt | date : 'medium' }} +

diff --git a/web/projects/marketplace/src/pages/show/package/package.module.ts b/web/projects/marketplace/src/pages/show/package/package.module.ts index 502ee82bb..8565a2352 100644 --- a/web/projects/marketplace/src/pages/show/package/package.module.ts +++ b/web/projects/marketplace/src/pages/show/package/package.module.ts @@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common' import { NgModule } from '@angular/core' import { IonicModule } from '@ionic/angular' import { - EmverPipesModule, + ExverPipesModule, SharedPipesModule, TickerModule, } from '@start9labs/shared' @@ -16,7 +16,7 @@ import { PackageComponent } from './package.component' CommonModule, IonicModule, SharedPipesModule, - EmverPipesModule, + ExverPipesModule, TickerModule, ], }) diff --git a/web/projects/marketplace/src/pipes/filter-packages.pipe.ts b/web/projects/marketplace/src/pipes/filter-packages.pipe.ts index 48313376f..ed546ada9 100644 --- a/web/projects/marketplace/src/pipes/filter-packages.pipe.ts +++ b/web/projects/marketplace/src/pipes/filter-packages.pipe.ts @@ -26,11 +26,11 @@ export class FilterPackagesPipe implements PipeTransform { distance: 16, keys: [ { - name: 'manifest.title', + name: 'title', weight: 1, }, { - name: 'manifest.id', + name: 'id', weight: 0.5, }, ], @@ -42,19 +42,19 @@ export class FilterPackagesPipe implements PipeTransform { useExtendedSearch: true, keys: [ { - name: 'manifest.title', + name: 'title', weight: 1, }, { - name: 'manifest.id', + name: 'id', weight: 0.5, }, { - name: 'manifest.description.short', + name: 'description.short', weight: 0.4, }, { - name: 'manifest.description.long', + name: 'description.long', weight: 0.1, }, ], @@ -71,7 +71,8 @@ export class FilterPackagesPipe implements PipeTransform { .filter(p => category === 'all' || p.categories.includes(category)) .sort((a, b) => { return ( - new Date(b.publishedAt).valueOf() - new Date(a.publishedAt).valueOf() + new Date(b.s9pk.publishedAt).valueOf() - + new Date(a.s9pk.publishedAt).valueOf() ) }) } diff --git a/web/projects/marketplace/src/public-api.ts b/web/projects/marketplace/src/public-api.ts index b9e8c4686..600ceabfe 100644 --- a/web/projects/marketplace/src/public-api.ts +++ b/web/projects/marketplace/src/public-api.ts @@ -10,8 +10,6 @@ export * from './pages/list/search/search.component' export * from './pages/list/search/search.module' export * from './pages/list/skeleton/skeleton.component' export * from './pages/list/skeleton/skeleton.module' -export * from './pages/release-notes/release-notes.component' -export * from './pages/release-notes/release-notes.module' export * from './pages/show/about/about.component' export * from './pages/show/about/about.module' export * from './pages/show/additional/additional.component' @@ -20,6 +18,8 @@ export * from './pages/show/dependencies/dependencies.component' export * from './pages/show/dependencies/dependencies.module' export * from './pages/show/package/package.component' export * from './pages/show/package/package.module' +export * from './pages/show/flavors/flavors.component' +export * from './pages/show/flavors/flavors.module' export * from './pipes/filter-packages.pipe' diff --git a/web/projects/marketplace/src/services/marketplace.service.ts b/web/projects/marketplace/src/services/marketplace.service.ts index af1b473d5..3fdabc426 100644 --- a/web/projects/marketplace/src/services/marketplace.service.ts +++ b/web/projects/marketplace/src/services/marketplace.service.ts @@ -12,18 +12,13 @@ export abstract class AbstractMarketplaceService { abstract getPackage$( id: string, - version: string, + version: string | null, + flavor: string | null, url?: string, - ): Observable // could be {} so need to check in show page - - abstract fetchReleaseNotes$( - id: string, - url?: string, - ): Observable> + ): Observable abstract fetchStatic$( - id: string, - type: string, - url?: string, + pkg: MarketplacePkg, + type: 'LICENSE.md' | 'instructions.md', ): Observable } diff --git a/web/projects/marketplace/src/types.ts b/web/projects/marketplace/src/types.ts index 7b99c7b80..e25aa59eb 100644 --- a/web/projects/marketplace/src/types.ts +++ b/web/projects/marketplace/src/types.ts @@ -1,48 +1,40 @@ -import { Url } from '@start9labs/shared' import { T } from '@start9labs/start-sdk' -export type StoreURL = string -export type StoreName = string - -export interface StoreIdentity { - url: StoreURL - name?: StoreName +export type GetPackageReq = { + id: string + version: string | null + sourceVersion: null // @TODO what is this? + otherVersions: 'short' +} +export type GetPackageRes = T.GetPackageResponse & { + otherVersions: { [version: string]: T.PackageInfoShort } } -export type Marketplace = Record -export interface StoreData { - info: StoreInfo +export type GetPackagesReq = { + id: null + version: null + sourceVersion: null + otherVersions: 'short' +} +export type GetPackagesRes = { + [id: T.PackageId]: GetPackageRes +} + +export type StoreIdentity = { + url: string + name?: string +} + +export type Marketplace = Record + +export type StoreData = { + info: T.RegistryInfo packages: MarketplacePkg[] } -export interface StoreInfo { - name: StoreName - categories: string[] -} - -export type StoreIdentityWithData = StoreData & StoreIdentity - -export interface MarketplacePkg { - icon: Url - license: Url - instructions: Url - manifest: T.Manifest - categories: string[] - versions: string[] - dependencyMetadata: { - [id: string]: DependencyMetadata +export type MarketplacePkg = T.PackageVersionInfo & + Omit & { + id: T.PackageId + version: string + flavor: string | null } - publishedAt: string -} - -export interface DependencyMetadata { - title: string - icon: Url - optional: boolean - hidden: boolean -} - -export interface Dependency { - description: string | null - optional: boolean -} diff --git a/web/projects/shared/assets/img/service-icons/fallback.png b/web/projects/shared/assets/img/service-icons/fallback.png new file mode 100644 index 0000000000000000000000000000000000000000..75f97cc58c1e08e82be6ae6dcff8cc7e6b3296e4 GIT binary patch literal 30439 zcmc$_1zQ|JvnaeQut;#-#e+L64#C~sgS$g;_W;4&NpN@fEbi`3kl+>|g5@LcIrp6J z{(?Kt%+u9f)g?1s-8C(rRF!4WQHf9i006q2tfV>s0QU;Q0gyqjiGgdm)xVsZx{NrW zcADhmHA7)8tF8n9_|gIZ!QlYF!)sCSF#zDf4gj2*0007+005y&Zl{{yD(r^|C;{0WM7|u z3jS9LpAYxH9P@$yhZ+u$5C6aLziO$oUh=OAii@nC8vyVI_um5t$j$}7a?`Wb)OFWY zQWP+Ec4RfRa5l4K^>%dmR|_EME$|9DTDqH3csn{cxe0g+QT~TQ;1&K?%tlG^9};(a zAxd2(RSF4bS4#>WR(4i)N?}wA3JO723o8M2N$LNt{+bh_v~hQL5nyBU^73N!;$(Gp zwPu6z^YgQ@bFgu6u)I>RxcNA_n|iZ2xl#QWlmEj<($dY`)z-z`*4c^TA74{5XAgHF zO3HtN{^$BHJ>6}s{#TNd+kcz&Y9QM`3>%b{o$Y^kzg89eS1O?5YHRr_`5%8_sNjDn z|F5$DmLtgaPx${&ng6o%Kc%mx3Zn|L{m*L?MuixCLjnLq0dkUJn%;2dJxJb$D;~5D zMZdP+%)NmNML##Brcqx?w|IXUpXZ(Y;rtJY%3i9^*aUS-mXD9m*g=WF7@xCI?qEaF zY9q^7B_6|1BQ@DXK$H>kZed0Y$KS6rLc27Q`pgs`;Bu9=Jzd7P?XT{yT9%FM9ltUV z@C_9NON2!TQU z0Wiu-|FV-;Apn=*e_|EkkN^Z9j@~JG(eDC+IYA_-%x16B>!aT7*mg?1B6Fd;swUVP zFt}i;%|FTur2kZlh5LV|8UpKHE8A(Qy^gn^wYP|f$njz7kjy_D)V{=gqobokZ+d)u z-08m)sQo%4*c&z4M4xjyqWncamBm;xt~HYw8M(dUMk(=|v%9AmYM`vVyd(gwj};de zhyrG>PvHOZCgtB7hXuAkw?5BZ(4rz|zt>mQ(~F}TQQ6&{(?9^2JeIbscAE;xD`Zeo z2|W`Gx6y#M2GnpJMo^LAH(gJS3M^8h4DDoIh%PU z;u8{zN=la4pF<>MuE*bB6_s|PD<~`^ea4HdpT&a5!^Opw1|p*&v&RYvazv+3lWFQS z=hsu?&s?pAkiO?N{qw_8Rz*R=WLGIA>9f58INpLi2q_63a9e$K$@M0oW6Q?LX}Clo z&HL{t)c@B~fK9J%WXV}{(o~i(Jq6Z2?tW%wv#7+c2FS__D;CQ2FA0hnkK4_&RJ2MJ zBkAuy4E#^m3|D(Csm9aO)Hy~)T~uee4lk(#6u<2H-{}2oGEIhVO=b}%_bG~JLJJ9T zaRmhpD1>ZA{ojh{u2TXcKO8RNThvWT$SDhmONg_NyGajkN-zG(uv8fq>hbPM`Y`cA zOmvLIfYsgOqjdU4LDmJ!xDN~=mC_1$Q~RY(YHpoUu+8sHtAoA@cLAE7LOMm}(b^Ck zd=Ad-{sJ6;{G9GeFR)bEIkUF_+j_Qbo!oQ=f61#ay2-!1Cl93S z@eQBcT(yBd`(2Uui}d(_pSQwv^>X8j+riU!1PdKo`{2Wv?)X`Xpzy?-EnJOba4u}j zX!_=9U^|tfdKy*wgD*129z1#x@0QEH>lwIGf2)0)S1dnMT(O><@((JtYdZ?CGeMsH z0w5-q&SGNWj+Q6X!xz+nICt~y_vE(YYDd5u?K=Gsnp8CEkX0#@{GTYZ-6i11tLdZ7 z*WM<$qu}vQ<7yBrF)(1Ws~B|IFLh40@OKLHCmhSOZchtVh1^Z<+%x+$B+%6^IacaB zW!NXZfzZ*zgQeCGl-zb`-%yZlP|^02Mx+&(rihE z6+XLlX*DtS@&XGA1`7T~OtrV6_-|D2m}8P?^sXOZfZM0mX{THvcSPBQVYZ(3jo`j< z{E0XVEHVO~t?%#Pw(d}-Gi0P+I{-3Bg|qM|L7a8;uP_OT@~WzlHMgS=*P|bvCN^Yd zjTa&;iBXeMlFY!478bFS0!~g=nTh@oNf3cwX9S&#?(5fn!5uR74psbG%1-2C`z`(^oQDna9wO(g$oSH3!kXERHgJ)4RjDlO- z#6&b=hKfgV1C5$oRsb0tod>45vl$<~&mBdtIAs0H0SX$_V#=W6r9=Am?OW-KZx=30 zsftUlpysQST&s@RZ*7ttJaB6~k&=*zI~uYm=Ltu`N5=0hNtDekaR)a(XFZ(INz7IWd~ML-l0!;cAk%xYIOuIR#rl-H(L_S(Li zl;?AJo29k@M04E=e__E{Cn|ZuVQo9_qJn&G`IIi($KnZMyGUPy4OF&eV+ zeH1pER*Sxv;2fwo*W;3JqHLh%$D6~>J-O%mzzR`UWJ`mY(c2HVURpabZn6&v@q`$K zih1os!G(Rp4v9t!ezCA%L4P17IU&v#wZ~jSSVv=gKdh?2Z5lrFsq_?EDJP^D8-g+N z+h0ikRE*sbhDR!Hl zl1q*$`LBgV`IC3+#xf1BE-!z(TXXZa-VyV^BHw}E6L07c#QXqNlg1+} zyAOIy9qV{Cv0hL@5W|J?ie z-*Em|mUkp?U%EiCMDfc?H=~MBr_OMog44R;@2;Bv6`c%&N>_uE0^GdK=)yS@w%)Md z0EL+-0pvGz^u|Kdr6YVOf8Q%RUYn9+u_U9!a@ zoFYk_J!;p-mq&%u13MGho0`Xupv&d-xe8&(#Q$M!YPG?Ub2+_ETr7~x%*?Em z$$p`KRv+OYsBBZ@HMVKTTwe=Q&6vhh8M@Q4vvb)yu4P*0 zS&8{Hdz_CM5>zbsYxQw%3%}z;kxEd?OSQSLwzqEi9~Rnd%3E;5wtH` zUD1q+<#~oVq2GLBUo{=~(<-Df$di5WbytDvY;Z5a!v>wJP}q2 zqoM+cgJPUObT%{h$H43{bzg8YT;DE^BPfk`c#6Y}S&YFT|81_hIRZ(t@10PrLYAvw za`voe5mH&5n$qqk@|{N11gNm-^Ex*@BXc_)NyrGqu~0ZXpuJDk8Q4?kSO5vUzh@7$ zY9f4}%94O8HZ}(Tm{?43ap=2lf=Zvz)`o2>n1gJRnd` z%+=zQ7(6%>;d$3~zn?`n-r)7i)4|{?0V3j`sF=8(lIg-onB~Sc!BhLk*MMnL zJRb9~6{v<{)2U2S&OOW73(EJs8b`JmWzWS&`4Dm-V+-z@p;F1tY}-4R+3w8N`j zP%>Xd*AY?QGf&J(3%OgQwkR!o3x$R#*cMo6>o_6^K0m|TINH-;aphn7avE4fBg-Qu z`O6t(pcdT55{dP4zC7hxK92b#3D`{L+zn*tNbgo?HP9cShQxk^;}S?Vw~ftV|0y10 zE4=!WCF2B#0HM^SrG{XkF~d_XA+U0fOJh;w!1aj+=bwRezbMOtS}qKplr?3R?oQ-)CcG0TJ<9{2%Px7v2 z#+bM*Oe9gcT=)dUroz|7fvb8h7%EM(HPO=zBT;$wvunmhvIqxDZTt#krG2`#srWmrQ$mzM@RsPsA zaIOCejH^ASq4< z-w!gggu`na&A8wqGUdKpHmIRtVnQffI1@)=166tufUOTu`TUd9OR?vB=|zJo89$u_ zI5^0X15+1R1xA&gejA^D3~FCCzQ`6`V88a3X8&Be18Z&6O|_7s5D$dk<6DYciN0aZ z&Me)!Q99Bi{2@p7Y$QX=pN6SQlxQ)l)YS*O27eeXYLrzn39zsQzun)-hc>l_nTY1} z(a0kQ7$`-V=20P==x?eDy=tS-xQI}pNZW+nx2tK$w3#aY(}}*X_9Ca8wps!O&XX3b zOYui6tmd=}JW+@U;8gIn0zsFj#CQJ06X%|=arR&G83jB7;?L`bljkiCN@*yQW0^}c z24Zp)orw?JF(H@5M_msfdQbwzTOJ~XH?C@pi`geM+c76BRvEq~&|Y)_f+jr*YHZK& zOIR{80y6IYrESDw(|N`vw`8KN{gtTWv&SO0suabpEM*&qGngV5$~wV<6`3iLL@AP<2BHx`%X&cGrfe9Se%vt~ z;LaN@3K;Q$EG0W1AMp-%-C?T(PvvSmWGdExnSmBe{T@a2oM-ipkmW+S{%@mHP30L z_|yif_TKMZ)(dJp*}G3)y6?&2FW!Y5%=(e+OqBURlV;*=lgQYQB`qDnz@R%3AuuAW zaxr(ay)9fsxwy;=#y55GPUZQe8VZ*I@?iU1WW6fzRnqOo19K}X!F@JLn)WfFit6U~ zIA3*sbYc?BQXu+Lnvr54`KYvxoA)V^ml#WT={-zzKDYs*DsW8hy09z5ZX(5;VKK&R zpajm5B!sK%U5K*_71Mg;BeLC$rA4NWnA4TL&fJB!31uJp;g>iCOHOSb^lhkxkfyEF ze>n0LZ1bYyaWsf58IqD!lI!?*;+P%%J*I-$8&D4!NAsXAs0uuPE!q9s=9`I z-hHD(Na|>Zq_rH+kHB~uM6cHvG+)b|yXsa7)crk{QSgLtBSd$qCeNU#q*-LvHA zo?Brsw>;Iz6$K6~@aQ+$;8_`{kqLzrhG{^o1&mIXm>tXh#cP1xYR$ z?^4U3@dNEbau*Tgtn`)mbOT5)*LfV~?E1~$PuYe3pkUlcvW+R5FKl|=c)5GB?F(R* z1=J(83CwiCdu$euSC+%DX=lAN1fG*PTV$1s%6qBPoZ6GDxa_}!EfZt0+4vjz3cxGc zm3QRe>&~xM39=l^>p#-ySsBIUSeoVE7$)uZ#%)H2qT97QIFGm}T~(pbySm zaB)1;@XkvP0f=Gvj)Nm~n%Gbin+OeTgGiZb)IlZD)zb!xkfD>Wd)WUm}yZbsX=oNTEGf`Nc5iU6B4%#~LkF=(RiKZDf1y>Voqc z8Uo0iY)rHYMPDCpwcdVqNkoi&S6-YmEX_vr+=SX3?~JshL`pl3nmw2zC+w6ijeZjM zGc$foWdk~CO~9(8isGTzX0>3{q-W}M8n|VE^{1}-$W9!pg)y(OFd-VCyvX6;=V#lY z+?pE4cFjIby~u<;I_Iwm3P;3hy4ewv2j2S@MOpn*w}W?`346x_e2>R|bmk|1(q)PG zQ{3KU8U&>8QsQZ$_yIQQ&R4FcnK%P>`oC9d^gb09idO>U79Yc=dIpOti=w?qr$V~? zHS#o>t@cy&yyPH_xM$hae=QqoELnbIa7|$h?-mqPxb40tyGX4j<%;n$^ zaH0t{`>eb;Z&=zxF|Fz{A+9`Q;+E(RKIYs7T$r6aNDCosSq#M0Am6ZPgkV*e78?nK z55UC0;EGV6bB(%iK0TIJvLqhYw%Y5UYc<~>_%22bE-on{%FZ0WnF6`x)c(Br^Tb~V zCt6LhU&3F|IBndbdxw8{FBc_CSej(?v5AI z%u7)%Oc~G$D+brbrVdT8wI+N>a;7HyY}fX(;pK(luUSqH@_?1`KCFT0NE=Qt4Y)ku zp^JR#wz4eN@+B2x*f!zVvtw3Lz_7~Rsl8cIbFHwhYd$0-1{p2d%lgJc0=TCQnNOqEk1w0qU=%Vxrfv1lnVEnQw|&Q;tC!}-*kKF+Gb zh7M|ip<+FY)E@{xLK+FL#TR&5)G)tJn0d4u>3gIfn^T9~oQz7XVMx_kxc`XZ?g|3F%KSOG1>}d+WJC+W9D!rd(j>*YYW~Pn@~c@C zP0cn6UOH?a$k_01$+VjFc#Y=S^xOFpe%-xvfxLqh4s?sD4Ev{NA= zl8|n$9J%Ht**U4Fb8?ewrF{T~f%`Wkgs>tm`84TADj~{X+jfUGZTlYmUCK7Yxc4k? z81b}~#%88r z^P|iVo2zl%rsB*Tq6nDk2jWj&`<%l3dR(VjY35n%(@-e|=Z{k8y zuo!0`+u4e2edp`{g)#cMR|-DJFbis0 z2q&`0Rh(Mp+dO&~0Vj8i*)_#0Rc>kIR|rGmO3E`VD1Fn(^cb2DI4bPd@00w4dgs=X zxNWj|NTLxROz|4+Q><?w<{*b&c);)~&=!bFF;@~Ga2EYi2Ah{+xBO6-%> zZjf7l4)$IL1;!gis#Hf0oIcqUkDsaLS2AR{V`nY#Ku@*^pcRJR6^Is{a98}|>@I3j z$AxtvIC-(rw}0sFKc*8Rjy58CD}YeVop!ykq8P&Upr}86hEJka6f9GJv-!r)4=-$Q z#l%d8Wo8qEC0wnIo0PDC&^S0bW4}ZiisU)}DmXrj87c$7uwo@OzWU-SMzDPPMaZ{s zbtFv~^k-c7q1Dsukbw0Yy~t;4_UKg{-l6tW$&|5xxazdJPRyF^} z+!I_pmNKz1xD!%=s%Z{*MdNN%XGc)z%Xb-*Zv#-V$n1w!)7;Mam^QY%y$$>@kZ3Cn z66M=aI1&G5n5fh~49z#-+glcu7LErN7AV=7c7`b=P6Y>w53I`4V+=J~Zuun=g$B+% zdLPV46qluiAV4%`1iKQj?sO7dF_N2|H`mc=(^F6F$*+j>L;Yc}_J*2ug9%xibWM6* z`X?}NLAK#h{Rm-vA@pXfWo}iO@SsE!TQRjzxp6Sva<>KDN!gnp zU8stC$$KMf+7pZefCxtICl$Wulw> zb_Sgzxxi9%(P5{oxO_ZJmc3mzM~r(R-_{8WT#nAD{q-cJk|AhgBE}V=%CV4hZH#D; z(0Vt;uZ_d-0aR_)d6#?6EN!-}7+nc1+7sHI3gP$8aRXpM9mLTV?ptbm$;xJON8ni3 zdOjTEWixDNd@nPRlc)RyHoLPpxto`;yiPi<2-HT3KABO=CH@xsXy#OI`rt7HrI&rP z?=Y10xw8C3?6WhL38|I*=IEjoEWGZ1Gr4 z6KcLzsiRNJr)BJw2+0J^(Ju4cw!k1^Z4#$$48&=LWboGUjX+Vs}fn5T71)Y9=CM>(xvizQ2|^Hw;wqDyj4iV!+z+uYbICx)X8V1g^E`1LPL ztagEX_@fj7NF0m zkgC{wN`FuPZah1W>+@BBXckh?XwK>wJIz;FN@!6yyu@%_=`^;26a~23V5(fS65ZY$ zK5>{vs)8wcpAdX3aEOV;j&jhf%rZhcRbQ3C_Ho|99&3n$z=*OFCdwG9q(H1FQe4UT z;=pDdtxHgJ7Pz1g%|#|JHQU6zu4h)CY4G*lUy6Z;i~Aen1>)32-moym4sh+wB!44e zy^!6PVqhS6_L90Nub?NNf)ViLJ7d4o`kT~IYB6bDIG#tR=#_>?@2Q?&IS+%1OHZ?s zrrXr3*O=p;)L2;HyPpdK^T8pA!}>;e^tW>eVJOYsM`Fby;o5q639a)ggpcP#|1bO7VcuB<>1aVAFqv8$U~>7&xhlvEZcU{*1|&n09-zsBH( zdbDP>IbM#;tPq{lfhrTbMOJ6Q=P4#o#|7r@0)n$%U{lZCRF*LE2}yeqke^UFH95IE z7xF-fjN3*d$jlKbykyf~N>BIcF4Ng%aLn@V+XudrH(08p; z4Lz$mUEsHqyfa&M!HOwE4+dv{4ULotj>ahwTwm|cmknz}+mFyDphJw8y5>8~!u9f} z$aHrTNK%daiFU++T_tKphkG@y&CV<0z+ElG0nN-KX(rw77OQo zy{5fDv;^sTOdF)~!vnar5nh&demg!TZu^`~L@OrCPH`}vT3i(xKg3wQEa`pI%_5xU zevp-}{Ya(H*(3Bm@#e~_^*V6dA&79d;=|L@sjQ=jv&n_-O3oFcGr4jKq+J_@!W?Kv zko@cm%ha?tjWE%#_g;?>qT^Nz%DMqhd3cYN!_Z9=oGi(Yk?NYIenZuFmwx=gas21f8SKpG&97VSIH^TmUgsg>@Q&yKnW6hNF ze##u=UI;u{nCDDWSHzIimpU%!5nT(BkGVwwO=QY)?pTasnzUZl2P^Q>>xQjUzQM23THd*|d?Eu2dfgV!p zBD7sLpSC8o5GuU?Xr>v_#|=6?5LOp9XAr4~7-Jr*Y`pnx19$X{C!9V6d>5m-Z20Q3 zCN5!i;=BRquV4bQ`C>6iLh8@zgg|PosUq*&J*=;s5#i64TUTM+4I}La!!fswHyMAg z$RVQ52|?GCW^ZB9>&MY%Jjw38ROj=_Cq2}=Zf=XJ-`^=y1<B7)+#2>ba7orceM{GrI6Q5*E@&yr4<&2csXD@r^=H?>C2BUn}!Y%?~utmKV zlaW<6XiZPCjUTD}Uc-x%-rCT?gJU|LpYJS8FmeT)yC|PwyQRSspNIt%h;b0=G>WCB zBaMTrkZSy8Knbok*Dgw^i61s++KVl-YOeX2L@O^&Y1B*Z7vjUWv&g$W6f5?Y=P$dW zZGMe?CX*2S9mhdqfi@a_$6BB4cdo0LKZd_Je_i_%1~ZN9%MZss4SjjpSzy?e{32uc z9So4Zo-qDP%WBvwy(Ka~37`x<#!}u#?OSPgUmuyI+p(}A&nMh5WnHk*JNW~23X@{% z_0a!bbi*dR?#cJ9nW1OSJ-Ec}l&n{K(tAG#+_(CMt0@sV%z(RH5&s)~2A(k;xZCJm zbi0d4wTguWF?s<<==g}}T_dPFSC<)yv}Dp18fj!3NHBh4qd5uoMr@Om)Xi}oeLK25 zL~gO`6ZYwnDi~72WkSR8)_cw<-wZx@y5ZiGh2b@xSBPFYC7v6jk?NqxNwloQp+yQC zxuzeUSNI8pY$$)Y=#3N;A%8zENsFeaKl6zq!6>r8eS?;!CXg;_6B8@S6w%K2^TpX) z>}r{BqwXjoS__dYGwm)RM)~rZI4Ve#5FT#ZwV@Zrw<@c(4G!cfJ(BSzmZ_^R(Kx|* z@IS;J`T{R|0w3@I$bY!2P%d`r+={3f0Q@NF!Vb$bV>6VEMnE`6g_6FVv8-?9BmsQ5 zy@}*z+Q4P#pArLKTr(jP3P?7ap*?cVbdQdmP~olf`kPhRI10+uI5JiSBXU?^Cq>Ya zJ9(`~hnn($DN)T4k#2-=VI1URv!X^b0ZU6Uy$luqc{M!)40V_akE6(mqiG)jDH0r! zM&_LHNE*JK9Fn=U_(@vMlAGfvFc6i*J?IhA+9Nvhx~(efcj`twr~D0LpGj23Tv5kT z2QWo*Fwr+6Q-hQaU+jCT=g(*8`kSZGPsZ=bLJTLIX@$8b6}bC1`V{spt>##?Ot-gU zQAFkp*y0CR39bK>iVfU7GCthMw^%lBB@5kEvRNq$itj@X1&|d7h)v75Aqa zqw01TM<>M_wAWZ5E0xQ0SrcJE!N1-V!4E&=D-Fu%zG`yF;>VnyKzqc#bgbs&Z-ph@ zNtfF?wMzwAMv;u;HWum?x5MjfVvLCbY^jsQy%}7^HB(ifQdKg-U~2~KOKLEEm0Gu) zMgIaKs9V_`DtYU#TxGJSYGz9K%xjS=G`(9Fp!Mt>=Y#6;5&;gP))mMA_}Zz9^ZaR7UM#w)?z*0G*k0z!fXI?-VXft@NgInInoS zt;aizvH$Cb#5t#^H2;{QChQz3zYt5zx$IVLEKLNEe8bj+4v4gRxw`NThlCL)_($dQ zp947h3Q^9c=wH>27@LPVZJH)K5=?m7Cdr4P%n>U-4_)ZMpJo1-0tX)6J#(vXobK)* z_;SQ0;i~=EhN&ZBD^*u5;>DET9nQZ&dULhgoo=QWZgx zuSuNgD5^leUYs~PyD{|K;5oJ#Lu;Z}0TilOHYc5d@)(X*&f(ImOgs>kF=PQE28%V% z?8Y;jUtPuCAAP=-(MY}grPKFWZu$}K-^r4u*YAkTFn+6x^nxwX*4TIgtMaL)-Cpa6U-}@E?8{H3Tk)SDgLgl1@8Md zfBG5fJ->iv)qAqg92b{tQ^WIfuBGiFBIc1m(&Vs_G6kQGjg9aV$|u^SPi>kM0_B)j zWl;rM2nayR=X7clOYX*WJV&c#&b#CuQCR+9Wlum4ZPlFC`Wri9eY8aS$RdvCQPc5q z&+f15^y`_`qEB0+;jGphw5rKC;X+d1%5BMq&bUbsCNHkR=vB~M$<;$zmxX2he@mIOotAEUMCpWjb<;Q}XcJv61gw`V6SoZ0o ztaVwfMc+0EsQES&!+S7~eELAT3_B|bCJ%lhOt#h%tNrtInf#i|h9PChxT%^gRRg)1(4D@eX)mD_i6 ziczVn?Iab4pPT|xQUJbgH&~!L_4b(Ek_D{W4l~EC^^B=_l!HD#tUf83z&_^`e=BK8 z$ucOXB`L_RzuE*~@WeymW67UE<%CU5_nW4kmG1nOh}mmvap%YO{%H$sw`+~E3u_Zm z*RFy$NAtxe4ji#{>u6lZ%nz;x_FR6!uA?cV2dW?6)V)qR#RxcoinKCt&)+N7jy=h)4=#**&ZWvL3dcL^6W1kgLGQk-N z+Bj1^f)HA^PJz(l!mw+J=&O>}=s$MR%DqnAVF{g^1+DZV4+ni|FVi#Jmm4`3Occ7i zt}0ZXu?(ay_9LYGI7I`sQkOmAV?U(~8TV74ZA#U=M}sqo-76Ea9m^2QDS63DkOz>_ zY|zd<{a$S6*_^?=58IxE?UHlMOC%;iqUm_(_6Q-t8a{CX+fu_PUrt66X4E%};BFf3 zM2W`Ajp~omob+*WE-Tgw*yBMYPMo~sAvbv%tWPf+Tl7U1*T9M*RfBr)7y+VhHc?Lc@d}$K>W`BVtLE!?gJW&L%2O{e9{3bjm5aRR|BrbIU44 z&z2eyf({xlh}wanb0{2GUnlA)Li|Xnheont#X*l_@4f#lwW;=2XBO02Dq194d+yBX zw^}(n7kxHN=z1IStK(GRJz`W;R53O9L=C%|MoJE8KUEzF!KuZ^h(oM^&Z()%MkPoD zm~%dQ#J+tCEXqNqSTC0)(%d;K>r|~mE)x7rRf(`0FZV7TVPB_Jm!}XX^lPTkBMo?< zU{QP@-DYbN_;y>?ePzlzb%QkL!+7pXZU$gi3EHJ`0Y|MO;L%atdSY9k_#GT!NQ`rH ztBK3X$@L2i?_%_BJ5nd!?dUU}NOdtBU1%N?gD!}CDjdOKA&Vvzs7?F+-R8?Z3gOE{ z*OtNZH#=a{o|73tK_iJ%uGrR0ad%@M42{{(#e3y$F6c`s`g=~A9ONNSI76HipUb@$ zpLg1W`wA+tyXQ;7-E#LQ@43Q1nYJ!Old=S?O5h^y#@>QlO~%F)xlNcx@`_%Mnk31Q zXs5GCL3FTP!6oGZ7&L_<(l zg{%_zzyUld*2tgsUb{>WG#F&EI~^o+&~JAdo;#zec31$Bowa;nmB zxqNRtzEStlq%8-knR2*~(TbklA9PASMJ~T3`JTL&pt6wOPcuYPso0_T!Rp7$jsB;J zbtJV($cX9%La9m0yI&0pg!O+%87xX@hFG-cE#Ij~*C2=`_E zS&9!cLqVoxii}=koikg9LmMi0gxQPa}Dddo|%2mu~Oq zQaGt4bU9r=6)hA66o_h0v%deXg_S(qDn^c7K=^ zjeCF!9lYYM>Kx+j7IYD% zo9e>WuEXLI-IDxCG$-#Z{Q1!tXpJAq!&NO+=P}@!eT6py;$DDKuH-7;Kd`SR3VE*|BmBe-M(f6pW`vS^r zIooAFCYsdqb7INeiKl8p71U=LQVFi+CWH) zm=8*)ZTn)+9z%@-v+?v1G_(BS^sX1_dq?W!0e8ir#(f>!NXI!|4mjc1O$`vt%Ob?b zj*g5AglFAxDLnG^dR_e3f88{V@x?FQ=YSX)RllZ-ooLB}f_&REa;y>ds1ybo(iCEtT#F{ByWWn6&>>UY>h?b3C|fN3 ztUW1uc7nm`G3;q22y6bDMZ_+umbRGHFJ&V<;oyGxY_Yen0!F@_!I$6)*FQVLzROA6 z`AEq|9wbhE|5X zs+PutgKSvrQ51FkAf*uhTyR8acxsy7>F1X}8jQHN6W1%_LReY}=XZF_ERzxhTG$W} zF3uh#+8}1X`F%KNOYS*Q>9xdeN$JenKQ*0`eUF9k!C73b0z3Mw$-hSmoI!y=2yaN? zh3}>BZI(3xNMhNpf6S;`onLXFfLwdll}sXILv$UD?|bZ2ex%cgzC=1)1M?@4P^gGYy!km30#6>j+GKV(Y0gn(8^12Bn#Zd;ujQEWk z<>(^z@6l3=Cq{US)X{@TMgNCWkKtCCxvS9mME@;I<6o_>%_K3K0)DGkV&rD|g=%y^ zxe({eIHF#x|KYfnrmL4#30_KjZOhZ@F}+hKk+YRAKcHia>YUiIlX58<2VhH^BguA) zO}}D{;T7QPbj1qPj?OwfTy~I=k=LJflI2{Amm0e_Qmj9QgOz>+^#~DXg|HbWn&kIE zWUzxSDs`)A!2apUV(2ts8X3L~e;wEYb-DYwtwH(hx(rLz6$><3oW}qyL}*iPJSqz9 zM}7T+oBjKN-}d?&@KHF%7UGkJ6DXj?9z>9-LAIulLk`?X+n<^>N7iUEdt!{Nv6#~s z{3*gQ-*h)a1BK`>Tinta4iD1{S9?q2a0dA*&#$Z0o#%8G+HfJB<~WXr6&cbV3X`iF zyums~K}dByJv-O=I2QD>3=wtV?cfCiIb58>C0~|bi0m9Lp&sB}Y0`B^IUIRqD1LQZj`p7A{ngAHxCc}oTPwuB{rK}#W1+PtV!O{ zO_A{nEp~-n?X!m zwGrPFRWk!DPiNWwS}l~{Ua@H+J|6?`%lxkYE*Pl}AM?Y7Sn5&K-VmjP1+Op&@>6lo zu`>h_`4T%z<48vcLhG!~R4Z}R>mT?hm=HUH^Z9t+Bsgx_aFE;b8nd8}AJgT88Uab5 zJrA>iHz>Zb8dm>K9YGMX{w$yo_pSv~z<<6ohla!Qa~UefZJGUS>xg$8$aqYyFXaed zZV-5pzQMLc;mv?qqEfzPU?1J!eif(~`1c3T=VY9``4b!NrEtMZ z^ZbCH2xQqI=(jgj?mMq@%|bdeI&-x7>UqUK57;?1Fi1;;8-(!x%1p4o9E!d(5?UEZ zgP)k`H-Pc-Y_b0?UN0j59wlWiXJBT9Zz<_?=OEr#O+nbz<}4N{My9Tzu0*MsXHATI z7+fqo5A{LSm-67kyngZ&wo-q!D+}*W;ttVMt1dI|Ap=IB&xUl$n9l6!*U;hKge$or zXBRiDL(Wwh_TQ1Wsc54I_8raO85>T-vU2(_A;1&_S`LcuA9$9u&C2K@PeQyDac`VP zI42Xk!hUh(WYaHoELVDT#OijJq4q${O68t-Ic|gkiEdvFi8T%>22Nd?`9pi6;~~L6 z1!Zx7+8u2xsBl_iBjCUBlh6?-&@(9@+cSH3~Xo# zY}Tior`ySBg(x7BN6_o$;ACn|sxG6-4F`g&U@F{z^5J>qxe>lFeF1E_r=Xb6>1;Y5 zhQbTBh}#XoQVHVP6LfF#O>f+{9YQbJ}CREfPC*79Np3L`XbxO(%-&8WP zKteO-zrED@7`*CwGkp)n$bU9%YHXojwp?C4aI+2z$K~gpwkBL;HM&zmN-# zAdp!{%+5h0k`;pvI7qb4j@Knr;b7?*A`}fxPNsw3Q?ZXWv%dfSj?&&T+h9g?M zV}Y{GtbgYpvUq+y6FQ3~-Wv%2COQL#sfUW4%+m{geTQi5Y`G~6u@Nzh`Fjd}zCN*N zVvOlRgcc>B1s>o=cQqGIT*9C%8JkDHy|${xxTB39;QC-+;mMC8%HCa|t8h}b+ljF! z&cWb8W=KU%w~4jSdPDUXJJfqQo~j+Z0{mIMGJ0;=NJ;{YzcR_M4)wo}m=E!&w){cP zGFm}}rQ%EgK;*RDss<67Y1*_)f+KO_xVG=~l-lfDQ4C2MlGs*cIai0<=I`q4U>BYYDn z14HGHZ-2&0hi~sX^QJ@<`Dzxb`WH8zpM0?SH6MwMZ1D4=pouKySX7D% zPEB4eGxqVV{DF9xx)Zx899_Kq*C|BGZf3Q$dbmMzaq{Su(tfj~DoY`De4`&NN|~Cr z@DEE4frmdC(TEnq^<<-B^Oz(P!GQql%bg+H2OZUQMG8TDWVu*QHdfQtAj|bWrJ#^)}x3O%Q&!a$U1%Je#bv7&@FfKGF7TZ z-4HKj@lci-I5nM6wiNqeoh8&efa~uxW{N{mMAzXWhou+8Gu=--sXZ^-L4&a=K@PBV z&6I)kgJpNhK;qEv?{9YQyq&e=J>13z{+(~d%;;_Fd7KPkx)(KTiw3iY&iCts-pT!c&7Eai z9Ko`HcX4-jf=h4@?(Tu$1P$))?(Xgofh$&YS@ARuH=|BqOOHFK;p|u<{Mm3bO^typ8G~VpYI4?pCbHCWarZB~x+}fGw z5CrvW-=KIN_QH`3`WarDmBldtfXnC~3m~pJ|Dtn-5>ii=DY3Jbz!OM8SnwWaSp7QH zR$jgy4u?WdDA+7SXjGJ~+6O}y(VBf(H`2-rusrx6K)TO?ZbA_xK~S6q_%snm5|wZK z1{EZRenbCuk^^xR9E{OZiZ2+y3V-sxIWCH5yBh8O0qk_k+Y!z=2WB%lZYYF(6cBZ;FjwL??%JzuM^WVbio0J?tiPT zZw0HcDmHs}!@&(Jt@jjohY4;oLCwBs{ecz5C-dp7YBJ<|7j`QAmBE3SZ=E-a!Z%L( zt;y<1b6p?-6@16&VlrIu*!P4A9Uf>QO@S^&ENC-dIgB0Ck0Q}o20uA-&F2^0i5_{J zRk~HGuP1L&T(qpY0?bvlOn~w*(LYj4SqB@l3?DqAL6G?e*gVhCv!~C`y(*aml*7XVmY4*dQg=2w@mxtH5?Vr_kyoA3YXXot3XrM8sB9n%N${wL5uBJt)ZX z4+>-iV{pe{LJ~Rigb6YDNR#bjC^Hya)D}giFDzjy)t~MKQr`%;|Xc_ zP@L29FJ?T2HnHfHk8OaXZe5Y+7mC9^&9iTts3Ln^bprr>8QTCi| z;+@Ksjbr`gZNIO0Hpe|LHanWe$_kvxzB}wc;HHxJpYQS`l!*kfBglTdJi5g?w@Ylh zi<+)`RE7+R10VE_UvKFeaSgDTZNOO|CxKNuWKAmR>;K@lYK_^jg#!JH~;&M2?g=MGl`~ z0UE#)({$Uni@(spUyrZm#y(1D&E_6|0iH9WkOvEQ;la}#ty_0<)Hj&rf@qL=l9HDf zt^w4P`577>aHdu-Dq=?(K*>F$7puKoZ{baP3aZ%EnEI@#>HI)(L9w{9V43}KSVcvC z%etJ7p;9%8b)RvzqDFw3n&_{c9;E11PoDo!NF8Ysh6R*msL0x77+W$a5t-<}xnsY2g%icMrI0#y0sxK=GYx4UzZPXb$UgahL>!g+>s}#*E zj68qS%rr+$_2cJYB^bWJ^8q8Jww<=O9^uS*`L?^x>;o%jt}y!gYbFy#ObrAyvh2NX zx#r{YhPyjaSdnEY&}HGx?&st4$@=u9jmeQ_+Mqp_(w;Usn;<4ORkG<__5U}~N8g{^9Bes*w(8NqAk&miJG#PAlrl9WSm&HF`KPG;xhw`+w4085KZ zI)6MUps&i(JH0h|A-iDK*VB#||6#4^$h+yz;RnNe8@vDOE6t;hv;cvr{g7HW^#;=t zFbpPzw9?KP>(D2wG?ZnEmEEXsdcR>(QSx^nP>J?DOFV;rSA~8PSlPX7kV^{4H2>pb z?*YO|93>yG>Gytqx^fVC!DP{|7I%@sZ(%r;rpMeKotEYK(4HJ_y^Dd%?uS4V_%-_Z z4pR7LoSZDq@wzaC2^P9&X^rMb$e<42nAGmwqu*#ijZxd|F__^4IDY!UoN77`m)LSNu_qle25?I13@Hg5cDzw2}16hZ8ze*(07R6j7~56Dxj` zVIh8E&cbNum?RV?;@B6~H}Te24oSI94l>{E=S{x)=b%%FmZ#!S{1pjX3$h7OIX_|g zyMAP=lMh$eM_g}h@mx>r~MH8{oqp) zoXeMV{Y*%1GNU@Ix(aq0^L|q+6c|;XJ~VM-K)U^-yuibwDw6Ek(r~3b`P+WiF_^MN z^!-BWoEnAMe0aOgK9-wdEZy8@C0^0tI!ObaVnIdqF2mCS#f$uj`Y^QPpNHn5Q0e8C ze;~Wb%rUN{R%eusA2ra^g>zVY@1$)Oz`0;^%EmOsck1xxq-L*Vm97sHM6Mhl0JwBK z5*YEgOMdL?o^~oLCwD*z%>B)oES{K@Z9=Ne+|#5bqEsStHv|lnkvf)nB|!wHH@F zClU1c54mul;lS_aM$b0!k9m}ic(OiS)azReX~9ipOK!UWPNw<4&1eF~&Hhl;Soq(G z;c-##K}~zp4Y568m@i{e;Ggs6mi+Vw)o&Jkv<2bksLd{ffkX~azQN~(Jo~c#3i|1v z^pA%CD-0kZ?u7zv!Q19ToLE^=qn`B!AlMsy%I+2N61}HSDjxJ~75_9*9(%7R^Vhg6 z$npSv6`E@Uy_EjbscBQV^iRHldn(Nm0~)M99OV6A!a*I30Owc$axpQv78MD=^Gj;p z?(?dzL%_6?Tkgm+Jw)hoEzE)gNd*uwt1rP*K9`~L)&jvLJg zC}FXGepfMZt~?)3ChNWDg3;hRdh4f#r)o31w8OXf1k(c%hvPm(S z-45mOJ@v5ON7wxx0|JarlXR*R=H5vejPV%SFYO`r>p#_~kV1pK$o_Xq=B_+Aj+*A^ z6+Ca1(XfM%Jy7%VD)?|N<^CBkcu{Z3KWtV)`1G^}2@;qZ77qrXI6p7c$Ll9B z6ay9i29iQKJUyq!@azb=IbNo26Q)R{5~2{L4~%j`hJAU>n{$8yl5k8`Y`=`23e?dA zRx)q|e4~(-4!XZryCKBD8#M)9?)KvIUE_!LtAkwBMDkm00~e#?RD^H{<~( z{_ll=+`6NIf;SDSZ2Q7|9Qw*j?#O~SyBvbj-NG%*SqSYi}fu0&ap_b@a#>pj73+!Y6u?@irrCY~JK0#=TW zBui@%>?iq>{@yUfB~a}9`HrH-?qKp8EI>K!%9Ch-rs27a!2@5u5ngc6wp{5Tcsr{V zK*iGSVFd`d*JW*7fWE3!Qj_NRqJs%Yd~CZI50Up?`B|Q1@=x_ zlhA)%yqi<83d9a4$P|qGN(AICWFxM zi;VKYbDQ8ZjLgqYkeDkgzI?kjdy|l**E_~NL3}j3y zNP|tsVhE_$0~2l%qpxzk((gRfKOzy)=! z^<{!ENtFHw+X^AJi$Y;!9PSwK@?I7ge^{<0K$kFY{zmBj?=pXvb8$nvY!Kj>cI*A6 zP>PG``7xMDUY+mZ^VN06TL7N9$(>01LFWW6C#F$Ajk;4{jy`TJ7@-5@(oN3 zD=Uw%!?9D^fI10fhky5R@V4=Hebx7ciNuPm{QEESU2T2{hqZURT`^r?!y&+`G;Krn z>t~$vg#9ZZT<;K=O0f3y_vTybHQY!!29a$%v-Rq<#qJ>uAQF%*#$`G(0=E@IZWR14 zNZK4OmiyyqsC%Ice7LYkT#0QvjwI>q8mJG$HVGkZO!p140ufG?wdC;2BJZyL041T_FMT@xZsLGv zszhY&bgCsbpx%iX07%ul`DA}0E{b`IteMN=TdZd+zUHV4Oybe8Y%Ex#Kmam=S9koS0+SvqO-SIAx>desicWhugO&lla_I~KaZ8i#PsfAr8=jPD$l#Pqw+iS!jd)#O$l2{G?Ohl)2# z^u%C7ID?#zX+jZ~xuh?L%SfCW$14C$tNqe_AAsi3o3i@jCvBfrWB%S*T+%F?NH!B#rWgL*YCqu~>`ocOp8--jtsinY- zsGpyK1$z9T)L~%SIxiSm^4SkO)+XGkEfQ=N{lFxwm+XwN znw&o+-)BvpzK_b9-sWTyj1bH;J#7O&ZOF9GkPY8gBxu^S6Lmua$dHZ)PQ3khQH-g3 zbku>caKk~NE%^ld-|hq#I7C0j0(t*7HC+x_`15Kj8~WFUp?&6c4BUAI;6BFXo)=6Y z@|^Ut(a_-wvC{~_s&gakpwzDTxK74(2Z^DeCCpQC5~^IR#wV9ajgHF7n3>@`_ZPq| z$;Bol9?n#1{wrY$}3@?5$IeDNE~Ml zbfBUq5J=?bd{4^C4kgW5N;-~dFdM@fRL#J5QBi#;$jvQ8?NzF8c@i7>JP3Z`eUJW) z^?SYggA*7|{Ry!sNE2{U^)cHAaJe(U9(^R21Z>^d9`-PD=nLO(p#8y`3#7nepfMG= zdP@lBK3fS0{qw{-Gau)zhtU6xm2fM&Tk-L!1I2?<|1`sMQ}W6`kkilJ8;OhxvchIG zQ)4?hIax5MDAr|duvnDywuku*K5OKiS26DzsGN3uqYVCLl_ltqo3_{;l*GWb?pV@p zh4WV;1%ZxWlzeDY_GkB8H$Oap^{pjxAgWi1(eH{i8xv&Ug1EB^Bs| za#zd0zM{%2RA&hbv*3yK^>zNi0?9~AD~hc6-4!JqNcbASVvuT5*ANV_2eD?Jvd^w5lo-)tU18mXHtE+kv47gckx!8o$iOUbZ zeAD@k2>cshg%RLZNbcoS<7x33#yxe!q&e!rQnMg9Xc1ev^F5Zm6w_UU+K&Q*&0}74 ziS(Q|s!iidLp1@@xO`GxUR*1ful862)i~f=IsQVG?9Y}VAD1o5PQt5#C1m_ni^3Jy^2D(1w13gGp2aYZJ zIzc?^|Mqv?;nxi2fInM#P;sXTk$|*g(J%667D(EfEfR0Fif3+uJ^czP%GDpsY<$Qo z5<4e1zh6S4Ez<;6yz6ZspwH;-Zj*?t-il|C7!+C>k$%fm%Ks8A8m zf6QcNhum=0oC^Ed4N~fndCT+riW1;@fT<*d_@@!9Ac#@FS(C|nCn{CjJ^VN0fq;ZW z(nsb~tzpg3qXO>Hi96*7!O8Df^EKq=YAJ)68|#x-jTWP58EdR0n6&5wzep5 zH=q|h;rA7l?U%3^&%>^VtCmea;v>SL;gM*9dqPtI@tx$W%?wk-cXKQRB=z8<36ZFG zP$2UAxzFD(Mw6cJZfeV&9C&5&Z#<*;;Vg2A9;afL+R9%g{Doze*vA=Z?zGJ^zn}MQK-=-;may~g0_b}fM@7NiK*>w$G?#=>j9eKg zYBM`O?)V_lkKtFtrpbJWij@Fgz6Pf*RBbbk_hs*OiZLCH+imloGlggzZZaXBjWaS4OU zVjSzmedQZXt8^WX|H6*S<>>4e5ZOQ{MB;Mohd@;*r|Z}h&oazOQf0?prBx7M&Ds65 zKVy2H?Yw7ZNttRLXux$ZmC?rRf&{Q`Mc5Kjl`JgN7oR^hW%b3`?9__Erc>Gd3mtL! z8e+*Nrqb>9cq}Woc&!eIQ!+gwZtNTp=V$w4rW;-&CI<&{-SsfM=;slr7VKb$(<8)I z%_WWsU8V^C(YUk%)R44a!=t)2BrwNo-4#as`cTHgOW9?pxH&f$w7M#w->Mm<@LM}; z$P1+b#;psB;kYvLYM-yP8^Mws_Sz*eR_oMx*QAVi^LIvVP{8y@#PGNbC4H7ZwXITDUy4dX5c5`=$G$7n2KDi@@V2#M0pwraPO}p zd5*7s@b|Sy0}DAO^+`nouq&98bQrOsUNAN^Vyy8fQ*ikwnF9O{9_780{tJa@w5CU3 zdTl6yPoLC`HUr0hFy?l+67qV(Ct{Ju4T1?=;=f8O51H!o&&F1-2#3QN5YAXgqtP9QF94YFJB>FU||*S1lR-hUYt!lnSr#Rn|H9`cOE?&1ORFOQMQ# zWG-#CE%lRSf=iCY0^Q^`egOagx?hu=XAcVcNE=mHrCr0t8q`-8P=Yt2lHYIRa6XXG z{mvtP%~@Z@x8@qpJdcfY#~mp>wrFznQ4%%HRHB*cj zWgkU&pb0fcbpANsVD}+gPJ*$c!{oNoyU)|bF}VTuC~hvmyY(AQVOJOF-4d~?fgfSU z59`_GjWF;Lg6WK0ab%sj_*8ar?U7P`ak}O2$KfrQjS~`R@t3QK3974SR<;r7kmknZ zehd4Zo_qbv2jghN_#p7N zB094y>@f;IC9^JV<`AvYpPE9FpZ9BAGT7Z~2vvIOu>&3<2h6+ErD8Ubg#7q)94c?m z1cNfM1rZ}=VR*oXBo+aJ6{Kk5!w>(MS({eh?@JXTcZm9rQ$!@f&M0-=&E$1&w_J@# zSjS1}p$J4ti$U`f;nIW#!$8dGpu3kwRj5H6qK@IqiFaAHOPdjevLcv}FW%%3c6GEU zOc(n02r@M;0tz!%z3O^LatzofP!L%89DUr;(T=*Ht@XL2u|51lLyVkWNSvopc7gem zeJrNG?n=@Za%nduMdrg|vgk#+Cj z6Mwg);mE`_Me26V-H0a$iNnb5V(4n3%k#m6ygMLIamIz04RytZUlUP>ZitKCOy@yc zSe%AB>CW&zs{J(YTdOzpClBU`nG2usYFu}@;}Orl6AD?ZkdYs z5&=4`%uf|gj2FgUO5!V_d8&;p4MiRvXNc5pE|YdWAr~%BK}M3!&N%Xo8djpQ93KiX zIwRBrUw&d|A>8uQqR&y-?p5C~y)Tc$q}G6MqaDvyFf6G<^u5W$Wj2uDNMd~NeN$07 z8NA;&QSp-H^lJ_E+tfpl@tGWpiizwDgdz`;wW;I0bO^`7F;ney{7D%pbEQfUdW${Q zdSR3wls-iNSiSKo0*~9vn+(m3Dj>^zX$_|-5WoCQ2H*j2$Q>?iRt)(Ng`Eq*hNoGz z){xT?c7bW#o!E>7L}$34B5wDLb!8$~NWiu9&~;8oyP4jFuK&8ltjGq%o+$B~tiyf1<8I z2jV>y6qexSWrFr6*j5balJG&g-8B$6^6dyd-ujZHgkln1!BsRTJk9%iYMI<~K=l_* zs5`O}nq?hNanld79QV2*&IE^%^0?E6SjB?mWWm>?PjPL>*aveNUR1#KsPc_i2RJhL zLdm++J+qtDHq%etw=M+|=H}dJWWv#)o+q>)Dq3HU+p+f8BtKJDHVWEo#e1U&jT3{D z_KPe6A@TfB2hgdEtqHR5$lvf^86(EHmOsv79ZyQB3wF|A;mnABOGWm-Bex_vQL`Y6 z4k==RhGgwe?y#=R9WXN)&vbqHMUmgC^m*Z3e)^{>zSq>uePJzsIJ-7gPWGI!t?A4= zd|J~`ozY;YpqsrvV_+K&EIZ}eV_3KZ3LL#?{HD-s(TFI2p0| z4MH+YuK8ZuX6mizyRR|xbuC?q{-Z(k&qd8Dm70-j{ip|LyYyR+X8~{JJYMn}GC(Io z&9wPUHt<8Z7M+|D1iQ~!XYk2m3qN4{Fm|*(VIo?@a~EJuY;P>(B#?aUiG^oPgFvW3 z^2w6P3&UcbD0k#HOc`dt!w%prxONnFk`DZH2dfnwlEJ63%5Q~HVj`M+Su}3$9qS0c zx#()H2KPWHqK(L0vF9C70n`XaH^d#|5Yzrd;Qk+;Ld8vPVk=l%NXdLR*TsvoGTtz@9$z8 zkqq9}Bzm5qTX#ldA)wd36|USa@Ct8fv4N0~@SxdaW*5BL~vXhp&Ma zv~dC_^~jVgVw1dafS1~Gxm6^yFM2o`XQ{un!#k0F;6l=^aAacOr(a)eEy53yt7Q*v zsdh0H#hm4Aw&&`sf ztjfKP6ax&emm?H6S0CJ;W`((qZ)x%tf69Hz<9nli144^Y11!|XX@0^g4w z&Rux*9aw@SjEm_`V?9~c{iauZ&YQZs?pHT;*ER|gNLMrfH*WMU3K0W6hxt(CR6K*| zg@(`~(-5wR9Gdzdwi1EXh95kd1V9~1G*3*P3q%Z6b1fy&VJ6ZddY_6j3ff>u&G=I- zT^npkuJ&}~LHoly|LJ!M0AodBntoC{ij`%f{W8lq5om!I;NRMzxdBId2i05nqM6ni zP_G|MK&pjbo#?8Jt?JIl0%XN~lOrVB7g|w6A7Jf`St1LSz{UT zX%pY43~kQ6jSb`i=&mx3Lj^Hlf!9Gyf>+du@{2}I;gQ_b(0TAzq&h)x@|Mz)3`J+8f#)LVhZ zhD`=(CY3l?zwdH7MpEiq9>hzmcf>D*#?(C2qoGx3D0iNPn;Y+7A;67>u=;NDN%iWd zMi1i0QE8zVu-#>k253@@Ao3y5856G_tmh$(@;`d!1zIdx!m?@mKZvHhJSFh%rPUy^ zfg%RcE57>CNgKxA*3~K{i^tx!sXm3NK2;=gnJsrlcvhYbJVaY#oJyJ*=f%;p#ckb; zc3Yhiaf$KnZIZ*hzum@lG-4>Y+W^`TJ>jDG_dGE)W3lni_XLE@S8-9sj;~KYWZJ*% z#E~|do?2YWXi>nCVIM7za{)AJ`8w;1c%QJx&wU)5n!Yl28`Ic(s~*&&juh_ zbtgn}68<<$zwqiA#Q}XhCqx3PBSdl-p{zpDMg$|}6Mc+wE7C8vc76n;Vp1Dv@KWqf zh%UXs14pA?Y%(&L1AlVPV+DU{VSiGg1rQ`cS>G0a4B($-27UHWU}VltK3h=__QN-v zR7~Z9X;UnjZ(`%ez6~fm8CJt9L1Z2lz`7$TKot&6>&Dj9mtOr$^!93(5|HSn$3H(4 zW}1GY{3e7wth%x**3m4OKyi=Gx9gDd{OYkkH4nw`w;Q&)Q*dl@GSI;?G(bexXyp6J z!;T6PK<%%s46F>{m2)_{z52GzH&Ok5LVoW?bz$aDF%IEo1C3I&fnlWt!!)yfgEm&U zTPhZ8XV7hfo2V*oGl{9p5`!Ohu#lFfQ*g6NKMXJzR!&+4UlGo7IO2>7uT6Pz00U>5 zDMaB6CTheucbrQL!tZ>c2oCRj%NZh~n=TsAkWsimaSZ*O<&u9XBLa-1-h}RP_@Wr3 zEc&^|26H!p83s*~2QYL;dkPSP=b#mtR{K7ExBJEowbo*ucSg^UjCvYCp;Nd+JGiyk zWSI2Fu-e;y^Q|0ki7Ir_xjcPsSZY4Iw#Kw{N|><)!{B!-2FpVlnX%K!d?u7=n;t`u z(VLOaDu{y2xr&Lwr}W*bi`CCSqS1$R2MYi<>6e+7ANb<+bs~_8A%IQQKLwGr>Ldjf z;2U1D4i?4E+x79RwKM0)p1%=Lm}M8^B{>EX^d%1T&@q9w!V*cW3D}6ch(JPt*e$$` zrq9QwMcjS3L;~&(l%2nJVrN2QdhRV6Y$N9DW+o)4k8fm^IU;^@k9_m9>{)7}0>TD5 zbtC!~nJ8fg_Z2Qa6P1>yJ0a5TUA~Xgus`dpB%Wi$$MyiIXeyB6IkwEDQ;CC$kZpcf zWN+~VT&8xyv9UQJE>AOmqEbt)FG;$}B|fC&0>U45lrZGj4Qm1dyLeA$1~M=16Vxp6 zwq=zh0wq6~!fiWreJ!h#TCFzS+6>T4Nc~!2aP%i#a;eX10da|eP@(Yu0?m|B0sbdqT|ZC11Oa1 zWOL{ptb9xTVDqs#MmAdBe1yL{uiw8Vjc7uN?zwyrAvPBEK~R=H=ASRI@DOdgZ{{P8 zB7qh=qC;$XP9I{-r(adS0_|1^<_r3xU>kSxJ3Y`lt)xm|B;s+v$uA}n>?8)RS*@iUlD zpG?bj<-@>ypU%j&SwPX`c-#fT&B8;&iIpbO5(s*NQ(tVSm{-PHLqEHq;_~;YCS8f@ zTk^r({@fL3#7MvdfZa8XeMAmxjou5U+@)<8g>dpqgo5p#8s07(KpC8oKc8`4o&y#i zau_hu^5X?3dMe;IuR&Rl5n8a=R*SH!rRUPBJP!ISWqL7+vr|9T(~>#JzxHB|eCy7R z{P5&N@~;6AiNhL$y`lsdnV)H^6AtEi_5{uJ#c_AB;?otTqFfr;cqRUPUyl>9UvoDz7GE{e^`77OJUqh@oFXf8_M>hD!_K2(^N)QYe+s<3rLPO*7APKB>APeOXapZ&*F8dMYhHZb3A7G$yX5L05Nm4-2b^ zjI}B;fRr)iAe~}9my?BZMY3Wp&mEs6q{9)$nI`Dd4ch{yHWraXkmc;Fs&iu8{eE*4 zEz8&QacZ4lf+xay6u9=MeOsE0(Jwa1ZlrYnT!6{IVjdYOXlK}d{V=h!uH7X}tqm#gQVd$dYFpL%hWjok>qdf@{#TZa73 zAJ%^1jGg>u9XB~cQmiNts!j;IE?&RQ6oB-IVxDb}yX7gTqx60feol$L2KEkCudtww z8V$U%=0rxM!k0GRBr|4t_63+6#gOB&Oq>+aUQ@tir#t=jetbkSDjV3zus9X3d~`mQ z@A%dOJ!9852OhZf*R)|E=>4{eS#IwQNw9SfexFlQzxv(i2}z6>q3)HSx)V-OL5r$f z7*`A4st<)?+dgxmhphb5O_F^IIt4>6dLbfS?^D^; z^-_zAnSgjMd4H!v1p2o8uAS}hyeas>h=S=t{u0EA;VE(B@2Q+%!KaaSY*FJ+Z@Xbs z{Ue1Mu&md?OMH`myq<~skOO`&ja2N(hx%vjAXPeNh11{hFwpjOzn2LTU1AmZ3|Ru+ z=^!(c0QPYuvOdSOmr8&oNFnAbRw1ys#grSU);4M#Q%G34>d!7@sGgFaj$S2!Wh{=t zEanVbOz86Pa;tN_=9h)_t!Fp$K{m_)_RSAz^4V=PK;G)so%i^xJc^`5*&_4b?@e>6>t5p2 zCt5>G=*-{yth2HN6%57o#jkT$z8E$+AwJ~pbJPDy`Ih41Tn~c76bvyRqES7vqS)#U zEDUqdhsh*)T19o-rEd$P?1MJynQHqv1r5E@EPbC`B_%cqB0FxtY;Tk z9nL7%|W_XRO zZE3b{7^vY%FrOr!a2zV9xVk^u%kwnQYd`M8ijMlX59h5Z8tUrvmwwV!yNHM&kns7f(%C`Mp3D^y-TM`=BgBP)>1ivT`bYAGMc`98M4mw2jjPA0A$n~ z+NbnmU4o1@Xop4v3Ytv91aD%mL2>nh_XSXaG6JbcljLM`WcTsY%5Bd3$o#NuV4Q`} z=o6bpzFopG-C&pqK3`seQkp>2&wV|cVVA2P=!GgRPrGz z=64DcHx1(bn)R{2(2LHPkmc|T2};*|7-K_p~HmzUcsapZx^a#dTraB@=$3Fw08b zpog>mLC;SOgbJkVa66O6L^;zGJG?cWn5d$`0d%f5t+J%`@=>#4h49Ds>q^oTVqK;~ z)a(3Ruu7B`$nX^1XTxN8nN`z}iL=s`j6NyuR)inyrrzZ9J<)nz Object.entries(marketplace).reduce((list, [_, store]) => { - store?.packages.forEach(({ manifest: { id, version } }) => { + store?.packages.forEach(({ id, version }) => { if ( local[id] && - this.emver.compare( + this.exver.compareExver( version, getManifest(local[id]).version || '', ) === 1 @@ -125,7 +125,7 @@ export class MenuComponent { @Inject(AbstractMarketplaceService) private readonly marketplaceService: MarketplaceService, private readonly splitPane: SplitPaneTracker, - private readonly emver: Emver, + private readonly exver: Exver, private readonly connection$: ConnectionService, ) {} } diff --git a/web/projects/ui/src/app/components/backup-drives/backup.service.ts b/web/projects/ui/src/app/components/backup-drives/backup.service.ts index 7e05178bf..837bebf9b 100644 --- a/web/projects/ui/src/app/components/backup-drives/backup.service.ts +++ b/web/projects/ui/src/app/components/backup-drives/backup.service.ts @@ -7,7 +7,8 @@ import { DiskBackupTarget, } from 'src/app/services/api/api.types' import { MappedBackupTarget } from 'src/app/types/mapped-backup-target' -import { getErrorMessage, Emver } from '@start9labs/shared' +import { Exver, getErrorMessage } from '@start9labs/shared' +import { Version } from '@start9labs/start-sdk' @Injectable({ providedIn: 'root', @@ -20,7 +21,7 @@ export class BackupService { constructor( private readonly embassyApi: ApiService, - private readonly emver: Emver, + private readonly exver: Exver, ) {} async getBackupTargets(): Promise { @@ -57,14 +58,15 @@ export class BackupService { hasAnyBackup(target: BackupTarget): boolean { return Object.values(target.startOs).some( - s => this.emver.compare(s.version, '0.3.6') !== -1, + s => this.exver.compareOsVersion(s.version, '0.3.6') !== 'less', ) } hasThisBackup(target: BackupTarget, id: string): boolean { return ( target.startOs[id] && - this.emver.compare(target.startOs[id].version, '0.3.6') !== -1 + this.exver.compareOsVersion(target.startOs[id].version, '0.3.6') !== + 'less' ) } } diff --git a/web/projects/ui/src/app/components/form/form-array/form-array.component.html b/web/projects/ui/src/app/components/form/form-array/form-array.component.html index 76c67f837..d66387fb5 100644 --- a/web/projects/ui/src/app/components/form/form-array/form-array.component.html +++ b/web/projects/ui/src/app/components/form/form-array/form-array.component.html @@ -28,7 +28,7 @@ [open]="!!open.get(item)" (openChange)="open.set(item, $event)" > - {{ item.value | mustache: $any(spec.spec).displayAs }} + {{ item.value | mustache : $any(spec.spec).displayAs }} diff --git a/web/projects/ui/src/app/components/form/form-datetime/form-datetime.component.html b/web/projects/ui/src/app/components/form/form-datetime/form-datetime.component.html index 05ffa69f3..37387a338 100644 --- a/web/projects/ui/src/app/components/form/form-datetime/form-datetime.component.html +++ b/web/projects/ui/src/app/components/form/form-datetime/form-datetime.component.html @@ -18,8 +18,8 @@ [disabled]="!!spec.disabled" [readOnly]="readOnly" [pseudoInvalid]="invalid" - [min]="spec.min ? (spec.min | tuiMapper: getLimit)[0] : min" - [max]="spec.max ? (spec.max | tuiMapper: getLimit)[0] : max" + [min]="spec.min ? (spec.min | tuiMapper : getLimit)[0] : min" + [max]="spec.max ? (spec.max | tuiMapper : getLimit)[0] : max" [(ngModel)]="value" (focusedChange)="onFocus($event)" > @@ -32,8 +32,8 @@ [disabled]="!!spec.disabled" [readOnly]="readOnly" [pseudoInvalid]="invalid" - [min]="spec.min ? (spec.min | tuiMapper: getLimit) : min" - [max]="spec.max ? (spec.max | tuiMapper: getLimit) : max" + [min]="spec.min ? (spec.min | tuiMapper : getLimit) : min" + [max]="spec.max ? (spec.max | tuiMapper : getLimit) : max" [(ngModel)]="value" (focusedChange)="onFocus($event)" > diff --git a/web/projects/ui/src/app/components/toast-container/refresh-alert/refresh-alert.service.ts b/web/projects/ui/src/app/components/toast-container/refresh-alert/refresh-alert.service.ts index 50cbc5a56..8cf5ce76d 100644 --- a/web/projects/ui/src/app/components/toast-container/refresh-alert/refresh-alert.service.ts +++ b/web/projects/ui/src/app/components/toast-container/refresh-alert/refresh-alert.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core' import { endWith, Observable } from 'rxjs' import { map } from 'rxjs/operators' -import { Emver } from '@start9labs/shared' +import { Exver } from '@start9labs/shared' import { PatchDB } from 'patch-db-client' import { ConfigService } from '../../../services/config.service' import { DataModel } from 'src/app/services/patch-db/data-model' @@ -9,13 +9,16 @@ import { DataModel } from 'src/app/services/patch-db/data-model' @Injectable({ providedIn: 'root' }) export class RefreshAlertService extends Observable { private readonly stream$ = this.patch.watch$('serverInfo', 'version').pipe( - map(version => !!this.emver.compare(this.config.version, version)), + map( + version => + this.exver.compareOsVersion(this.config.version, version) !== 'equal', + ), endWith(false), ) constructor( private readonly patch: PatchDB, - private readonly emver: Emver, + private readonly exver: Exver, private readonly config: ConfigService, ) { super(subscriber => this.stream$.subscribe(subscriber)) diff --git a/web/projects/ui/src/app/modals/app-recover-select/to-options.pipe.ts b/web/projects/ui/src/app/modals/app-recover-select/to-options.pipe.ts index 8a607896e..1c2d99f53 100644 --- a/web/projects/ui/src/app/modals/app-recover-select/to-options.pipe.ts +++ b/web/projects/ui/src/app/modals/app-recover-select/to-options.pipe.ts @@ -1,5 +1,5 @@ import { Pipe, PipeTransform } from '@angular/core' -import { Emver } from '@start9labs/shared' +import { Exver } from '@start9labs/shared' import { PackageBackupInfo } from 'src/app/services/api/api.types' import { ConfigService } from 'src/app/services/config.service' import { PackageDataEntry } from 'src/app/services/patch-db/data-model' @@ -19,7 +19,7 @@ export interface AppRecoverOption extends PackageBackupInfo { export class ToOptionsPipe implements PipeTransform { constructor( private readonly config: ConfigService, - private readonly emver: Emver, + private readonly exver: Exver, ) {} transform( @@ -44,7 +44,9 @@ export class ToOptionsPipe implements PipeTransform { } private compare(version: string): boolean { - // checks to see if backup was made on a newer version of eOS - return this.emver.compare(version, this.config.version) === 1 + // checks to see if backup was made on a newer version of startOS + return ( + this.exver.compareOsVersion(version, this.config.version) === 'greater' + ) } } diff --git a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts index dd4e4b36e..63c534bb6 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts @@ -150,7 +150,7 @@ export class AppActionsPage { try { await this.embassyApi.uninstallPackage({ id: this.pkgId }) this.embassyApi - .setDbValue(['ack-instructions', this.pkgId], false) + .setDbValue(['ackInstructions', this.pkgId], false) .catch(e => console.error('Failed to mark instructions as unseen', e)) this.navCtrl.navigateRoot('/services') } catch (e: any) { diff --git a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.html b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.html index 9173de39f..e353cf9b2 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.html @@ -11,7 +11,7 @@

{{ manifest.title }}

-

{{ manifest.version | displayEmver }}

+

{{ manifest.version }}

diff --git a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.module.ts b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.module.ts index 89998c9bf..aa4c5fcd6 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.module.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list.module.ts @@ -4,7 +4,7 @@ import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' import { AppListPage } from './app-list.page' import { - EmverPipesModule, + ExverPipesModule, ResponsiveColModule, TextSpinnerComponentModule, TickerModule, @@ -29,7 +29,7 @@ const routes: Routes = [ imports: [ CommonModule, StatusComponentModule, - EmverPipesModule, + ExverPipesModule, TextSpinnerComponentModule, LaunchablePipeModule, UiPipeModule, diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts index 86744cc91..8db082f42 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.module.ts @@ -4,7 +4,7 @@ import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' import { AppShowPage } from './app-show.page' import { - EmverPipesModule, + ExverPipesModule, ResponsiveColModule, SharedPipesModule, } from '@start9labs/shared' @@ -49,7 +49,7 @@ const routes: Routes = [ InstallingProgressPipeModule, IonicModule, RouterModule.forChild(routes), - EmverPipesModule, + ExverPipesModule, LaunchablePipeModule, UiPipeModule, ResponsiveColModule, diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html index 5b3123131..390c6c642 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.html @@ -7,8 +7,7 @@ @@ -34,9 +33,7 @@ - +
diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts index cb1f6f5f9..52855be3c 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/app-show.page.ts @@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' import { NavController } from '@ionic/angular' import { PatchDB } from 'patch-db-client' import { + AllPackageData, DataModel, InstallingState, PackageDataEntry, @@ -47,17 +48,19 @@ export class AppShowPage { private readonly pkgId = getPkgId(this.route) readonly pkgPlus$ = combineLatest([ - this.patch.watch$('packageData', this.pkgId), + this.patch.watch$('packageData'), this.depErrorService.getPkgDepErrors$(this.pkgId), ]).pipe( - tap(([pkg, _]) => { + tap(([allPkgs, _]) => { + const pkg = allPkgs[this.pkgId] // if package disappears, navigate to list page if (!pkg) this.navCtrl.navigateRoot('/services') }), - map(([pkg, depErrors]) => { + map(([allPkgs, depErrors]) => { + const pkg = allPkgs[this.pkgId] return { pkg, - dependencies: this.getDepInfo(pkg, depErrors), + dependencies: this.getDepInfo(pkg, allPkgs, depErrors), status: renderPkgStatus(pkg, depErrors), } }), @@ -81,17 +84,45 @@ export class AppShowPage { private getDepInfo( pkg: PackageDataEntry, + allPkgs: AllPackageData, depErrors: PkgDependencyErrors, ): DependencyInfo[] { const manifest = getManifest(pkg) return Object.keys(pkg.currentDependencies) .filter(id => !!manifest.dependencies[id]) - .map(id => this.getDepValues(pkg, manifest, id, depErrors)) + .map(id => this.getDepValues(pkg, allPkgs, manifest, id, depErrors)) + } + + private getDepDetails( + pkg: PackageDataEntry, + allPkgs: AllPackageData, + depId: string, + ) { + const { title, icon, versionRange } = pkg.currentDependencies[depId] + + if ( + allPkgs[depId] && + (allPkgs[depId].stateInfo.state === 'installed' || + allPkgs[depId].stateInfo.state === 'updating') + ) { + return { + title: allPkgs[depId].stateInfo.manifest!.title, + icon: allPkgs[depId].icon, + versionRange, + } + } else { + return { + title: title ? title : depId, + icon: icon ? icon : 'assets/img/service-icons/fallback.png', + versionRange, + } + } } private getDepValues( pkg: PackageDataEntry, + allPkgs: AllPackageData, manifest: T.Manifest, depId: string, depErrors: PkgDependencyErrors, @@ -103,11 +134,15 @@ export class AppShowPage { depErrors, ) - const { title, icon, versionSpec } = pkg.currentDependencies[depId] + const { title, icon, versionRange } = this.getDepDetails( + pkg, + allPkgs, + depId, + ) return { id: depId, - version: versionSpec, + version: versionRange, title, icon, errorText: errorText @@ -190,7 +225,7 @@ export class AppShowPage { const dependentInfo: DependentInfo = { id: pkgManifest.id, title: pkgManifest.title, - version: pkg.currentDependencies[depId].versionSpec, + version: pkg.currentDependencies[depId].versionRange, } const navigationExtras: NavigationExtras = { state: { dependentInfo }, diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.html index 73fc2158b..e524e13b5 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.html @@ -6,11 +6,11 @@

Version

-

{{ manifest.version | displayEmver }}

+

{{ pkg.stateInfo.manifest.version }}

License

-

{{ manifest.license }}

+

{{ pkg.stateInfo.manifest.license }}

Marketing Site

-

{{ manifest.marketingSite || 'Not provided' }}

+

{{ pkg.stateInfo.manifest.marketingSite || 'Not provided' }}

@@ -54,52 +54,52 @@

Source Repository

-

{{ manifest.upstreamRepo }}

+

{{ pkg.stateInfo.manifest.upstreamRepo }}

Wrapper Repository

-

{{ manifest.wrapperRepo }}

+

{{ pkg.stateInfo.manifest.wrapperRepo }}

Support Site

-

{{ manifest.supportSite || 'Not provided' }}

+

{{ pkg.stateInfo.manifest.supportSite || 'Not provided' }}

Donation Link

-

{{ manifest.donationUrl || 'Not provided' }}

+

{{ pkg.stateInfo.manifest.donationUrl || 'Not provided' }}

diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.ts index 278fa3d88..9aa152917 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-additional/app-show-additional.component.ts @@ -1,9 +1,12 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ModalController, ToastController } from '@ionic/angular' import { copyToClipboard, MarkdownComponent } from '@start9labs/shared' -import { T } from '@start9labs/start-sdk' import { from } from 'rxjs' import { ApiService } from 'src/app/services/api/embassy-api.service' +import { + InstalledState, + PackageDataEntry, +} from 'src/app/services/patch-db/data-model' @Component({ selector: 'app-show-additional', @@ -12,7 +15,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service' }) export class AppShowAdditionalComponent { @Input() - manifest!: T.Manifest + pkg!: PackageDataEntry constructor( private readonly modalCtrl: ModalController, @@ -35,16 +38,12 @@ export class AppShowAdditionalComponent { } async presentModalLicense() { - const { id, version } = this.manifest + const { id } = this.pkg.stateInfo.manifest const modal = await this.modalCtrl.create({ componentProps: { title: 'License', - content: from( - this.api.getStatic( - `/public/package-data/${id}/${version}/LICENSE.md`, - ), - ), + content: from(this.api.getStaticInstalled(id, 'LICENSE.md')), }, component: MarkdownComponent, }) diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.html index e9f7b97d7..b184b83ff 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-dependencies/app-show-dependencies.component.html @@ -15,7 +15,7 @@ > {{ dep.title }}

-

{{ dep.version | displayEmver }}

+

{{ dep.version }}

{{ dep.errorText || 'satisfied' }} diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-header/app-show-header.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-header/app-show-header.component.html index 06f517222..efe34b5ec 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-header/app-show-header.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-header/app-show-header.component.html @@ -9,7 +9,7 @@

{{ manifest.title }}

-

{{ manifest.version | displayEmver }}

+

{{ manifest.version }}

diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.html index 9b62a6402..ea80b3003 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.html @@ -7,10 +7,12 @@

, private readonly formDialog: FormDialogService, ) {} @@ -42,7 +42,7 @@ export class ToButtonsPipe implements PipeTransform { return [ // instructions { - action: () => this.presentModalInstructions(manifest), + action: () => this.presentModalInstructions(pkg), title: 'Instructions', description: `Understand how to use ${manifest.title}`, icon: 'list-outline', @@ -103,17 +103,20 @@ export class ToButtonsPipe implements PipeTransform { ] } - private async presentModalInstructions(manifest: T.Manifest) { + private async presentModalInstructions( + pkg: PackageDataEntry, + ) { this.apiService - .setDbValue(['ack-instructions', manifest.id], true) + .setDbValue(['ackInstructions', pkg.stateInfo.manifest.id], true) .catch(e => console.error('Failed to mark instructions as seen', e)) const modal = await this.modalCtrl.create({ componentProps: { title: 'Instructions', content: from( - this.apiService.getStatic( - `/public/package-data/${manifest.id}/${manifest.version}/INSTRUCTIONS.md`, + this.api.getStaticInstalled( + pkg.stateInfo.manifest.id, + 'instructions.md', ), ), }, diff --git a/web/projects/ui/src/app/pages/login/ca-wizard/ca-wizard.component.html b/web/projects/ui/src/app/pages/login/ca-wizard/ca-wizard.component.html index 49f65cc14..8f1582db1 100644 --- a/web/projects/ui/src/app/pages/login/ca-wizard/ca-wizard.component.html +++ b/web/projects/ui/src/app/pages/login/ca-wizard/ca-wizard.component.html @@ -97,7 +97,4 @@
-
+ diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts index c10ce42aa..b86d5df12 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.module.ts @@ -5,7 +5,7 @@ import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' import { SharedPipesModule, - EmverPipesModule, + ExverPipesModule, ResponsiveColModule, } from '@start9labs/shared' import { @@ -34,7 +34,7 @@ const routes: Routes = [ FormsModule, RouterModule.forChild(routes), SharedPipesModule, - EmverPipesModule, + ExverPipesModule, FilterPackagesPipeModule, MarketplaceStatusModule, BadgeMenuComponentModule, diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html index 734cb8910..32a6120ec 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.html @@ -62,9 +62,10 @@ > diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts index e1f13883a..0f0fa5752 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts @@ -1,6 +1,7 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { AbstractMarketplaceService } from '@start9labs/marketplace' +import { T } from '@start9labs/start-sdk' import { TuiDialogService } from '@taiga-ui/core' import { PatchDB } from 'patch-db-client' import { map } from 'rxjs' @@ -20,12 +21,21 @@ export class MarketplaceListPage { readonly store$ = this.marketplaceService.getSelectedStore$().pipe( map(({ info, packages }) => { - const categories = new Set() - if (info.categories.includes('featured')) categories.add('featured') - info.categories.forEach(c => categories.add(c)) - categories.add('all') + const categories = new Map() + if (info.categories['featured']) + categories.set('featured', info.categories['featured']) + Object.keys(info.categories).forEach(c => + categories.set(c, info.categories[c]), + ) + categories.set('all', { + name: 'All', + description: { + short: 'All registry packages', + long: 'An unfiltered list of all packages available on this registry.', + }, + }) - return { categories: Array.from(categories), packages } + return { categories, packages } }), ) diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-routing.module.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-routing.module.ts index d5d6304e4..cc4946dc1 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-routing.module.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-routing.module.ts @@ -17,13 +17,6 @@ const routes: Routes = [ m => m.MarketplaceShowPageModule, ), }, - { - path: ':pkgId/notes', - loadChildren: () => - import('./release-notes/release-notes.module').then( - m => m.ReleaseNotesPageModule, - ), - }, ] @NgModule({ diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.html b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.html index 6995dde5e..797a2fb13 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.html +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.html @@ -1,11 +1,15 @@
- View Installed + {{ + localPkg.stateInfo.state === 'installed' + ? 'View Installed' + : 'View Installing' + }} - - Install + + {{ localFlavor ? 'Switch' : 'Install' }}
diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts index 5c83c536f..0b76579bc 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-controls/marketplace-show-controls.component.ts @@ -10,7 +10,7 @@ import { MarketplacePkg, } from '@start9labs/marketplace' import { - Emver, + Exver, ErrorService, isEmptyObject, LoadingService, @@ -44,6 +44,9 @@ export class MarketplaceShowControlsComponent { @Input() localPkg!: PackageDataEntry | null + @Input() + localFlavor!: boolean + readonly showDevTools$ = this.ClientStorageService.showDevTools$ constructor( @@ -52,7 +55,7 @@ export class MarketplaceShowControlsComponent { @Inject(AbstractMarketplaceService) private readonly marketplaceService: MarketplaceService, private readonly loader: LoadingService, - private readonly emver: Emver, + private readonly exver: Exver, private readonly errorService: ErrorService, private readonly patch: PatchDB, ) {} @@ -79,7 +82,7 @@ export class MarketplaceShowControlsComponent { const localManifest = getManifest(this.localPkg) if ( - this.emver.compare(localManifest.version, this.pkg.manifest.version) !== + this.exver.compareExver(localManifest.version, this.pkg.version) !== 0 && hasCurrentDeps(localManifest.id, await getAllPackages(this.patch)) ) { @@ -136,9 +139,9 @@ export class MarketplaceShowControlsComponent { private async dryInstall(url: string) { const breakages = dryUpdate( - this.pkg.manifest, + this.pkg, await getAllPackages(this.patch), - this.emver, + this.exver, ) if (isEmptyObject(breakages)) { @@ -152,7 +155,7 @@ export class MarketplaceShowControlsComponent { } private async alertInstall(url: string) { - const installAlert = this.pkg.manifest.alerts.install + const installAlert = this.pkg.alerts.install if (!installAlert) return this.install(url) @@ -179,7 +182,7 @@ export class MarketplaceShowControlsComponent { private async install(url: string) { const loader = this.loader.open('Beginning Install...').subscribe() - const { id, version } = this.pkg.manifest + const { id, version } = this.pkg try { await this.marketplaceService.installPackage(id, version, url) diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.html b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.html index a7e5f9cb6..631b45018 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.html +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.html @@ -13,16 +13,16 @@

- {{ title }} version {{ version | displayEmver }} is compatible. + {{ title }} version {{ version }} is compatible. - {{ title }} version {{ version | displayEmver }} is NOT compatible. + {{ title }} version {{ version }} is NOT compatible.

diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.ts index 76c648867..7c39714f5 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show-dependent/marketplace-show-dependent.component.ts @@ -24,10 +24,10 @@ export class MarketplaceShowDependentComponent { constructor(@Inject(DOCUMENT) private readonly document: Document) {} get title(): string { - return this.pkg.manifest.title + return this.pkg.title } get version(): string { - return this.pkg.manifest.version + return this.pkg.version } } diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts index 058cbb158..7d0d3995b 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.module.ts @@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common' import { RouterModule, Routes } from '@angular/router' import { IonicModule } from '@ionic/angular' import { - EmverPipesModule, + ExverPipesModule, MarkdownPipeModule, SharedPipesModule, TextSpinnerComponentModule, @@ -13,6 +13,7 @@ import { AdditionalModule, DependenciesModule, PackageModule, + FlavorsModule, } from '@start9labs/marketplace' import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module' import { MarketplaceShowPage } from './marketplace-show.page' @@ -35,13 +36,14 @@ const routes: Routes = [ RouterModule.forChild(routes), TextSpinnerComponentModule, SharedPipesModule, - EmverPipesModule, + ExverPipesModule, MarkdownPipeModule, MarketplaceStatusModule, PackageModule, AboutModule, DependenciesModule, AdditionalModule, + FlavorsModule, UiPipeModule, ], declarations: [ diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html index f563e88d6..a0f7622ad 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.html @@ -1,21 +1,14 @@ - + -
+
-

- {{ pkgId }} @{{ version === '*' ? 'latest' : version }} not found in - this registry -

+

{{ pkgId }} not found in this registry

@@ -25,21 +18,26 @@ [url]="url" [pkg]="pkg" [localPkg]="localPkg$ | async" + [localFlavor]="!!(localFlavor$ | async)" > + diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.ts index 876e71924..018ddbb64 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-show/marketplace-show.page.ts @@ -1,11 +1,15 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' -import { ActivatedRoute } from '@angular/router' -import { getPkgId } from '@start9labs/shared' -import { AbstractMarketplaceService } from '@start9labs/marketplace' +import { ActivatedRoute, Router } from '@angular/router' +import { Exver, getPkgId } from '@start9labs/shared' +import { + AbstractMarketplaceService, + MarketplacePkg, +} from '@start9labs/marketplace' import { PatchDB } from 'patch-db-client' -import { BehaviorSubject } from 'rxjs' -import { filter, shareReplay, switchMap } from 'rxjs/operators' +import { combineLatest, Observable } from 'rxjs' +import { filter, map, shareReplay, startWith, switchMap } from 'rxjs/operators' import { DataModel } from 'src/app/services/patch-db/data-model' +import { getManifest } from 'src/app/util/get-package-data' @Component({ selector: 'marketplace-show', @@ -17,21 +21,61 @@ export class MarketplaceShowPage { readonly pkgId = getPkgId(this.route) readonly url = this.route.snapshot.queryParamMap.get('url') || undefined - readonly loadVersion$ = new BehaviorSubject('*') + readonly localPkg$ = combineLatest([ + this.patch.watch$('packageData', this.pkgId).pipe(filter(Boolean)), + this.route.queryParamMap, + ]).pipe( + map(([pkg, paramMap]) => + this.exver.getFlavor(getManifest(pkg).version) === paramMap.get('flavor') + ? pkg + : null, + ), + shareReplay({ bufferSize: 1, refCount: true }), + ) - readonly localPkg$ = this.patch - .watch$('packageData', this.pkgId) - .pipe(filter(Boolean), shareReplay({ bufferSize: 1, refCount: true })) + readonly localFlavor$ = this.localPkg$.pipe( + map(pkg => !pkg), + startWith(false), + ) - readonly pkg$ = this.loadVersion$.pipe( - switchMap(version => - this.marketplaceService.getPackage$(this.pkgId, version, this.url), + readonly pkg$: Observable = this.route.queryParamMap.pipe( + switchMap(paramMap => + this.marketplaceService.getPackage$( + this.pkgId, + paramMap.get('version'), + paramMap.get('flavor'), + this.url, + ), + ), + ) + + readonly flavors$ = this.route.queryParamMap.pipe( + switchMap(paramMap => + this.marketplaceService + .getSelectedStore$() + .pipe( + map(s => + s.packages.filter( + p => p.id === this.pkgId && p.flavor !== paramMap.get('flavor'), + ), + ), + ), ), ) constructor( private readonly route: ActivatedRoute, + private readonly router: Router, private readonly patch: PatchDB, private readonly marketplaceService: AbstractMarketplaceService, + private readonly exver: Exver, ) {} + + updateVersion(version: string) { + this.router.navigate([], { + relativeTo: this.route, + queryParams: { version }, + queryParamsHandling: 'merge', + }) + } } diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.html b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.html index 8958cdda2..173fda66d 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.html +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.html @@ -1,13 +1,13 @@ - +
Installed Update Available diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.ts index 9db50dc29..6686b2842 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.component.ts @@ -8,6 +8,7 @@ import { isRestoring, getManifest, } from 'src/app/util/get-package-data' +import { Exver } from '@start9labs/shared' @Component({ selector: 'marketplace-status', @@ -16,8 +17,7 @@ import { }) export class MarketplaceStatusComponent { @Input() version!: string - - @Input() localPkg?: PackageDataEntry + @Input() localPkg!: PackageDataEntry isInstalled = isInstalled isInstalling = isInstalling @@ -26,6 +26,15 @@ export class MarketplaceStatusComponent { isRestoring = isRestoring get localVersion(): string { - return this.localPkg ? getManifest(this.localPkg).version : '' + return getManifest(this.localPkg).version } + + get sameFlavor(): boolean { + return ( + this.exver.getFlavor(this.version) === + this.exver.getFlavor(this.localVersion) + ) + } + + constructor(private readonly exver: Exver) {} } diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.module.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.module.ts index c31f4fd16..d95b919a2 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.module.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-status/marketplace-status.module.ts @@ -1,7 +1,7 @@ import { CommonModule } from '@angular/common' import { NgModule } from '@angular/core' import { IonicModule } from '@ionic/angular' -import { EmverPipesModule } from '@start9labs/shared' +import { ExverPipesModule } from '@start9labs/shared' import { InstallingProgressPipeModule } from '../../../pipes/install-progress/install-progress.module' import { MarketplaceStatusComponent } from './marketplace-status.component' @@ -9,7 +9,7 @@ import { MarketplaceStatusComponent } from './marketplace-status.component' imports: [ CommonModule, IonicModule, - EmverPipesModule, + ExverPipesModule, InstallingProgressPipeModule, ], declarations: [MarketplaceStatusComponent], diff --git a/web/projects/ui/src/app/pages/server-routes/lan/lan.page.html b/web/projects/ui/src/app/pages/server-routes/lan/lan.page.html index b61412445..c6c16d28e 100644 --- a/web/projects/ui/src/app/pages/server-routes/lan/lan.page.html +++ b/web/projects/ui/src/app/pages/server-routes/lan/lan.page.html @@ -35,5 +35,5 @@ - + diff --git a/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.module.ts b/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.module.ts index eff288eb2..503aa57ad 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.module.ts +++ b/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.module.ts @@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common' import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' import { ServerSpecsPage } from './server-specs.page' -import { EmverPipesModule } from '@start9labs/shared' +import { ExverPipesModule } from '@start9labs/shared' import { TuiLetModule } from '@taiga-ui/cdk' import { QRComponentModule } from 'src/app/components/qr/qr.component.module' @@ -20,7 +20,7 @@ const routes: Routes = [ IonicModule, RouterModule.forChild(routes), QRComponentModule, - EmverPipesModule, + ExverPipesModule, TuiLetModule, ], declarations: [ServerSpecsPage], diff --git a/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.html b/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.html index f9df6eb37..03d7ef3d7 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.html +++ b/web/projects/ui/src/app/pages/server-routes/server-specs/server-specs.page.html @@ -13,7 +13,7 @@

Version

-

{{ server.version | displayEmver }}

+

{{ server.version }}

diff --git a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.module.ts b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.module.ts index 863b2d127..27a3757dd 100644 --- a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.module.ts +++ b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.module.ts @@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { SideloadPage } from './sideload.page' import { Routes, RouterModule } from '@angular/router' -import { EmverPipesModule, SharedPipesModule } from '@start9labs/shared' +import { ExverPipesModule, SharedPipesModule } from '@start9labs/shared' import { DragNDropDirective } from './dnd.directive' const routes: Routes = [ @@ -19,7 +19,7 @@ const routes: Routes = [ IonicModule, RouterModule.forChild(routes), SharedPipesModule, - EmverPipesModule, + ExverPipesModule, ], declarations: [SideloadPage, DragNDropDirective], }) diff --git a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.html b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.html index 6a54fe82e..0d413075e 100644 --- a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.html +++ b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.html @@ -77,7 +77,7 @@ [src]="toUpload.icon | trustUrl" />

{{ toUpload.manifest.title }}

-

{{ toUpload.manifest.version | displayEmver }}

+

{{ toUpload.manifest.version }}

diff --git a/web/projects/ui/src/app/pages/updates/updates.module.ts b/web/projects/ui/src/app/pages/updates/updates.module.ts index 7b6c6594c..4930463b0 100644 --- a/web/projects/ui/src/app/pages/updates/updates.module.ts +++ b/web/projects/ui/src/app/pages/updates/updates.module.ts @@ -5,7 +5,7 @@ import { RouterModule, Routes } from '@angular/router' import { FilterUpdatesPipe, UpdatesPage } from './updates.page' import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' import { - EmverPipesModule, + ExverPipesModule, MarkdownPipeModule, SharedPipesModule, } from '@start9labs/shared' @@ -34,7 +34,7 @@ const routes: Routes = [ RoundProgressModule, InstallingProgressPipeModule, StoreIconComponentModule, - EmverPipesModule, + ExverPipesModule, ], }) export class UpdatesPageModule {} diff --git a/web/projects/ui/src/app/pages/updates/updates.page.html b/web/projects/ui/src/app/pages/updates/updates.page.html index 2bb3860c6..fd4d19133 100644 --- a/web/projects/ui/src/app/pages/updates/updates.page.html +++ b/web/projects/ui/src/app/pages/updates/updates.page.html @@ -30,27 +30,21 @@ >
- + -

{{ pkg.manifest.title }}

+

{{ pkg.title }}

- - {{ local.stateInfo.manifest.version | displayEmver }} - + {{ local.stateInfo.manifest.version }}     - - {{ pkg.manifest.version | displayEmver }} - + {{ pkg.version }}

-

+

{{ error }}

@@ -69,17 +63,17 @@ - {{ marketplaceService.updateErrors[pkg.manifest.id] ? - 'Retry' : 'Update' }} + {{ marketplaceService.updateErrors[pkg.id] ? 'Retry' : + 'Update' }} @@ -88,12 +82,12 @@
What's new
-

+

View listing diff --git a/web/projects/ui/src/app/pages/updates/updates.page.ts b/web/projects/ui/src/app/pages/updates/updates.page.ts index d1ffc4829..e25632bc8 100644 --- a/web/projects/ui/src/app/pages/updates/updates.page.ts +++ b/web/projects/ui/src/app/pages/updates/updates.page.ts @@ -13,7 +13,7 @@ import { MarketplacePkg, StoreIdentity, } from '@start9labs/marketplace' -import { Emver, isEmptyObject } from '@start9labs/shared' +import { Exver, isEmptyObject } from '@start9labs/shared' import { Pipe, PipeTransform } from '@angular/core' import { combineLatest, map, Observable } from 'rxjs' import { AlertController, NavController } from '@ionic/angular' @@ -24,7 +24,6 @@ import { isUpdating, } from 'src/app/util/get-package-data' import { dryUpdate } from 'src/app/util/dry-update' -import { T } from '@start9labs/start-sdk' interface UpdatesData { hosts: StoreIdentity[] @@ -59,7 +58,7 @@ export class UpdatesPage { private readonly patch: PatchDB, private readonly navCtrl: NavController, private readonly alertCtrl: AlertController, - private readonly emver: Emver, + private readonly exver: Exver, ) {} viewInMarketplace(event: Event, url: string, id: string) { @@ -70,29 +69,29 @@ export class UpdatesPage { }) } - async tryUpdate(manifest: T.Manifest, url: string, e: Event): Promise { + async tryUpdate(pkg: MarketplacePkg, url: string, e: Event): Promise { e.stopPropagation() - const { id, version } = manifest + const { id, version } = pkg delete this.marketplaceService.updateErrors[id] this.marketplaceService.updateQueue[id] = true - // manifest.id OK because same as local id for update - if (hasCurrentDeps(manifest.id, await getAllPackages(this.patch))) { - this.dryInstall(manifest, url) + // id OK because same as local id for update + if (hasCurrentDeps(id, await getAllPackages(this.patch))) { + this.dryInstall(pkg, url) } else { this.install(id, version, url) } } - private async dryInstall(manifest: T.Manifest, url: string) { - const { id, version, title } = manifest + private async dryInstall(pkg: MarketplacePkg, url: string) { + const { id, version, title } = pkg const breakages = dryUpdate( - manifest, + pkg, await getAllPackages(this.patch), - this.emver, + this.exver, ) if (isEmptyObject(breakages)) { @@ -159,18 +158,19 @@ export class UpdatesPage { name: 'filterUpdates', }) export class FilterUpdatesPipe implements PipeTransform { - constructor(private readonly emver: Emver) {} + constructor(private readonly exver: Exver) {} transform( pkgs: MarketplacePkg[], local: Record>, ): MarketplacePkg[] { - return pkgs.filter(({ manifest }) => { - const localPkg = local[manifest.id] + return pkgs.filter(({ id, version, flavor }) => { + const localPkg = local[id] return ( localPkg && - this.emver.compare( - manifest.version, + this.exver.getFlavor(localPkg.stateInfo.manifest.version) === flavor && + this.exver.compareExver( + version, localPkg.stateInfo.manifest.version, ) === 1 ) diff --git a/web/projects/ui/src/app/services/api/api-icons.ts b/web/projects/ui/src/app/services/api/api-icons.ts index 3b0b08b33..55c3172da 100644 --- a/web/projects/ui/src/app/services/api/api-icons.ts +++ b/web/projects/ui/src/app/services/api/api-icons.ts @@ -1,8 +1,10 @@ +const REGISTRY_ICON = + 'data:image/png;base64,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' const BTC_ICON = - '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' + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAABAKADAAQAAAABAAABAAAAAACU0HdKAAAACXBIWXMAAAsTAAALEwEAmpwYAAABWWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgpMwidZAABAAElEQVR4Ae29CYBcRZ34X/Ved8+Ri5yTTEIIEM5wHwreKCICiq4Sd5V1V92VFZIo+Nd1V3cZ1mO9ViUJsK5/xRNX4omLosjlhYqCIDcBAkkm931Mn69+n2/Ve5OeyRzdM9093dOvkjfv9Tvq+FZ9v/W96ltaxWlcQ8AopQ9qIDcHTAe/KR8P9vaAWcQ3GwsCA3R5YzWgmWtrkVvQ8xrw9PgQ0R+110Y9CuJerYzWo0Pgg8oozl+ATxlSckwoGnMkxgSgAfqtFwlXKQ/Elj4LVBfIXeLsbG5WvtrBt1MXabVvn6eyWU/tT6X2+unkxPZEQe1vzavMnsI6PxnMm5owag/H9tUF3aXypYLHCBkorl8FiE+pZcfvjRwCMQEYOeyq9mVXl/Kujmb0S1Qw2Cxuke4rMyZm9rbONjrf6Wl/VmCCQ5TW0/lmBs9nQCNm8vsQbVQrFW6BmMg5xeFzeByCugF/hZcocAjS55jTd/NoJ892aqN3yrXR/FZ6C3lvCkxhcyanNx3S7m1W7+7uGbKOQhgkDdEW+zz+U3MIxASg5iDvW6Cd3btAM0F42HZm3aDvG0oJQfjgzM65OtBHaRMcFSh9JAg3n/fm89UcE6jJ/J7UktBJneAuPxTYb1Fa0FoOOUVn+6P3NlmQ+FM8GCSL3hvRdZhvLmtUYNR+jn1IGHuo9WbeXcMXT5DrahPoZwKTf3ri+zZv4l6fJERr1WLlXXIJtwdpb58P4h9VhUBxn1e1oDjzAxAQJAjZ5QERvufazvnaL5yklb8oCMwi3j4OJJvLeUZri05ydtgLqRCkLoCJBc5cC/EQNJfD9a17dzT9LHlFyeWrlceUrn3+eBAFSyxkjpcD/iGdM/u52sbL3RT8qPbU/bkgeHhiyntYX9a9NcosOptL4EaEIMQcQgSSmp1HMzBqVsnxUJBhFh9olhf5fO+GjuN8z19EO8/geAGdstDTqjPVypWgNEeBIw+Wg1QF7greS9/JIQks5K8QlhokRwUskZHS5KckOcvhUXcv4TsqYWtErdJpg1ih1vH7Cd66zxjz52wh+PMhV256hvu9aTA49b4QX1QUAjUZMBWtcQNlFg3ma2B1YeMFlW3ac0PHrGTeO5VZ/Rzj6VfCRh8N+z4llMhVAUk8VxActzK59JHrJ0HzGiG5q+mI/1piAGcAnrs8aICfStCAkFMwwinkzRZ0Ew8rz9zOW79J53oemnrlTvQMLkXwi0WFCCKVP8cEoMIw7VXg9ZNvMys6jwUXXgkVeDVAPxX2+bBkiiuH7CJTO5R39RFUbxRkLxWClijwckgSlE76yvPhFITwpTNWgFlNq/+kdHAbNO/utiUbnosyvxkxweoNYjEhAklFzkA/TqOFACNaq5vt/K30Yjtr2yzTy2cdiaT8aqa91wXGvKitRR8iDwKQPpu3c6NwBdIH0SGPmyYBNzFlRpyRDxcEuGg+D3qyAQpE7zdwEbfwSh9iYM2aEFjVZb+PCErTwK2SDY0JwCigCQpbZV4x0gt7nygkXou0/iaG9ovbWvU0KSKXY4oPeomDwF2Y4ThFEBB+x5kiLUInfe37YtEgwR1sQpb4DXTyu7m897MpV63b7p5AK8THIeYKInCUfY4JQNkgY9B1gbyY7SLEl9890zvPRp59GyThYjT1nZJtHqTPH0B6QfgY3qXDWziDkBgo308COu6ks+Y5CO+PVODd1P6+db+Psou4goHMqNE78flgCMQD8mCYDHqn/yDbe93M2Ykg+VeM0rfy0dkgvldAuYUCTxxqJMVI7+Aw2r+9xABFou/BGWBVyKNi/E3gqZswlf5g0rKNW6QQS5wfgTiv6uW2Rlv2uP4+JgAldK9F/MV45IUz0p7PzV6UTHpvB/H/pjWlD5UsmJnkJIgvMI3Ze4FGdVJEDHxgb0vA72AN2oBve8p8vWVp9+Nyk96wepmIS6tOVRo/15gADNGH/RG/Z/mclzK3vINP3tTaqicXckWzPWZvRl0MzyHgWdFHTmcgxECl0Bd4SaUyadyVlfoeK6C+3L5k/b3ybCA9jdyPk4NAPGAHGAkW8YsUS7nrO8/NF/RVDKfzYfN11rnCxrP9ALAbo1uWK8C06ifRFaA0lPUMP4MJ+Fzb0vV3Sp1iQjBwz8QEoAguIj9ew29s+XZm6Vkx7xVaB1cxeC4SxGdgyduC+LFsL5CovyQdJH3n019CCLC+qh/DLPxX27INv5LqWh0B51hZKNBw8qq7auK//WeHnuWzX661vwyrlGj0/V7Ej9n8xhglB8SDiBDkEM9+gMFwRdsV3b+WRvTn8hqjYZWvZVNzAP0VRbu/0HlM0jf/Cl28FMT3YsSv/ICraY4hIWCQ+y2OI8ByoL6pPf2frUvWPyl16a/nqWn96qCwpiUA0vGRhth8fsEh2URuKTP+lS2teiomJumaAiq9WLFXB4N01FUo5ghYYEX/bg9U8IX2VGa5vmzHLsm/eDyMurwGyqDpCMDB7H7nO0H0f2HGXxh66+X57cca/QYaxaVW1RGCAsrCRKgsfEpp81HWHHxDsmhGsaCpCEAxlc8u7zyVdbWfxJZ8HgE1VCZvnXdi5V6pyNTA78Hf2TUIrD3wJZZBJmd+UVDmXyYs3fDHiBBE3GEDN7OkqjcFAbCz/jVY8NHum8/Na0sngw8CnQ8x67eKpjiEVOy8U9KQGVcv2b4XfU8ma9KMk09uzXmfPvSqdT3WWnA1hEK4hnGcxj0BKJ71e66dzfp777OsyjtNbPl0eJ7eDZecjONejps2HATyBDFJpPAs7Emr+7QufLBt6ca75aPi8TNcJo34fNwSABC71xXUfL1jQmaX91Ec9d7XmsSen4vZ/UYcrFWus8z0AePDF3EwCNR1bX7uw/qKLXutbqDIFbzK9ahp9uOS7RX2DcpmRI7bv3zuWendibtaWrwrgWyE/LLqfNwSv5qOoPFTmIwHXyYHOEOfZdzLeoLkPdnls18g40jGU+RENH6aPA6R4K4ulTinC9YeIpCd3vn/IeT9O1R9Qjzrj6dhW/W2iJKw0JLSCXQDewNtrm5fsuFzUup4EwnGzSxYzPL33DB7gcp5N7Bg5/wiv32Z9eMUQ6AcCBRgJf0U6wsgBLcEWbWk/f3da8eTSDAuRICINRNWLX3dnPNN3vu1ID8afomYLbJdjPzlDPv43QgCPuMnYKl3AU/C1+uU+q2ML2siZFRF4y56uRHPDc8BFLNk+1d2/jNReT5B0AiPmHuyIizW8DfiqKzPOucZVwnGVYEdkj44XkSChiYAUOAEtv28uX7+1HQhvxKnnrfi1CHmPVmxF8/69YlIjVyrAn4BNngpOqUbW9OFpfoDm/YVT0KN1riGJQAR8mdWzl4UGO/bsPwn4uMdL9VttBHYePV1zkOt2utJB3/wtPe3srAoGo+N1pyGIwDM7lq2wxavvp5r575K+eYmtPyzkPfFhz9m+RttBDZgfUWpBOLk4TgT6Ac2KhP8dduyjfc0onKwoQiAKF0E8WXM9KzofAenGwgf3UIQzljeF6DEqdYQiPQC+yj43W1Lu2+yExQ/GsWFuGGsAMXIn1nZ+e8JT30FE40gv7D98cxf66EflycQsEpBVhdOYCx+a//yzg8I4ssh47URQNQQHECxkoWZ/39YvPGPsF5i4BNurCEA3QiDIa7jiCEgXKmHKMrKQnVt69L175OciietEedc5Q/rngBEyC/n9ObOryF3vS1cwSd1r/v6V7n/4uzrBwIyGRmU0R7K6BsRB94pVZM9DRfX8R4FdY1AEfJv/EzHhENa/W/ijPEGce4BrjLr13XdpfPj1HQQECIQwKFKHMmbWlPdf68vU7loHNcjNOoWiSKgbf/k1Clt7W03t7bp86yZLw7TVY/jKK5TBAEXdcgSgUwm+H6LmfBWvWx1JhrP0Wv1cq5LAhAByyF/649aW72XWzNfrOyrl3ET12MoCBQRgZ6MuaUtXXhrvToM1Z0CLUJ+A9vfNqHtuzHyDzXS4md1CQG3Q5SIAXmWFb8+3erdbG5c0CprCEQnUE91risOIEL+7i92tk/LmVUtKe+CeOavp+ES12UEEMijE2BZcfDdlq0b3iJ+LPVkHagbDsACBQrJOTEtq75FAI8Y+Ucw2uJP6gsCaAUTTGLEFvDenJ4+50apXUQE6qGmdUEAunCaiDz80tM7v2G1/c6vP3bwqYdREtdhxBCwLDaKayECrS3e29MrOpdLZpYI1MFmsmMuAkAhRWViXSf3r+i8joCdlwuwuBtvyjHiYTfIh7qVBwDb5Dhbj+pBXoxvVxwCjHKWqgdMbqIb+CR+Av8iXK/q4r7tlIqXWFKGY88B3MzsD3DSKzs/0Ja0yG+9qhinY06cSoJgQ7wE0ovuKfsM+5k/y3USijsFbwoOolwIDY5TlSHAeKYXhBMw7EfwITiBy4ULuLtrbJWCY9rzdyHvn8N6/p4Vcy6FCnyD6CuS5O+Y1svWYlz9AfnNBuXN+3sm/13KbF7FbhjcgtRqaIFKHMFFi4O66eGBcAiuM8YVGOqjMQHrBjyiDue1513cumTdT+AEbFyLsajemCHaAeRnC24V/MTzVFuBDdsAwthzJWPRE9UqU6NGKYDQrZ5qefOdTPzzVbD9ORVsflyZTU+pYOOdymz7KdxBMUFYSC8IZSBZgsDDOFUSAgVWsfrZgtkRGH3OxGXrH4wsYJUspJS8xoQARI1Nr5x7NBu4/7LF1x0AI47iU0qPlfuObgO5n1Z6/rtUyxuvB7GF5T+QTAaOYM9WRxA2QxA23KHM1v/rRxA6D3wQX1UKAgXZg4BFbU8Rw/Kc9qvWrR+LdQM1JwCwO1bjb7D1p7PqDhb3nAUQ4vX8lRpW/fPxJiuz/3GVOHuFSr54CTN6xNrLGWar3wgw6Z3K7N0GQXhMBXAIZuM9ymxnyzz7WfRt/0Li3yOEQJ7FQ4l0j7mtdXv3haITkHgCtYwlUFN2m+Gj1dVuKKUzLJtsjZF/hAOnjM+c45k3+5TwG+kFsF6HyC8EwR5IX/Ko9RDlzThSJY6/SKXOuVIlX/lR9IfreSYiwXDDRfJFl9CfqpRR2yZ7NcH6FiEC5/dM6/ykbTvRrmoJg+F6tLJ1CTX++1fMeS/eUf9A40Xvh5Aap6pAQOT//E6lJ89WemrExvcbX5YYDEAQApHIwPt0Wqm9XPjt/BEVzWCJPER5aK0MiB1iYfAOgRZwXdsxPVgF6/W+iAEm6asP7Lm286+FCxD9WK0qWzMC0Cv3L+88FyXoZyR6r6R+w7FW7W6ScpD38+uVnn4xuDjPtVkQfqhUTBB4z6y/L5QahkN+rAeJWUrPejMfrVZm32Mcj1r9g1IQD0sQJscE4WDYix+MRQYiC12fWdFx4jlYxgRfDn618ndqQgCs3C9uvtfNnE1Lv9SSUEm4zmhdf+VbFefoIAAHIKY+b85J/IEYOJpbGnSEELAVTrDpMaVlKFod7SCfCtuf3650x1kq9cb/Vqm3rVXJ19+uEmcuV3ruO/gIXQIEQaGLEIWkUhGHMCkkCDUZhoNUvi5ue/lAFdpSeirbEn5NVsHKwiHBm2rXruqsBmPOyf1dSqWD5BeQdxbgDBEr/ardszb/SP4/OSxNZvESxpToBCAAZg8Bb7f+iJldxAdEgcESHobC/fudL4bjn265fm8aHMcx5yqT3aPM7o+oYNvTKuh+CB+Ev5Dn15SSMJrCjEgVE3NseYNl3yT3RRQQfcCpgWr7hFI7rqhFu6tOAFSX8hlLefF8aknqt1g3X7q8Fo1r6jJk2i7sUXpSC+z/IPL/oAASVgHvjJ2blNm1Fg7+eLiBnYO8DRbjzCrFebOOdu+I/kCUjCSdmqT0jEkoFo+AILwaQrEXwtKF78GzKlj3R6wNEIWd96A7EOJUDotisx9ff9h0BL2YIeDt5bjF/1Iv7f6O6ANEJKhWQ6uKiKHcn88un3ty3piPE8FX2iF0P05Vh4DI/9j/57wL5x9mWEklQ94hr+n+k0VJbaU1l8VBfy2h2aDU5EXKmz7fPbYWhqgw+lw4irBwnZyo9LSJSk1boPyjzoGwpFX+oe+p/J2XKtUCATGicWzS5Nzf8RS0lsDP9qyc87u2JRuesyI0ysFqQKUEfnBkxdLn2soxn5vXVtBGFvkcgqdfLPePDJzlfyXyP9D25sD++ywCKof2Cu6K/L/hz24iH0r+l3UFhe0g/9lwGx2unqI/6E1cRwTB3heCwFgWooA/LLF0cUsuOBWD9Uvu/bBZLzzZ5wIr2TwT6GsFCGIZAFrFQK0YbKpGANQqJ2ymE4V/pTEvRr4R5BeJL041gYBvR4zXeXpYmqUAw5dsZ2vwc/cG5PZbQ/k/O/h3LCYS+V93ngUyQwyGLSYiCGSJ/zf7OaMbwNHIjsQhyhm8BuPxicQQCFAKXpy+ttPpAjChV6OhVck0Mvntu3bO6fABV2WzdlRUpaxqAKXh87RsOVq2SRhce+X/UlvlMDjYuVGpXesgAFNB6qFEUAiNlf+PDQsokVMNCYXZh+fhlq+gK5hPOZgS49QLgTwic6DVf6SvnXdUtawCFUdK+tWx/l3Qd60/g6tvO/yLzP5VYWF6oVW3FzLSBUNAJE/MXsjm1SHmRRDALJd7DuXbu60TkH1QMvTdi2YjyjmLpNJ1gyTraAShmHwsvgaHhi+VXJB93+x4HqUgxEocjUQ0iFMEATENSkzBaawd/Li9GXrRRi9U4lxxAqBCViUzY84yWP9zYtYfEJt96LYewSf/CRRza+g3iIA3nQNlmHWdrbBkFNn/Z58Qyv+CySUipsjp8PQBBEBm9qFnf1j+/FaQ/6XQtkjRWGI5ZC2psB5CY/G+8kPRldDAf8UqgCiQ8PUlbDv2N3SNEatAJVtUUahbbSUODOnrO48xRnfl8nYKKW9EVLJ1Y52XZcU3o4U/RSXPW6USp34CDfiFINUzzHrYw/c/CQI9Sy3pBksQKsUhOJB7c88sDwKR/L+rG1v9L2D/Z/H9EHK5yP8wCF4n5ZQk/xdVRwhNkMMvAOcgOwqHEjOKvmumS2cVYPWcJd+f2LN89sxzuvAS7KocC1lRahL1DSLjf7CRxxRZ6MC9qpQRlVXXZz0BR5ic8ue9RiVOwUWWZLLYwWU9/rb1xOjADr7hD9jBf+SsXyCCVYT7C0AKuAOV4wMccKz+tET22BKd/Wjksc2PSP7H/r9jA/Z/ELNtEUi6Q6o9SEL+Z3B6s3jPJqmjxebw9yAnITQyncmqw003IhUhPsTy/yDAUl4mj1WgTS/IpL0uXnJKwcHeLvN+xZAzUvylV86+AIq1WBwa4Dr94bXCZda4oV73QgQJFWQCkhR28NnYzDnUInGMSSuzA4KwdR0E4X4VrCdAx66fFREE2Gx/Hngli3Ggp8MRBIn7l3sKF9ylsOUyg5MES0tKDnnNJseWD23/Z+jA/qvJhylv2tww91LLca8HOyGCe3Ahbqeewe6SatikL4mDkDT9st2fn/tNfeX6eyN8Gy08KkIAZFwzxgrmiyqZzuqrk+ySWsgi2eHYPNoKNu73IBNOLgp9nJ51VNgM24kgcXjGPq6TbTw/llkUInH8K5XJvAeOYK0KtjxHxJ4HIQgQg113W9dZ+UqnAKnlEED0Xg5BZt6IQ3D2f1/y8ylcyiqVAAj+ivy/4YHS5P/C84g070DRODL5P1h3HwXGqQQISM8QVVjYO/Nhri+yVgHXs+FgKiGXAV6pCAEIbf6FTH7OO1tT3gtixR+Qlr7Kr0NB9qoDCGIVcfRlH4Sk/2wXCqJCEFpwne04Xnkc6oTzmPAjgoDeYPOj2Mx/AcdA1B6xmElWeNYpnxlUHGokI5EhuO91Rv7/A/T6QLdCQmF2dyOe3I3gNpO3hpL/k1Y/6M99AYWJZaOMJO3H0cjIQiPLdAxhaSgj23H+qlUIEkrswv0rZl/SvnTjKqtwR+c2mnaPmgCIQkKo0b7Pz5gTFPS/YreU5P6OpmaN/i3r4E02q/y55xNkg2WwkvogvrtlQWWhFYGsmCAgY7dOQWSYgsiARl9dBEH4JxR0q/GhZ3HNZiwLmxAZdv+GlXagv+B/YoJC9YDVEbm6rMTHdJvoJpSV/ykv2D5EDqH8PzskNKLKd9g8xDc8igjNns3U/0YCFM/nHpxSnEqBgGHJsPIK3ofNp2bcphdv3QM4RxVBaNQEQD3ikN1PpN7X0qrnN73ir7cbIwXZcdyh10pFEAFnMUEQhLFJsBsOgYg9et4ZRPg9gzzz4M6VyNGs2tsCUZAFNmv+F6vDmXAdkVtu+PmwJzsVo5R72FV1qInF2v8hDlMOhdCE7L+r9LClRC8EO1E07mGxUjv6jWBXdDs+Dw0BP51jg5GUPjmjUpfx6mcj7nvozwZ/OioCYGf/Lsx+X+g8Bvr/jznn8dfEcn8EaJApwLlF5P+ZR4Y3oxk+eqfEcy/XEH7fhyAkWHo71R5q1nFW4WLOvJSyyVvkf5tKLFdeE7MclgnBb9gX9/mAfxEzrPz/LghNp3ujt54DfnDQTcM6g+ZWEB8EklJvaFlsyXSwxNzQ8XW9eNPm0XABjuyXWnT/9453ZD/wzOV4LE0NF/uUOOL6ZzaOfltT3BrMcK9mNo4QpELtE0SzR9h1QhDsAdYLk9A2DRFgGoXxo9QUEhWzl+W/Ox6ALT+ajIQKDDI8xNGIQeh1nsafEcj/omjchFOUzX5UImypLRxP73kSQRsu4LB0PiFcgIrW3YykkYP08PBZRbJ/ZkXnsbj8vj2c/Uec3/AlNtAbEiCDCdTrPAelHko6m6pEF4sJghQREYSyWHJHLIKta0DMPyJWPEn8DxDUeixOoSHE9rNeixFz5xhHbw4EQJJz5XPXQ/2NCI04Gm3+DvrKuXwb+/8PBbJBnumC0E1tLtu3YkbnaNYJjBph2fDsfex3Fi31rdIoHwQMdXvbIYi2DjKAuFQEqUR7IoJQTl7yDUl8E1J/9VOVOOt65S34R8SIZ5TqCWP79UAUYPHsmgYWd+hDjoYujEL+twuNIC7WyamcysbvSlexZFi4gLmeStFRpJAbt9dl/HEjtYwP5NVQ9g/2fmHeScYEb4tX+xUDkFkSpxYJhut1HBU+qHe66Oqn26cq/9jznfNGnvUL+67GEMBiHcx1wZYn4Q6+o9RuWdNA24566wHzZkhAiqEw1LXI/8JzYHMY6rX42dAQ0AX8wkjv3n9d51f04u61EV4O/VnfpyMiAFEWfqJwOXb/iWGYr4g/jB4359na/9fgIPNaZslDHQzqHf97ewq0jFQHYk6cMkH5U2DTDz+b+1gc9l6pgj3blFn7B1yNxfdgpPJ/aP+PZ/9eyI/gwnEBrboTy9u7+L5rJFxA2UMzojLp5bOODFTiDwlfTYMzdAR9BK0Yd5/ITjz72InnjM+q5DlXhQgVgsdCu2yQjxGIioiBdG8pNv6hairyP5yC2bFGZb77MkQLhFif+ccuFxnqw/jZEBAIcAzycoFZk/cLL5z0nvItAuXrAEJZw+jkW2Wtcqz57989jhHSUw/jAchuZXLAbNlkfkdKulAh1v/r+vkd1V3O4TAprnvZ9YcAkIJdG3E0Woun4XCBRuoHEnVcE8cFJPUCP+cvlnrefU157vdliQD0ufP5Xzl3Omt93o7nn5Rp/8hFnIAATi26dboqPHgDMvNjdpstr+M4aw7UE0SbHtnnQ2hZBWGIbPUOwDJl/b7NcURE/P8JIgxNQXwwMQfQF0Yj+gVbxXdavcMsX/glvWx1JsRTR3GHybIsAhB5HWW0ejM7my6Mff4Hgi6KLTED7vwlirM7nZpLGADZnqvjrWjaTyZQ5wls1XUoXnDMgtbeHuYjs+qokGyg+tTJPTtNsAVODx6Eorzaj79BAm/FxAwIAaZAk+FmSWO2ThpUN9Xw8A4MWCN2Wraw7yJq9b0IT0upYcmzdzg0zSNdi1JHTN/xSwjAC8UtkUJi5d9AkBbE1rKEF+wXMAWozrNrnZutkN0pLPiZ/QblzT1V+YeeBkGY74jBeCYCNNtkdsMZPa6Cp36hCs98CUK5xtFAcT6SyBfiQRlbB4BBWUlMgrKxyC/alna/upwvSycA7FUmDgc9K+a+Ep7jDtg4R64RC8opsGnfFeuArNWXMNpCEAq7cBba6IAHnfAO/5BKnPa3bhWgBZKAdzyDFm5g93o8iu9XhSd/iGXhRlmJDIiOBBjAKJD9AWIzoR0Kw/0RXHR4mA8KwYsmvG/jfZGyfrhPQ+3OcK8VPzeX4vgj3Jro/sfzCC1u9OivLRfA7CY77MjmF34SBoFZz+66c6QKHv2kyq5apAqP3BKWNV5BC2EL9R568jyVOOH1quXi61XqkgeUf8p/0Pan8TMgGpHoB8QDcTB35NH3yPjJweGhxAtIaF+/zTasRMegkkZZRE32f27eXJUM/oS80ZErWPI8AgIyfuBeuZbQDZgPVYEpMLtaJd9wl/KPfAVIEApelSuovnKS9knq1XswqxAjsPDI/6nC4x+0dFK3LQA2KAwtR2Dfjv8MDIEgldBeNm+eb23zTtX/sG474B12qXBpCByZ/hKFi9isQJBfZP/Svh24svHdPhAAEYQzsIu90ZH98QaC/cAtWMQIkaTP++Pkh7QvamPIFXisakye8wGVWrxaJc5eyShbg88AbsgSVt1GUB7H8Bhdtwryiy5gfronON9mFW7OM1S2JSGxyP6SCYt+HHsxVI7xsxFCAGSQWS51vDLdN6tC98Mun2iWHGGujfGZEIJwKAohAMe96Ueq5EuuUKm3rFbesR/CueoRKOOzjhDEkufg3QooAaDD00cFkkOnYQmAucRp+fcvn/cCspItvuisePYfGqyjeKrR52AmC7ofDDMZtotGUVgdfiqEQAaxED4OIQSpCz6qUohFsrza7HuU54Q8suGThx3fddjAqlbJc+ty9Dl72ZBXd7GnYNfQuDr86LrEVVjr4CL2Lpf3cQqPSXD1urFgJ0OTlnBclthWryjJ2SKaaNvrDJki8UA4Akyq/lGvQFH4bVYqLmcEyspE/AY8CEFsKZBejJJmoWYePG3ztXmtvTmMMnBIAiDDz641xgRojDk/hLVlMqIS43PlISA4qa1MXPm8D8rRIpoMg7BbI4Ig53pIds6hLsBDt09XyZcuVckL78KaiksxClNnKYjNhUVdFS2yvMDeWzw0hRySAETbfPVs6Xyh9vRpMftfBOaqXYrGG3ScdTIlgJTVQMQwT9mXL//QDwk9zvLc/dsoTwqmTMuGRwSB21YuH0uCENbJEid0pQvhBv7qLzhPIZWmMRl601zd+Rsn5WVztq/OyFw/7yQ7goYQA0pyBdaBOh+2QjYnYCP32POveoMMehxkrAOhN2N+WIx0ZoiMFSvY5VnoflzlvvtGiA0lTHolIcxPUnoOAUdnLLTBPrTs9ydOOcViZESQrPa+YhUqLaOoTOrgdZyAXuC7Knv7v6jguW8Rf4Ew6jaK8dBzWmkFNfRbIgaIT0BbTzZ4DS15aKhlwoMSAIaIY/9ls48c7L9b+NPQkKn7yotiS3b1mfkG5xosFY4GfSUrb9lq/Lg2sgGI9bU5FZdcwotv4xAdG6NCTzmROrxI6c7T7R4D3vQF6N5m9q2Plc+FOFWaQA3TWIGJiAREJU5dtFzl7pyjCo99NiYCEdikOwSBjboYJeB/hVa88G70kjsPSgCiTQd6sp0vgKaeChGQfvbqTVfUtzkN/gsCIA6D/qxTMHkTLsumSiOXm/1NZh+s/x0URiEGH4TEAg6iDLcigoif9z7i9u36ojJP0+3ixTztpRCDlyt//kuYfdkOXPYdsIREvpc8SdUgVi7ng/9K2UIEWqep5LkfQR+wVwVP/zf7GR4LJ0B7irmWg78e33dgkEIx4NTsrHls8LDuIQiBxioQdtSB5g9OAMJ30Ca+LNXiJWL2/wDQqncFBwAB0J1nuCIEsSqNVA7/CcxBqK9dt0MADgeRcpQnirQs1/td2T4adp89CIQrMdD9Xb+DQ/iVCsQ6ObldeQuuQjN/HouZ4BRahI0g1ZoQREQgNUUlX92lsvvWsD/CbSgIWU9g21Fp4uma2QB/IzGgPZMpvIj6DioGDC4wXeK0h0GgX+GuGqDZDV1FBitx+W0swemHhS05iGBXoIUuT7P1OTzsyG4wU5o4I8gyXdm003IIc5VqPUqpCYugE8Q7ePhjKvfDl6ns996J5+I32PH4WUesLHsuZVSj7gM0PyICEzpU8lWfDjkaKVtYmyZPtgu06AGUCvG5P0QGJACwCzj9QUOv62TRujrRhiCme/t/HP+uIAQ0rDf2bT31QhRwIJukSs/+Nk/p8rwKNqL5t70vs38JSeKcG9yTZbtwvtNtR7PxCV6Lm3+gcne9XWW+c4TK/fp6uO+1Yb0ZLqIjqEWyRAC+d/aJKvHSVYQ1f4Y6sraiVkSoFm0sv4xoA5EzZNs+wWcYtINweEACEGkNvUCd2pLQcwhBLD058LvlVyz+YiAIiOYN9t+bdSozbchSH9xfA31Zxj03K5u0yP8/x5lO5P10Gd+Hr9qVjbgtC2eQPBzlIBr4/AKVv/cKlf32fJV/4LvOUSecncsvYORfJE6AgB56qVIZ0Wa2jzyjxv9SNhAJiNk5z0+2nG6bM8DagIGRuteHWL9CFECkGvFztqwm/YMCEDKr572Q9svsWQWQh1nK9uNm9z1wyfMoB6ozmiSRfIQr0HgwWkJwqMr94hKV+eEyFbCJqXNrlIKr0J7ielvRAwAm2lTizPdQJx7a+AvFLzXdtUkkGUuBeZlteS9eH4DDQQSAbhJtYWC+eDoL1s3LQ/n/INbhQBbx1eghIJ2Ut+PVm35EmF01EMblGWx5GgcaivHYvKAiCh4ZHhCSSDyYcKIya/5HZb97tCo8+xsZUTyXoxptItsohSKTP/9Upee/HS6ABUSiVGnmBCFE1HuxgEDwuj8oDiIAqsvxndnc+qOZhI6E/bff9v8w/l1BCETy/7TXY9vG+UZSOJjdjwr9FZacMWA2RVMBir6KJpDcigdbnDku24mi8CVE/PmZK6XK+A/QKJ8xDhfgH/smF3HcLiGuaCMbKTO3hZgxR6Q/32FnFtHvFTegzw/7IFw8UAj844j7N4Ww30I1hHzHqVoQEFMbE6jI/7qVde82VRrkDvtMBnv5hjuUTtH1I5H/S4IBecuW37JDcWKhyt2GH9nzv3NErRqiTZ86Obh5EmdRtmUsWFNHnzea5Qc9LmHDTcLTswsJ/yTb7n6Lgw4mAOHkoL2AaJX2k6rT7WbpkMHb6duJy5t7pnulGkgS9qLZuQ6b/h0g53w3Ww9eqVE+kdkY5PMp2LSr/B3vIcLPpuoTgZBuepNmYBVYgmfls6GoM8rmNODnISiCRMqu3z9loCYcTAC6QkHN6DMPlhgGyiK+NzoI0E2BeLSBG/jgu1QNmuvylIi8Tv6vxVJaaRuORVgKgm1/VvkHb3bNq4Z409sJlCkE1G9VevYpoY4TDquZk/DwRol2Wal+qwP7EADgJl1jzA0ds3j1aMalJG7FqWoQsPL/E7jasihnsoCdVA0E6ZX/2e3X0oJKy/+u6gf/FSKwC2/Bw1XhoWVwH+vcK9XgcvoV7k2b67yVa+WP0K/8OvmpBY/p8uP3LJ890+J3kT9AHwIQbiig0vnECbw4lxhjgv4xAahmT0IAxOnOm1lF+T9ENpMmJv/GXyD/I5tXTf4fCFiMQB9tPMxA0P2X8AVLhQZ6uWL39JT5AFayq35ZFat05TPSoshnTpnFSg8WSpAWH1AE9iUAYeFam+NaWsUzBdVUEbUIH8enikIARYvQ2U6no6mK/T+sr9m1Ad+du0FGPA1Ha/8vGwZYH6ADhe6Hwi8HHHpl5zrUB1ocqoT7t2sdmnYe09B/WR6MTKSdjLnowKTetxdCBSDriY9pbqI51LCq5DNhyJD/mZC9GUeFGVdjtnJ5ms2E0sJvp3L2/3JggZ+DKJX3rnWaeYuP1WhrUZ2kDCnTigBNSwAE2+00ztlxADBiEZT6EADd5R7wYkwAIghV8yyYn3sc5d+bWJc/25VURflfYu47aQCbY62TcBwyEjObEHlYV1CLJLC0I7zKhKYWbRlFGbTehgkD6/HZ5keI53LdSwDsS9wwn+kQ9fDhod4ECMapahAI5X898wysANMAvgzUCoM8kv97diF//xT5H+O4mOfGIknTbH1qhJBSVo2KGgtwllGmDTEJ+I9e+7l51jUSsNiB1ksAVJe7sTelDifj6XnkAJJ9qYyC4lfLggDgBcxe5wllfTWSl83ujcQAuAeWGE5DtI61TtbawYCSSEMtk8LSqzu8TIF2WpGHRU/NTQm04DPQnjklGRxmgR/i+wECEHoIJbV/OJu0TkNxGKv/qo0kzFCCF95MltbaZIluhUt1eQr7L/E+7DZbY4IMCSuK2ziDIpiHnEmFG9s3ux5WK1ppR4Z5NWDbt7i6/QXm49ErEJiCT9Chtp4hvh8gAI9Gs713eKqV22gOOapLousWYjWoWCT/T7+QgJzTwwKrAG5r/4eab34ixLle/U8NGllUhCg7wXtv1pFFN6t7afbgc2DxvgpwrW7VK5u7w2PZNswzQeAWm4T4foAAhJrBQAeHhiOlyaFW2T44KDcIgHDietbZLFiDAMhArbQCMJxlTc8O5P+fwG3Aeo+F/C+YX9hBsJOTEXdCc+dBAKn8jWAry56F3lkiWPn8GyxHF89b64gDsKTxAAG42tFK/s4NrxqsfY1ZXW8OIbZssv1RlUaY3Wjet/+KhTljIf/TLj0Z7f9G5R35dhdZuBrErhhyQkiDLDEMibBsR/gY6DyK61Mf127DEKMX2OqEIcIseKSLgJl0CxcmIgAxB2AhVaU/whLjaqWnHV6lAiRb26Ww/49hbuSnrDqsNXUXJUdhL2bOOco/6a+kUiRXL3dd4b8R17N/JwTgNhyBOiguXeFCGjI7h+FaHSa1F3ynF/ryRs92LZAlKXNCE2BDtrIhKi1IkSP+3/QL2PxyfvWqHMr/hW78/6UUCQDiEW5cyq96kuElFG4qwSWfY6vv69nhewHIKPerP7eYvdtZd0BAECIbj4nVo+rwLb8AwWsgf5i5UfDcJXqIJKODJx3TcjO4mhyEVFQexakKECBKjfX/73iZW/9fFaQIO5V4+Sq7xpH6fX9y+CeMQOIY+jyUAGWGlKCfFZuZGUxCaIhybPb9WSVf8WWVOOF1DpDVx31bTuH53zsdSwv6BwFFnAjzZRV9h+xN90jE1LTAxRGAa6y23yRM0EFkt4liMiDVqKtsWU35x5t9VBXbHXYfjj/JV39GmbM+xEKgvxAMBGvApruV2XqbQxB5TUZBgrrYbcAEWzis/zxyg2UH3YAYsrJ26QiejSJmEN7M7H/MujgnX/M9lTj5jXxKQVUhdMW1ot7C29pNQlB6Srti9j8CEHsFSN+qlMn4EnVmswLvHQEIbYJZracmtGq1PkAyMOz78k2cKgoBkf/BE292LTTiLOdkUYwc3kxMcCfSrT3vVmbPdhVsehiCgHvwlntxEvqRjfodkX3rt+/PBKGwHHiIDL1se/9BIYgNdNDyqzybjWB3l9gG/gldKnHGpaxxoExJVUf+A2UEm59WppvYA3a/QHwB4mQhYLuAmNOJhIdc5JIjAOEPAohOhCv04QD693L0fnweLQRE/s6vRvn3anAL99+aJLqzqEd121RMj1OdTf7EiyEI20D+zxEpeBORfNEXbGdbsJ1wCvtZuZfhEAlBTJYhrveZG8jXEospsl3YJSw0PF35R7wIYnMELRP2W17gspeAVLPBTqQJnvx5WGwJnEs1q1NPedMHzrlXtXim4AgAE78jAKFTQBCYSa3EiitkmaLiXYCr030i/2cL7K/HbrxtEIBazIyCgYKEUZIyi5L4IVhfhBlHgLxn2ycmuwfk34skgC9tHpaea7UXwpAm1l+UUhMw6812HEY71xPhGIRbiJLVOoV6huhetc5hWRLxqPDwB6kTnEdAG+LkIOCcgYJUQnuZrBIRwKY+HID24Pcg2qS+I8Teiv9UBgIOE72OhZXJbiS59J+NLUGQLo+oBGKDOA1xRHdcMacMX1pEXKSMSMk4/FejfEO4DCE0BZX/01et+kK3MZDDKW+UmY+nz40Hxpu8cRwAE78jAOzwJK1k7yAeCCBj/K9aryMkO/kfYbxekiUIfVHdjoHeYSAX/Z8XVz58bpF+qPeKv6ngdchFFZ7+tQqe+BSz/7Eg/+4KFjBusmL/NE2cVrshpG2UIwC9kYDZLd5R8DHoxXED5MEbIpif34D8/xLkf7G41nNiCPSOgt6LQSo83PNBPqvE7ZD1NzueZY/Cd6F2ELiO0XLnSrSn2nlAq9n9HTWtUndz9BHQCB7YHk/+ApoqJZH/c7uJVns+29Yh/0vqz467u/HfUiAQ6Rhye1Tu7o8ptftpzJmyrqJGAUdKqWNdvmOw1yr1Cjh/RwDCdQCeF4oEdVnp8VApB26v42gaw6wpAzhOI4NAL/LvVdm7PqOCZ76CgYuAN7IhyQHWZWR5j/OvYPItByDNdCJA2GATGHjUMWTnxjngxUguJNbriAKAxLAuu8t7lYwe5sutbET6UVV4Yjkc1XEgP74IfZnasrMf5x/YFQCepywHoBD9+xAANgPldzwoqzIIBPPzm5D/X3Qg/n9VChqvmYrwyhFaFoKtTyHzX62C574N8rOiMthOw+OxW0rvGyOuWi45AtALt1gEiABT8TN71Zvs88qbc0H15f/eWbK3YyvenJplaMUk2hFZGPI9Kv/wT1T+t2+24b7czL+N6vRRZ9Wseg1ZENsFSr2v4bAXgFfsOEI/2aReruJUeQh4dgx7s1iEI5COZNiKFySzJPkXp0jXYO/3e1b8Xj1ehzO+ybCpyXP3s73Y11Ww5kZMfYcp1SLrDmK2v+xu09oGSLiaDx0HUHYO8QflQUAQHpgDba/DRmbm82ogolBvxLwda1A3ZN3yWx/PvP4OOZZDcO8eRCzKa1j13pY6EtQj2L5GBWsfUoXHvol//y0WhnoCMDR4+QX7KD+e+cvtBALE9UZIsQQAUMvcAMh1pirjstwajrf3xVk+vwVf+Rci/xOgolopxOn8/avYhw932Hl/j7//CRCdUygbl91D5rNCj6jvliAUEaCIINQDhyB1oR6Ftfeq/C8/RFCPXwE7ACYj1SI+rsnBzhCCRW2oFkzHY77GQtS2zHEA4cCBBgDdGKgV73Nr/0f+7xD7v/PCrPzM6xDHZGGVNz/o/Lm6v6ryz9EarI2oIAjJTQDSWRKX7yzlTTsUojCP+9jNixF/rImBrQvz+uyTVeLMf1P5P3yW1Yo/hwAgOsnwNPGsP9LxCWYLpiusABIfSq16JHIFDuMB0Pdx7CSBTMWTb8mq13EsOcMNVEP+D4m42UMorC3fYqaXsrK4HSMnK8QAtuk2228lPNitqvAgOI8hSE95OU5JL1HefFbvzVqIyDCf6omCOJwELDGQn7WfFDQLjfxjXq38w05X+b/covL3vcOFNU/F5j56ZETJYj99SVwAFm0rdckljrEqygwSOwadXVSBcXgJ8iDLyiaV3uwoAGj1EMpsYX2/8HETpFxZxxvRdGTlxAKOJF0MQWAMmF2/hMW+B3GB91n7o2e+Q3mHQgw6T4EgHAWRmHKgP2pODBiuUiY7JiXORJSZfyY2/w8Qv4DdjdpZRxGI5j9OI4EAI4MR4pITAcIfhpWC0YP4XCEIWPl/K/I38v8h7Mpb5RSsfwBMlkIsvS8qDTnAushCjCwbzUuJwyBMEATxC8mz9HftjSr/LBp2RoWe8yblL7xYeQsQF6Yv4IZwEqSaEQLqJ5NRWJ7XsUil3vBVlbvnM6rw6GdZvgyHY1iifGCfS1u9+M8wEHAgdeuki1YDcpsZyrCDu+UAol4eJrP48fAQiOR/8f9vC2fUinNZILvkmWH/v80PuAAdw/rD8428Y99DrvYQTVJHMusLd4AlYeP3VG7t96zuwJt/Jez4hcqbdxK/Z7o2R0Ok4m3pB9Iof8QmPWGWSp3/cZWbdpQq/O4yiJfoBah7TAT6AW3Qn8L/Yzcxu6M3HAcAJZAb6F7hqxgIcaogBAAxuObNQnatmvzvCECwZyux/r4DYhwdIkYZzbBiIavo7MYhDIfk4cQEaLNiRPDU51lm+3l2MT5bece+QyWOg5hNOdRlbvUZMnzsECqjwDJfFcuFlEXAkeRZ74ZQTVL5O96qVDtu1dYLsMz8mvN1LQZAo7XjAKLFQHeHwAiUtzM0EMTG1YoMEJCCGdau/7cKQMm0Goji8jRbVjuRX2bzg0SAchok3AHSoDW3wSW0LATRGC2E2c7/6t0qc/O5Kve7rzD5bqY5MlSknXxT7WSJgCsncepfK//sldThYYiCBLipQfnVbl8184epg5nSWTb9ZBOQXg7AIrosC5SyjQ52pnP2Ere1GKKj7g+R/2VLrCmnIv/PG3V2g2YQssnB+j86Ca6iLLHoDvY7YpAglqDY4tN7sNG/S2W/u1gVnryT50wrUgeZoaudbDkyRrVKvujdyj/lGpiWRyACIl7FRGAY8GvodM4EBccBrAJs9oMoIIgxO+jGHoKGxLAcBpIlPWbNhcmiAOx8PfK/hGInhcjqflTibzjoMzuR///s7Do2pHcl8u6Xh+QrrreED9cTTyBw6O9V9kevUtmf/huOeRtpG8OpJpxASGxgrVIvvwoLwd+i/3ic8nF2iNPAEGCYCFpDI7Nw+o4DWBTFAwg/KRQ8WUydrvgYDfNvvhOacwBvl/+KHFCNGTJEuGDXZkx634UAiPxfTWOOIB8zvpjh2GtQTzgJrfwnVfZ7b2IDUghQ7wxd5d6OdALse5B44RXh6A4tFVUuulGzxwFIhmOPznvIbiTigDgOIAwIonVhO0rCtOcoQDi1NGpzx7reIEqAWwVKdXGyccnS4ApXzOVptj0byv+i161F1wkhQD8Q4OLMclyz616IwKmq8Myva0sEaK2Pj4B/wscQBeACPOG0atH+Cndj9bMzPnjN/x1tUxPOiYIudAQgHJcT2jZv97TZ7ru71a/SeC5B5P8A9n/KmdYHv3pNdZ0XbIjs/9bJq3rFHZQz5Qs3kITImUNV7taX4sf/p9oRActVeSpx2t8glqAHYCPSyMH1oKo2+Q07rxu1Tv39ml4W0aI6XWhJpr4MBYFSzzthISajoxovVv7fifzP+v+2SS4rx1mNKtu+H9Nb0nnW/v9QaP+3bt59X6v6L4ZRAOIlcCgKpqrcT16MqmBNbYhAqHfQ09jT4LiPAIt1lBvrAgbocnEAADZmLcOQAOBOWOud602X4wZ4GBGAAfKIb5UOAXGogcXqOBmgV1f+N7vYsGPrt5mFj6JM2PIxScIJ4JSTnKPUnozK33sDdYEYhdNOLarkLXg55UlJsS/LgPC2sFFr5dmqxQ7fewmACvcHhKNi2Zp9k+Ebp5FBQGbEHrvgxpt5eJiFg/7I8hvsK5en2bbG+e/YzT1rYIobrDqCfeI7gHNO8NinVeGpX7k3azSSvBkLWOX4YrZeX+OI7qD1bL4H9IzthcB466X1lyxypPIAAQi9AXlvncJZgFSNESv5jv8kbGmwkQU2Z7Pk9rDqtTcUKYLu+8PeQjtfFykt3sRYB7BKyEKoanMBkj9DVk+YSQyECyEA+C3ggh2nEALC8rMfSDZj9/t73t4NfX8OEIDwBj7h69NZkwOksobVUoLmBKTQvxGykqy1Nbl9LKg5h9BVEx34JLuKprBr7Pr/v4Tyf50QAFYh6tQCzII3oAtw4636I8nBwz/0tOoXVdF+rEFmgMbHAIDcvzOXL6yxJYa+PwcIQHgDkr0acrotwRdAsokJgICGQ9xMxbQk9rySCQJr6lHGewTfOCD/V5gCRPb/HWvpLfH/H0v53w6poj+IIbL7FBOx2YhpzqbaDCU9eX5RPeLLEAImgVgPOm+fOMOssfdC0/8BAtDlkL31ig1rURBuaF5ToAxUAQujVz2n1L5H8DdnEItcKUjXhyAcAB8vuyRraUX+Z/x7h54S3YyeVvDsCEqw7Xnrqet25R1L+b9/0xhwEEGTTvd/UN3fsmOxdEtIIKtbWIPkTlc4mOhn9Ns3yfJJkcosRe4dwTLfAzPWCVhH8secjrAJOQAxIWXXsfT1b1Rq8cMqedHPVeKM/yKU1hvA6E3QhSKCIByBJQiHcBaM56ynskx9tfJP+6LyZuKVF0LbXVTwr5Wr6bTu+0LGZCzMf0O1B+yXQbV/S/hS71Ab6qPRP5OZC2uki3vpiOToM23wHMI5DT2AZccii5+0iumqKK2yaF9AXIhUgpZKFL0x/i9lzwQmUv+418PCL1KKwz/uXDiAf2LXqXUq2PQUYbUeQ769k+i7P3Vsrky8dtaxlFUlXvAplXzB34WwEhBWeiC6PE12D/7/Y2n/L2E4uOhTJbxYmVd0IoH1ZSGEh4nOZ3jXuPzKtKLiuTg8NuqpMGcZrZZd7EsAwqeEDHvC5K0CrEZku+INHlmGIufnHiNO3nksMDnD5RGIMA8sku3Km3G0PdSiCzC3X0ZYhQ3E33ua5ehYVvatxfzVoSSCjj/3JHBeBh9wD2fqkVVokK/CfM3OdRCh70HGF1JWjVntQap24DYwk+ZP6gxvRVTywBtVuRJe18cjUO3gSFaliIbK1Ir+KpFOmwAv3yds3SOFPz/6EoBQERj4/upM3uyCm5pSCKwYUOkprD5hCPtvWCuVOP7vsCKh/BNEs2vrpbpc89+NalZLpyYRIAMPP4iCJZXW6aV4wMnorxbYXL5mOwpAJjo9UTzwRGdRL4n64VBiQ4u1h1aQGlXN5HPQQlyRvcOpQ72JRTUCQnExaP5SLALIFMxGk/MJnkC65MB68b4zfJcb4hPak0SWNGuTYgmwI15O4zzZtft78N2frrwjXjJAY4GFILTVpshjIQhyMLPZGRnkFwJhfdPluYWdXFQ+WcJiVLD+96H8P1bef4M1jbYLRwJDpaceGr5URXgUVyOHKZTARs5qY7nc4qfNdw0HIHMYYv1T7Vetg1XlOlQAynUfAiDdJgoC/Y41wk8+0FSKQNT2JrOW2Hf/ii5vvkPmIWdwoBURhOg9GeO9BELAW40kVIaU3Yuv0f31Zf93NQMGIko9o7y578Izb4G7WyP8F6WjpcFRn0R1atazDBewXJsAtohhDX4Xg6LPD/sgdAlGYPh9NSex4kqM/TWjM4BlhZP2jzkvrE6dzh7CbZACsf/v/CFC3JH0aj2xutRPiClV8o58LfVrk2mFGteGApg96yx84j8hBATswqRqJnRJIX7ba/4cTADCJ0QO/QuKAzRgiLhFLEP04bg6e8j+mSeVd9h7WbxzjGtanc8gZsd6K/8rDwQ7INKNfbeIC27uCSwoZyv/yIFEqepW0exxsS5qRXCq25pR504MAOXj2bs7MPoRm1uvw5/L+2ACECoIWpOYApVa15KAhIxrj0BpHzSOk7/ojYwbkeVl9hfSWYfJeXQg/9/nlr3LAiAPRZto3MY8MZzEjyKXVYmX/CfKyQ5gKRxBDWApZRCpSKwyrjiZu5o+GavHM+b59kmpxyw0upyeL4LMQQQA4DmHoMu6twLSh0OR1vGd0Vfj6Wzl1cdZt3+JEj9ym2oxYEcDQ3b+NbvwRxDOfx+cXc+TuB6j+ZLAmPbAOiGErGZJkBz5yZuGafRh5Z91HZuKvNyVXgtYhmKR2b0Jvcg3UD7OhRiIJrDpUxgDQN8rej3TZdf59sHlgaeN0CEIC+DtkNMLxzUY7cYdiKrHvYWBA+LUasYaDVCZ9ZOv+axKnPUBVej+ozIb4Os2/wSdwF8s82KJdnIGjZpFKdLfIgSiHWevwIo7xgihkX3FcogkmWdV4uVfIm7/31OWJCm7BrO/LYtWbsctGt8M3X4cPyS8ayqPsAAAI5ZJREFUZZxsF3jmHguJfvK/3BuYAIRwMwXz20zGZCDiLeBFbXszrENVT2L6y2/Hmxdb/uFnV7WoimYOhusJ0+3hdR5P1uzzt+e9cAVbmAEf5HgMgvBTnIT+7AiC4CBNVYn5nHFXtjoDEXPoUivuCHEQIhHe49fAiYysqMGwsWcyzT2L78RzOEidAUH6D7tpiEX6MSCkwdrfhdWWdjR9YgGQ8nuyZkdeaWsBUP3kf4HQwARgsdMqbd7Z+pc5M7JPoAc4if0CxiEBmMis9ZjyT/93pSfjsWZbWLsZa1RD1HZHmAOETE+aYw/ZvkuQ2ey+nNgcm5CJHyU04TMQg6chEN9Sai/PwI9QtINt53WPGdybzjGBB0IpJAkwoiQv8TuAUBTw7S9st4yE5KGnvwzLyd+qxAmvczK/fFJT5KdeImaIWzTrImz168oqEsGw5meTSGidz5pHJy/ttmsAdJfD6+KaDEgAAKex8kLXmnTPys7fMEhkVBWPiOI8GvSa0UuwCmv6O+qVYRukiQ1CAPrL1pYghPUXDmHyPOVzqPmnu7bl8BvYew2Wg12ED3vCHfvYSrwHhE7LjkLMnoSKtBHF+oOB3xaxxLFn4qlKTXqj8mceg9vzy7CaLMRrEuIhKUL8/nVzT6vzNywz2IkIsuXbeP8eTT3sgrfqlNc4uTr536g7pMoOn0skALaNUYiwgr6bz95j8UIGxnhJYvrb/6TyT/4EJitMf7Zt/JGzpQENQgii/rBIV1TnPgSB+8mJiDpYC/BwVvNOCb+CFcj1wAXt5iCoZ5ZIPhmUZ1kQSGb7IOcwv+UQdIpgv+wk3M41kXeUj9IvSrYsftQS8aOyQ4IdEIlYnA/1BDgYYts3dRKzPegvEYDo+V9ZWAwg/8v9ATkA+0EkLyTMvemc6sac0JkrWCFR+MHGT2yJIOPV60CGzuNHLw4r/d0ibHN5if/hn8Zpd3+CIJTN4kVI4exzujLJ+nk5RuKyP6aIL11BW2iHSe9QhSdudoaPulsUNQZDBvRHbPdYz/NkT0vPfbYGRf7/xTUaFJlFXhC2of2K7rV88CtZWUmyQ8heNfofg5a45TCV//U7VYYQqbl7VqjCYz8lxD2yMjOiTVbIFey3FIDWM2P2zqyNBgAhZHLQ5Rb5o/rTpdIme0j7wjb23pNn0f2o/eG3Nr8QNlF2tTzbvmDCX/8glpBbsOIsoq7IMXEyIrLRMz+fdtmOXZb9F65ggDQ4ByAvh2wDQ+YWPsdONp6SDFzMYgX279t0q8qvu9UpxggHoKcTC6DzXIKAsPx3JjLupA7c2yc45CkGQSNzCL3tEKIQ/ei9iG6E58Hu93ut1j+FmKHwKzx+qx3dWvozTgIBLyc7fGjwVtIg7L88GpoAhGJAKpW9K51NrUcMmDuuxAAZ+TKLJReA4CICcM2yWrPxFpV//hZHELitp74OgkCAz1nHoi84ihWDnbCbeLzJACxO44IgFDeojq8F1sC/sPYBFTz1WYKvoseRzUniFLTC/qcL5om9+7LOLjoI+y+gGpIAWDHgZgKKLt66oWfF3Lv8pLo0h8l5fMFYWFxxkIlmD5C6lyBwXUBBtvnHKr/+x45zbicOwCGvhiC8VGn0B96MwyAI8yAgQxGEOp1BG7YjZXajb9h7sfDA1znTEN1PPGnYto264kawGr+vO2b+89Y9RvBXS4jagdOQBKD4E8StHwHoS4vvjc9rGUhFBEGEKUsQkA3Em6aA6Wzz9yEI37fNl7UveupFxBB8NZGEToIgzGc58VynJS/mECzZdLPW+IRbDVslsr+M6ufuU8HT16HLOQ4iEHv+hT2g87D/XmB+XEqPDDs1AWsJFGp2r5w7PWnM/amEnp/NW14XEtyMSbQr0E1xIBBSG6AwzOOCisVMeCM9YTI6hAshCC9V3uwTHIcwaTYEATNanEYPgRD5Tc92lf3B30GM/w8CfTiwj5V/ADdgEZ+XyZkHWyamzrL+/yH+Dgb4YTkAQX7LRixev23/ijk/8Hz9XsWiq8EyHP/34aZsvOtowBVzCEk4hG3oEL6t8mu/7XRrk+ZDEF6hvDlnsdyYeIHiqWe9asY/pKrZwvyfbkL7/38QXMy4gcT/G3Yuq2Z16iXvACbVY1ufVYL8d3UpfAHB1iFSabN45BNgzE3ECJAMx3+MgCGA1veREAREBuEEZJtsKzIczsA8VqkJJ+BokyF09zdV/heXq8IjiA3yvInJZ1/YlfnLKv5g/df8VhX+uBSnJGBsWf8Y+YGkYe+PRCZt9udN4QcC2VcAHTkPlUojAF1uyLYv2/gHYgv9ujUFwAlJN1TGzftMCALcgSUI2zHIICqkTkBRyOX8F4RgKRN0wvbK4Jdzs6ZQ6x9sW61yd/wjhBSAKolc18Qw6TsWghR4CTTumrRs46PW9t81PI6WRAAE3UUMsOUZdVMM876QH/KXiAsFzFNIB94slFU2lTljwcdZrbecJTUbQQiR3+zbrHI/vVyp3cSqSXYAh0gMc2Bp8r/aSqZKfcPCYQjbfzGcSiIA9oPQllgICrewMlAiBgtBKHMqKy66Sa5lvXz+GaWnXYBDET705aRwxi+sewBX11+wmu95Bj3axmYiCBHy790E8r8fpd/todYfkSuW+6PRFISuv6vbUj232ZtD2P6jj+Q8rBIwepkxFyoDN29Kr+y8GZ+A948/n4CotRU8hwFH/Nk4ErXAtgrHGs3kwxVj38tbW3fhwS+gSDwRheLL8UE4FZPjiSzumQtRwcJgzY1lchXDlT3Wzy3xc5yPuGfnbluKx+ZPlWoVk98Oalf63DXWTalB+db1F2v/N7S4/g5j+y+uT8kEwH4UKgO18b6WyQTvRukwiYVXdkgXZxpfF0OAiOwyjvEgdEmYplIGrwOr6dnL7r9s/z3lcFYv4oOwa6UqPEV+8F966gvJ9xwIAv4HHaJnwGV5wowS8w+rU3cn2i3IH/pQiMIvd+cypXYS06JVNP7oVUqCX901rFoVsoE/0hmzReWDr9tCIqV9CSWWPW1EyoWeFXO+1trivZ2CEXJD/UAJBTbXKwJeHIjM0yr1lsfDZccyuEsAu0UCdmpd9yeV/f4ZjPkj+U5ADc22gUvJI7+OA09FaIqNzDXxMEKFfZ9diU8LkaiEcuqlQ+yMfwDxZYWfmPoKf1jC6IJzSiDzi2I1Rv7+PVZobdF+OhP8d9vSDe9xJvvBPf/6f1weByBfR8oF33wF5P9b7jiToF163D/7Jv8tDkP59Wwh9ioCdEh8vvJTsIVoPii79URcDu3sJ16KYT4JFvcnZyMJizqGRU17H+AkIb9GkCwC9vuuFELV75OyfkZIL7K8LUuIWo8qPHOvyt/3eRVswM7fJopTABDs4VwK51RWDRr7ZcRybZRP2L4skXxvlMasWlVek8onAGG4sLbLN96TXtF5R0tKn0vcceFrZRTGqRgCIv8TZMOfcz6LVULELAmpQi4BhZ/s/uMgO4DGWxSCVikIl5FlU9POtyIGhIrGcif/weo1EGGI2thbRu9F9IRzRKX6Xva+IOVFSC+v7NsKt/Ogyj/4NRU8/w3nbNkuLP/O8JOByujNrTkvMMW3MPtnMurHYqKnq8RrV9jEklPZBIBuCJWBqkBJ1+Xy5lxK86S74y7qD3cXdERLxCGBTqjR7v/WQb9DYJr0HrwK74K9J1rHUCYv3JLFPcufdQaLkojvJ0g7GEIfVBg3eL/w2K2YKwkfOeMYZl1Y7pYWiBZnG/xzoI/63YsIRW+5RaOh6LL3Kyps9hFbcEc3M/4v8enHs28bm1ExInW7rOsnSEvs4dcLroMurMlfeXDhqmDU/9jnYTTvg94d4kbZBMDmFXIBE5Z2/7BneecdyCCvggsQyhNzAb3Ahl0NekAkqGMHcepsGggTej846MJsf45FGL8HqgtBCNjgwZLgO8XpOXjGlZUcpTGZXSr326uIE/iUW+JAnfWkV8JNQFCmHIr4QgTiqYehYMTikIAwsG20kggxCUKEyRoH7lmtpC2bPIUaybbqhBUzcpahYXft3UfoctpEu4LNxCXc9D1+s7+B6DCwkOgJgvjC7ouiT2BVHrzKanqjv8zsL7J/Jmt+NHFp988tyV9c3uwvIBgRAaBberkAdNyfZ/EBQq4V0MQmEPeaQFbU9Pm1zKgEFhFTXVnJIWaw9Vk78euJsPiDbv8tvYEoIISm19GoxMJCTsFse5bNRUD+ySgPZWtdltma7b/G5n7nAaZFelUOGTEtLIlugdikiIvQinKuBf1GC4FBBZPzOD1JiDXORhyg8vucI9R+Npzet4ZrvheBUVJqDjqLYy3f6mb8GPEdYIb5K7M/sj+TLufCcvv2zeBfrQiALTDkAlqXrb+1Z0Xnz3APfk3MBRR1nJX/88rvFPkftlxSL3vsfg76V6ZzK/+j1OPSRi4a7GW7s9Fq/ALezAw6zb1VMgl2LwZbWc0IzupJsqZBltVyP9GJgjHFlWC8zOpClMBcmc0z7EzU8zTX91mct7dDpO51SZCso3rI2SO/BJwQweqdiY98pFCzm4PnNkUfRL/j84AQCGf/noz5cfvSTXcCPq1HgPyS94g4APmQrjrABWhveS4XvIbbCL2WOsU9iTQk+O5mZUFoMCS0bQv8Bk3RrNyDea/7J8j0IPVQ7L9G+w8D4Heczaw8WXpFhsOg2fd5YN8LiKf/u1DUjxSN5FEcE6HvRzRKGiZRRNERiNQHt+MsEfKiUILwiIiGxXBBeLgBS0zkvTiNCALR7J8JCtqYz9k8Rjj7y7d2fhlRReSjiAtYsu4nOATdikwi1QvnghHnOg4+BKwBgx3OXc88ImxPiUgZvm12yDZX99NDQgBAniGS4LHu1TMM8WKfR27atYrGTTjZ2IkeSjJsEuJAF4ucL4pJq6zDRCfaenswo0toLhFZZH8++w6chW2DK3PYIuIXBoeAm/2h896P2pZtvAcaO+LZXwoZFQFg3FkuQDIK8vmPo5HMcU8Ugc3d0yL/F9bY0GESP9CmkvHfgS7YvNpOwnY14aDgJFOJ348ezpuFyaycZGdnOgr53+y6nV6DPRekjlM9Q8BA7HH6gap6+tNSUQJajwqHR/WxVEBkD9NF+PArN9/L0P1SSytqQafqkcfNmYQtZ9Lz5oj/v7DKkkqkAJH8v+nB8JMhZmW70Ajl3czX4SgEp1BWcvUJtq51EoYsW46Zt7IgOAYvF7D7M5L0f7cvWfd7wbvFq8rX/BfXe9QEoDgzrc2ne3rMVqxEwlA2sSiQsOjuzcKsJQRaWOZSUjQr72f7LuLc69QsvoWNHiyFC408WWgkjkbyvZXrB/ug6L59r1j+H8LMWPRZfDlmEAhYgZtg9l+PuP3ZStWiIgRAd7GJCCuQ2pZseA5XpC8kknZ2aVIxAPZf3Fbx3NUd0QKgEmf/sFeDnetgy/8CW46SbRjiIQzDgXJKBbl7z2Swy5cl/1dq2MX5jAACRlwvGEmfbl+2fp31+QfvRpBPn08qQgBsjuH649aU+TxU6hGCE4ouYGjtVZ+qjJMfVv5/Hvn/Io75rlEl43+ImJuwmQvnLya+IeV/XrLyv/jLSyqxoIjTsPL/zyA0Cykmlv8dDOvyb0HwCbPfAy1B+xdtDUtc7z9caypGAOAonVnwsu79WIk+DJsiyboID1eJcfVc3HJF/p99FviLGaCcZM2EebzkHg1xeTj5H/v/jAuR/3HCGUEKtrGjrnD+HuzK6CeTEdQg/mRYCIhdDSc7NuQBwfS/6WWrM+F6/1LZvSGLqBgBkFJEIdjVhd56SfePsqxNFrMgc1KTcQG4xwILb/6L+QsTJN1kuGNNZ0P0WTQri/y/8TaIByv9hrT/s9AI+uDNwQnTyv/SASVyAFbRCKHB/m9tNja2Ht/Hqf4ggKu/4FEhUF+egNOdAb9G6vQzUOMqSgCkgKvDUghR+hEiCG9I+naIjVpWGajy9XcPBBT7Pwp1s0fs+Ov5DTsgeGmF9RBBLVEAJCHSF7fD7GJW3nUfmM2sPqT9H10wvdc30EhxToNcR4QmvRcCcBcLjYRIUcc41SMEgpQo/tLmuYKX+0g1Kija+oqmSCGoF3ev7VnZ+RHf018W9qU5krQT5xjvUJX/1d8R0IK1+tMvYJY+nQVBhPOaNk95U+byXGT7ItobEQT0B8GGR+zMrnHDHdL/P4B3t/7/+OTbVOLsb1kS+MrtzxJc81eh/B9bAEIg1t3JMnXGfHjiFVs2MvsnwK+KKmsqTgAsBKOYAUu6v5JeOedNRA66AMWgVLw65dVdt9HU5ELiWKBlf/4rxAT9iuUC7B6C01+LfuAUjpOJ6cdimoggWJ0pOC/yv01DSE5i/889y+zPOoNyA42GuVv/f2jV0AuNwpfj01hAIA/rn+jJBN9vX7bhW8wRutLIL42qCkIyFxkrq3SpQBv9frSXZ8BpzsoXrKapaOobC7jWokyBAK6wRE5V/pE4A3EWUPewa9CaL6n809Kb3GKprTcda8HsRcqb+wLYcQ+z3J2cZU3/UPZ/8f9nOVjZgUZlGAF+lAfBBhEzBBZQgTjVGwSChK/E5r85F+Q/ZCvXhSDZNahJaMT1rwoBkNpArYK7YFlalnY/3rN87j8z1m5k5PGAP6LXbIZkmR64gQiZI4IgM7ioRnp2qGDNDRIy0BGECdwuzOOREABhmAZLYaCRmUeFL4iKpQS6CvgF8qaHQCPdP0fRKIuHYgIwGJTH5H6IH7Y3g+CDU9675aly4/yVU+8SRk052fV99xzkFeEE2pat/2q2YG6EpWGKazarQBFMBKmFGNhdg3aA6NDfloUs40WOl/BX+QW8LFg6VKLLZJ19O5/PO829KIq96Bj2e15F/jd7hAMQT8Mm0c8OBdJ6ehZq/bN59f+3vXfj1ywnPcKlvqU0q6oEoLgC+UThQ7A0q9nAQLiOIQTc4q/G+bUlCIgKliCwmk7LrD8MAbAs/G5Mfy9EeQ8hkDw8uAmRKaxcIVP8YATB5R1sWeMmfk/8FGICUEejTBx+0Pqrh1sTiQ/Wol41YcUjFmbv8s7zfK1uk6EqQ5QG1qT8WgCytmUI3c6haNyBIvAKG3PA6xQrw3yW6bMoyBfELkrW9Cfgdt9lf/ZRVXj4o8TeOxr8h4jEqR4gICv9WOLP5JgPzm27cuPdEd5Us3I1Q8CoMewn8OHWlPexOHrQaLuVrjMgdJbVfDKJC1VFh2AJApuEeJ0c04/gHg5FCfH0c8mkd6nM/74WE+BDEIoZ5AEhiVM9QKBAVC1x9/1Q+7LuT0X4Uu2K1Y4AMDZlyEqDCCd+M8uGLwm3Gq+aIrLawBvz/K0nEMoAa8wBtOIbkEW+R8CSR6In8GZdxmIhTI6zj+NYROTdJ1XulpfwwmG8IJJYLAIAhLFO1uSHiHxT29Lut0ll6M1efKlm5WqGfIL8VqHRhW+c1u8xaXMs8s6JbDQqoxAhNk5lQ8C6Fxez8GB9y1GMHLEygNiFjAqe+6Iyz4QEYWI7mv/j+IHyz/qTWHpcdrHxB5WDAHiRZ2+NBJt7PNCT6rlcco7wpHKlDJ5TzTiAqAoRa5O9bvaZQeDd5XlqAn7OMg3JnBWnikIAkBKcBKwX8gtNwE1Z4vJZ5V+M/BUF9cgyC4iRykIftZ0IEq9MLVv/YIQfI8uu/K9qjnSykOEu/ANSV2y8zwTmPZ4wOig/qHo8Isvvv2G+gK7amH1YGAzRfmVtpi/OBjGohwFcLR6LN4ydgH0/uMwiP3hRyYU+pTSi5hxAVKmIzSGk+NUoP7qsUlD2FmgWJ6EIEPG5+SDgnH2M+MWk08G/tS3b8LFaz/wR0MeOAIDoTP52KmLR0JchAu9srvUCURfE5yaEQN8dfYUHJvGn5qxZzUWAqLMF+YULkN/rgvbLUYLcLosf+CneMHGKITBeISAaf9nS68etSzdYpZ/qqo3GfyCAjhkHEFUmYn32fmFWh+8n7oITOA5xQIhAzSwUUV3icwyBKkMgz/hmkU9wX7rQc97UK3fujMZ/lcsdNPsxJwBSswgI6ZVzj2af859jHjwM82BMBAbttvhBA0LAIX/WEMfNnCsBdKNxP5ZtGTMRoLjRovkUYLQuWf+kb/TFcAAbWuM1A8Ugiq8bGwJ56+OfNWvzJrhYkF8sYbXW+A8EwrrgAKKKoROwEU/2X9v5IrQD/9fi66msIowdhSIAxeeGgwAIlk8xmaXzwcZE0js/9U/Y+sNxXg+NqQsOIAKE7rLLhxPt7+3+baDVWwglto/NEMRLUIhAnGIINBoECklB/pzZTmicN9Ub8gsw64oASIUsEUAcmLik+3alg8W5vOkJA4vGREAAFKeGgEA48/uM350EwvormdQs288kV08NqCsRoBgwkYJk77Vzzk94+jtQ0snZfCwOFMMovq5bCFiZvydrtmsdvKltaW2W9o4EGnVLAKQxERHoWTn3VVgHVqEYnJrJx9aBkXR0/E3NIOCQP2e6se6/uX3J+nujcVyzGpRRUN2JAMV1t9YBFCZtS9bfwf3XgfxbRJvKdV2xUcV1jq+bGAJGWVNfJh884+vgPEH+etH2D9Yrdc0BRJWOtKbZlZ2nsXLwB3hSzY+dhSLoxOc6gYBd049H6yMq5V/cetnap6NxWyf1G7Aadc0BRDUWxaBQ0tSS7vu18c6VTRJ73YbD9QTRu/E5hkCNISD+++Lbn+hJm98WlH9eoyC/wKkhOICoQ4UInAMx2L1y7vRkEHyztdU7nwVEYh0QQtZQbYnaFJ8bGgISx0Lj3qvhSL+zx8v9w6wrtuytZ5m/P7QbDmki4Ap7lZ7e+T9Q3ncAfNlrQChxQ3A0/Tsh/t2QECiwoM0nyrXqyZlPty/t/mdpRTQ+G6VFDUcA+gOZeAL/gZ/Av0nAm3xglYPxIqJGGX2NW0/r3YdZGn81tbR1aff1dlyyuhVxVbiChkkNOWOG1gFbd4Io/ns2p9+SC9R2WWkF5GMLQcMMvwarqNM3WWUfyL+uoMwFgvxwox4TkOzd11DIL9BvSA4gGjYCdLXK7ZeeuX7eSaYQfLWlRZ8a6gWkbQ1J4KL2xee6goCT91uQ99Pmrkyh8A+HXLnpGWH5FZvhMthqHsyjEtBpaASRoCKRr0DL5eseamnPvjydDb4BJ8Cu5Bb5Y/fhSoySOI+8T/DOlK80u/Ve29oy5zUW+dFDyfhrVOSXbm1oDqB4XBYrX/Yvn/t+zzMfQ0HTmiGugEFZA30eN20tbnd8XVUIyKweoGj2CeKxmxhWS9uWbfy6lFg83qpagypn3tAcQDFsIr0APabbl63/r4AwyyD//YgEkVKw4eSz4vbF1zWHgGj5xcQnyH+35xdeKsjfK+9XccPOWrZ03M2KQgDUzU4vsOVTMyZNak99AnFgicgExBbI8zTmBmo5whqvLJn13SaduQCFsv7P1lT3R/VlKjdeZv3iLhl3BCBqXHFnEWDkr9kq6zOwcvNQEEoHyzFuuJ+ozfF51BAQnZHf2qpVusc8DnIsbV3W/QvJtXg8jbqUOspg3CKBiARdYp7hYC32/7YmvLNA/hsJMKLRDUi7xVwohCBOMQTcrA+7DyhMJm1WZPPeiwX5BfGtiW+csPz9u3rccgDFDS2m3pkVnW8IjPpka5s+BnOOvGapfvH78XVTQUA0/Ilkklk/re5DRPxQ29L1dwoEisfNeIVIUxAA25lwAnIWZw1z/fypPYX8NTR+SejHHSkIxy1HNF4H8CjaJYRfIxZ6cIb7QfxPsT/Fp45atjojXKPk24iOPeXCo2kIQAQYOtcGHpXfuZVzXpYP9MchAi8RzQDxBmRQSOc3HVwEHk2ShNgb4kr4AVcohm8tGP3hiWzMKe1vhlm/uJ+bcqBbCn88rpvIdSLfpZfPfTvagg+j/DkqlzHctPoBkQebEj7FA2QcXeMOogoJXyd8epYFZH+E0n+8ZWn3D6WNgvjqEjz6mmx5eVMP8GJqb1hinA6C97Fh4dKWVj0lCyFAVxBzBI1PAUTRI9tw+4mUyPlmnTbqM1vy3pcOvWpdj0wA6prG9OOvRNc0NQEQADI6ev0G5PfeFR0n+sZfxkzw1zgRTWQPNxzAYkIgsGmwZBEfBZ8vCj6CdWylp7+qsmp5+/u710pbiieABmtbxarb9AQggqSYDK8OxQK5l10+9+SCNldy+RYURa0xIYggVfdnK+P3In7G7GDP+a8qP1jZevmmZ6T2zcruD9RzMQHoB5X+GuDs9XNOLxT0UjiCt8ARtObgCIhLaDXIfBpbDfrBbwx/WsSXPSR8Znxs+TsCY25kBc9/t/7TuqekXhbxH2UBWQMu260WXGMCMAhk+xOCfSvmnIEP0TvRCyxua9HTA0iA3bZMGE1ZeRgvNhoEklW8LQo7VDXSA2j1Wa+Hci9j1tMl38LL68bJS7sfl9JjxB+8D2ICMDhs7JP+hCD9uZkLTTJ5KQ/fxqBbCPKLRlnejbkCC7Ga/LGzPSX5mHCV4RehuB+GBn9NJbxvt79n3XqpRYz4w/dFTACGh5F9wxKCIh2BOBNl8rnFRuu/44WzMCHqQg7fAreZqXwT+xNYyFXsj1BZQXzFZpu+J6a8tF3c9UtQ/cZd6eAHsz+waZ88jxFfoFBaiglAaXDqfesgQoDyMD+z80X5grmUNYgXMSPNlZezcAWhGVF+xsRAoFB+6kX6SKknJIDNNp+D8P7A5As3TXjfxvuibGPEjyBR+jkmAKXDqs+b1n4chiOLHuy5oWNWSyHxWryL3sq9l8IVtMmAZYPISHEor8bEIALYwOeDkZ73Mhm1C6XeL5H5b2pLJn6mL39+R/S5RfwmdOKJ2j+ac0wARgM9vmW0Wj8CyUY8C6Ps9n5h1kleInmuDsxFqKfOaEnpSfJyHmJA9OLoPUJO8K+ZFYhOkSdILwciPA47hNq2upWM2c2t3+O4c0s2ULdPfl/3E/KOpHi2d3AY7d+YAIwWgkXfR1xBf5dSViAey7NzGeFvYGCfipgwTQa4cAdELZKRf4Ag2KFvnxblPK4uI2S3CE/LbGx9uxDXsffbAMifAc8PVULd3np5EdIjbin0MP3hO66gU+PGxASgSgCPdAX9B2v62nlHBX5wug7UqxjKL6b4hYgKSTv/gQCEmy7WHbj+aVQu4cDsLlC2CI8/hUV4S+IQhlDk0Wq1muM3OOv/3Av8+1qXrX1aPpDUS1Rj+70DSIX/xgSgwgAdKLteYtAvfLT5TMeEnjZzkqeSLwQ9Xqq0WcR5Hg5HE2w+oAZOR2JZkJ8RlyDX0m9CFgStxr4PHaJLvdDNgbPR3M7sTgAWhQKvV/MBwqep81qQ/REU+fcG+eAPrX72fr1su7D7NvG5E6tipI9AUrXz2A+eqjWtPjPuJQYDDG7z5RmTenr847XxT6BjTgMRTqcVCzhmwCWALySZLznyYmLgBZAtvNNLCIr79MA1VzKbHrhhczvoj+CuIHHvg4GuhOy4JGeP9z3wHESHJkXIzgOQXaq4lcsNyPF/1p7+A4q8h9KpnoenXbZjl8vC/R0KLsXvxdeVhcBw46GypcW59YEAyNGrQOwvKsiLgrDplR0LCsY/imWsh8MOHM3MeSwrFo9kLftMEG8S4c2S/LMvW7SEHMgMLEfAjd5rflOaVThI1pJ/mKLraCzI2V4LtQCn7Q/Bcvkth70hZ3mIDgOzXJZ7e8h2N289C216lEePUcpqowtPtXZser5YQSrlSttkUxe5Hqjt9n78p+oQkG6MU51AwCLFNSCGKLoG4BCiapobF7Rm9ufnmUJ+LgRhlud5HRCHecbz5pHHoWDVLLB6Eu+nOFrCcwpW3IsQWXCXUtzBSUiCSOMichTAYH4KZyFiBwujVYZX01zv4nobZ5nV1yGyrIHpX68LZoNO+t27VWaj7I7Ls4NSjPAHgaQubsQEoC66YeBKgGxadfUSBOmrYLiFLMJKr562MDlNbW9pU22TfT+YVDDBZKxrk2HDJ4CwiYJnEtqYBPw6IdI5G+MVlJdLeiZdCLysp/IZpncQ32S4v5uomDtadXqn2j4HIvBIvpQ6UFfR2MMLcHRRqiUxA7czvjt2EIgJwNjBfsQl9yEMksujdi4Paolokcx+N2W/4hGQXNSXV1sloNSIKsapESAQE4BG6KUy6miJg7wfoaCIFJJErChKFnGLfttLmbGL0qpViOeC2JJA7igHMurzXtEn8WWDQeD/AdFt4iH/JZC9AAAAAElFTkSuQmCC' const LND_ICON = - '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' + 'data:image/png;base64,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' const PROXY_ICON = - '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' + 'data:image/png;base64,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' -export { BTC_ICON, LND_ICON, PROXY_ICON } +export { REGISTRY_ICON, BTC_ICON, LND_ICON, PROXY_ICON } diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index c1f98dcd4..4ce3f670a 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -3,11 +3,26 @@ import { PackageDataEntry, } from 'src/app/services/patch-db/data-model' import { Metric, NotificationLevel, RR, ServerNotifications } from './api.types' -import { BTC_ICON, LND_ICON, PROXY_ICON } from './api-icons' -import { DependencyMetadata, MarketplacePkg } from '@start9labs/marketplace' +import { BTC_ICON, LND_ICON, PROXY_ICON, REGISTRY_ICON } from './api-icons' import { Log } from '@start9labs/shared' import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec' import { T, CB } from '@start9labs/start-sdk' +import { GetPackagesRes } from '@start9labs/marketplace' + +const mockBlake3Commitment: T.Blake3Commitment = { + hash: 'fakehash', + size: 0, +} + +const mockMerkleArchiveCommitment: T.MerkleArchiveCommitment = { + rootSighash: 'fakehash', + rootMaxsize: 0, +} + +const mockDescription = { + short: 'Lorem ipsum dolor sit amet', + long: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', +} export module Mock { export const ServerUpdated: T.ServerStatus = { @@ -37,17 +52,42 @@ export module Mock { }, } - export const ReleaseNotes: RR.GetReleaseNotesRes = { - '0.19.2': - 'Contrary to popular belief, Lorem Ipsum is not simply random text.', - '0.19.1': 'release notes for Bitcoin 0.19.1', - '0.19.0': 'release notes for Bitcoin 0.19.0', + export const RegistryInfo: T.RegistryInfo = { + name: 'Start9 Registry', + icon: REGISTRY_ICON, + categories: { + bitcoin: { + name: 'Bitcoin', + description: mockDescription, + }, + featured: { + name: 'Featured', + description: mockDescription, + }, + lightning: { + name: 'Lightning', + description: mockDescription, + }, + communications: { + name: 'Communications', + description: mockDescription, + }, + data: { + name: 'Data', + description: mockDescription, + }, + ai: { + name: 'AI', + description: mockDescription, + }, + }, } export const MockManifestBitcoind: T.Manifest = { id: 'bitcoind', title: 'Bitcoin Core', - version: '0.21.0', + version: '0.21.0:0', + satisfies: [], gitHash: 'abcdefgh', description: { short: 'A Bitcoin full node by Bitcoin Core.', @@ -90,7 +130,8 @@ export module Mock { export const MockManifestLnd: T.Manifest = { id: 'lnd', title: 'Lightning Network Daemon', - version: '0.11.1', + version: '0.11.1:0', + satisfies: [], gitHash: 'abcdefgh', description: { short: 'A bolt spec compliant client.', @@ -116,11 +157,13 @@ export module Mock { bitcoind: { description: 'LND needs bitcoin to live.', optional: true, + s9pk: '', }, 'btc-rpc-proxy': { description: 'As long as Bitcoin is pruned, LND needs Bitcoin Proxy to fetch block over the P2P network.', optional: true, + s9pk: '', }, }, hasConfig: true, @@ -143,7 +186,8 @@ export module Mock { export const MockManifestBitcoinProxy: T.Manifest = { id: 'btc-rpc-proxy', title: 'Bitcoin Proxy', - version: '0.2.2', + version: '0.2.2:0', + satisfies: [], gitHash: 'lmnopqrx', description: { short: 'A super charger for your Bitcoin node.', @@ -168,6 +212,7 @@ export module Mock { bitcoind: { description: 'Bitcoin Proxy requires a Bitcoin node.', optional: false, + s9pk: '', }, }, hasConfig: false, @@ -187,149 +232,509 @@ export module Mock { }, } - export const BitcoinDep: DependencyMetadata = { + export const BitcoinDep: T.DependencyMetadata = { title: 'Bitcoin Core', icon: BTC_ICON, optional: false, - hidden: true, + description: 'Needed to run', } - export const ProxyDep: DependencyMetadata = { + export const ProxyDep: T.DependencyMetadata = { title: 'Bitcoin Proxy', icon: PROXY_ICON, optional: true, - hidden: false, + description: 'Needed to run', } - export const MarketplacePkgs: { - [id: string]: { - [version: string]: MarketplacePkg - } + export const OtherPackageVersions: { + [id: T.PackageId]: GetPackagesRes } = { bitcoind: { - '0.19.0': { - icon: BTC_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: { - ...Mock.MockManifestBitcoind, - version: '0.19.0', + '=26.1.0:0.1.0': { + best: { + '26.1.0:0.1.0': { + title: 'Bitcoin Core', + description: mockDescription, + hardwareRequirements: { arch: null, device: {}, ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/bitcoind-startos', + upstreamRepo: 'https://github.com/bitcoin/bitcoin', + supportSite: 'https://bitcoin.org', + marketingSite: 'https://bitcoin.org', + releaseNotes: 'Even better support for Bitcoin and wallets!', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: BTC_ICON, + sourceVersion: null, + dependencyMetadata: {}, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/bitcoind-startos/releases/download/v26.1.0/bitcoind.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + '#knots:26.1.20240325:0': { + title: 'Bitcoin Knots', + description: { + short: 'An alternate fully verifying implementation of Bitcoin', + long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.', + }, + hardwareRequirements: { arch: null, device: {}, ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos', + upstreamRepo: 'https://github.com/bitcoinknots/bitcoin', + supportSite: 'https://bitcoinknots.org', + marketingSite: 'https://bitcoinknots.org', + releaseNotes: 'Even better support for Bitcoin and wallets!', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: BTC_ICON, + sourceVersion: null, + dependencyMetadata: {}, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/bitcoinknots-startos/releases/download/v26.1.20240513/bitcoind.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + }, + categories: ['bitcoin', 'featured'], + otherVersions: { + '27.0.0:1.0.0': { + releaseNotes: 'Even better support for Bitcoin and wallets!', + }, + '#knots:27.1.0:0': { + releaseNotes: 'Even better support for Bitcoin and wallets!', + }, }, - categories: ['bitcoin', 'cryptocurrency'], - versions: ['0.19.0', '0.20.0', '0.21.0'], - dependencyMetadata: {}, - publishedAt: new Date().toISOString(), }, - '0.20.0': { - icon: BTC_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: { - ...Mock.MockManifestBitcoind, - version: '0.20.0', + '=#knots:26.1.20240325:0': { + best: { + '26.1.0:0.1.0': { + title: 'Bitcoin Core', + description: mockDescription, + hardwareRequirements: { arch: null, device: {}, ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/bitcoind-startos', + upstreamRepo: 'https://github.com/bitcoin/bitcoin', + supportSite: 'https://bitcoin.org', + marketingSite: 'https://bitcoin.org', + releaseNotes: 'Even better support for Bitcoin and wallets!', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: BTC_ICON, + sourceVersion: null, + dependencyMetadata: {}, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/bitcoind-startos/releases/download/v26.1.0/bitcoind.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + '#knots:26.1.20240325:0': { + title: 'Bitcoin Knots', + description: { + short: 'An alternate fully verifying implementation of Bitcoin', + long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.', + }, + hardwareRequirements: { arch: null, device: {}, ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos', + upstreamRepo: 'https://github.com/bitcoinknots/bitcoin', + supportSite: 'https://bitcoinknots.org', + marketingSite: 'https://bitcoinknots.org', + releaseNotes: 'Even better support for Bitcoin and wallets!', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: BTC_ICON, + sourceVersion: null, + dependencyMetadata: {}, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/bitcoinknots-startos/releases/download/v26.1.20240513/bitcoind.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, }, - categories: ['bitcoin', 'cryptocurrency'], - versions: ['0.19.0', '0.20.0', '0.21.0'], - dependencyMetadata: {}, - publishedAt: new Date().toISOString(), - }, - '0.21.0': { - icon: BTC_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: { - ...Mock.MockManifestBitcoind, - version: '0.21.0', - releaseNotes: - 'For a complete list of changes, please visit https://bitcoincore.org/en/releases/0.21.0/
  • Taproot!
  • New RPCs
  • Experimental Descriptor Wallets
', + categories: ['bitcoin', 'featured'], + otherVersions: { + '27.0.0:1.0.0': { + releaseNotes: 'Even better support for Bitcoin and wallets!', + }, + '#knots:27.1.0:0': { + releaseNotes: 'Even better support for Bitcoin and wallets!', + }, }, - categories: ['bitcoin', 'cryptocurrency'], - versions: ['0.19.0', '0.20.0', '0.21.0'], - dependencyMetadata: {}, - publishedAt: new Date().toISOString(), - }, - latest: { - icon: BTC_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: { - ...Mock.MockManifestBitcoind, - releaseNotes: - 'For a complete list of changes, please visit https://bitcoincore.org/en/releases/0.21.0/
Or in [markdown](https://bitcoincore.org/en/releases/0.21.0/)
  • Taproot!
  • New RPCs
  • Experimental Descriptor Wallets
', - }, - categories: ['bitcoin', 'cryptocurrency'], - versions: ['0.19.0', '0.20.0', '0.21.0'], - dependencyMetadata: {}, - publishedAt: new Date().toISOString(), }, }, lnd: { - '0.11.0': { - icon: LND_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: { - ...Mock.MockManifestLnd, - version: '0.11.0', - releaseNotes: 'release notes for LND 0.11.0', + '=0.17.5:0': { + best: { + '0.17.5:0': { + title: 'LND', + description: mockDescription, + hardwareRequirements: { arch: null, device: {}, ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/lnd-startos', + upstreamRepo: 'https://github.com/lightningnetwork/lnd', + supportSite: 'https://lightning.engineering/slack.html', + marketingSite: 'https://lightning.engineering/', + releaseNotes: 'Upstream release to 0.17.5', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: LND_ICON, + sourceVersion: null, + dependencyMetadata: { + bitcoind: { + title: 'Bitcoin Core', + icon: BTC_ICON, + description: 'Used for RPC requests', + optional: false, + }, + 'btc-rpc-proxy': { + title: 'Bitcoin Proxy', + icon: PROXY_ICON, + description: 'Used for authorized proxying of RPC requests', + optional: true, + }, + }, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/lnd-startos/releases/download/v0.17.5/lnd.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, }, - categories: ['bitcoin', 'lightning', 'cryptocurrency'], - versions: ['0.11.0', '0.11.1'], - dependencyMetadata: { - bitcoind: BitcoinDep, - 'btc-rpc-proxy': ProxyDep, + categories: ['lightning', 'featured'], + otherVersions: { + '0.18.0:0.0.1': { + releaseNotes: 'Upstream release and minor fixes.', + }, + '0.17.4-beta:1.0-alpha': { + releaseNotes: 'Upstream release to 0.17.4', + }, }, - publishedAt: new Date().toISOString(), }, - '0.11.1': { - icon: LND_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: { - ...Mock.MockManifestLnd, - version: '0.11.1', - releaseNotes: 'release notes for LND 0.11.1', + '=0.17.4-beta:1.0-alpha': { + best: { + '0.17.4-beta:1.0-alpha': { + title: 'LND', + description: mockDescription, + hardwareRequirements: { arch: null, device: {}, ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/lnd-startos', + upstreamRepo: 'https://github.com/lightningnetwork/lnd', + supportSite: 'https://lightning.engineering/slack.html', + marketingSite: 'https://lightning.engineering/', + releaseNotes: 'Upstream release to 0.17.4', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: LND_ICON, + sourceVersion: null, + dependencyMetadata: { + bitcoind: { + title: 'Bitcoin Core', + icon: BTC_ICON, + description: 'Used for RPC requests', + optional: false, + }, + 'btc-rpc-proxy': { + title: 'Bitcoin Proxy', + icon: PROXY_ICON, + description: 'Used for authorized proxying of RPC requests', + optional: true, + }, + }, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/lnd-startos/releases/download/v0.17.4/lnd.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, }, - categories: ['bitcoin', 'lightning', 'cryptocurrency'], - versions: ['0.11.0', '0.11.1'], - dependencyMetadata: { - bitcoind: BitcoinDep, - 'btc-rpc-proxy': ProxyDep, + categories: ['lightning', 'featured'], + otherVersions: { + '0.18.0:0.0.1': { + releaseNotes: 'Upstream release and minor fixes.', + }, + '0.17.5:0': { + releaseNotes: 'Upstream release to 0.17.5', + }, }, - publishedAt: new Date().toISOString(), - }, - latest: { - icon: LND_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: Mock.MockManifestLnd, - categories: ['bitcoin', 'lightning', 'cryptocurrency'], - versions: ['0.11.0', '0.11.1'], - dependencyMetadata: { - bitcoind: BitcoinDep, - 'btc-rpc-proxy': ProxyDep, - }, - publishedAt: new Date(new Date().valueOf() + 10).toISOString(), }, }, 'btc-rpc-proxy': { - latest: { - icon: PROXY_ICON, - license: 'licenseUrl', - instructions: 'instructionsUrl', - manifest: Mock.MockManifestBitcoinProxy, - categories: ['bitcoin'], - versions: ['0.2.2'], - dependencyMetadata: { - bitcoind: BitcoinDep, + '=0.3.2.6:0': { + best: { + '0.3.2.6:0': { + title: 'Bitcoin Proxy', + description: mockDescription, + hardwareRequirements: { arch: null, device: {}, ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/Start9Labs/btc-rpc-proxy-wrappers', + upstreamRepo: 'https://github.com/Kixunil/btc-rpc-proxy', + supportSite: 'https://github.com/Kixunil/btc-rpc-proxy/issues', + marketingSite: '', + releaseNotes: 'Upstream release and minor fixes.', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: PROXY_ICON, + sourceVersion: null, + dependencyMetadata: {}, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/btc-rpc-proxy-startos/releases/download/v0.3.2.7.1/btc-rpc-proxy.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + }, + categories: ['bitcoin'], + otherVersions: { + '0.3.2.7:0': { + releaseNotes: 'Upstream release and minor fixes.', + }, }, - publishedAt: new Date().toISOString(), }, }, } - export const MarketplacePkgsList: RR.GetMarketplacePackagesRes = - Object.values(Mock.MarketplacePkgs).map(service => service['latest']) + export const RegistryPackages: GetPackagesRes = { + bitcoind: { + best: { + '27.0.0:1.0.0': { + title: 'Bitcoin Core', + description: mockDescription, + hardwareRequirements: { arch: null, device: {}, ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/bitcoind-startos', + upstreamRepo: 'https://github.com/bitcoin/bitcoin', + supportSite: 'https://bitcoin.org', + marketingSite: 'https://bitcoin.org', + releaseNotes: 'Even better support for Bitcoin and wallets!', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: BTC_ICON, + sourceVersion: null, + dependencyMetadata: {}, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/bitcoind-startos/releases/download/v27.0.0/bitcoind.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + '#knots:27.1.0:0': { + title: 'Bitcoin Knots', + description: { + short: 'An alternate fully verifying implementation of Bitcoin', + long: 'Bitcoin Knots is a combined Bitcoin node and wallet. Not only is it easy to use, but it also ensures bitcoins you receive are both real bitcoins and really yours.', + }, + hardwareRequirements: { arch: null, device: {}, ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/bitcoinknots-startos', + upstreamRepo: 'https://github.com/bitcoinknots/bitcoin', + supportSite: 'https://bitcoinknots.org', + marketingSite: 'https://bitcoinknots.org', + releaseNotes: 'Even better support for Bitcoin and wallets!', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: BTC_ICON, + sourceVersion: null, + dependencyMetadata: {}, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/bitcoinknots-startos/releases/download/v26.1.20240513/bitcoind.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + }, + categories: ['bitcoin', 'featured'], + otherVersions: { + '26.1.0:0.1.0': { + releaseNotes: 'Even better support for Bitcoin and wallets!', + }, + '#knots:26.1.20240325:0': { + releaseNotes: 'Even better Knots support for Bitcoin and wallets!', + }, + }, + }, + lnd: { + best: { + '0.18.0:0.0.1': { + title: 'LND', + description: mockDescription, + hardwareRequirements: { arch: null, device: {}, ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/start9labs/lnd-startos', + upstreamRepo: 'https://github.com/lightningnetwork/lnd', + supportSite: 'https://lightning.engineering/slack.html', + marketingSite: 'https://lightning.engineering/', + releaseNotes: 'Upstream release and minor fixes.', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: LND_ICON, + sourceVersion: null, + dependencyMetadata: { + bitcoind: { + title: 'Bitcoin Core', + icon: BTC_ICON, + description: 'Used for RPC requests', + optional: false, + }, + 'btc-rpc-proxy': { + title: 'Bitcoin Proxy', + icon: null, + description: 'Used for authorized RPC requests', + optional: true, + }, + }, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/lnd-startos/releases/download/v0.18.0.1/lnd.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + }, + categories: ['lightning', 'featured'], + otherVersions: { + '0.17.5:0': { + releaseNotes: 'Upstream release to 0.17.5', + }, + '0.17.4-beta:1.0-alpha': { + releaseNotes: 'Upstream release to 0.17.4', + }, + }, + }, + 'btc-rpc-proxy': { + best: { + '0.3.2.7:0': { + title: 'Bitcoin Proxy', + description: mockDescription, + hardwareRequirements: { arch: null, device: {}, ram: null }, + license: 'mit', + wrapperRepo: 'https://github.com/Start9Labs/btc-rpc-proxy-wrappers', + upstreamRepo: 'https://github.com/Kixunil/btc-rpc-proxy', + supportSite: 'https://github.com/Kixunil/btc-rpc-proxy/issues', + marketingSite: '', + releaseNotes: 'Upstream release and minor fixes.', + osVersion: '0.3.6', + gitHash: 'fakehash', + icon: PROXY_ICON, + sourceVersion: null, + dependencyMetadata: {}, + donationUrl: null, + alerts: { + install: 'test', + uninstall: 'test', + start: 'test', + stop: 'test', + restore: 'test', + }, + s9pk: { + url: 'https://github.com/Start9Labs/btc-rpc-proxy-startos/releases/download/v0.3.2.7/btc-rpc-proxy.s9pk', + commitment: mockMerkleArchiveCommitment, + signatures: {}, + publishedAt: Date.now().toString(), + }, + }, + }, + categories: ['bitcoin'], + otherVersions: { + '0.3.2.6:0': { + releaseNotes: 'Upstream release and minor fixes.', + }, + }, + }, + } export const Notifications: ServerNotifications = [ { @@ -658,13 +1063,13 @@ export module Mock { packageBackups: { bitcoind: { title: 'Bitcoin Core', - version: '0.21.0', + version: '0.21.0:0', osVersion: '0.3.6', timestamp: new Date().toISOString(), }, 'btc-rpc-proxy': { title: 'Bitcoin Proxy', - version: '0.2.2', + version: '0.2.2:0', osVersion: '0.3.6', timestamp: new Date().toISOString(), }, @@ -1489,8 +1894,7 @@ export module Mock { title: Mock.MockManifestBitcoind.title, icon: 'assets/img/service-icons/bitcoind.svg', kind: 'running', - registryUrl: '', - versionSpec: '>=26.0.0', + versionRange: '>=26.0.0', healthChecks: [], configSatisfied: true, }, @@ -1576,8 +1980,7 @@ export module Mock { title: Mock.MockManifestBitcoind.title, icon: 'assets/img/service-icons/bitcoind.svg', kind: 'running', - registryUrl: 'https://registry.start9.com', - versionSpec: '>=26.0.0', + versionRange: '>=26.0.0', healthChecks: [], configSatisfied: true, }, @@ -1585,8 +1988,7 @@ export module Mock { title: Mock.MockManifestBitcoinProxy.title, icon: 'assets/img/service-icons/btc-rpc-proxy.png', kind: 'exists', - registryUrl: 'https://community-registry.start9.com', - versionSpec: '>2.0.0', + versionRange: '>2.0.0', configSatisfied: false, }, }, diff --git a/web/projects/ui/src/app/services/api/api.types.ts b/web/projects/ui/src/app/services/api/api.types.ts index 5ada03cad..770792328 100644 --- a/web/projects/ui/src/app/services/api/api.types.ts +++ b/web/projects/ui/src/app/services/api/api.types.ts @@ -1,5 +1,4 @@ import { Dump } from 'patch-db-client' -import { MarketplacePkg, StoreInfo } from '@start9labs/marketplace' import { PackagePropertiesVersioned } from 'src/app/util/properties.util' import { DataModel } from 'src/app/services/patch-db/data-model' import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared' @@ -223,12 +222,7 @@ export module RR { export type GetPackageMetricsReq = { id: string } // package.metrics export type GetPackageMetricsRes = Metric - export type InstallPackageReq = { - id: string - versionSpec?: string - versionPriority?: 'min' | 'max' - registry: string - } // package.install + export type InstallPackageReq = T.InstallParams export type InstallPackageRes = null export type GetPackageConfigReq = { id: string } // package.config.get @@ -287,26 +281,13 @@ export module RR { progress: string // guid } - // marketplace + // registry - export type GetMarketplaceInfoReq = { serverId: string } - export type GetMarketplaceInfoRes = StoreInfo + /** these are returned in ASCENDING order. the newest available version will be the LAST in the object */ + export type GetRegistryOsUpdateRes = { [version: string]: T.OsVersionInfo } export type CheckOSUpdateReq = { serverId: string } export type CheckOSUpdateRes = OSUpdate - - export type GetMarketplacePackagesReq = { - ids?: { id: string; version: string }[] - // iff !ids - category?: string - query?: string - page?: number - perPage?: number - } - export type GetMarketplacePackagesRes = MarketplacePkg[] - - export type GetReleaseNotesReq = { id: string } - export type GetReleaseNotesRes = { [version: string]: string } } export interface OSUpdate { diff --git a/web/projects/ui/src/app/services/api/embassy-api.service.ts b/web/projects/ui/src/app/services/api/embassy-api.service.ts index a5bce8c62..579292300 100644 --- a/web/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-api.service.ts @@ -1,17 +1,32 @@ import { Observable } from 'rxjs' import { RR } from './api.types' +import { RPCOptions } from '@start9labs/shared' +import { T } from '@start9labs/start-sdk' +import { + GetPackageRes, + GetPackagesRes, + MarketplacePkg, +} from '@start9labs/marketplace' export abstract class ApiService { // http - // for getting static files: ex icons, instructions, licenses - abstract getStatic(url: string): Promise - // for sideloading packages abstract uploadPackage(guid: string, body: Blob): Promise abstract uploadFile(body: Blob): Promise + // for getting static files: ex icons, instructions, licenses + abstract getStaticProxy( + pkg: MarketplacePkg, + path: 'LICENSE.md' | 'instructions.md', + ): Promise + + abstract getStaticInstalled( + id: T.PackageId, + path: 'LICENSE.md' | 'instructions.md', + ): Promise + // websocket abstract openWebsocket$( @@ -120,14 +135,23 @@ export abstract class ApiService { // marketplace URLs - abstract marketplaceProxy( - path: string, - params: Record, - url: string, + abstract registryRequest( + registryUrl: string, + options: RPCOptions, ): Promise abstract checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise + abstract getRegistryInfo(registryUrl: string): Promise + + abstract getRegistryPackage( + url: string, + id: string, + versionRange: string | null, + ): Promise + + abstract getRegistryPackages(registryUrl: string): Promise + // notification abstract getNotifications( diff --git a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts index cef2c13ff..87be49e36 100644 --- a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -18,6 +18,15 @@ import { AuthService } from '../auth.service' import { DOCUMENT } from '@angular/common' import { DataModel } from '../patch-db/data-model' import { Dump, pathFromArray } from 'patch-db-client' +import { T } from '@start9labs/start-sdk' +import { + GetPackageReq, + GetPackageRes, + GetPackagesReq, + GetPackagesRes, + MarketplacePkg, +} from '@start9labs/marketplace' +import { blake3 } from '@noble/hashes/blake3' @Injectable() export class LiveApiService extends ApiService { @@ -29,17 +38,7 @@ export class LiveApiService extends ApiService { @Inject(PATCH_CACHE) private readonly cache$: Observable>, ) { super() - ; (window as any).rpcClient = this - } - - // for getting static files: ex icons, instructions, licenses - - async getStatic(url: string): Promise { - return this.httpRequest({ - method: Method.GET, - url, - responseType: 'text', - }) + ;(window as any).rpcClient = this } // for sideloading packages @@ -62,6 +61,36 @@ export class LiveApiService extends ApiService { }) } + // for getting static files: ex. instructions, licenses + + async getStaticProxy( + pkg: MarketplacePkg, + path: 'LICENSE.md' | 'instructions.md', + ): Promise { + const encodedUrl = encodeURIComponent(pkg.s9pk.url) + + return this.httpRequest({ + method: Method.GET, + url: `/s9pk/proxy/${encodedUrl}/${path}`, + params: { + rootSighash: pkg.s9pk.commitment.rootSighash, + rootMaxsize: pkg.s9pk.commitment.rootMaxsize, + }, + responseType: 'text', + }) + } + + async getStaticInstalled( + id: T.PackageId, + path: 'LICENSE.md' | 'instructions.md', + ): Promise { + return this.httpRequest({ + method: Method.GET, + url: `/s9pk/installed/${id}.s9pk/${path}`, + responseType: 'text', + }) + } + // websocket openWebsocket$( @@ -257,24 +286,63 @@ export class LiveApiService extends ApiService { // marketplace URLs - async marketplaceProxy( - path: string, - qp: Record, - baseUrl: string, + async registryRequest( + registryUrl: string, + options: RPCOptions, ): Promise { - const fullUrl = `${baseUrl}${path}?${new URLSearchParams(qp).toString()}` return this.rpcRequest({ - method: 'marketplace.get', - params: { url: fullUrl }, + ...options, + method: `registry.${options.method}`, + params: { registry: registryUrl, ...options.params }, }) } async checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise { - return this.marketplaceProxy( - '/eos/v0/latest', - qp, - this.config.marketplace.start9, - ) + const { serverId } = qp + + return this.registryRequest(this.config.marketplace.start9, { + method: 'os.version.get', + params: { serverId }, + }) + } + + async getRegistryInfo(registryUrl: string): Promise { + return this.registryRequest(registryUrl, { + method: 'info', + params: {}, + }) + } + + async getRegistryPackage( + registryUrl: string, + id: string, + versionRange: string | null, + ): Promise { + const params: GetPackageReq = { + id, + version: versionRange, + sourceVersion: null, + otherVersions: 'short', + } + + return this.registryRequest(registryUrl, { + method: 'package.get', + params, + }) + } + + async getRegistryPackages(registryUrl: string): Promise { + const params: GetPackagesReq = { + id: null, + version: null, + sourceVersion: null, + otherVersions: 'short', + } + + return this.registryRequest(registryUrl, { + method: 'package.get', + params, + }) } // notification @@ -504,6 +572,29 @@ export class LiveApiService extends ApiService { private async httpRequest(opts: HttpOptions): Promise { const res = await this.http.httpRequest(opts) + if (res.headers.get('Repr-Digest')) { + // verify + const digest = res.headers.get('Repr-Digest')! + let data: Uint8Array + if (opts.responseType === 'arrayBuffer') { + data = Buffer.from(res.body as ArrayBuffer) + } else if (opts.responseType === 'text') { + data = Buffer.from(res.body as string) + } else if ((opts.responseType as string) === 'blob') { + data = Buffer.from(await (res.body as Blob).arrayBuffer()) + } else { + console.warn( + `could not verify Repr-Digest for responseType ${ + opts.responseType || 'json' + }`, + ) + return res.body + } + const computedDigest = Buffer.from(blake3(data)).toString('base64') + if (`blake3=:${computedDigest}:` === digest) return res.body + console.debug(computedDigest, digest) + throw new Error('File digest mismatch.') + } return res.body } } diff --git a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts index 21491dc52..e063fb6e4 100644 --- a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core' -import { Log, RPCErrorDetails, pauseFor } from '@start9labs/shared' +import { Log, RPCErrorDetails, RPCOptions, pauseFor } from '@start9labs/shared' import { ApiService } from './embassy-api.service' import { Operation, @@ -30,8 +30,12 @@ import { } from 'rxjs' import { mockPatchData } from './mock-patch' import { AuthService } from '../auth.service' -import { StoreInfo } from '@start9labs/marketplace' import { T } from '@start9labs/start-sdk' +import { + GetPackageRes, + GetPackagesRes, + MarketplacePkg, +} from '@start9labs/marketplace' const PROGRESS: T.FullProgress = { overall: { @@ -48,10 +52,7 @@ const PROGRESS: T.FullProgress = { }, { name: 'Validating', - progress: { - done: 0, - total: 40, - }, + progress: null, }, { name: 'Installing', @@ -80,14 +81,25 @@ export class MockApiService extends ApiService { .subscribe() } - async getStatic(url: string): Promise { + async uploadPackage(guid: string, body: Blob): Promise { + await pauseFor(2000) + return 'success' + } + + async getStaticProxy( + pkg: MarketplacePkg, + path: 'LICENSE.md' | 'instructions.md', + ): Promise { await pauseFor(2000) return markdown } - async uploadPackage(guid: string, body: Blob): Promise { + async getStaticInstalled( + id: T.PackageId, + path: 'LICENSE.md' | 'instructions.md', + ): Promise { await pauseFor(2000) - return 'success' + return markdown } // websocket @@ -136,7 +148,7 @@ export class MockApiService extends ApiService { this.stateIndex++ - return this.stateIndex === 1 ? 'initializing' : 'running' + return this.stateIndex === 1 ? 'running' : 'running' } // db @@ -448,34 +460,13 @@ export class MockApiService extends ApiService { // marketplace URLs - async marketplaceProxy( - path: string, - params: Record, - url: string, + async registryRequest( + registryUrl: string, + options: RPCOptions, ): Promise { await pauseFor(2000) - if (path === '/package/v0/info') { - const info: StoreInfo = { - name: 'Start9 Registry', - categories: [ - 'bitcoin', - 'lightning', - 'data', - 'featured', - 'messaging', - 'social', - 'alt coin', - ], - } - return info - } else if (path === '/package/v0/index') { - return Mock.MarketplacePkgsList - } else if (path.startsWith('/package/v0/release-notes')) { - return Mock.ReleaseNotes - } else if (path.includes('instructions') || path.includes('license')) { - return markdown - } + return Error('do not call directly') } async checkOSUpdate(qp: RR.CheckOSUpdateReq): Promise { @@ -483,6 +474,29 @@ export class MockApiService extends ApiService { return Mock.MarketplaceEos } + async getRegistryInfo(registryUrl: string): Promise { + await pauseFor(2000) + return Mock.RegistryInfo + } + + async getRegistryPackage( + url: string, + id: string, + versionRange: string, + ): Promise { + await pauseFor(2000) + if (!versionRange) { + return Mock.RegistryPackages[id] + } else { + return Mock.OtherPackageVersions[id][versionRange] + } + } + + async getRegistryPackages(registryUrl: string): Promise { + await pauseFor(2000) + return Mock.RegistryPackages + } + // notification async getNotifications( @@ -742,11 +756,11 @@ export class MockApiService extends ApiService { ...Mock.LocalPkgs[params.id], stateInfo: { // if installing - // state: PackageState.Installing, + state: 'installing', // if updating - state: 'updating', - manifest: mockPatchData.packageData[params.id].stateInfo.manifest!, + // state: 'updating', + // manifest: mockPatchData.packageData[params.id].stateInfo.manifest!, // both installingInfo: { @@ -1129,11 +1143,7 @@ export class MockApiService extends ApiService { const progress = JSON.parse(JSON.stringify(PROGRESS)) for (let [i, phase] of progress.phases.entries()) { - if ( - !phase.progress || - typeof phase.progress !== 'object' || - !phase.progress.total - ) { + if (!phase.progress || phase.progress === true || !phase.progress.total) { await pauseFor(2000) const patches: Operation[] = [ diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 8411b7b7c..7b0d6b1df 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -88,7 +88,7 @@ export const mockPatchData: DataModel = { state: 'installed', manifest: { ...Mock.MockManifestBitcoind, - version: '0.20.0', + version: '0.20.0:0', }, }, icon: '/assets/img/service-icons/bitcoind.svg', @@ -295,7 +295,7 @@ export const mockPatchData: DataModel = { state: 'installed', manifest: { ...Mock.MockManifestLnd, - version: '0.11.0', + version: '0.11.0:0.0.1', }, }, icon: '/assets/img/service-icons/lnd.png', @@ -368,8 +368,7 @@ export const mockPatchData: DataModel = { title: 'Bitcoin Core', icon: 'assets/img/service-icons/bitcoind.svg', kind: 'running', - registryUrl: 'https://registry.start9.com', - versionSpec: '>=26.0.0', + versionRange: '>=26.0.0', healthChecks: [], configSatisfied: true, }, @@ -377,8 +376,7 @@ export const mockPatchData: DataModel = { title: 'Bitcoin Proxy', icon: 'assets/img/service-icons/btc-rpc-proxy.png', kind: 'running', - registryUrl: 'https://community-registry.start9.com', - versionSpec: '>2.0.0', + versionRange: '>2.0.0', healthChecks: [], configSatisfied: false, }, diff --git a/web/projects/ui/src/app/services/dep-error.service.ts b/web/projects/ui/src/app/services/dep-error.service.ts index 5abee0d6b..a46a5123e 100644 --- a/web/projects/ui/src/app/services/dep-error.service.ts +++ b/web/projects/ui/src/app/services/dep-error.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core' -import { Emver } from '@start9labs/shared' +import { Exver } from '@start9labs/shared' import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators' import { PatchDB } from 'patch-db-client' import { @@ -39,7 +39,7 @@ export class DepErrorService { ) constructor( - private readonly emver: Emver, + private readonly exver: Exver, private readonly patch: PatchDB, ) {} @@ -87,11 +87,17 @@ export class DepErrorService { const depManifest = dep.stateInfo.manifest // incorrect version - if (!this.emver.satisfies(depManifest.version, currentDep.versionSpec)) { - return { - type: 'incorrectVersion', - expected: currentDep.versionSpec, - received: depManifest.version, + if (!this.exver.satisfies(depManifest.version, currentDep.versionRange)) { + if ( + depManifest.satisfies.some( + v => !this.exver.satisfies(v, currentDep.versionRange), + ) + ) { + return { + type: 'incorrectVersion', + expected: currentDep.versionRange, + received: depManifest.version, + } } } diff --git a/web/projects/ui/src/app/services/eos.service.ts b/web/projects/ui/src/app/services/eos.service.ts index 81b97bf11..c61c667e3 100644 --- a/web/projects/ui/src/app/services/eos.service.ts +++ b/web/projects/ui/src/app/services/eos.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@angular/core' -import { Emver } from '@start9labs/shared' import { BehaviorSubject, combineLatest } from 'rxjs' import { distinctUntilChanged, map } from 'rxjs/operators' import { OSUpdate } from 'src/app/services/api/api.types' @@ -7,6 +6,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service' import { PatchDB } from 'patch-db-client' import { getServerInfo } from 'src/app/util/get-server-info' import { DataModel } from './patch-db/data-model' +import { Exver } from '@start9labs/shared' @Injectable({ providedIn: 'root', @@ -47,15 +47,15 @@ export class EOSService { constructor( private readonly api: ApiService, - private readonly emver: Emver, private readonly patch: PatchDB, + private readonly exver: Exver, ) {} async loadEos(): Promise { const { version, id } = await getServerInfo(this.patch) this.osUpdate = await this.api.checkOSUpdate({ serverId: id }) const updateAvailable = - this.emver.compare(this.osUpdate.version, version) === 1 + this.exver.compareOsVersion(this.osUpdate.version, version) === 'greater' this.updateAvailable$.next(updateAvailable) } } diff --git a/web/projects/ui/src/app/services/marketplace.service.ts b/web/projects/ui/src/app/services/marketplace.service.ts index 40d2247e7..6ce3b136f 100644 --- a/web/projects/ui/src/app/services/marketplace.service.ts +++ b/web/projects/ui/src/app/services/marketplace.service.ts @@ -1,11 +1,11 @@ -import { Injectable } from '@angular/core' +import { Inject, Injectable } from '@angular/core' import { - MarketplacePkg, AbstractMarketplaceService, StoreData, Marketplace, - StoreInfo, StoreIdentity, + MarketplacePkg, + GetPackageRes, } from '@start9labs/marketplace' import { BehaviorSubject, @@ -37,8 +37,9 @@ import { tap, } from 'rxjs/operators' import { ConfigService } from './config.service' -import { sameUrl } from '@start9labs/shared' +import { Exver, sameUrl } from '@start9labs/shared' import { ClientStorageService } from './client-storage.service' +import { ExtendedVersion, T } from '@start9labs/start-sdk' @Injectable() export class MarketplaceService implements AbstractMarketplaceService { @@ -93,11 +94,9 @@ export class MarketplaceService implements AbstractMarketplaceService { mergeMap(({ url, name }) => this.fetchStore$(url).pipe( tap(data => { - if (data?.info) this.updateStoreName(url, name, data.info.name) - }), - map(data => { - return [url, data] + if (data?.info.name) this.updateStoreName(url, name, data.info.name) }), + map(data => [url, data]), startWith<[string, StoreData | null]>([url, null]), ), ), @@ -148,6 +147,7 @@ export class MarketplaceService implements AbstractMarketplaceService { private readonly patch: PatchDB, private readonly config: ConfigService, private readonly clientStorageService: ClientStorageService, + private readonly exver: Exver, ) {} getKnownHosts$(filtered = false): Observable { @@ -170,28 +170,29 @@ export class MarketplaceService implements AbstractMarketplaceService { getPackage$( id: string, - version: string, - optionalUrl?: string, + version: string | null, + flavor: string | null, + registryUrl?: string, ): Observable { - return this.patch.watch$('ui', 'marketplace').pipe( - switchMap(uiMarketplace => { - const url = optionalUrl || uiMarketplace.selectedUrl + return this.selectedHost$.pipe( + switchMap(selected => + this.marketplace$.pipe( + switchMap(m => { + const url = registryUrl || selected.url - if (version !== '*' || !uiMarketplace.knownHosts[url]) { - return this.fetchPackage$(id, version, url) - } + const pkg = m[url]?.packages.find( + p => + p.id === id && + p.flavor === flavor && + (!version || this.exver.compareExver(p.version, version) === 0), + ) - return this.marketplace$.pipe( - map(m => m[url]), - filter(Boolean), - take(1), - map( - store => - store.packages.find(p => p.manifest.id === id) || - ({} as MarketplacePkg), - ), - ) - }), + return !!pkg + ? of(pkg) + : this.fetchPackage$(url, id, version, flavor) + }), + ), + ), ) } @@ -210,56 +211,22 @@ export class MarketplaceService implements AbstractMarketplaceService { ): Promise { const params: RR.InstallPackageReq = { id, - versionSpec: `=${version}`, + version, registry: url, } await this.api.installPackage(params) } - fetchInfo$(url: string): Observable { - return this.patch.watch$('serverInfo').pipe( - take(1), - switchMap(serverInfo => { - const qp: RR.GetMarketplaceInfoReq = { serverId: serverInfo.id } - return this.api.marketplaceProxy( - '/package/v0/info', - qp, - url, - ) - }), - ) + fetchInfo$(url: string): Observable { + return from(this.api.getRegistryInfo(url)) } - fetchReleaseNotes$( - id: string, - url?: string, - ): Observable> { - return this.selectedHost$.pipe( - switchMap(m => { - return from( - this.api.marketplaceProxy>( - `/package/v0/release-notes/${id}`, - {}, - url || m.url, - ), - ) - }), - ) - } - - fetchStatic$(id: string, type: string, url?: string): Observable { - return this.selectedHost$.pipe( - switchMap(m => { - return from( - this.api.marketplaceProxy( - `/package/v0/${type}/${id}`, - {}, - url || m.url, - ), - ) - }), - ) + fetchStatic$( + pkg: MarketplacePkg, + type: 'LICENSE.md' | 'instructions.md', + ): Observable { + return from(this.api.getStaticProxy(pkg, type)) } private fetchStore$(url: string): Observable { @@ -273,33 +240,57 @@ export class MarketplaceService implements AbstractMarketplaceService { ) } - private fetchPackages$( - url: string, - params: Omit = {}, - ): Observable { - const qp: RR.GetMarketplacePackagesReq = { - ...params, - page: 1, - perPage: 100, - } - if (qp.ids) qp.ids = JSON.stringify(qp.ids) - - return from( - this.api.marketplaceProxy( - '/package/v0/index', - qp, - url, - ), + private fetchPackages$(url: string): Observable { + return from(this.api.getRegistryPackages(url)).pipe( + map(packages => { + return Object.entries(packages).flatMap(([id, pkgInfo]) => + Object.keys(pkgInfo.best).map(version => + this.convertToMarketplacePkg( + id, + version, + this.exver.getFlavor(version), + pkgInfo, + ), + ), + ) + }), ) } - private fetchPackage$( + convertToMarketplacePkg( id: string, - version: string, + version: string | null, + flavor: string | null, + pkgInfo: GetPackageRes, + ): MarketplacePkg { + version = + version || + Object.keys(pkgInfo.best).find(v => this.exver.getFlavor(v) === flavor) || + null + + return !version || !pkgInfo.best[version] + ? ({} as MarketplacePkg) + : { + id, + version, + flavor, + ...pkgInfo, + ...pkgInfo.best[version], + } + } + + private fetchPackage$( url: string, + id: string, + version: string | null, + flavor: string | null, ): Observable { - return this.fetchPackages$(url, { ids: [{ id, version }] }).pipe( - map(pkgs => pkgs[0] || {}), + return from( + this.api.getRegistryPackage(url, id, version ? `=${version}` : null), + ).pipe( + map(pkgInfo => + this.convertToMarketplacePkg(id, version, flavor, pkgInfo), + ), ) } diff --git a/web/projects/ui/src/app/services/patch-db/data-model.ts b/web/projects/ui/src/app/services/patch-db/data-model.ts index 33daecda7..d7deb31df 100644 --- a/web/projects/ui/src/app/services/patch-db/data-model.ts +++ b/web/projects/ui/src/app/services/patch-db/data-model.ts @@ -50,6 +50,10 @@ export type PackageDataEntry = stateInfo: T } +export type AllPackageData = NonNullable< + T.AllPackageData & Record> +> + export type StateInfo = InstalledState | InstallingState | UpdatingState export type InstalledState = { diff --git a/web/projects/ui/src/app/util/dry-update.ts b/web/projects/ui/src/app/util/dry-update.ts index 2d4d1aa10..e9386df3c 100644 --- a/web/projects/ui/src/app/util/dry-update.ts +++ b/web/projects/ui/src/app/util/dry-update.ts @@ -1,18 +1,19 @@ -import { Emver } from '@start9labs/shared' +import { Exver } from '@start9labs/shared' import { DataModel } from '../services/patch-db/data-model' import { getManifest } from './get-package-data' export function dryUpdate( { id, version }: { id: string; version: string }, pkgs: DataModel['packageData'], - emver: Emver, + exver: Exver, ): string[] { return Object.values(pkgs) .filter( pkg => Object.keys(pkg.currentDependencies || {}).some( pkgId => pkgId === id, - ) && !emver.satisfies(version, pkg.currentDependencies[id].versionSpec), + ) && + !exver.satisfies(version, pkg.currentDependencies[id].versionRange), ) .map(pkg => getManifest(pkg).title) } diff --git a/web/projects/ui/src/manifest.webmanifest b/web/projects/ui/src/manifest.webmanifest index fee3469fc..cecc31bfe 100644 --- a/web/projects/ui/src/manifest.webmanifest +++ b/web/projects/ui/src/manifest.webmanifest @@ -5,8 +5,8 @@ "background_color": "#1e1e1e", "display": "standalone", "scope": ".", - "start_url": "/?version=0351", - "id": "/?version=0351", + "start_url": "/?version=036", + "id": "/?version=036", "icons": [ { "src": "assets/img/icon.png", diff --git a/web/projects/ui/src/polyfills.ts b/web/projects/ui/src/polyfills.ts index 67caa24e8..813796e34 100644 --- a/web/projects/ui/src/polyfills.ts +++ b/web/projects/ui/src/polyfills.ts @@ -52,8 +52,8 @@ * */ -;(window as any).global = window -;(window as any).process = { env: { DEBUG: undefined }, browser: true } +(window as any).global = window +; (window as any).process = { env: { DEBUG: undefined }, browser: true } import { Buffer } from 'buffer' window.Buffer = Buffer From 019142efc958abf373fee2f39c0b93179fdf2bcf Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Tue, 23 Jul 2024 12:18:17 -0600 Subject: [PATCH 065/125] v0.3.6-alpha.0 (#2680) * v0.3.6-alpha.0 * show welcome on fresh install --- core/Cargo.lock | 2 +- core/startos/Cargo.toml | 2 +- core/startos/src/db/model/public.rs | 2 +- core/startos/src/os_install/mod.rs | 17 +++++++++++++ core/startos/src/version/mod.rs | 24 +++++++++++++++---- .../version/{v0_3_6.rs => v0_3_6_alpha_0.rs} | 13 ++++++---- sdk/lib/osBindings/Public.ts | 2 +- web/package.json | 2 +- web/patchdb-ui-seed.json | 2 +- .../modals/os-welcome/os-welcome.page.html | 22 +++-------------- 10 files changed, 53 insertions(+), 35 deletions(-) rename core/startos/src/version/{v0_3_6.rs => v0_3_6_alpha_0.rs} (66%) diff --git a/core/Cargo.lock b/core/Cargo.lock index e3576a8d8..a3464c7dc 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -4949,7 +4949,7 @@ dependencies = [ [[package]] name = "start-os" -version = "0.3.5-rev.2" +version = "0.3.6-alpha.0" dependencies = [ "aes", "async-compression", diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index a531477b2..ae4382bc2 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -14,7 +14,7 @@ keywords = [ name = "start-os" readme = "README.md" repository = "https://github.com/Start9Labs/start-os" -version = "0.3.5-rev.2" +version = "0.3.6-alpha.0" license = "MIT" [lib] diff --git a/core/startos/src/db/model/public.rs b/core/startos/src/db/model/public.rs index 5f8dc029a..725475602 100644 --- a/core/startos/src/db/model/public.rs +++ b/core/startos/src/db/model/public.rs @@ -31,7 +31,7 @@ use crate::{ARCH, PLATFORM}; pub struct Public { pub server_info: ServerInfo, pub package_data: AllPackageData, - #[ts(type = "any")] + #[ts(type = "unknown")] pub ui: Value, } impl Public { diff --git a/core/startos/src/os_install/mod.rs b/core/startos/src/os_install/mod.rs index 28fd2a3be..3d80f6cbd 100644 --- a/core/startos/src/os_install/mod.rs +++ b/core/startos/src/os_install/mod.rs @@ -147,6 +147,23 @@ pub async fn execute( overwrite |= disk.guid.is_none() && disk.partitions.iter().all(|p| p.guid.is_none()); + if !overwrite + && (disk + .guid + .as_ref() + .map_or(false, |g| g.starts_with("EMBASSY_")) + || disk + .partitions + .iter() + .flat_map(|p| p.guid.as_ref()) + .any(|g| g.starts_with("EMBASSY_"))) + { + return Err(Error::new( + eyre!("installing over versions before 0.3.6 is unsupported"), + ErrorKind::InvalidRequest, + )); + } + let part_info = partition(&mut disk, overwrite).await?; if let Some(efi) = &part_info.efi { diff --git a/core/startos/src/version/mod.rs b/core/startos/src/version/mod.rs index 003a44326..9ad571759 100644 --- a/core/startos/src/version/mod.rs +++ b/core/startos/src/version/mod.rs @@ -13,18 +13,19 @@ use crate::Error; mod v0_3_5; mod v0_3_5_1; mod v0_3_5_2; -mod v0_3_6; +mod v0_3_6_alpha_0; -pub type Current = v0_3_6::Version; +pub type Current = v0_3_6_alpha_0::Version; #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[serde(untagged)] +#[allow(non_camel_case_types)] enum Version { LT0_3_5(LTWrapper), V0_3_5(Wrapper), V0_3_5_1(Wrapper), V0_3_5_2(Wrapper), - V0_3_6(Wrapper), + V0_3_6_alpha_0(Wrapper), Other(exver::Version), } @@ -44,7 +45,7 @@ impl Version { Version::V0_3_5(Wrapper(x)) => x.semver(), Version::V0_3_5_1(Wrapper(x)) => x.semver(), Version::V0_3_5_2(Wrapper(x)) => x.semver(), - Version::V0_3_6(Wrapper(x)) => x.semver(), + Version::V0_3_6_alpha_0(Wrapper(x)) => x.semver(), Version::Other(x) => x.clone(), } } @@ -212,6 +213,19 @@ pub async fn init( mut progress: PhaseProgressTrackerHandle, ) -> Result<(), Error> { progress.start(); + db.mutate(|db| { + db.as_public_mut() + .as_server_info_mut() + .as_version_mut() + .map_mutate(|v| { + Ok(if v == exver::Version::new([0, 3, 6], []) { + v0_3_6_alpha_0::Version::new().semver() + } else { + v + }) + }) + }) + .await?; // TODO: remove before releasing 0.3.6 let version = Version::from_exver_version( db.peek() .await @@ -231,7 +245,7 @@ pub async fn init( Version::V0_3_5(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, Version::V0_3_5_1(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, Version::V0_3_5_2(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, - Version::V0_3_6(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, + Version::V0_3_6_alpha_0(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, Version::Other(_) => { return Err(Error::new( eyre!("Cannot downgrade"), diff --git a/core/startos/src/version/v0_3_6.rs b/core/startos/src/version/v0_3_6_alpha_0.rs similarity index 66% rename from core/startos/src/version/v0_3_6.rs rename to core/startos/src/version/v0_3_6_alpha_0.rs index 9564a7376..1da34f708 100644 --- a/core/startos/src/version/v0_3_6.rs +++ b/core/startos/src/version/v0_3_6_alpha_0.rs @@ -1,24 +1,27 @@ -use exver::VersionRange; +use exver::{PreReleaseSegment, VersionRange}; use super::v0_3_5::V0_3_0_COMPAT; -use super::{v0_3_5_1, VersionT}; +use super::{v0_3_5_2, VersionT}; use crate::db::model::Database; use crate::prelude::*; lazy_static::lazy_static! { - static ref V0_3_6: exver::Version = exver::Version::new([0, 3, 6], []); + static ref V0_3_6_alpha_0: exver::Version = exver::Version::new( + [0, 3, 6], + [PreReleaseSegment::String("alpha".into()), 0.into()] + ); } #[derive(Clone, Debug)] pub struct Version; impl VersionT for Version { - type Previous = v0_3_5_1::Version; + type Previous = v0_3_5_2::Version; fn new() -> Self { Version } fn semver(&self) -> exver::Version { - V0_3_6.clone() + V0_3_6_alpha_0.clone() } fn compat(&self) -> &'static VersionRange { &V0_3_0_COMPAT diff --git a/sdk/lib/osBindings/Public.ts b/sdk/lib/osBindings/Public.ts index c77ae05e3..4fb186607 100644 --- a/sdk/lib/osBindings/Public.ts +++ b/sdk/lib/osBindings/Public.ts @@ -5,5 +5,5 @@ import type { ServerInfo } from "./ServerInfo" export type Public = { serverInfo: ServerInfo packageData: AllPackageData - ui: any + ui: unknown } diff --git a/web/package.json b/web/package.json index b304d4f94..6f72a68ec 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "startos-ui", - "version": "0.3.6", + "version": "0.3.6-alpha.0", "author": "Start9 Labs, Inc", "homepage": "https://start9.com/", "license": "MIT", diff --git a/web/patchdb-ui-seed.json b/web/patchdb-ui-seed.json index 6236275cd..9626d2558 100644 --- a/web/patchdb-ui-seed.json +++ b/web/patchdb-ui-seed.json @@ -1,6 +1,6 @@ { "name": null, - "ackWelcome": "0.3.6", + "ackWelcome": "0.0.0", "marketplace": { "selectedUrl": "https://registry.start9.com/", "knownHosts": { diff --git a/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html b/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html index 9603d454a..3def4478b 100644 --- a/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html +++ b/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html @@ -12,25 +12,9 @@

This Release

-

0.3.6

-

- View the complete - - release notes - - for more details. -

-
Highlights
-
    -
  • Ditch Podman, replace with LXC
  • -
  • -
  • -
  • -
+

0.3.6-alpha.0

+
This is an ALPHA release! DO NOT use for production data!
+
Expect that any data you create or store on this version of the OS can be LOST FOREVER!
Date: Tue, 23 Jul 2024 12:35:38 -0600 Subject: [PATCH 066/125] Bugfix/wsl build (#2681) * explicitly declare squashfs as loop device * Update update-image.sh --- container-runtime/update-image.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/container-runtime/update-image.sh b/container-runtime/update-image.sh index 1c571408a..3a8813518 100755 --- a/container-runtime/update-image.sh +++ b/container-runtime/update-image.sh @@ -8,7 +8,7 @@ if mountpoint tmp/combined; then sudo umount -R tmp/combined; fi if mountpoint tmp/lower; then sudo umount tmp/lower; fi sudo rm -rf tmp mkdir -p tmp/lower tmp/upper tmp/work tmp/combined -sudo mount debian.${ARCH}.squashfs tmp/lower +sudo mount -o loop -t squashfs debian.${ARCH}.squashfs tmp/lower sudo mount -t overlay -olowerdir=tmp/lower,upperdir=tmp/upper,workdir=tmp/work overlay tmp/combined QEMU= From ab465a755eb9ec02626008ea3fe832aebcb697c4 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Wed, 24 Jul 2024 22:40:13 -0600 Subject: [PATCH 067/125] default to all category and fix rounding for progress (#2682) * default to all category and fix rounding for progress * Update install-progress.pipe.ts --- .../app-show-progress.component.html | 4 ++-- .../marketplace-list/marketplace-list.page.ts | 12 ++++++------ .../ui/src/app/pages/updates/updates.page.html | 2 +- .../pipes/install-progress/install-progress.pipe.ts | 8 ++++---- web/projects/ui/src/app/services/api/api.fixures.ts | 6 +++--- .../ui/src/app/services/marketplace.service.ts | 10 +++------- 6 files changed, 19 insertions(+), 23 deletions(-) diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.html index ea80b3003..1b1714f64 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-progress/app-show-progress.component.html @@ -2,7 +2,7 @@

{{ phase.name }} - : {{ progress * 100 }}% + : {{ progress }}%

diff --git a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts index 0f0fa5752..c08f00703 100644 --- a/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts +++ b/web/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts @@ -22,11 +22,7 @@ export class MarketplaceListPage { readonly store$ = this.marketplaceService.getSelectedStore$().pipe( map(({ info, packages }) => { const categories = new Map() - if (info.categories['featured']) - categories.set('featured', info.categories['featured']) - Object.keys(info.categories).forEach(c => - categories.set(c, info.categories[c]), - ) + categories.set('all', { name: 'All', description: { @@ -35,6 +31,10 @@ export class MarketplaceListPage { }, }) + Object.keys(info.categories).forEach(c => + categories.set(c, info.categories[c]), + ) + return { categories, packages } }), ) @@ -88,7 +88,7 @@ export class MarketplaceListPage { private readonly route: ActivatedRoute, ) {} - category = 'featured' + category = 'all' query = '' async presentModalMarketplaceSettings() { diff --git a/web/projects/ui/src/app/pages/updates/updates.page.html b/web/projects/ui/src/app/pages/updates/updates.page.html index fd4d19133..31c712d98 100644 --- a/web/projects/ui/src/app/pages/updates/updates.page.html +++ b/web/projects/ui/src/app/pages/updates/updates.page.html @@ -53,7 +53,7 @@ *ngIf="local.stateInfo.state === 'updating' else notUpdating" > = this.patch .watch$('ui', 'marketplace', 'knownHosts') .pipe( - map((hosts: UIMarketplaceData['knownHosts']) => { + map(hosts => { const { start9, community } = this.config.marketplace let arr = [ toStoreIdentity(start9, hosts[start9]), From b36b62c68eed6e2b80b5febe165099e4e3212b18 Mon Sep 17 00:00:00 2001 From: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> Date: Thu, 25 Jul 2024 11:44:51 -0600 Subject: [PATCH 068/125] Feature/callbacks (#2678) * wip * initialize callbacks * wip * smtp * list_service_interfaces * wip * wip * fix domains * fix hostname handling in NetService * misc fixes * getInstalledPackages * misc fixes * publish v6 lib * refactor service effects * fix import * fix container runtime * fix tests * apply suggestions from review --- Makefile | 17 +- .../src/Adapters/EffectCreator.ts | 301 ++++ .../src/Adapters/HostSystemStartOs.ts | 303 ---- container-runtime/src/Adapters/RpcListener.ts | 247 +-- .../DockerProcedureContainer.ts | 24 +- .../Systems/SystemForEmbassy/MainLoop.ts | 40 +- .../Systems/SystemForEmbassy/index.ts | 109 +- .../SystemForEmbassy/polyfillEffects.ts | 1 + .../src/Adapters/Systems/SystemForStartOs.ts | 145 +- .../src/Interfaces/AllGetDependencies.ts | 5 +- .../src/Interfaces/HostSystem.ts | 8 - .../src/Interfaces/MakeEffects.ts | 4 + container-runtime/src/Interfaces/System.ts | 57 +- .../src/Models/CallbackHolder.ts | 6 +- container-runtime/src/Models/JsonPath.ts | 2 - container-runtime/src/index.ts | 5 +- ...-startos-bins.sh => build-containerbox.sh} | 3 - core/build-startbox.sh | 42 + core/models/src/id/package.rs | 5 + core/models/src/procedure_name.rs | 4 - core/startos/src/auth.rs | 1 + core/startos/src/backup/os.rs | 5 +- core/startos/src/bins/container_cli.rs | 2 +- core/startos/src/context/rpc.rs | 3 + core/startos/src/context/setup.rs | 4 +- core/startos/src/db/model/public.rs | 6 +- core/startos/src/hostname.rs | 25 +- core/startos/src/lib.rs | 12 + core/startos/src/net/dns.rs | 2 +- core/startos/src/net/host/address.rs | 34 + core/startos/src/net/host/mod.rs | 4 + core/startos/src/net/net_controller.rs | 238 +-- core/startos/src/net/ssl.rs | 59 +- core/startos/src/net/vhost.rs | 18 +- core/startos/src/registry/os/asset/add.rs | 16 +- core/startos/src/registry/os/asset/get.rs | 7 +- core/startos/src/registry/os/asset/sign.rs | 13 +- core/startos/src/registry/os/index.rs | 20 +- core/startos/src/registry/os/version/mod.rs | 20 +- .../startos/src/registry/os/version/signer.rs | 8 +- core/startos/src/service/cli.rs | 2 +- core/startos/src/service/effects/action.rs | 101 ++ core/startos/src/service/effects/callbacks.rs | 311 ++++ core/startos/src/service/effects/config.rs | 53 + core/startos/src/service/effects/context.rs | 27 + core/startos/src/service/effects/control.rs | 66 + .../startos/src/service/effects/dependency.rs | 371 +++++ core/startos/src/service/effects/health.rs | 46 + core/startos/src/service/effects/image.rs | 163 ++ core/startos/src/service/effects/mod.rs | 174 ++ core/startos/src/service/effects/net/bind.rs | 56 + core/startos/src/service/effects/net/host.rs | 73 + core/startos/src/service/effects/net/info.rs | 9 + .../src/service/effects/net/interface.rs | 188 +++ core/startos/src/service/effects/net/mod.rs | 5 + core/startos/src/service/effects/net/ssl.rs | 169 ++ core/startos/src/service/effects/prelude.rs | 16 + core/startos/src/service/effects/store.rs | 93 ++ core/startos/src/service/effects/system.rs | 39 + core/startos/src/service/mod.rs | 5 +- .../src/service/persistent_container.rs | 48 +- core/startos/src/service/rpc.rs | 112 ++ .../src/service/service_effect_handler.rs | 1431 ----------------- core/startos/src/system.rs | 46 + core/startos/src/util/collections/eq_map.rs | 1213 ++++++++++++++ core/startos/src/util/collections/mod.rs | 3 + core/startos/src/util/mod.rs | 2 + core/startos/src/util/rpc_client.rs | 57 + sdk/Makefile | 2 + sdk/lib/StartSdk.ts | 55 +- sdk/lib/osBindings/AddAssetParams.ts | 3 +- sdk/lib/osBindings/AddVersionParams.ts | 3 +- .../osBindings/{Callback.ts => CallbackId.ts} | 2 +- sdk/lib/osBindings/CheckDependenciesParam.ts | 2 +- sdk/lib/osBindings/ChrootParams.ts | 10 - sdk/lib/osBindings/DependencyRequirement.ts | 8 +- sdk/lib/osBindings/ExecuteAction.ts | 8 +- sdk/lib/osBindings/ExportActionParams.ts | 8 +- ...amsPackageId.ts => GetConfiguredParams.ts} | 3 +- sdk/lib/osBindings/GetHostInfoParams.ts | 7 +- sdk/lib/osBindings/GetOsAssetParams.ts | 3 +- sdk/lib/osBindings/GetPrimaryUrlParams.ts | 11 +- .../osBindings/GetServiceInterfaceParams.ts | 7 +- .../osBindings/GetServicePortForwardParams.ts | 5 +- sdk/lib/osBindings/GetSslCertificateParams.ts | 7 +- sdk/lib/osBindings/GetSslKeyParams.ts | 6 +- sdk/lib/osBindings/GetStoreParams.ts | 8 +- sdk/lib/osBindings/GetSystemSmtpParams.ts | 4 +- sdk/lib/osBindings/HostAddress.ts | 4 +- .../osBindings/ListServiceInterfacesParams.ts | 7 +- .../osBindings/ListVersionSignersParams.ts | 3 +- sdk/lib/osBindings/MountTarget.ts | 6 +- sdk/lib/osBindings/OsIndex.ts | 5 +- sdk/lib/osBindings/OsVersionInfoMap.ts | 4 + sdk/lib/osBindings/ParamsMaybePackageId.ts | 3 - sdk/lib/osBindings/RemoveActionParams.ts | 3 - sdk/lib/osBindings/RemoveAddressParams.ts | 4 - sdk/lib/osBindings/RemoveVersionParams.ts | 3 +- sdk/lib/osBindings/ServerInfo.ts | 3 +- sdk/lib/osBindings/SetSystemSmtpParams.ts | 3 - sdk/lib/osBindings/SignAssetParams.ts | 3 +- sdk/lib/osBindings/SmtpValue.ts | 9 + sdk/lib/osBindings/VersionSignerParams.ts | 3 +- sdk/lib/osBindings/index.ts | 11 +- sdk/lib/store/getStore.ts | 1 - sdk/lib/test/startosTypeValidation.test.ts | 29 +- sdk/lib/types.ts | 293 ++-- sdk/lib/util/GetSslCertificate.ts | 47 + sdk/lib/util/GetSystemSmtp.ts | 4 +- sdk/lib/util/getServiceInterface.ts | 37 +- sdk/lib/util/getServiceInterfaces.ts | 15 +- sdk/package.json | 2 +- .../ui/src/app/services/api/mock-patch.ts | 2 +- 113 files changed, 4853 insertions(+), 2517 deletions(-) create mode 100644 container-runtime/src/Adapters/EffectCreator.ts delete mode 100644 container-runtime/src/Adapters/HostSystemStartOs.ts delete mode 100644 container-runtime/src/Interfaces/HostSystem.ts create mode 100644 container-runtime/src/Interfaces/MakeEffects.ts rename core/{build-startos-bins.sh => build-containerbox.sh} (83%) create mode 100755 core/build-startbox.sh create mode 100644 core/startos/src/service/effects/action.rs create mode 100644 core/startos/src/service/effects/callbacks.rs create mode 100644 core/startos/src/service/effects/config.rs create mode 100644 core/startos/src/service/effects/context.rs create mode 100644 core/startos/src/service/effects/control.rs create mode 100644 core/startos/src/service/effects/dependency.rs create mode 100644 core/startos/src/service/effects/health.rs create mode 100644 core/startos/src/service/effects/image.rs create mode 100644 core/startos/src/service/effects/mod.rs create mode 100644 core/startos/src/service/effects/net/bind.rs create mode 100644 core/startos/src/service/effects/net/host.rs create mode 100644 core/startos/src/service/effects/net/info.rs create mode 100644 core/startos/src/service/effects/net/interface.rs create mode 100644 core/startos/src/service/effects/net/mod.rs create mode 100644 core/startos/src/service/effects/net/ssl.rs create mode 100644 core/startos/src/service/effects/prelude.rs create mode 100644 core/startos/src/service/effects/store.rs create mode 100644 core/startos/src/service/effects/system.rs delete mode 100644 core/startos/src/service/service_effect_handler.rs create mode 100644 core/startos/src/util/collections/eq_map.rs create mode 100644 core/startos/src/util/collections/mod.rs rename sdk/lib/osBindings/{Callback.ts => CallbackId.ts} (76%) delete mode 100644 sdk/lib/osBindings/ChrootParams.ts rename sdk/lib/osBindings/{ParamsPackageId.ts => GetConfiguredParams.ts} (51%) create mode 100644 sdk/lib/osBindings/OsVersionInfoMap.ts delete mode 100644 sdk/lib/osBindings/ParamsMaybePackageId.ts delete mode 100644 sdk/lib/osBindings/RemoveActionParams.ts delete mode 100644 sdk/lib/osBindings/RemoveAddressParams.ts delete mode 100644 sdk/lib/osBindings/SetSystemSmtpParams.ts create mode 100644 sdk/lib/osBindings/SmtpValue.ts create mode 100644 sdk/lib/util/GetSslCertificate.ts diff --git a/Makefile b/Makefile index c0492ba55..4fb5033d0 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,6 @@ BASENAME := $(shell ./basename.sh) PLATFORM := $(shell if [ -f ./PLATFORM.txt ]; then cat ./PLATFORM.txt; else echo unknown; fi) ARCH := $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo aarch64; else echo $(PLATFORM) | sed 's/-nonfree$$//g'; fi) IMAGE_TYPE=$(shell if [ "$(PLATFORM)" = raspberrypi ]; then echo img; else echo iso; fi) -BINS := core/target/$(ARCH)-unknown-linux-musl/release/startbox core/target/$(ARCH)-unknown-linux-musl/release/containerbox WEB_UIS := web/dist/raw/ui web/dist/raw/setup-wizard web/dist/raw/install-wizard FIRMWARE_ROMS := ./firmware/$(PLATFORM) $(shell jq --raw-output '.[] | select(.platform[] | contains("$(PLATFORM)")) | "./firmware/$(PLATFORM)/" + .id + ".rom.gz"' build/lib/firmware.json) BUILD_SRC := $(shell git ls-files build) build/lib/depends build/lib/conflicts $(FIRMWARE_ROMS) @@ -16,7 +15,7 @@ STARTD_SRC := core/startos/startd.service $(BUILD_SRC) COMPAT_SRC := $(shell git ls-files system-images/compat/) UTILS_SRC := $(shell git ls-files system-images/utils/) BINFMT_SRC := $(shell git ls-files system-images/binfmt/) -CORE_SRC := $(shell git ls-files core) $(shell git ls-files --recurse-submodules patch-db) web/dist/static web/patchdb-ui-seed.json $(GIT_HASH_FILE) +CORE_SRC := $(shell git ls-files core) $(shell git ls-files --recurse-submodules patch-db) $(GIT_HASH_FILE) WEB_SHARED_SRC := $(shell git ls-files web/projects/shared) $(shell ls -p web/ | grep -v / | sed 's/^/web\//g') web/node_modules/.package-lock.json web/config.json patch-db/client/dist web/patchdb-ui-seed.json sdk/dist WEB_UI_SRC := $(shell git ls-files web/projects/ui) WEB_SETUP_WIZARD_SRC := $(shell git ls-files web/projects/setup-wizard) @@ -24,7 +23,7 @@ WEB_INSTALL_WIZARD_SRC := $(shell git ls-files web/projects/install-wizard) PATCH_DB_CLIENT_SRC := $(shell git ls-files --recurse-submodules patch-db/client) GZIP_BIN := $(shell which pigz || which gzip) TAR_BIN := $(shell which gtar || which tar) -COMPILED_TARGETS := $(BINS) system-images/compat/docker-images/$(ARCH).tar system-images/utils/docker-images/$(ARCH).tar system-images/binfmt/docker-images/$(ARCH).tar container-runtime/rootfs.$(ARCH).squashfs +COMPILED_TARGETS := core/target/$(ARCH)-unknown-linux-musl/release/startbox core/target/$(ARCH)-unknown-linux-musl/release/containerbox system-images/compat/docker-images/$(ARCH).tar system-images/utils/docker-images/$(ARCH).tar system-images/binfmt/docker-images/$(ARCH).tar container-runtime/rootfs.$(ARCH).squashfs ALL_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) cargo-deps/$(ARCH)-unknown-linux-musl/release/startos-backup-fs $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo cargo-deps/aarch64-unknown-linux-musl/release/pi-beep; fi) $(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]; then echo cargo-deps/$(ARCH)-unknown-linux-musl/release/tokio-console; fi') $(PLATFORM_FILE) ifeq ($(REMOTE),) @@ -91,7 +90,7 @@ format: cd core && cargo +nightly fmt test: $(CORE_SRC) $(ENVIRONMENT_FILE) - (cd core && cargo build && cargo test) + (cd core && cargo build --features=test && cargo test --features=test) (cd sdk && make test) cli: @@ -257,9 +256,13 @@ system-images/utils/docker-images/$(ARCH).tar: $(UTILS_SRC) system-images/binfmt/docker-images/$(ARCH).tar: $(BINFMT_SRC) cd system-images/binfmt && make docker-images/$(ARCH).tar && touch docker-images/$(ARCH).tar -$(BINS): $(CORE_SRC) $(ENVIRONMENT_FILE) - cd core && ARCH=$(ARCH) ./build-startos-bins.sh - touch $(BINS) +core/target/$(ARCH)-unknown-linux-musl/release/startbox: $(CORE_SRC) web/dist/static web/patchdb-ui-seed.json $(ENVIRONMENT_FILE) + ARCH=$(ARCH) ./core/build-startbox.sh + touch core/target/$(ARCH)-unknown-linux-musl/release/startbox + +core/target/$(ARCH)-unknown-linux-musl/release/containerbox: $(CORE_SRC) $(ENVIRONMENT_FILE) + ARCH=$(ARCH) ./core/build-containerbox.sh + touch core/target/$(ARCH)-unknown-linux-musl/release/containerbox web/node_modules/.package-lock.json: web/package.json sdk/dist npm --prefix web ci diff --git a/container-runtime/src/Adapters/EffectCreator.ts b/container-runtime/src/Adapters/EffectCreator.ts new file mode 100644 index 000000000..0ef299151 --- /dev/null +++ b/container-runtime/src/Adapters/EffectCreator.ts @@ -0,0 +1,301 @@ +import { types as T } from "@start9labs/start-sdk" +import * as net from "net" +import { object, string, number, literals, some, unknown } from "ts-matches" +import { Effects } from "../Models/Effects" + +import { CallbackHolder } from "../Models/CallbackHolder" +import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk" +const matchRpcError = object({ + error: object( + { + code: number, + message: string, + data: some( + string, + object( + { + details: string, + debug: string, + }, + ["debug"], + ), + ), + }, + ["data"], + ), +}) +const testRpcError = matchRpcError.test +const testRpcResult = object({ + result: unknown, +}).test +type RpcError = typeof matchRpcError._TYPE + +const SOCKET_PATH = "/media/startos/rpc/host.sock" +let hostSystemId = 0 + +export type EffectContext = { + procedureId: string | null + callbacks: CallbackHolder | null +} + +const rpcRoundFor = + (procedureId: string | null) => + ( + method: K, + params: Record, + ) => { + const id = hostSystemId++ + const client = net.createConnection({ path: SOCKET_PATH }, () => { + client.write( + JSON.stringify({ + id, + method, + params: { ...params, procedureId }, + }) + "\n", + ) + }) + let bufs: Buffer[] = [] + return new Promise((resolve, reject) => { + client.on("data", (data) => { + try { + bufs.push(data) + if (data.reduce((acc, x) => acc || x == 10, false)) { + const res: unknown = JSON.parse( + Buffer.concat(bufs).toString().split("\n")[0], + ) + if (testRpcError(res)) { + let message = res.error.message + console.error("Error in host RPC:", { method, params }) + if (string.test(res.error.data)) { + message += ": " + res.error.data + console.error(`Details: ${res.error.data}`) + } else { + if (res.error.data?.details) { + message += ": " + res.error.data.details + console.error(`Details: ${res.error.data.details}`) + } + if (res.error.data?.debug) { + message += "\n" + res.error.data.debug + console.error(`Debug: ${res.error.data.debug}`) + } + } + reject(new Error(`${message}@${method}`)) + } else if (testRpcResult(res)) { + resolve(res.result) + } else { + reject(new Error(`malformed response ${JSON.stringify(res)}`)) + } + } + } catch (error) { + reject(error) + } + client.end() + }) + client.on("error", (error) => { + reject(error) + }) + }) + } + +function makeEffects(context: EffectContext): Effects { + const rpcRound = rpcRoundFor(context.procedureId) + const self: Effects = { + bind(...[options]: Parameters) { + return rpcRound("bind", { + ...options, + stack: new Error().stack, + }) as ReturnType + }, + clearBindings(...[]: Parameters) { + return rpcRound("clearBindings", {}) as ReturnType< + T.Effects["clearBindings"] + > + }, + clearServiceInterfaces( + ...[]: Parameters + ) { + return rpcRound("clearServiceInterfaces", {}) as ReturnType< + T.Effects["clearServiceInterfaces"] + > + }, + getInstalledPackages(...[]: Parameters) { + return rpcRound("getInstalledPackages", {}) as ReturnType< + T.Effects["getInstalledPackages"] + > + }, + createOverlayedImage(options: { + imageId: string + }): Promise<[string, string]> { + return rpcRound("createOverlayedImage", options) as ReturnType< + T.Effects["createOverlayedImage"] + > + }, + destroyOverlayedImage(options: { guid: string }): Promise { + return rpcRound("destroyOverlayedImage", options) as ReturnType< + T.Effects["destroyOverlayedImage"] + > + }, + executeAction(...[options]: Parameters) { + return rpcRound("executeAction", options) as ReturnType< + T.Effects["executeAction"] + > + }, + exportAction(...[options]: Parameters) { + return rpcRound("exportAction", options) as ReturnType< + T.Effects["exportAction"] + > + }, + exportServiceInterface: (( + ...[options]: Parameters + ) => { + return rpcRound("exportServiceInterface", options) as ReturnType< + T.Effects["exportServiceInterface"] + > + }) as Effects["exportServiceInterface"], + exposeForDependents( + ...[options]: Parameters + ) { + return rpcRound("exposeForDependents", options) as ReturnType< + T.Effects["exposeForDependents"] + > + }, + getConfigured(...[]: Parameters) { + return rpcRound("getConfigured", {}) as ReturnType< + T.Effects["getConfigured"] + > + }, + getContainerIp(...[]: Parameters) { + return rpcRound("getContainerIp", {}) as ReturnType< + T.Effects["getContainerIp"] + > + }, + getHostInfo: ((...[allOptions]: Parameters) => { + const options = { + ...allOptions, + callback: context.callbacks?.addCallback(allOptions.callback) || null, + } + return rpcRound("getHostInfo", options) as ReturnType< + T.Effects["getHostInfo"] + > as any + }) as Effects["getHostInfo"], + getServiceInterface( + ...[options]: Parameters + ) { + return rpcRound("getServiceInterface", { + ...options, + callback: context.callbacks?.addCallback(options.callback) || null, + }) as ReturnType + }, + + getPrimaryUrl(...[options]: Parameters) { + return rpcRound("getPrimaryUrl", { + ...options, + callback: context.callbacks?.addCallback(options.callback) || null, + }) as ReturnType + }, + getServicePortForward( + ...[options]: Parameters + ) { + return rpcRound("getServicePortForward", options) as ReturnType< + T.Effects["getServicePortForward"] + > + }, + getSslCertificate(options: Parameters[0]) { + return rpcRound("getSslCertificate", options) as ReturnType< + T.Effects["getSslCertificate"] + > + }, + getSslKey(options: Parameters[0]) { + return rpcRound("getSslKey", options) as ReturnType< + T.Effects["getSslKey"] + > + }, + getSystemSmtp(...[options]: Parameters) { + return rpcRound("getSystemSmtp", { + ...options, + callback: context.callbacks?.addCallback(options.callback) || null, + }) as ReturnType + }, + listServiceInterfaces( + ...[options]: Parameters + ) { + return rpcRound("listServiceInterfaces", { + ...options, + callback: context.callbacks?.addCallback(options.callback) || null, + }) as ReturnType + }, + mount(...[options]: Parameters) { + return rpcRound("mount", options) as ReturnType + }, + clearActions(...[]: Parameters) { + return rpcRound("clearActions", {}) as ReturnType< + T.Effects["clearActions"] + > + }, + restart(...[]: Parameters) { + return rpcRound("restart", {}) as ReturnType + }, + setConfigured(...[configured]: Parameters) { + return rpcRound("setConfigured", { configured }) as ReturnType< + T.Effects["setConfigured"] + > + }, + setDependencies( + dependencies: Parameters[0], + ): ReturnType { + return rpcRound("setDependencies", dependencies) as ReturnType< + T.Effects["setDependencies"] + > + }, + checkDependencies( + options: Parameters[0], + ): ReturnType { + return rpcRound("checkDependencies", options) as ReturnType< + T.Effects["checkDependencies"] + > + }, + getDependencies(): ReturnType { + return rpcRound("getDependencies", {}) as ReturnType< + T.Effects["getDependencies"] + > + }, + setHealth(...[options]: Parameters) { + return rpcRound("setHealth", options) as ReturnType< + T.Effects["setHealth"] + > + }, + + setMainStatus(o: { status: "running" | "stopped" }): Promise { + return rpcRound("setMainStatus", o) as ReturnType + }, + + shutdown(...[]: Parameters) { + return rpcRound("shutdown", {}) as ReturnType + }, + store: { + get: async (options: any) => + rpcRound("getStore", { + ...options, + callback: context.callbacks?.addCallback(options.callback) || null, + }) as any, + set: async (options: any) => + rpcRound("setStore", options) as ReturnType, + } as T.Effects["store"], + } + return self +} + +export function makeProcedureEffects(procedureId: string): Effects { + return makeEffects({ procedureId, callbacks: null }) +} + +export function makeMainEffects(): MainEffects { + const rpcRound = rpcRoundFor(null) + return { + _type: "main", + clearCallbacks: () => { + return rpcRound("clearCallbacks", {}) as Promise + }, + ...makeEffects({ procedureId: null, callbacks: new CallbackHolder() }), + } +} diff --git a/container-runtime/src/Adapters/HostSystemStartOs.ts b/container-runtime/src/Adapters/HostSystemStartOs.ts deleted file mode 100644 index 1996af0fd..000000000 --- a/container-runtime/src/Adapters/HostSystemStartOs.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { types as T } from "@start9labs/start-sdk" -import * as net from "net" -import { object, string, number, literals, some, unknown } from "ts-matches" -import { Effects } from "../Models/Effects" - -import { CallbackHolder } from "../Models/CallbackHolder" -const matchRpcError = object({ - error: object( - { - code: number, - message: string, - data: some( - string, - object( - { - details: string, - debug: string, - }, - ["debug"], - ), - ), - }, - ["data"], - ), -}) -const testRpcError = matchRpcError.test -const testRpcResult = object({ - result: unknown, -}).test -type RpcError = typeof matchRpcError._TYPE - -const SOCKET_PATH = "/media/startos/rpc/host.sock" -const MAIN = "/main" as const -let hostSystemId = 0 -export const hostSystemStartOs = - (callbackHolder: CallbackHolder) => - (procedureId: null | string): Effects => { - const rpcRound = ( - method: K, - params: Record, - ) => { - const id = hostSystemId++ - const client = net.createConnection({ path: SOCKET_PATH }, () => { - client.write( - JSON.stringify({ - id, - method, - params: { ...params, procedureId: procedureId }, - }) + "\n", - ) - }) - let bufs: Buffer[] = [] - return new Promise((resolve, reject) => { - client.on("data", (data) => { - try { - bufs.push(data) - if (data.reduce((acc, x) => acc || x == 10, false)) { - const res: unknown = JSON.parse( - Buffer.concat(bufs).toString().split("\n")[0], - ) - if (testRpcError(res)) { - let message = res.error.message - console.error({ method, params, hostSystemStartOs: true }) - if (string.test(res.error.data)) { - message += ": " + res.error.data - console.error(res.error.data) - } else { - if (res.error.data?.details) { - message += ": " + res.error.data.details - console.error(res.error.data.details) - } - if (res.error.data?.debug) { - message += "\n" + res.error.data.debug - console.error("Debug: " + res.error.data.debug) - } - } - reject(new Error(`${message}@${method}`)) - } else if (testRpcResult(res)) { - resolve(res.result) - } else { - reject(new Error(`malformed response ${JSON.stringify(res)}`)) - } - } - } catch (error) { - reject(error) - } - client.end() - }) - client.on("error", (error) => { - reject(error) - }) - }) - } - const self: Effects = { - bind(...[options]: Parameters) { - return rpcRound("bind", { - ...options, - stack: new Error().stack, - }) as ReturnType - }, - clearBindings(...[]: Parameters) { - return rpcRound("clearBindings", {}) as ReturnType< - T.Effects["clearBindings"] - > - }, - clearServiceInterfaces( - ...[]: Parameters - ) { - return rpcRound("clearServiceInterfaces", {}) as ReturnType< - T.Effects["clearServiceInterfaces"] - > - }, - createOverlayedImage(options: { - imageId: string - }): Promise<[string, string]> { - return rpcRound("createOverlayedImage", options) as ReturnType< - T.Effects["createOverlayedImage"] - > - }, - destroyOverlayedImage(options: { guid: string }): Promise { - return rpcRound("destroyOverlayedImage", options) as ReturnType< - T.Effects["destroyOverlayedImage"] - > - }, - executeAction(...[options]: Parameters) { - return rpcRound("executeAction", options) as ReturnType< - T.Effects["executeAction"] - > - }, - exists(...[packageId]: Parameters) { - return rpcRound("exists", packageId) as ReturnType - }, - exportAction(...[options]: Parameters) { - return rpcRound("exportAction", options) as ReturnType< - T.Effects["exportAction"] - > - }, - exportServiceInterface: (( - ...[options]: Parameters - ) => { - return rpcRound("exportServiceInterface", options) as ReturnType< - T.Effects["exportServiceInterface"] - > - }) as Effects["exportServiceInterface"], - exposeForDependents( - ...[options]: Parameters - ) { - return rpcRound("exposeForDependents", options) as ReturnType< - T.Effects["exposeForDependents"] - > - }, - getConfigured(...[]: Parameters) { - return rpcRound("getConfigured", {}) as ReturnType< - T.Effects["getConfigured"] - > - }, - getContainerIp(...[]: Parameters) { - return rpcRound("getContainerIp", {}) as ReturnType< - T.Effects["getContainerIp"] - > - }, - getHostInfo: ((...[allOptions]: any[]) => { - const options = { - ...allOptions, - callback: callbackHolder.addCallback(allOptions.callback), - } - return rpcRound("getHostInfo", options) as ReturnType< - T.Effects["getHostInfo"] - > as any - }) as Effects["getHostInfo"], - getServiceInterface( - ...[options]: Parameters - ) { - return rpcRound("getServiceInterface", { - ...options, - callback: callbackHolder.addCallback(options.callback), - }) as ReturnType - }, - - getPrimaryUrl(...[options]: Parameters) { - return rpcRound("getPrimaryUrl", { - ...options, - callback: callbackHolder.addCallback(options.callback), - }) as ReturnType - }, - getServicePortForward( - ...[options]: Parameters - ) { - return rpcRound("getServicePortForward", options) as ReturnType< - T.Effects["getServicePortForward"] - > - }, - getSslCertificate( - options: Parameters[0], - ) { - return rpcRound("getSslCertificate", options) as ReturnType< - T.Effects["getSslCertificate"] - > - }, - getSslKey(options: Parameters[0]) { - return rpcRound("getSslKey", options) as ReturnType< - T.Effects["getSslKey"] - > - }, - getSystemSmtp(...[options]: Parameters) { - return rpcRound("getSystemSmtp", { - ...options, - callback: callbackHolder.addCallback(options.callback), - }) as ReturnType - }, - listServiceInterfaces( - ...[options]: Parameters - ) { - return rpcRound("listServiceInterfaces", { - ...options, - callback: callbackHolder.addCallback(options.callback), - }) as ReturnType - }, - mount(...[options]: Parameters) { - return rpcRound("mount", options) as ReturnType - }, - removeAction(...[options]: Parameters) { - return rpcRound("removeAction", options) as ReturnType< - T.Effects["removeAction"] - > - }, - removeAddress(...[options]: Parameters) { - return rpcRound("removeAddress", options) as ReturnType< - T.Effects["removeAddress"] - > - }, - restart(...[]: Parameters) { - return rpcRound("restart", {}) as ReturnType - }, - running(...[packageId]: Parameters) { - return rpcRound("running", { packageId }) as ReturnType< - T.Effects["running"] - > - }, - // runRsync(...[options]: Parameters) { - // - // return rpcRound('executeAction', options) as ReturnType - // - // return rpcRound('executeAction', options) as ReturnType - // } - setConfigured(...[configured]: Parameters) { - return rpcRound("setConfigured", { configured }) as ReturnType< - T.Effects["setConfigured"] - > - }, - setDependencies( - dependencies: Parameters[0], - ): ReturnType { - return rpcRound("setDependencies", dependencies) as ReturnType< - T.Effects["setDependencies"] - > - }, - checkDependencies( - options: Parameters[0], - ): ReturnType { - return rpcRound("checkDependencies", options) as ReturnType< - T.Effects["checkDependencies"] - > - }, - getDependencies(): ReturnType { - return rpcRound("getDependencies", {}) as ReturnType< - T.Effects["getDependencies"] - > - }, - setHealth(...[options]: Parameters) { - return rpcRound("setHealth", options) as ReturnType< - T.Effects["setHealth"] - > - }, - - setMainStatus(o: { status: "running" | "stopped" }): Promise { - return rpcRound("setMainStatus", o) as ReturnType< - T.Effects["setHealth"] - > - }, - - shutdown(...[]: Parameters) { - return rpcRound("shutdown", {}) as ReturnType - }, - stopped(...[packageId]: Parameters) { - return rpcRound("stopped", { packageId }) as ReturnType< - T.Effects["stopped"] - > - }, - store: { - get: async (options: any) => - rpcRound("getStore", { - ...options, - callback: callbackHolder.addCallback(options.callback), - }) as any, - set: async (options: any) => - rpcRound("setStore", options) as ReturnType< - T.Effects["store"]["set"] - >, - } as T.Effects["store"], - } - return self - } diff --git a/container-runtime/src/Adapters/RpcListener.ts b/container-runtime/src/Adapters/RpcListener.ts index 04e9bc40f..6e8e7aac8 100644 --- a/container-runtime/src/Adapters/RpcListener.ts +++ b/container-runtime/src/Adapters/RpcListener.ts @@ -15,15 +15,16 @@ import { } from "ts-matches" import { types as T } from "@start9labs/start-sdk" -import * as CP from "child_process" -import * as Mod from "module" import * as fs from "fs" import { CallbackHolder } from "../Models/CallbackHolder" import { AllGetDependencies } from "../Interfaces/AllGetDependencies" -import { HostSystem } from "../Interfaces/HostSystem" import { jsonPath } from "../Models/JsonPath" -import { System } from "../Interfaces/System" +import { RunningMain, System } from "../Interfaces/System" +import { + MakeMainEffects, + MakeProcedureEffects, +} from "../Interfaces/MakeEffects" type MaybePromise = T | Promise export const matchRpcResult = anyOf( object({ result: any }), @@ -45,7 +46,7 @@ export const matchRpcResult = anyOf( }), ) export type RpcResult = typeof matchRpcResult._TYPE -type SocketResponse = { jsonrpc: "2.0"; id: IdType } & RpcResult +type SocketResponse = ({ jsonrpc: "2.0"; id: IdType } & RpcResult) | null const SOCKET_PARENT = "/media/startos/rpc" const SOCKET_PATH = "/media/startos/rpc/service.sock" @@ -80,7 +81,6 @@ const sandboxRunType = object({ ), }) const callbackType = object({ - id: idType, method: literal("callback"), params: object({ callback: number, @@ -91,6 +91,14 @@ const initType = object({ id: idType, method: literal("init"), }) +const startType = object({ + id: idType, + method: literal("start"), +}) +const stopType = object({ + id: idType, + method: literal("stop"), +}) const exitType = object({ id: idType, method: literal("exit"), @@ -104,33 +112,40 @@ const evalType = object({ }) const jsonParse = (x: string) => JSON.parse(x) -function reduceMethod( - methodArgs: object, - effects: HostSystem, -): (previousValue: any, currentValue: string) => any { - return (x: any, method: string) => - Promise.resolve(x) - .then((x) => x[method]) - .then((x) => - typeof x !== "function" - ? x - : x({ - ...methodArgs, - effects, - }), + +const handleRpc = (id: IdType, result: Promise) => + result + .then((result) => ({ + jsonrpc, + id, + ...result, + })) + .then((x) => { + if ( + ("result" in x && x.result === undefined) || + !("error" in x || "result" in x) ) -} + (x as any).result = null + return x + }) + .catch((error) => ({ + jsonrpc, + id, + error: { + code: 0, + message: typeof error, + data: { details: "" + error, debug: error?.stack }, + }, + })) const hasId = object({ id: idType }).test export class RpcListener { unixSocketServer = net.createServer(async (server) => {}) private _system: System | undefined - private _effects: HostSystem | undefined + private _makeProcedureEffects: MakeProcedureEffects | undefined + private _makeMainEffects: MakeMainEffects | undefined - constructor( - readonly getDependencies: AllGetDependencies, - private callbacks = new CallbackHolder(), - ) { + constructor(readonly getDependencies: AllGetDependencies) { if (!fs.existsSync(SOCKET_PARENT)) { fs.mkdirSync(SOCKET_PARENT, { recursive: true }) } @@ -165,8 +180,13 @@ export class RpcListener { code: 1, }, }) - const writeDataToSocket = (x: SocketResponse) => - new Promise((resolve) => s.write(JSON.stringify(x) + "\n", resolve)) + const writeDataToSocket = (x: SocketResponse) => { + if (x != null) { + return new Promise((resolve) => + s.write(JSON.stringify(x) + "\n", resolve), + ) + } + } s.on("data", (a) => Promise.resolve(a) .then((b) => b.toString()) @@ -181,107 +201,116 @@ export class RpcListener { }) } - private get effects() { - return this.getDependencies.hostSystem()(this.callbacks) - } - private get system() { if (!this._system) throw new Error("System not initialized") return this._system } + private get makeProcedureEffects() { + if (!this._makeProcedureEffects) { + this._makeProcedureEffects = this.getDependencies.makeProcedureEffects() + } + return this._makeProcedureEffects + } + + private get makeMainEffects() { + if (!this._makeMainEffects) { + this._makeMainEffects = this.getDependencies.makeMainEffects() + } + return this._makeMainEffects + } + private dealWithInput(input: unknown): MaybePromise { return matches(input) - .when(some(runType, sandboxRunType), async ({ id, params }) => { + .when(runType, async ({ id, params }) => { const system = this.system const procedure = jsonPath.unsafeCast(params.procedure) - return system - .execute(this.effects, { - id: params.id, + const effects = this.getDependencies.makeProcedureEffects()(params.id) + return handleRpc( + id, + system.execute(effects, { procedure, input: params.input, timeout: params.timeout, - }) - .then((result) => ({ - jsonrpc, - id, - ...result, - })) - .then((x) => { - if ( - ("result" in x && x.result === undefined) || - !("error" in x || "result" in x) - ) - (x as any).result = null - return x - }) - .catch((error) => ({ - jsonrpc, - id, - error: { - code: 0, - message: typeof error, - data: { details: "" + error, debug: error?.stack }, - }, - })) + }), + ) }) - .when(callbackType, async ({ id, params: { callback, args } }) => - Promise.resolve(this.callbacks.callCallback(callback, args)) - .then((result) => ({ - jsonrpc, - id, - result, - })) - .catch((error) => ({ - jsonrpc, - id, - - error: { - code: 0, - message: typeof error, - data: { - details: error?.message ?? String(error), - debug: error?.stack, - }, - }, - })), - ) - .when(exitType, async ({ id }) => { - if (this._system) await this._system.exit(this.effects(null)) - delete this._system - delete this._effects - - return { - jsonrpc, + .when(sandboxRunType, async ({ id, params }) => { + const system = this.system + const procedure = jsonPath.unsafeCast(params.procedure) + const effects = this.makeProcedureEffects(params.id) + return handleRpc( id, - result: null, - } + system.sandbox(effects, { + procedure, + input: params.input, + timeout: params.timeout, + }), + ) + }) + .when(callbackType, async ({ params: { callback, args } }) => { + this.system.callCallback(callback, args) + return null + }) + .when(startType, async ({ id }) => { + return handleRpc( + id, + this.system + .start(this.makeMainEffects()) + .then((result) => ({ result })), + ) + }) + .when(stopType, async ({ id }) => { + return handleRpc( + id, + this.system.stop().then((result) => ({ result })), + ) + }) + .when(exitType, async ({ id }) => { + return handleRpc( + id, + (async () => { + if (this._system) await this._system.exit() + })().then((result) => ({ result })), + ) }) .when(initType, async ({ id }) => { - this._system = await this.getDependencies.system() - - return { - jsonrpc, + return handleRpc( id, - result: null, - } + (async () => { + if (!this._system) { + const system = await this.getDependencies.system() + await system.init() + this._system = system + } + })().then((result) => ({ result })), + ) }) .when(evalType, async ({ id, params }) => { - const result = await new Function( - `return (async () => { return (${params.script}) }).call(this)`, - ).call({ - listener: this, - require: require, - }) - return { - jsonrpc, + return handleRpc( id, - result: !["string", "number", "boolean", "null", "object"].includes( - typeof result, - ) - ? null - : result, - } + (async () => { + const result = await new Function( + `return (async () => { return (${params.script}) }).call(this)`, + ).call({ + listener: this, + require: require, + }) + return { + jsonrpc, + id, + result: ![ + "string", + "number", + "boolean", + "null", + "object", + ].includes(typeof result) + ? null + : result, + } + })(), + ) }) .when(shape({ id: idType, method: string }), ({ id, method }) => ({ jsonrpc, diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts index c06395a17..5db0b6245 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts @@ -14,6 +14,7 @@ export class DockerProcedureContainer { // } static async of( effects: T.Effects, + packageId: string, data: DockerProcedure, volumes: { [id: VolumeId]: Volume }, ) { @@ -38,16 +39,25 @@ export class DockerProcedureContainer { mounts[mount], ) } else if (volumeMount.type === "certificate") { - volumeMount + const hostnames = [ + `${packageId}.embassy`, + ...new Set( + Object.values( + ( + await effects.getHostInfo({ + hostId: volumeMount["interface-id"], + }) + )?.hostnameInfo || {}, + ) + .flatMap((h) => h) + .flatMap((h) => (h.kind === "onion" ? [h.hostname.value] : [])), + ).values(), + ] const certChain = await effects.getSslCertificate({ - packageId: null, - hostId: volumeMount["interface-id"], - algorithm: null, + hostnames, }) const key = await effects.getSslKey({ - packageId: null, - hostId: volumeMount["interface-id"], - algorithm: null, + hostnames, }) await fs.writeFile( `${path}/${volumeMount["interface-id"]}.cert.pem`, diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts index f6e7614ed..f8f0a2d6e 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/MainLoop.ts @@ -1,10 +1,10 @@ import { polyfillEffects } from "./polyfillEffects" import { DockerProcedureContainer } from "./DockerProcedureContainer" import { SystemForEmbassy } from "." -import { hostSystemStartOs } from "../../HostSystemStartOs" -import { Daemons, T, daemons, utils } from "@start9labs/start-sdk" +import { T, utils } from "@start9labs/start-sdk" import { Daemon } from "@start9labs/start-sdk/cjs/lib/mainFn/Daemon" import { Effects } from "../../../Models/Effects" +import { off } from "node:process" const EMBASSY_HEALTH_INTERVAL = 15 * 1000 const EMBASSY_PROPERTIES_LOOP = 30 * 1000 @@ -14,24 +14,28 @@ const EMBASSY_PROPERTIES_LOOP = 30 * 1000 * Also, this has an ability to clean itself up too if need be. */ export class MainLoop { - private healthLoops: - | { - name: string - interval: NodeJS.Timeout - }[] - | undefined + private healthLoops?: { + name: string + interval: NodeJS.Timeout + }[] - private mainEvent: - | Promise<{ - daemon: Daemon - }> - | undefined - constructor( + private mainEvent?: { + daemon: Daemon + } + + private constructor( readonly system: SystemForEmbassy, readonly effects: Effects, - ) { - this.healthLoops = this.constructHealthLoops() - this.mainEvent = this.constructMainEvent() + ) {} + + static async of( + system: SystemForEmbassy, + effects: Effects, + ): Promise { + const res = new MainLoop(system, effects) + res.healthLoops = res.constructHealthLoops() + res.mainEvent = await res.constructMainEvent() + return res } private async constructMainEvent() { @@ -46,6 +50,7 @@ export class MainLoop { const jsMain = (this.system.moduleCode as any)?.jsMain const dockerProcedureContainer = await DockerProcedureContainer.of( effects, + this.system.manifest.id, this.system.manifest.main, this.system.manifest.volumes, ) @@ -135,6 +140,7 @@ export class MainLoop { if (actionProcedure.type === "docker") { const container = await DockerProcedureContainer.of( effects, + manifest.id, actionProcedure, manifest.volumes, ) diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index be8a4d163..2e0836bb0 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -3,8 +3,8 @@ import * as fs from "fs/promises" import { polyfillEffects } from "./polyfillEffects" import { Duration, duration, fromDuration } from "../../../Models/Duration" -import { System } from "../../../Interfaces/System" -import { matchManifest, Manifest, Procedure } from "./matchManifest" +import { System, Procedure } from "../../../Interfaces/System" +import { matchManifest, Manifest } from "./matchManifest" import * as childProcess from "node:child_process" import { DockerProcedureContainer } from "./DockerProcedureContainer" import { promisify } from "node:util" @@ -27,7 +27,6 @@ import { Parser, array, } from "ts-matches" -import { hostSystemStartOs } from "../../HostSystemStartOs" import { JsonPath, unNestPath } from "../../../Models/JsonPath" import { RpcResult, matchRpcResult } from "../../RpcListener" import { CT } from "@start9labs/start-sdk" @@ -48,6 +47,7 @@ import { transformConfigSpec, transformOldConfigToNew, } from "./transformConfigSpec" +import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk" type Optional = A | undefined | null function todo(): never { @@ -203,16 +203,39 @@ export class SystemForEmbassy implements System { readonly manifest: Manifest, readonly moduleCode: Partial, ) {} + + async init(): Promise {} + + async exit(): Promise { + if (this.currentRunning) await this.currentRunning.clean() + delete this.currentRunning + } + + async start(effects: MainEffects): Promise { + if (!!this.currentRunning) return + + this.currentRunning = await MainLoop.of(this, effects) + } + callCallback(_callback: number, _args: any[]): void {} + async stop(): Promise { + const { currentRunning } = this + this.currentRunning?.clean() + delete this.currentRunning + if (currentRunning) { + await currentRunning.clean({ + timeout: fromDuration(this.manifest.main["sigterm-timeout"] || "30s"), + }) + } + } + async execute( - effectCreator: ReturnType, + effects: Effects, options: { - id: string procedure: JsonPath input: unknown timeout?: number | undefined }, ): Promise { - const effects = effectCreator(options.id) return this._execute(effects, options) .then((x) => matches(x) @@ -267,10 +290,6 @@ export class SystemForEmbassy implements System { } }) } - async exit(): Promise { - if (this.currentRunning) await this.currentRunning.clean() - delete this.currentRunning - } async _execute( effects: Effects, options: { @@ -294,7 +313,7 @@ export class SystemForEmbassy implements System { case "/actions/metadata": return todo() case "/init": - return this.init( + return this.initProcedure( effects, string.optional().unsafeCast(input), options.timeout || null, @@ -305,10 +324,6 @@ export class SystemForEmbassy implements System { string.optional().unsafeCast(input), options.timeout || null, ) - case "/main/start": - return this.mainStart(effects, options.timeout || null) - case "/main/stop": - return this.mainStop(effects, options.timeout || null) default: const procedures = unNestPath(options.procedure) switch (true) { @@ -345,7 +360,14 @@ export class SystemForEmbassy implements System { } throw new Error(`Could not find the path for ${options.procedure}`) } - private async init( + async sandbox( + effects: Effects, + options: { procedure: Procedure; input: unknown; timeout?: number }, + ): Promise { + return this.execute(effects, options) + } + + private async initProcedure( effects: Effects, previousVersion: Optional, timeoutMs: number | null, @@ -470,42 +492,22 @@ export class SystemForEmbassy implements System { // TODO Do a migration down if the version exists await effects.setMainStatus({ status: "stopped" }) } - private async mainStart( - effects: Effects, - timeoutMs: number | null, - ): Promise { - if (!!this.currentRunning) return - this.currentRunning = new MainLoop(this, effects) - } - private async mainStop( - effects: Effects, - timeoutMs: number | null, - ): Promise { - try { - const { currentRunning } = this - this.currentRunning?.clean() - delete this.currentRunning - if (currentRunning) { - await currentRunning.clean({ - timeout: utils.inMs(this.manifest.main["sigterm-timeout"]), - }) - } - return - } finally { - await effects.setMainStatus({ status: "stopped" }) - } - } private async createBackup( effects: Effects, timeoutMs: number | null, ): Promise { const backup = this.manifest.backup.create if (backup.type === "docker") { - const container = await DockerProcedureContainer.of(effects, backup, { - ...this.manifest.volumes, - BACKUP: { type: "backup", readonly: false }, - }) + const container = await DockerProcedureContainer.of( + effects, + this.manifest.id, + backup, + { + ...this.manifest.volumes, + BACKUP: { type: "backup", readonly: false }, + }, + ) await container.execFail([backup.entrypoint, ...backup.args], timeoutMs) } else { const moduleCode = await this.moduleCode @@ -520,6 +522,7 @@ export class SystemForEmbassy implements System { if (restoreBackup.type === "docker") { const container = await DockerProcedureContainer.of( effects, + this.manifest.id, restoreBackup, { ...this.manifest.volumes, @@ -552,6 +555,7 @@ export class SystemForEmbassy implements System { if (config.type === "docker") { const container = await DockerProcedureContainer.of( effects, + this.manifest.id, config, this.manifest.volumes, ) @@ -594,6 +598,7 @@ export class SystemForEmbassy implements System { if (setConfigValue.type === "docker") { const container = await DockerProcedureContainer.of( effects, + this.manifest.id, setConfigValue, this.manifest.volumes, ) @@ -702,6 +707,7 @@ export class SystemForEmbassy implements System { if (procedure.type === "docker") { const container = await DockerProcedureContainer.of( effects, + this.manifest.id, procedure, this.manifest.volumes, ) @@ -744,6 +750,7 @@ export class SystemForEmbassy implements System { if (setConfigValue.type === "docker") { const container = await DockerProcedureContainer.of( effects, + this.manifest.id, setConfigValue, this.manifest.volumes, ) @@ -785,6 +792,7 @@ export class SystemForEmbassy implements System { if (actionProcedure.type === "docker") { const container = await DockerProcedureContainer.of( effects, + this.manifest.id, actionProcedure, this.manifest.volumes, ) @@ -825,6 +833,7 @@ export class SystemForEmbassy implements System { if (actionProcedure.type === "docker") { const container = await DockerProcedureContainer.of( effects, + this.manifest.id, actionProcedure, this.manifest.volumes, ) @@ -1039,15 +1048,15 @@ async function updateConfig( } } const url: string = - filled === null + filled === null || filled.addressInfo === null ? "" : catchFn(() => utils.hostnameInfoToAddress( specValue.target === "lan-address" - ? filled.addressInfo.localHostnames[0] || - filled.addressInfo.onionHostnames[0] - : filled.addressInfo.onionHostnames[0] || - filled.addressInfo.localHostnames[0], + ? filled.addressInfo!.localHostnames[0] || + filled.addressInfo!.onionHostnames[0] + : filled.addressInfo!.onionHostnames[0] || + filled.addressInfo!.localHostnames[0], ), ) || "" mutConfigValue[key] = url diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts index 2b7363cbf..6481a7a56 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/polyfillEffects.ts @@ -126,6 +126,7 @@ export const polyfillEffects = ( } { const dockerProcedureContainer = DockerProcedureContainer.of( effects, + manifest.id, manifest.main, manifest.volumes, ) diff --git a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts index 2c455dc42..78c21b7c7 100644 --- a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts +++ b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts @@ -1,43 +1,84 @@ -import { ExecuteResult, System } from "../../Interfaces/System" +import { ExecuteResult, Procedure, System } from "../../Interfaces/System" import { unNestPath } from "../../Models/JsonPath" import matches, { any, number, object, string, tuple } from "ts-matches" -import { hostSystemStartOs } from "../HostSystemStartOs" import { Effects } from "../../Models/Effects" import { RpcResult, matchRpcResult } from "../RpcListener" import { duration } from "../../Models/Duration" import { T } from "@start9labs/start-sdk" -import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk" import { Volume } from "../../Models/Volume" +import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk" +import { CallbackHolder } from "../../Models/CallbackHolder" + export const STARTOS_JS_LOCATION = "/usr/lib/startos/package/index.js" + +type RunningMain = { + effects: MainEffects + stop: () => Promise + callbacks: CallbackHolder +} + export class SystemForStartOs implements System { - private onTerm: (() => Promise) | undefined + private runningMain: RunningMain | undefined + static of() { return new SystemForStartOs(require(STARTOS_JS_LOCATION)) } + constructor(readonly abi: T.ABI) {} + + async init(): Promise {} + + async exit(): Promise {} + + async start(effects: MainEffects): Promise { + if (this.runningMain) await this.stop() + let mainOnTerm: () => Promise | undefined + const started = async (onTerm: () => Promise) => { + await effects.setMainStatus({ status: "running" }) + mainOnTerm = onTerm + } + const daemons = await ( + await this.abi.main({ + effects: effects as MainEffects, + started, + }) + ).build() + this.runningMain = { + effects, + stop: async () => { + if (mainOnTerm) await mainOnTerm() + await daemons.term() + }, + callbacks: new CallbackHolder(), + } + } + + callCallback(callback: number, args: any[]): void { + if (this.runningMain) { + this.runningMain.callbacks + .callCallback(callback, args) + .catch((error) => console.error(`callback ${callback} failed`, error)) + } else { + console.warn(`callback ${callback} ignored because system is not running`) + } + } + + async stop(): Promise { + if (this.runningMain) { + await this.runningMain.stop() + await this.runningMain.effects.clearCallbacks() + this.runningMain = undefined + } + } + async execute( - effectCreator: ReturnType, + effects: Effects, options: { - id: string - procedure: - | "/init" - | "/uninit" - | "/main/start" - | "/main/stop" - | "/config/set" - | "/config/get" - | "/backup/create" - | "/backup/restore" - | "/actions/metadata" - | `/actions/${string}/get` - | `/actions/${string}/run` - | `/dependencies/${string}/query` - | `/dependencies/${string}/update` + procedure: Procedure input: unknown timeout?: number | undefined }, ): Promise { - const effects = effectCreator(options.id) return this._execute(effects, options) .then((x) => matches(x) @@ -93,22 +134,9 @@ export class SystemForStartOs implements System { }) } async _execute( - effects: Effects, + effects: Effects | MainEffects, options: { - procedure: - | "/init" - | "/uninit" - | "/main/start" - | "/main/stop" - | "/config/set" - | "/config/get" - | "/backup/create" - | "/backup/restore" - | "/actions/metadata" - | `/actions/${string}/get` - | `/actions/${string}/run` - | `/dependencies/${string}/query` - | `/dependencies/${string}/update` + procedure: Procedure input: unknown timeout?: number | undefined }, @@ -123,30 +151,15 @@ export class SystemForStartOs implements System { const nextVersion = string.optional().unsafeCast(options.input) || null return this.abi.uninit({ effects, nextVersion }) } - case "/main/start": { - if (this.onTerm) await this.onTerm() - const started = async (onTerm: () => Promise) => { - await effects.setMainStatus({ status: "running" }) - this.onTerm = onTerm - } - const daemons = await ( - await this.abi.main({ - effects: { ...effects, _type: "main" }, - started, - }) - ).build() - this.onTerm = daemons.term - return - } - case "/main/stop": { - try { - if (this.onTerm) await this.onTerm() - delete this.onTerm - return - } finally { - await effects.setMainStatus({ status: "stopped" }) - } - } + // case "/main/start": { + // + // } + // case "/main/stop": { + // if (this.onTerm) await this.onTerm() + // await effects.setMainStatus({ status: "stopped" }) + // delete this.onTerm + // return duration(30, "s") + // } case "/config/set": { const input = options.input as any // TODO return this.abi.setConfig({ effects, input }) @@ -169,6 +182,9 @@ export class SystemForStartOs implements System { case "/actions/metadata": { return this.abi.actionsMetadata({ effects }) } + case "/properties": { + throw new Error("TODO") + } default: const procedures = unNestPath(options.procedure) const id = procedures[2] @@ -199,9 +215,12 @@ export class SystemForStartOs implements System { } return } - throw new Error(`Method ${options.procedure} not implemented.`) } - async exit(effects: Effects): Promise { - return void null + + async sandbox( + effects: Effects, + options: { procedure: Procedure; input: unknown; timeout?: number }, + ): Promise { + return this.execute(effects, options) } } diff --git a/container-runtime/src/Interfaces/AllGetDependencies.ts b/container-runtime/src/Interfaces/AllGetDependencies.ts index 88a200900..ca5c43585 100644 --- a/container-runtime/src/Interfaces/AllGetDependencies.ts +++ b/container-runtime/src/Interfaces/AllGetDependencies.ts @@ -1,6 +1,7 @@ import { GetDependency } from "./GetDependency" import { System } from "./System" -import { GetHostSystem, HostSystem } from "./HostSystem" +import { MakeMainEffects, MakeProcedureEffects } from "./MakeEffects" export type AllGetDependencies = GetDependency<"system", Promise> & - GetDependency<"hostSystem", GetHostSystem> + GetDependency<"makeProcedureEffects", MakeProcedureEffects> & + GetDependency<"makeMainEffects", MakeMainEffects> diff --git a/container-runtime/src/Interfaces/HostSystem.ts b/container-runtime/src/Interfaces/HostSystem.ts deleted file mode 100644 index 4ba986e3b..000000000 --- a/container-runtime/src/Interfaces/HostSystem.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { types as T } from "@start9labs/start-sdk" - -import { CallbackHolder } from "../Models/CallbackHolder" -import { Effects } from "../Models/Effects" -export type HostSystem = Effects -export type GetHostSystem = ( - callbackHolder: CallbackHolder, -) => (procedureId: null | string) => Effects diff --git a/container-runtime/src/Interfaces/MakeEffects.ts b/container-runtime/src/Interfaces/MakeEffects.ts new file mode 100644 index 000000000..3b25f8180 --- /dev/null +++ b/container-runtime/src/Interfaces/MakeEffects.ts @@ -0,0 +1,4 @@ +import { Effects } from "../Models/Effects" +import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk" +export type MakeProcedureEffects = (procedureId: string) => Effects +export type MakeMainEffects = () => MainEffects diff --git a/container-runtime/src/Interfaces/System.ts b/container-runtime/src/Interfaces/System.ts index 58e045356..01fd3c5ff 100644 --- a/container-runtime/src/Interfaces/System.ts +++ b/container-runtime/src/Interfaces/System.ts @@ -1,33 +1,54 @@ import { types as T } from "@start9labs/start-sdk" -import { JsonPath } from "../Models/JsonPath" import { RpcResult } from "../Adapters/RpcListener" -import { hostSystemStartOs } from "../Adapters/HostSystemStartOs" +import { Effects } from "../Models/Effects" +import { CallbackHolder } from "../Models/CallbackHolder" +import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk" + +export type Procedure = + | "/init" + | "/uninit" + | "/config/set" + | "/config/get" + | "/backup/create" + | "/backup/restore" + | "/actions/metadata" + | "/properties" + | `/actions/${string}/get` + | `/actions/${string}/run` + | `/dependencies/${string}/query` + | `/dependencies/${string}/update` + export type ExecuteResult = | { ok: unknown } | { err: { code: number; message: string } } export type System = { - // init(effects: Effects): Promise - // exit(effects: Effects): Promise - // start(effects: Effects): Promise - // stop(effects: Effects, options: { timeout: number, signal?: number }): Promise + init(): Promise + + start(effects: MainEffects): Promise + callCallback(callback: number, args: any[]): void + stop(): Promise execute( - effectCreator: ReturnType, + effects: Effects, options: { - id: string - procedure: JsonPath + procedure: Procedure + input: unknown + timeout?: number + }, + ): Promise + sandbox( + effects: Effects, + options: { + procedure: Procedure input: unknown timeout?: number }, ): Promise - // sandbox( - // effects: Effects, - // options: { - // procedure: JsonPath - // input: unknown - // timeout?: number - // }, - // ): Promise - exit(effects: T.Effects): Promise + exit(): Promise +} + +export type RunningMain = { + callbacks: CallbackHolder + stop(): Promise } diff --git a/container-runtime/src/Models/CallbackHolder.ts b/container-runtime/src/Models/CallbackHolder.ts index 6539dda88..b51af0bee 100644 --- a/container-runtime/src/Models/CallbackHolder.ts +++ b/container-runtime/src/Models/CallbackHolder.ts @@ -1,12 +1,14 @@ export class CallbackHolder { constructor() {} - private root = (Math.random() + 1).toString(36).substring(7) private inc = 0 private callbacks = new Map() private newId() { return this.inc++ } - addCallback(callback: Function) { + addCallback(callback?: Function) { + if (!callback) { + return + } const id = this.newId() this.callbacks.set(id, callback) return id diff --git a/container-runtime/src/Models/JsonPath.ts b/container-runtime/src/Models/JsonPath.ts index 314019154..95a2b3a00 100644 --- a/container-runtime/src/Models/JsonPath.ts +++ b/container-runtime/src/Models/JsonPath.ts @@ -28,8 +28,6 @@ export const jsonPath = some( literals( "/init", "/uninit", - "/main/start", - "/main/stop", "/config/set", "/config/get", "/backup/create", diff --git a/container-runtime/src/index.ts b/container-runtime/src/index.ts index 74be5b73a..5454bee3d 100644 --- a/container-runtime/src/index.ts +++ b/container-runtime/src/index.ts @@ -1,12 +1,13 @@ import { RpcListener } from "./Adapters/RpcListener" import { SystemForEmbassy } from "./Adapters/Systems/SystemForEmbassy" -import { hostSystemStartOs } from "./Adapters/HostSystemStartOs" +import { makeMainEffects, makeProcedureEffects } from "./Adapters/EffectCreator" import { AllGetDependencies } from "./Interfaces/AllGetDependencies" import { getSystem } from "./Adapters/Systems" const getDependencies: AllGetDependencies = { system: getSystem, - hostSystem: () => hostSystemStartOs, + makeProcedureEffects: () => makeProcedureEffects, + makeMainEffects: () => makeMainEffects, } new RpcListener(getDependencies) diff --git a/core/build-startos-bins.sh b/core/build-containerbox.sh similarity index 83% rename from core/build-startos-bins.sh rename to core/build-containerbox.sh index f81ddb093..f988d2de3 100755 --- a/core/build-startos-bins.sh +++ b/core/build-containerbox.sh @@ -28,9 +28,6 @@ set +e fail= echo "FEATURES=\"$FEATURES\"" echo "RUSTFLAGS=\"$RUSTFLAGS\"" -if ! rust-musl-builder sh -c "(cd core && cargo build --release --no-default-features --features cli,daemon,$FEATURES --locked --bin startbox --target=$ARCH-unknown-linux-musl)"; then - fail=true -fi if ! rust-musl-builder sh -c "(cd core && cargo build --release --no-default-features --features container-runtime,$FEATURES --locked --bin containerbox --target=$ARCH-unknown-linux-musl)"; then fail=true fi diff --git a/core/build-startbox.sh b/core/build-startbox.sh new file mode 100755 index 000000000..0604ba5da --- /dev/null +++ b/core/build-startbox.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +cd "$(dirname "${BASH_SOURCE[0]}")" + +set -e +shopt -s expand_aliases + +if [ -z "$ARCH" ]; then + ARCH=$(uname -m) +fi + +USE_TTY= +if tty -s; then + USE_TTY="-it" +fi + +cd .. +FEATURES="$(echo $ENVIRONMENT | sed 's/-/,/g')" +RUSTFLAGS="" + +if [[ "${ENVIRONMENT}" =~ (^|-)unstable($|-) ]]; then + RUSTFLAGS="--cfg tokio_unstable" +fi + +alias 'rust-musl-builder'='docker run $USE_TTY --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$HOME/.cargo/git":/root/.cargo/git -v "$(pwd)":/home/rust/src -w /home/rust/src -P messense/rust-musl-cross:$ARCH-musl' + +set +e +fail= +echo "FEATURES=\"$FEATURES\"" +echo "RUSTFLAGS=\"$RUSTFLAGS\"" +if ! rust-musl-builder sh -c "(cd core && cargo build --release --no-default-features --features cli,daemon,$FEATURES --locked --bin startbox --target=$ARCH-unknown-linux-musl)"; then + fail=true +fi +set -e +cd core + +sudo chown -R $USER target +sudo chown -R $USER ~/.cargo + +if [ -n "$fail" ]; then + exit 1 +fi diff --git a/core/models/src/id/package.rs b/core/models/src/id/package.rs index d2665e59a..6e22b9d51 100644 --- a/core/models/src/id/package.rs +++ b/core/models/src/id/package.rs @@ -61,6 +61,11 @@ impl Borrow for PackageId { self.0.as_ref() } } +impl<'a> Borrow for &'a PackageId { + fn borrow(&self) -> &str { + self.0.as_ref() + } +} impl AsRef for PackageId { fn as_ref(&self) -> &Path { self.0.as_ref().as_ref() diff --git a/core/models/src/procedure_name.rs b/core/models/src/procedure_name.rs index c8ae8c3a8..466835818 100644 --- a/core/models/src/procedure_name.rs +++ b/core/models/src/procedure_name.rs @@ -4,8 +4,6 @@ use crate::{ActionId, PackageId}; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ProcedureName { - StartMain, - StopMain, GetConfig, SetConfig, CreateBackup, @@ -25,8 +23,6 @@ impl ProcedureName { match self { ProcedureName::Init => "/init".to_string(), ProcedureName::Uninit => "/uninit".to_string(), - ProcedureName::StartMain => "/main/start".to_string(), - ProcedureName::StopMain => "/main/stop".to_string(), ProcedureName::SetConfig => "/config/set".to_string(), ProcedureName::GetConfig => "/config/get".to_string(), ProcedureName::CreateBackup => "/backup/create".to_string(), diff --git a/core/startos/src/auth.rs b/core/startos/src/auth.rs index d33320b78..03005998f 100644 --- a/core/startos/src/auth.rs +++ b/core/startos/src/auth.rs @@ -319,6 +319,7 @@ fn display_sessions(params: WithIoFormat, arg: SessionList) { pub struct ListParams { #[arg(skip)] #[ts(skip)] + #[serde(rename = "__auth_session")] // from Auth middleware session: InternedString, } diff --git a/core/startos/src/backup/os.rs b/core/startos/src/backup/os.rs index 7c8119e79..6f08c5f43 100644 --- a/core/startos/src/backup/os.rs +++ b/core/startos/src/backup/os.rs @@ -1,3 +1,4 @@ +use imbl_value::InternedString; use openssl::pkey::{PKey, Private}; use openssl::x509::X509; use patch_db::Value; @@ -97,7 +98,7 @@ impl OsBackupV0 { #[serde(rename = "kebab-case")] struct OsBackupV1 { server_id: String, // uuidv4 - hostname: String, // embassy-- + hostname: InternedString, // embassy-- net_key: Base64<[u8; 32]>, // Ed25519 Secret Key root_ca_key: Pem>, // PEM Encoded OpenSSL Key root_ca_cert: Pem, // PEM Encoded OpenSSL X509 Certificate @@ -127,7 +128,7 @@ impl OsBackupV1 { struct OsBackupV2 { server_id: String, // uuidv4 - hostname: String, // - + hostname: InternedString, // - root_ca_key: Pem>, // PEM Encoded OpenSSL Key root_ca_cert: Pem, // PEM Encoded OpenSSL X509 Certificate ssh_key: Pem, // PEM Encoded OpenSSH Key diff --git a/core/startos/src/bins/container_cli.rs b/core/startos/src/bins/container_cli.rs index a33a99131..db7cbd36a 100644 --- a/core/startos/src/bins/container_cli.rs +++ b/core/startos/src/bins/container_cli.rs @@ -15,7 +15,7 @@ pub fn main(args: impl IntoIterator) { EmbassyLogger::init(); if let Err(e) = CliApp::new( |cfg: ContainerClientConfig| Ok(ContainerCliContext::init(cfg)), - crate::service::service_effect_handler::service_effect_handler(), + crate::service::effects::handler(), ) .run(args) { diff --git a/core/startos/src/context/rpc.rs b/core/startos/src/context/rpc.rs index 4920a6223..ba1ab3fbb 100644 --- a/core/startos/src/context/rpc.rs +++ b/core/startos/src/context/rpc.rs @@ -29,6 +29,7 @@ use crate::net::wifi::WpaCli; use crate::prelude::*; use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle}; use crate::rpc_continuations::{OpenAuthedContinuations, RpcContinuations}; +use crate::service::effects::callbacks::ServiceCallbacks; use crate::service::ServiceMap; use crate::shutdown::Shutdown; use crate::system::get_mem_info; @@ -52,6 +53,7 @@ pub struct RpcContextSeed { pub lxc_manager: Arc, pub open_authed_continuations: OpenAuthedContinuations, pub rpc_continuations: RpcContinuations, + pub callbacks: ServiceCallbacks, pub wifi_manager: Option>>, pub current_secret: Arc, pub client: Client, @@ -225,6 +227,7 @@ impl RpcContext { lxc_manager: Arc::new(LxcManager::new()), open_authed_continuations: OpenAuthedContinuations::new(), rpc_continuations: RpcContinuations::new(), + callbacks: Default::default(), wifi_manager: wifi_interface .clone() .map(|i| Arc::new(RwLock::new(WpaCli::init(i)))), diff --git a/core/startos/src/context/setup.rs b/core/startos/src/context/setup.rs index 6041f49b9..de8dced07 100644 --- a/core/startos/src/context/setup.rs +++ b/core/startos/src/context/setup.rs @@ -5,6 +5,7 @@ use std::time::Duration; use futures::{Future, StreamExt}; use helpers::NonDetachingJoinHandle; +use imbl_value::InternedString; use josekit::jwk::Jwk; use patch_db::PatchDb; use rpc_toolkit::Context; @@ -40,7 +41,8 @@ lazy_static::lazy_static! { #[ts(export)] pub struct SetupResult { pub tor_address: String, - pub lan_address: String, + #[ts(type = "string")] + pub lan_address: InternedString, pub root_ca: String, } impl TryFrom<&AccountInfo> for SetupResult { diff --git a/core/startos/src/db/model/public.rs b/core/startos/src/db/model/public.rs index 725475602..b20693a90 100644 --- a/core/startos/src/db/model/public.rs +++ b/core/startos/src/db/model/public.rs @@ -20,6 +20,7 @@ use crate::db::model::package::AllPackageData; use crate::net::utils::{get_iface_ipv4_addr, get_iface_ipv6_addr}; use crate::prelude::*; use crate::progress::FullProgress; +use crate::system::SmtpValue; use crate::util::cpupower::Governor; use crate::version::{Current, VersionT}; use crate::{ARCH, PLATFORM}; @@ -107,7 +108,8 @@ pub struct ServerInfo { #[ts(type = "string")] pub platform: InternedString, pub id: String, - pub hostname: String, + #[ts(type = "string")] + pub hostname: InternedString, #[ts(type = "string")] pub version: Version, #[ts(type = "string | null")] @@ -135,7 +137,7 @@ pub struct ServerInfo { #[serde(default)] pub zram: bool, pub governor: Option, - pub smtp: Option, + pub smtp: Option, } #[derive(Debug, Deserialize, Serialize, HasModel, TS)] diff --git a/core/startos/src/hostname.rs b/core/startos/src/hostname.rs index c4332354c..36bb5d8a4 100644 --- a/core/startos/src/hostname.rs +++ b/core/startos/src/hostname.rs @@ -1,3 +1,5 @@ +use imbl_value::InternedString; +use lazy_format::lazy_format; use rand::{thread_rng, Rng}; use tokio::process::Command; use tracing::instrument; @@ -5,7 +7,7 @@ use tracing::instrument; use crate::util::Invoke; use crate::{Error, ErrorKind}; #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] -pub struct Hostname(pub String); +pub struct Hostname(pub InternedString); lazy_static::lazy_static! { static ref ADJECTIVES: Vec = include_str!("./assets/adjectives.txt").lines().map(|x| x.to_string()).collect(); @@ -18,15 +20,16 @@ impl AsRef for Hostname { } impl Hostname { - pub fn lan_address(&self) -> String { - format!("https://{}.local", self.0) + pub fn lan_address(&self) -> InternedString { + InternedString::from_display(&lazy_format!("https://{}.local", self.0)) } - pub fn local_domain_name(&self) -> String { - format!("{}.local", self.0) + pub fn local_domain_name(&self) -> InternedString { + InternedString::from_display(&lazy_format!("{}.local", self.0)) } - pub fn no_dot_host_name(&self) -> String { - self.0.to_owned() + + pub fn no_dot_host_name(&self) -> InternedString { + self.0.clone() } } @@ -34,7 +37,9 @@ pub fn generate_hostname() -> Hostname { let mut rng = thread_rng(); let adjective = &ADJECTIVES[rng.gen_range(0..ADJECTIVES.len())]; let noun = &NOUNS[rng.gen_range(0..NOUNS.len())]; - Hostname(format!("{adjective}-{noun}")) + Hostname(InternedString::from_display(&lazy_format!( + "{adjective}-{noun}" + ))) } pub fn generate_id() -> String { @@ -48,12 +53,12 @@ pub async fn get_current_hostname() -> Result { .invoke(ErrorKind::ParseSysInfo) .await?; let out_string = String::from_utf8(out)?; - Ok(Hostname(out_string.trim().to_owned())) + Ok(Hostname(out_string.trim().into())) } #[instrument(skip_all)] pub async fn set_hostname(hostname: &Hostname) -> Result<(), Error> { - let hostname: &String = &hostname.0; + let hostname = &*hostname.0; Command::new("hostnamectl") .arg("--static") .arg("set-hostname") diff --git a/core/startos/src/lib.rs b/core/startos/src/lib.rs index 4882d998e..feeb5a647 100644 --- a/core/startos/src/lib.rs +++ b/core/startos/src/lib.rs @@ -224,6 +224,18 @@ pub fn server() -> ParentHandler { }) .with_call_remote::(), ) + .subcommand( + "set-smtp", + from_fn_async(system::set_system_smtp) + .no_display() + .with_call_remote::(), + ) + .subcommand( + "clear-smtp", + from_fn_async(system::clear_system_smtp) + .no_display() + .with_call_remote::(), + ) } pub fn package() -> ParentHandler { diff --git a/core/startos/src/net/dns.rs b/core/startos/src/net/dns.rs index ba69b6c16..090e845b0 100644 --- a/core/startos/src/net/dns.rs +++ b/core/startos/src/net/dns.rs @@ -34,7 +34,7 @@ struct Resolver { impl Resolver { async fn resolve(&self, name: &Name) -> Option> { match name.iter().next_back() { - Some(b"embassy") => { + Some(b"embassy") | Some(b"startos") => { if let Some(pkg) = name.iter().rev().skip(1).next() { if let Some(ip) = self.services.read().await.get(&Some( std::str::from_utf8(pkg) diff --git a/core/startos/src/net/host/address.rs b/core/startos/src/net/host/address.rs index d9e2f4206..9b16441ce 100644 --- a/core/startos/src/net/host/address.rs +++ b/core/startos/src/net/host/address.rs @@ -1,7 +1,13 @@ +use std::fmt; +use std::str::FromStr; + +use imbl_value::InternedString; use serde::{Deserialize, Serialize}; use torut::onion::OnionAddressV3; use ts_rs::TS; +use crate::prelude::*; + #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, TS)] #[serde(rename_all = "camelCase")] #[serde(tag = "kind")] @@ -11,4 +17,32 @@ pub enum HostAddress { #[ts(type = "string")] address: OnionAddressV3, }, + Domain { + #[ts(type = "string")] + address: InternedString, + }, +} + +impl FromStr for HostAddress { + type Err = Error; + fn from_str(s: &str) -> Result { + if let Some(addr) = s.strip_suffix(".onion") { + Ok(HostAddress::Onion { + address: addr + .parse::() + .with_kind(ErrorKind::ParseUrl)?, + }) + } else { + Ok(HostAddress::Domain { address: s.into() }) + } + } +} + +impl fmt::Display for HostAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Onion { address } => write!(f, "{address}"), + Self::Domain { address } => write!(f, "{address}"), + } + } } diff --git a/core/startos/src/net/host/mod.rs b/core/startos/src/net/host/mod.rs index 6cbb2dfd5..175fe3e83 100644 --- a/core/startos/src/net/host/mod.rs +++ b/core/startos/src/net/host/mod.rs @@ -40,6 +40,10 @@ impl Host { hostname_info: BTreeMap::new(), } } + pub fn addresses(&self) -> impl Iterator { + // TODO: handle primary + self.addresses.iter() + } } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)] diff --git a/core/startos/src/net/net_controller.rs b/core/startos/src/net/net_controller.rs index 270c7ca09..521888665 100644 --- a/core/startos/src/net/net_controller.rs +++ b/core/startos/src/net/net_controller.rs @@ -4,6 +4,7 @@ use std::sync::{Arc, Weak}; use color_eyre::eyre::eyre; use imbl::OrdMap; +use imbl_value::InternedString; use models::{HostId, OptionExt, PackageId}; use torut::onion::{OnionAddressV3, TorSecretKeyV3}; use tracing::instrument; @@ -67,6 +68,14 @@ impl PreInitNetController { alpn.clone(), ) .await?; + self.vhost + .add( + Some("startos".into()), + 443, + ([127, 0, 0, 1], 80).into(), + alpn.clone(), + ) + .await?; // LAN IP self.os_bindings.push( @@ -113,7 +122,9 @@ impl PreInitNetController { self.os_bindings.push( self.vhost .add( - Some(tor_key.public().get_onion_address().to_string()), + Some(InternedString::from_display( + &tor_key.public().get_onion_address(), + )), 443, ([127, 0, 0, 1], 80).into(), alpn.clone(), @@ -189,7 +200,15 @@ impl NetController { #[derive(Default, Debug)] struct HostBinds { - lan: BTreeMap, Vec>)>, + lan: BTreeMap< + u16, + ( + LanInfo, + Option, + BTreeSet, + Vec>, + ), + >, tor: BTreeMap, Vec>)>, } @@ -234,20 +253,35 @@ impl NetService { .await?; self.update(id, host).await } + pub async fn clear_bindings(&mut self) -> Result<(), Error> { - // TODO BLUJ - Ok(()) + let ctrl = self.net_controller()?; + let mut errors = ErrorCollection::new(); + for (_, binds) in std::mem::take(&mut self.binds) { + for (_, (lan, _, _, rc)) in binds.lan { + drop(rc); + if let Some(external) = lan.assigned_ssl_port { + ctrl.vhost.gc(None, external).await?; + } + if let Some(external) = lan.assigned_port { + ctrl.forward.gc(external).await?; + } + } + for (addr, (_, rcs)) in binds.tor { + drop(rcs); + errors.handle(ctrl.tor.gc(Some(addr), None).await); + } + } + std::mem::take(&mut self.dns); + errors.handle(ctrl.dns.gc(Some(self.id.clone()), self.ip).await); + errors.into_result() } async fn update(&mut self, id: HostId, host: Host) -> Result<(), Error> { let ctrl = self.net_controller()?; let mut hostname_info = BTreeMap::new(); - let binds = { - if !self.binds.contains_key(&id) { - self.binds.insert(id.clone(), Default::default()); - } - self.binds.get_mut(&id).unwrap() - }; + let binds = self.binds.entry(id.clone()).or_default(); + let peek = ctrl.db.peek().await; // LAN @@ -256,37 +290,71 @@ impl NetService { let hostname = server_info.as_hostname().de()?; for (port, bind) in &host.bindings { let old_lan_bind = binds.lan.remove(port); - let old_lan_port = old_lan_bind.as_ref().map(|(external, _, _)| *external); let lan_bind = old_lan_bind - .filter(|(external, ssl, _)| ssl == &bind.options.add_ssl && bind.lan == *external); // only keep existing binding if relevant details match + .as_ref() + .filter(|(external, ssl, _, _)| { + ssl == &bind.options.add_ssl && bind.lan == *external + }) + .cloned(); // only keep existing binding if relevant details match if bind.lan.assigned_port.is_some() || bind.lan.assigned_ssl_port.is_some() { let new_lan_bind = if let Some(b) = lan_bind { b } else { - let mut rcs = Vec::with_capacity(2); + let mut rcs = Vec::with_capacity(2 + host.addresses.len()); + let mut hostnames = BTreeSet::new(); if let Some(ssl) = &bind.options.add_ssl { let external = bind .lan .assigned_ssl_port .or_not_found("assigned ssl port")?; + let target = (self.ip, *port).into(); + let connect_ssl = if let Some(alpn) = ssl.alpn.clone() { + Err(alpn) + } else { + if bind.options.secure.as_ref().map_or(false, |s| s.ssl) { + Ok(()) + } else { + Err(AlpnInfo::Reflect) + } + }; rcs.push( ctrl.vhost - .add( - None, - external, - (self.ip, *port).into(), - if let Some(alpn) = ssl.alpn.clone() { - Err(alpn) - } else { - if bind.options.secure.as_ref().map_or(false, |s| s.ssl) { - Ok(()) - } else { - Err(AlpnInfo::Reflect) - } - }, - ) + .add(None, external, target, connect_ssl.clone()) .await?, ); + for address in host.addresses() { + match address { + HostAddress::Onion { address } => { + let hostname = InternedString::from_display(address); + if hostnames.insert(hostname.clone()) { + rcs.push( + ctrl.vhost + .add( + Some(hostname), + external, + target, + connect_ssl.clone(), + ) + .await?, + ); + } + } + HostAddress::Domain { address } => { + if hostnames.insert(address.clone()) { + rcs.push( + ctrl.vhost + .add( + Some(address.clone()), + external, + target, + connect_ssl.clone(), + ) + .await?, + ); + } + } + } + } } if let Some(security) = bind.options.secure { if bind.options.add_ssl.is_some() && security.ssl { @@ -297,7 +365,7 @@ impl NetService { rcs.push(ctrl.forward.add(external, (self.ip, *port).into()).await?); } } - (bind.lan, bind.options.add_ssl.clone(), rcs) + (bind.lan, bind.options.add_ssl.clone(), hostnames, rcs) }; let mut bind_hostname_info: Vec = hostname_info.remove(port).unwrap_or_default(); @@ -337,9 +405,12 @@ impl NetService { hostname_info.insert(*port, bind_hostname_info); binds.lan.insert(*port, new_lan_bind); } - if let Some(lan) = old_lan_port { + if let Some((lan, _, hostnames, _)) = old_lan_bind { if let Some(external) = lan.assigned_ssl_port { ctrl.vhost.gc(None, external).await?; + for hostname in hostnames { + ctrl.vhost.gc(Some(hostname), external).await?; + } } if let Some(external) = lan.assigned_port { ctrl.forward.gc(external).await?; @@ -347,18 +418,21 @@ impl NetService { } } let mut removed = BTreeSet::new(); - binds.lan.retain(|internal, (external, _, _)| { + binds.lan.retain(|internal, (external, _, hostnames, _)| { if host.bindings.contains_key(internal) { true } else { - removed.insert(*external); + removed.insert((*external, std::mem::take(hostnames))); false } }); - for lan in removed { + for (lan, hostnames) in removed { if let Some(external) = lan.assigned_ssl_port { ctrl.vhost.gc(None, external).await?; + for hostname in hostnames { + ctrl.vhost.gc(Some(hostname), external).await?; + } } if let Some(external) = lan.assigned_port { ctrl.forward.gc(external).await?; @@ -401,48 +475,44 @@ impl NetService { ); } } + let mut keep_tor_addrs = BTreeSet::new(); - for addr in match host.kind { - HostKind::Multi => { - // itertools::Either::Left( - host.addresses.iter() - // ) - } // HostKind::Single | HostKind::Static => itertools::Either::Right(&host.primary), - } { - match addr { - HostAddress::Onion { address } => { - keep_tor_addrs.insert(address); - let old_tor_bind = binds.tor.remove(address); - let tor_bind = old_tor_bind.filter(|(ports, _)| ports == &tor_binds); - let new_tor_bind = if let Some(tor_bind) = tor_bind { - tor_bind - } else { - let key = peek - .as_private() - .as_key_store() - .as_onion() - .get_key(address)?; - let rcs = ctrl - .tor - .add(key, tor_binds.clone().into_iter().collect()) - .await?; - (tor_binds.clone(), rcs) - }; - for (internal, ports) in &tor_hostname_ports { - let mut bind_hostname_info = - hostname_info.remove(internal).unwrap_or_default(); - bind_hostname_info.push(HostnameInfo::Onion { - hostname: OnionHostname { - value: address.to_string(), - port: ports.non_ssl, - ssl_port: ports.ssl, - }, - }); - hostname_info.insert(*internal, bind_hostname_info); - } - binds.tor.insert(address.clone(), new_tor_bind); - } + for tor_addr in host.addresses().filter_map(|a| { + if let HostAddress::Onion { address } = a { + Some(address) + } else { + None } + }) { + keep_tor_addrs.insert(tor_addr); + let old_tor_bind = binds.tor.remove(tor_addr); + let tor_bind = old_tor_bind.filter(|(ports, _)| ports == &tor_binds); + let new_tor_bind = if let Some(tor_bind) = tor_bind { + tor_bind + } else { + let key = peek + .as_private() + .as_key_store() + .as_onion() + .get_key(tor_addr)?; + let rcs = ctrl + .tor + .add(key, tor_binds.clone().into_iter().collect()) + .await?; + (tor_binds.clone(), rcs) + }; + for (internal, ports) in &tor_hostname_ports { + let mut bind_hostname_info = hostname_info.remove(internal).unwrap_or_default(); + bind_hostname_info.push(HostnameInfo::Onion { + hostname: OnionHostname { + value: tor_addr.to_string(), + port: ports.non_ssl, + ssl_port: ports.ssl, + }, + }); + hostname_info.insert(*internal, bind_hostname_info); + } + binds.tor.insert(tor_addr.clone(), new_tor_bind); } for addr in binds.tor.keys() { if !keep_tor_addrs.contains(addr) { @@ -462,26 +532,8 @@ impl NetService { pub async fn remove_all(mut self) -> Result<(), Error> { self.shutdown = true; - let mut errors = ErrorCollection::new(); if let Some(ctrl) = Weak::upgrade(&self.controller) { - for (_, binds) in std::mem::take(&mut self.binds) { - for (_, (lan, _, rc)) in binds.lan { - drop(rc); - if let Some(external) = lan.assigned_ssl_port { - ctrl.vhost.gc(None, external).await?; - } - if let Some(external) = lan.assigned_port { - ctrl.forward.gc(external).await?; - } - } - for (addr, (_, rcs)) in binds.tor { - drop(rcs); - errors.handle(ctrl.tor.gc(Some(addr), None).await); - } - } - std::mem::take(&mut self.dns); - errors.handle(ctrl.dns.gc(Some(self.id.clone()), self.ip).await); - errors.into_result() + self.clear_bindings().await } else { tracing::warn!("NetService dropped after NetController is shutdown"); Err(Error::new( @@ -495,11 +547,11 @@ impl NetService { self.ip } - pub fn get_ext_port(&self, host_id: HostId, internal_port: u16) -> Result { + pub fn get_lan_port(&self, host_id: HostId, internal_port: u16) -> Result { let host_id_binds = self.binds.get_key_value(&host_id); match host_id_binds { Some((_, binds)) => { - if let Some((lan, _, _)) = binds.lan.get(&internal_port) { + if let Some((lan, _, _, _)) = binds.lan.get(&internal_port) { Ok(*lan) } else { Err(Error::new( diff --git a/core/startos/src/net/ssl.rs b/core/startos/src/net/ssl.rs index 382006072..29bcd9652 100644 --- a/core/startos/src/net/ssl.rs +++ b/core/startos/src/net/ssl.rs @@ -1,13 +1,13 @@ -use std::cmp::Ordering; +use std::cmp::{min, Ordering}; use std::collections::{BTreeMap, BTreeSet}; use std::net::IpAddr; use std::path::Path; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use futures::FutureExt; use imbl_value::InternedString; use libc::time_t; -use openssl::asn1::{Asn1Integer, Asn1Time}; +use openssl::asn1::{Asn1Integer, Asn1Time, Asn1TimeRef}; use openssl::bn::{BigNum, MsbOption}; use openssl::ec::{EcGroup, EcKey}; use openssl::hash::MessageDigest; @@ -17,6 +17,7 @@ use openssl::x509::{X509Builder, X509Extension, X509NameBuilder, X509}; use openssl::*; use patch_db::HasModel; use serde::{Deserialize, Serialize}; +use tokio::time::Instant; use tracing::instrument; use crate::account::AccountInfo; @@ -126,12 +127,18 @@ impl Model { } } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] pub struct CertData { pub keys: PKeyPair, pub certs: CertPair, } +impl CertData { + pub fn expiration(&self) -> Result { + self.certs.expiration() + } +} +#[derive(Debug, Clone, PartialEq, Eq)] pub struct FullchainCertData { pub root: X509, pub int: X509, @@ -144,6 +151,16 @@ impl FullchainCertData { pub fn fullchain_nistp256(&self) -> Vec<&X509> { vec![&self.leaf.certs.nistp256, &self.int, &self.root] } + pub fn expiration(&self) -> Result { + [ + asn1_time_to_system_time(self.root.not_after())?, + asn1_time_to_system_time(self.int.not_after())?, + self.leaf.expiration()?, + ] + .into_iter() + .min() + .ok_or_else(|| Error::new(eyre!("unreachable"), ErrorKind::Unknown)) + } } static CERTIFICATE_VERSION: i32 = 2; // X509 version 3 is actually encoded as '2' in the cert because fuck you. @@ -155,6 +172,26 @@ fn unix_time(time: SystemTime) -> time_t { .unwrap_or_default() } +lazy_static::lazy_static! { + static ref ASN1_UNIX_EPOCH: Asn1Time = Asn1Time::from_unix(0).unwrap(); +} + +fn asn1_time_to_system_time(time: &Asn1TimeRef) -> Result { + let diff = time.diff(&**ASN1_UNIX_EPOCH)?; + let mut res = UNIX_EPOCH; + if diff.days >= 0 { + res += Duration::from_secs(diff.days as u64 * 86400); + } else { + res -= Duration::from_secs((-1 * diff.days) as u64 * 86400); + } + if diff.secs >= 0 { + res += Duration::from_secs(diff.secs as u64); + } else { + res -= Duration::from_secs((-1 * diff.secs) as u64); + } + Ok(res) +} + #[derive(Debug, Clone, Deserialize, Serialize)] pub struct PKeyPair { #[serde(with = "crate::util::serde::pem")] @@ -162,6 +199,12 @@ pub struct PKeyPair { #[serde(with = "crate::util::serde::pem")] pub nistp256: PKey, } +impl PartialEq for PKeyPair { + fn eq(&self, other: &Self) -> bool { + self.ed25519.public_eq(&other.ed25519) && self.nistp256.public_eq(&other.nistp256) + } +} +impl Eq for PKeyPair {} #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] pub struct CertPair { @@ -170,6 +213,14 @@ pub struct CertPair { #[serde(with = "crate::util::serde::pem")] pub nistp256: X509, } +impl CertPair { + pub fn expiration(&self) -> Result { + Ok(min( + asn1_time_to_system_time(self.ed25519.not_after())?, + asn1_time_to_system_time(self.nistp256.not_after())?, + )) + } +} pub async fn root_ca_start_time() -> Result { Ok(if check_time_is_synchronized().await? { diff --git a/core/startos/src/net/vhost.rs b/core/startos/src/net/vhost.rs index e6a9d5b21..cdd752709 100644 --- a/core/startos/src/net/vhost.rs +++ b/core/startos/src/net/vhost.rs @@ -46,7 +46,7 @@ impl VHostController { #[instrument(skip_all)] pub async fn add( &self, - hostname: Option, + hostname: Option, external: u16, target: SocketAddr, connect_ssl: Result<(), AlpnInfo>, // Ok: yes, connect using ssl, pass through alpn; Err: connect tcp, use provided strategy for alpn @@ -70,7 +70,7 @@ impl VHostController { Ok(rc?) } #[instrument(skip_all)] - pub async fn gc(&self, hostname: Option, external: u16) -> Result<(), Error> { + pub async fn gc(&self, hostname: Option, external: u16) -> Result<(), Error> { let mut writable = self.servers.lock().await; if let Some(server) = writable.remove(&external) { server.gc(hostname).await?; @@ -102,7 +102,7 @@ impl Default for AlpnInfo { } struct VHostServer { - mapping: Weak, BTreeMap>>>>, + mapping: Weak, BTreeMap>>>>, _thread: NonDetachingJoinHandle<()>, } impl VHostServer { @@ -179,7 +179,7 @@ impl VHostServer { } }; let target_name = - mid.client_hello().server_name().map(|s| s.to_owned()); + mid.client_hello().server_name().map(|s| s.into()); let target = { let mapping = mapping.read().await; mapping @@ -208,9 +208,7 @@ impl VHostServer { let mut tcp_stream = TcpStream::connect(target.addr).await?; let hostnames = target_name - .as_ref() .into_iter() - .map(InternedString::intern) .chain( db.peek() .await @@ -405,7 +403,11 @@ impl VHostServer { .into(), }) } - async fn add(&self, hostname: Option, target: TargetInfo) -> Result, Error> { + async fn add( + &self, + hostname: Option, + target: TargetInfo, + ) -> Result, Error> { if let Some(mapping) = Weak::upgrade(&self.mapping) { let mut writable = mapping.write().await; let mut targets = writable.remove(&hostname).unwrap_or_default(); @@ -424,7 +426,7 @@ impl VHostServer { )) } } - async fn gc(&self, hostname: Option) -> Result<(), Error> { + async fn gc(&self, hostname: Option) -> Result<(), Error> { if let Some(mapping) = Weak::upgrade(&self.mapping) { let mut writable = mapping.write().await; let mut targets = writable.remove(&hostname).unwrap_or_default(); diff --git a/core/startos/src/registry/os/asset/add.rs b/core/startos/src/registry/os/asset/add.rs index c18d05b8f..d609063ea 100644 --- a/core/startos/src/registry/os/asset/add.rs +++ b/core/startos/src/registry/os/asset/add.rs @@ -4,6 +4,7 @@ use std::path::PathBuf; use chrono::Utc; use clap::Parser; +use exver::Version; use imbl_value::InternedString; use itertools::Itertools; use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; @@ -27,7 +28,6 @@ use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::ArchiveSource; use crate::util::io::open_file; use crate::util::serde::Base64; -use crate::util::VersionString; pub fn add_api() -> ParentHandler { ParentHandler::new() @@ -55,7 +55,8 @@ pub fn add_api() -> ParentHandler { #[serde(rename_all = "camelCase")] #[ts(export)] pub struct AddAssetParams { - pub version: VersionString, + #[ts(type = "string")] + pub version: Version, #[ts(type = "string")] pub platform: InternedString, #[ts(type = "string")] @@ -154,7 +155,7 @@ pub struct CliAddAssetParams { #[arg(short = 'p', long = "platform")] pub platform: InternedString, #[arg(short = 'v', long = "version")] - pub version: VersionString, + pub version: Version, pub file: PathBuf, pub url: Url, } @@ -209,11 +210,18 @@ pub async fn cli_add_asset( hash: Base64(*blake3.as_bytes()), size, }; - let signature = Ed25519.sign_commitment(ctx.developer_key()?, &commitment, SIG_CONTEXT)?; + let signature = AnySignature::Ed25519(Ed25519.sign_commitment( + ctx.developer_key()?, + &commitment, + SIG_CONTEXT, + )?); sign_phase.complete(); verify_phase.start(); let src = HttpSource::new(ctx.client.clone(), url.clone()).await?; + if let Some(size) = src.size().await { + verify_phase.set_total(size); + } let mut writer = verify_phase.writer(VerifyingWriter::new( tokio::io::sink(), Some((blake3::Hash::from_bytes(*commitment.hash), commitment.size)), diff --git a/core/startos/src/registry/os/asset/get.rs b/core/startos/src/registry/os/asset/get.rs index b185cf6a4..ad0010dca 100644 --- a/core/startos/src/registry/os/asset/get.rs +++ b/core/startos/src/registry/os/asset/get.rs @@ -3,6 +3,7 @@ use std::panic::UnwindSafe; use std::path::{Path, PathBuf}; use clap::Parser; +use exver::Version; use helpers::AtomicFile; use imbl_value::{json, InternedString}; use itertools::Itertools; @@ -21,7 +22,6 @@ use crate::registry::signer::commitment::blake3::Blake3Commitment; use crate::registry::signer::commitment::Commitment; use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::util::io::open_file; -use crate::util::VersionString; pub fn get_api() -> ParentHandler { ParentHandler::new() @@ -37,7 +37,8 @@ pub fn get_api() -> ParentHandler { #[serde(rename_all = "camelCase")] #[ts(export)] pub struct GetOsAssetParams { - pub version: VersionString, + #[ts(type = "string")] + pub version: Version, #[ts(type = "string")] pub platform: InternedString, } @@ -91,7 +92,7 @@ pub async fn get_squashfs( #[command(rename_all = "kebab-case")] #[serde(rename_all = "camelCase")] pub struct CliGetOsAssetParams { - pub version: VersionString, + pub version: Version, pub platform: InternedString, #[arg(long = "download", short = 'd')] pub download: Option, diff --git a/core/startos/src/registry/os/asset/sign.rs b/core/startos/src/registry/os/asset/sign.rs index 12903d8a1..18b603daf 100644 --- a/core/startos/src/registry/os/asset/sign.rs +++ b/core/startos/src/registry/os/asset/sign.rs @@ -3,6 +3,7 @@ use std::panic::UnwindSafe; use std::path::PathBuf; use clap::Parser; +use exver::Version; use imbl_value::InternedString; use itertools::Itertools; use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; @@ -23,7 +24,6 @@ use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile; use crate::s9pk::merkle_archive::source::ArchiveSource; use crate::util::io::open_file; use crate::util::serde::Base64; -use crate::util::VersionString; pub fn sign_api() -> ParentHandler { ParentHandler::new() @@ -51,7 +51,8 @@ pub fn sign_api() -> ParentHandler { #[serde(rename_all = "camelCase")] #[ts(export)] pub struct SignAssetParams { - version: VersionString, + #[ts(type = "string")] + version: Version, #[ts(type = "string")] platform: InternedString, #[ts(skip)] @@ -137,7 +138,7 @@ pub struct CliSignAssetParams { #[arg(short = 'p', long = "platform")] pub platform: InternedString, #[arg(short = 'v', long = "version")] - pub version: VersionString, + pub version: Version, pub file: PathBuf, } @@ -189,7 +190,11 @@ pub async fn cli_sign_asset( hash: Base64(*blake3.as_bytes()), size, }; - let signature = Ed25519.sign_commitment(ctx.developer_key()?, &commitment, SIG_CONTEXT)?; + let signature = AnySignature::Ed25519(Ed25519.sign_commitment( + ctx.developer_key()?, + &commitment, + SIG_CONTEXT, + )?); sign_phase.complete(); index_phase.start(); diff --git a/core/startos/src/registry/os/index.rs b/core/startos/src/registry/os/index.rs index 0b1ca5b89..b61cb8f96 100644 --- a/core/startos/src/registry/os/index.rs +++ b/core/startos/src/registry/os/index.rs @@ -1,6 +1,6 @@ use std::collections::{BTreeMap, BTreeSet}; -use exver::VersionRange; +use exver::{Version, VersionRange}; use imbl_value::InternedString; use serde::{Deserialize, Serialize}; use ts_rs::TS; @@ -10,14 +10,28 @@ use crate::registry::asset::RegistryAsset; use crate::registry::context::RegistryContext; use crate::registry::signer::commitment::blake3::Blake3Commitment; use crate::rpc_continuations::Guid; -use crate::util::VersionString; #[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] #[serde(rename_all = "camelCase")] #[model = "Model"] #[ts(export)] pub struct OsIndex { - pub versions: BTreeMap, + pub versions: OsVersionInfoMap, +} + +#[derive(Debug, Default, Deserialize, Serialize, TS)] +pub struct OsVersionInfoMap( + #[ts(as = "BTreeMap::")] pub BTreeMap, +); +impl Map for OsVersionInfoMap { + type Key = Version; + type Value = OsVersionInfo; + fn key_str(key: &Self::Key) -> Result, Error> { + Ok(InternedString::from_display(key)) + } + fn key_string(key: &Self::Key) -> Result { + Ok(InternedString::from_display(key)) + } } #[derive(Debug, Default, Deserialize, Serialize, HasModel, TS)] diff --git a/core/startos/src/registry/os/version/mod.rs b/core/startos/src/registry/os/version/mod.rs index 242ae28a7..4c0568a80 100644 --- a/core/startos/src/registry/os/version/mod.rs +++ b/core/startos/src/registry/os/version/mod.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use chrono::Utc; use clap::Parser; -use exver::VersionRange; +use exver::{Version, VersionRange}; use itertools::Itertools; use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; @@ -15,7 +15,6 @@ use crate::registry::context::RegistryContext; use crate::registry::os::index::OsVersionInfo; use crate::registry::signer::sign::AnyVerifyingKey; use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat}; -use crate::util::VersionString; pub mod signer; @@ -53,7 +52,8 @@ pub fn version_api() -> ParentHandler { #[serde(rename_all = "camelCase")] #[ts(export)] pub struct AddVersionParams { - pub version: VersionString, + #[ts(type = "string")] + pub version: Version, pub headline: String, pub release_notes: String, #[ts(type = "string")] @@ -99,7 +99,8 @@ pub async fn add_version( #[serde(rename_all = "camelCase")] #[ts(export)] pub struct RemoveVersionParams { - pub version: VersionString, + #[ts(type = "string")] + pub version: Version, } pub async fn remove_version( @@ -124,7 +125,7 @@ pub async fn remove_version( pub struct GetOsVersionParams { #[ts(type = "string | null")] #[arg(long = "src")] - pub source: Option, + pub source: Option, #[ts(type = "string | null")] #[arg(long = "target")] pub target: Option, @@ -144,7 +145,7 @@ pub async fn get_version( server_id, arch, }: GetOsVersionParams, -) -> Result, Error> { +) -> Result, Error> { if let (Some(pool), Some(server_id), Some(arch)) = (&ctx.pool, server_id, arch) { let created_at = Utc::now(); @@ -176,10 +177,7 @@ pub async fn get_version( .collect() } -pub fn display_version_info( - params: WithIoFormat, - info: BTreeMap, -) { +pub fn display_version_info(params: WithIoFormat, info: BTreeMap) { use prettytable::*; if let Some(format) = params.format { @@ -197,7 +195,7 @@ pub fn display_version_info( ]); for (version, info) in &info { table.add_row(row![ - version.as_str(), + &version.to_string(), &info.headline, &info.release_notes, &info.iso.keys().into_iter().join(", "), diff --git a/core/startos/src/registry/os/version/signer.rs b/core/startos/src/registry/os/version/signer.rs index bb15860aa..51f7c6719 100644 --- a/core/startos/src/registry/os/version/signer.rs +++ b/core/startos/src/registry/os/version/signer.rs @@ -1,6 +1,7 @@ use std::collections::BTreeMap; use clap::Parser; +use exver::Version; use rpc_toolkit::{from_fn_async, Context, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use ts_rs::TS; @@ -12,7 +13,6 @@ use crate::registry::context::RegistryContext; use crate::registry::signer::SignerInfo; use crate::rpc_continuations::Guid; use crate::util::serde::HandlerExtSerde; -use crate::util::VersionString; pub fn signer_api() -> ParentHandler { ParentHandler::new() @@ -44,7 +44,8 @@ pub fn signer_api() -> ParentHandler { #[serde(rename_all = "camelCase")] #[ts(export)] pub struct VersionSignerParams { - pub version: VersionString, + #[ts(type = "string")] + pub version: Version, pub signer: Guid, } @@ -104,7 +105,8 @@ pub async fn remove_version_signer( #[serde(rename_all = "camelCase")] #[ts(export)] pub struct ListVersionSignersParams { - pub version: VersionString, + #[ts(type = "string")] + pub version: Version, } pub async fn list_version_signers( diff --git a/core/startos/src/service/cli.rs b/core/startos/src/service/cli.rs index f5e04999c..95add37fb 100644 --- a/core/startos/src/service/cli.rs +++ b/core/startos/src/service/cli.rs @@ -9,7 +9,7 @@ use rpc_toolkit::{call_remote_socket, yajrc, CallRemote, Context, Empty}; use tokio::runtime::Runtime; use crate::lxc::HOST_RPC_SERVER_SOCKET; -use crate::service::service_effect_handler::EffectContext; +use crate::service::effects::context::EffectContext; #[derive(Debug, Default, Parser)] pub struct ContainerClientConfig { diff --git a/core/startos/src/service/effects/action.rs b/core/startos/src/service/effects/action.rs new file mode 100644 index 000000000..4719c6d3d --- /dev/null +++ b/core/startos/src/service/effects/action.rs @@ -0,0 +1,101 @@ +use std::collections::BTreeMap; + +use models::{ActionId, PackageId}; + +use crate::action::ActionResult; +use crate::db::model::package::ActionMetadata; +use crate::rpc_continuations::Guid; +use crate::service::effects::prelude::*; + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct ExportActionParams { + #[ts(optional)] + package_id: Option, + id: ActionId, + metadata: ActionMetadata, +} +pub async fn export_action(context: EffectContext, data: ExportActionParams) -> Result<(), Error> { + let context = context.deref()?; + let package_id = context.seed.id.clone(); + context + .seed + .ctx + .db + .mutate(|db| { + let model = db + .as_public_mut() + .as_package_data_mut() + .as_idx_mut(&package_id) + .or_not_found(&package_id)? + .as_actions_mut(); + let mut value = model.de()?; + value + .insert(data.id, data.metadata) + .map(|_| ()) + .unwrap_or_default(); + model.ser(&value) + }) + .await?; + Ok(()) +} + +pub async fn clear_actions(context: EffectContext) -> Result<(), Error> { + let context = context.deref()?; + let package_id = context.seed.id.clone(); + context + .seed + .ctx + .db + .mutate(|db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(&package_id) + .or_not_found(&package_id)? + .as_actions_mut() + .ser(&BTreeMap::new()) + }) + .await?; + Ok(()) +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct ExecuteAction { + #[serde(default)] + #[ts(skip)] + procedure_id: Guid, + #[ts(optional)] + package_id: Option, + action_id: ActionId, + #[ts(type = "any")] + input: Value, +} +pub async fn execute_action( + context: EffectContext, + ExecuteAction { + procedure_id, + package_id, + action_id, + input, + }: ExecuteAction, +) -> Result { + let context = context.deref()?; + + if let Some(package_id) = package_id { + context + .seed + .ctx + .services + .get(&package_id) + .await + .as_ref() + .or_not_found(&package_id)? + .action(procedure_id, action_id, input) + .await + } else { + context.action(procedure_id, action_id, input).await + } +} diff --git a/core/startos/src/service/effects/callbacks.rs b/core/startos/src/service/effects/callbacks.rs new file mode 100644 index 000000000..1a9250aa8 --- /dev/null +++ b/core/startos/src/service/effects/callbacks.rs @@ -0,0 +1,311 @@ +use std::cmp::min; +use std::collections::{BTreeMap, BTreeSet}; +use std::sync::{Arc, Mutex, Weak}; +use std::time::{Duration, SystemTime}; + +use futures::future::join_all; +use helpers::NonDetachingJoinHandle; +use imbl::{vector, Vector}; +use imbl_value::InternedString; +use models::{HostId, PackageId, ServiceInterfaceId}; +use patch_db::json_ptr::JsonPointer; +use tracing::warn; + +use crate::net::ssl::FullchainCertData; +use crate::prelude::*; +use crate::service::effects::context::EffectContext; +use crate::service::effects::net::ssl::Algorithm; +use crate::service::rpc::CallbackHandle; +use crate::service::{Service, ServiceActorSeed}; +use crate::util::collections::EqMap; + +#[derive(Default)] +pub struct ServiceCallbacks(Mutex); + +#[derive(Default)] +struct ServiceCallbackMap { + get_service_interface: BTreeMap<(PackageId, ServiceInterfaceId), Vec>, + list_service_interfaces: BTreeMap>, + get_system_smtp: Vec, + get_host_info: BTreeMap<(PackageId, HostId), Vec>, + get_ssl_certificate: EqMap< + (BTreeSet, FullchainCertData, Algorithm), + (NonDetachingJoinHandle<()>, Vec), + >, + get_store: BTreeMap>>, +} + +impl ServiceCallbacks { + fn mutate(&self, f: impl FnOnce(&mut ServiceCallbackMap) -> T) -> T { + let mut this = self.0.lock().unwrap(); + f(&mut *this) + } + + pub fn gc(&self) { + self.mutate(|this| { + this.get_service_interface.retain(|_, v| { + v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0); + !v.is_empty() + }); + this.list_service_interfaces.retain(|_, v| { + v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0); + !v.is_empty() + }); + this.get_system_smtp + .retain(|h| h.handle.is_active() && h.seed.strong_count() > 0); + this.get_host_info.retain(|_, v| { + v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0); + !v.is_empty() + }); + this.get_ssl_certificate.retain(|_, (_, v)| { + v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0); + !v.is_empty() + }); + this.get_store.retain(|_, v| { + v.retain(|_, v| { + v.retain(|h| h.handle.is_active() && h.seed.strong_count() > 0); + !v.is_empty() + }); + !v.is_empty() + }); + }) + } + + pub(super) fn add_get_service_interface( + &self, + package_id: PackageId, + service_interface_id: ServiceInterfaceId, + handler: CallbackHandler, + ) { + self.mutate(|this| { + this.get_service_interface + .entry((package_id, service_interface_id)) + .or_default() + .push(handler); + }) + } + + #[must_use] + pub fn get_service_interface( + &self, + id: &(PackageId, ServiceInterfaceId), + ) -> Option { + self.mutate(|this| { + Some(CallbackHandlers( + this.get_service_interface.remove(id).unwrap_or_default(), + )) + .filter(|cb| !cb.0.is_empty()) + }) + } + + pub(super) fn add_list_service_interfaces( + &self, + package_id: PackageId, + handler: CallbackHandler, + ) { + self.mutate(|this| { + this.list_service_interfaces + .entry(package_id) + .or_default() + .push(handler); + }) + } + + #[must_use] + pub fn list_service_interfaces(&self, id: &PackageId) -> Option { + self.mutate(|this| { + Some(CallbackHandlers( + this.list_service_interfaces.remove(id).unwrap_or_default(), + )) + .filter(|cb| !cb.0.is_empty()) + }) + } + + pub(super) fn add_get_system_smtp(&self, handler: CallbackHandler) { + self.mutate(|this| { + this.get_system_smtp.push(handler); + }) + } + + #[must_use] + pub fn get_system_smtp(&self) -> Option { + self.mutate(|this| { + Some(CallbackHandlers(std::mem::take(&mut this.get_system_smtp))) + .filter(|cb| !cb.0.is_empty()) + }) + } + + pub(super) fn add_get_host_info( + &self, + package_id: PackageId, + host_id: HostId, + handler: CallbackHandler, + ) { + self.mutate(|this| { + this.get_host_info + .entry((package_id, host_id)) + .or_default() + .push(handler); + }) + } + + #[must_use] + pub fn get_host_info(&self, id: &(PackageId, HostId)) -> Option { + self.mutate(|this| { + Some(CallbackHandlers( + this.get_host_info.remove(id).unwrap_or_default(), + )) + .filter(|cb| !cb.0.is_empty()) + }) + } + + pub(super) fn add_get_ssl_certificate( + &self, + ctx: EffectContext, + hostnames: BTreeSet, + cert: FullchainCertData, + algorithm: Algorithm, + handler: CallbackHandler, + ) { + self.mutate(|this| { + this.get_ssl_certificate + .entry((hostnames.clone(), cert.clone(), algorithm)) + .or_insert_with(|| { + ( + tokio::spawn(async move { + if let Err(e) = async { + loop { + match cert + .expiration() + .ok() + .and_then(|e| e.duration_since(SystemTime::now()).ok()) + { + Some(d) => { + tokio::time::sleep(min(Duration::from_secs(86400), d)) + .await + } + _ => break, + } + } + let Ok(ctx) = ctx.deref() else { + return Ok(()); + }; + + if let Some((_, callbacks)) = + ctx.seed.ctx.callbacks.mutate(|this| { + this.get_ssl_certificate + .remove(&(hostnames, cert, algorithm)) + }) + { + CallbackHandlers(callbacks).call(vector![]).await?; + } + Ok::<_, Error>(()) + } + .await + { + tracing::error!( + "Error in callback handler for getSslCertificate: {e}" + ); + tracing::debug!("{e:?}"); + } + }) + .into(), + Vec::new(), + ) + }) + .1 + .push(handler); + }) + } + + pub(super) fn add_get_store( + &self, + package_id: PackageId, + path: JsonPointer, + handler: CallbackHandler, + ) { + self.mutate(|this| { + this.get_store + .entry(package_id) + .or_default() + .entry(path) + .or_default() + .push(handler) + }) + } + + #[must_use] + pub fn get_store( + &self, + package_id: &PackageId, + path: &JsonPointer, + ) -> Option { + self.mutate(|this| { + if let Some(watched) = this.get_store.get_mut(package_id) { + let mut res = Vec::new(); + watched.retain(|ptr, cbs| { + if ptr.starts_with(path) || path.starts_with(ptr) { + res.append(cbs); + false + } else { + true + } + }); + Some(CallbackHandlers(res)) + } else { + None + } + .filter(|cb| !cb.0.is_empty()) + }) + } +} + +pub struct CallbackHandler { + handle: CallbackHandle, + seed: Weak, +} +impl CallbackHandler { + pub fn new(service: &Service, handle: CallbackHandle) -> Self { + Self { + handle, + seed: Arc::downgrade(&service.seed), + } + } + pub async fn call(mut self, args: Vector) -> Result<(), Error> { + if let Some(seed) = self.seed.upgrade() { + seed.persistent_container + .callback(self.handle.take(), args) + .await?; + } + Ok(()) + } +} +impl Drop for CallbackHandler { + fn drop(&mut self) { + if self.handle.is_active() { + warn!("Callback handler dropped while still active!"); + } + } +} + +pub struct CallbackHandlers(Vec); +impl CallbackHandlers { + pub async fn call(self, args: Vector) -> Result<(), Error> { + let mut err = ErrorCollection::new(); + for res in join_all(self.0.into_iter().map(|cb| cb.call(args.clone()))).await { + err.handle(res); + } + err.into_result() + } +} + +pub(super) fn clear_callbacks(context: EffectContext) -> Result<(), Error> { + let context = context.deref()?; + context + .seed + .persistent_container + .state + .send_if_modified(|s| !std::mem::take(&mut s.callbacks).is_empty()); + context.seed.ctx.callbacks.gc(); + Ok(()) +} diff --git a/core/startos/src/service/effects/config.rs b/core/startos/src/service/effects/config.rs new file mode 100644 index 000000000..647d3e272 --- /dev/null +++ b/core/startos/src/service/effects/config.rs @@ -0,0 +1,53 @@ +use models::PackageId; + +use crate::service::effects::prelude::*; + +#[derive(Debug, Clone, Serialize, Deserialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct GetConfiguredParams { + #[ts(optional)] + package_id: Option, +} +pub async fn get_configured(context: EffectContext) -> Result { + let context = context.deref()?; + let peeked = context.seed.ctx.db.peek().await; + let package_id = &context.seed.id; + peeked + .as_public() + .as_package_data() + .as_idx(package_id) + .or_not_found(package_id)? + .as_status() + .as_configured() + .de() +} + +#[derive(Debug, Clone, Serialize, Deserialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct SetConfigured { + configured: bool, +} +pub async fn set_configured( + context: EffectContext, + SetConfigured { configured }: SetConfigured, +) -> Result<(), Error> { + let context = context.deref()?; + let package_id = &context.seed.id; + context + .seed + .ctx + .db + .mutate(|db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(package_id) + .or_not_found(package_id)? + .as_status_mut() + .as_configured_mut() + .ser(&configured) + }) + .await?; + Ok(()) +} diff --git a/core/startos/src/service/effects/context.rs b/core/startos/src/service/effects/context.rs new file mode 100644 index 000000000..b97499332 --- /dev/null +++ b/core/startos/src/service/effects/context.rs @@ -0,0 +1,27 @@ +use std::sync::{Arc, Weak}; + +use rpc_toolkit::Context; + +use crate::prelude::*; +use crate::service::Service; + +#[derive(Clone)] +pub(in crate::service) struct EffectContext(Weak); +impl EffectContext { + pub fn new(service: Weak) -> Self { + Self(service) + } +} +impl Context for EffectContext {} +impl EffectContext { + pub(super) fn deref(&self) -> Result, Error> { + if let Some(seed) = Weak::upgrade(&self.0) { + Ok(seed) + } else { + Err(Error::new( + eyre!("Service has already been destroyed"), + ErrorKind::InvalidRequest, + )) + } + } +} diff --git a/core/startos/src/service/effects/control.rs b/core/startos/src/service/effects/control.rs new file mode 100644 index 000000000..6b3c6f8a0 --- /dev/null +++ b/core/startos/src/service/effects/control.rs @@ -0,0 +1,66 @@ +use std::str::FromStr; + +use clap::builder::ValueParserFactory; + +use crate::service::effects::prelude::*; +use crate::util::clap::FromStrParser; + +pub async fn restart( + context: EffectContext, + ProcedureId { procedure_id }: ProcedureId, +) -> Result<(), Error> { + let context = context.deref()?; + context.restart(procedure_id).await?; + Ok(()) +} + +pub async fn shutdown( + context: EffectContext, + ProcedureId { procedure_id }: ProcedureId, +) -> Result<(), Error> { + let context = context.deref()?; + context.stop(procedure_id).await?; + Ok(()) +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub enum SetMainStatusStatus { + Running, + Stopped, +} +impl FromStr for SetMainStatusStatus { + type Err = color_eyre::eyre::Report; + fn from_str(s: &str) -> Result { + match s { + "running" => Ok(Self::Running), + "stopped" => Ok(Self::Stopped), + _ => Err(eyre!("unknown status {s}")), + } + } +} +impl ValueParserFactory for SetMainStatusStatus { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + FromStrParser::new() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct SetMainStatus { + status: SetMainStatusStatus, +} +pub async fn set_main_status( + context: EffectContext, + SetMainStatus { status }: SetMainStatus, +) -> Result<(), Error> { + let context = context.deref()?; + match status { + SetMainStatusStatus::Running => context.seed.started(), + SetMainStatusStatus::Stopped => context.seed.stopped(), + } + Ok(()) +} diff --git a/core/startos/src/service/effects/dependency.rs b/core/startos/src/service/effects/dependency.rs new file mode 100644 index 000000000..dfc8795f0 --- /dev/null +++ b/core/startos/src/service/effects/dependency.rs @@ -0,0 +1,371 @@ +use std::collections::{BTreeMap, BTreeSet}; +use std::path::PathBuf; +use std::str::FromStr; + +use clap::builder::ValueParserFactory; +use exver::VersionRange; +use itertools::Itertools; +use models::{HealthCheckId, PackageId, VolumeId}; +use patch_db::json_ptr::JsonPointer; + +use crate::db::model::package::{ + CurrentDependencies, CurrentDependencyInfo, CurrentDependencyKind, ManifestPreference, +}; +use crate::rpc_continuations::Guid; +use crate::service::effects::prelude::*; +use crate::status::health_check::HealthCheckResult; +use crate::util::clap::FromStrParser; + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct MountTarget { + package_id: PackageId, + volume_id: VolumeId, + subpath: Option, + readonly: bool, +} +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct MountParams { + location: String, + target: MountTarget, +} +pub async fn mount( + context: EffectContext, + MountParams { + location, + target: + MountTarget { + package_id, + volume_id, + subpath, + readonly, + }, + }: MountParams, +) -> Result<(), Error> { + // TODO + todo!() +} + +pub async fn get_installed_packages(context: EffectContext) -> Result, Error> { + context + .deref()? + .seed + .ctx + .db + .peek() + .await + .into_public() + .into_package_data() + .keys() +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct ExposeForDependentsParams { + #[ts(type = "string[]")] + paths: Vec, +} +pub async fn expose_for_dependents( + context: EffectContext, + ExposeForDependentsParams { paths }: ExposeForDependentsParams, +) -> Result<(), Error> { + Ok(()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub enum DependencyKind { + Exists, + Running, +} +#[derive(Debug, Clone, Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase", tag = "kind")] +#[serde(rename_all_fields = "camelCase")] +#[ts(export)] +pub enum DependencyRequirement { + Running { + id: PackageId, + health_checks: BTreeSet, + #[ts(type = "string")] + version_range: VersionRange, + }, + Exists { + id: PackageId, + #[ts(type = "string")] + version_range: VersionRange, + }, +} +// filebrowser:exists,bitcoind:running:foo+bar+baz +impl FromStr for DependencyRequirement { + type Err = Error; + fn from_str(s: &str) -> Result { + match s.split_once(':') { + Some((id, "e")) | Some((id, "exists")) => Ok(Self::Exists { + id: id.parse()?, + version_range: "*".parse()?, // TODO + }), + Some((id, rest)) => { + let health_checks = match rest.split_once(':') { + Some(("r", rest)) | Some(("running", rest)) => rest + .split('+') + .map(|id| id.parse().map_err(Error::from)) + .collect(), + Some((kind, _)) => Err(Error::new( + eyre!("unknown dependency kind {kind}"), + ErrorKind::InvalidRequest, + )), + None => match rest { + "r" | "running" => Ok(BTreeSet::new()), + kind => Err(Error::new( + eyre!("unknown dependency kind {kind}"), + ErrorKind::InvalidRequest, + )), + }, + }?; + Ok(Self::Running { + id: id.parse()?, + health_checks, + version_range: "*".parse()?, // TODO + }) + } + None => Ok(Self::Running { + id: s.parse()?, + health_checks: BTreeSet::new(), + version_range: "*".parse()?, // TODO + }), + } + } +} +impl ValueParserFactory for DependencyRequirement { + type Parser = FromStrParser; + fn value_parser() -> Self::Parser { + FromStrParser::new() + } +} +#[derive(Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[command(rename_all = "camelCase")] +#[ts(export)] +pub struct SetDependenciesParams { + #[serde(default)] + procedure_id: Guid, + dependencies: Vec, +} +pub async fn set_dependencies( + context: EffectContext, + SetDependenciesParams { + procedure_id, + dependencies, + }: SetDependenciesParams, +) -> Result<(), Error> { + let context = context.deref()?; + let id = &context.seed.id; + + let mut deps = BTreeMap::new(); + for dependency in dependencies { + let (dep_id, kind, version_range) = match dependency { + DependencyRequirement::Exists { id, version_range } => { + (id, CurrentDependencyKind::Exists, version_range) + } + DependencyRequirement::Running { + id, + health_checks, + version_range, + } => ( + id, + CurrentDependencyKind::Running { health_checks }, + version_range, + ), + }; + let config_satisfied = + if let Some(dep_service) = &*context.seed.ctx.services.get(&dep_id).await { + context + .dependency_config( + procedure_id.clone(), + dep_id.clone(), + dep_service.get_config(procedure_id.clone()).await?.config, + ) + .await? + .is_none() + } else { + true + }; + let info = CurrentDependencyInfo { + title: context + .seed + .persistent_container + .s9pk + .dependency_metadata(&dep_id) + .await? + .map(|m| m.title), + icon: context + .seed + .persistent_container + .s9pk + .dependency_icon_data_url(&dep_id) + .await?, + kind, + version_range, + config_satisfied, + }; + deps.insert(dep_id, info); + } + context + .seed + .ctx + .db + .mutate(|db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(id) + .or_not_found(id)? + .as_current_dependencies_mut() + .ser(&CurrentDependencies(deps)) + }) + .await +} + +pub async fn get_dependencies(context: EffectContext) -> Result, Error> { + let context = context.deref()?; + let id = &context.seed.id; + let db = context.seed.ctx.db.peek().await; + let data = db + .as_public() + .as_package_data() + .as_idx(id) + .or_not_found(id)? + .as_current_dependencies() + .de()?; + + data.0 + .into_iter() + .map(|(id, current_dependency_info)| { + let CurrentDependencyInfo { + version_range, + kind, + .. + } = current_dependency_info; + Ok::<_, Error>(match kind { + CurrentDependencyKind::Exists => { + DependencyRequirement::Exists { id, version_range } + } + CurrentDependencyKind::Running { health_checks } => { + DependencyRequirement::Running { + id, + health_checks, + version_range, + } + } + }) + }) + .try_collect() +} + +#[derive(Debug, Clone, Serialize, Deserialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct CheckDependenciesParam { + #[ts(optional)] + package_ids: Option>, +} +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct CheckDependenciesResult { + package_id: PackageId, + is_installed: bool, + is_running: bool, + config_satisfied: bool, + health_checks: BTreeMap, + #[ts(type = "string | null")] + version: Option, +} +pub async fn check_dependencies( + context: EffectContext, + CheckDependenciesParam { package_ids }: CheckDependenciesParam, +) -> Result, Error> { + let context = context.deref()?; + let db = context.seed.ctx.db.peek().await; + let current_dependencies = db + .as_public() + .as_package_data() + .as_idx(&context.seed.id) + .or_not_found(&context.seed.id)? + .as_current_dependencies() + .de()?; + let package_ids: Vec<_> = package_ids + .unwrap_or_else(|| current_dependencies.0.keys().cloned().collect()) + .into_iter() + .filter_map(|x| { + let info = current_dependencies.0.get(&x)?; + Some((x, info)) + }) + .collect(); + let mut results = Vec::with_capacity(package_ids.len()); + + for (package_id, dependency_info) in package_ids { + let Some(package) = db.as_public().as_package_data().as_idx(&package_id) else { + results.push(CheckDependenciesResult { + package_id, + is_installed: false, + is_running: false, + config_satisfied: false, + health_checks: Default::default(), + version: None, + }); + continue; + }; + let manifest = package.as_state_info().as_manifest(ManifestPreference::New); + let installed_version = manifest.as_version().de()?.into_version(); + let satisfies = manifest.as_satisfies().de()?; + let version = Some(installed_version.clone()); + if ![installed_version] + .into_iter() + .chain(satisfies.into_iter().map(|v| v.into_version())) + .any(|v| v.satisfies(&dependency_info.version_range)) + { + results.push(CheckDependenciesResult { + package_id, + is_installed: false, + is_running: false, + config_satisfied: false, + health_checks: Default::default(), + version, + }); + continue; + } + let is_installed = true; + let status = package.as_status().as_main().de()?; + let is_running = if is_installed { + status.running() + } else { + false + }; + let health_checks = + if let CurrentDependencyKind::Running { health_checks } = &dependency_info.kind { + status + .health() + .cloned() + .unwrap_or_default() + .into_iter() + .filter(|(id, _)| health_checks.contains(id)) + .collect() + } else { + Default::default() + }; + results.push(CheckDependenciesResult { + package_id, + is_installed, + is_running, + config_satisfied: dependency_info.config_satisfied, + health_checks, + version, + }); + } + Ok(results) +} diff --git a/core/startos/src/service/effects/health.rs b/core/startos/src/service/effects/health.rs new file mode 100644 index 000000000..c8ef8fc4e --- /dev/null +++ b/core/startos/src/service/effects/health.rs @@ -0,0 +1,46 @@ +use models::HealthCheckId; + +use crate::service::effects::prelude::*; +use crate::status::health_check::HealthCheckResult; +use crate::status::MainStatus; + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct SetHealth { + id: HealthCheckId, + #[serde(flatten)] + result: HealthCheckResult, +} +pub async fn set_health( + context: EffectContext, + SetHealth { id, result }: SetHealth, +) -> Result<(), Error> { + let context = context.deref()?; + + let package_id = &context.seed.id; + context + .seed + .ctx + .db + .mutate(move |db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(package_id) + .or_not_found(package_id)? + .as_status_mut() + .as_main_mut() + .mutate(|main| { + match main { + &mut MainStatus::Running { ref mut health, .. } + | &mut MainStatus::BackingUp { ref mut health, .. } => { + health.insert(id, result); + } + _ => (), + } + Ok(()) + }) + }) + .await?; + Ok(()) +} diff --git a/core/startos/src/service/effects/image.rs b/core/startos/src/service/effects/image.rs new file mode 100644 index 000000000..69c516ce5 --- /dev/null +++ b/core/startos/src/service/effects/image.rs @@ -0,0 +1,163 @@ +use std::ffi::OsString; +use std::os::unix::process::CommandExt; +use std::path::{Path, PathBuf}; + +use models::ImageId; +use rpc_toolkit::Context; +use tokio::process::Command; + +use crate::disk::mount::filesystem::overlayfs::OverlayGuard; +use crate::rpc_continuations::Guid; +use crate::service::effects::prelude::*; +use crate::util::Invoke; + +#[derive(Debug, Clone, Serialize, Deserialize, Parser)] +pub struct ChrootParams { + #[arg(short = 'e', long = "env")] + env: Option, + #[arg(short = 'w', long = "workdir")] + workdir: Option, + #[arg(short = 'u', long = "user")] + user: Option, + path: PathBuf, + command: OsString, + args: Vec, +} +pub fn chroot( + _: C, + ChrootParams { + env, + workdir, + user, + path, + command, + args, + }: ChrootParams, +) -> Result<(), Error> { + let mut cmd = std::process::Command::new(command); + if let Some(env) = env { + for (k, v) in std::fs::read_to_string(env)? + .lines() + .map(|l| l.trim()) + .filter_map(|l| l.split_once("=")) + { + cmd.env(k, v); + } + } + nix::unistd::setsid().ok(); // https://stackoverflow.com/questions/25701333/os-setsid-operation-not-permitted + std::os::unix::fs::chroot(path)?; + if let Some(uid) = user.as_deref().and_then(|u| u.parse::().ok()) { + cmd.uid(uid); + } else if let Some(user) = user { + let (uid, gid) = std::fs::read_to_string("/etc/passwd")? + .lines() + .find_map(|l| { + let mut split = l.trim().split(":"); + if user != split.next()? { + return None; + } + split.next(); // throw away x + Some((split.next()?.parse().ok()?, split.next()?.parse().ok()?)) + // uid gid + }) + .or_not_found(lazy_format!("{user} in /etc/passwd"))?; + cmd.uid(uid); + cmd.gid(gid); + }; + if let Some(workdir) = workdir { + cmd.current_dir(workdir); + } + cmd.args(args); + Err(cmd.exec().into()) +} + +#[derive(Debug, Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct DestroyOverlayedImageParams { + guid: Guid, +} +#[instrument(skip_all)] +pub async fn destroy_overlayed_image( + context: EffectContext, + DestroyOverlayedImageParams { guid }: DestroyOverlayedImageParams, +) -> Result<(), Error> { + let context = context.deref()?; + if context + .seed + .persistent_container + .overlays + .lock() + .await + .remove(&guid) + .is_none() + { + tracing::warn!("Could not find a guard to remove on the destroy overlayed image; assumming that it already is removed and will be skipping"); + } + Ok(()) +} + +#[derive(Debug, Deserialize, Serialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct CreateOverlayedImageParams { + image_id: ImageId, +} +#[instrument(skip_all)] +pub async fn create_overlayed_image( + context: EffectContext, + CreateOverlayedImageParams { image_id }: CreateOverlayedImageParams, +) -> Result<(PathBuf, Guid), Error> { + let context = context.deref()?; + if let Some(image) = context + .seed + .persistent_container + .images + .get(&image_id) + .cloned() + { + let guid = Guid::new(); + let rootfs_dir = context + .seed + .persistent_container + .lxc_container + .get() + .ok_or_else(|| { + Error::new( + eyre!("PersistentContainer has been destroyed"), + ErrorKind::Incoherent, + ) + })? + .rootfs_dir(); + let mountpoint = rootfs_dir + .join("media/startos/overlays") + .join(guid.as_ref()); + tokio::fs::create_dir_all(&mountpoint).await?; + let container_mountpoint = Path::new("/").join( + mountpoint + .strip_prefix(rootfs_dir) + .with_kind(ErrorKind::Incoherent)?, + ); + tracing::info!("Mounting overlay {guid} for {image_id}"); + let guard = OverlayGuard::mount(image, &mountpoint).await?; + Command::new("chown") + .arg("100000:100000") + .arg(&mountpoint) + .invoke(ErrorKind::Filesystem) + .await?; + tracing::info!("Mounted overlay {guid} for {image_id}"); + context + .seed + .persistent_container + .overlays + .lock() + .await + .insert(guid.clone(), guard); + Ok((container_mountpoint, guid)) + } else { + Err(Error::new( + eyre!("image {image_id} not found in s9pk"), + ErrorKind::NotFound, + )) + } +} diff --git a/core/startos/src/service/effects/mod.rs b/core/startos/src/service/effects/mod.rs new file mode 100644 index 000000000..91a12a4d1 --- /dev/null +++ b/core/startos/src/service/effects/mod.rs @@ -0,0 +1,174 @@ +use rpc_toolkit::{from_fn, from_fn_async, Context, HandlerExt, ParentHandler}; + +use crate::echo; +use crate::prelude::*; +use crate::service::cli::ContainerCliContext; +use crate::service::effects::context::EffectContext; + +mod action; +pub mod callbacks; +mod config; +pub mod context; +mod control; +mod dependency; +mod health; +mod image; +mod net; +mod prelude; +mod store; +mod system; + +pub fn handler() -> ParentHandler { + ParentHandler::new() + .subcommand("gitInfo", from_fn(|_: C| crate::version::git_info())) + .subcommand( + "echo", + from_fn(echo::).with_call_remote::(), + ) + // action + .subcommand( + "executeAction", + from_fn_async(action::execute_action).no_cli(), + ) + .subcommand( + "exportAction", + from_fn_async(action::export_action).no_cli(), + ) + .subcommand( + "clearActions", + from_fn_async(action::clear_actions).no_cli(), + ) + // callbacks + .subcommand( + "clearCallbacks", + from_fn(callbacks::clear_callbacks).no_cli(), + ) + // config + .subcommand( + "getConfigured", + from_fn_async(config::get_configured).no_cli(), + ) + .subcommand( + "setConfigured", + from_fn_async(config::set_configured) + .no_display() + .with_call_remote::(), + ) + // control + .subcommand( + "restart", + from_fn_async(control::restart) + .no_display() + .with_call_remote::(), + ) + .subcommand( + "shutdown", + from_fn_async(control::shutdown) + .no_display() + .with_call_remote::(), + ) + .subcommand( + "setMainStatus", + from_fn_async(control::set_main_status) + .no_display() + .with_call_remote::(), + ) + // dependency + .subcommand( + "setDependencies", + from_fn_async(dependency::set_dependencies) + .no_display() + .with_call_remote::(), + ) + .subcommand( + "getDependencies", + from_fn_async(dependency::get_dependencies) + .no_display() + .with_call_remote::(), + ) + .subcommand( + "checkDependencies", + from_fn_async(dependency::check_dependencies) + .no_display() + .with_call_remote::(), + ) + .subcommand("mount", from_fn_async(dependency::mount).no_cli()) + .subcommand( + "getInstalledPackages", + from_fn_async(dependency::get_installed_packages).no_cli(), + ) + .subcommand( + "exposeForDependents", + from_fn_async(dependency::expose_for_dependents).no_cli(), + ) + // health + .subcommand("setHealth", from_fn_async(health::set_health).no_cli()) + // image + .subcommand( + "chroot", + from_fn(image::chroot::).no_display(), + ) + .subcommand( + "createOverlayedImage", + from_fn_async(image::create_overlayed_image) + .with_custom_display_fn(|_, (path, _)| Ok(println!("{}", path.display()))) + .with_call_remote::(), + ) + .subcommand( + "destroyOverlayedImage", + from_fn_async(image::destroy_overlayed_image).no_cli(), + ) + // net + .subcommand("bind", from_fn_async(net::bind::bind).no_cli()) + .subcommand( + "getServicePortForward", + from_fn_async(net::bind::get_service_port_forward).no_cli(), + ) + .subcommand( + "clearBindings", + from_fn_async(net::bind::clear_bindings).no_cli(), + ) + .subcommand( + "getHostInfo", + from_fn_async(net::host::get_host_info).no_cli(), + ) + .subcommand( + "getPrimaryUrl", + from_fn_async(net::host::get_primary_url).no_cli(), + ) + .subcommand( + "getContainerIp", + from_fn_async(net::info::get_container_ip).no_cli(), + ) + .subcommand( + "exportServiceInterface", + from_fn_async(net::interface::export_service_interface).no_cli(), + ) + .subcommand( + "getServiceInterface", + from_fn_async(net::interface::get_service_interface).no_cli(), + ) + .subcommand( + "listServiceInterfaces", + from_fn_async(net::interface::list_service_interfaces).no_cli(), + ) + .subcommand( + "clearServiceInterfaces", + from_fn_async(net::interface::clear_service_interfaces).no_cli(), + ) + .subcommand( + "getSslCertificate", + from_fn_async(net::ssl::get_ssl_certificate).no_cli(), + ) + .subcommand("getSslKey", from_fn_async(net::ssl::get_ssl_key).no_cli()) + // store + .subcommand("getStore", from_fn_async(store::get_store).no_cli()) + .subcommand("setStore", from_fn_async(store::set_store).no_cli()) + // system + .subcommand( + "getSystemSmtp", + from_fn_async(system::get_system_smtp).no_cli(), + ) + + // TODO Callbacks +} diff --git a/core/startos/src/service/effects/net/bind.rs b/core/startos/src/service/effects/net/bind.rs new file mode 100644 index 000000000..ba273323a --- /dev/null +++ b/core/startos/src/service/effects/net/bind.rs @@ -0,0 +1,56 @@ +use models::{HostId, PackageId}; + +use crate::net::host::binding::{BindOptions, LanInfo}; +use crate::net::host::HostKind; +use crate::service::effects::prelude::*; + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct BindParams { + kind: HostKind, + id: HostId, + internal_port: u16, + #[serde(flatten)] + options: BindOptions, +} +pub async fn bind( + context: EffectContext, + BindParams { + kind, + id, + internal_port, + options, + }: BindParams, +) -> Result<(), Error> { + let context = context.deref()?; + let mut svc = context.seed.persistent_container.net_service.lock().await; + svc.bind(kind, id, internal_port, options).await +} + +pub async fn clear_bindings(context: EffectContext) -> Result<(), Error> { + let context = context.deref()?; + let mut svc = context.seed.persistent_container.net_service.lock().await; + svc.clear_bindings().await?; + Ok(()) +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct GetServicePortForwardParams { + #[ts(optional)] + package_id: Option, + host_id: HostId, + internal_port: u32, +} +pub async fn get_service_port_forward( + context: EffectContext, + data: GetServicePortForwardParams, +) -> Result { + let internal_port = data.internal_port as u16; + + let context = context.deref()?; + let net_service = context.seed.persistent_container.net_service.lock().await; + net_service.get_lan_port(data.host_id, internal_port) +} diff --git a/core/startos/src/service/effects/net/host.rs b/core/startos/src/service/effects/net/host.rs new file mode 100644 index 000000000..d320e7fe9 --- /dev/null +++ b/core/startos/src/service/effects/net/host.rs @@ -0,0 +1,73 @@ +use models::{HostId, PackageId}; + +use crate::net::host::address::HostAddress; +use crate::net::host::Host; +use crate::service::effects::callbacks::CallbackHandler; +use crate::service::effects::prelude::*; +use crate::service::rpc::CallbackId; + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct GetPrimaryUrlParams { + #[ts(optional)] + package_id: Option, + host_id: HostId, + #[ts(optional)] + callback: Option, +} +pub async fn get_primary_url( + context: EffectContext, + GetPrimaryUrlParams { + package_id, + host_id, + callback, + }: GetPrimaryUrlParams, +) -> Result, Error> { + let context = context.deref()?; + let package_id = package_id.unwrap_or_else(|| context.seed.id.clone()); + + Ok(None) // TODO +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct GetHostInfoParams { + host_id: HostId, + #[ts(optional)] + package_id: Option, + #[ts(optional)] + callback: Option, +} +pub async fn get_host_info( + context: EffectContext, + GetHostInfoParams { + host_id, + package_id, + callback, + }: GetHostInfoParams, +) -> Result, Error> { + let context = context.deref()?; + let db = context.seed.ctx.db.peek().await; + let package_id = package_id.unwrap_or_else(|| context.seed.id.clone()); + + let res = db + .as_public() + .as_package_data() + .as_idx(&package_id) + .and_then(|m| m.as_hosts().as_idx(&host_id)) + .map(|m| m.de()) + .transpose()?; + + if let Some(callback) = callback { + let callback = callback.register(&context.seed.persistent_container); + context.seed.ctx.callbacks.add_get_host_info( + package_id, + host_id, + CallbackHandler::new(&context, callback), + ); + } + + Ok(res) +} diff --git a/core/startos/src/service/effects/net/info.rs b/core/startos/src/service/effects/net/info.rs new file mode 100644 index 000000000..c33a1a81e --- /dev/null +++ b/core/startos/src/service/effects/net/info.rs @@ -0,0 +1,9 @@ +use std::net::Ipv4Addr; + +use crate::service::effects::prelude::*; + +pub async fn get_container_ip(context: EffectContext) -> Result { + let context = context.deref()?; + let net_service = context.seed.persistent_container.net_service.lock().await; + Ok(net_service.get_ip()) +} diff --git a/core/startos/src/service/effects/net/interface.rs b/core/startos/src/service/effects/net/interface.rs new file mode 100644 index 000000000..e636e9b57 --- /dev/null +++ b/core/startos/src/service/effects/net/interface.rs @@ -0,0 +1,188 @@ +use std::collections::BTreeMap; + +use imbl::vector; +use models::{PackageId, ServiceInterfaceId}; + +use crate::net::service_interface::{AddressInfo, ServiceInterface, ServiceInterfaceType}; +use crate::service::effects::callbacks::CallbackHandler; +use crate::service::effects::prelude::*; +use crate::service::rpc::CallbackId; + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct ExportServiceInterfaceParams { + id: ServiceInterfaceId, + name: String, + description: String, + has_primary: bool, + disabled: bool, + masked: bool, + address_info: AddressInfo, + r#type: ServiceInterfaceType, +} +pub async fn export_service_interface( + context: EffectContext, + ExportServiceInterfaceParams { + id, + name, + description, + has_primary, + disabled, + masked, + address_info, + r#type, + }: ExportServiceInterfaceParams, +) -> Result<(), Error> { + let context = context.deref()?; + let package_id = context.seed.id.clone(); + + let service_interface = ServiceInterface { + id: id.clone(), + name, + description, + has_primary, + disabled, + masked, + address_info, + interface_type: r#type, + }; + + context + .seed + .ctx + .db + .mutate(|db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(&package_id) + .or_not_found(&package_id)? + .as_service_interfaces_mut() + .insert(&id, &service_interface)?; + Ok(()) + }) + .await?; + if let Some(callbacks) = context + .seed + .ctx + .callbacks + .get_service_interface(&(package_id.clone(), id)) + { + callbacks.call(vector![]).await?; + } + if let Some(callbacks) = context + .seed + .ctx + .callbacks + .list_service_interfaces(&package_id) + { + callbacks.call(vector![]).await?; + } + + Ok(()) +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct GetServiceInterfaceParams { + #[ts(optional)] + package_id: Option, + service_interface_id: ServiceInterfaceId, + #[ts(optional)] + callback: Option, +} +pub async fn get_service_interface( + context: EffectContext, + GetServiceInterfaceParams { + package_id, + service_interface_id, + callback, + }: GetServiceInterfaceParams, +) -> Result, Error> { + let context = context.deref()?; + let package_id = package_id.unwrap_or_else(|| context.seed.id.clone()); + let db = context.seed.ctx.db.peek().await; + + let interface = db + .as_public() + .as_package_data() + .as_idx(&package_id) + .and_then(|m| m.as_service_interfaces().as_idx(&service_interface_id)) + .map(|m| m.de()) + .transpose()?; + + if let Some(callback) = callback { + let callback = callback.register(&context.seed.persistent_container); + context.seed.ctx.callbacks.add_get_service_interface( + package_id, + service_interface_id, + CallbackHandler::new(&context, callback), + ); + } + + Ok(interface) +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct ListServiceInterfacesParams { + #[ts(optional)] + package_id: Option, + #[ts(optional)] + callback: Option, +} +pub async fn list_service_interfaces( + context: EffectContext, + ListServiceInterfacesParams { + package_id, + callback, + }: ListServiceInterfacesParams, +) -> Result, Error> { + let context = context.deref()?; + let package_id = package_id.unwrap_or_else(|| context.seed.id.clone()); + + let res = context + .seed + .ctx + .db + .peek() + .await + .into_public() + .into_package_data() + .into_idx(&package_id) + .map(|m| m.into_service_interfaces().de()) + .transpose()? + .unwrap_or_default(); + + if let Some(callback) = callback { + let callback = callback.register(&context.seed.persistent_container); + context + .seed + .ctx + .callbacks + .add_list_service_interfaces(package_id, CallbackHandler::new(&context, callback)); + } + + Ok(res) +} + +pub async fn clear_service_interfaces(context: EffectContext) -> Result<(), Error> { + let context = context.deref()?; + let package_id = context.seed.id.clone(); + + context + .seed + .ctx + .db + .mutate(|db| { + db.as_public_mut() + .as_package_data_mut() + .as_idx_mut(&package_id) + .or_not_found(&package_id)? + .as_service_interfaces_mut() + .ser(&Default::default()) + }) + .await +} diff --git a/core/startos/src/service/effects/net/mod.rs b/core/startos/src/service/effects/net/mod.rs new file mode 100644 index 000000000..cf13451a6 --- /dev/null +++ b/core/startos/src/service/effects/net/mod.rs @@ -0,0 +1,5 @@ +pub mod bind; +pub mod host; +pub mod info; +pub mod interface; +pub mod ssl; diff --git a/core/startos/src/service/effects/net/ssl.rs b/core/startos/src/service/effects/net/ssl.rs new file mode 100644 index 000000000..d37a2d241 --- /dev/null +++ b/core/startos/src/service/effects/net/ssl.rs @@ -0,0 +1,169 @@ +use std::collections::BTreeSet; + +use imbl_value::InternedString; +use itertools::Itertools; +use openssl::pkey::{PKey, Private}; + +use crate::service::effects::callbacks::CallbackHandler; +use crate::service::effects::prelude::*; +use crate::service::rpc::CallbackId; +use crate::util::serde::Pem; + +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, TS, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub enum Algorithm { + Ecdsa, + Ed25519, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct GetSslCertificateParams { + #[ts(type = "string[]")] + hostnames: BTreeSet, + #[ts(optional)] + algorithm: Option, //"ecdsa" | "ed25519" + #[ts(optional)] + callback: Option, +} +pub async fn get_ssl_certificate( + ctx: EffectContext, + GetSslCertificateParams { + hostnames, + algorithm, + callback, + }: GetSslCertificateParams, +) -> Result, Error> { + let context = ctx.deref()?; + let algorithm = algorithm.unwrap_or(Algorithm::Ecdsa); + + let cert = context + .seed + .ctx + .db + .mutate(|db| { + let errfn = |h: &str| Error::new(eyre!("unknown hostname: {h}"), ErrorKind::NotFound); + let entries = db.as_public().as_package_data().as_entries()?; + let packages = entries.iter().map(|(k, _)| k).collect::>(); + let allowed_hostnames = entries + .iter() + .map(|(_, m)| m.as_hosts().as_entries()) + .flatten_ok() + .map_ok(|(_, m)| m.as_addresses().de()) + .map(|a| a.and_then(|a| a)) + .flatten_ok() + .map_ok(|a| InternedString::from_display(&a)) + .try_collect::<_, BTreeSet<_>, _>()?; + for hostname in &hostnames { + if let Some(internal) = hostname + .strip_suffix(".embassy") + .or_else(|| hostname.strip_suffix(".startos")) + { + if !packages.contains(internal) { + return Err(errfn(&*hostname)); + } + } else { + if !allowed_hostnames.contains(hostname) { + return Err(errfn(&*hostname)); + } + } + } + db.as_private_mut() + .as_key_store_mut() + .as_local_certs_mut() + .cert_for(&hostnames) + }) + .await?; + let fullchain = match algorithm { + Algorithm::Ecdsa => cert.fullchain_nistp256(), + Algorithm::Ed25519 => cert.fullchain_ed25519(), + }; + + let res = fullchain + .into_iter() + .map(|c| c.to_pem()) + .map_ok(String::from_utf8) + .map(|a| Ok::<_, Error>(a??)) + .try_collect()?; + + if let Some(callback) = callback { + let callback = callback.register(&context.seed.persistent_container); + context.seed.ctx.callbacks.add_get_ssl_certificate( + ctx, + hostnames, + cert, + algorithm, + CallbackHandler::new(&context, callback), + ); + } + + Ok(res) +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct GetSslKeyParams { + #[ts(type = "string[]")] + hostnames: BTreeSet, + #[ts(optional)] + algorithm: Option, //"ecdsa" | "ed25519" +} +pub async fn get_ssl_key( + context: EffectContext, + GetSslKeyParams { + hostnames, + algorithm, + }: GetSslKeyParams, +) -> Result>, Error> { + let context = context.deref()?; + let package_id = &context.seed.id; + let algorithm = algorithm.unwrap_or(Algorithm::Ecdsa); + + let cert = context + .seed + .ctx + .db + .mutate(|db| { + let errfn = |h: &str| Error::new(eyre!("unknown hostname: {h}"), ErrorKind::NotFound); + let allowed_hostnames = db + .as_public() + .as_package_data() + .as_idx(package_id) + .into_iter() + .map(|m| m.as_hosts().as_entries()) + .flatten_ok() + .map_ok(|(_, m)| m.as_addresses().de()) + .map(|a| a.and_then(|a| a)) + .flatten_ok() + .map_ok(|a| InternedString::from_display(&a)) + .try_collect::<_, BTreeSet<_>, _>()?; + for hostname in &hostnames { + if let Some(internal) = hostname + .strip_suffix(".embassy") + .or_else(|| hostname.strip_suffix(".startos")) + { + if internal != &**package_id { + return Err(errfn(&*hostname)); + } + } else { + if !allowed_hostnames.contains(hostname) { + return Err(errfn(&*hostname)); + } + } + } + db.as_private_mut() + .as_key_store_mut() + .as_local_certs_mut() + .cert_for(&hostnames) + }) + .await?; + let key = match algorithm { + Algorithm::Ecdsa => cert.leaf.keys.nistp256, + Algorithm::Ed25519 => cert.leaf.keys.ed25519, + }; + + Ok(Pem(key)) +} diff --git a/core/startos/src/service/effects/prelude.rs b/core/startos/src/service/effects/prelude.rs new file mode 100644 index 000000000..2dc848c0c --- /dev/null +++ b/core/startos/src/service/effects/prelude.rs @@ -0,0 +1,16 @@ +pub use clap::Parser; +pub use serde::{Deserialize, Serialize}; +pub use ts_rs::TS; + +pub use crate::prelude::*; +use crate::rpc_continuations::Guid; +pub(super) use crate::service::effects::context::EffectContext; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct ProcedureId { + #[serde(default)] + #[arg(default_value_t, long)] + pub procedure_id: Guid, +} diff --git a/core/startos/src/service/effects/store.rs b/core/startos/src/service/effects/store.rs new file mode 100644 index 000000000..ab4484ab6 --- /dev/null +++ b/core/startos/src/service/effects/store.rs @@ -0,0 +1,93 @@ +use imbl::vector; +use imbl_value::json; +use models::PackageId; +use patch_db::json_ptr::JsonPointer; + +use crate::service::effects::callbacks::CallbackHandler; +use crate::service::effects::prelude::*; +use crate::service::rpc::CallbackId; + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct GetStoreParams { + #[ts(optional)] + package_id: Option, + #[ts(type = "string")] + path: JsonPointer, + #[ts(optional)] + callback: Option, +} +pub async fn get_store( + context: EffectContext, + GetStoreParams { + package_id, + path, + callback, + }: GetStoreParams, +) -> Result { + let context = context.deref()?; + let peeked = context.seed.ctx.db.peek().await; + let package_id = package_id.unwrap_or(context.seed.id.clone()); + let value = peeked + .as_private() + .as_package_stores() + .as_idx(&package_id) + .or_not_found(&package_id)? + .de()?; + + if let Some(callback) = callback { + let callback = callback.register(&context.seed.persistent_container); + context.seed.ctx.callbacks.add_get_store( + package_id, + path.clone(), + CallbackHandler::new(&context, callback), + ); + } + + Ok(path + .get(&value) + .ok_or_else(|| Error::new(eyre!("Did not find value at path"), ErrorKind::NotFound))? + .clone()) +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct SetStoreParams { + #[ts(type = "any")] + value: Value, + #[ts(type = "string")] + path: JsonPointer, +} +pub async fn set_store( + context: EffectContext, + SetStoreParams { value, path }: SetStoreParams, +) -> Result<(), Error> { + let context = context.deref()?; + let package_id = &context.seed.id; + context + .seed + .ctx + .db + .mutate(|db| { + let model = db + .as_private_mut() + .as_package_stores_mut() + .upsert(package_id, || Ok(json!({})))?; + let mut model_value = model.de()?; + if model_value.is_null() { + model_value = json!({}); + } + path.set(&mut model_value, value, true) + .with_kind(ErrorKind::ParseDbField)?; + model.ser(&model_value) + }) + .await?; + + if let Some(callbacks) = context.seed.ctx.callbacks.get_store(package_id, &path) { + callbacks.call(vector![]).await?; + } + + Ok(()) +} diff --git a/core/startos/src/service/effects/system.rs b/core/startos/src/service/effects/system.rs new file mode 100644 index 000000000..abf0a33c6 --- /dev/null +++ b/core/startos/src/service/effects/system.rs @@ -0,0 +1,39 @@ +use crate::service::effects::callbacks::CallbackHandler; +use crate::service::effects::prelude::*; +use crate::service::rpc::CallbackId; +use crate::system::SmtpValue; + +#[derive(Debug, Clone, Serialize, Deserialize, TS, Parser)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct GetSystemSmtpParams { + #[arg(skip)] + callback: Option, +} +pub async fn get_system_smtp( + context: EffectContext, + GetSystemSmtpParams { callback }: GetSystemSmtpParams, +) -> Result, Error> { + let context = context.deref()?; + let res = context + .seed + .ctx + .db + .peek() + .await + .into_public() + .into_server_info() + .into_smtp() + .de()?; + + if let Some(callback) = callback { + let callback = callback.register(&context.seed.persistent_container); + context + .seed + .ctx + .callbacks + .add_get_system_smtp(CallbackHandler::new(&context, callback)); + } + + Ok(res) +} diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index 9103b70a1..efaa28d4e 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -27,10 +27,7 @@ use crate::progress::{NamedProgress, Progress}; use crate::rpc_continuations::Guid; use crate::s9pk::S9pk; use crate::service::service_map::InstallProgressHandles; -use crate::service::transition::TransitionKind; use crate::status::health_check::HealthCheckResult; -use crate::status::MainStatus; -use crate::util::actor::background::BackgroundJobQueue; use crate::util::actor::concurrent::ConcurrentActor; use crate::util::actor::Actor; use crate::util::io::create_file; @@ -43,11 +40,11 @@ pub mod cli; mod config; mod control; mod dependencies; +pub mod effects; pub mod persistent_container; mod properties; mod rpc; mod service_actor; -pub mod service_effect_handler; pub mod service_map; mod start_stop; mod transition; diff --git a/core/startos/src/service/persistent_container.rs b/core/startos/src/service/persistent_container.rs index e8c504a92..62d3b0975 100644 --- a/core/startos/src/service/persistent_container.rs +++ b/core/startos/src/service/persistent_container.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::path::Path; use std::sync::{Arc, Weak}; use std::time::Duration; @@ -6,6 +6,7 @@ use std::time::Duration; use futures::future::ready; use futures::{Future, FutureExt}; use helpers::NonDetachingJoinHandle; +use imbl::Vector; use models::{ImageId, ProcedureName, VolumeId}; use rpc_toolkit::{Empty, Server, ShutdownHandle}; use serde::de::DeserializeOwned; @@ -13,8 +14,6 @@ use tokio::process::Command; use tokio::sync::{oneshot, watch, Mutex, OnceCell}; use tracing::instrument; -use super::service_effect_handler::{service_effect_handler, EffectContext}; -use super::transition::{TransitionKind, TransitionState}; use crate::context::RpcContext; use crate::disk::mount::filesystem::bind::Bind; use crate::disk::mount::filesystem::idmapped::IdMapped; @@ -28,7 +27,11 @@ use crate::prelude::*; use crate::rpc_continuations::Guid; use crate::s9pk::merkle_archive::source::FileSource; use crate::s9pk::S9pk; +use crate::service::effects::context::EffectContext; +use crate::service::effects::handler; +use crate::service::rpc::{CallbackHandle, CallbackId, CallbackParams}; use crate::service::start_stop::StartStop; +use crate::service::transition::{TransitionKind, TransitionState}; use crate::service::{rpc, RunningStatus, Service}; use crate::util::io::create_file; use crate::util::rpc_client::UnixRpcClient; @@ -42,6 +45,8 @@ const RPC_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); pub struct ServiceState { // This contains the start time and health check information for when the service is running. Note: Will be overwritting to the db, pub(super) running_status: Option, + // This tracks references to callbacks registered by the running service: + pub(super) callbacks: BTreeSet>, /// Setting this value causes the service actor to try to bring the service to the specified state. This is done in the background job created in ServiceActor::init pub(super) desired_state: StartStop, /// Override the current desired state for the service during a transition (this is protected by a guard that sets this value to null on drop) @@ -61,6 +66,7 @@ impl ServiceState { pub fn new(desired_state: StartStop) -> Self { Self { running_status: Default::default(), + callbacks: Default::default(), temp_desired_state: Default::default(), transition_state: Default::default(), desired_state, @@ -308,10 +314,7 @@ impl PersistentContainer { #[instrument(skip_all)] pub async fn init(&self, seed: Weak) -> Result<(), Error> { let socket_server_context = EffectContext::new(seed); - let server = Server::new( - move || ready(Ok(socket_server_context.clone())), - service_effect_handler(), - ); + let server = Server::new(move || ready(Ok(socket_server_context.clone())), handler()); let path = self .lxc_container .get() @@ -430,21 +433,13 @@ impl PersistentContainer { #[instrument(skip_all)] pub async fn start(&self) -> Result<(), Error> { - self.execute( - Guid::new(), - ProcedureName::StartMain, - Value::Null, - Some(Duration::from_secs(5)), // TODO - ) - .await?; + self.rpc_client.request(rpc::Start, Empty {}).await?; Ok(()) } #[instrument(skip_all)] pub async fn stop(&self) -> Result<(), Error> { - let timeout: Option = self - .execute(Guid::new(), ProcedureName::StopMain, Value::Null, None) - .await?; + self.rpc_client.request(rpc::Stop, Empty {}).await?; Ok(()) } @@ -480,6 +475,19 @@ impl PersistentContainer { .and_then(from_value) } + #[instrument(skip_all)] + pub async fn callback(&self, handle: CallbackHandle, args: Vector) -> Result<(), Error> { + let mut params = None; + self.state.send_if_modified(|s| { + params = handle.params(&mut s.callbacks, args); + params.is_some() + }); + if let Some(params) = params { + self._callback(params).await?; + } + Ok(()) + } + #[instrument(skip_all)] async fn _execute( &self, @@ -523,6 +531,12 @@ impl PersistentContainer { fut.await? }) } + + #[instrument(skip_all)] + async fn _callback(&self, params: CallbackParams) -> Result<(), Error> { + self.rpc_client.notify(rpc::Callback, params).await?; + Ok(()) + } } impl Drop for PersistentContainer { diff --git a/core/startos/src/service/rpc.rs b/core/startos/src/service/rpc.rs index eff44b2cf..25d8fb067 100644 --- a/core/startos/src/service/rpc.rs +++ b/core/startos/src/service/rpc.rs @@ -1,5 +1,8 @@ +use std::collections::BTreeSet; +use std::sync::{Arc, Weak}; use std::time::Duration; +use imbl::Vector; use imbl_value::Value; use models::ProcedureName; use rpc_toolkit::yajrc::RpcMethod; @@ -8,6 +11,8 @@ use ts_rs::TS; use crate::prelude::*; use crate::rpc_continuations::Guid; +use crate::service::persistent_container::PersistentContainer; +use crate::util::Never; #[derive(Clone)] pub struct Init; @@ -27,6 +32,42 @@ impl serde::Serialize for Init { } } +#[derive(Clone)] +pub struct Start; +impl RpcMethod for Start { + type Params = Empty; + type Response = (); + fn as_str<'a>(&'a self) -> &'a str { + "start" + } +} +impl serde::Serialize for Start { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +#[derive(Clone)] +pub struct Stop; +impl RpcMethod for Stop { + type Params = Empty; + type Response = (); + fn as_str<'a>(&'a self) -> &'a str { + "stop" + } +} +impl serde::Serialize for Stop { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + #[derive(Clone)] pub struct Exit; impl RpcMethod for Exit { @@ -104,3 +145,74 @@ impl serde::Serialize for Sandbox { serializer.serialize_str(self.as_str()) } } + +#[derive( + Clone, Copy, Debug, serde::Deserialize, serde::Serialize, TS, PartialEq, Eq, PartialOrd, Ord, +)] +#[ts(type = "number")] +pub struct CallbackId(u64); +impl CallbackId { + pub fn register(self, container: &PersistentContainer) -> CallbackHandle { + let this = Arc::new(self); + let res = Arc::downgrade(&this); + container + .state + .send_if_modified(|s| s.callbacks.insert(this)); + CallbackHandle(res) + } +} + +pub struct CallbackHandle(Weak); +impl CallbackHandle { + pub fn is_active(&self) -> bool { + self.0.strong_count() > 0 + } + pub fn params( + self, + registered: &mut BTreeSet>, + args: Vector, + ) -> Option { + if let Some(id) = self.0.upgrade() { + if let Some(strong) = registered.get(&id) { + if Arc::ptr_eq(strong, &id) { + registered.remove(&id); + return Some(CallbackParams::new(&*id, args)); + } + } + } + None + } + pub fn take(&mut self) -> Self { + Self(std::mem::take(&mut self.0)) + } +} + +#[derive(Clone, serde::Deserialize, serde::Serialize, TS)] +pub struct CallbackParams { + id: u64, + #[ts(type = "any[]")] + args: Vector, +} +impl CallbackParams { + fn new(id: &CallbackId, args: Vector) -> Self { + Self { id: id.0, args } + } +} + +#[derive(Clone)] +pub struct Callback; +impl RpcMethod for Callback { + type Params = CallbackParams; + type Response = Never; + fn as_str<'a>(&'a self) -> &'a str { + "callback" + } +} +impl serde::Serialize for Callback { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} diff --git a/core/startos/src/service/service_effect_handler.rs b/core/startos/src/service/service_effect_handler.rs deleted file mode 100644 index 79ca4dc42..000000000 --- a/core/startos/src/service/service_effect_handler.rs +++ /dev/null @@ -1,1431 +0,0 @@ -use std::collections::{BTreeMap, BTreeSet}; -use std::ffi::OsString; -use std::net::Ipv4Addr; -use std::os::unix::process::CommandExt; -use std::path::{Path, PathBuf}; -use std::str::FromStr; -use std::sync::{Arc, Weak}; - -use clap::builder::ValueParserFactory; -use clap::Parser; -use exver::VersionRange; -use imbl_value::json; -use itertools::Itertools; -use models::{ - ActionId, DataUrl, HealthCheckId, HostId, ImageId, PackageId, ServiceInterfaceId, VolumeId, -}; -use patch_db::json_ptr::JsonPointer; -use rpc_toolkit::{from_fn, from_fn_async, Context, Empty, HandlerExt, ParentHandler}; -use serde::{Deserialize, Serialize}; -use tokio::process::Command; -use ts_rs::TS; -use url::Url; - -use crate::db::model::package::{ - ActionMetadata, CurrentDependencies, CurrentDependencyInfo, CurrentDependencyKind, - ManifestPreference, -}; -use crate::disk::mount::filesystem::overlayfs::OverlayGuard; -use crate::echo; -use crate::net::host::address::HostAddress; -use crate::net::host::binding::{BindOptions, LanInfo}; -use crate::net::host::{Host, HostKind}; -use crate::net::service_interface::{AddressInfo, ServiceInterface, ServiceInterfaceType}; -use crate::prelude::*; -use crate::rpc_continuations::Guid; -use crate::s9pk::merkle_archive::source::http::HttpSource; -use crate::s9pk::rpc::SKIP_ENV; -use crate::s9pk::S9pk; -use crate::service::cli::ContainerCliContext; -use crate::service::Service; -use crate::status::health_check::HealthCheckResult; -use crate::status::MainStatus; -use crate::util::clap::FromStrParser; -use crate::util::Invoke; - -#[derive(Clone)] -pub(super) struct EffectContext(Weak); -impl EffectContext { - pub fn new(service: Weak) -> Self { - Self(service) - } -} -impl Context for EffectContext {} -impl EffectContext { - fn deref(&self) -> Result, Error> { - if let Some(seed) = Weak::upgrade(&self.0) { - Ok(seed) - } else { - Err(Error::new( - eyre!("Service has already been destroyed"), - ErrorKind::InvalidRequest, - )) - } - } -} - -pub fn service_effect_handler() -> ParentHandler { - ParentHandler::new() - .subcommand("gitInfo", from_fn(|_: C| crate::version::git_info())) - .subcommand( - "echo", - from_fn(echo::).with_call_remote::(), - ) - .subcommand( - "chroot", - from_fn(chroot::).no_display(), - ) - .subcommand("exists", from_fn_async(exists).no_cli()) - .subcommand("executeAction", from_fn_async(execute_action).no_cli()) - .subcommand("getConfigured", from_fn_async(get_configured).no_cli()) - .subcommand( - "stopped", - from_fn_async(stopped) - .no_display() - .with_call_remote::(), - ) - .subcommand( - "running", - from_fn_async(running) - .no_display() - .with_call_remote::(), - ) - .subcommand( - "restart", - from_fn_async(restart) - .no_display() - .with_call_remote::(), - ) - .subcommand( - "shutdown", - from_fn_async(shutdown) - .no_display() - .with_call_remote::(), - ) - .subcommand( - "setConfigured", - from_fn_async(set_configured) - .no_display() - .with_call_remote::(), - ) - .subcommand( - "setMainStatus", - from_fn_async(set_main_status).with_call_remote::(), - ) - .subcommand("setHealth", from_fn_async(set_health).no_cli()) - .subcommand("getStore", from_fn_async(get_store).no_cli()) - .subcommand("setStore", from_fn_async(set_store).no_cli()) - .subcommand( - "exposeForDependents", - from_fn_async(expose_for_dependents).no_cli(), - ) - .subcommand( - "createOverlayedImage", - from_fn_async(create_overlayed_image) - .with_custom_display_fn(|_, (path, _)| Ok(println!("{}", path.display()))) - .with_call_remote::(), - ) - .subcommand( - "destroyOverlayedImage", - from_fn_async(destroy_overlayed_image).no_cli(), - ) - .subcommand( - "getSslCertificate", - from_fn_async(get_ssl_certificate).no_cli(), - ) - .subcommand("getSslKey", from_fn_async(get_ssl_key).no_cli()) - .subcommand( - "getServiceInterface", - from_fn_async(get_service_interface).no_cli(), - ) - .subcommand("clearBindings", from_fn_async(clear_bindings).no_cli()) - .subcommand("bind", from_fn_async(bind).no_cli()) - .subcommand("getHostInfo", from_fn_async(get_host_info).no_cli()) - .subcommand( - "setDependencies", - from_fn_async(set_dependencies) - .no_display() - .with_call_remote::(), - ) - .subcommand( - "getDependencies", - from_fn_async(get_dependencies) - .no_display() - .with_call_remote::(), - ) - .subcommand( - "checkDependencies", - from_fn_async(check_dependencies) - .no_display() - .with_call_remote::(), - ) - .subcommand("setSystemSmtp", from_fn_async(set_system_smtp).no_cli()) - .subcommand("getSystemSmtp", from_fn_async(get_system_smtp).no_cli()) - .subcommand("getContainerIp", from_fn_async(get_container_ip).no_cli()) - .subcommand( - "getServicePortForward", - from_fn_async(get_service_port_forward).no_cli(), - ) - .subcommand( - "clearServiceInterfaces", - from_fn_async(clear_network_interfaces).no_cli(), - ) - .subcommand( - "exportServiceInterface", - from_fn_async(export_service_interface).no_cli(), - ) - .subcommand("getPrimaryUrl", from_fn_async(get_primary_url).no_cli()) - .subcommand( - "listServiceInterfaces", - from_fn_async(list_service_interfaces).no_cli(), - ) - .subcommand("removeAddress", from_fn_async(remove_address).no_cli()) - .subcommand("exportAction", from_fn_async(export_action).no_cli()) - .subcommand("removeAction", from_fn_async(remove_action).no_cli()) - .subcommand("mount", from_fn_async(mount).no_cli()) - - // TODO Callbacks -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct GetSystemSmtpParams { - callback: Callback, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct SetSystemSmtpParams { - smtp: String, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct GetServicePortForwardParams { - #[ts(type = "string | null")] - package_id: Option, - internal_port: u32, - host_id: HostId, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct ExportServiceInterfaceParams { - id: ServiceInterfaceId, - name: String, - description: String, - has_primary: bool, - disabled: bool, - masked: bool, - address_info: AddressInfo, - r#type: ServiceInterfaceType, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct GetPrimaryUrlParams { - #[ts(type = "string | null")] - package_id: Option, - service_interface_id: ServiceInterfaceId, - callback: Callback, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct ListServiceInterfacesParams { - #[ts(type = "string | null")] - package_id: Option, - callback: Callback, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct RemoveAddressParams { - id: ServiceInterfaceId, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct ExportActionParams { - #[ts(type = "string")] - id: ActionId, - metadata: ActionMetadata, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct RemoveActionParams { - #[ts(type = "string")] - id: ActionId, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct MountTarget { - #[ts(type = "string")] - package_id: PackageId, - #[ts(type = "string")] - volume_id: VolumeId, - subpath: Option, - readonly: bool, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct MountParams { - location: String, - target: MountTarget, -} -async fn set_system_smtp(context: EffectContext, data: SetSystemSmtpParams) -> Result<(), Error> { - let context = context.deref()?; - context - .seed - .ctx - .db - .mutate(|db| { - let model = db.as_public_mut().as_server_info_mut().as_smtp_mut(); - model.ser(&mut Some(data.smtp)) - }) - .await -} -async fn get_system_smtp( - context: EffectContext, - data: GetSystemSmtpParams, -) -> Result { - let context = context.deref()?; - let res = context - .seed - .ctx - .db - .peek() - .await - .into_public() - .into_server_info() - .into_smtp() - .de()?; - - match res { - Some(smtp) => Ok(smtp), - None => Err(Error::new( - eyre!("SMTP not found"), - crate::ErrorKind::NotFound, - )), - } -} -async fn get_container_ip(context: EffectContext, _: Empty) -> Result { - let context = context.deref()?; - let net_service = context.seed.persistent_container.net_service.lock().await; - Ok(net_service.get_ip()) -} -async fn get_service_port_forward( - context: EffectContext, - data: GetServicePortForwardParams, -) -> Result { - let internal_port = data.internal_port as u16; - - let context = context.deref()?; - let net_service = context.seed.persistent_container.net_service.lock().await; - net_service.get_ext_port(data.host_id, internal_port) -} -async fn clear_network_interfaces(context: EffectContext, _: Empty) -> Result<(), Error> { - let context = context.deref()?; - let package_id = context.seed.id.clone(); - - context - .seed - .ctx - .db - .mutate(|db| { - let model = db - .as_public_mut() - .as_package_data_mut() - .as_idx_mut(&package_id) - .or_not_found(&package_id)? - .as_service_interfaces_mut(); - let mut new_map = BTreeMap::new(); - model.ser(&mut new_map) - }) - .await -} -async fn export_service_interface( - context: EffectContext, - ExportServiceInterfaceParams { - id, - name, - description, - has_primary, - disabled, - masked, - address_info, - r#type, - }: ExportServiceInterfaceParams, -) -> Result<(), Error> { - let context = context.deref()?; - let package_id = context.seed.id.clone(); - - let service_interface = ServiceInterface { - id: id.clone(), - name, - description, - has_primary, - disabled, - masked, - address_info, - interface_type: r#type, - }; - let svc_interface_with_host_info = service_interface; - - context - .seed - .ctx - .db - .mutate(|db| { - db.as_public_mut() - .as_package_data_mut() - .as_idx_mut(&package_id) - .or_not_found(&package_id)? - .as_service_interfaces_mut() - .insert(&id, &svc_interface_with_host_info)?; - Ok(()) - }) - .await?; - Ok(()) -} -async fn get_primary_url( - context: EffectContext, - GetPrimaryUrlParams { - package_id, - service_interface_id, - callback, - }: GetPrimaryUrlParams, -) -> Result, Error> { - let context = context.deref()?; - let package_id = package_id.unwrap_or_else(|| context.seed.id.clone()); - - Ok(None) // TODO -} -async fn list_service_interfaces( - context: EffectContext, - ListServiceInterfacesParams { - package_id, - callback, - }: ListServiceInterfacesParams, -) -> Result, Error> { - let context = context.deref()?; - let package_id = package_id.unwrap_or_else(|| context.seed.id.clone()); - - context - .seed - .ctx - .db - .peek() - .await - .into_public() - .into_package_data() - .into_idx(&package_id) - .or_not_found(&package_id)? - .into_service_interfaces() - .de() -} -async fn remove_address(context: EffectContext, data: RemoveAddressParams) -> Result<(), Error> { - let context = context.deref()?; - let package_id = context.seed.id.clone(); - - context - .seed - .ctx - .db - .mutate(|db| { - let model = db - .as_public_mut() - .as_package_data_mut() - .as_idx_mut(&package_id) - .or_not_found(&package_id)? - .as_service_interfaces_mut(); - model.remove(&data.id) - }) - .await?; - Ok(()) -} -async fn export_action(context: EffectContext, data: ExportActionParams) -> Result<(), Error> { - let context = context.deref()?; - let package_id = context.seed.id.clone(); - context - .seed - .ctx - .db - .mutate(|db| { - let model = db - .as_public_mut() - .as_package_data_mut() - .as_idx_mut(&package_id) - .or_not_found(&package_id)? - .as_actions_mut(); - let mut value = model.de()?; - value - .insert(data.id, data.metadata) - .map(|_| ()) - .unwrap_or_default(); - model.ser(&value) - }) - .await?; - Ok(()) -} -async fn remove_action(context: EffectContext, data: RemoveActionParams) -> Result<(), Error> { - let context = context.deref()?; - let package_id = context.seed.id.clone(); - context - .seed - .ctx - .db - .mutate(|db| { - let model = db - .as_public_mut() - .as_package_data_mut() - .as_idx_mut(&package_id) - .or_not_found(&package_id)? - .as_actions_mut(); - let mut value = model.de()?; - value.remove(&data.id).map(|_| ()).unwrap_or_default(); - model.ser(&value) - }) - .await?; - Ok(()) -} -async fn mount(context: EffectContext, data: MountParams) -> Result { - // TODO - todo!() -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[ts(export)] -struct Callback(#[ts(type = "() => void")] i64); - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct GetHostInfoParams { - host_id: HostId, - #[ts(type = "string | null")] - package_id: Option, - callback: Callback, -} -async fn get_host_info( - context: EffectContext, - GetHostInfoParams { - callback, - package_id, - host_id, - }: GetHostInfoParams, -) -> Result { - let context = context.deref()?; - let db = context.seed.ctx.db.peek().await; - let package_id = package_id.unwrap_or_else(|| context.seed.id.clone()); - - db.as_public() - .as_package_data() - .as_idx(&package_id) - .or_not_found(&package_id)? - .as_hosts() - .as_idx(&host_id) - .or_not_found(&host_id)? - .de() -} - -async fn clear_bindings(context: EffectContext, _: Empty) -> Result<(), Error> { - let context = context.deref()?; - let mut svc = context.seed.persistent_container.net_service.lock().await; - svc.clear_bindings().await?; - Ok(()) -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct BindParams { - kind: HostKind, - id: HostId, - internal_port: u16, - #[serde(flatten)] - options: BindOptions, -} -async fn bind(context: EffectContext, bind_params: Value) -> Result<(), Error> { - let BindParams { - kind, - id, - internal_port, - options, - } = from_value(bind_params)?; - let context = context.deref()?; - let mut svc = context.seed.persistent_container.net_service.lock().await; - svc.bind(kind, id, internal_port, options).await -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct GetServiceInterfaceParams { - #[ts(type = "string | null")] - package_id: Option, - service_interface_id: ServiceInterfaceId, - callback: Callback, -} - -async fn get_service_interface( - context: EffectContext, - GetServiceInterfaceParams { - callback, - package_id, - service_interface_id, - }: GetServiceInterfaceParams, -) -> Result { - let context = context.deref()?; - let package_id = package_id.unwrap_or_else(|| context.seed.id.clone()); - let db = context.seed.ctx.db.peek().await; - - let interface = db - .as_public() - .as_package_data() - .as_idx(&package_id) - .or_not_found(&package_id)? - .as_service_interfaces() - .as_idx(&service_interface_id) - .or_not_found(&service_interface_id)? - .de()?; - Ok(interface) -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct ChrootParams { - #[arg(short = 'e', long = "env")] - env: Option, - #[arg(short = 'w', long = "workdir")] - workdir: Option, - #[arg(short = 'u', long = "user")] - user: Option, - path: PathBuf, - #[ts(type = "string")] - command: OsString, - #[ts(type = "string[]")] - args: Vec, -} -fn chroot( - _: C, - ChrootParams { - env, - workdir, - user, - path, - command, - args, - }: ChrootParams, -) -> Result<(), Error> { - let mut cmd = std::process::Command::new(command); - if let Some(env) = env { - for (k, v) in std::fs::read_to_string(env)? - .lines() - .map(|l| l.trim()) - .filter_map(|l| l.split_once("=")) - .filter(|(k, _)| !SKIP_ENV.contains(&k)) - { - cmd.env(k, v); - } - } - nix::unistd::setsid().ok(); // https://stackoverflow.com/questions/25701333/os-setsid-operation-not-permitted - std::os::unix::fs::chroot(path)?; - if let Some(uid) = user.as_deref().and_then(|u| u.parse::().ok()) { - cmd.uid(uid); - } else if let Some(user) = user { - let (uid, gid) = std::fs::read_to_string("/etc/passwd")? - .lines() - .find_map(|l| { - let mut split = l.trim().split(":"); - if user != split.next()? { - return None; - } - split.next(); // throw away x - Some((split.next()?.parse().ok()?, split.next()?.parse().ok()?)) - // uid gid - }) - .or_not_found(lazy_format!("{user} in /etc/passwd"))?; - cmd.uid(uid); - cmd.gid(gid); - }; - if let Some(workdir) = workdir { - cmd.current_dir(workdir); - } - cmd.args(args); - Err(cmd.exec().into()) -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -enum Algorithm { - Ecdsa, - Ed25519, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct GetSslCertificateParams { - package_id: Option, - host_id: String, - algorithm: Option, //"ecdsa" | "ed25519" -} - -async fn get_ssl_certificate( - context: EffectContext, - GetSslCertificateParams { - package_id, - algorithm, - host_id, - }: GetSslCertificateParams, -) -> Result { - // TODO - let fake = include_str!("./fake.cert.pem"); - Ok(json!([fake, fake, fake])) -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct GetSslKeyParams { - package_id: Option, - host_id: String, - algorithm: Option, -} - -async fn get_ssl_key( - context: EffectContext, - GetSslKeyParams { - package_id, - host_id, - algorithm, - }: GetSslKeyParams, -) -> Result { - // TODO - let fake = include_str!("./fake.cert.key"); - Ok(json!(fake)) -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct GetStoreParams { - #[ts(type = "string | null")] - package_id: Option, - #[ts(type = "string")] - path: JsonPointer, -} - -async fn get_store( - context: EffectContext, - GetStoreParams { package_id, path }: GetStoreParams, -) -> Result { - let context = context.deref()?; - let peeked = context.seed.ctx.db.peek().await; - let package_id = package_id.unwrap_or(context.seed.id.clone()); - let value = peeked - .as_private() - .as_package_stores() - .as_idx(&package_id) - .or_not_found(&package_id)? - .de()?; - - Ok(path - .get(&value) - .ok_or_else(|| Error::new(eyre!("Did not find value at path"), ErrorKind::NotFound))? - .clone()) -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct SetStoreParams { - #[ts(type = "any")] - value: Value, - #[ts(type = "string")] - path: JsonPointer, -} - -async fn set_store( - context: EffectContext, - SetStoreParams { value, path }: SetStoreParams, -) -> Result<(), Error> { - let context = context.deref()?; - let package_id = context.seed.id.clone(); - context - .seed - .ctx - .db - .mutate(|db| { - let model = db - .as_private_mut() - .as_package_stores_mut() - .upsert(&package_id, || Ok(json!({})))?; - let mut model_value = model.de()?; - if model_value.is_null() { - model_value = json!({}); - } - path.set(&mut model_value, value, true) - .with_kind(ErrorKind::ParseDbField)?; - model.ser(&model_value) - }) - .await?; - Ok(()) -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct ExposeForDependentsParams { - #[ts(type = "string[]")] - paths: Vec, -} - -async fn expose_for_dependents( - context: EffectContext, - ExposeForDependentsParams { paths }: ExposeForDependentsParams, -) -> Result<(), Error> { - Ok(()) -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] -#[ts(export)] -#[serde(rename_all = "camelCase")] -struct ParamsPackageId { - #[ts(type = "string")] - package_id: PackageId, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "camelCase")] -#[ts(export)] -struct ParamsMaybePackageId { - #[ts(type = "string | null")] - package_id: Option, -} - -async fn exists(context: EffectContext, params: ParamsPackageId) -> Result { - let context = context.deref()?; - let peeked = context.seed.ctx.db.peek().await; - let package = peeked - .as_public() - .as_package_data() - .as_idx(¶ms.package_id) - .is_some(); - Ok(json!(package)) -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct ExecuteAction { - #[serde(default)] - procedure_id: Guid, - #[ts(type = "string | null")] - service_id: Option, - #[ts(type = "string")] - action_id: ActionId, - #[ts(type = "any")] - input: Value, -} -async fn execute_action( - context: EffectContext, - ExecuteAction { - procedure_id, - service_id, - action_id, - input, - }: ExecuteAction, -) -> Result { - let context = context.deref()?; - let package_id = service_id - .clone() - .unwrap_or_else(|| context.seed.id.clone()); - - Ok(json!(context.action(procedure_id, action_id, input).await?)) -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct FromService {} -async fn get_configured(context: EffectContext, _: Empty) -> Result { - let context = context.deref()?; - let peeked = context.seed.ctx.db.peek().await; - let package_id = &context.seed.id; - let package = peeked - .as_public() - .as_package_data() - .as_idx(package_id) - .or_not_found(package_id)? - .as_status() - .as_configured() - .de()?; - Ok(json!(package)) -} - -async fn stopped(context: EffectContext, params: ParamsMaybePackageId) -> Result { - let context = context.deref()?; - let peeked = context.seed.ctx.db.peek().await; - let package_id = params.package_id.unwrap_or_else(|| context.seed.id.clone()); - let package = peeked - .as_public() - .as_package_data() - .as_idx(&package_id) - .or_not_found(&package_id)? - .as_status() - .as_main() - .de()?; - Ok(json!(matches!(package, MainStatus::Stopped))) -} -async fn running(context: EffectContext, params: ParamsPackageId) -> Result { - let context = context.deref()?; - let peeked = context.seed.ctx.db.peek().await; - let package_id = params.package_id; - let package = peeked - .as_public() - .as_package_data() - .as_idx(&package_id) - .or_not_found(&package_id)? - .as_status() - .as_main() - .de()?; - Ok(json!(matches!(package, MainStatus::Running { .. }))) -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct ProcedureId { - #[serde(default)] - #[arg(default_value_t, long)] - procedure_id: Guid, -} - -async fn restart( - context: EffectContext, - ProcedureId { procedure_id }: ProcedureId, -) -> Result<(), Error> { - let context = context.deref()?; - context.restart(procedure_id).await?; - Ok(()) -} - -async fn shutdown( - context: EffectContext, - ProcedureId { procedure_id }: ProcedureId, -) -> Result<(), Error> { - let context = context.deref()?; - context.stop(procedure_id).await?; - Ok(()) -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "camelCase")] -#[ts(export)] -struct SetConfigured { - configured: bool, -} -async fn set_configured(context: EffectContext, params: SetConfigured) -> Result { - let context = context.deref()?; - let package_id = &context.seed.id; - context - .seed - .ctx - .db - .mutate(|db| { - db.as_public_mut() - .as_package_data_mut() - .as_idx_mut(package_id) - .or_not_found(package_id)? - .as_status_mut() - .as_configured_mut() - .ser(¶ms.configured) - }) - .await?; - Ok(json!(())) -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -enum SetMainStatusStatus { - Running, - Stopped, -} -impl FromStr for SetMainStatusStatus { - type Err = color_eyre::eyre::Report; - fn from_str(s: &str) -> Result { - match s { - "running" => Ok(Self::Running), - "stopped" => Ok(Self::Stopped), - _ => Err(eyre!("unknown status {s}")), - } - } -} -impl ValueParserFactory for SetMainStatusStatus { - type Parser = FromStrParser; - fn value_parser() -> Self::Parser { - FromStrParser::new() - } -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "camelCase")] -#[ts(export)] -struct SetMainStatus { - status: SetMainStatusStatus, -} -async fn set_main_status(context: EffectContext, params: SetMainStatus) -> Result { - let context = context.deref()?; - match params.status { - SetMainStatusStatus::Running => context.seed.started(), - SetMainStatusStatus::Stopped => context.seed.stopped(), - } - Ok(Value::Null) -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct SetHealth { - id: HealthCheckId, - #[serde(flatten)] - result: HealthCheckResult, -} - -async fn set_health( - context: EffectContext, - SetHealth { id, result }: SetHealth, -) -> Result { - let context = context.deref()?; - - let package_id = &context.seed.id; - context - .seed - .ctx - .db - .mutate(move |db| { - db.as_public_mut() - .as_package_data_mut() - .as_idx_mut(package_id) - .or_not_found(package_id)? - .as_status_mut() - .as_main_mut() - .mutate(|main| { - match main { - &mut MainStatus::Running { ref mut health, .. } - | &mut MainStatus::BackingUp { ref mut health, .. } => { - health.insert(id, result); - } - _ => (), - } - Ok(()) - }) - }) - .await?; - Ok(json!(())) -} -#[derive(serde::Deserialize, serde::Serialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "camelCase")] -#[ts(export)] -pub struct DestroyOverlayedImageParams { - guid: Guid, -} - -#[instrument(skip_all)] -pub async fn destroy_overlayed_image( - context: EffectContext, - DestroyOverlayedImageParams { guid }: DestroyOverlayedImageParams, -) -> Result<(), Error> { - let context = context.deref()?; - if context - .seed - .persistent_container - .overlays - .lock() - .await - .remove(&guid) - .is_none() - { - tracing::warn!("Could not find a guard to remove on the destroy overlayed image; assumming that it already is removed and will be skipping"); - } - Ok(()) -} -#[derive(serde::Deserialize, serde::Serialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "camelCase")] -#[ts(export)] -pub struct CreateOverlayedImageParams { - image_id: ImageId, -} - -#[instrument(skip_all)] -pub async fn create_overlayed_image( - context: EffectContext, - CreateOverlayedImageParams { image_id }: CreateOverlayedImageParams, -) -> Result<(PathBuf, Guid), Error> { - let context = context.deref()?; - if let Some(image) = context - .seed - .persistent_container - .images - .get(&image_id) - .cloned() - { - let guid = Guid::new(); - let rootfs_dir = context - .seed - .persistent_container - .lxc_container - .get() - .ok_or_else(|| { - Error::new( - eyre!("PersistentContainer has been destroyed"), - ErrorKind::Incoherent, - ) - })? - .rootfs_dir(); - let mountpoint = rootfs_dir - .join("media/startos/overlays") - .join(guid.as_ref()); - tokio::fs::create_dir_all(&mountpoint).await?; - let container_mountpoint = Path::new("/").join( - mountpoint - .strip_prefix(rootfs_dir) - .with_kind(ErrorKind::Incoherent)?, - ); - tracing::info!("Mounting overlay {guid} for {image_id}"); - let guard = OverlayGuard::mount(image, &mountpoint).await?; - Command::new("chown") - .arg("100000:100000") - .arg(&mountpoint) - .invoke(ErrorKind::Filesystem) - .await?; - tracing::info!("Mounted overlay {guid} for {image_id}"); - context - .seed - .persistent_container - .overlays - .lock() - .await - .insert(guid.clone(), guard); - Ok((container_mountpoint, guid)) - } else { - Err(Error::new( - eyre!("image {image_id} not found in s9pk"), - ErrorKind::NotFound, - )) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -enum DependencyKind { - Exists, - Running, -} - -#[derive(Debug, Clone, Deserialize, Serialize, TS)] -#[serde(rename_all = "camelCase", tag = "kind")] -#[ts(export)] -enum DependencyRequirement { - #[serde(rename_all = "camelCase")] - Running { - #[ts(type = "string")] - id: PackageId, - #[ts(type = "string[]")] - health_checks: BTreeSet, - #[ts(type = "string")] - version_range: VersionRange, - }, - #[serde(rename_all = "camelCase")] - Exists { - #[ts(type = "string")] - id: PackageId, - #[ts(type = "string")] - version_range: VersionRange, - }, -} -// filebrowser:exists,bitcoind:running:foo+bar+baz -impl FromStr for DependencyRequirement { - type Err = Error; - fn from_str(s: &str) -> Result { - match s.split_once(':') { - Some((id, "e")) | Some((id, "exists")) => Ok(Self::Exists { - id: id.parse()?, - version_range: "*".parse()?, // TODO - }), - Some((id, rest)) => { - let health_checks = match rest.split_once(':') { - Some(("r", rest)) | Some(("running", rest)) => rest - .split('+') - .map(|id| id.parse().map_err(Error::from)) - .collect(), - Some((kind, _)) => Err(Error::new( - eyre!("unknown dependency kind {kind}"), - ErrorKind::InvalidRequest, - )), - None => match rest { - "r" | "running" => Ok(BTreeSet::new()), - kind => Err(Error::new( - eyre!("unknown dependency kind {kind}"), - ErrorKind::InvalidRequest, - )), - }, - }?; - Ok(Self::Running { - id: id.parse()?, - health_checks, - version_range: "*".parse()?, // TODO - }) - } - None => Ok(Self::Running { - id: s.parse()?, - health_checks: BTreeSet::new(), - version_range: "*".parse()?, // TODO - }), - } - } -} -impl ValueParserFactory for DependencyRequirement { - type Parser = FromStrParser; - fn value_parser() -> Self::Parser { - FromStrParser::new() - } -} - -#[derive(Deserialize, Serialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "camelCase")] -#[ts(export)] -struct SetDependenciesParams { - #[serde(default)] - procedure_id: Guid, - dependencies: Vec, -} - -async fn set_dependencies( - context: EffectContext, - SetDependenciesParams { - procedure_id, - dependencies, - }: SetDependenciesParams, -) -> Result<(), Error> { - let context = context.deref()?; - let id = &context.seed.id; - - let mut deps = BTreeMap::new(); - for dependency in dependencies { - let (dep_id, kind, version_range) = match dependency { - DependencyRequirement::Exists { id, version_range } => { - (id, CurrentDependencyKind::Exists, version_range) - } - DependencyRequirement::Running { - id, - health_checks, - version_range, - } => ( - id, - CurrentDependencyKind::Running { health_checks }, - version_range, - ), - }; - let config_satisfied = - if let Some(dep_service) = &*context.seed.ctx.services.get(&dep_id).await { - context - .dependency_config( - procedure_id.clone(), - dep_id.clone(), - dep_service.get_config(procedure_id.clone()).await?.config, - ) - .await? - .is_none() - } else { - true - }; - let info = CurrentDependencyInfo { - title: context - .seed - .persistent_container - .s9pk - .dependency_metadata(&dep_id) - .await? - .map(|m| m.title), - icon: context - .seed - .persistent_container - .s9pk - .dependency_icon_data_url(&dep_id) - .await?, - kind, - version_range, - config_satisfied, - }; - deps.insert(dep_id, info); - } - context - .seed - .ctx - .db - .mutate(|db| { - db.as_public_mut() - .as_package_data_mut() - .as_idx_mut(id) - .or_not_found(id)? - .as_current_dependencies_mut() - .ser(&CurrentDependencies(deps)) - }) - .await -} - -async fn get_dependencies(context: EffectContext) -> Result, Error> { - let context = context.deref()?; - let id = &context.seed.id; - let db = context.seed.ctx.db.peek().await; - let data = db - .as_public() - .as_package_data() - .as_idx(id) - .or_not_found(id)? - .as_current_dependencies() - .de()?; - - data.0 - .into_iter() - .map(|(id, current_dependency_info)| { - let CurrentDependencyInfo { - version_range, - kind, - .. - } = current_dependency_info; - Ok::<_, Error>(match kind { - CurrentDependencyKind::Exists => { - DependencyRequirement::Exists { id, version_range } - } - CurrentDependencyKind::Running { health_checks } => { - DependencyRequirement::Running { - id, - health_checks, - version_range, - } - } - }) - }) - .try_collect() -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] -#[serde(rename_all = "camelCase")] -#[command(rename_all = "camelCase")] -#[ts(export)] -struct CheckDependenciesParam { - package_ids: Option>, -} -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export)] -struct CheckDependenciesResult { - package_id: PackageId, - is_installed: bool, - is_running: bool, - config_satisfied: bool, - health_checks: BTreeMap, - #[ts(type = "string | null")] - version: Option, -} - -async fn check_dependencies( - context: EffectContext, - CheckDependenciesParam { package_ids }: CheckDependenciesParam, -) -> Result, Error> { - let context = context.deref()?; - let db = context.seed.ctx.db.peek().await; - let current_dependencies = db - .as_public() - .as_package_data() - .as_idx(&context.seed.id) - .or_not_found(&context.seed.id)? - .as_current_dependencies() - .de()?; - let package_ids: Vec<_> = package_ids - .unwrap_or_else(|| current_dependencies.0.keys().cloned().collect()) - .into_iter() - .filter_map(|x| { - let info = current_dependencies.0.get(&x)?; - Some((x, info)) - }) - .collect(); - let mut results = Vec::with_capacity(package_ids.len()); - - for (package_id, dependency_info) in package_ids { - let Some(package) = db.as_public().as_package_data().as_idx(&package_id) else { - results.push(CheckDependenciesResult { - package_id, - is_installed: false, - is_running: false, - config_satisfied: false, - health_checks: Default::default(), - version: None, - }); - continue; - }; - let manifest = package.as_state_info().as_manifest(ManifestPreference::New); - let installed_version = manifest.as_version().de()?.into_version(); - let satisfies = manifest.as_satisfies().de()?; - let version = Some(installed_version.clone()); - if ![installed_version] - .into_iter() - .chain(satisfies.into_iter().map(|v| v.into_version())) - .any(|v| v.satisfies(&dependency_info.version_range)) - { - results.push(CheckDependenciesResult { - package_id, - is_installed: false, - is_running: false, - config_satisfied: false, - health_checks: Default::default(), - version, - }); - continue; - } - let is_installed = true; - let status = package.as_status().as_main().de()?; - let is_running = if is_installed { - status.running() - } else { - false - }; - let health_checks = - if let CurrentDependencyKind::Running { health_checks } = &dependency_info.kind { - status - .health() - .cloned() - .unwrap_or_default() - .into_iter() - .filter(|(id, _)| health_checks.contains(id)) - .collect() - } else { - Default::default() - }; - results.push(CheckDependenciesResult { - package_id, - is_installed, - is_running, - config_satisfied: dependency_info.config_satisfied, - health_checks, - version, - }); - } - Ok(results) -} diff --git a/core/startos/src/system.rs b/core/startos/src/system.rs index 7a8cb9afe..7af94588b 100644 --- a/core/startos/src/system.rs +++ b/core/startos/src/system.rs @@ -5,6 +5,7 @@ use chrono::Utc; use clap::Parser; use color_eyre::eyre::eyre; use futures::FutureExt; +use imbl::vector; use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use tokio::process::Command; @@ -824,6 +825,51 @@ async fn get_disk_info() -> Result { }) } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Parser, TS)] +#[ts(export)] +#[serde(rename_all = "camelCase")] +pub struct SmtpValue { + #[arg(long)] + pub server: String, + #[arg(long)] + pub port: u16, + #[arg(long)] + pub from: String, + #[arg(long)] + pub login: String, + #[arg(long)] + pub password: Option, +} +pub async fn set_system_smtp(ctx: RpcContext, smtp: SmtpValue) -> Result<(), Error> { + let smtp = Some(smtp); + ctx.db + .mutate(|db| { + db.as_public_mut() + .as_server_info_mut() + .as_smtp_mut() + .ser(&smtp) + }) + .await?; + if let Some(callbacks) = ctx.callbacks.get_system_smtp() { + callbacks.call(vector![to_value(&smtp)?]).await?; + } + Ok(()) +} +pub async fn clear_system_smtp(ctx: RpcContext) -> Result<(), Error> { + ctx.db + .mutate(|db| { + db.as_public_mut() + .as_server_info_mut() + .as_smtp_mut() + .ser(&None) + }) + .await?; + if let Some(callbacks) = ctx.callbacks.get_system_smtp() { + callbacks.call(vector![Value::Null]).await?; + } + Ok(()) +} + #[tokio::test] #[ignore] pub async fn test_get_temp() { diff --git a/core/startos/src/util/collections/eq_map.rs b/core/startos/src/util/collections/eq_map.rs new file mode 100644 index 000000000..5078866a5 --- /dev/null +++ b/core/startos/src/util/collections/eq_map.rs @@ -0,0 +1,1213 @@ +use std::borrow::Borrow; +use std::fmt; +use std::ops::{Index, IndexMut}; + +pub struct EqMap(Vec<(K, V)>); +impl Default for EqMap { + fn default() -> Self { + Self(Default::default()) + } +} +impl EqMap { + pub fn new() -> Self { + Self::default() + } + + pub fn clear(&mut self) { + self.0.clear() + } + + /// Returns the key-value pair corresponding to the supplied key as a borrowed tuple. + /// + /// The supplied key may be any borrowed form of the map's key type, but the equality + /// on the borrowed form *must* match the equality on the key type. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::new(); + /// map.insert(1, "a"); + /// assert_eq!(map.get_key_value(&1), Some((&1, &"a"))); + /// assert_eq!(map.get_key_value(&2), None); + /// ``` + pub fn get_key_value_ref(&self, key: &Q) -> Option<&(K, V)> + where + K: Borrow + Eq, + Q: Eq, + { + self.0.iter().find(|(k, _)| k.borrow() == key) + } + + /// Returns the key-value pair corresponding to the supplied key as a mutably borrowed tuple. + /// + /// The supplied key may be any borrowed form of the map's key type, but the equality + /// on the borrowed form *must* match the equality on the key type. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::new(); + /// map.insert(1, "a"); + /// assert_eq!(map.get_key_value(&1), Some((&1, &"a"))); + /// assert_eq!(map.get_key_value(&2), None); + /// ``` + pub fn get_key_value_mut(&mut self, key: &Q) -> Option<&mut (K, V)> + where + K: Borrow + Eq, + Q: Eq, + { + self.0.iter_mut().find(|(k, _)| k.borrow() == key) + } + + /// Returns a reference to the value corresponding to the key. + /// + /// The key may be any borrowed form of the map's key type, but the equality + /// on the borrowed form *must* match the equality on the key type. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::new(); + /// map.insert(1, "a"); + /// assert_eq!(map.get(&1), Some(&"a")); + /// assert_eq!(map.get(&2), None); + /// ``` + pub fn get(&self, key: &Q) -> Option<&V> + where + K: Borrow + Eq, + Q: Eq, + { + self.get_key_value_ref(key).map(|(_, v)| v) + } + + /// Returns the key-value pair corresponding to the supplied key. + /// + /// The supplied key may be any borrowed form of the map's key type, but the equality + /// on the borrowed form *must* match the equality on the key type. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::new(); + /// map.insert(1, "a"); + /// assert_eq!(map.get_key_value(&1), Some((&1, &"a"))); + /// assert_eq!(map.get_key_value(&2), None); + /// ``` + pub fn get_key_value(&self, key: &Q) -> Option<(&K, &V)> + where + K: Borrow + Eq, + Q: Eq, + { + self.get_key_value_ref(key).map(|(k, v)| (k, v)) + } + + /// Removes and returns an element in the map. + /// There is no guarantee about which element this might be + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::new(); + /// map.insert(1, "a"); + /// map.insert(2, "b"); + /// while let Some((_key, _val)) = map.pop() { } + /// assert!(map.is_empty()); + /// ``` + pub fn pop(&mut self) -> Option<(K, V)> + where + K: Eq, + { + self.0.pop() + } + + /// Returns `true` if the map contains a value for the specified key. + /// + /// The key may be any borrowed form of the map's key type, but the equality + /// on the borrowed form *must* match the equality on the key type. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::new(); + /// map.insert(1, "a"); + /// assert_eq!(map.contains_key(&1), true); + /// assert_eq!(map.contains_key(&2), false); + /// ``` + pub fn contains_key(&self, key: &Q) -> bool + where + K: Borrow + Eq, + Q: Eq, + { + self.get(key).is_some() + } + + /// Returns a mutable reference to the value corresponding to the key. + /// + /// The key may be any borrowed form of the map's key type, but the equality + /// on the borrowed form *must* match the equality on the key type. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::new(); + /// map.insert(1, "a"); + /// if let Some(x) = map.get_mut(&1) { + /// *x = "b"; + /// } + /// assert_eq!(map[&1], "b"); + /// ``` + // See `get` for implementation notes, this is basically a copy-paste with mut's added + pub fn get_mut(&mut self, key: &Q) -> Option<&mut V> + where + K: Borrow + Eq, + Q: Eq, + { + self.get_key_value_mut(key).map(|(_, v)| v) + } + + /// Inserts a key-value pair into the map. + /// + /// If the map did not have this key present, `None` is returned. + /// + /// If the map did have this key present, the value is updated, and the old + /// value is returned. The key is not updated, though; this matters for + /// types that can be `==` without being identical. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::new(); + /// assert_eq!(map.insert(37, "a"), None); + /// assert_eq!(map.is_empty(), false); + /// + /// map.insert(37, "b"); + /// assert_eq!(map.insert(37, "c"), Some("b")); + /// assert_eq!(map[&37], "c"); + /// ``` + pub fn insert(&mut self, key: K, value: V) -> Option + where + K: Eq, + { + match self.entry(key) { + Occupied(mut entry) => Some(entry.insert(value)), + Vacant(entry) => { + entry.insert(value); + None + } + } + } + + /// Tries to insert a key-value pair into the map, and returns + /// a mutable reference to the value in the entry. + /// + /// If the map already had this key present, nothing is updated, and + /// an error containing the occupied entry and the value is returned. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::new(); + /// assert_eq!(map.try_insert(37, "a").unwrap(), &"a"); + /// + /// let err = map.try_insert(37, "b").unwrap_err(); + /// assert_eq!(err.entry.key(), &37); + /// assert_eq!(err.entry.get(), &"a"); + /// assert_eq!(err.value, "b"); + /// ``` + pub fn try_insert(&mut self, key: K, value: V) -> Result<&mut V, OccupiedError<'_, K, V>> + where + K: Eq, + { + match self.entry(key) { + Occupied(entry) => Err(OccupiedError { entry, value }), + Vacant(entry) => Ok(entry.insert(value)), + } + } + + /// Removes a key from the map, returning the value at the key if the key + /// was previously in the map. + /// + /// The key may be any borrowed form of the map's key type, but the equality + /// on the borrowed form *must* match the equality on the key type. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::new(); + /// map.insert(1, "a"); + /// assert_eq!(map.remove(&1), Some("a")); + /// assert_eq!(map.remove(&1), None); + /// ``` + pub fn remove(&mut self, key: &Q) -> Option + where + K: Borrow + Eq, + Q: Eq, + { + self.remove_entry(key).map(|(_, v)| v) + } + + /// Removes a key from the map, returning the stored key and value if the key + /// was previously in the map. + /// + /// The key may be any borrowed form of the map's key type, but the equality + /// on the borrowed form *must* match the equality on the key type. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::new(); + /// map.insert(1, "a"); + /// assert_eq!(map.remove_entry(&1), Some((1, "a"))); + /// assert_eq!(map.remove_entry(&1), None); + /// ``` + pub fn remove_entry(&mut self, key: &Q) -> Option<(K, V)> + where + K: Borrow + Eq, + Q: Eq, + { + self.0 + .iter() + .enumerate() + .find(|(_, (k, _))| k.borrow() == key) + .map(|(idx, _)| idx) + .map(|idx| self.0.swap_remove(idx)) + } + + /// Retains only the elements specified by the predicate. + /// + /// In other words, remove all pairs `(k, v)` for which `f(&k, &mut v)` returns `false`. + /// The elements are visited in ascending key order. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map: EqMap = (0..8).map(|x| (x, x*10)).collect(); + /// // Keep only the elements with even-numbered keys. + /// map.retain(|&k, _| k % 2 == 0); + /// assert!(map.into_iter().eq(vec![(0, 0), (2, 20), (4, 40), (6, 60)])); + /// ``` + #[inline] + pub fn retain(&mut self, mut f: F) + where + K: Eq, + F: FnMut(&K, &mut V) -> bool, + { + self.0.retain_mut(|(k, v)| f(k, v)) + } + + /// Moves all elements from `other` into `self`, leaving `other` empty. + /// + /// If a key from `other` is already present in `self`, the respective + /// value from `self` will be overwritten with the respective value from `other`. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut a = EqMap::new(); + /// a.insert(1, "a"); + /// a.insert(2, "b"); + /// a.insert(3, "c"); // Note: Key (3) also present in b. + /// + /// let mut b = EqMap::new(); + /// b.insert(3, "d"); // Note: Key (3) also present in a. + /// b.insert(4, "e"); + /// b.insert(5, "f"); + /// + /// a.append(&mut b); + /// + /// assert_eq!(a.len(), 5); + /// assert_eq!(b.len(), 0); + /// + /// assert_eq!(a[&1], "a"); + /// assert_eq!(a[&2], "b"); + /// assert_eq!(a[&3], "d"); // Note: "c" has been overwritten. + /// assert_eq!(a[&4], "e"); + /// assert_eq!(a[&5], "f"); + /// ``` + pub fn append(&mut self, other: &mut Self) + where + K: Eq, + { + for k in other.keys() { + self.remove(k); + } + self.0.append(&mut other.0) + } + + /// Gets the given key's corresponding entry in the map for in-place manipulation. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut count: EqMap<&str, usize> = EqMap::new(); + /// + /// // count the number of occurrences of letters in the vec + /// for x in ["a", "b", "a", "c", "a", "b"] { + /// count.entry(x).and_modify(|curr| *curr += 1).or_insert(1); + /// } + /// + /// assert_eq!(count["a"], 3); + /// assert_eq!(count["b"], 2); + /// assert_eq!(count["c"], 1); + /// ``` + pub fn entry(&mut self, key: K) -> Entry<'_, K, V> + where + K: Eq, + { + match self.0.iter().enumerate().find(|(_, (k, _))| k == &key) { + Some((idx, _)) => Occupied(OccupiedEntry { map: self, idx }), + None => Vacant(VacantEntry { key, map: self }), + } + } + + // /// Creates an iterator that visits all elements (key-value pairs) and + // /// uses a closure to determine if an element should be removed. If the + // /// closure returns `true`, the element is removed from the map and yielded. + // /// If the closure returns `false`, or panics, the element remains in the map + // /// and will not be yielded. + // /// + // /// The iterator also lets you mutate the value of each element in the + // /// closure, regardless of whether you choose to keep or remove it. + // /// + // /// If the returned `ExtractIf` is not exhausted, e.g. because it is dropped without iterating + // /// or the iteration short-circuits, then the remaining elements will be retained. + // /// Use [`retain`] with a negated predicate if you do not need the returned iterator. + // /// + // /// [`retain`]: EqMap::retain + // /// + // /// # Examples + // /// + // /// Splitting a map into even and odd keys, reusing the original map: + // /// + // /// ``` + // /// use startos::util::collections::EqMap; + // /// + // /// let mut map: EqMap = (0..8).map(|x| (x, x)).collect(); + // /// let evens: EqMap<_, _> = map.extract_if(|k, _v| k % 2 == 0).collect(); + // /// let odds = map; + // /// assert_eq!(evens.keys().copied().collect::>(), [0, 2, 4, 6]); + // /// assert_eq!(odds.keys().copied().collect::>(), [1, 3, 5, 7]); + // /// ``` + // pub fn extract_if(&mut self, pred: F) -> ExtractIf<'_, K, V, F> + // where + // K: Eq, + // F: FnMut(&K, &mut V) -> bool, + // { + // let (inner, alloc) = self.extract_if_inner(); + // ExtractIf { pred, inner, alloc } + // } + + /// Creates a consuming iterator visiting all the keys. + /// The map cannot be used after calling this. + /// The iterator element type is `K`. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut a = EqMap::new(); + /// a.insert(2, "b"); + /// a.insert(1, "a"); + /// + /// let keys: Vec = a.into_keys().collect(); + /// assert_eq!(keys, [2, 1]); + /// ``` + #[inline] + pub fn into_keys(self) -> IntoKeys { + IntoKeys(self.0.into_iter()) + } + + /// Creates a consuming iterator visiting all the values. + /// The map cannot be used after calling this. + /// The iterator element type is `V`. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut a = EqMap::new(); + /// a.insert(1, "hello"); + /// a.insert(2, "goodbye"); + /// + /// let values: Vec<&str> = a.into_values().collect(); + /// assert_eq!(values, ["hello", "goodbye"]); + /// ``` + #[inline] + pub fn into_values(self) -> IntoValues { + IntoValues(self.0.into_iter()) + } + + pub fn iter_ref(&self) -> std::slice::Iter<'_, (K, V)> { + self.0.iter() + } + + /// Gets an iterator over the entries of the map, in no particular order. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::new(); + /// map.insert(3, "c"); + /// map.insert(2, "b"); + /// map.insert(1, "a"); + /// + /// for (key, value) in map.iter() { + /// println!("{key}: {value}"); + /// } + /// + /// let (first_key, first_value) = map.iter().next().unwrap(); + /// assert_eq!((*first_key, *first_value), (3, "c")); + /// ``` + pub fn iter(&self) -> std::iter::Map, fn(&(K, V)) -> (&K, &V)> { + self.0.iter().map(|(k, v)| (k, v)) + } + + /// Gets a mutable iterator over the entries of the map, in no particular order. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map = EqMap::from([ + /// ("a", 1), + /// ("b", 2), + /// ("c", 3), + /// ]); + /// + /// // add 10 to the value if the key isn't "a" + /// for (key, value) in map.iter_mut() { + /// if key != &"a" { + /// *value += 10; + /// } + /// } + /// ``` + pub fn iter_mut( + &mut self, + ) -> std::iter::Map, fn(&mut (K, V)) -> (&K, &mut V)> { + self.0.iter_mut().map(|(k, v)| (&*k, v)) + } + + /// Gets an iterator over the keys of the map. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut a = EqMap::new(); + /// a.insert(2, "b"); + /// a.insert(1, "a"); + /// + /// let keys: Vec<_> = a.keys().cloned().collect(); + /// assert_eq!(keys, [2, 1]); + /// ``` + pub fn keys(&self) -> std::iter::Map, fn(&(K, V)) -> &K> { + self.0.iter().map(|(k, _)| k) + } + + /// Gets an iterator over the values of the map. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut a = EqMap::new(); + /// a.insert(1, "hello"); + /// a.insert(2, "goodbye"); + /// + /// let values: Vec<&str> = a.values().cloned().collect(); + /// assert_eq!(values, ["hello", "goodbye"]); + /// ``` + pub fn values(&self) -> std::iter::Map, fn(&(K, V)) -> &V> { + self.0.iter().map(|(_, v)| v) + } + + /// Gets a mutable iterator over the values of the map. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut a = EqMap::new(); + /// a.insert(1, String::from("hello")); + /// a.insert(2, String::from("goodbye")); + /// + /// for value in a.values_mut() { + /// value.push_str("!"); + /// } + /// + /// let values: Vec = a.values().cloned().collect(); + /// assert_eq!(values, [String::from("hello!"), + /// String::from("goodbye!")]); + /// ``` + pub fn values_mut( + &mut self, + ) -> std::iter::Map, fn(&mut (K, V)) -> &mut V> { + self.0.iter_mut().map(|(_, v)| v) + } + + /// Returns the number of elements in the map. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut a = EqMap::new(); + /// assert_eq!(a.len(), 0); + /// a.insert(1, "a"); + /// assert_eq!(a.len(), 1); + /// ``` + #[must_use] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns `true` if the map contains no elements. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut a = EqMap::new(); + /// assert!(a.is_empty()); + /// a.insert(1, "a"); + /// assert!(!a.is_empty()); + /// ``` + #[must_use] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl fmt::Debug for EqMap { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_map().entries(self.iter()).finish() + } +} + +impl Index<&Q> for EqMap +where + K: Borrow + Eq, + Q: Eq, +{ + type Output = V; + + /// Returns a reference to the value corresponding to the supplied key. + /// + /// # Panics + /// + /// Panics if the key is not present in the `BTreeMap`. + #[inline] + fn index(&self, key: &Q) -> &V { + self.get(key).expect("no entry found for key") + } +} + +impl IndexMut<&Q> for EqMap +where + K: Borrow + Eq, + Q: Eq, +{ + /// Returns a reference to the value corresponding to the supplied key. + /// + /// # Panics + /// + /// Panics if the key is not present in the `BTreeMap`. + #[inline] + fn index_mut(&mut self, key: &Q) -> &mut V { + self.get_mut(key).expect("no entry found for key") + } +} + +impl IntoIterator for EqMap { + type IntoIter = std::vec::IntoIter<(K, V)>; + type Item = (K, V); + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl Extend<(K, V)> for EqMap { + fn extend>(&mut self, iter: T) { + self.0.extend(iter) + } +} + +impl FromIterator<(K, V)> for EqMap { + fn from_iter>(iter: T) -> Self { + Self(Vec::from_iter(iter)) + } +} + +impl From<[(K, V); N]> for EqMap { + /// Converts a `[(K, V); N]` into a `EqMap<(K, V)>`. + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let map1 = EqMap::from([(1, 2), (3, 4)]); + /// let map2: EqMap<_, _> = [(1, 2), (3, 4)].into(); + /// assert_eq!(map1, map2); + /// ``` + fn from(arr: [(K, V); N]) -> Self { + EqMap(Vec::from(arr)) + } +} + +impl PartialEq for EqMap { + fn eq(&self, other: &Self) -> bool { + self.len() == other.len() && self.iter().all(|(k, v)| other.get(k) == Some(v)) + } +} +impl Eq for EqMap {} + +use Entry::*; + +/// A view into a single entry in a map, which may either be vacant or occupied. +/// +/// This `enum` is constructed from the [`entry`] method on [`EqMap`]. +/// +/// [`entry`]: EqMap::entry +pub enum Entry<'a, K: Eq + 'a, V: 'a> { + Vacant(VacantEntry<'a, K, V>), + + /// An occupied entry. + Occupied(OccupiedEntry<'a, K, V>), +} + +impl fmt::Debug for Entry<'_, K, V> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + Vacant(ref v) => f.debug_tuple("Entry").field(v).finish(), + Occupied(ref o) => f.debug_tuple("Entry").field(o).finish(), + } + } +} + +/// A view into a vacant entry in a `EqMap`. +/// It is part of the [`Entry`] enum. +pub struct VacantEntry<'a, K: Eq, V> { + key: K, + map: &'a mut EqMap, +} + +impl fmt::Debug for VacantEntry<'_, K, V> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("VacantEntry").field(self.key()).finish() + } +} + +/// A view into an occupied entry in a `EqMap`. +/// It is part of the [`Entry`] enum. +pub struct OccupiedEntry<'a, K: Eq, V> { + map: &'a mut EqMap, + idx: usize, +} + +impl fmt::Debug for OccupiedEntry<'_, K, V> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OccupiedEntry") + .field("key", self.key()) + .field("value", self.get()) + .finish() + } +} + +/// The error returned by [`try_insert`](EqMap::try_insert) when the key already exists. +/// +/// Contains the occupied entry, and the value that was not inserted. +pub struct OccupiedError<'a, K: Eq + 'a, V: 'a> { + /// The entry in the map that was already occupied. + pub entry: OccupiedEntry<'a, K, V>, + /// The value which was not inserted, because the entry was already occupied. + pub value: V, +} + +impl fmt::Debug for OccupiedError<'_, K, V> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("OccupiedError") + .field("key", self.entry.key()) + .field("old_value", self.entry.get()) + .field("new_value", &self.value) + .finish() + } +} + +impl<'a, K: fmt::Debug + Eq, V: fmt::Debug> fmt::Display for OccupiedError<'a, K, V> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "failed to insert {:?}, key {:?} already exists with value {:?}", + self.value, + self.entry.key(), + self.entry.get(), + ) + } +} + +impl<'a, K: fmt::Debug + Eq, V: fmt::Debug> std::error::Error for OccupiedError<'a, K, V> { + fn description(&self) -> &str { + "key already exists" + } +} + +impl<'a, K: Eq, V> Entry<'a, K, V> { + /// Ensures a value is in the entry by inserting the default if empty, and returns + /// a mutable reference to the value in the entry. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// map.entry("poneyland").or_insert(12); + /// + /// assert_eq!(map["poneyland"], 12); + /// ``` + pub fn or_insert(self, default: V) -> &'a mut V { + match self { + Occupied(entry) => entry.into_mut(), + Vacant(entry) => entry.insert(default), + } + } + + /// Ensures a value is in the entry by inserting the result of the default function if empty, + /// and returns a mutable reference to the value in the entry. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map: EqMap<&str, String> = EqMap::new(); + /// let s = "hoho".to_string(); + /// + /// map.entry("poneyland").or_insert_with(|| s); + /// + /// assert_eq!(map["poneyland"], "hoho".to_string()); + /// ``` + pub fn or_insert_with V>(self, default: F) -> &'a mut V { + match self { + Occupied(entry) => entry.into_mut(), + Vacant(entry) => entry.insert(default()), + } + } + + /// Ensures a value is in the entry by inserting, if empty, the result of the default function. + /// This method allows for generating key-derived values for insertion by providing the default + /// function a reference to the key that was moved during the `.entry(key)` method call. + /// + /// The reference to the moved key is provided so that cloning or copying the key is + /// unnecessary, unlike with `.or_insert_with(|| ... )`. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// + /// map.entry("poneyland").or_insert_with_key(|key| key.chars().count()); + /// + /// assert_eq!(map["poneyland"], 9); + /// ``` + #[inline] + pub fn or_insert_with_key V>(self, default: F) -> &'a mut V { + match self { + Occupied(entry) => entry.into_mut(), + Vacant(entry) => { + let value = default(entry.key()); + entry.insert(value) + } + } + } + + /// Returns a reference to this entry's key. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// assert_eq!(map.entry("poneyland").key(), &"poneyland"); + /// ``` + pub fn key(&self) -> &K { + match *self { + Occupied(ref entry) => entry.key(), + Vacant(ref entry) => entry.key(), + } + } + + /// Provides in-place mutable access to an occupied entry before any + /// potential inserts into the map. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// + /// map.entry("poneyland") + /// .and_modify(|e| { *e += 1 }) + /// .or_insert(42); + /// assert_eq!(map["poneyland"], 42); + /// + /// map.entry("poneyland") + /// .and_modify(|e| { *e += 1 }) + /// .or_insert(42); + /// assert_eq!(map["poneyland"], 43); + /// ``` + pub fn and_modify(self, f: F) -> Self + where + F: FnOnce(&mut V), + { + match self { + Occupied(mut entry) => { + f(entry.get_mut()); + Occupied(entry) + } + Vacant(entry) => Vacant(entry), + } + } +} + +impl<'a, K: Eq, V: Default> Entry<'a, K, V> { + /// Ensures a value is in the entry by inserting the default value if empty, + /// and returns a mutable reference to the value in the entry. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map: EqMap<&str, Option> = EqMap::new(); + /// map.entry("poneyland").or_default(); + /// + /// assert_eq!(map["poneyland"], None); + /// ``` + pub fn or_default(self) -> &'a mut V { + match self { + Occupied(entry) => entry.into_mut(), + Vacant(entry) => entry.insert(Default::default()), + } + } +} + +impl<'a, K: Eq, V> VacantEntry<'a, K, V> { + /// Gets a reference to the key that would be used when inserting a value + /// through the VacantEntry. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// assert_eq!(map.entry("poneyland").key(), &"poneyland"); + /// ``` + pub fn key(&self) -> &K { + &self.key + } + + /// Take ownership of the key. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// use startos::util::collections::eq_map::Entry; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// + /// if let Entry::Vacant(v) = map.entry("poneyland") { + /// v.into_key(); + /// } + /// ``` + pub fn into_key(self) -> K { + self.key + } + + /// Sets the value of the entry with the `VacantEntry`'s key, + /// and returns a mutable reference to it. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// use startos::util::collections::eq_map::Entry; + /// + /// let mut map: EqMap<&str, u32> = EqMap::new(); + /// + /// if let Entry::Vacant(o) = map.entry("poneyland") { + /// o.insert(37); + /// } + /// assert_eq!(map["poneyland"], 37); + /// ``` + pub fn insert(self, value: V) -> &'a mut V { + self.map.0.push((self.key, value)); + self.map.0.last_mut().map(|(_, v)| v).unwrap() + } +} + +impl<'a, K: Eq, V> OccupiedEntry<'a, K, V> { + /// Gets a reference to the key in the entry. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// map.entry("poneyland").or_insert(12); + /// assert_eq!(map.entry("poneyland").key(), &"poneyland"); + /// ``` + #[must_use] + pub fn key(&self) -> &K { + &self.map.0[self.idx].0 + } + + /// Take ownership of the key and value from the map. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// use startos::util::collections::eq_map::Entry; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// map.entry("poneyland").or_insert(12); + /// + /// if let Entry::Occupied(o) = map.entry("poneyland") { + /// // We delete the entry from the map. + /// o.remove_entry(); + /// } + /// + /// // If now try to get the value, it will panic: + /// // println!("{}", map["poneyland"]); + /// ``` + pub fn remove_entry(self) -> (K, V) { + self.map.0.swap_remove(self.idx) + } + + /// Gets a reference to the value in the entry. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// use startos::util::collections::eq_map::Entry; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// map.entry("poneyland").or_insert(12); + /// + /// if let Entry::Occupied(o) = map.entry("poneyland") { + /// assert_eq!(o.get(), &12); + /// } + /// ``` + #[must_use] + pub fn get(&self) -> &V { + &self.map.0[self.idx].1 + } + + /// Gets a mutable reference to the value in the entry. + /// + /// If you need a reference to the `OccupiedEntry` that may outlive the + /// destruction of the `Entry` value, see [`into_mut`]. + /// + /// [`into_mut`]: OccupiedEntry::into_mut + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// use startos::util::collections::eq_map::Entry; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// map.entry("poneyland").or_insert(12); + /// + /// assert_eq!(map["poneyland"], 12); + /// if let Entry::Occupied(mut o) = map.entry("poneyland") { + /// *o.get_mut() += 10; + /// assert_eq!(*o.get(), 22); + /// + /// // We can use the same Entry multiple times. + /// *o.get_mut() += 2; + /// } + /// assert_eq!(map["poneyland"], 24); + /// ``` + pub fn get_mut(&mut self) -> &mut V { + &mut self.map.0[self.idx].1 + } + + /// Converts the entry into a mutable reference to its value. + /// + /// If you need multiple references to the `OccupiedEntry`, see [`get_mut`]. + /// + /// [`get_mut`]: OccupiedEntry::get_mut + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// use startos::util::collections::eq_map::Entry; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// map.entry("poneyland").or_insert(12); + /// + /// assert_eq!(map["poneyland"], 12); + /// if let Entry::Occupied(o) = map.entry("poneyland") { + /// *o.into_mut() += 10; + /// } + /// assert_eq!(map["poneyland"], 22); + /// ``` + #[must_use = "`self` will be dropped if the result is not used"] + pub fn into_mut(self) -> &'a mut V { + &mut self.map.0[self.idx].1 + } + + /// Sets the value of the entry with the `OccupiedEntry`'s key, + /// and returns the entry's old value. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// use startos::util::collections::eq_map::Entry; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// map.entry("poneyland").or_insert(12); + /// + /// if let Entry::Occupied(mut o) = map.entry("poneyland") { + /// assert_eq!(o.insert(15), 12); + /// } + /// assert_eq!(map["poneyland"], 15); + /// ``` + pub fn insert(&mut self, value: V) -> V { + std::mem::replace(self.get_mut(), value) + } + + /// Takes the value of the entry out of the map, and returns it. + /// + /// # Examples + /// + /// ``` + /// use startos::util::collections::EqMap; + /// use startos::util::collections::eq_map::Entry; + /// + /// let mut map: EqMap<&str, usize> = EqMap::new(); + /// map.entry("poneyland").or_insert(12); + /// + /// if let Entry::Occupied(o) = map.entry("poneyland") { + /// assert_eq!(o.remove(), 12); + /// } + /// // If we try to get "poneyland"'s value, it'll panic: + /// // println!("{}", map["poneyland"]); + /// ``` + pub fn remove(self) -> V { + self.remove_entry().1 + } +} + +pub struct IntoValues(std::vec::IntoIter<(K, V)>); +impl<'a, K: Eq, V> From> for std::vec::IntoIter<(K, V)> { + fn from(value: IntoValues) -> Self { + value.0 + } +} +impl Iterator for IntoValues { + type Item = V; + fn next(&mut self) -> Option { + self.0.next().map(|(_, v)| v) + } + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } + fn count(self) -> usize + where + Self: Sized, + { + self.0.count() + } +} +impl DoubleEndedIterator for IntoValues { + fn next_back(&mut self) -> Option { + self.0.next_back().map(|(_, v)| v) + } +} +impl ExactSizeIterator for IntoValues { + fn len(&self) -> usize { + self.0.len() + } +} + +pub struct IntoKeys(std::vec::IntoIter<(K, V)>); +impl<'a, K: Eq, V> From> for std::vec::IntoIter<(K, V)> { + fn from(value: IntoKeys) -> Self { + value.0 + } +} +impl Iterator for IntoKeys { + type Item = K; + fn next(&mut self) -> Option { + self.0.next().map(|(k, _)| k) + } + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } + fn count(self) -> usize + where + Self: Sized, + { + self.0.count() + } +} +impl DoubleEndedIterator for IntoKeys { + fn next_back(&mut self) -> Option { + self.0.next_back().map(|(k, _)| k) + } +} +impl ExactSizeIterator for IntoKeys { + fn len(&self) -> usize { + self.0.len() + } +} diff --git a/core/startos/src/util/collections/mod.rs b/core/startos/src/util/collections/mod.rs new file mode 100644 index 000000000..aa6e3ddb5 --- /dev/null +++ b/core/startos/src/util/collections/mod.rs @@ -0,0 +1,3 @@ +pub mod eq_map; + +pub use eq_map::EqMap; diff --git a/core/startos/src/util/mod.rs b/core/startos/src/util/mod.rs index 3b66d7507..1641e4496 100644 --- a/core/startos/src/util/mod.rs +++ b/core/startos/src/util/mod.rs @@ -34,8 +34,10 @@ use crate::shutdown::Shutdown; use crate::util::io::create_file; use crate::util::serde::{deserialize_from_str, serialize_display}; use crate::{Error, ErrorKind, ResultExt as _}; + pub mod actor; pub mod clap; +pub mod collections; pub mod cpupower; pub mod crypto; pub mod future; diff --git a/core/startos/src/util/rpc_client.rs b/core/startos/src/util/rpc_client.rs index 36fe0031a..fc93e4c64 100644 --- a/core/startos/src/util/rpc_client.rs +++ b/core/startos/src/util/rpc_client.rs @@ -138,6 +138,31 @@ impl RpcClient { err.data = Some(json!("RpcClient thread has terminated")); Err(err) } + + pub async fn notify( + &mut self, + method: T, + params: T::Params, + ) -> Result<(), RpcError> + where + T: Serialize, + T::Params: Serialize, + { + let request = RpcRequest { + id: None, + method, + params, + }; + self.writer + .write_all((dbg!(serde_json::to_string(&request))? + "\n").as_bytes()) + .await + .map_err(|e| { + let mut err = rpc_toolkit::yajrc::INTERNAL_ERROR.clone(); + err.data = Some(json!(e.to_string())); + err + })?; + Ok(()) + } } #[derive(Clone)] @@ -224,4 +249,36 @@ impl UnixRpcClient { }; res } + + pub async fn notify(&self, method: T, params: T::Params) -> Result<(), RpcError> + where + T: Serialize + Clone, + T::Params: Serialize + Clone, + { + let mut tries = 0; + let res = loop { + let mut client = self.pool.clone().get().await?; + if client.handler.is_finished() { + client.destroy(); + continue; + } + let res = client.notify(method.clone(), params.clone()).await; + match &res { + Err(e) if e.code == rpc_toolkit::yajrc::INTERNAL_ERROR.code => { + let mut e = Error::from(e.clone()); + e.kind = ErrorKind::Filesystem; + tracing::error!("{e}"); + tracing::debug!("{e:?}"); + client.destroy(); + } + _ => break res, + } + tries += 1; + if tries > MAX_TRIES { + tracing::warn!("Max Tries exceeded"); + break res; + } + }; + res + } } diff --git a/sdk/Makefile b/sdk/Makefile index ef1c886f5..660a476c4 100644 --- a/sdk/Makefile +++ b/sdk/Makefile @@ -3,6 +3,8 @@ version = $(shell git tag --sort=committerdate | tail -1) .PHONY: test clean bundle fmt buildOutput check +all: bundle + test: $(TS_FILES) lib/test/output.ts npm test diff --git a/sdk/lib/StartSdk.ts b/sdk/lib/StartSdk.ts index f09d83750..7cdedff90 100644 --- a/sdk/lib/StartSdk.ts +++ b/sdk/lib/StartSdk.ts @@ -77,6 +77,7 @@ import { ExposedStorePaths } from "./store/setupExposeStore" import { PathBuilder, extractJsonPath, pathBuilder } from "./store/PathBuilder" import { checkAllDependencies } from "./dependencies/dependencies" import { health } from "." +import { GetSslCertificate } from "./util/GetSslCertificate" export const SDKVersion = testTypeVersion("0.3.6") @@ -88,14 +89,29 @@ type AnyNeverCond = never export type ServiceInterfaceType = "ui" | "p2p" | "api" -export type MainEffects = Effects & { _type: "main" } +export type MainEffects = Effects & { + _type: "main" + clearCallbacks: () => Promise +} export type Signals = NodeJS.Signals export const SIGTERM: Signals = "SIGTERM" export const SIGKILL: Signals = "SIGKILL" export const NO_TIMEOUT = -1 -function removeConstType() { - return (t: T) => t as T & (E extends MainEffects ? {} : { const: never }) +function removeCallbackTypes(effects: E) { + return (t: T) => { + if ("_type" in effects && effects._type === "main") { + return t as E extends MainEffects ? T : Omit + } else { + if ("const" in t) { + delete t.const + } + if ("watch" in t) { + delete t.watch + } + return t as E extends MainEffects ? T : Omit + } + } } export class StartSdk { @@ -129,26 +145,23 @@ export class StartSdk { checkAllDependencies, serviceInterface: { getOwn: (effects: E, id: ServiceInterfaceId) => - removeConstType()( + removeCallbackTypes(effects)( getServiceInterface(effects, { id, - packageId: null, }), ), get: ( effects: E, opts: { id: ServiceInterfaceId; packageId: PackageId }, - ) => removeConstType()(getServiceInterface(effects, opts)), + ) => + removeCallbackTypes(effects)(getServiceInterface(effects, opts)), getAllOwn: (effects: E) => - removeConstType()( - getServiceInterfaces(effects, { - packageId: null, - }), - ), + removeCallbackTypes(effects)(getServiceInterfaces(effects, {})), getAll: ( effects: E, opts: { packageId: PackageId }, - ) => removeConstType()(getServiceInterfaces(effects, opts)), + ) => + removeCallbackTypes(effects)(getServiceInterfaces(effects, opts)), }, store: { @@ -157,7 +170,7 @@ export class StartSdk { packageId: string, path: PathBuilder, ) => - removeConstType()( + removeCallbackTypes(effects)( getStore(effects, path, { packageId, }), @@ -165,7 +178,10 @@ export class StartSdk { getOwn: ( effects: E, path: PathBuilder, - ) => removeConstType()(getStore(effects, path)), + ) => + removeCallbackTypes(effects)( + getStore(effects, path), + ), setOwn: >( effects: E, path: Path, @@ -241,7 +257,16 @@ export class StartSdk { }, ) => new ServiceInterfaceBuilder({ ...options, effects }), getSystemSmtp: (effects: E) => - removeConstType()(new GetSystemSmtp(effects)), + removeCallbackTypes(effects)(new GetSystemSmtp(effects)), + + getSslCerificate: ( + effects: E, + hostnames: string[], + algorithm?: T.Algorithm, + ) => + removeCallbackTypes(effects)( + new GetSslCertificate(effects, hostnames, algorithm), + ), createDynamicAction: < ConfigType extends diff --git a/sdk/lib/osBindings/AddAssetParams.ts b/sdk/lib/osBindings/AddAssetParams.ts index ffd7db675..7522a1cb4 100644 --- a/sdk/lib/osBindings/AddAssetParams.ts +++ b/sdk/lib/osBindings/AddAssetParams.ts @@ -1,10 +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 { Blake3Commitment } from "./Blake3Commitment" -import type { Version } from "./Version" export type AddAssetParams = { - version: Version + version: string platform: string url: string signature: AnySignature diff --git a/sdk/lib/osBindings/AddVersionParams.ts b/sdk/lib/osBindings/AddVersionParams.ts index 4ecbb7dcc..9fc281a6f 100644 --- a/sdk/lib/osBindings/AddVersionParams.ts +++ b/sdk/lib/osBindings/AddVersionParams.ts @@ -1,8 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Version } from "./Version" export type AddVersionParams = { - version: Version + version: string headline: string releaseNotes: string sourceVersion: string diff --git a/sdk/lib/osBindings/Callback.ts b/sdk/lib/osBindings/CallbackId.ts similarity index 76% rename from sdk/lib/osBindings/Callback.ts rename to sdk/lib/osBindings/CallbackId.ts index 1e5cb1af5..0ac5d7ce2 100644 --- a/sdk/lib/osBindings/Callback.ts +++ b/sdk/lib/osBindings/CallbackId.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Callback = () => void +export type CallbackId = number diff --git a/sdk/lib/osBindings/CheckDependenciesParam.ts b/sdk/lib/osBindings/CheckDependenciesParam.ts index 54580a7ff..3a00faf4f 100644 --- a/sdk/lib/osBindings/CheckDependenciesParam.ts +++ b/sdk/lib/osBindings/CheckDependenciesParam.ts @@ -1,4 +1,4 @@ // 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" -export type CheckDependenciesParam = { packageIds: Array | null } +export type CheckDependenciesParam = { packageIds?: Array } diff --git a/sdk/lib/osBindings/ChrootParams.ts b/sdk/lib/osBindings/ChrootParams.ts deleted file mode 100644 index 19131b224..000000000 --- a/sdk/lib/osBindings/ChrootParams.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ChrootParams = { - env: string | null - workdir: string | null - user: string | null - path: string - command: string - args: string[] -} diff --git a/sdk/lib/osBindings/DependencyRequirement.ts b/sdk/lib/osBindings/DependencyRequirement.ts index 01b8e12ce..3b857c476 100644 --- a/sdk/lib/osBindings/DependencyRequirement.ts +++ b/sdk/lib/osBindings/DependencyRequirement.ts @@ -1,10 +1,12 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HealthCheckId } from "./HealthCheckId" +import type { PackageId } from "./PackageId" export type DependencyRequirement = | { kind: "running" - id: string - healthChecks: string[] + id: PackageId + healthChecks: Array versionRange: string } - | { kind: "exists"; id: string; versionRange: string } + | { kind: "exists"; id: PackageId; versionRange: string } diff --git a/sdk/lib/osBindings/ExecuteAction.ts b/sdk/lib/osBindings/ExecuteAction.ts index b4eb60949..6e3c44f79 100644 --- a/sdk/lib/osBindings/ExecuteAction.ts +++ b/sdk/lib/osBindings/ExecuteAction.ts @@ -1,9 +1,9 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Guid } from "./Guid" +import type { ActionId } from "./ActionId" +import type { PackageId } from "./PackageId" export type ExecuteAction = { - procedureId: Guid - serviceId: string | null - actionId: string + packageId?: PackageId + actionId: ActionId input: any } diff --git a/sdk/lib/osBindings/ExportActionParams.ts b/sdk/lib/osBindings/ExportActionParams.ts index 5eee8fc63..8bcfbc349 100644 --- a/sdk/lib/osBindings/ExportActionParams.ts +++ b/sdk/lib/osBindings/ExportActionParams.ts @@ -1,4 +1,10 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ActionId } from "./ActionId" import type { ActionMetadata } from "./ActionMetadata" +import type { PackageId } from "./PackageId" -export type ExportActionParams = { id: string; metadata: ActionMetadata } +export type ExportActionParams = { + packageId?: PackageId + id: ActionId + metadata: ActionMetadata +} diff --git a/sdk/lib/osBindings/ParamsPackageId.ts b/sdk/lib/osBindings/GetConfiguredParams.ts similarity index 51% rename from sdk/lib/osBindings/ParamsPackageId.ts rename to sdk/lib/osBindings/GetConfiguredParams.ts index f4dd1c1eb..66fb6e320 100644 --- a/sdk/lib/osBindings/ParamsPackageId.ts +++ b/sdk/lib/osBindings/GetConfiguredParams.ts @@ -1,3 +1,4 @@ // 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" -export type ParamsPackageId = { packageId: string } +export type GetConfiguredParams = { packageId?: PackageId } diff --git a/sdk/lib/osBindings/GetHostInfoParams.ts b/sdk/lib/osBindings/GetHostInfoParams.ts index 120b4cfe1..ff6d9d709 100644 --- a/sdk/lib/osBindings/GetHostInfoParams.ts +++ b/sdk/lib/osBindings/GetHostInfoParams.ts @@ -1,9 +1,10 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Callback } from "./Callback" +import type { CallbackId } from "./CallbackId" import type { HostId } from "./HostId" +import type { PackageId } from "./PackageId" export type GetHostInfoParams = { hostId: HostId - packageId: string | null - callback: Callback + packageId?: PackageId + callback?: CallbackId } diff --git a/sdk/lib/osBindings/GetOsAssetParams.ts b/sdk/lib/osBindings/GetOsAssetParams.ts index 9872d0b59..100f711c7 100644 --- a/sdk/lib/osBindings/GetOsAssetParams.ts +++ b/sdk/lib/osBindings/GetOsAssetParams.ts @@ -1,4 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Version } from "./Version" -export type GetOsAssetParams = { version: Version; platform: string } +export type GetOsAssetParams = { version: string; platform: string } diff --git a/sdk/lib/osBindings/GetPrimaryUrlParams.ts b/sdk/lib/osBindings/GetPrimaryUrlParams.ts index dbafa4152..06bf73976 100644 --- a/sdk/lib/osBindings/GetPrimaryUrlParams.ts +++ b/sdk/lib/osBindings/GetPrimaryUrlParams.ts @@ -1,9 +1,10 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Callback } from "./Callback" -import type { ServiceInterfaceId } from "./ServiceInterfaceId" +import type { CallbackId } from "./CallbackId" +import type { HostId } from "./HostId" +import type { PackageId } from "./PackageId" export type GetPrimaryUrlParams = { - packageId: string | null - serviceInterfaceId: ServiceInterfaceId - callback: Callback + packageId?: PackageId + hostId: HostId + callback?: CallbackId } diff --git a/sdk/lib/osBindings/GetServiceInterfaceParams.ts b/sdk/lib/osBindings/GetServiceInterfaceParams.ts index 0a8bdfcb2..b71591e17 100644 --- a/sdk/lib/osBindings/GetServiceInterfaceParams.ts +++ b/sdk/lib/osBindings/GetServiceInterfaceParams.ts @@ -1,9 +1,10 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Callback } from "./Callback" +import type { CallbackId } from "./CallbackId" +import type { PackageId } from "./PackageId" import type { ServiceInterfaceId } from "./ServiceInterfaceId" export type GetServiceInterfaceParams = { - packageId: string | null + packageId?: PackageId serviceInterfaceId: ServiceInterfaceId - callback: Callback + callback?: CallbackId } diff --git a/sdk/lib/osBindings/GetServicePortForwardParams.ts b/sdk/lib/osBindings/GetServicePortForwardParams.ts index beb423d9a..63236328e 100644 --- a/sdk/lib/osBindings/GetServicePortForwardParams.ts +++ b/sdk/lib/osBindings/GetServicePortForwardParams.ts @@ -1,8 +1,9 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { HostId } from "./HostId" +import type { PackageId } from "./PackageId" export type GetServicePortForwardParams = { - packageId: string | null - internalPort: number + packageId?: PackageId hostId: HostId + internalPort: number } diff --git a/sdk/lib/osBindings/GetSslCertificateParams.ts b/sdk/lib/osBindings/GetSslCertificateParams.ts index a33eff540..85c677540 100644 --- a/sdk/lib/osBindings/GetSslCertificateParams.ts +++ b/sdk/lib/osBindings/GetSslCertificateParams.ts @@ -1,8 +1,9 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Algorithm } from "./Algorithm" +import type { CallbackId } from "./CallbackId" export type GetSslCertificateParams = { - packageId: string | null - hostId: string - algorithm: Algorithm | null + hostnames: string[] + algorithm?: Algorithm + callback?: CallbackId } diff --git a/sdk/lib/osBindings/GetSslKeyParams.ts b/sdk/lib/osBindings/GetSslKeyParams.ts index 0438c345a..2ca3076c8 100644 --- a/sdk/lib/osBindings/GetSslKeyParams.ts +++ b/sdk/lib/osBindings/GetSslKeyParams.ts @@ -1,8 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Algorithm } from "./Algorithm" -export type GetSslKeyParams = { - packageId: string | null - hostId: string - algorithm: Algorithm | null -} +export type GetSslKeyParams = { hostnames: string[]; algorithm?: Algorithm } diff --git a/sdk/lib/osBindings/GetStoreParams.ts b/sdk/lib/osBindings/GetStoreParams.ts index dc3d4e211..e134cd4a6 100644 --- a/sdk/lib/osBindings/GetStoreParams.ts +++ b/sdk/lib/osBindings/GetStoreParams.ts @@ -1,3 +1,9 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { CallbackId } from "./CallbackId" +import type { PackageId } from "./PackageId" -export type GetStoreParams = { packageId: string | null; path: string } +export type GetStoreParams = { + packageId?: PackageId + path: string + callback?: CallbackId +} diff --git a/sdk/lib/osBindings/GetSystemSmtpParams.ts b/sdk/lib/osBindings/GetSystemSmtpParams.ts index 650d59c49..73b91057c 100644 --- a/sdk/lib/osBindings/GetSystemSmtpParams.ts +++ b/sdk/lib/osBindings/GetSystemSmtpParams.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Callback } from "./Callback" +import type { CallbackId } from "./CallbackId" -export type GetSystemSmtpParams = { callback: Callback } +export type GetSystemSmtpParams = { callback: CallbackId | null } diff --git a/sdk/lib/osBindings/HostAddress.ts b/sdk/lib/osBindings/HostAddress.ts index 0388e49c7..73b46d8e5 100644 --- a/sdk/lib/osBindings/HostAddress.ts +++ b/sdk/lib/osBindings/HostAddress.ts @@ -1,3 +1,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type HostAddress = { kind: "onion"; address: string } +export type HostAddress = + | { kind: "onion"; address: string } + | { kind: "domain"; address: string } diff --git a/sdk/lib/osBindings/ListServiceInterfacesParams.ts b/sdk/lib/osBindings/ListServiceInterfacesParams.ts index 4140831d0..fd27ace2b 100644 --- a/sdk/lib/osBindings/ListServiceInterfacesParams.ts +++ b/sdk/lib/osBindings/ListServiceInterfacesParams.ts @@ -1,7 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Callback } from "./Callback" +import type { CallbackId } from "./CallbackId" +import type { PackageId } from "./PackageId" export type ListServiceInterfacesParams = { - packageId: string | null - callback: Callback + packageId?: PackageId + callback?: CallbackId } diff --git a/sdk/lib/osBindings/ListVersionSignersParams.ts b/sdk/lib/osBindings/ListVersionSignersParams.ts index d066fbeb4..baf516bf2 100644 --- a/sdk/lib/osBindings/ListVersionSignersParams.ts +++ b/sdk/lib/osBindings/ListVersionSignersParams.ts @@ -1,4 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Version } from "./Version" -export type ListVersionSignersParams = { version: Version } +export type ListVersionSignersParams = { version: string } diff --git a/sdk/lib/osBindings/MountTarget.ts b/sdk/lib/osBindings/MountTarget.ts index e4888d075..bbee5453b 100644 --- a/sdk/lib/osBindings/MountTarget.ts +++ b/sdk/lib/osBindings/MountTarget.ts @@ -1,8 +1,10 @@ // 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 { VolumeId } from "./VolumeId" export type MountTarget = { - packageId: string - volumeId: string + packageId: PackageId + volumeId: VolumeId subpath: string | null readonly: boolean } diff --git a/sdk/lib/osBindings/OsIndex.ts b/sdk/lib/osBindings/OsIndex.ts index 9fb795402..fe9a4e395 100644 --- a/sdk/lib/osBindings/OsIndex.ts +++ b/sdk/lib/osBindings/OsIndex.ts @@ -1,5 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { OsVersionInfo } from "./OsVersionInfo" -import type { Version } from "./Version" +import type { OsVersionInfoMap } from "./OsVersionInfoMap" -export type OsIndex = { versions: { [key: Version]: OsVersionInfo } } +export type OsIndex = { versions: OsVersionInfoMap } diff --git a/sdk/lib/osBindings/OsVersionInfoMap.ts b/sdk/lib/osBindings/OsVersionInfoMap.ts new file mode 100644 index 000000000..6f333f1fb --- /dev/null +++ b/sdk/lib/osBindings/OsVersionInfoMap.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { OsVersionInfo } from "./OsVersionInfo" + +export type OsVersionInfoMap = { [key: string]: OsVersionInfo } diff --git a/sdk/lib/osBindings/ParamsMaybePackageId.ts b/sdk/lib/osBindings/ParamsMaybePackageId.ts deleted file mode 100644 index a20bb9aa5..000000000 --- a/sdk/lib/osBindings/ParamsMaybePackageId.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type ParamsMaybePackageId = { packageId: string | null } diff --git a/sdk/lib/osBindings/RemoveActionParams.ts b/sdk/lib/osBindings/RemoveActionParams.ts deleted file mode 100644 index c343620b8..000000000 --- a/sdk/lib/osBindings/RemoveActionParams.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type RemoveActionParams = { id: string } diff --git a/sdk/lib/osBindings/RemoveAddressParams.ts b/sdk/lib/osBindings/RemoveAddressParams.ts deleted file mode 100644 index 14099ebbc..000000000 --- a/sdk/lib/osBindings/RemoveAddressParams.ts +++ /dev/null @@ -1,4 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ServiceInterfaceId } from "./ServiceInterfaceId" - -export type RemoveAddressParams = { id: ServiceInterfaceId } diff --git a/sdk/lib/osBindings/RemoveVersionParams.ts b/sdk/lib/osBindings/RemoveVersionParams.ts index d00a6ee9e..2c974de56 100644 --- a/sdk/lib/osBindings/RemoveVersionParams.ts +++ b/sdk/lib/osBindings/RemoveVersionParams.ts @@ -1,4 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Version } from "./Version" -export type RemoveVersionParams = { version: Version } +export type RemoveVersionParams = { version: string } diff --git a/sdk/lib/osBindings/ServerInfo.ts b/sdk/lib/osBindings/ServerInfo.ts index 935e3a99f..76840cfc4 100644 --- a/sdk/lib/osBindings/ServerInfo.ts +++ b/sdk/lib/osBindings/ServerInfo.ts @@ -2,6 +2,7 @@ import type { Governor } from "./Governor" import type { IpInfo } from "./IpInfo" import type { ServerStatus } from "./ServerStatus" +import type { SmtpValue } from "./SmtpValue" import type { WifiInfo } from "./WifiInfo" export type ServerInfo = { @@ -28,5 +29,5 @@ export type ServerInfo = { ntpSynced: boolean zram: boolean governor: Governor | null - smtp: string | null + smtp: SmtpValue | null } diff --git a/sdk/lib/osBindings/SetSystemSmtpParams.ts b/sdk/lib/osBindings/SetSystemSmtpParams.ts deleted file mode 100644 index 49c66e86c..000000000 --- a/sdk/lib/osBindings/SetSystemSmtpParams.ts +++ /dev/null @@ -1,3 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type SetSystemSmtpParams = { smtp: string } diff --git a/sdk/lib/osBindings/SignAssetParams.ts b/sdk/lib/osBindings/SignAssetParams.ts index d55a061a7..39f54ad69 100644 --- a/sdk/lib/osBindings/SignAssetParams.ts +++ b/sdk/lib/osBindings/SignAssetParams.ts @@ -1,9 +1,8 @@ // 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 { Version } from "./Version" export type SignAssetParams = { - version: Version + version: string platform: string signature: AnySignature } diff --git a/sdk/lib/osBindings/SmtpValue.ts b/sdk/lib/osBindings/SmtpValue.ts new file mode 100644 index 000000000..5291d6602 --- /dev/null +++ b/sdk/lib/osBindings/SmtpValue.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SmtpValue = { + server: string + port: number + from: string + login: string + password: string | null +} diff --git a/sdk/lib/osBindings/VersionSignerParams.ts b/sdk/lib/osBindings/VersionSignerParams.ts index 102eecefd..781e2a4df 100644 --- a/sdk/lib/osBindings/VersionSignerParams.ts +++ b/sdk/lib/osBindings/VersionSignerParams.ts @@ -1,5 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Guid } from "./Guid" -import type { Version } from "./Version" -export type VersionSignerParams = { version: Version; signer: Guid } +export type VersionSignerParams = { version: string; signer: Guid } diff --git a/sdk/lib/osBindings/index.ts b/sdk/lib/osBindings/index.ts index 70ce65f29..2bf2bf69c 100644 --- a/sdk/lib/osBindings/index.ts +++ b/sdk/lib/osBindings/index.ts @@ -24,11 +24,10 @@ export { BindOptions } from "./BindOptions" export { BindParams } from "./BindParams" export { Blake3Commitment } from "./Blake3Commitment" export { BlockDev } from "./BlockDev" -export { Callback } from "./Callback" +export { CallbackId } from "./CallbackId" export { Category } from "./Category" export { CheckDependenciesParam } from "./CheckDependenciesParam" export { CheckDependenciesResult } from "./CheckDependenciesResult" -export { ChrootParams } from "./ChrootParams" export { Cifs } from "./Cifs" export { ContactInfo } from "./ContactInfo" export { CreateOverlayedImageParams } from "./CreateOverlayedImageParams" @@ -50,6 +49,7 @@ export { ExportServiceInterfaceParams } from "./ExportServiceInterfaceParams" export { ExposeForDependentsParams } from "./ExposeForDependentsParams" export { FullIndex } from "./FullIndex" export { FullProgress } from "./FullProgress" +export { GetConfiguredParams } from "./GetConfiguredParams" export { GetHostInfoParams } from "./GetHostInfoParams" export { GetOsAssetParams } from "./GetOsAssetParams" export { GetOsVersionParams } from "./GetOsVersionParams" @@ -99,6 +99,7 @@ export { MountTarget } from "./MountTarget" export { NamedProgress } from "./NamedProgress" export { OnionHostname } from "./OnionHostname" export { OsIndex } from "./OsIndex" +export { OsVersionInfoMap } from "./OsVersionInfoMap" export { OsVersionInfo } from "./OsVersionInfo" export { PackageDataEntry } from "./PackageDataEntry" export { PackageDetailLevel } from "./PackageDetailLevel" @@ -108,8 +109,6 @@ export { PackageInfoShort } from "./PackageInfoShort" export { PackageInfo } from "./PackageInfo" export { PackageState } from "./PackageState" export { PackageVersionInfo } from "./PackageVersionInfo" -export { ParamsMaybePackageId } from "./ParamsMaybePackageId" -export { ParamsPackageId } from "./ParamsPackageId" export { PasswordType } from "./PasswordType" export { PathOrUrl } from "./PathOrUrl" export { ProcedureId } from "./ProcedureId" @@ -118,8 +117,6 @@ export { Public } from "./Public" export { RecoverySource } from "./RecoverySource" export { RegistryAsset } from "./RegistryAsset" export { RegistryInfo } from "./RegistryInfo" -export { RemoveActionParams } from "./RemoveActionParams" -export { RemoveAddressParams } from "./RemoveAddressParams" export { RemoveVersionParams } from "./RemoveVersionParams" export { RequestCommitment } from "./RequestCommitment" export { Security } from "./Security" @@ -138,13 +135,13 @@ export { SetHealth } from "./SetHealth" export { SetMainStatusStatus } from "./SetMainStatusStatus" export { SetMainStatus } from "./SetMainStatus" export { SetStoreParams } from "./SetStoreParams" -export { SetSystemSmtpParams } from "./SetSystemSmtpParams" export { SetupExecuteParams } from "./SetupExecuteParams" export { SetupProgress } from "./SetupProgress" export { SetupResult } from "./SetupResult" export { SetupStatusRes } from "./SetupStatusRes" export { SignAssetParams } from "./SignAssetParams" export { SignerInfo } from "./SignerInfo" +export { SmtpValue } from "./SmtpValue" export { Status } from "./Status" export { UpdatingState } from "./UpdatingState" export { VerifyCifsParams } from "./VerifyCifsParams" diff --git a/sdk/lib/store/getStore.ts b/sdk/lib/store/getStore.ts index 38265c7fd..5250a02a1 100644 --- a/sdk/lib/store/getStore.ts +++ b/sdk/lib/store/getStore.ts @@ -28,7 +28,6 @@ export class GetStore { return this.effects.store.get({ ...this.options, path: extractJsonPath(this.path), - callback: () => {}, }) } diff --git a/sdk/lib/test/startosTypeValidation.test.ts b/sdk/lib/test/startosTypeValidation.test.ts index 79ec62106..846967c31 100644 --- a/sdk/lib/test/startosTypeValidation.test.ts +++ b/sdk/lib/test/startosTypeValidation.test.ts @@ -2,14 +2,13 @@ import { Effects } from "../types" import { CheckDependenciesParam, ExecuteAction, + GetConfiguredParams, SetMainStatus, } from ".././osBindings" import { CreateOverlayedImageParams } from ".././osBindings" import { DestroyOverlayedImageParams } from ".././osBindings" import { BindParams } from ".././osBindings" import { GetHostInfoParams } from ".././osBindings" -import { ParamsPackageId } from ".././osBindings" -import { ParamsMaybePackageId } from ".././osBindings" import { SetConfigured } from ".././osBindings" import { SetHealth } from ".././osBindings" import { ExposeForDependentsParams } from ".././osBindings" @@ -22,11 +21,12 @@ import { GetServicePortForwardParams } from ".././osBindings" import { ExportServiceInterfaceParams } from ".././osBindings" import { GetPrimaryUrlParams } from ".././osBindings" import { ListServiceInterfacesParams } from ".././osBindings" -import { RemoveAddressParams } from ".././osBindings" import { ExportActionParams } from ".././osBindings" -import { RemoveActionParams } from ".././osBindings" import { MountParams } from ".././osBindings" function typeEquality(_a: ExpectedType) {} + +type WithCallback = Omit & { callback: () => void } + describe("startosTypeValidation ", () => { test(`checking the params match`, () => { const testInput: any = {} @@ -39,32 +39,29 @@ describe("startosTypeValidation ", () => { createOverlayedImage: {} as CreateOverlayedImageParams, destroyOverlayedImage: {} as DestroyOverlayedImageParams, clearBindings: undefined, + getInstalledPackages: undefined, bind: {} as BindParams, - getHostInfo: {} as GetHostInfoParams, - exists: {} as ParamsPackageId, - getConfigured: undefined, - stopped: {} as ParamsMaybePackageId, - running: {} as ParamsPackageId, + getHostInfo: {} as WithCallback, + getConfigured: {} as GetConfiguredParams, restart: undefined, shutdown: undefined, setConfigured: {} as SetConfigured, setHealth: {} as SetHealth, exposeForDependents: {} as ExposeForDependentsParams, - getSslCertificate: {} as GetSslCertificateParams, + getSslCertificate: {} as WithCallback, getSslKey: {} as GetSslKeyParams, - getServiceInterface: {} as GetServiceInterfaceParams, + getServiceInterface: {} as WithCallback, setDependencies: {} as SetDependenciesParams, store: {} as never, - getSystemSmtp: {} as GetSystemSmtpParams, + getSystemSmtp: {} as WithCallback, getContainerIp: undefined, getServicePortForward: {} as GetServicePortForwardParams, clearServiceInterfaces: undefined, exportServiceInterface: {} as ExportServiceInterfaceParams, - getPrimaryUrl: {} as GetPrimaryUrlParams, - listServiceInterfaces: {} as ListServiceInterfacesParams, - removeAddress: {} as RemoveAddressParams, + getPrimaryUrl: {} as WithCallback, + listServiceInterfaces: {} as WithCallback, exportAction: {} as ExportActionParams, - removeAction: {} as RemoveActionParams, + clearActions: undefined, mount: {} as MountParams, checkDependencies: {} as CheckDependenciesParam, getDependencies: undefined, diff --git a/sdk/lib/types.ts b/sdk/lib/types.ts index e9c766448..9a3157ed3 100644 --- a/sdk/lib/types.ts +++ b/sdk/lib/types.ts @@ -13,6 +13,8 @@ import { BindParams, Manifest, CheckDependenciesResult, + ActionId, + HostId, } from "./osBindings" import { MainEffects, ServiceInterfaceType, Signals } from "./StartSdk" @@ -289,187 +291,51 @@ export type PropertiesReturn = { /** Used to reach out from the pure js runtime */ export type Effects = { + // action + + /** Run an action exported by a service */ executeAction(opts: { - serviceId: string | null + packageId?: PackageId + actionId: ActionId input: Input }): Promise + /** Define an action that can be invoked by a user or service */ + exportAction(options: { + id: ActionId + metadata: ActionMetadata + }): Promise + /** Remove all exported actions */ + clearActions(): Promise - /** A low level api used by makeOverlay */ - createOverlayedImage(options: { imageId: string }): Promise<[string, string]> + // config - /** A low level api used by destroyOverlay + makeOverlay:destroy */ - destroyOverlayedImage(options: { guid: string }): Promise - - /** Removes all network bindings */ - clearBindings(): Promise - /** Creates a host connected to the specified port with the provided options */ - bind(options: BindParams): Promise - /** Retrieves the current hostname(s) associated with a host id */ - // getHostInfo(options: { - // kind: "static" | "single" - // serviceInterfaceId: string - // packageId: string | null - // callback: () => void - // }): Promise - getHostInfo(options: { - hostId: string - packageId: string | null - callback: () => void - }): Promise - - // /** - // * Run rsync between two volumes. This is used to backup data between volumes. - // * This is a long running process, and a structure that we can either wait for, or get the progress of. - // */ - // runRsync(options: { - // srcVolume: string - // dstVolume: string - // srcPath: string - // dstPath: string - // // rsync options: https://linux.die.net/man/1/rsync - // options: BackupOptions - // }): { - // id: () => Promise - // wait: () => Promise - // progress: () => Promise - // } - - store: { - /** Get a value in a json like data, can be observed and subscribed */ - get(options: { - /** If there is no packageId it is assumed the current package */ - packageId?: string - /** The path defaults to root level, using the [JsonPath](https://jsonpath.com/) */ - path: StorePath - callback: (config: unknown, previousConfig: unknown) => void - }): Promise - /** Used to store values that can be accessed and subscribed to */ - set(options: { - /** Sets the value for the wrapper at the path, it will override, using the [JsonPath](https://jsonpath.com/) */ - path: StorePath - value: ExtractStore - }): Promise - } - - setMainStatus(o: SetMainStatus): Promise - - getSystemSmtp(input: { - callback: (config: unknown, previousConfig: unknown) => void - }): Promise - - /** Get the IP address of the container */ - getContainerIp(): Promise - /** - * Get the port address for another service - */ - getServicePortForward(options: { - internalPort: number - packageId: string | null - }): Promise - - /** Removes all network interfaces */ - clearServiceInterfaces(): Promise - /** When we want to create a link in the front end interfaces, and example is - * exposing a url to view a web service - */ - exportServiceInterface(options: ExportServiceInterfaceParams): Promise - - exposeForDependents(options: { paths: string[] }): Promise - - /** - * There are times that we want to see the addresses that where exported - * @param options.addressId If we want to filter the address id - * - * Note: any auth should be filtered out already - */ - getServiceInterface(options: { - packageId: PackageId | null - serviceInterfaceId: ServiceInterfaceId - callback: () => void - }): Promise - - /** - * The user sets the primary url for a interface - * @param options - */ - getPrimaryUrl(options: GetPrimaryUrlParams): Promise - - /** - * There are times that we want to see the addresses that where exported - * @param options.addressId If we want to filter the address id - * - * Note: any auth should be filtered out already - */ - listServiceInterfaces(options: { - packageId: PackageId | null - callback: () => void - }): Promise> - - /** - *Remove an address that was exported. Used problably during main or during setConfig. - * @param options - */ - removeAddress(options: { id: string }): Promise - - /** - * - * @param options - */ - exportAction(options: { id: string; metadata: ActionMetadata }): Promise - /** - * Remove an action that was exported. Used problably during main or during setConfig. - */ - removeAction(options: { id: string }): Promise - - getConfigured(): Promise - /** - * This called after a valid set config as well as during init. - * @param configured - */ + /** Returns whether or not the package has been configured */ + getConfigured(options: { packageId?: PackageId }): Promise + /** Indicates that this package has been configured. Called during setConfig or init */ setConfigured(options: { configured: boolean }): Promise - /** - * - * @returns PEM encoded fullchain (ecdsa) - */ - getSslCertificate: (options: { - packageId: string | null - hostId: string - algorithm: "ecdsa" | "ed25519" | null - }) => Promise<[string, string, string]> - /** - * @returns PEM encoded ssl key (ecdsa) - */ - getSslKey: (options: { - packageId: string | null - hostId: string - algorithm: "ecdsa" | "ed25519" | null - }) => Promise + // control - setHealth(o: SetHealth): Promise + /** restart this service's main function */ + restart(): Promise + /** stop this service's main function */ + shutdown(): Promise + /** indicate to the host os what runstate the service is in */ + setMainStatus(options: SetMainStatus): Promise - /** Set the dependencies of what the service needs, usually ran during the set config as a best practice */ + // dependency + + /** Set the dependencies of what the service needs, usually run during the set config as a best practice */ setDependencies(options: { dependencies: Dependencies }): Promise - /** Get the list of the dependencies, both the dynamic set by the effect of setDependencies and the end result any required in the manifest */ getDependencies(): Promise - - /** When one wants to checks the status of several services during the checking of dependencies. The result will include things like the status - * of the service and what the current health checks are. - */ + /** Test whether current dependency requirements are satisfied */ checkDependencies(options: { - packageIds: PackageId[] | null + packageIds?: PackageId[] }): Promise - /** Exists could be useful during the runtime to know if some service exists, option dep */ - exists(options: { packageId: PackageId }): Promise - /** Exists could be useful during the runtime to know if some service is running, option dep */ - running(options: { packageId: PackageId }): Promise - - restart(): Promise - shutdown(): Promise - + /** mount a volume of a dependency */ mount(options: { location: string target: { @@ -479,8 +345,103 @@ export type Effects = { readonly: boolean } }): Promise + /** Returns a list of the ids of all installed packages */ + getInstalledPackages(): Promise + /** grants access to certain paths in the store to dependents */ + exposeForDependents(options: { paths: string[] }): Promise - stopped(options: { packageId: string | null }): Promise + // health + + /** sets the result of a health check */ + setHealth(o: SetHealth): Promise + + // image + + /** A low level api used by Overlay */ + createOverlayedImage(options: { imageId: string }): Promise<[string, string]> + /** A low level api used by Overlay */ + destroyOverlayedImage(options: { guid: string }): Promise + + // net + + // bind + /** Creates a host connected to the specified port with the provided options */ + bind(options: BindParams): Promise + /** Get the port address for a service */ + getServicePortForward(options: { + packageId?: PackageId + hostId: HostId + internalPort: number + }): Promise + /** Removes all network bindings */ + clearBindings(): Promise + // host + /** Returns information about the specified host, if it exists */ + getHostInfo(options: { + packageId?: PackageId + hostId: HostId + callback?: () => void + }): Promise + /** Returns the primary url that a user has selected for a host, if it exists */ + getPrimaryUrl(options: { + packageId?: PackageId + hostId: HostId + callback?: () => void + }): Promise + /** Returns the IP address of the container */ + getContainerIp(): Promise + // interface + /** Creates an interface bound to a specific host and port to show to the user */ + exportServiceInterface(options: ExportServiceInterfaceParams): Promise + /** Returns an exported service interface */ + getServiceInterface(options: { + packageId?: PackageId + serviceInterfaceId: ServiceInterfaceId + callback?: () => void + }): Promise + /** Returns all exported service interfaces for a package */ + listServiceInterfaces(options: { + packageId?: PackageId + callback?: () => void + }): Promise> + /** Removes all service interfaces */ + clearServiceInterfaces(): Promise + // ssl + /** Returns a PEM encoded fullchain for the hostnames specified */ + getSslCertificate: (options: { + hostnames: string[] + algorithm?: "ecdsa" | "ed25519" + callback?: () => void + }) => Promise<[string, string, string]> + /** Returns a PEM encoded private key corresponding to the certificate for the hostnames specified */ + getSslKey: (options: { + hostnames: string[] + algorithm?: "ecdsa" | "ed25519" + }) => Promise + + // store + + store: { + /** Get a value in a json like data, can be observed and subscribed */ + get(options: { + /** If there is no packageId it is assumed the current package */ + packageId?: string + /** The path defaults to root level, using the [JsonPath](https://jsonpath.com/) */ + path: StorePath + callback?: () => void + }): Promise + /** Used to store values that can be accessed and subscribed to */ + set(options: { + /** Sets the value for the wrapper at the path, it will override, using the [JsonPath](https://jsonpath.com/) */ + path: StorePath + value: ExtractStore + }): Promise + } + + // system + + /** Returns globally configured SMTP settings, if they exist */ + getSystemSmtp(options: { callback?: () => void }): Promise } /** rsync options: https://linux.die.net/man/1/rsync diff --git a/sdk/lib/util/GetSslCertificate.ts b/sdk/lib/util/GetSslCertificate.ts new file mode 100644 index 000000000..df19607d4 --- /dev/null +++ b/sdk/lib/util/GetSslCertificate.ts @@ -0,0 +1,47 @@ +import { T } from ".." +import { Effects } from "../types" + +export class GetSslCertificate { + constructor( + readonly effects: Effects, + readonly hostnames: string[], + readonly algorithm?: T.Algorithm, + ) {} + + /** + * Returns the system SMTP credentials. Restarts the service if the credentials change + */ + const() { + return this.effects.getSslCertificate({ + hostnames: this.hostnames, + algorithm: this.algorithm, + callback: this.effects.restart, + }) + } + /** + * Returns the system SMTP credentials. Does nothing if the credentials change + */ + once() { + return this.effects.getSslCertificate({ + hostnames: this.hostnames, + algorithm: this.algorithm, + }) + } + /** + * Watches the system SMTP credentials. Takes a custom callback function to run whenever the credentials change + */ + async *watch() { + while (true) { + let callback: () => void + const waitForNext = new Promise((resolve) => { + callback = resolve + }) + yield await this.effects.getSslCertificate({ + hostnames: this.hostnames, + algorithm: this.algorithm, + callback: () => callback(), + }) + await waitForNext + } + } +} diff --git a/sdk/lib/util/GetSystemSmtp.ts b/sdk/lib/util/GetSystemSmtp.ts index 1853afd78..498a2d8b2 100644 --- a/sdk/lib/util/GetSystemSmtp.ts +++ b/sdk/lib/util/GetSystemSmtp.ts @@ -15,9 +15,7 @@ export class GetSystemSmtp { * Returns the system SMTP credentials. Does nothing if the credentials change */ once() { - return this.effects.getSystemSmtp({ - callback: () => {}, - }) + return this.effects.getSystemSmtp({}) } /** * Watches the system SMTP credentials. Takes a custom callback function to run whenever the credentials change diff --git a/sdk/lib/util/getServiceInterface.ts b/sdk/lib/util/getServiceInterface.ts index a2f17be10..b41d8a15f 100644 --- a/sdk/lib/util/getServiceInterface.ts +++ b/sdk/lib/util/getServiceInterface.ts @@ -55,9 +55,9 @@ export type ServiceInterfaceFilled = { /** Whether or not to mask the URIs for this interface. Useful if the URIs contain sensitive information, such as a password, macaroon, or API key */ masked: boolean /** Information about the host for this binding */ - host: Host + host: Host | null /** URI information */ - addressInfo: FilledAddressInfo + addressInfo: FilledAddressInfo | null /** Indicates if we are a ui/p2p/api for the kind of interface that this is representing */ type: ServiceInterfaceType /** The primary hostname for the service, as chosen by the user */ @@ -183,33 +183,36 @@ const makeInterfaceFilled = async ({ }: { effects: Effects id: string - packageId: string | null - callback: () => void + packageId?: string + callback?: () => void }) => { const serviceInterfaceValue = await effects.getServiceInterface({ serviceInterfaceId: id, packageId, callback, }) + if (!serviceInterfaceValue) { + return null + } const hostId = serviceInterfaceValue.addressInfo.hostId const host = await effects.getHostInfo({ packageId, hostId, callback, }) - const primaryUrl = await effects - .getPrimaryUrl({ - serviceInterfaceId: id, - packageId, - callback, - }) - .catch((e) => null) + const primaryUrl = await effects.getPrimaryUrl({ + hostId, + packageId, + callback, + }) const interfaceFilled: ServiceInterfaceFilled = { ...serviceInterfaceValue, primaryUrl: primaryUrl, host, - addressInfo: filledAddress(host, serviceInterfaceValue.addressInfo), + addressInfo: host + ? filledAddress(host, serviceInterfaceValue.addressInfo) + : null, get primaryHostname() { if (primaryUrl == null) return null return getHostname(primaryUrl) @@ -221,7 +224,7 @@ const makeInterfaceFilled = async ({ export class GetServiceInterface { constructor( readonly effects: Effects, - readonly opts: { id: string; packageId: string | null }, + readonly opts: { id: string; packageId?: string }, ) {} /** @@ -230,7 +233,7 @@ export class GetServiceInterface { async const() { const { id, packageId } = this.opts const callback = this.effects.restart - const interfaceFilled: ServiceInterfaceFilled = await makeInterfaceFilled({ + const interfaceFilled = await makeInterfaceFilled({ effects: this.effects, id, packageId, @@ -244,12 +247,10 @@ export class GetServiceInterface { */ async once() { const { id, packageId } = this.opts - const callback = () => {} - const interfaceFilled: ServiceInterfaceFilled = await makeInterfaceFilled({ + const interfaceFilled = await makeInterfaceFilled({ effects: this.effects, id, packageId, - callback, }) return interfaceFilled @@ -277,7 +278,7 @@ export class GetServiceInterface { } export function getServiceInterface( effects: Effects, - opts: { id: string; packageId: string | null }, + opts: { id: string; packageId?: string }, ) { return new GetServiceInterface(effects, opts) } diff --git a/sdk/lib/util/getServiceInterfaces.ts b/sdk/lib/util/getServiceInterfaces.ts index c4cdc6b59..9f0e242b8 100644 --- a/sdk/lib/util/getServiceInterfaces.ts +++ b/sdk/lib/util/getServiceInterfaces.ts @@ -11,8 +11,8 @@ const makeManyInterfaceFilled = async ({ callback, }: { effects: Effects - packageId: string | null - callback: () => void + packageId?: string + callback?: () => void }) => { const serviceInterfaceValues = await effects.listServiceInterfaces({ packageId, @@ -27,9 +27,12 @@ const makeManyInterfaceFilled = async ({ hostId, callback, }) + if (!host) { + throw new Error(`host ${hostId} not found!`) + } const primaryUrl = await effects .getPrimaryUrl({ - serviceInterfaceId: serviceInterfaceValue.id, + hostId, packageId, callback, }) @@ -52,7 +55,7 @@ const makeManyInterfaceFilled = async ({ export class GetServiceInterfaces { constructor( readonly effects: Effects, - readonly opts: { packageId: string | null }, + readonly opts: { packageId?: string }, ) {} /** @@ -75,12 +78,10 @@ export class GetServiceInterfaces { */ async once() { const { packageId } = this.opts - const callback = () => {} const interfaceFilled: ServiceInterfaceFilled[] = await makeManyInterfaceFilled({ effects: this.effects, packageId, - callback, }) return interfaceFilled @@ -107,7 +108,7 @@ export class GetServiceInterfaces { } export function getServiceInterfaces( effects: Effects, - opts: { packageId: string | null }, + opts: { packageId?: string }, ) { return new GetServiceInterfaces(effects, opts) } diff --git a/sdk/package.json b/sdk/package.json index e03153722..1f9090b14 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@start9labs/start-sdk", - "version": "0.3.6-alpha5", + "version": "0.3.6-alpha6", "description": "Software development kit to facilitate packaging services for StartOS", "main": "./cjs/lib/index.js", "types": "./cjs/lib/index.d.ts", diff --git a/web/projects/ui/src/app/services/api/mock-patch.ts b/web/projects/ui/src/app/services/api/mock-patch.ts index 7b0d6b1df..8b4c28d1e 100644 --- a/web/projects/ui/src/app/services/api/mock-patch.ts +++ b/web/projects/ui/src/app/services/api/mock-patch.ts @@ -74,7 +74,7 @@ export const mockPatchData: DataModel = { platform: 'x86_64-nonfree', zram: true, governor: 'performance', - smtp: 'todo', + smtp: null, wifi: { interface: 'wlan0', ssids: [], From e04b93a51a212987a21b2e6a8f760a91caea8573 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Thu, 25 Jul 2024 12:17:13 -0600 Subject: [PATCH 069/125] fix builds on platforms without kernel support for `squashfs` --- .github/workflows/startos-iso.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/startos-iso.yaml b/.github/workflows/startos-iso.yaml index c51eafc5c..010c79594 100644 --- a/.github/workflows/startos-iso.yaml +++ b/.github/workflows/startos-iso.yaml @@ -83,7 +83,7 @@ jobs: uses: docker/setup-qemu-action@v3 - name: Set up system dependencies - run: sudo apt-get update && sudo apt-get install -y qemu-user-static systemd-container + run: sudo apt-get update && sudo apt-get install -y qemu-user-static systemd-container squashfuse - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 From c3d17bf8476bf312c08f7a7808526a0be93b3cc4 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Thu, 25 Jul 2024 12:26:49 -0600 Subject: [PATCH 070/125] fix sync_db middleware --- core/startos/src/context/rpc.rs | 4 ++- core/startos/src/db/mod.rs | 48 +++++++++++++++++++++++++++++-- core/startos/src/middleware/db.rs | 9 +++--- 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/core/startos/src/context/rpc.rs b/core/startos/src/context/rpc.rs index ba1ab3fbb..cea2059d3 100644 --- a/core/startos/src/context/rpc.rs +++ b/core/startos/src/context/rpc.rs @@ -11,7 +11,7 @@ use josekit::jwk::Jwk; use reqwest::{Client, Proxy}; use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::{CallRemote, Context, Empty}; -use tokio::sync::{broadcast, Mutex, RwLock}; +use tokio::sync::{broadcast, watch, Mutex, RwLock}; use tokio::time::Instant; use tracing::instrument; @@ -43,6 +43,7 @@ pub struct RpcContextSeed { pub datadir: PathBuf, pub disk_guid: Arc, pub db: TypedPatchDb, + pub sync_db: watch::Sender, pub account: RwLock, pub net_controller: Arc, pub s9pk_arch: Option<&'static str>, @@ -212,6 +213,7 @@ impl RpcContext { find_eth_iface().await? }, disk_guid, + sync_db: watch::Sender::new(db.sequence().await), db, account: RwLock::new(account), net_controller, diff --git a/core/startos/src/db/mod.rs b/core/startos/src/db/mod.rs index e59161e9b..ef35bd30d 100644 --- a/core/startos/src/db/mod.rs +++ b/core/startos/src/db/mod.rs @@ -10,10 +10,12 @@ use clap::Parser; use imbl_value::InternedString; use itertools::Itertools; use patch_db::json_ptr::{JsonPointer, ROOT}; -use patch_db::{Dump, Revision}; +use patch_db::{DiffPatch, Dump, Revision}; use rpc_toolkit::yajrc::RpcError; use rpc_toolkit::{from_fn_async, Context, HandlerArgs, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; +use tokio::sync::mpsc::{self, UnboundedReceiver}; +use tokio::sync::watch; use tracing::instrument; use ts_rs::TS; @@ -124,14 +126,56 @@ pub struct SubscribeRes { pub guid: Guid, } +struct DbSubscriber { + rev: u64, + sub: UnboundedReceiver, + sync_db: watch::Receiver, +} +impl DbSubscriber { + async fn recv(&mut self) -> Option { + loop { + tokio::select! { + rev = self.sub.recv() => { + if let Some(rev) = rev.as_ref() { + self.rev = rev.id; + } + return rev + } + _ = self.sync_db.changed() => { + let id = *self.sync_db.borrow(); + if id > self.rev { + match self.sub.try_recv() { + Ok(rev) => { + self.rev = rev.id; + return Some(rev) + } + Err(mpsc::error::TryRecvError::Disconnected) => { + return None + } + Err(mpsc::error::TryRecvError::Empty) => { + return Some(Revision { id, patch: DiffPatch::default() }) + } + } + } + } + } + } + } +} + pub async fn subscribe( ctx: RpcContext, SubscribeParams { pointer, session }: SubscribeParams, ) -> Result { - let (dump, mut sub) = ctx + let (dump, sub) = ctx .db .dump_and_sub(pointer.unwrap_or_else(|| PUBLIC.clone())) .await; + let mut sub = DbSubscriber { + rev: dump.id, + sub, + sync_db: ctx.sync_db.subscribe(), + }; let guid = Guid::new(); ctx.rpc_continuations .add( diff --git a/core/startos/src/middleware/db.rs b/core/startos/src/middleware/db.rs index e8b4f8887..4e5f0e037 100644 --- a/core/startos/src/middleware/db.rs +++ b/core/startos/src/middleware/db.rs @@ -36,10 +36,11 @@ impl Middleware for SyncDb { async fn process_http_response(&mut self, context: &RpcContext, response: &mut Response) { if let Err(e) = async { if self.sync_db { - response.headers_mut().append( - "X-Patch-Sequence", - HeaderValue::from_str(&context.db.sequence().await.to_string())?, - ); + let id = context.db.sequence().await; + response + .headers_mut() + .append("X-Patch-Sequence", HeaderValue::from_str(&id.to_string())?); + context.sync_db.send_replace(id); } Ok::<_, InvalidHeaderValue>(()) } From 099b77cf9bbcbb0dbc9d435f238a5e5185104212 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Thu, 25 Jul 2024 12:30:05 -0600 Subject: [PATCH 071/125] fix .local service resolution --- core/startos/src/net/net_controller.rs | 114 ++++++++++--------------- 1 file changed, 47 insertions(+), 67 deletions(-) diff --git a/core/startos/src/net/net_controller.rs b/core/startos/src/net/net_controller.rs index 521888665..b7a8022b4 100644 --- a/core/startos/src/net/net_controller.rs +++ b/core/startos/src/net/net_controller.rs @@ -29,6 +29,7 @@ pub struct PreInitNetController { tor: TorController, vhost: VHostController, os_bindings: Vec>, + server_hostnames: Vec>, } impl PreInitNetController { #[instrument(skip_all)] @@ -44,6 +45,7 @@ impl PreInitNetController { tor: TorController::new(tor_control, tor_socks), vhost: VHostController::new(db), os_bindings: Vec::new(), + server_hostnames: Vec::new(), }; res.add_os_bindings(hostname, os_tor_key).await?; Ok(res) @@ -59,64 +61,26 @@ impl PreInitNetController { MaybeUtf8String("h2".into()), ])); - // Internal DNS - self.vhost - .add( - Some("embassy".into()), - 443, - ([127, 0, 0, 1], 80).into(), - alpn.clone(), - ) - .await?; - self.vhost - .add( - Some("startos".into()), - 443, - ([127, 0, 0, 1], 80).into(), - alpn.clone(), - ) - .await?; + self.server_hostnames = vec![ + // LAN IP + None, + // Internal DNS + Some("embassy".into()), + Some("startos".into()), + // localhost + Some("localhost".into()), + Some(hostname.no_dot_host_name()), + // LAN mDNS + Some(hostname.local_domain_name()), + ]; - // LAN IP - self.os_bindings.push( - self.vhost - .add(None, 443, ([127, 0, 0, 1], 80).into(), alpn.clone()) - .await?, - ); - - // localhost - self.os_bindings.push( - self.vhost - .add( - Some("localhost".into()), - 443, - ([127, 0, 0, 1], 80).into(), - alpn.clone(), - ) - .await?, - ); - self.os_bindings.push( - self.vhost - .add( - Some(hostname.no_dot_host_name()), - 443, - ([127, 0, 0, 1], 80).into(), - alpn.clone(), - ) - .await?, - ); - - // LAN mDNS - self.os_bindings.push( - self.vhost - .add( - Some(hostname.local_domain_name()), - 443, - ([127, 0, 0, 1], 80).into(), - alpn.clone(), - ) - .await?, - ); + for hostname in self.server_hostnames.iter().cloned() { + self.os_bindings.push( + self.vhost + .add(hostname, 443, ([127, 0, 0, 1], 80).into(), alpn.clone()) + .await?, + ); + } // Tor self.os_bindings.push( @@ -154,6 +118,7 @@ pub struct NetController { pub(super) dns: DnsController, pub(super) forward: LanPortForwardController, pub(super) os_bindings: Vec>, + pub(super) server_hostnames: Vec>, } impl NetController { @@ -163,6 +128,7 @@ impl NetController { tor, vhost, os_bindings, + server_hostnames, }: PreInitNetController, dns_bind: &[SocketAddr], ) -> Result { @@ -173,6 +139,7 @@ impl NetController { dns: DnsController::init(dns_bind).await?, forward: LanPortForwardController::new(), os_bindings, + server_hostnames, }; res.os_bindings .push(res.dns.add(None, HOST_IP.into()).await?); @@ -258,10 +225,15 @@ impl NetService { let ctrl = self.net_controller()?; let mut errors = ErrorCollection::new(); for (_, binds) in std::mem::take(&mut self.binds) { - for (_, (lan, _, _, rc)) in binds.lan { + for (_, (lan, _, hostnames, rc)) in binds.lan { drop(rc); if let Some(external) = lan.assigned_ssl_port { - ctrl.vhost.gc(None, external).await?; + for hostname in ctrl.server_hostnames.iter().cloned() { + ctrl.vhost.gc(hostname, external).await?; + } + for hostname in hostnames { + ctrl.vhost.gc(Some(hostname), external).await?; + } } if let Some(external) = lan.assigned_port { ctrl.forward.gc(external).await?; @@ -317,11 +289,13 @@ impl NetService { Err(AlpnInfo::Reflect) } }; - rcs.push( - ctrl.vhost - .add(None, external, target, connect_ssl.clone()) - .await?, - ); + for hostname in ctrl.server_hostnames.iter().cloned() { + rcs.push( + ctrl.vhost + .add(hostname, external, target, connect_ssl.clone()) + .await?, + ); + } for address in host.addresses() { match address { HostAddress::Onion { address } => { @@ -407,7 +381,9 @@ impl NetService { } if let Some((lan, _, hostnames, _)) = old_lan_bind { if let Some(external) = lan.assigned_ssl_port { - ctrl.vhost.gc(None, external).await?; + for hostname in ctrl.server_hostnames.iter().cloned() { + ctrl.vhost.gc(hostname, external).await?; + } for hostname in hostnames { ctrl.vhost.gc(Some(hostname), external).await?; } @@ -429,7 +405,9 @@ impl NetService { }); for (lan, hostnames) in removed { if let Some(external) = lan.assigned_ssl_port { - ctrl.vhost.gc(None, external).await?; + for hostname in ctrl.server_hostnames.iter().cloned() { + ctrl.vhost.gc(hostname, external).await?; + } for hostname in hostnames { ctrl.vhost.gc(Some(hostname), external).await?; } @@ -533,7 +511,9 @@ impl NetService { pub async fn remove_all(mut self) -> Result<(), Error> { self.shutdown = true; if let Some(ctrl) = Weak::upgrade(&self.controller) { - self.clear_bindings().await + self.clear_bindings().await?; + drop(ctrl); + Ok(()) } else { tracing::warn!("NetService dropped after NetController is shutdown"); Err(Error::new( From 1a0536d2123bbe96730fa252ff3986fb7b883703 Mon Sep 17 00:00:00 2001 From: J H Date: Thu, 25 Jul 2024 13:25:18 -0600 Subject: [PATCH 072/125] fix: Optional input --- container-runtime/src/Adapters/RpcListener.ts | 4 ++-- .../Systems/SystemForEmbassy/index.ts | 4 ++-- .../src/Adapters/Systems/SystemForStartOs.ts | 6 +++--- core/startos/src/action.rs | 1 + core/startos/src/util/serde.rs | 8 +++++++ .../app-actions/app-actions.page.ts | 21 +++++++++++++++++-- 6 files changed, 35 insertions(+), 9 deletions(-) diff --git a/container-runtime/src/Adapters/RpcListener.ts b/container-runtime/src/Adapters/RpcListener.ts index 6e8e7aac8..28f578149 100644 --- a/container-runtime/src/Adapters/RpcListener.ts +++ b/container-runtime/src/Adapters/RpcListener.ts @@ -64,7 +64,7 @@ const runType = object({ input: any, timeout: number, }, - ["timeout"], + ["timeout", "input"], ), }) const sandboxRunType = object({ @@ -77,7 +77,7 @@ const sandboxRunType = object({ input: any, timeout: number, }, - ["timeout"], + ["timeout", "input"], ), }) const callbackType = object({ diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index 2e0836bb0..e5d2e096b 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -232,7 +232,7 @@ export class SystemForEmbassy implements System { effects: Effects, options: { procedure: JsonPath - input: unknown + input?: unknown timeout?: number | undefined }, ): Promise { @@ -294,7 +294,7 @@ export class SystemForEmbassy implements System { effects: Effects, options: { procedure: JsonPath - input: unknown + input?: unknown timeout?: number | undefined }, ): Promise { diff --git a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts index 78c21b7c7..029b504c0 100644 --- a/container-runtime/src/Adapters/Systems/SystemForStartOs.ts +++ b/container-runtime/src/Adapters/Systems/SystemForStartOs.ts @@ -75,7 +75,7 @@ export class SystemForStartOs implements System { effects: Effects, options: { procedure: Procedure - input: unknown + input?: unknown timeout?: number | undefined }, ): Promise { @@ -137,7 +137,7 @@ export class SystemForStartOs implements System { effects: Effects | MainEffects, options: { procedure: Procedure - input: unknown + input?: unknown timeout?: number | undefined }, ): Promise { @@ -219,7 +219,7 @@ export class SystemForStartOs implements System { async sandbox( effects: Effects, - options: { procedure: Procedure; input: unknown; timeout?: number }, + options: { procedure: Procedure; input?: unknown; timeout?: number }, ): Promise { return this.execute(effects, options) } diff --git a/core/startos/src/action.rs b/core/startos/src/action.rs index e93af4a4d..7c4492adc 100644 --- a/core/startos/src/action.rs +++ b/core/startos/src/action.rs @@ -58,6 +58,7 @@ pub struct ActionParams { pub action_id: ActionId, #[command(flatten)] #[ts(type = "{ [key: string]: any } | null")] + #[serde(default)] pub input: StdinDeserializable>, } // impl C diff --git a/core/startos/src/util/serde.rs b/core/startos/src/util/serde.rs index bac2c524a..88d7bfc11 100644 --- a/core/startos/src/util/serde.rs +++ b/core/startos/src/util/serde.rs @@ -568,6 +568,14 @@ where #[derive(Deserialize, Serialize, TS)] pub struct StdinDeserializable(pub T); +impl Default for StdinDeserializable +where + T: Default, +{ + fn default() -> Self { + Self(T::default()) + } +} impl FromArgMatches for StdinDeserializable where T: DeserializeOwned, diff --git a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts index 63c534bb6..99bd70e48 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts @@ -20,6 +20,20 @@ import { import { getAllPackages, getManifest } from 'src/app/util/get-package-data' import { hasCurrentDeps } from 'src/app/util/has-deps' +const allowedStatuses = { + onlyRunning: new Set(['running']), + onlyStopped: new Set(['stopped']), + any: new Set([ + 'running', + 'stopped', + 'restarting', + 'restoring', + 'stopping', + 'starting', + 'backingUp', + ]), +} + @Component({ selector: 'app-actions', templateUrl: './app-actions.page.html', @@ -46,7 +60,10 @@ export class AppActionsPage { status: T.Status, action: { key: string; value: T.ActionMetadata }, ) { - if (status && action.value.allowedStatuses.includes(status.main.status)) { + if ( + status && + allowedStatuses[action.value.allowedStatuses].has(status.main.status) + ) { if (!isEmptyObject(action.value.input || {})) { this.formDialog.open(FormComponent, { label: action.value.name, @@ -84,7 +101,7 @@ export class AppActionsPage { await alert.present() } } else { - const statuses = [...action.value.allowedStatuses] + const statuses = [...allowedStatuses[action.value.allowedStatuses]] const last = statuses.pop() let statusesStr = statuses.join(', ') let error = '' From a9373d9779ceb240a2f800eaa9dea3dc76f8cb59 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Thu, 25 Jul 2024 14:34:03 -0600 Subject: [PATCH 073/125] don't show "Bytes" for overall progress --- core/startos/src/progress.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/core/startos/src/progress.rs b/core/startos/src/progress.rs index 5a2e5ef27..cc3257132 100644 --- a/core/startos/src/progress.rs +++ b/core/startos/src/progress.rs @@ -18,7 +18,9 @@ use crate::prelude::*; lazy_static::lazy_static! { static ref SPINNER: ProgressStyle = ProgressStyle::with_template("{spinner} {msg}...").unwrap(); - static ref PERCENTAGE: ProgressStyle = ProgressStyle::with_template("{msg} {percent}% {wide_bar} [{bytes}/{total_bytes}] [{binary_bytes_per_sec} {eta}]").unwrap(); + static ref PERCENTAGE: ProgressStyle = ProgressStyle::with_template("{msg} {percent}% {wide_bar} [{human_pos}/{human_len}] [{per_sec} {eta}]").unwrap(); + static ref PERCENTAGE_BYTES: ProgressStyle = ProgressStyle::with_template("{msg} {percent}% {wide_bar} [{binary_bytes}/{binary_total_bytes}] [{binary_bytes_per_sec} {eta}]").unwrap(); + static ref STEPS: ProgressStyle = ProgressStyle::with_template("{spinner} {wide_msg} [{human_pos}/?] [{per_sec} {elapsed}]").unwrap(); static ref BYTES: ProgressStyle = ProgressStyle::with_template("{spinner} {wide_msg} [{bytes}/?] [{binary_bytes_per_sec} {elapsed}]").unwrap(); } @@ -38,7 +40,7 @@ impl Progress { pub fn new() -> Self { Progress::NotStarted(()) } - pub fn update_bar(self, bar: &ProgressBar) { + pub fn update_bar(self, bar: &ProgressBar, bytes: bool) { match self { Self::NotStarted(()) => { bar.set_style(SPINNER.clone()); @@ -51,7 +53,11 @@ impl Progress { bar.finish(); } Self::Progress { done, total: None } => { - bar.set_style(BYTES.clone()); + if bytes { + bar.set_style(BYTES.clone()); + } else { + bar.set_style(STEPS.clone()); + } bar.set_position(done); bar.tick(); } @@ -59,7 +65,11 @@ impl Progress { done, total: Some(total), } => { - bar.set_style(PERCENTAGE.clone()); + if bytes { + bar.set_style(PERCENTAGE_BYTES.clone()); + } else { + bar.set_style(PERCENTAGE.clone()); + } bar.set_position(done); bar.set_length(total); bar.tick(); @@ -490,7 +500,7 @@ impl PhasedProgressBar { ); } } - progress.overall.update_bar(&self.overall); + progress.overall.update_bar(&self.overall, false); for (name, bar) in self.phases.iter() { if let Some(progress) = progress.phases.iter().find_map(|p| { if &p.name == name { @@ -499,7 +509,7 @@ impl PhasedProgressBar { None } }) { - progress.update_bar(bar); + progress.update_bar(bar, true); } } } From 419e3f7f2b15b9fdfc58d4ab900b5760762ec884 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Thu, 25 Jul 2024 14:34:30 -0600 Subject: [PATCH 074/125] fix https redirect --- core/startos/Cargo.toml | 7 +- core/startos/src/net/vhost.rs | 28 ++++-- core/startos/src/util/io.rs | 164 +++++++++++++++++++++------------- 3 files changed, 129 insertions(+), 70 deletions(-) diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index ae4382bc2..2e6df1e3d 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -98,7 +98,12 @@ hex = "0.4.3" hmac = "0.12.1" http = "1.0.0" http-body-util = "0.1" -hyper-util = { version = "0.1.5", features = ["tokio", "service"] } +hyper-util = { version = "0.1.5", features = [ + "tokio", + "service", + "http1", + "http2", +] } id-pool = { version = "0.2.2", default-features = false, features = [ "serde", "u16", diff --git a/core/startos/src/net/vhost.rs b/core/startos/src/net/vhost.rs index cdd752709..9fc7c8384 100644 --- a/core/startos/src/net/vhost.rs +++ b/core/startos/src/net/vhost.rs @@ -13,6 +13,7 @@ use http::Uri; use imbl_value::InternedString; use models::ResultExt; use serde::{Deserialize, Serialize}; +use tokio::io::AsyncWriteExt; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::{Mutex, RwLock}; use tokio_rustls::rustls::pki_types::{ @@ -27,7 +28,7 @@ use ts_rs::TS; use crate::db::model::Database; use crate::net::static_server::server_error; use crate::prelude::*; -use crate::util::io::BackTrackingReader; +use crate::util::io::BackTrackingIO; use crate::util::serde::MaybeUtf8String; // not allowed: <=1024, >=32768, 5355, 5432, 9050, 6010, 9051, 5353 @@ -129,8 +130,7 @@ impl VHostServer { tracing::debug!("{e:?}"); } - let mut stream = BackTrackingReader::new(stream); - stream.start_buffering(); + let mut stream = BackTrackingIO::new(stream); let mapping = mapping.clone(); let db = db.clone(); tokio::spawn(async move { @@ -156,6 +156,7 @@ impl VHostServer { .and_then(|host| host.to_str().ok()); let uri = Uri::from_parts({ let mut parts = req.uri().to_owned().into_parts(); + parts.scheme = Some("https".parse()?); parts.authority = host.map(FromStr::from_str).transpose()?; parts })?; @@ -313,8 +314,12 @@ impl VHostServer { ) .await .with_kind(crate::ErrorKind::OpenSsl)?; + let mut accept = mid.into_stream(Arc::new(cfg)); + let io = accept.get_mut().unwrap(); + let buffered = io.stop_buffering(); + io.write_all(&buffered).await?; let mut tls_stream = - match mid.into_stream(Arc::new(cfg)).await { + match accept.await { Ok(a) => a, Err(e) => { tracing::trace!( "VHostController: failed to accept TLS connection on port {port}: {e}"); @@ -322,7 +327,6 @@ impl VHostServer { return Ok(()) } }; - tls_stream.get_mut().0.stop_buffering(); tokio::io::copy_bidirectional( &mut tls_stream, &mut target_stream, @@ -335,8 +339,12 @@ impl VHostServer { { cfg.alpn_protocols.push(proto.into()); } + let mut accept = mid.into_stream(Arc::new(cfg)); + let io = accept.get_mut().unwrap(); + let buffered = io.stop_buffering(); + io.write_all(&buffered).await?; let mut tls_stream = - match mid.into_stream(Arc::new(cfg)).await { + match accept.await { Ok(a) => a, Err(e) => { tracing::trace!( "VHostController: failed to accept TLS connection on port {port}: {e}"); @@ -344,7 +352,6 @@ impl VHostServer { return Ok(()) } }; - tls_stream.get_mut().0.stop_buffering(); tokio::io::copy_bidirectional( &mut tls_stream, &mut tcp_stream, @@ -353,8 +360,12 @@ impl VHostServer { } Err(AlpnInfo::Specified(alpn)) => { cfg.alpn_protocols = alpn.into_iter().map(|a| a.0).collect(); + let mut accept = mid.into_stream(Arc::new(cfg)); + let io = accept.get_mut().unwrap(); + let buffered = io.stop_buffering(); + io.write_all(&buffered).await?; let mut tls_stream = - match mid.into_stream(Arc::new(cfg)).await { + match accept.await { Ok(a) => a, Err(e) => { tracing::trace!( "VHostController: failed to accept TLS connection on port {port}: {e}"); @@ -362,7 +373,6 @@ impl VHostServer { return Ok(()) } }; - tls_stream.get_mut().0.stop_buffering(); tokio::io::copy_bidirectional( &mut tls_stream, &mut tcp_stream, diff --git a/core/startos/src/util/io.rs b/core/startos/src/util/io.rs index bba45fa69..6d9c8a4ff 100644 --- a/core/startos/src/util/io.rs +++ b/core/startos/src/util/io.rs @@ -411,107 +411,151 @@ impl> CursorExt for Cursor { } } -#[pin_project::pin_project] #[derive(Debug)] -pub struct BackTrackingReader { - #[pin] - reader: T, - buffer: Cursor>, - buffering: bool, +enum BTBuffer { + NotBuffering, + Buffering { read: Vec, write: Vec }, + Rewound { read: Cursor> }, } -impl BackTrackingReader { - pub fn new(reader: T) -> Self { - Self { - reader, - buffer: Cursor::new(Vec::new()), - buffering: false, - } - } - pub fn start_buffering(&mut self) { - self.buffer.set_position(0); - self.buffer.get_mut().truncate(0); - self.buffering = true; - } - pub fn stop_buffering(&mut self) { - self.buffer.set_position(0); - self.buffer.get_mut().truncate(0); - self.buffering = false; - } - pub fn rewind(&mut self) { - self.buffering = false; - } - pub fn unwrap(self) -> T { - self.reader +impl Default for BTBuffer { + fn default() -> Self { + BTBuffer::NotBuffering } } -impl AsyncRead for BackTrackingReader { +#[pin_project::pin_project] +#[derive(Debug)] +pub struct BackTrackingIO { + #[pin] + io: T, + buffer: BTBuffer, +} +impl BackTrackingIO { + pub fn new(io: T) -> Self { + Self { + io, + buffer: BTBuffer::Buffering { + read: Vec::new(), + write: Vec::new(), + }, + } + } + #[must_use] + pub fn stop_buffering(&mut self) -> Vec { + match std::mem::take(&mut self.buffer) { + BTBuffer::Buffering { write, .. } => write, + BTBuffer::NotBuffering => Vec::new(), + BTBuffer::Rewound { read } => { + self.buffer = BTBuffer::Rewound { read }; + Vec::new() + } + } + } + pub fn rewind(&mut self) -> Vec { + match std::mem::take(&mut self.buffer) { + BTBuffer::Buffering { read, write } => { + self.buffer = BTBuffer::Rewound { + read: Cursor::new(read), + }; + write + } + BTBuffer::NotBuffering => Vec::new(), + BTBuffer::Rewound { read } => { + self.buffer = BTBuffer::Rewound { read }; + Vec::new() + } + } + } + pub fn unwrap(self) -> T { + self.io + } +} + +impl AsyncRead for BackTrackingIO { fn poll_read( self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { let this = self.project(); - if *this.buffering { - let filled = buf.filled().len(); - let res = this.reader.poll_read(cx, buf); - this.buffer - .get_mut() - .extend_from_slice(&buf.filled()[filled..]); - res - } else { - let mut ready = false; - if (this.buffer.position() as usize) < this.buffer.get_ref().len() { - this.buffer.pure_read(buf); - ready = true; + match this.buffer { + BTBuffer::Buffering { read, .. } => { + let filled = buf.filled().len(); + let res = this.io.poll_read(cx, buf); + read.extend_from_slice(&buf.filled()[filled..]); + res } - if buf.remaining() > 0 { - match this.reader.poll_read(cx, buf) { - Poll::Pending => { - if ready { - Poll::Ready(Ok(())) - } else { - Poll::Pending - } - } - a => a, + BTBuffer::NotBuffering => this.io.poll_read(cx, buf), + BTBuffer::Rewound { read } => { + let mut ready = false; + if (read.position() as usize) < read.get_ref().len() { + read.pure_read(buf); + ready = true; + } + if buf.remaining() > 0 { + match this.io.poll_read(cx, buf) { + Poll::Pending => { + if ready { + Poll::Ready(Ok(())) + } else { + Poll::Pending + } + } + a => a, + } + } else { + Poll::Ready(Ok(())) } - } else { - Poll::Ready(Ok(())) } } } } -impl AsyncWrite for BackTrackingReader { +impl AsyncWrite for BackTrackingIO { fn is_write_vectored(&self) -> bool { - self.reader.is_write_vectored() + self.io.is_write_vectored() } fn poll_flush( self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> Poll> { - self.project().reader.poll_flush(cx) + self.project().io.poll_flush(cx) } fn poll_shutdown( self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, ) -> Poll> { - self.project().reader.poll_shutdown(cx) + self.project().io.poll_shutdown(cx) } fn poll_write( self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, buf: &[u8], ) -> Poll> { - self.project().reader.poll_write(cx, buf) + let this = self.project(); + if let BTBuffer::Buffering { write, .. } = this.buffer { + write.extend_from_slice(buf); + Poll::Ready(Ok(buf.len())) + } else { + this.io.poll_write(cx, buf) + } } fn poll_write_vectored( self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, bufs: &[std::io::IoSlice<'_>], ) -> Poll> { - self.project().reader.poll_write_vectored(cx, bufs) + let this = self.project(); + if let BTBuffer::Buffering { write, .. } = this.buffer { + let len = bufs.iter().map(|b| b.len()).sum(); + write.reserve(len); + for buf in bufs { + write.extend_from_slice(buf); + } + Poll::Ready(Ok(len)) + } else { + this.io.poll_write_vectored(cx, bufs) + } } } From 69baa44a3abac81e6eabb50f076e30d72ce697a0 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Thu, 25 Jul 2024 15:44:40 -0600 Subject: [PATCH 075/125] use squashfuse if available --- container-runtime/update-image.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/container-runtime/update-image.sh b/container-runtime/update-image.sh index 3a8813518..61429821c 100755 --- a/container-runtime/update-image.sh +++ b/container-runtime/update-image.sh @@ -8,7 +8,11 @@ if mountpoint tmp/combined; then sudo umount -R tmp/combined; fi if mountpoint tmp/lower; then sudo umount tmp/lower; fi sudo rm -rf tmp mkdir -p tmp/lower tmp/upper tmp/work tmp/combined -sudo mount -o loop -t squashfs debian.${ARCH}.squashfs tmp/lower +if which squashfuse > /dev/null; then + sudo squashfuse debian.${ARCH}.squashfs tmp/lower +else + sudo mount debian.${ARCH}.squashfs tmp/lower +fi sudo mount -t overlay -olowerdir=tmp/lower,upperdir=tmp/upper,workdir=tmp/work overlay tmp/combined QEMU= From 854044229c726f57aa73a3b81e36eca65d940ec3 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Thu, 25 Jul 2024 15:44:51 -0600 Subject: [PATCH 076/125] reduce reliance on sudo --- core/build-containerbox.sh | 5 +---- core/build-registrybox.sh | 5 +---- core/build-startbox.sh | 5 +---- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/core/build-containerbox.sh b/core/build-containerbox.sh index f988d2de3..e4a8f6e7a 100755 --- a/core/build-containerbox.sh +++ b/core/build-containerbox.sh @@ -28,15 +28,12 @@ set +e fail= echo "FEATURES=\"$FEATURES\"" echo "RUSTFLAGS=\"$RUSTFLAGS\"" -if ! rust-musl-builder sh -c "(cd core && cargo build --release --no-default-features --features container-runtime,$FEATURES --locked --bin containerbox --target=$ARCH-unknown-linux-musl)"; then +if ! rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features container-runtime,$FEATURES --locked --bin containerbox --target=$ARCH-unknown-linux-musl && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo"; then fail=true fi set -e cd core -sudo chown -R $USER target -sudo chown -R $USER ~/.cargo - if [ -n "$fail" ]; then exit 1 fi diff --git a/core/build-registrybox.sh b/core/build-registrybox.sh index 9928b6714..9db57dd80 100755 --- a/core/build-registrybox.sh +++ b/core/build-registrybox.sh @@ -28,15 +28,12 @@ set +e fail= echo "FEATURES=\"$FEATURES\"" echo "RUSTFLAGS=\"$RUSTFLAGS\"" -if ! rust-musl-builder sh -c "(cd core && cargo build --release --no-default-features --features cli,registry,$FEATURES --locked --bin registrybox --target=$ARCH-unknown-linux-musl)"; then +if ! rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features cli,registry,$FEATURES --locked --bin registrybox --target=$ARCH-unknown-linux-musl && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo"; then fail=true fi set -e cd core -sudo chown -R $USER target -sudo chown -R $USER ~/.cargo - if [ -n "$fail" ]; then exit 1 fi diff --git a/core/build-startbox.sh b/core/build-startbox.sh index 0604ba5da..55a455f09 100755 --- a/core/build-startbox.sh +++ b/core/build-startbox.sh @@ -28,15 +28,12 @@ set +e fail= echo "FEATURES=\"$FEATURES\"" echo "RUSTFLAGS=\"$RUSTFLAGS\"" -if ! rust-musl-builder sh -c "(cd core && cargo build --release --no-default-features --features cli,daemon,$FEATURES --locked --bin startbox --target=$ARCH-unknown-linux-musl)"; then +if ! rust-musl-builder sh -c "cd core && cargo build --release --no-default-features --features cli,daemon,$FEATURES --locked --bin startbox --target=$ARCH-unknown-linux-musl && chown -R $UID:$UID target && chown -R $UID:$UID /root/.cargo"; then fail=true fi set -e cd core -sudo chown -R $USER target -sudo chown -R $USER ~/.cargo - if [ -n "$fail" ]; then exit 1 fi From 370c38ec76d6d5429f894a27c9c282bc7f7d478f Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Thu, 25 Jul 2024 16:14:04 -0600 Subject: [PATCH 077/125] fix launchUI button --- .../Systems/SystemForEmbassy/index.ts | 6 +- .../app-list-pkg/app-list-pkg.component.html | 2 +- .../app-list-pkg/app-list-pkg.component.ts | 8 ++- .../app-show-status.component.html | 4 +- .../app-show-status.component.ts | 11 +++- .../ui/src/app/services/config.service.ts | 62 +++++++++---------- .../src/app/services/ui-launcher.service.ts | 9 +-- 7 files changed, 59 insertions(+), 43 deletions(-) diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index 2e0836bb0..eabe1401d 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -444,7 +444,11 @@ export class SystemForEmbassy implements System { description: interfaceValue.description, hasPrimary: false, disabled: false, - type: "api", + type: + interfaceValue.ui && + (origin.scheme === "http" || origin.sslScheme === "https") + ? "ui" + : "api", masked: false, path: "", schemeOverride: null, diff --git a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.html b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.html index e353cf9b2..8225d7e53 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.html @@ -25,7 +25,7 @@ slot="end" fill="clear" color="primary" - (click)="launchUi($event, pkg.entry.serviceInterfaces)" + (click)="launchUi($event, pkg.entry.serviceInterfaces, pkg.entry.hosts)" [disabled]=" !(pkg.entry.stateInfo.state | isLaunchable : pkgMainStatus.status) " diff --git a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.ts b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.ts index 4df818956..1464ac8a2 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-list/app-list-pkg/app-list-pkg.component.ts @@ -27,9 +27,13 @@ export class AppListPkgComponent { return this.pkgMainStatus.status === 'stopping' ? '30s' : null // @dr-bonez TODO } - launchUi(e: Event, interfaces: PackageDataEntry['serviceInterfaces']): void { + launchUi( + e: Event, + interfaces: PackageDataEntry['serviceInterfaces'], + hosts: PackageDataEntry['hosts'], + ): void { e.stopPropagation() e.preventDefault() - this.launcherService.launch(interfaces) + this.launcherService.launch(interfaces, hosts) } } diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html index be7bc1c30..c5d18907c 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.html @@ -56,13 +56,13 @@
Launch UI diff --git a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts index 186bbe39a..308db67be 100644 --- a/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts +++ b/web/projects/ui/src/app/pages/apps-routes/app-show/components/app-show-status/app-show-status.component.ts @@ -55,6 +55,10 @@ export class AppShowStatusComponent { return this.pkg.serviceInterfaces } + get hosts(): PackageDataEntry['hosts'] { + return this.pkg.hosts + } + get pkgStatus(): T.Status { return this.pkg.status } @@ -79,8 +83,11 @@ export class AppShowStatusComponent { return this.pkgStatus?.main.status === 'stopping' ? '30s' : null // @dr-bonez TODO } - launchUi(interfaces: PackageDataEntry['serviceInterfaces']): void { - this.launcherService.launch(interfaces) + launchUi( + interfaces: PackageDataEntry['serviceInterfaces'], + hosts: PackageDataEntry['hosts'], + ): void { + this.launcherService.launch(interfaces, hosts) } async presentModalConfig(): Promise { diff --git a/web/projects/ui/src/app/services/config.service.ts b/web/projects/ui/src/app/services/config.service.ts index 42116bdd8..a148112f0 100644 --- a/web/projects/ui/src/app/services/config.service.ts +++ b/web/projects/ui/src/app/services/config.service.ts @@ -59,50 +59,50 @@ export class ConfigService { /** ${scheme}://${username}@${host}:${externalPort}${suffix} */ launchableAddress( interfaces: PackageDataEntry['serviceInterfaces'], - host: T.Host, + hosts: PackageDataEntry['hosts'], ): string { - const ui = Object.values(interfaces).find(i => i.type === 'ui') + const ui = Object.values(interfaces).find( + i => + i.type === 'ui' && + (i.addressInfo.scheme === 'http' || + i.addressInfo.sslScheme === 'https'), + ) // TODO: select if multiple if (!ui) return '' + const hostnameInfo = + hosts[ui.addressInfo.hostId]?.hostnameInfo[ui.addressInfo.internalPort] + + console.debug(hostnameInfo) + + if (!hostnameInfo) return '' + const addressInfo = ui.addressInfo - const scheme = this.isHttps() ? 'https' : 'http' + const scheme = this.isHttps() + ? ui.addressInfo.sslScheme === 'https' + ? 'https' + : 'http' + : ui.addressInfo.scheme === 'http' + ? 'http' + : 'https' const username = addressInfo.username ? addressInfo.username + '@' : '' const suffix = addressInfo.suffix || '' const url = new URL(`${scheme}://${username}placeholder${suffix}`) - if (host.kind === 'multi') { - const onionHostname = host.addresses.find(h => h.kind === 'onion') - ?.address as T.OnionHostname | undefined + const onionHostname = hostnameInfo.find(h => h.kind === 'onion') + ?.hostname as T.OnionHostname | undefined - if (!onionHostname) - throw new Error('Expecting that there is an onion hostname') - - if (this.isTor() && onionHostname) { - url.hostname = onionHostname.value - } - // TODO Handle single - // else { - // const ipHostname = host.addresses.find(h => h.kind === 'ip') - // ?.hostname as T.ExportedIpHostname - - // if (!ipHostname) return '' - - // url.hostname = this.hostname - // url.port = String(ipHostname.sslPort || ipHostname.port) - // } + if (this.isTor() && onionHostname) { + url.hostname = onionHostname.value } else { - throw new Error('unimplemented') - // const hostname = {} as T.ExportedHostnameInfo // host.hostname + const ipHostname = hostnameInfo.find(h => h.kind === 'ip')?.hostname as + | T.IpHostname + | undefined - // if (!hostname) return '' + if (!ipHostname) return '' - // if (this.isTor() && hostname.kind === 'onion') { - // url.hostname = (hostname.hostname as T.ExportedOnionHostname).value - // } else { - // url.hostname = this.hostname - // url.port = String(hostname.hostname.sslPort || hostname.hostname.port) - // } + url.hostname = this.hostname + url.port = String(ipHostname.sslPort || ipHostname.port) } return url.href diff --git a/web/projects/ui/src/app/services/ui-launcher.service.ts b/web/projects/ui/src/app/services/ui-launcher.service.ts index 9c36036f3..7c3475fc0 100644 --- a/web/projects/ui/src/app/services/ui-launcher.service.ts +++ b/web/projects/ui/src/app/services/ui-launcher.service.ts @@ -13,11 +13,12 @@ export class UiLauncherService { private readonly config: ConfigService, ) {} - launch(interfaces: PackageDataEntry['serviceInterfaces']): void { - // TODO @Matt - const host = {} as any + launch( + interfaces: PackageDataEntry['serviceInterfaces'], + hosts: PackageDataEntry['hosts'], + ): void { this.windowRef.open( - this.config.launchableAddress(interfaces, host), + this.config.launchableAddress(interfaces, hosts), '_blank', 'noreferrer', ) From 2a1fd16849a50132a42fee2b644d3af7f9779136 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Thu, 25 Jul 2024 17:07:30 -0600 Subject: [PATCH 078/125] curl fail and show error --- container-runtime/download-base-image.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/container-runtime/download-base-image.sh b/container-runtime/download-base-image.sh index f7dc4c844..7650fccb1 100755 --- a/container-runtime/download-base-image.sh +++ b/container-runtime/download-base-image.sh @@ -16,4 +16,4 @@ elif [ "$_ARCH" = "aarch64" ]; then _ARCH=arm64 fi -curl https://images.linuxcontainers.org/$(curl --silent https://images.linuxcontainers.org/meta/1.0/index-system | grep "^$DISTRO;$VERSION;$_ARCH;$FLAVOR;" | head -n1 | sed 's/^.*;//g')/rootfs.squashfs --output debian.${ARCH}.squashfs \ No newline at end of file +curl -fsS https://images.linuxcontainers.org/$(curl -fsS https://images.linuxcontainers.org/meta/1.0/index-system | grep "^$DISTRO;$VERSION;$_ARCH;$FLAVOR;" | head -n1 | sed 's/^.*;//g')/rootfs.squashfs --output debian.${ARCH}.squashfs \ No newline at end of file From 64315df85f55f9963bffe78fcb87e176759bed5f Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Thu, 25 Jul 2024 17:34:48 -0600 Subject: [PATCH 079/125] log url for download --- container-runtime/download-base-image.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/container-runtime/download-base-image.sh b/container-runtime/download-base-image.sh index 7650fccb1..7fb134f31 100755 --- a/container-runtime/download-base-image.sh +++ b/container-runtime/download-base-image.sh @@ -16,4 +16,8 @@ elif [ "$_ARCH" = "aarch64" ]; then _ARCH=arm64 fi -curl -fsS https://images.linuxcontainers.org/$(curl -fsS https://images.linuxcontainers.org/meta/1.0/index-system | grep "^$DISTRO;$VERSION;$_ARCH;$FLAVOR;" | head -n1 | sed 's/^.*;//g')/rootfs.squashfs --output debian.${ARCH}.squashfs \ No newline at end of file +URL="https://images.linuxcontainers.org/$(curl -fsSL https://images.linuxcontainers.org/meta/1.0/index-system | grep "^$DISTRO;$VERSION;$_ARCH;$FLAVOR;" | head -n1 | sed 's/^.*;//g')/rootfs.squashfs" + +echo "Downloading $URL to debian.${ARCH}.squashfs" + +curl -fsSL "$URL" > debian.${ARCH}.squashfs \ No newline at end of file From e4782dee68bb724d6a6b84c532a0fabd84fe7626 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 26 Jul 2024 01:41:11 -0600 Subject: [PATCH 080/125] fix ca cert issue --- build/lib/scripts/enable-kiosk | 21 ++++++++++----------- core/startos/src/init.rs | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/build/lib/scripts/enable-kiosk b/build/lib/scripts/enable-kiosk index ad7cd4bf3..45bed5fe9 100755 --- a/build/lib/scripts/enable-kiosk +++ b/build/lib/scripts/enable-kiosk @@ -14,14 +14,8 @@ if ! id kiosk; then useradd -s /bin/bash --create-home kiosk fi -# create kiosk script -cat > /home/kiosk/kiosk.sh << 'EOF' -#!/bin/sh -PROFILE=$(mktemp -d) -if [ -f /usr/local/share/ca-certificates/startos-root-ca.crt ]; then - certutil -A -n "StartOS Local Root CA" -t "TCu,Cuw,Tuw" -i /usr/local/share/ca-certificates/startos-root-ca.crt -d $PROFILE -fi -cat >> $PROFILE/prefs.js << EOT +mkdir /home/kiosk/fx-profile +cat >> /home/kiosk/fx-profile/prefs.js << EOF user_pref("app.normandy.api_url", ""); user_pref("app.normandy.enabled", false); user_pref("app.shield.optoutstudies.enabled", false); @@ -87,7 +81,11 @@ user_pref("toolkit.telemetry.shutdownPingSender.enabled", false); user_pref("toolkit.telemetry.unified", false); user_pref("toolkit.telemetry.updatePing.enabled", false); user_pref("toolkit.telemetry.cachedClientID", ""); -EOT +EOF + +# create kiosk script +cat > /home/kiosk/kiosk.sh << 'EOF' +#!/bin/sh while ! curl "http://localhost" > /dev/null; do sleep 1 done @@ -101,8 +99,7 @@ done killall firefox-esr ) & matchbox-window-manager -use_titlebar no & -firefox-esr http://localhost --profile $PROFILE -rm -rf $PROFILE +firefox-esr http://localhost --profile /home/kiosk/fx-profile EOF chmod +x /home/kiosk/kiosk.sh @@ -116,6 +113,8 @@ fi EOF fi +chown -R kiosk:kiosk /home/kiosk + # enable autologin mkdir -p /etc/systemd/system/getty@tty1.service.d cat > /etc/systemd/system/getty@tty1.service.d/autologin.conf << 'EOF' diff --git a/core/startos/src/init.rs b/core/startos/src/init.rs index 735ca85a5..5e0fa932f 100644 --- a/core/startos/src/init.rs +++ b/core/startos/src/init.rs @@ -398,6 +398,20 @@ pub async fn init( Command::new("update-ca-certificates") .invoke(crate::ErrorKind::OpenSsl) .await?; + if tokio::fs::metadata("/home/kiosk/profile").await.is_ok() { + Command::new("certutil") + .arg("-A") + .arg("-n") + .arg("StartOS Local Root CA") + .arg("-t") + .arg("TCu,Cuw,Tuw") + .arg("-i") + .arg("/usr/local/share/ca-certificates/startos-root-ca.crt") + .arg("-d") + .arg("/home/kiosk/fx-profile") + .invoke(ErrorKind::OpenSsl) + .await?; + } load_ca_cert.complete(); load_wifi.start(); @@ -422,6 +436,12 @@ pub async fn init( tokio::fs::remove_dir_all(&tmp_var).await?; } crate::disk::mount::util::bind(&tmp_var, "/var/tmp", false).await?; + let downloading = cfg + .datadir() + .join(format!("package-data/archive/downloading")); + if tokio::fs::metadata(&downloading).await.is_ok() { + tokio::fs::remove_dir_all(&downloading).await?; + } let tmp_docker = cfg .datadir() .join(format!("package-data/tmp/{CONTAINER_TOOL}")); From a743785faf26eb6144847240be6752914b72f2cb Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 26 Jul 2024 01:42:10 -0600 Subject: [PATCH 081/125] cleanup on uninstall --- core/startos/src/db/model/package.rs | 12 +++++ core/startos/src/service/mod.rs | 70 +++++++++++++++++++++++----- 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/core/startos/src/db/model/package.rs b/core/startos/src/db/model/package.rs index e3b6e613b..cb537a2b5 100644 --- a/core/startos/src/db/model/package.rs +++ b/core/startos/src/db/model/package.rs @@ -63,6 +63,18 @@ impl PackageState { )), } } + pub fn expect_removing(&self) -> Result<&InstalledState, Error> { + match self { + Self::Removing(a) => Ok(a), + _ => Err(Error::new( + eyre!( + "Package {} is not in removing state", + self.as_manifest(ManifestPreference::Old).id + ), + ErrorKind::InvalidRequest, + )), + } + } pub fn into_installing_info(self) -> Option { match self { Self::Installing(InstallingState { installing_info }) diff --git a/core/startos/src/service/mod.rs b/core/startos/src/service/mod.rs index efaa28d4e..2beb5c9fa 100644 --- a/core/startos/src/service/mod.rs +++ b/core/startos/src/service/mod.rs @@ -29,9 +29,8 @@ use crate::s9pk::S9pk; use crate::service::service_map::InstallProgressHandles; use crate::status::health_check::HealthCheckResult; use crate::util::actor::concurrent::ConcurrentActor; -use crate::util::actor::Actor; use crate::util::io::create_file; -use crate::util::serde::Pem; +use crate::util::serde::{NoOutput, Pem}; use crate::util::Never; use crate::volume::data_dir; @@ -80,7 +79,7 @@ impl ServiceRef { ) -> Result<(), Error> { self.seed .persistent_container - .execute( + .execute::( Guid::new(), ProcedureName::Uninit, to_value(&target_version)?, @@ -90,10 +89,60 @@ impl ServiceRef { let id = self.seed.persistent_container.s9pk.as_manifest().id.clone(); let ctx = self.seed.ctx.clone(); self.shutdown().await?; + if target_version.is_none() { - ctx.db - .mutate(|d| d.as_public_mut().as_package_data_mut().remove(&id)) - .await?; + if let Some(pde) = ctx + .db + .mutate(|d| { + if let Some(pde) = d + .as_public_mut() + .as_package_data_mut() + .remove(&id)? + .map(|d| d.de()) + .transpose()? + { + d.as_private_mut().as_available_ports_mut().mutate(|p| { + p.free( + pde.hosts + .0 + .values() + .flat_map(|h| h.bindings.values()) + .flat_map(|b| { + b.lan + .assigned_port + .into_iter() + .chain(b.lan.assigned_ssl_port) + }), + ); + Ok(()) + })?; + Ok(Some(pde)) + } else { + Ok(None) + } + }) + .await? + { + let state = pde.state_info.expect_removing()?; + for volume_id in &state.manifest.volumes { + let path = data_dir(&ctx.datadir, &state.manifest.id, volume_id); + if tokio::fs::metadata(&path).await.is_ok() { + tokio::fs::remove_dir_all(&path).await?; + } + } + let logs_dir = ctx.datadir.join("logs").join(&state.manifest.id); + if tokio::fs::metadata(&logs_dir).await.is_ok() { + tokio::fs::remove_dir_all(&logs_dir).await?; + } + let archive_path = ctx + .datadir + .join("archive") + .join("installed") + .join(&state.manifest.id); + if tokio::fs::metadata(&archive_path).await.is_ok() { + tokio::fs::remove_file(&archive_path).await?; + } + } } Ok(()) } @@ -187,10 +236,9 @@ impl Service { let ctx = ctx.clone(); move |s9pk: S9pk, i: Model| async move { for volume_id in &s9pk.as_manifest().volumes { - let tmp_path = - data_dir(&ctx.datadir, &s9pk.as_manifest().id.clone(), volume_id); - if tokio::fs::metadata(&tmp_path).await.is_err() { - tokio::fs::create_dir_all(&tmp_path).await?; + let path = data_dir(&ctx.datadir, &s9pk.as_manifest().id, volume_id); + if tokio::fs::metadata(&path).await.is_err() { + tokio::fs::create_dir_all(&path).await?; } } let start_stop = if i.as_status().as_main().de()?.running() { @@ -368,7 +416,7 @@ impl Service { service .seed .persistent_container - .execute( + .execute::( Guid::new(), ProcedureName::Init, to_value(&src_version)?, From dfb7658c3ef74cfd936477e94e21f42365cca3b7 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 26 Jul 2024 01:43:46 -0600 Subject: [PATCH 082/125] implement mount for dependencies --- .../startos/src/service/effects/dependency.rs | 47 +++++++++++++++++-- core/startos/src/service/effects/image.rs | 5 +- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/core/startos/src/service/effects/dependency.rs b/core/startos/src/service/effects/dependency.rs index dfc8795f0..34619ebd4 100644 --- a/core/startos/src/service/effects/dependency.rs +++ b/core/startos/src/service/effects/dependency.rs @@ -7,14 +7,20 @@ use exver::VersionRange; use itertools::Itertools; use models::{HealthCheckId, PackageId, VolumeId}; use patch_db::json_ptr::JsonPointer; +use tokio::process::Command; use crate::db::model::package::{ CurrentDependencies, CurrentDependencyInfo, CurrentDependencyKind, ManifestPreference, }; +use crate::disk::mount::filesystem::bind::Bind; +use crate::disk::mount::filesystem::idmapped::IdMapped; +use crate::disk::mount::filesystem::{FileSystem, MountType}; use crate::rpc_continuations::Guid; use crate::service::effects::prelude::*; use crate::status::health_check::HealthCheckResult; use crate::util::clap::FromStrParser; +use crate::util::Invoke; +use crate::volume::data_dir; #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[ts(export)] @@ -29,7 +35,7 @@ pub struct MountTarget { #[ts(export)] #[serde(rename_all = "camelCase")] pub struct MountParams { - location: String, + location: PathBuf, target: MountTarget, } pub async fn mount( @@ -45,8 +51,43 @@ pub async fn mount( }, }: MountParams, ) -> Result<(), Error> { - // TODO - todo!() + let context = context.deref()?; + let subpath = subpath.unwrap_or_default(); + let subpath = subpath.strip_prefix("/").unwrap_or(&subpath); + let source = data_dir(&context.seed.ctx.datadir, &package_id, &volume_id).join(subpath); + if readonly && tokio::fs::metadata(&source).await.is_err() { + return Err(Error::new( + eyre!("{volume_id}/{} does not exist", subpath.display()), + ErrorKind::NotFound, + )); + } + let location = location.strip_prefix("/").unwrap_or(&location); + let mountpoint = context + .seed + .persistent_container + .lxc_container + .get() + .or_not_found("lxc container")? + .rootfs_dir() + .join(location); + tokio::fs::create_dir_all(&mountpoint).await?; + Command::new("chown") + .arg("100000:100000") + .arg(&mountpoint) + .invoke(crate::ErrorKind::Filesystem) + .await?; + IdMapped::new(Bind::new(source), 0, 100000, 65536) + .mount( + mountpoint, + if readonly { + MountType::ReadOnly + } else { + MountType::ReadWrite + }, + ) + .await?; + + Ok(()) } pub async fn get_installed_packages(context: EffectContext) -> Result, Error> { diff --git a/core/startos/src/service/effects/image.rs b/core/startos/src/service/effects/image.rs index 69c516ce5..af62047ed 100644 --- a/core/startos/src/service/effects/image.rs +++ b/core/startos/src/service/effects/image.rs @@ -83,15 +83,16 @@ pub async fn destroy_overlayed_image( DestroyOverlayedImageParams { guid }: DestroyOverlayedImageParams, ) -> Result<(), Error> { let context = context.deref()?; - if context + if let Some(overlay) = context .seed .persistent_container .overlays .lock() .await .remove(&guid) - .is_none() { + overlay.unmount(true).await?; + } else { tracing::warn!("Could not find a guard to remove on the destroy overlayed image; assumming that it already is removed and will be skipping"); } Ok(()) From 2754302fb710de8fb41fad2a15982a0b0812daad Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 26 Jul 2024 02:02:58 -0600 Subject: [PATCH 083/125] standardize result type for sideload progress --- core/startos/src/install/mod.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/core/startos/src/install/mod.rs b/core/startos/src/install/mod.rs index 394136024..8d4f65312 100644 --- a/core/startos/src/install/mod.rs +++ b/core/startos/src/install/mod.rs @@ -12,7 +12,7 @@ use itertools::Itertools; use models::VersionString; use reqwest::header::{HeaderMap, CONTENT_LENGTH}; use reqwest::Url; -use rpc_toolkit::yajrc::RpcError; +use rpc_toolkit::yajrc::{GenericRpcMethod, RpcError}; use rpc_toolkit::HandlerArgs; use rustyline_async::ReadlineEvent; use serde::{Deserialize, Serialize}; @@ -202,11 +202,12 @@ pub async fn sideload( use axum::extract::ws::Message; async move { if let Err(e) = async { + type RpcResponse = rpc_toolkit::yajrc::RpcResponse::>; tokio::select! { res = async { while let Some(progress) = progress_listener.next().await { ws.send(Message::Text( - serde_json::to_string(&Ok::<_, ()>(progress)) + serde_json::to_string(&RpcResponse::from_result::(Ok(progress))) .with_kind(ErrorKind::Serialization)?, )) .await @@ -217,7 +218,7 @@ pub async fn sideload( err = err_recv => { if let Ok(e) = err { ws.send(Message::Text( - serde_json::to_string(&Err::<(), _>(e)) + serde_json::to_string(&RpcResponse::from_result::(Err(e))) .with_kind(ErrorKind::Serialization)?, )) .await @@ -406,14 +407,18 @@ pub async fn cli_install( let mut progress = FullProgress::new(); + type RpcResponse = rpc_toolkit::yajrc::RpcResponse< + GenericRpcMethod<&'static str, (), FullProgress>, + >; + loop { tokio::select! { msg = ws.next() => { if let Some(msg) = msg { if let Message::Text(t) = msg.with_kind(ErrorKind::Network)? { progress = - serde_json::from_str::>(&t) - .with_kind(ErrorKind::Deserialization)??; + serde_json::from_str::(&t) + .with_kind(ErrorKind::Deserialization)?.result?; bar.update(&progress); } } else { From 078bf4102923587989b53c38036cd07fb96f33b6 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 26 Jul 2024 02:07:39 -0600 Subject: [PATCH 084/125] bump version --- core/Cargo.lock | 251 +++++++++--------- core/startos/Cargo.toml | 2 +- core/startos/src/version/mod.rs | 3 +- core/startos/src/version/v0_3_6_alpha_1.rs | 35 +++ web/package.json | 2 +- web/patchdb-ui-seed.json | 3 +- .../modals/os-welcome/os-welcome.page.html | 2 +- 7 files changed, 173 insertions(+), 125 deletions(-) create mode 100644 core/startos/src/version/v0_3_6_alpha_1.rs diff --git a/core/Cargo.lock b/core/Cargo.lock index a3464c7dc..69d23b92c 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -107,9 +107,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.14" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" dependencies = [ "anstyle", "anstyle-parse", @@ -122,33 +122,33 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -162,9 +162,9 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arrayref" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" +checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" [[package]] name = "arrayvec" @@ -200,9 +200,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5" +checksum = "fec134f64e2bc57411226dfc4e52dec859ddfc7e711fc5e07b612584f000e4aa" dependencies = [ "brotli", "flate2", @@ -231,7 +231,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -242,7 +242,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -268,9 +268,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "aws-lc-rs" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a47f2fb521b70c11ce7369a6c5fa4bd6af7e5d62ec06303875bafe7c6ba245" +checksum = "4ae74d9bd0a7530e8afd1770739ad34b36838829d6ad61818f9230f683f5ad77" dependencies = [ "aws-lc-sys", "mirai-annotations", @@ -280,9 +280,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2927c7af777b460b7ccd95f8b67acd7b4c04ec8896bf0c8e80ba30523cffc057" +checksum = "2e89b6941c2d1a7045538884d6e760ccfffdf8e1ffc2613d8efa74305e1f3752" dependencies = [ "bindgen", "cc", @@ -532,7 +532,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.71", + "syn 2.0.72", "which", ] @@ -687,9 +687,9 @@ checksum = "981520c98f422fcc584dc1a95c334e6953900b9106bc47a9839b81790009eb21" [[package]] name = "cc" -version = "1.1.5" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324c74f2155653c90b04f25b2a47a8a631360cb908f92a772695f430c7e31052" +checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" dependencies = [ "jobserver", "libc", @@ -799,9 +799,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.9" +version = "4.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" +checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3" dependencies = [ "clap_builder", "clap_derive", @@ -809,9 +809,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.9" +version = "4.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" +checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa" dependencies = [ "anstream", "anstyle", @@ -821,21 +821,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.8" +version = "4.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" +checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] name = "clap_lex" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "cmake" @@ -875,9 +875,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "concurrent-queue" @@ -956,18 +956,18 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const_format" -version = "0.2.32" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" +checksum = "c990efc7a285731f9a4378d81aff2f0e85a2c8781a05ef0f8baa8dac54d0ff48" dependencies = [ "const_format_proc_macros", ] [[package]] name = "const_format_proc_macros" -version = "0.2.32" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" +checksum = "e026b6ce194a874cb9cf32cd5772d1ef9767cc8fcb5765948d74f37a9d8b2bf6" dependencies = [ "proc-macro2", "quote", @@ -1125,7 +1125,7 @@ dependencies = [ "crossterm_winapi", "futures-core", "libc", - "mio", + "mio 0.8.11", "parking_lot", "signal-hook", "signal-hook-mio", @@ -1236,7 +1236,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -1260,7 +1260,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -1271,7 +1271,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -1302,7 +1302,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -1325,7 +1325,7 @@ checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -1348,7 +1348,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -1570,7 +1570,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -1828,7 +1828,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -2236,7 +2236,7 @@ dependencies = [ "http 1.1.0", "hyper 1.4.1", "hyper-util", - "rustls 0.23.11", + "rustls 0.23.12", "rustls-pki-types", "tokio", "tokio-rustls", @@ -2515,9 +2515,9 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.0" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "isocountry" @@ -2620,21 +2620,21 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] [[package]] name = "josekit" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0953340cf63354cec4a385f1fbcb3f409a5823778cae236078892f6030ed4565" +checksum = "54b85e2125819afc4fd2ae57416207e792c7e12797858e5db2a6c6f24a166829" dependencies = [ "anyhow", - "base64 0.21.7", + "base64 0.22.1", "flate2", "once_cell", "openssl", @@ -2777,9 +2777,9 @@ checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", "windows-targets 0.52.6", @@ -2955,6 +2955,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" +dependencies = [ + "hermit-abi", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + [[package]] name = "mirai-annotations" version = "1.12.0" @@ -3229,7 +3241,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -3274,9 +3286,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.64" +version = "0.10.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" dependencies = [ "bitflags 2.6.0", "cfg-if", @@ -3295,7 +3307,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -3315,9 +3327,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.102" +version = "0.9.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" dependencies = [ "cc", "libc", @@ -3502,7 +3514,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -3558,7 +3570,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -3602,9 +3614,9 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "portable-atomic" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" [[package]] name = "powerfmt" @@ -3631,7 +3643,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -3703,7 +3715,7 @@ checksum = "6ff7ff745a347b87471d859a377a9a404361e7efc2a971d73424a6d183c0fc77" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -3726,7 +3738,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -4181,15 +4193,15 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.11" +version = "0.23.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" +checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" dependencies = [ "aws-lc-rs", "log", "once_cell", "rustls-pki-types", - "rustls-webpki 0.102.5", + "rustls-webpki 0.102.6", "subtle", "zeroize", ] @@ -4231,9 +4243,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.5" +version = "0.102.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" +checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" dependencies = [ "aws-lc-rs", "ring", @@ -4395,7 +4407,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -4422,9 +4434,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] @@ -4468,7 +4480,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -4571,7 +4583,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" dependencies = [ "libc", - "mio", + "mio 0.8.11", "signal-hook", ] @@ -4880,9 +4892,9 @@ dependencies = [ [[package]] name = "sscanf" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c713ebd15ce561dd4a13ed62bc2a0368e16806fc30dcaf66ecf1256b2a3fdde6" +checksum = "a147d3cf7e723671ed11355b5b008c8019195f7fc902e213f5557d931e9f839d" dependencies = [ "const_format", "lazy_static", @@ -4892,16 +4904,16 @@ dependencies = [ [[package]] name = "sscanf_macro" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84955aa74a157e5834d58a07be11af7f0ab923f0194a0bb2ea6b3db8b5d1611d" +checksum = "af3a37bdf8e90e77cc60f74473edf28d922ae2eacdd595e67724ccd2381774cc" dependencies = [ "convert_case 0.6.0", "proc-macro2", "quote", "regex-syntax 0.6.29", "strsim 0.10.0", - "syn 2.0.71", + "syn 2.0.72", "unicode-width", ] @@ -4949,7 +4961,7 @@ dependencies = [ [[package]] name = "start-os" -version = "0.3.6-alpha.0" +version = "0.3.6-alpha.1" dependencies = [ "aes", "async-compression", @@ -5060,7 +5072,7 @@ dependencies = [ "tokio-tar", "tokio-tungstenite 0.23.1", "tokio-util", - "toml 0.8.15", + "toml 0.8.16", "torut", "tower-service", "tracing", @@ -5138,9 +5150,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.71" +version = "2.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" dependencies = [ "proc-macro2", "quote", @@ -5267,7 +5279,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -5348,22 +5360,21 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.1" +version = "1.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" +checksum = "d040ac2b29ab03b09d4129c2f5bbd012a3ac2f79d38ff506a4bf8dd34b0eac8a" dependencies = [ "backtrace", "bytes", "libc", - "mio", - "num_cpus", + "mio 1.0.1", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "tracing", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -5378,13 +5389,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -5403,7 +5414,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.11", + "rustls 0.23.12", "rustls-pki-types", "tokio", ] @@ -5499,21 +5510,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28" +checksum = "81967dd0dd2c1ab0bc3468bd7caecc32b8a4aa47d0c8c695d8c2b2108168d62c" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.16", + "toml_edit 0.22.17", ] [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "f8fb9f64314842840f1d940ac544da178732128f1c78c21772e876579e0da1db" dependencies = [ "serde", ] @@ -5544,15 +5555,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.16" +version = "0.22.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788" +checksum = "8d9f8729f5aea9562aac1cc0441f5d6de3cff1ee0c5d67293eeca5eb36ee7c16" dependencies = [ "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.13", + "winnow 0.6.16", ] [[package]] @@ -5653,7 +5664,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -5805,7 +5816,7 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", "termcolor", ] @@ -5865,7 +5876,7 @@ checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -5936,9 +5947,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "unicode-xid" @@ -6011,9 +6022,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wait-timeout" @@ -6082,7 +6093,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", "wasm-bindgen-shared", ] @@ -6116,7 +6127,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6368,9 +6379,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.13" +version = "0.6.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" +checksum = "b480ae9340fc261e6be3e95a1ba86d54ae3f9171132a73ce8d4bbaf68339507c" dependencies = [ "memchr", ] @@ -6470,7 +6481,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -6490,7 +6501,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index 2e6df1e3d..5e84aac5e 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -14,7 +14,7 @@ keywords = [ name = "start-os" readme = "README.md" repository = "https://github.com/Start9Labs/start-os" -version = "0.3.6-alpha.0" +version = "0.3.6-alpha.1" license = "MIT" [lib] diff --git a/core/startos/src/version/mod.rs b/core/startos/src/version/mod.rs index 9ad571759..09d438344 100644 --- a/core/startos/src/version/mod.rs +++ b/core/startos/src/version/mod.rs @@ -14,8 +14,9 @@ mod v0_3_5; mod v0_3_5_1; mod v0_3_5_2; mod v0_3_6_alpha_0; +mod v0_3_6_alpha_1; -pub type Current = v0_3_6_alpha_0::Version; +pub type Current = v0_3_6_alpha_1::Version; #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[serde(untagged)] diff --git a/core/startos/src/version/v0_3_6_alpha_1.rs b/core/startos/src/version/v0_3_6_alpha_1.rs new file mode 100644 index 000000000..8f40c3fde --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_1.rs @@ -0,0 +1,35 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_0, VersionT}; +use crate::db::model::Database; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref V0_3_6_alpha_1: exver::Version = exver::Version::new( + [0, 3, 6], + [PreReleaseSegment::String("alpha".into()), 1.into()] + ); +} + +#[derive(Clone, Debug)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_0::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> exver::Version { + V0_3_6_alpha_1.clone() + } + fn compat(&self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + async fn up(&self, _db: &TypedPatchDb) -> Result<(), Error> { + Ok(()) + } + async fn down(&self, _db: &TypedPatchDb) -> Result<(), Error> { + Ok(()) + } +} diff --git a/web/package.json b/web/package.json index 6f72a68ec..f20272d99 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "startos-ui", - "version": "0.3.6-alpha.0", + "version": "0.3.6-alpha.1", "author": "Start9 Labs, Inc", "homepage": "https://start9.com/", "license": "MIT", diff --git a/web/patchdb-ui-seed.json b/web/patchdb-ui-seed.json index 9626d2558..15e020cb3 100644 --- a/web/patchdb-ui-seed.json +++ b/web/patchdb-ui-seed.json @@ -20,5 +20,6 @@ }, "ackInstructions": {}, "theme": "Dark", - "widgets": [] + "widgets": [], + "ack-welcome": "0.3.6-alpha.1" } diff --git a/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html b/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html index 3def4478b..d53a0746d 100644 --- a/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html +++ b/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html @@ -12,7 +12,7 @@

This Release

-

0.3.6-alpha.0

+

0.3.6-alpha.1

This is an ALPHA release! DO NOT use for production data!
Expect that any data you create or store on this version of the OS can be LOST FOREVER!
From 8aecec0b9a86a37ad9e6f0dc5ab46ac79abaa005 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 26 Jul 2024 11:09:46 -0600 Subject: [PATCH 085/125] fix canonicalization --- core/helpers/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/helpers/src/lib.rs b/core/helpers/src/lib.rs index d913aefee..80631fea2 100644 --- a/core/helpers/src/lib.rs +++ b/core/helpers/src/lib.rs @@ -50,7 +50,8 @@ pub async fn canonicalize( } let path = path.as_ref(); if tokio::fs::metadata(path).await.is_err() { - if let (Some(parent), Some(file_name)) = (path.parent(), path.file_name()) { + let parent = path.parent().unwrap_or(Path::new(".")); + if let Some(file_name) = path.file_name() { if create_parent && tokio::fs::metadata(parent).await.is_err() { return Ok(create_canonical_folder(parent).await?.join(file_name)); } else { From e0b47feb8bb13b35ab4bccdf5dfb99fa4651f016 Mon Sep 17 00:00:00 2001 From: Jade <2364004+Blu-J@users.noreply.github.com> Date: Fri, 26 Jul 2024 11:19:16 -0600 Subject: [PATCH 086/125] Fixing: Some getConfigs where breaking in new system (#2685) --- Makefile | 1 + container-runtime/jest.config.js | 8 + container-runtime/package-lock.json | 5670 ++++++++++++++++- container-runtime/package.json | 10 +- .../__fixtures__/embasyPagesConfig.ts | 127 + .../SystemForEmbassy/__fixtures__/searNXG.ts | 39 + .../transformConfigSpec.test.ts.snap | 253 + .../transformConfigSpec.test.ts | 23 + .../SystemForEmbassy/transformConfigSpec.ts | 9 +- container-runtime/tsconfig.json | 5 +- 10 files changed, 6083 insertions(+), 62 deletions(-) create mode 100644 container-runtime/jest.config.js create mode 100644 container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/embasyPagesConfig.ts create mode 100644 container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/searNXG.ts create mode 100644 container-runtime/src/Adapters/Systems/SystemForEmbassy/__snapshots__/transformConfigSpec.test.ts.snap create mode 100644 container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.test.ts diff --git a/Makefile b/Makefile index 4fb5033d0..90c00ae2d 100644 --- a/Makefile +++ b/Makefile @@ -92,6 +92,7 @@ format: test: $(CORE_SRC) $(ENVIRONMENT_FILE) (cd core && cargo build --features=test && cargo test --features=test) (cd sdk && make test) + (cd container-runtime && npm test) cli: cd core && ./install-cli.sh diff --git a/container-runtime/jest.config.js b/container-runtime/jest.config.js new file mode 100644 index 000000000..f499f03f9 --- /dev/null +++ b/container-runtime/jest.config.js @@ -0,0 +1,8 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: "ts-jest", + automock: false, + testEnvironment: "node", + rootDir: "./src/", + modulePathIgnorePatterns: ["./dist/"], +} diff --git a/container-runtime/package-lock.json b/container-runtime/package-lock.json index e63bec6a1..eb7778c83 100644 --- a/container-runtime/package-lock.json +++ b/container-runtime/package-lock.json @@ -15,6 +15,7 @@ "esbuild-plugin-resolve": "^2.0.0", "filebrowser": "^1.0.0", "isomorphic-fetch": "^3.0.0", + "jest": "^29.7.0", "lodash.merge": "^4.6.2", "node-fetch": "^3.1.0", "ts-matches": "^5.5.1", @@ -25,14 +26,16 @@ "devDependencies": { "@swc/cli": "^0.1.62", "@swc/core": "^1.3.65", + "@types/jest": "^29.5.12", "@types/node": "^20.11.13", "prettier": "^3.2.5", + "ts-jest": "^29.2.3", "typescript": ">5.2" } }, "../sdk/dist": { "name": "@start9labs/start-sdk", - "version": "0.3.6-alpha5", + "version": "0.3.6-alpha6", "license": "MIT", "dependencies": { "@iarna/toml": "^2.2.5", @@ -48,17 +51,944 @@ "@types/jest": "^29.4.0", "@types/lodash.merge": "^4.6.2", "jest": "^29.4.3", + "peggy": "^3.0.2", "prettier": "^3.2.5", "ts-jest": "^29.0.5", "ts-node": "^10.9.1", + "ts-pegjs": "^4.2.1", "tsx": "^4.7.1", "typescript": "^5.0.4" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.9.tgz", + "integrity": "sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", + "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.9", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-module-transforms": "^7.24.9", + "@babel/helpers": "^7.24.8", + "@babel/parser": "^7.24.8", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.10.tgz", + "integrity": "sha512-o9HBZL1G2129luEUlG1hB4N/nlYNWHnpwlND9eOMclRqqu1YDy2sSYVCFUZwl8I1Gxh+QSRrP2vD7EpUmFVXxg==", + "dependencies": { + "@babel/types": "^7.24.9", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz", + "integrity": "sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==", + "dependencies": { + "@babel/compat-data": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.9.tgz", + "integrity": "sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.8.tgz", + "integrity": "sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==", + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.8.tgz", + "integrity": "sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.8.tgz", + "integrity": "sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.8", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.8", + "@babel/types": "^7.24.8", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/@babel/types": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.9.tgz", + "integrity": "sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==", + "dependencies": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" + }, "node_modules/@iarna/toml": { "version": "2.2.5", "license": "ISC" }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@mole-inc/bin-wrapper": { "version": "8.0.1", "dev": true, @@ -131,6 +1061,11 @@ "node": ">= 8" } }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "dev": true, @@ -142,6 +1077,22 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, "node_modules/@start9labs/start-sdk": { "resolved": "../sdk/dist", "link": true @@ -273,6 +1224,43 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "dev": true, @@ -284,11 +1272,50 @@ "@types/responselike": "^1.0.0" } }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "dev": true, "license": "MIT" }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "node_modules/@types/keyv": { "version": "3.1.4", "dev": true, @@ -299,7 +1326,6 @@ }, "node_modules/@types/node": { "version": "20.14.2", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~5.26.4" @@ -313,6 +1339,24 @@ "@types/node": "*" } }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" + }, + "node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" + }, "node_modules/accepts": { "version": "1.3.8", "license": "MIT", @@ -324,6 +1368,54 @@ "node": ">= 0.6" } }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/arch": { "version": "2.2.0", "dev": true, @@ -343,13 +1435,135 @@ ], "license": "MIT" }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "license": "MIT" }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", - "dev": true, "license": "MIT" }, "node_modules/bin-check": { @@ -536,7 +1750,6 @@ }, "node_modules/braces": { "version": "3.0.3", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -545,6 +1758,62 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.23.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", + "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001640", + "electron-to-chromium": "^1.4.820", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, "node_modules/bytes": { "version": "3.1.2", "license": "MIT", @@ -608,6 +1877,96 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001643", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz", + "integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/clone-response": { "version": "1.0.3", "dev": true, @@ -619,6 +1978,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, "node_modules/commander": { "version": "7.2.0", "dev": true, @@ -627,6 +2016,11 @@ "node": ">= 10" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, "node_modules/content-disposition": { "version": "0.5.4", "license": "MIT", @@ -644,6 +2038,11 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, "node_modules/cookie": { "version": "0.6.0", "license": "MIT", @@ -655,6 +2054,26 @@ "version": "1.0.6", "license": "MIT" }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/cross-spawn": { "version": "5.1.0", "dev": true, @@ -704,6 +2123,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/defer-to-connect": { "version": "2.0.1", "dev": true, @@ -742,10 +2182,62 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/ee-first": { "version": "1.1.1", "license": "MIT" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.2.tgz", + "integrity": "sha512-kc4r3U3V3WLaaZqThjYz/Y6z8tJe+7K0bbjUVo3i+LWIypVdMx5nXCkwRe6SWbY6ILqLdc1rKcKmr3HoH7wjSQ==" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, "node_modules/encodeurl": { "version": "1.0.2", "license": "MIT", @@ -761,6 +2253,14 @@ "once": "^1.4.0" } }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-define-property": { "version": "1.0.0", "license": "MIT", @@ -782,6 +2282,14 @@ "version": "2.0.0", "license": "MIT" }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "license": "MIT" @@ -797,6 +2305,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/etag": { "version": "1.8.1", "license": "MIT", @@ -832,6 +2352,29 @@ "node": ">=4" } }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/express": { "version": "4.19.2", "license": "MIT", @@ -910,6 +2453,11 @@ "node": ">=8.6.0" } }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, "node_modules/fastq": { "version": "1.17.1", "dev": true, @@ -918,6 +2466,14 @@ "reusify": "^1.0.4" } }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dependencies": { + "bser": "2.1.1" + } + }, "node_modules/fetch-blob": { "version": "3.2.0", "funding": [ @@ -968,6 +2524,27 @@ "version": "2.20.3", "license": "MIT" }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/filename-reserved-regex": { "version": "3.0.0", "dev": true, @@ -997,7 +2574,6 @@ }, "node_modules/fill-range": { "version": "7.1.1", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -1022,6 +2598,18 @@ "node": ">= 0.8" } }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-versions": { "version": "5.1.0", "dev": true, @@ -1060,6 +2648,24 @@ "node": ">= 0.6" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "license": "MIT", @@ -1067,6 +2673,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.4", "license": "MIT", @@ -1084,6 +2706,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/get-stream": { "version": "3.0.0", "dev": true, @@ -1092,6 +2722,26 @@ "node": ">=4" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "dev": true, @@ -1103,6 +2753,34 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, "node_modules/gopd": { "version": "1.0.1", "license": "MIT", @@ -1137,6 +2815,19 @@ "url": "https://github.com/sindresorhus/got?sponsor=1" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.2", "license": "MIT", @@ -1177,6 +2868,11 @@ "node": ">= 0.4" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "dev": true, @@ -1210,7 +2906,6 @@ }, "node_modules/human-signals": { "version": "2.1.0", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.17.0" @@ -1245,6 +2940,42 @@ ], "license": "BSD-3-Clause" }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "license": "ISC" @@ -1256,6 +2987,25 @@ "node": ">= 0.10" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-core-module": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", + "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "dev": true, @@ -1264,6 +3014,22 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/is-glob": { "version": "4.0.3", "dev": true, @@ -1277,7 +3043,6 @@ }, "node_modules/is-number": { "version": "7.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -1301,7 +3066,6 @@ }, "node_modules/isexe": { "version": "2.0.0", - "dev": true, "license": "ISC" }, "node_modules/isomorphic-fetch": { @@ -1330,11 +3094,821 @@ } } }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-changed-files/node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/jest-changed-files/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/jest-changed-files/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-changed-files/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-changed-files/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-changed-files/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-changed-files/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-changed-files/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-changed-files/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/keyv": { "version": "4.5.4", "dev": true, @@ -1343,6 +3917,44 @@ "json-buffer": "3.0.1" } }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -1365,6 +3977,34 @@ "yallist": "^2.1.2" } }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/media-typer": { "version": "0.3.0", "license": "MIT", @@ -1378,7 +4018,6 @@ }, "node_modules/merge-stream": { "version": "2.0.0", - "dev": true, "license": "MIT" }, "node_modules/merge2": { @@ -1398,7 +4037,6 @@ }, "node_modules/micromatch": { "version": "4.0.7", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -1437,7 +4075,6 @@ }, "node_modules/mimic-fn": { "version": "2.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -1469,6 +4106,11 @@ "version": "2.0.0", "license": "MIT" }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, "node_modules/negotiator": { "version": "0.6.3", "license": "MIT", @@ -1509,6 +4151,24 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/normalize-url": { "version": "6.1.0", "dev": true, @@ -1550,7 +4210,6 @@ }, "node_modules/once": { "version": "1.4.0", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -1558,7 +4217,6 @@ }, "node_modules/onetime": { "version": "5.1.2", - "dev": true, "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" @@ -1597,6 +4255,70 @@ "node": ">=4" } }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parseurl": { "version": "1.3.3", "license": "MIT", @@ -1604,6 +4326,22 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "2.0.1", "dev": true, @@ -1612,6 +4350,11 @@ "node": ">=4" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, "node_modules/path-to-regexp": { "version": "0.1.7", "license": "MIT" @@ -1628,9 +4371,13 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + }, "node_modules/picomatch": { "version": "2.3.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -1647,6 +4394,25 @@ "node": ">=0.10.0" } }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/prettier": { "version": "3.3.2", "dev": true, @@ -1661,6 +4427,42 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "license": "MIT", @@ -1686,6 +4488,21 @@ "once": "^1.3.1" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, "node_modules/qs": { "version": "6.11.0", "license": "BSD-3-Clause", @@ -1749,6 +4566,11 @@ "node": ">= 0.8" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, "node_modules/readable-stream": { "version": "3.6.2", "dev": true, @@ -1777,11 +4599,62 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-alpn": { "version": "1.2.1", "dev": true, "license": "MIT" }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "engines": { + "node": ">=10" + } + }, "node_modules/responselike": { "version": "2.0.1", "dev": true, @@ -1848,7 +4721,6 @@ }, "node_modules/semver": { "version": "7.6.2", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1977,12 +4849,15 @@ }, "node_modules/signal-exit": { "version": "3.0.7", - "dev": true, "license": "ISC" }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" + }, "node_modules/slash": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2018,6 +4893,47 @@ "node": ">= 8" } }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "engines": { + "node": ">=8" + } + }, "node_modules/statuses": { "version": "2.0.1", "license": "MIT", @@ -2033,6 +4949,50 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-eof": { "version": "1.0.0", "dev": true, @@ -2043,12 +5003,22 @@ }, "node_modules/strip-final-newline": { "version": "2.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-outer": { "version": "2.0.0", "dev": true, @@ -2076,9 +5046,76 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -2125,6 +5162,54 @@ "node": ">=12" } }, + "node_modules/ts-jest": { + "version": "29.2.3", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.3.tgz", + "integrity": "sha512-yCcfVdiBFngVz9/keHin9EnsrQtQtEu3nRykNy9RVp+FiPFFbPJ3Sg6Qg4+TkmH0vMP5qsTKgXSsk80HRwvdgQ==", + "dev": true, + "dependencies": { + "bs-logger": "0.x", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, "node_modules/ts-matches": { "version": "5.5.1", "license": "MIT" @@ -2133,6 +5218,25 @@ "version": "2.6.3", "license": "0BSD" }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "1.6.18", "license": "MIT", @@ -2158,7 +5262,6 @@ }, "node_modules/undici-types": { "version": "5.26.5", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -2168,6 +5271,35 @@ "node": ">= 0.8" } }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "dev": true, @@ -2180,6 +5312,19 @@ "node": ">= 0.4.0" } }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/vary": { "version": "1.1.2", "license": "MIT", @@ -2187,6 +5332,14 @@ "node": ">= 0.8" } }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dependencies": { + "makeerror": "1.0.12" + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "license": "MIT", @@ -2221,11 +5374,46 @@ "which": "bin/which" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", - "dev": true, "license": "ISC" }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "2.1.2", "dev": true, @@ -2240,12 +5428,741 @@ "engines": { "node": ">= 14" } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } }, "dependencies": { + "@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "requires": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + } + }, + "@babel/compat-data": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.9.tgz", + "integrity": "sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng==" + }, + "@babel/core": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", + "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.9", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-module-transforms": "^7.24.9", + "@babel/helpers": "^7.24.8", + "@babel/parser": "^7.24.8", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "dependencies": { + "debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + } + } + }, + "@babel/generator": { + "version": "7.24.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.10.tgz", + "integrity": "sha512-o9HBZL1G2129luEUlG1hB4N/nlYNWHnpwlND9eOMclRqqu1YDy2sSYVCFUZwl8I1Gxh+QSRrP2vD7EpUmFVXxg==", + "requires": { + "@babel/types": "^7.24.9", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz", + "integrity": "sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==", + "requires": { + "@babel/compat-data": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "requires": { + "yallist": "^3.0.2" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "requires": { + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "requires": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "requires": { + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "requires": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-module-transforms": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.9.tgz", + "integrity": "sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw==", + "requires": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==" + }, + "@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "requires": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "requires": { + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==" + }, + "@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==" + }, + "@babel/helper-validator-option": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==" + }, + "@babel/helpers": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.8.tgz", + "integrity": "sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==", + "requires": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.8" + } + }, + "@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "requires": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.8.tgz", + "integrity": "sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==" + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "requires": { + "@babel/helper-plugin-utils": "^7.24.7" + } + }, + "@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "requires": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + } + }, + "@babel/traverse": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.8.tgz", + "integrity": "sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==", + "requires": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.8", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.8", + "@babel/types": "^7.24.8", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "dependencies": { + "debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "@babel/types": { + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.9.tgz", + "integrity": "sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==", + "requires": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" + }, "@iarna/toml": { "version": "2.2.5" }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==" + }, + "@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "requires": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "requires": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + } + }, + "@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "requires": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + } + }, + "@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "requires": { + "jest-get-type": "^29.6.3" + } + }, + "@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "requires": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + } + }, + "@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + } + }, + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "requires": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + } + }, + "@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "requires": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "requires": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + } + }, + "@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "requires": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + } + }, + "@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "requires": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "requires": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" + }, + "@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==" + }, + "@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "@mole-inc/bin-wrapper": { "version": "8.0.1", "dev": true, @@ -2293,10 +6210,31 @@ "fastq": "^1.6.0" } }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" + }, "@sindresorhus/is": { "version": "4.6.0", "dev": true }, + "@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, "@start9labs/start-sdk": { "version": "file:../sdk/dist", "requires": { @@ -2309,10 +6247,12 @@ "jest": "^29.4.3", "lodash.merge": "^4.6.2", "mime": "^4.0.3", + "peggy": "^3.0.2", "prettier": "^3.2.5", "ts-jest": "^29.0.5", "ts-matches": "^5.5.1", "ts-node": "^10.9.1", + "ts-pegjs": "^4.2.1", "tsx": "^4.7.1", "typescript": "^5.0.4", "yaml": "^2.2.2" @@ -2381,6 +6321,43 @@ "version": "0.3.0", "dev": true }, + "@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "requires": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "requires": { + "@babel/types": "^7.20.7" + } + }, "@types/cacheable-request": { "version": "6.0.3", "dev": true, @@ -2391,10 +6368,49 @@ "@types/responselike": "^1.0.0" } }, + "@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "requires": { + "@types/node": "*" + } + }, "@types/http-cache-semantics": { "version": "4.0.4", "dev": true }, + "@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==" + }, + "@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "requires": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "@types/keyv": { "version": "3.1.4", "dev": true, @@ -2404,7 +6420,6 @@ }, "@types/node": { "version": "20.14.2", - "dev": true, "requires": { "undici-types": "~5.26.4" } @@ -2416,6 +6431,24 @@ "@types/node": "*" } }, + "@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" + }, + "@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" + }, "accepts": { "version": "1.3.8", "requires": { @@ -2423,17 +6456,144 @@ "negotiator": "0.6.3" } }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "requires": { + "type-fest": "^0.21.3" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, "arch": { "version": "2.2.0", "dev": true }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, "array-flatten": { "version": "1.1.1" }, - "balanced-match": { - "version": "1.0.2", + "async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", "dev": true }, + "babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "requires": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "dependencies": { + "istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + } + } + }, + "babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "requires": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, + "balanced-match": { + "version": "1.0.2" + }, "bin-check": { "version": "4.1.0", "dev": true, @@ -2548,11 +6708,43 @@ }, "braces": { "version": "3.0.3", - "dev": true, "requires": { "fill-range": "^7.1.1" } }, + "browserslist": { + "version": "4.23.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", + "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", + "requires": { + "caniuse-lite": "^1.0.30001640", + "electron-to-chromium": "^1.4.820", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.1.0" + } + }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "requires": { + "node-int64": "^0.4.0" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, "bytes": { "version": "3.1.2" }, @@ -2592,6 +6784,55 @@ "set-function-length": "^1.2.1" } }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "caniuse-lite": { + "version": "1.0.30001643", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz", + "integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==" + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==" + }, + "ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==" + }, + "cjs-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==" + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, "clone-response": { "version": "1.0.3", "dev": true, @@ -2599,10 +6840,38 @@ "mimic-response": "^1.0.0" } }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==" + }, + "collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==" + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, "commander": { "version": "7.2.0", "dev": true }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, "content-disposition": { "version": "0.5.4", "requires": { @@ -2612,12 +6881,31 @@ "content-type": { "version": "1.0.5" }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, "cookie": { "version": "0.6.0" }, "cookie-signature": { "version": "1.0.6" }, + "create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + } + }, "cross-spawn": { "version": "5.1.0", "dev": true, @@ -2649,6 +6937,17 @@ } } }, + "dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "requires": {} + }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" + }, "defer-to-connect": { "version": "2.0.1", "dev": true @@ -2667,9 +6966,43 @@ "destroy": { "version": "1.2.0" }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==" + }, + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==" + }, "ee-first": { "version": "1.1.1" }, + "ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "requires": { + "jake": "^10.8.5" + } + }, + "electron-to-chromium": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.2.tgz", + "integrity": "sha512-kc4r3U3V3WLaaZqThjYz/Y6z8tJe+7K0bbjUVo3i+LWIypVdMx5nXCkwRe6SWbY6ILqLdc1rKcKmr3HoH7wjSQ==" + }, + "emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, "encodeurl": { "version": "1.0.2" }, @@ -2680,6 +7013,14 @@ "once": "^1.4.0" } }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, "es-define-property": { "version": "1.0.0", "requires": { @@ -2692,6 +7033,11 @@ "esbuild-plugin-resolve": { "version": "2.0.0" }, + "escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==" + }, "escape-html": { "version": "1.0.3" }, @@ -2699,6 +7045,11 @@ "version": "5.0.0", "dev": true }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, "etag": { "version": "1.8.1" }, @@ -2722,6 +7073,23 @@ "pify": "^2.2.0" } }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==" + }, + "expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "requires": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + } + }, "express": { "version": "4.19.2", "requires": { @@ -2784,6 +7152,11 @@ "micromatch": "^4.0.4" } }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, "fastq": { "version": "1.17.1", "dev": true, @@ -2791,6 +7164,14 @@ "reusify": "^1.0.4" } }, + "fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "requires": { + "bser": "2.1.1" + } + }, "fetch-blob": { "version": "3.2.0", "requires": { @@ -2820,6 +7201,26 @@ } } }, + "filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "requires": { + "minimatch": "^5.0.1" + }, + "dependencies": { + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "filename-reserved-regex": { "version": "3.0.0", "dev": true @@ -2835,7 +7236,6 @@ }, "fill-range": { "version": "7.1.1", - "dev": true, "requires": { "to-regex-range": "^5.0.1" } @@ -2852,6 +7252,15 @@ "unpipe": "~1.0.0" } }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, "find-versions": { "version": "5.1.0", "dev": true, @@ -2871,9 +7280,30 @@ "fresh": { "version": "0.5.2" }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "optional": true + }, "function-bind": { "version": "1.1.2" }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, "get-intrinsic": { "version": "1.2.4", "requires": { @@ -2884,10 +7314,47 @@ "hasown": "^2.0.0" } }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==" + }, "get-stream": { "version": "3.0.0", "dev": true }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, "glob-parent": { "version": "5.1.2", "dev": true, @@ -2895,6 +7362,11 @@ "is-glob": "^4.0.1" } }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + }, "gopd": { "version": "1.0.1", "requires": { @@ -2918,6 +7390,16 @@ "responselike": "^2.0.0" } }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, "has-property-descriptors": { "version": "1.0.2", "requires": { @@ -2936,6 +7418,11 @@ "function-bind": "^1.1.2" } }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" + }, "http-cache-semantics": { "version": "4.1.1", "dev": true @@ -2959,8 +7446,7 @@ } }, "human-signals": { - "version": "2.1.0", - "dev": true + "version": "2.1.0" }, "iconv-lite": { "version": "0.4.24", @@ -2972,16 +7458,62 @@ "version": "1.2.1", "dev": true }, + "import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "inherits": { "version": "2.0.4" }, "ipaddr.js": { "version": "1.9.1" }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "is-core-module": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", + "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "requires": { + "hasown": "^2.0.2" + } + }, "is-extglob": { "version": "2.1.1", "dev": true }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==" + }, "is-glob": { "version": "4.0.3", "dev": true, @@ -2990,8 +7522,7 @@ } }, "is-number": { - "version": "7.0.0", - "dev": true + "version": "7.0.0" }, "is-plain-obj": { "version": "1.1.0", @@ -3002,8 +7533,7 @@ "dev": true }, "isexe": { - "version": "2.0.0", - "dev": true + "version": "2.0.0" }, "isomorphic-fetch": { "version": "3.0.0", @@ -3020,10 +7550,608 @@ } } }, + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==" + }, + "istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "requires": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + } + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "requires": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "requires": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + } + }, + "jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "requires": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "requires": { + "path-key": "^3.0.0" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "requires": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "requires": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + } + }, + "jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "requires": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + } + }, + "jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "requires": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + } + }, + "jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + } + }, + "jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==" + }, + "jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "requires": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + } + }, + "jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "requires": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + } + }, + "jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + } + }, + "jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "requires": {} + }, + "jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==" + }, + "jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "requires": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + } + }, + "jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "requires": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + } + }, + "jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "requires": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + } + }, + "jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "requires": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + } + }, + "jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "requires": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + } + }, + "jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "requires": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "requires": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "dependencies": { + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==" + } + } + }, + "jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "requires": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + } + }, + "jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "requires": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + }, "json-buffer": { "version": "3.0.1", "dev": true }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + }, "keyv": { "version": "4.5.4", "dev": true, @@ -3031,6 +8159,35 @@ "json-buffer": "3.0.1" } }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==" + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3048,6 +8205,28 @@ "yallist": "^2.1.2" } }, + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "requires": { + "semver": "^7.5.3" + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "requires": { + "tmpl": "1.0.5" + } + }, "media-typer": { "version": "0.3.0" }, @@ -3055,8 +8234,7 @@ "version": "1.0.1" }, "merge-stream": { - "version": "2.0.0", - "dev": true + "version": "2.0.0" }, "merge2": { "version": "1.4.1", @@ -3067,7 +8245,6 @@ }, "micromatch": { "version": "4.0.7", - "dev": true, "requires": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -3086,8 +8263,7 @@ } }, "mimic-fn": { - "version": "2.1.0", - "dev": true + "version": "2.1.0" }, "mimic-response": { "version": "1.0.1", @@ -3103,6 +8279,11 @@ "ms": { "version": "2.0.0" }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, "negotiator": { "version": "0.6.3" }, @@ -3117,6 +8298,21 @@ "formdata-polyfill": "^4.0.10" } }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" + }, + "node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, "normalize-url": { "version": "6.1.0", "dev": true @@ -3139,14 +8335,12 @@ }, "once": { "version": "1.4.0", - "dev": true, "requires": { "wrappy": "1" } }, "onetime": { "version": "5.1.2", - "dev": true, "requires": { "mimic-fn": "^2.1.0" } @@ -3166,13 +8360,70 @@ "version": "1.0.0", "dev": true }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + } + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, "parseurl": { "version": "1.3.3" }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + }, "path-key": { "version": "2.0.1", "dev": true }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, "path-to-regexp": { "version": "0.1.7" }, @@ -3180,18 +8431,61 @@ "version": "5.0.0", "dev": true }, + "picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + }, "picomatch": { - "version": "2.3.1", - "dev": true + "version": "2.3.1" }, "pify": { "version": "2.3.0", "dev": true }, + "pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==" + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "requires": { + "find-up": "^4.0.0" + } + }, "prettier": { "version": "3.3.2", "dev": true }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" + } + } + }, + "prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, "proxy-addr": { "version": "2.0.7", "requires": { @@ -3211,6 +8505,11 @@ "once": "^1.3.1" } }, + "pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==" + }, "qs": { "version": "6.11.0", "requires": { @@ -3237,6 +8536,11 @@ "unpipe": "1.0.0" } }, + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, "readable-stream": { "version": "3.6.2", "dev": true, @@ -3253,10 +8557,43 @@ "readable-stream": "^3.6.0" } }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, "resolve-alpn": { "version": "1.2.1", "dev": true }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "requires": { + "resolve-from": "^5.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" + }, + "resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==" + }, "responselike": { "version": "2.0.1", "dev": true, @@ -3282,8 +8619,7 @@ "version": "2.1.2" }, "semver": { - "version": "7.6.2", - "dev": true + "version": "7.6.2" }, "semver-regex": { "version": "4.0.5", @@ -3363,12 +8699,15 @@ } }, "signal-exit": { - "version": "3.0.7", - "dev": true + "version": "3.0.7" + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" }, "slash": { - "version": "3.0.0", - "dev": true + "version": "3.0.0" }, "sort-keys": { "version": "1.1.2", @@ -3388,6 +8727,42 @@ "version": "0.7.4", "dev": true }, + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + } + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" + } + } + }, "statuses": { "version": "2.0.1" }, @@ -3398,13 +8773,49 @@ "safe-buffer": "~5.2.0" } }, + "string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==" + }, "strip-eof": { "version": "1.0.0", "dev": true }, "strip-final-newline": { - "version": "2.0.0", - "dev": true + "version": "2.0.0" + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" }, "strip-outer": { "version": "2.0.0", @@ -3418,9 +8829,60 @@ "peek-readable": "^5.0.0" } }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==" + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" + }, "to-regex-range": { "version": "5.0.1", - "dev": true, "requires": { "is-number": "^7.0.0" } @@ -3446,12 +8908,39 @@ "escape-string-regexp": "^5.0.0" } }, + "ts-jest": { + "version": "29.2.3", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.3.tgz", + "integrity": "sha512-yCcfVdiBFngVz9/keHin9EnsrQtQtEu3nRykNy9RVp+FiPFFbPJ3Sg6Qg4+TkmH0vMP5qsTKgXSsk80HRwvdgQ==", + "dev": true, + "requires": { + "bs-logger": "0.x", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" + } + }, "ts-matches": { "version": "5.5.1" }, "tslib": { "version": "2.6.3" }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + }, + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==" + }, "type-is": { "version": "1.6.18", "requires": { @@ -3464,12 +8953,20 @@ "dev": true }, "undici-types": { - "version": "5.26.5", - "dev": true + "version": "5.26.5" }, "unpipe": { "version": "1.0.0" }, + "update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "requires": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + } + }, "util-deprecate": { "version": "1.0.2", "dev": true @@ -3477,9 +8974,27 @@ "utils-merge": { "version": "1.0.1" }, + "v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + } + }, "vary": { "version": "1.1.2" }, + "walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "requires": { + "makeerror": "1.0.12" + } + }, "web-streams-polyfill": { "version": "3.3.3" }, @@ -3503,9 +9018,32 @@ "isexe": "^2.0.0" } }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, "wrappy": { - "version": "1.0.2", - "dev": true + "version": "1.0.2" + }, + "write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "requires": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" }, "yallist": { "version": "2.1.2", @@ -3513,6 +9051,30 @@ }, "yaml": { "version": "2.4.5" + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" } } } diff --git a/container-runtime/package.json b/container-runtime/package.json index 515f50ee7..24e6a6fe6 100644 --- a/container-runtime/package.json +++ b/container-runtime/package.json @@ -6,7 +6,8 @@ "scripts": { "check": "tsc --noEmit", "build": "prettier . '!tmp/**' --write && rm -rf dist && tsc", - "tsc": "rm -rf dist; tsc" + "tsc": "rm -rf dist; tsc", + "test": "jest -c ./jest.config.js" }, "author": "", "prettier": { @@ -17,12 +18,13 @@ }, "dependencies": { "@iarna/toml": "^2.2.5", - "@start9labs/start-sdk": "file:../sdk/dist", - "@noble/hashes": "^1.4.0", "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", + "@start9labs/start-sdk": "file:../sdk/dist", "esbuild-plugin-resolve": "^2.0.0", "filebrowser": "^1.0.0", "isomorphic-fetch": "^3.0.0", + "jest": "^29.7.0", "lodash.merge": "^4.6.2", "node-fetch": "^3.1.0", "ts-matches": "^5.5.1", @@ -33,8 +35,10 @@ "devDependencies": { "@swc/cli": "^0.1.62", "@swc/core": "^1.3.65", + "@types/jest": "^29.5.12", "@types/node": "^20.11.13", "prettier": "^3.2.5", + "ts-jest": "^29.2.3", "typescript": ">5.2" } } diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/embasyPagesConfig.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/embasyPagesConfig.ts new file mode 100644 index 000000000..cb70bd123 --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/embasyPagesConfig.ts @@ -0,0 +1,127 @@ +export default { + homepage: { + name: "Homepage", + description: + "The page that will be displayed when your Start9 Pages .onion address is visited. Since this page is technically publicly accessible, you can choose to which type of page to display.", + type: "union", + default: "welcome", + tag: { + id: "type", + name: "Type", + "variant-names": { + welcome: "Welcome", + index: "Table of Contents", + "web-page": "Web Page", + redirect: "Redirect", + }, + }, + variants: { + welcome: {}, + index: {}, + "web-page": { + source: { + name: "Folder Location", + description: "The service that contains your website files.", + type: "enum", + values: ["filebrowser", "nextcloud"], + "value-names": {}, + default: "nextcloud", + }, + folder: { + type: "string", + name: "Folder Path", + placeholder: "e.g. websites/resume", + description: + 'The path to the folder that contains the static files of your website. For example, a value of "projects/resume" would tell Start9 Pages to look for that folder path in the selected service.', + pattern: + "^(\\.|[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)(/[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|/([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)*/?$", + "pattern-description": "Must be a valid relative file path", + nullable: false, + }, + }, + redirect: { + target: { + type: "string", + name: "Target Subdomain", + description: + "The name of the subdomain to redirect users to. This must be a valid subdomain site within your Start9 Pages.", + pattern: "^[a-z-]+$", + "pattern-description": + "May contain only lowercase characters and hyphens.", + nullable: false, + }, + }, + }, + }, + subdomains: { + type: "list", + name: "Subdomains", + description: "The websites you want to serve.", + default: [], + range: "[0, *)", + subtype: "object", + spec: { + "unique-by": "name", + "display-as": "{{name}}", + spec: { + name: { + type: "string", + nullable: false, + name: "Subdomain name", + description: + 'The subdomain of your Start9 Pages .onion address to host the website on. For example, a value of "me" would produce a website hosted at http://me.xxxxxx.onion.', + pattern: "^[a-z-]+$", + "pattern-description": + "May contain only lowercase characters and hyphens", + }, + settings: { + type: "union", + name: "Settings", + description: + "The desired behavior you want to occur when the subdomain is visited. You can either redirect to another subdomain, or load a stored web page.", + default: "web-page", + tag: { + id: "type", + name: "Type", + "variant-names": { "web-page": "Web Page", redirect: "Redirect" }, + }, + variants: { + "web-page": { + source: { + name: "Folder Location", + description: "The service that contains your website files.", + type: "enum", + values: ["filebrowser", "nextcloud"], + "value-names": {}, + default: "nextcloud", + }, + folder: { + type: "string", + name: "Folder Path", + placeholder: "e.g. websites/resume", + description: + 'The path to the folder that contains the website files. For example, a value of "projects/resume" would tell Start9 Pages to look for that folder path in the selected service.', + pattern: + "^(\\.|[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)(/[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|/([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)*/?$", + "pattern-description": "Must be a valid relative file path", + nullable: false, + }, + }, + redirect: { + target: { + type: "string", + name: "Target Subdomain", + description: + "The subdomain of your Start9 Pages .onion address to redirect to. This should be the name of another subdomain on Start9 Pages. Leave empty to redirect to the homepage.", + pattern: "^[a-z-]+$", + "pattern-description": + "May contain only lowercase characters and hyphens.", + nullable: false, + }, + }, + }, + }, + }, + }, + }, +} diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/searNXG.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/searNXG.ts new file mode 100644 index 000000000..51eb06b9a --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/searNXG.ts @@ -0,0 +1,39 @@ +export default { + "instance-name": { + type: "string", + name: "SearXNG Instance Name", + description: + "Enter a name for your SearXNG instance. This is the name that will be listed if you want to share your SearXNG engine publicly.", + nullable: false, + default: "My SearXNG Engine", + placeholder: "Uncle Jim SearXNG Engine", + }, + "tor-url": { + name: "Enable Tor address as the base URL", + description: + "Activates the utilization of a .onion address as the primary URL, particularly beneficial for publicly hosted instances over the Tor network.", + type: "boolean", + default: false, + }, + "enable-metrics": { + name: "Enable Stats", + description: + "Your SearXNG instance will collect anonymous stats about its own usage and performance. You can view these metrics by appending `/stats` or `/stats/errors` to your SearXNG URL.", + type: "boolean", + default: true, + }, //, + // "email-address": { + // "type": "string", + // "name": "Email Address", + // "description": "Your Email address - required to create an SSL certificate.", + // "nullable": false, + // "default": "youremail@domain.com", + // }, + // "public-host": { + // "type": "string", + // "name": "Public Domain Name", + // "description": "Enter a domain name here if you want to share your SearXNG engine publicly. You will also need to modify your domain name's DNS settings to point to your Start9 server.", + // "nullable": true, + // "placeholder": "https://search.mydomain.com" + // } +} diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__snapshots__/transformConfigSpec.test.ts.snap b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__snapshots__/transformConfigSpec.test.ts.snap new file mode 100644 index 000000000..1d434c78d --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__snapshots__/transformConfigSpec.test.ts.snap @@ -0,0 +1,253 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`transformConfigSpec transformConfigSpec(embassyPages) 1`] = ` +{ + "homepage": { + "default": "welcome", + "description": null, + "disabled": false, + "immutable": false, + "name": "Type", + "required": true, + "type": "union", + "variants": { + "index": { + "name": "Table of Contents", + "spec": {}, + }, + "redirect": { + "name": "Redirect", + "spec": { + "target": { + "default": null, + "description": "The name of the subdomain to redirect users to. This must be a valid subdomain site within your Start9 Pages.", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "name": "Target Subdomain", + "patterns": [ + { + "description": "May contain only lowercase characters and hyphens.", + "regex": "^[a-z-]+$", + }, + ], + "placeholder": null, + "required": true, + "type": "text", + "warning": null, + }, + }, + }, + "web-page": { + "name": "Web Page", + "spec": { + "folder": { + "default": null, + "description": "The path to the folder that contains the static files of your website. For example, a value of "projects/resume" would tell Start9 Pages to look for that folder path in the selected service.", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "name": "Folder Path", + "patterns": [ + { + "description": "Must be a valid relative file path", + "regex": "^(\\.|[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)(/[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|/([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)*/?$", + }, + ], + "placeholder": "e.g. websites/resume", + "required": true, + "type": "text", + "warning": null, + }, + "source": { + "default": "nextcloud", + "description": "The service that contains your website files.", + "disabled": false, + "immutable": false, + "name": "Folder Location", + "required": false, + "type": "select", + "values": { + "filebrowser": undefined, + "nextcloud": undefined, + }, + "warning": null, + }, + }, + }, + "welcome": { + "name": "Welcome", + "spec": {}, + }, + }, + "warning": null, + }, + "subdomains": { + "default": [], + "description": "The websites you want to serve.", + "disabled": false, + "maxLength": null, + "minLength": null, + "name": "Subdomains", + "spec": { + "displayAs": "{{name}}", + "spec": { + "name": { + "default": null, + "description": "The subdomain of your Start9 Pages .onion address to host the website on. For example, a value of "me" would produce a website hosted at http://me.xxxxxx.onion.", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "name": "Subdomain name", + "patterns": [ + { + "description": "May contain only lowercase characters and hyphens", + "regex": "^[a-z-]+$", + }, + ], + "placeholder": null, + "required": true, + "type": "text", + "warning": null, + }, + "settings": { + "default": "web-page", + "description": null, + "disabled": false, + "immutable": false, + "name": "Type", + "required": true, + "type": "union", + "variants": { + "redirect": { + "name": "Redirect", + "spec": { + "target": { + "default": null, + "description": "The subdomain of your Start9 Pages .onion address to redirect to. This should be the name of another subdomain on Start9 Pages. Leave empty to redirect to the homepage.", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "name": "Target Subdomain", + "patterns": [ + { + "description": "May contain only lowercase characters and hyphens.", + "regex": "^[a-z-]+$", + }, + ], + "placeholder": null, + "required": true, + "type": "text", + "warning": null, + }, + }, + }, + "web-page": { + "name": "Web Page", + "spec": { + "folder": { + "default": null, + "description": "The path to the folder that contains the website files. For example, a value of "projects/resume" would tell Start9 Pages to look for that folder path in the selected service.", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "name": "Folder Path", + "patterns": [ + { + "description": "Must be a valid relative file path", + "regex": "^(\\.|[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)(/[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|/([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)*/?$", + }, + ], + "placeholder": "e.g. websites/resume", + "required": true, + "type": "text", + "warning": null, + }, + "source": { + "default": "nextcloud", + "description": "The service that contains your website files.", + "disabled": false, + "immutable": false, + "name": "Folder Location", + "required": false, + "type": "select", + "values": { + "filebrowser": undefined, + "nextcloud": undefined, + }, + "warning": null, + }, + }, + }, + }, + "warning": null, + }, + }, + "type": "object", + "uniqueBy": "name", + }, + "type": "list", + "warning": null, + }, +} +`; + +exports[`transformConfigSpec transformConfigSpec(searNXG) 1`] = ` +{ + "enable-metrics": { + "default": true, + "description": "Your SearXNG instance will collect anonymous stats about its own usage and performance. You can view these metrics by appending \`/stats\` or \`/stats/errors\` to your SearXNG URL.", + "disabled": false, + "immutable": false, + "name": "Enable Stats", + "type": "toggle", + "warning": null, + }, + "instance-name": { + "default": "My SearXNG Engine", + "description": "Enter a name for your SearXNG instance. This is the name that will be listed if you want to share your SearXNG engine publicly.", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "name": "SearXNG Instance Name", + "patterns": [], + "placeholder": "Uncle Jim SearXNG Engine", + "required": true, + "type": "text", + "warning": null, + }, + "tor-url": { + "default": false, + "description": "Activates the utilization of a .onion address as the primary URL, particularly beneficial for publicly hosted instances over the Tor network.", + "disabled": false, + "immutable": false, + "name": "Enable Tor address as the base URL", + "type": "toggle", + "warning": null, + }, +} +`; diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.test.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.test.ts new file mode 100644 index 000000000..e3f463d03 --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.test.ts @@ -0,0 +1,23 @@ +import { matchOldConfigSpec, transformConfigSpec } from "./transformConfigSpec" +import fixtureEmbasyPagesConfig from "./__fixtures__/embasyPagesConfig" +import searNXG from "./__fixtures__/searNXG" + +describe("transformConfigSpec", () => { + test("matchOldConfigSpec(embassyPages.homepage.variants[web-page])", () => { + matchOldConfigSpec.unsafeCast( + fixtureEmbasyPagesConfig.homepage.variants["web-page"], + ) + }) + test("matchOldConfigSpec(embassyPages)", () => { + matchOldConfigSpec.unsafeCast(fixtureEmbasyPagesConfig) + }) + test("transformConfigSpec(embassyPages)", () => { + const spec = matchOldConfigSpec.unsafeCast(fixtureEmbasyPagesConfig) + expect(transformConfigSpec(spec)).toMatchSnapshot() + }) + + test("transformConfigSpec(searNXG)", () => { + const spec = matchOldConfigSpec.unsafeCast(searNXG) + expect(transformConfigSpec(spec)).toMatchSnapshot() + }) +}) diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts index cb7809903..be3aa89b3 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts @@ -104,7 +104,7 @@ export function transformConfigSpec(oldSpec: OldConfigSpec): CT.InputSpec { : [], minLength: null, maxLength: null, - masked: oldVal.masked, + masked: oldVal.masked || false, generate: null, inputmode: "text", placeholder: oldVal.placeholder || null, @@ -347,11 +347,11 @@ type OldDefaultString = typeof matchOldDefaultString._TYPE export const matchOldValueSpecString = object( { + type: literals("string"), + name: string, masked: boolean, copyable: boolean, - type: literals("string"), nullable: boolean, - name: string, placeholder: string, pattern: string, "pattern-description": string, @@ -361,6 +361,9 @@ export const matchOldValueSpecString = object( warning: string, }, [ + "masked", + "copyable", + "nullable", "placeholder", "pattern", "pattern-description", diff --git a/container-runtime/tsconfig.json b/container-runtime/tsconfig.json index 0b2fe6e32..6981133d6 100644 --- a/container-runtime/tsconfig.json +++ b/container-runtime/tsconfig.json @@ -13,9 +13,10 @@ "declaration": true, "noImplicitAny": true, "esModuleInterop": true, - "types": ["node"], + "types": ["node", "jest"], "moduleResolution": "Node16", - "skipLibCheck": true + "skipLibCheck": true, + "resolveJsonModule": true }, "ts-node": { "compilerOptions": { From a3b94816f9fb1b56435b4400d99d6e17dcc3645d Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 26 Jul 2024 12:29:59 -0600 Subject: [PATCH 087/125] ephemeral logins --- core/startos/src/auth.rs | 50 ++++++++++++++----- core/startos/src/context/rpc.rs | 4 ++ core/startos/src/middleware/auth.rs | 42 ++++++++++------ core/startos/src/util/mod.rs | 1 + core/startos/src/util/sync.rs | 9 ++++ sdk/lib/osBindings/LoginParams.ts | 6 ++- .../ui/src/app/pages/login/login.page.ts | 1 + .../ui/src/app/services/api/api.types.ts | 1 + .../ui/src/app/services/config.service.ts | 2 - 9 files changed, 87 insertions(+), 29 deletions(-) create mode 100644 core/startos/src/util/sync.rs diff --git a/core/startos/src/auth.rs b/core/startos/src/auth.rs index 03005998f..68e16c244 100644 --- a/core/startos/src/auth.rs +++ b/core/startos/src/auth.rs @@ -185,6 +185,8 @@ pub struct LoginParams { #[serde(rename = "__auth_userAgent")] // from Auth middleware user_agent: Option, #[serde(default)] + ephemeral: bool, + #[serde(default)] #[ts(type = "any")] metadata: Value, } @@ -195,28 +197,46 @@ pub async fn login_impl( LoginParams { password, user_agent, + ephemeral, metadata, }: LoginParams, ) -> Result { let password = password.unwrap_or_default().decrypt(&ctx)?; - ctx.db - .mutate(|db| { - check_password_against_db(db, &password)?; - let hash_token = HashSessionToken::new(); - db.as_private_mut().as_sessions_mut().insert( - hash_token.hashed(), - &Session { + if ephemeral { + check_password_against_db(&ctx.db.peek().await, &password)?; + let hash_token = HashSessionToken::new(); + ctx.ephemeral_sessions.mutate(|s| { + s.0.insert( + hash_token.hashed().clone(), + Session { logged_in: Utc::now(), last_active: Utc::now(), user_agent, metadata, }, - )?; + ) + }); + Ok(hash_token.to_login_res()) + } else { + ctx.db + .mutate(|db| { + check_password_against_db(db, &password)?; + let hash_token = HashSessionToken::new(); + db.as_private_mut().as_sessions_mut().insert( + hash_token.hashed(), + &Session { + logged_in: Utc::now(), + last_active: Utc::now(), + user_agent, + metadata, + }, + )?; - Ok(hash_token.to_login_res()) - }) - .await + Ok(hash_token.to_login_res()) + }) + .await + } } #[derive(Deserialize, Serialize, Parser, TS)] @@ -329,9 +349,15 @@ pub async fn list( ctx: RpcContext, ListParams { session, .. }: ListParams, ) -> Result { + let mut sessions = ctx.db.peek().await.into_private().into_sessions().de()?; + ctx.ephemeral_sessions.mutate(|s| { + sessions + .0 + .extend(s.0.iter().map(|(k, v)| (k.clone(), v.clone()))) + }); Ok(SessionList { current: HashSessionToken::from_token(session).hashed().clone(), - sessions: ctx.db.peek().await.into_private().into_sessions().de()?, + sessions, }) } diff --git a/core/startos/src/context/rpc.rs b/core/startos/src/context/rpc.rs index cea2059d3..0d00abb3a 100644 --- a/core/startos/src/context/rpc.rs +++ b/core/startos/src/context/rpc.rs @@ -17,6 +17,7 @@ use tracing::instrument; use super::setup::CURRENT_SECRET; use crate::account::AccountInfo; +use crate::auth::Sessions; use crate::context::config::ServerConfig; use crate::db::model::Database; use crate::dependencies::compute_dependency_config_errs; @@ -34,6 +35,7 @@ use crate::service::ServiceMap; use crate::shutdown::Shutdown; use crate::system::get_mem_info; use crate::util::lshw::{lshw, LshwDevice}; +use crate::util::sync::SyncMutex; pub struct RpcContextSeed { is_closed: AtomicBool, @@ -42,6 +44,7 @@ pub struct RpcContextSeed { pub ethernet_interface: String, pub datadir: PathBuf, pub disk_guid: Arc, + pub ephemeral_sessions: SyncMutex, pub db: TypedPatchDb, pub sync_db: watch::Sender, pub account: RwLock, @@ -213,6 +216,7 @@ impl RpcContext { find_eth_iface().await? }, disk_guid, + ephemeral_sessions: SyncMutex::new(Sessions::new()), sync_db: watch::Sender::new(db.sequence().await), db, account: RwLock::new(account), diff --git a/core/startos/src/middleware/auth.rs b/core/startos/src/middleware/auth.rs index fd60894db..9b04afb38 100644 --- a/core/startos/src/middleware/auth.rs +++ b/core/startos/src/middleware/auth.rs @@ -51,6 +51,11 @@ impl HasLoggedOutSessions { for sid in &to_log_out { ctx.open_authed_continuations.kill(sid) } + ctx.ephemeral_sessions.mutate(|s| { + for sid in &to_log_out { + s.0.remove(sid); + } + }); ctx.db .mutate(|db| { let sessions = db.as_private_mut().as_sessions_mut(); @@ -110,20 +115,29 @@ impl HasValidSession { ctx: &RpcContext, ) -> Result { let session_hash = session_token.hashed(); - ctx.db - .mutate(|db| { - db.as_private_mut() - .as_sessions_mut() - .as_idx_mut(session_hash) - .ok_or_else(|| { - Error::new(eyre!("UNAUTHORIZED"), crate::ErrorKind::Authorization) - })? - .mutate(|s| { - s.last_active = Utc::now(); - Ok(()) - }) - }) - .await?; + if !ctx.ephemeral_sessions.mutate(|s| { + if let Some(session) = s.0.get_mut(session_hash) { + session.last_active = Utc::now(); + true + } else { + false + } + }) { + ctx.db + .mutate(|db| { + db.as_private_mut() + .as_sessions_mut() + .as_idx_mut(session_hash) + .ok_or_else(|| { + Error::new(eyre!("UNAUTHORIZED"), crate::ErrorKind::Authorization) + })? + .mutate(|s| { + s.last_active = Utc::now(); + Ok(()) + }) + }) + .await?; + } Ok(Self(SessionType::Session(session_token))) } diff --git a/core/startos/src/util/mod.rs b/core/startos/src/util/mod.rs index 1641e4496..fab0b127d 100644 --- a/core/startos/src/util/mod.rs +++ b/core/startos/src/util/mod.rs @@ -49,6 +49,7 @@ pub mod net; pub mod rpc; pub mod rpc_client; pub mod serde; +pub mod sync; #[derive(Clone, Copy, Debug, ::serde::Deserialize, ::serde::Serialize)] pub enum Never {} diff --git a/core/startos/src/util/sync.rs b/core/startos/src/util/sync.rs new file mode 100644 index 000000000..f8ec425e0 --- /dev/null +++ b/core/startos/src/util/sync.rs @@ -0,0 +1,9 @@ +pub struct SyncMutex(std::sync::Mutex); +impl SyncMutex { + pub fn new(t: T) -> Self { + Self(std::sync::Mutex::new(t)) + } + pub fn mutate U, U>(&self, f: F) -> U { + f(&mut *self.0.lock().unwrap()) + } +} diff --git a/sdk/lib/osBindings/LoginParams.ts b/sdk/lib/osBindings/LoginParams.ts index 272f7ed07..acaf5b8a1 100644 --- a/sdk/lib/osBindings/LoginParams.ts +++ b/sdk/lib/osBindings/LoginParams.ts @@ -1,4 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { PasswordType } from "./PasswordType" -export type LoginParams = { password: PasswordType | null; metadata: any } +export type LoginParams = { + password: PasswordType | null + ephemeral: boolean + metadata: any +} diff --git a/web/projects/ui/src/app/pages/login/login.page.ts b/web/projects/ui/src/app/pages/login/login.page.ts index 065a4cc7f..29d4321f8 100644 --- a/web/projects/ui/src/app/pages/login/login.page.ts +++ b/web/projects/ui/src/app/pages/login/login.page.ts @@ -40,6 +40,7 @@ export class LoginPage { await this.api.login({ password: this.password, metadata: { platforms: getPlatforms() }, + ephemeral: window.location.host === 'localhost', }) this.password = '' diff --git a/web/projects/ui/src/app/services/api/api.types.ts b/web/projects/ui/src/app/services/api/api.types.ts index 770792328..eac7cd7f4 100644 --- a/web/projects/ui/src/app/services/api/api.types.ts +++ b/web/projects/ui/src/app/services/api/api.types.ts @@ -33,6 +33,7 @@ export module RR { export type LoginReq = { password: string metadata: SessionMetadata + ephemeral?: boolean } // auth.login - unauthed export type loginRes = null diff --git a/web/projects/ui/src/app/services/config.service.ts b/web/projects/ui/src/app/services/config.service.ts index a148112f0..69fafe815 100644 --- a/web/projects/ui/src/app/services/config.service.ts +++ b/web/projects/ui/src/app/services/config.service.ts @@ -73,8 +73,6 @@ export class ConfigService { const hostnameInfo = hosts[ui.addressInfo.hostId]?.hostnameInfo[ui.addressInfo.internalPort] - console.debug(hostnameInfo) - if (!hostnameInfo) return '' const addressInfo = ui.addressInfo From 6d42ae26298edc3a925dd0ae06f821d406f396ec Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 26 Jul 2024 12:30:26 -0600 Subject: [PATCH 088/125] brackets for ipv6 --- sdk/lib/util/getServiceInterface.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sdk/lib/util/getServiceInterface.ts b/sdk/lib/util/getServiceInterface.ts index b41d8a15f..9148c8f9a 100644 --- a/sdk/lib/util/getServiceInterface.ts +++ b/sdk/lib/util/getServiceInterface.ts @@ -90,6 +90,8 @@ export const addressHostToUrl = ( } else if (host.kind === "ip") { if (host.hostname.kind === "domain") { hostname = `${host.hostname.subdomain ? `${host.hostname.subdomain}.` : ""}${host.hostname.domain}` + } else if (host.hostname.kind === "ipv6") { + hostname = `[${host.hostname.value}]` } else { hostname = host.hostname.value } From 5cef6874f6ce8c7ea6e08b0f403e8042cb50bc51 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 26 Jul 2024 12:33:22 -0600 Subject: [PATCH 089/125] install before test --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 90c00ae2d..f8afaf29c 100644 --- a/Makefile +++ b/Makefile @@ -92,7 +92,7 @@ format: test: $(CORE_SRC) $(ENVIRONMENT_FILE) (cd core && cargo build --features=test && cargo test --features=test) (cd sdk && make test) - (cd container-runtime && npm test) + (cd container-runtime && npm i && npm test) cli: cd core && ./install-cli.sh From 698bdd619f8c3368bf7facc9327b7e7500e5cbd9 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 26 Jul 2024 13:26:23 -0600 Subject: [PATCH 090/125] fix version mapping --- core/startos/src/version/mod.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/startos/src/version/mod.rs b/core/startos/src/version/mod.rs index 09d438344..ebd178e37 100644 --- a/core/startos/src/version/mod.rs +++ b/core/startos/src/version/mod.rs @@ -27,6 +27,7 @@ enum Version { V0_3_5_1(Wrapper), V0_3_5_2(Wrapper), V0_3_6_alpha_0(Wrapper), + V0_3_6_alpha_1(Wrapper), Other(exver::Version), } @@ -47,6 +48,7 @@ impl Version { Version::V0_3_5_1(Wrapper(x)) => x.semver(), Version::V0_3_5_2(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_0(Wrapper(x)) => x.semver(), + Version::V0_3_6_alpha_1(Wrapper(x)) => x.semver(), Version::Other(x) => x.clone(), } } @@ -247,6 +249,7 @@ pub async fn init( Version::V0_3_5_1(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, Version::V0_3_5_2(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, Version::V0_3_6_alpha_0(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, + Version::V0_3_6_alpha_1(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, Version::Other(_) => { return Err(Error::new( eyre!("Cannot downgrade"), @@ -291,6 +294,12 @@ mod tests { Just(Version::V0_3_5(Wrapper(v0_3_5::Version::new()))), Just(Version::V0_3_5_1(Wrapper(v0_3_5_1::Version::new()))), Just(Version::V0_3_5_2(Wrapper(v0_3_5_2::Version::new()))), + Just(Version::V0_3_6_alpha_0(Wrapper( + v0_3_6_alpha_0::Version::new() + ))), + Just(Version::V0_3_6_alpha_1(Wrapper( + v0_3_6_alpha_1::Version::new() + ))), em_version().prop_map(Version::Other), ] } From 10ede0d21c145e42a0cc63d2f4202361220bbca7 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 26 Jul 2024 14:47:43 -0600 Subject: [PATCH 091/125] delegate pointer removal to config transformer --- .../Systems/SystemForEmbassy/index.ts | 58 ++++++------------- .../SystemForEmbassy/transformConfigSpec.ts | 44 ++++++++++++-- 2 files changed, 58 insertions(+), 44 deletions(-) diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index df67dd962..5f8000f25 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -45,6 +45,7 @@ import { OldConfigSpec, matchOldConfigSpec, transformConfigSpec, + transformNewConfigToOld, transformOldConfigToNew, } from "./transformConfigSpec" import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk" @@ -101,6 +102,11 @@ const matchSetResult = object( ["depends-on", "dependsOn"], ) +type OldGetConfigRes = { + config?: null | Record + spec: OldConfigSpec +} + export type PackagePropertiesV2 = { [name: string]: PackagePropertyObject | PackagePropertyString } @@ -546,14 +552,12 @@ export class SystemForEmbassy implements System { effects: Effects, timeoutMs: number | null, ): Promise { - return this.getConfigUncleaned(effects, timeoutMs) - .then(removePointers) - .then(convertToNewConfig) + return this.getConfigUncleaned(effects, timeoutMs).then(convertToNewConfig) } private async getConfigUncleaned( effects: Effects, timeoutMs: number | null, - ): Promise { + ): Promise { const config = this.manifest.config?.get if (!config) return { spec: {} } if (config.type === "docker") { @@ -590,13 +594,14 @@ export class SystemForEmbassy implements System { newConfigWithoutPointers: unknown, timeoutMs: number | null, ): Promise { - const newConfig = structuredClone(newConfigWithoutPointers) - await updateConfig( - effects, - this.manifest, - await this.getConfigUncleaned(effects, timeoutMs).then((x) => x.spec), - newConfig, + const spec = await this.getConfigUncleaned(effects, timeoutMs).then( + (x) => x.spec, ) + const newConfig = transformNewConfigToOld( + spec, + structuredClone(newConfigWithoutPointers as Record), + ) + await updateConfig(effects, this.manifest, spec, newConfig) const setConfigValue = this.manifest.config?.set if (!setConfigValue) return if (setConfigValue.type === "docker") { @@ -895,14 +900,6 @@ export class SystemForEmbassy implements System { })) as any } } -async function removePointers(value: T.ConfigRes): Promise { - const startingSpec = structuredClone(value.spec) - const config = - value.config && cleanConfigFromPointers(value.config, startingSpec) - const spec = cleanSpecOfPointers(startingSpec) - - return { config, spec } -} const matchPointer = object({ type: literal("pointer"), @@ -962,27 +959,6 @@ type CleanConfigFromPointers = } : null -function cleanConfigFromPointers( - config: C, - spec: S, -): CleanConfigFromPointers { - const newConfig = {} as CleanConfigFromPointers - - if (!(object.test(config) && object.test(spec)) || newConfig == null) - return null as CleanConfigFromPointers - - for (const key of Object.keys(spec)) { - if (!isKeyOf(key, spec)) continue - if (!isKeyOf(key, config)) continue - const partSpec = spec[key] - if (matchPointer.test(partSpec)) continue - ;(newConfig as any)[key] = matchSpec.test(partSpec) - ? cleanConfigFromPointers(config[key], partSpec.spec) - : config[key] - } - return newConfig as CleanConfigFromPointers -} - async function updateConfig( effects: Effects, manifest: Manifest, @@ -1081,7 +1057,9 @@ function extractServiceInterfaceId(manifest: Manifest, specInterface: string) { const serviceInterfaceId = `${specInterface}-${internalPort}` return serviceInterfaceId } -async function convertToNewConfig(value: T.ConfigRes): Promise { +async function convertToNewConfig( + value: OldGetConfigRes, +): Promise { const valueSpec: OldConfigSpec = matchOldConfigSpec.unsafeCast(value.spec) const spec = transformConfigSpec(valueSpec) if (!value.config) return { spec, config: null } diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts index be3aa89b3..9a82d5c16 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts @@ -12,6 +12,7 @@ import { deferred, every, nill, + literal, } from "ts-matches" export function transformConfigSpec(oldSpec: OldConfigSpec): CT.InputSpec { @@ -38,7 +39,7 @@ export function transformConfigSpec(oldSpec: OldConfigSpec): CT.InputSpec { values: oldVal.values.reduce( (obj, curr) => ({ ...obj, - [curr]: oldVal["value-names"][curr], + [curr]: oldVal["value-names"][curr] || curr, }), {}, ), @@ -109,7 +110,7 @@ export function transformConfigSpec(oldSpec: OldConfigSpec): CT.InputSpec { inputmode: "text", placeholder: oldVal.placeholder || null, } - } else { + } else if (oldVal.type === "union") { newVal = { type: "union", name: oldVal.tag.name, @@ -119,7 +120,7 @@ export function transformConfigSpec(oldSpec: OldConfigSpec): CT.InputSpec { (obj, [id, spec]) => ({ ...obj, [id]: { - name: oldVal.tag["variant-names"][id], + name: oldVal.tag["variant-names"][id] || id, spec: transformConfigSpec(matchOldConfigSpec.unsafeCast(spec)), }, }), @@ -130,6 +131,10 @@ export function transformConfigSpec(oldSpec: OldConfigSpec): CT.InputSpec { default: oldVal.default, immutable: false, } + } else if (oldVal.type === "pointer") { + return inputSpec + } else { + throw new Error(`unknown spec ${JSON.stringify(oldVal)}`) } return { @@ -175,6 +180,10 @@ export function transformOldConfigToNew( ) } + if (isPointer(val)) { + return obj + } + return { ...obj, [key]: newVal, @@ -201,7 +210,7 @@ export function transformNewConfigToOld( [val.tag.id]: config[key].selection, ...transformNewConfigToOld( matchOldConfigSpec.unsafeCast(val.variants[config[key].selection]), - config[key].unionSelectValue, + config[key].value, ), } } @@ -313,6 +322,10 @@ function isList(val: OldValueSpec): val is OldValueSpecList { return val.type === "list" } +function isPointer(val: OldValueSpec): val is OldValueSpecPointer { + return val.type === "pointer" +} + function isEnumList( val: OldValueSpecList, ): val is OldValueSpecList & { subtype: "enum" } { @@ -522,6 +535,28 @@ const matchOldValueSpecList = every( ) type OldValueSpecList = typeof matchOldValueSpecList._TYPE +const matchOldValueSpecPointer = every( + object({ + type: literal("pointer"), + }), + anyOf( + object({ + subtype: literal("package"), + target: literals("tor-key", "tor-address", "lan-address"), + "package-id": string, + interface: string, + }), + object({ + subtype: literal("package"), + target: literals("config"), + "package-id": string, + selector: string, + multi: boolean, + }), + ), +) +type OldValueSpecPointer = typeof matchOldValueSpecPointer._TYPE + export const matchOldValueSpec = anyOf( matchOldValueSpecString, matchOldValueSpecNumber, @@ -530,6 +565,7 @@ export const matchOldValueSpec = anyOf( matchOldValueSpecEnum, matchOldValueSpecList, matchOldValueSpecUnion, + matchOldValueSpecPointer, ) type OldValueSpec = typeof matchOldValueSpec._TYPE From 25b33fb0317c3b8cdd441b05cb542b7c48fd773d Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 26 Jul 2024 14:47:55 -0600 Subject: [PATCH 092/125] use ci for test --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index f8afaf29c..8f6bd0203 100644 --- a/Makefile +++ b/Makefile @@ -92,7 +92,7 @@ format: test: $(CORE_SRC) $(ENVIRONMENT_FILE) (cd core && cargo build --features=test && cargo test --features=test) (cd sdk && make test) - (cd container-runtime && npm i && npm test) + (cd container-runtime && npm ci && npm test) cli: cd core && ./install-cli.sh From 948075831096ff69209385ecbb286140723f91b8 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 26 Jul 2024 15:05:25 -0600 Subject: [PATCH 093/125] decrease lxc-net init weight --- core/startos/src/init.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/startos/src/init.rs b/core/startos/src/init.rs index 5e0fa932f..7b944ebe3 100644 --- a/core/startos/src/init.rs +++ b/core/startos/src/init.rs @@ -235,7 +235,7 @@ impl InitPhases { sync_clock: handle.add_phase("Synchronizing system clock".into(), Some(10)), enable_zram: handle.add_phase("Enabling ZRAM".into(), Some(1)), update_server_info: handle.add_phase("Updating server info".into(), Some(1)), - launch_service_network: handle.add_phase("Launching service intranet".into(), Some(10)), + launch_service_network: handle.add_phase("Launching service intranet".into(), Some(1)), run_migrations: handle.add_phase("Running migrations".into(), Some(10)), validate_db: handle.add_phase("Validating database".into(), Some(1)), postinit: if Path::new("/media/startos/config/postinit.sh").exists() { From bf55367f4d04f6645d0b318655c48314def6eab4 Mon Sep 17 00:00:00 2001 From: J H Date: Fri, 26 Jul 2024 15:12:22 -0600 Subject: [PATCH 094/125] chore: remove the need for the method in the autoconfig --- .../src/Adapters/Systems/SystemForEmbassy/index.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index 5f8000f25..30b221d4d 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -886,10 +886,7 @@ export class SystemForEmbassy implements System { // TODO: docker const moduleCode = await this.moduleCode const method = moduleCode.dependencies?.[id]?.autoConfigure - if (!method) - throw new Error( - `Expecting that the method dependency autoConfigure ${id} exists`, - ) + if (!method) return return (await method( polyfillEffects(effects, this.manifest), oldConfig as any, From 6abdc39fe5d7a5ffbb525f743a55c0f45ccd5d20 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 26 Jul 2024 17:44:16 -0600 Subject: [PATCH 095/125] ignore error on dependent mounts in polyfill --- .../DockerProcedureContainer.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts index 5db0b6245..012a70eee 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/DockerProcedureContainer.ts @@ -68,15 +68,17 @@ export class DockerProcedureContainer { key, ) } else if (volumeMount.type === "pointer") { - await effects.mount({ - location: path, - target: { - packageId: volumeMount["package-id"], - subpath: volumeMount.path, - readonly: volumeMount.readonly, - volumeId: volumeMount["volume-id"], - }, - }) + await effects + .mount({ + location: path, + target: { + packageId: volumeMount["package-id"], + subpath: volumeMount.path, + readonly: volumeMount.readonly, + volumeId: volumeMount["volume-id"], + }, + }) + .catch(console.warn) } else if (volumeMount.type === "backup") { await overlay.mount({ type: "backup", subpath: null }, mounts[mount]) } From bfe3029d31c1c3dd9e2e981896c4dbbc4d369194 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 26 Jul 2024 17:49:44 -0600 Subject: [PATCH 096/125] fix dependency autoconfig --- .../Adapters/Systems/SystemForEmbassy/index.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index 30b221d4d..ff5a6ee74 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -348,12 +348,7 @@ export class SystemForEmbassy implements System { options.timeout || null, ) case procedures[1] === "dependencies" && procedures[3] === "query": - return this.dependenciesAutoconfig( - effects, - procedures[2], - input, - options.timeout || null, - ) + return null case procedures[1] === "dependencies" && procedures[3] === "update": return this.dependenciesAutoconfig( @@ -836,9 +831,9 @@ export class SystemForEmbassy implements System { id: string, oldConfig: unknown, timeoutMs: number | null, - ): Promise { + ): Promise { const actionProcedure = this.manifest.dependencies?.[id]?.config?.check - if (!actionProcedure) return { message: "Action not found", value: null } + if (!actionProcedure) return null if (actionProcedure.type === "docker") { const container = await DockerProcedureContainer.of( effects, @@ -880,16 +875,19 @@ export class SystemForEmbassy implements System { private async dependenciesAutoconfig( effects: Effects, id: string, - oldConfig: unknown, + input: unknown, timeoutMs: number | null, ): Promise { + const oldConfig = object({ remoteConfig: any }).unsafeCast( + input, + ).remoteConfig // TODO: docker const moduleCode = await this.moduleCode const method = moduleCode.dependencies?.[id]?.autoConfigure if (!method) return return (await method( polyfillEffects(effects, this.manifest), - oldConfig as any, + oldConfig, ).then((x) => { if ("result" in x) return x.result if ("error" in x) throw new Error("Error getting config: " + x.error) From be217b5354d5cbe997bbb3c0ab715e1a77530048 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 26 Jul 2024 19:06:11 -0600 Subject: [PATCH 097/125] update-grub on update --- core/startos/src/update/mod.rs | 84 ++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/core/startos/src/update/mod.rs b/core/startos/src/update/mod.rs index 79d852754..51d8d77ae 100644 --- a/core/startos/src/update/mod.rs +++ b/core/startos/src/update/mod.rs @@ -18,6 +18,12 @@ use tracing::instrument; use ts_rs::TS; use crate::context::{CliContext, RpcContext}; +use crate::disk::mount::filesystem::bind::Bind; +use crate::disk::mount::filesystem::block_dev::BlockDev; +use crate::disk::mount::filesystem::efivarfs::{self, EfiVarFs}; +use crate::disk::mount::filesystem::overlayfs::OverlayGuard; +use crate::disk::mount::filesystem::MountType; +use crate::disk::mount::guard::{GenericMountGuard, MountGuard, TmpMountGuard}; use crate::notifications::{notify, NotificationLevel}; use crate::prelude::*; use crate::progress::{FullProgressTracker, PhaseProgressTrackerHandle, PhasedProgressBar}; @@ -247,6 +253,7 @@ async fn maybe_do_update( asset.validate(SIG_CONTEXT, asset.all_signers())?; let progress = FullProgressTracker::new(); + let prune_phase = progress.add_phase("Pruning Old OS Images".into(), Some(2)); let mut download_phase = progress.add_phase("Downloading File".into(), Some(100)); download_phase.set_total(asset.commitment.size); let reverify_phase = progress.add_phase("Reverifying File".into(), Some(10)); @@ -300,6 +307,7 @@ async fn maybe_do_update( asset, UpdateProgressHandles { progress, + prune_phase, download_phase, reverify_phase, sync_boot_phase, @@ -369,6 +377,7 @@ async fn maybe_do_update( struct UpdateProgressHandles { progress: FullProgressTracker, + prune_phase: PhaseProgressTrackerHandle, download_phase: PhaseProgressTrackerHandle, reverify_phase: PhaseProgressTrackerHandle, sync_boot_phase: PhaseProgressTrackerHandle, @@ -381,12 +390,20 @@ async fn do_update( asset: RegistryAsset, UpdateProgressHandles { progress, + mut prune_phase, mut download_phase, mut reverify_phase, mut sync_boot_phase, mut finalize_phase, }: UpdateProgressHandles, ) -> Result<(), Error> { + prune_phase.start(); + Command::new("/usr/lib/startos/scripts/prune-images") + .arg(asset.commitment.size.to_string()) + .invoke(ErrorKind::Filesystem) + .await?; + prune_phase.complete(); + download_phase.start(); let path = Path::new("/media/startos/images") .join(hex::encode(&asset.commitment.hash[..16])) @@ -420,6 +437,72 @@ async fn do_update( .arg("boot") .invoke(crate::ErrorKind::Filesystem) .await?; + if &*PLATFORM != "raspberrypi" { + let mountpoint = "/media/startos/next"; + let root_guard = OverlayGuard::mount( + TmpMountGuard::mount(&BlockDev::new(&path), MountType::ReadOnly).await?, + mountpoint, + ) + .await?; + let startos = MountGuard::mount( + &Bind::new("/media/startos/root"), + root_guard.path().join("media/startos/root"), + MountType::ReadOnly, + ) + .await?; + let boot_guard = MountGuard::mount( + &Bind::new("/boot"), + root_guard.path().join("boot"), + MountType::ReadWrite, + ) + .await?; + let dev = MountGuard::mount( + &Bind::new("/dev"), + root_guard.path().join("dev"), + MountType::ReadWrite, + ) + .await?; + let proc = MountGuard::mount( + &Bind::new("/proc"), + root_guard.path().join("proc"), + MountType::ReadWrite, + ) + .await?; + let sys = MountGuard::mount( + &Bind::new("/sys"), + root_guard.path().join("sys"), + MountType::ReadWrite, + ) + .await?; + let efivarfs = if tokio::fs::metadata("/sys/firmware/efi").await.is_ok() { + Some( + MountGuard::mount( + &EfiVarFs, + root_guard.path().join("sys/firmware/efi/efivars"), + MountType::ReadWrite, + ) + .await?, + ) + } else { + None + }; + + Command::new("chroot") + .arg(root_guard.path()) + .arg("update-grub2") + .invoke(ErrorKind::Grub) + .await?; + + if let Some(efivarfs) = efivarfs { + efivarfs.unmount(false).await?; + } + sys.unmount(false).await?; + proc.unmount(false).await?; + dev.unmount(false).await?; + boot_guard.unmount(false).await?; + startos.unmount(false).await?; + root_guard.unmount(false).await?; + } sync_boot_phase.complete(); finalize_phase.start(); @@ -429,6 +512,7 @@ async fn do_update( .arg("/media/startos/config/current.rootfs") .invoke(crate::ErrorKind::Filesystem) .await?; + Command::new("sync").invoke(ErrorKind::Filesystem).await?; finalize_phase.complete(); progress.complete(); From e65c0a0d1d08a0571ec451601369614be91ca508 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 26 Jul 2024 19:19:19 -0600 Subject: [PATCH 098/125] fix tests --- Makefile | 16 +++++++++++----- .../transformConfigSpec.test.ts.snap | 8 ++++---- sdk/lib/osBindings/ApiState.ts | 3 +++ sdk/lib/osBindings/EchoParams.ts | 3 +++ sdk/lib/osBindings/index.ts | 2 ++ 5 files changed, 23 insertions(+), 9 deletions(-) create mode 100644 sdk/lib/osBindings/ApiState.ts create mode 100644 sdk/lib/osBindings/EchoParams.ts diff --git a/Makefile b/Makefile index 8f6bd0203..b17d91b7a 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ endif .DELETE_ON_ERROR: -.PHONY: all metadata install clean format cli uis ui reflash deb $(IMAGE_TYPE) squashfs sudo wormhole wormhole-deb test +.PHONY: all metadata install clean format cli uis ui reflash deb $(IMAGE_TYPE) squashfs sudo wormhole wormhole-deb test test-core test-sdk test-container-runtime all: $(ALL_TARGETS) @@ -89,10 +89,16 @@ clean: format: cd core && cargo +nightly fmt -test: $(CORE_SRC) $(ENVIRONMENT_FILE) - (cd core && cargo build --features=test && cargo test --features=test) - (cd sdk && make test) - (cd container-runtime && npm ci && npm test) +test: | test-core test-sdk test-container-runtime + +test-core: $(CORE_SRC) $(ENVIRONMENT_FILE) + cd core && cargo build --features=test && cargo test --features=test + +test-sdk: $(shell git ls-files sdk) sdk/lib/osBindings + cd sdk && make test + +test-container-runtime: container-runtime/node_modules $(shell git ls-files container-runtime/src) container-runtime/package.json container-runtime/tsconfig.json + cd container-runtime && npm test cli: cd core && ./install-cli.sh diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__snapshots__/transformConfigSpec.test.ts.snap b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__snapshots__/transformConfigSpec.test.ts.snap index 1d434c78d..369e8fa4b 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__snapshots__/transformConfigSpec.test.ts.snap +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__snapshots__/transformConfigSpec.test.ts.snap @@ -76,8 +76,8 @@ exports[`transformConfigSpec transformConfigSpec(embassyPages) 1`] = ` "required": false, "type": "select", "values": { - "filebrowser": undefined, - "nextcloud": undefined, + "filebrowser": "filebrowser", + "nextcloud": "nextcloud", }, "warning": null, }, @@ -192,8 +192,8 @@ exports[`transformConfigSpec transformConfigSpec(embassyPages) 1`] = ` "required": false, "type": "select", "values": { - "filebrowser": undefined, - "nextcloud": undefined, + "filebrowser": "filebrowser", + "nextcloud": "nextcloud", }, "warning": null, }, diff --git a/sdk/lib/osBindings/ApiState.ts b/sdk/lib/osBindings/ApiState.ts new file mode 100644 index 000000000..c3a43828a --- /dev/null +++ b/sdk/lib/osBindings/ApiState.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ApiState = "error" | "initializing" | "running" diff --git a/sdk/lib/osBindings/EchoParams.ts b/sdk/lib/osBindings/EchoParams.ts new file mode 100644 index 000000000..232dfb8ab --- /dev/null +++ b/sdk/lib/osBindings/EchoParams.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type EchoParams = { message: string } diff --git a/sdk/lib/osBindings/index.ts b/sdk/lib/osBindings/index.ts index 2bf2bf69c..2708aef8c 100644 --- a/sdk/lib/osBindings/index.ts +++ b/sdk/lib/osBindings/index.ts @@ -15,6 +15,7 @@ export { AlpnInfo } from "./AlpnInfo" export { AnySignature } from "./AnySignature" export { AnySigningKey } from "./AnySigningKey" export { AnyVerifyingKey } from "./AnyVerifyingKey" +export { ApiState } from "./ApiState" export { AttachParams } from "./AttachParams" export { BackupProgress } from "./BackupProgress" export { BackupTargetFS } from "./BackupTargetFS" @@ -42,6 +43,7 @@ export { DepInfo } from "./DepInfo" export { Description } from "./Description" export { DestroyOverlayedImageParams } from "./DestroyOverlayedImageParams" export { Duration } from "./Duration" +export { EchoParams } from "./EchoParams" export { EncryptedWire } from "./EncryptedWire" export { ExecuteAction } from "./ExecuteAction" export { ExportActionParams } from "./ExportActionParams" From 6f07ec259767f7b7686e2b935db1fb30fc95470e Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 26 Jul 2024 19:29:24 -0600 Subject: [PATCH 099/125] fix bindings --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b17d91b7a..adcbc6bf1 100644 --- a/Makefile +++ b/Makefile @@ -231,7 +231,7 @@ sdk/lib/osBindings: core/startos/bindings core/startos/bindings: $(shell git ls-files core) $(ENVIRONMENT_FILE) rm -rf core/startos/bindings - (cd core/ && cargo test --features=test '::export_bindings_') + (cd core/ && cargo test --features=test 'export_bindings_') touch core/startos/bindings sdk/dist: $(shell git ls-files sdk) sdk/lib/osBindings From 3e7578d6704719129fa9a01d4e06b2c3df247236 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Fri, 26 Jul 2024 20:31:09 -0600 Subject: [PATCH 100/125] bump version --- core/Cargo.lock | 2 +- core/startos/Cargo.toml | 2 +- core/startos/src/version/mod.rs | 9 ++++- core/startos/src/version/v0_3_6_alpha_2.rs | 35 +++++++++++++++++++ web/package.json | 2 +- web/patchdb-ui-seed.json | 2 +- .../modals/os-welcome/os-welcome.page.html | 2 +- 7 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 core/startos/src/version/v0_3_6_alpha_2.rs diff --git a/core/Cargo.lock b/core/Cargo.lock index 69d23b92c..9c7b79c0d 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -4961,7 +4961,7 @@ dependencies = [ [[package]] name = "start-os" -version = "0.3.6-alpha.1" +version = "0.3.6-alpha.2" dependencies = [ "aes", "async-compression", diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index 5e84aac5e..6969b5301 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -14,7 +14,7 @@ keywords = [ name = "start-os" readme = "README.md" repository = "https://github.com/Start9Labs/start-os" -version = "0.3.6-alpha.1" +version = "0.3.6-alpha.2" license = "MIT" [lib] diff --git a/core/startos/src/version/mod.rs b/core/startos/src/version/mod.rs index ebd178e37..39e6eae72 100644 --- a/core/startos/src/version/mod.rs +++ b/core/startos/src/version/mod.rs @@ -15,8 +15,9 @@ mod v0_3_5_1; mod v0_3_5_2; mod v0_3_6_alpha_0; mod v0_3_6_alpha_1; +mod v0_3_6_alpha_2; -pub type Current = v0_3_6_alpha_1::Version; +pub type Current = v0_3_6_alpha_2::Version; #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[serde(untagged)] @@ -28,6 +29,7 @@ enum Version { V0_3_5_2(Wrapper), V0_3_6_alpha_0(Wrapper), V0_3_6_alpha_1(Wrapper), + V0_3_6_alpha_2(Wrapper), Other(exver::Version), } @@ -49,6 +51,7 @@ impl Version { Version::V0_3_5_2(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_0(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_1(Wrapper(x)) => x.semver(), + Version::V0_3_6_alpha_2(Wrapper(x)) => x.semver(), Version::Other(x) => x.clone(), } } @@ -250,6 +253,7 @@ pub async fn init( Version::V0_3_5_2(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, Version::V0_3_6_alpha_0(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, Version::V0_3_6_alpha_1(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, + Version::V0_3_6_alpha_2(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, Version::Other(_) => { return Err(Error::new( eyre!("Cannot downgrade"), @@ -300,6 +304,9 @@ mod tests { Just(Version::V0_3_6_alpha_1(Wrapper( v0_3_6_alpha_1::Version::new() ))), + Just(Version::V0_3_6_alpha_2(Wrapper( + v0_3_6_alpha_2::Version::new() + ))), em_version().prop_map(Version::Other), ] } diff --git a/core/startos/src/version/v0_3_6_alpha_2.rs b/core/startos/src/version/v0_3_6_alpha_2.rs new file mode 100644 index 000000000..4b26a05dd --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_2.rs @@ -0,0 +1,35 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_1, VersionT}; +use crate::db::model::Database; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref V0_3_6_alpha_2: exver::Version = exver::Version::new( + [0, 3, 6], + [PreReleaseSegment::String("alpha".into()), 2.into()] + ); +} + +#[derive(Clone, Debug)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_1::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> exver::Version { + V0_3_6_alpha_2.clone() + } + fn compat(&self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + async fn up(&self, _db: &TypedPatchDb) -> Result<(), Error> { + Ok(()) + } + async fn down(&self, _db: &TypedPatchDb) -> Result<(), Error> { + Ok(()) + } +} diff --git a/web/package.json b/web/package.json index f20272d99..4a7047041 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "startos-ui", - "version": "0.3.6-alpha.1", + "version": "0.3.6-alpha.2", "author": "Start9 Labs, Inc", "homepage": "https://start9.com/", "license": "MIT", diff --git a/web/patchdb-ui-seed.json b/web/patchdb-ui-seed.json index 15e020cb3..f15a8d09b 100644 --- a/web/patchdb-ui-seed.json +++ b/web/patchdb-ui-seed.json @@ -21,5 +21,5 @@ "ackInstructions": {}, "theme": "Dark", "widgets": [], - "ack-welcome": "0.3.6-alpha.1" + "ack-welcome": "0.3.6-alpha.2" } diff --git a/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html b/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html index d53a0746d..258c92f83 100644 --- a/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html +++ b/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html @@ -12,7 +12,7 @@

This Release

-

0.3.6-alpha.1

+

0.3.6-alpha.2

This is an ALPHA release! DO NOT use for production data!
Expect that any data you create or store on this version of the OS can be LOST FOREVER!
From 63e26b6050382f7d9c86f4a496193063023324db Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Mon, 29 Jul 2024 10:15:46 -0600 Subject: [PATCH 101/125] fix race condition --- core/startos/src/service/persistent_container.rs | 5 +++++ core/startos/src/service/service_actor.rs | 13 +++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/core/startos/src/service/persistent_container.rs b/core/startos/src/service/persistent_container.rs index 62d3b0975..e1324b307 100644 --- a/core/startos/src/service/persistent_container.rs +++ b/core/startos/src/service/persistent_container.rs @@ -43,6 +43,8 @@ const RPC_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); #[derive(Debug)] pub struct ServiceState { + // indicates whether the service container runtime has been initialized yet + pub(super) rt_initialized: bool, // This contains the start time and health check information for when the service is running. Note: Will be overwritting to the db, pub(super) running_status: Option, // This tracks references to callbacks registered by the running service: @@ -65,6 +67,7 @@ pub struct ServiceStateKinds { impl ServiceState { pub fn new(desired_state: StartStop) -> Self { Self { + rt_initialized: false, running_status: Default::default(), callbacks: Default::default(), temp_desired_state: Default::default(), @@ -369,6 +372,8 @@ impl PersistentContainer { self.rpc_client.request(rpc::Init, Empty {}).await?; + self.state.send_modify(|s| s.rt_initialized = true); + Ok(()) } diff --git a/core/startos/src/service/service_actor.rs b/core/startos/src/service/service_actor.rs index e6e1c4ede..e6578264c 100644 --- a/core/startos/src/service/service_actor.rs +++ b/core/startos/src/service/service_actor.rs @@ -2,10 +2,9 @@ use std::sync::Arc; use std::time::Duration; use imbl::OrdMap; -use models::PackageId; use super::start_stop::StartStop; - +use super::ServiceActorSeed; use crate::prelude::*; use crate::service::transition::TransitionKind; use crate::service::SYNC_RETRY_COOLDOWN_SECONDS; @@ -13,8 +12,6 @@ use crate::status::MainStatus; use crate::util::actor::background::BackgroundJobQueue; use crate::util::actor::Actor; -use super::ServiceActorSeed; - #[derive(Clone)] pub(super) struct ServiceActor(pub(super) Arc); @@ -26,12 +23,12 @@ enum ServiceActorLoopNext { impl Actor for ServiceActor { fn init(&mut self, jobs: &BackgroundJobQueue) { let seed = self.0.clone(); + let mut current = seed.persistent_container.state.subscribe(); jobs.add_job(async move { - let id = seed.id.clone(); - let mut current = seed.persistent_container.state.subscribe(); + let _ = current.wait_for(|s| s.rt_initialized).await; loop { - match service_actor_loop(¤t, &seed, &id).await { + match service_actor_loop(¤t, &seed).await { ServiceActorLoopNext::Wait => tokio::select! { _ = current.changed() => (), }, @@ -45,8 +42,8 @@ impl Actor for ServiceActor { async fn service_actor_loop( current: &tokio::sync::watch::Receiver, seed: &Arc, - id: &PackageId, ) -> ServiceActorLoopNext { + let id = &seed.id; let kinds = current.borrow().kinds(); if let Err(e) = async { let main_status = match ( From 0ed6eb70296ffe65b3b91c1a9da7c28744d62533 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Mon, 29 Jul 2024 11:13:35 -0600 Subject: [PATCH 102/125] Fix sessions (#2689) * add loggedIn key to sessions * show loggedIn timestamp in list * don't double hash active session --------- Co-authored-by: Aiden McClelland --- core/startos/src/auth.rs | 4 ++-- core/startos/src/util/sync.rs | 3 +++ .../server-routes/sessions/sessions.page.html | 22 +++++++++++++++---- .../server-routes/sessions/sessions.page.ts | 20 +++++------------ .../ui/src/app/services/api/api.fixures.ts | 2 ++ .../ui/src/app/services/api/api.types.ts | 1 + 6 files changed, 31 insertions(+), 21 deletions(-) diff --git a/core/startos/src/auth.rs b/core/startos/src/auth.rs index 68e16c244..d998e9897 100644 --- a/core/startos/src/auth.rs +++ b/core/startos/src/auth.rs @@ -350,13 +350,13 @@ pub async fn list( ListParams { session, .. }: ListParams, ) -> Result { let mut sessions = ctx.db.peek().await.into_private().into_sessions().de()?; - ctx.ephemeral_sessions.mutate(|s| { + ctx.ephemeral_sessions.peek(|s| { sessions .0 .extend(s.0.iter().map(|(k, v)| (k.clone(), v.clone()))) }); Ok(SessionList { - current: HashSessionToken::from_token(session).hashed().clone(), + current: session, sessions, }) } diff --git a/core/startos/src/util/sync.rs b/core/startos/src/util/sync.rs index f8ec425e0..1edd21ce1 100644 --- a/core/startos/src/util/sync.rs +++ b/core/startos/src/util/sync.rs @@ -6,4 +6,7 @@ impl SyncMutex { pub fn mutate U, U>(&self, f: F) -> U { f(&mut *self.0.lock().unwrap()) } + pub fn peek U, U>(&self, f: F) -> U { + f(&*self.0.lock().unwrap()) + } } diff --git a/web/projects/ui/src/app/pages/server-routes/sessions/sessions.page.html b/web/projects/ui/src/app/pages/server-routes/sessions/sessions.page.html index 3e349b49b..2473e6041 100644 --- a/web/projects/ui/src/app/pages/server-routes/sessions/sessions.page.html +++ b/web/projects/ui/src/app/pages/server-routes/sessions/sessions.page.html @@ -52,8 +52,15 @@ >

{{ getPlatformName(currentSession.metadata.platforms) }}

-

Last Active: {{ currentSession.lastActive| date : 'medium' }}

-

{{ currentSession.userAgent }}

+

{{ agent }}

+

+ First Seen + : {{ currentSession.loggedIn| date : 'medium' }} +

+

+ Last Active + : {{ currentSession.lastActive| date : 'medium' }} +

@@ -78,8 +85,15 @@ >

{{ getPlatformName(session.metadata.platforms) }}

-

Last Active: {{ session.lastActive | date : 'medium' }}

-

{{ session.userAgent }}

+

{{ agent }}

+

+ First Seen + : {{ currentSession.loggedIn| date : 'medium' }} +

+

+ Last Active + : {{ currentSession.lastActive| date : 'medium' }} +

{ - return { - id, - ...session, - } - }) - .sort((a, b) => { - return ( - new Date(b.lastActive).valueOf() - new Date(a.lastActive).valueOf() - ) - }) + .map(([id, session]) => ({ id, ...session })) + .sort( + (a, b) => + new Date(b.lastActive).valueOf() - new Date(a.lastActive).valueOf(), + ) } catch (e: any) { this.errorService.handleError(e) } finally { @@ -108,10 +102,6 @@ export class SessionsPage { return 'Unknown Device' } } - - asIsOrder(a: any, b: any) { - return 0 - } } interface SessionWithId extends Session { diff --git a/web/projects/ui/src/app/services/api/api.fixures.ts b/web/projects/ui/src/app/services/api/api.fixures.ts index 23545d6b3..d1ce9c47c 100644 --- a/web/projects/ui/src/app/services/api/api.fixures.ts +++ b/web/projects/ui/src/app/services/api/api.fixures.ts @@ -924,6 +924,7 @@ export module Mock { current: 'b7b1a9cef4284f00af9e9dda6e676177', sessions: { '9513226517c54ddd8107d6d7b9d8aed7': { + loggedIn: '2021-07-14T20:49:17.774Z', lastActive: '2021-07-14T20:49:17.774Z', userAgent: 'AppleWebKit/{WebKit Rev} (KHTML, like Gecko)', metadata: { @@ -931,6 +932,7 @@ export module Mock { }, }, b7b1a9cef4284f00af9e9dda6e676177: { + loggedIn: '2021-07-14T20:49:17.774Z', lastActive: '2021-06-14T20:49:17.774Z', userAgent: 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0', diff --git a/web/projects/ui/src/app/services/api/api.types.ts b/web/projects/ui/src/app/services/api/api.types.ts index eac7cd7f4..faa4a072e 100644 --- a/web/projects/ui/src/app/services/api/api.types.ts +++ b/web/projects/ui/src/app/services/api/api.types.ts @@ -354,6 +354,7 @@ export interface Metric { } export interface Session { + loggedIn: string lastActive: string userAgent: string metadata: SessionMetadata From 75e5250509cbdafbe474b71051b8646e21868ac1 Mon Sep 17 00:00:00 2001 From: Jade <2364004+Blu-J@users.noreply.github.com> Date: Mon, 29 Jul 2024 11:24:10 -0600 Subject: [PATCH 103/125] fixed: Transforming for bitcoind and nostr (#2688) --- .../SystemForEmbassy/__fixtures__/bitcoind.ts | 387 +++++++++++++ .../SystemForEmbassy/__fixtures__/nostr.ts | 28 + .../transformConfigSpec.test.ts.snap | 538 ++++++++++++++++++ .../transformConfigSpec.test.ts | 10 + .../SystemForEmbassy/transformConfigSpec.ts | 8 +- 5 files changed, 967 insertions(+), 4 deletions(-) create mode 100644 container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/bitcoind.ts create mode 100644 container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/nostr.ts diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/bitcoind.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/bitcoind.ts new file mode 100644 index 000000000..9a643b39d --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/bitcoind.ts @@ -0,0 +1,387 @@ +export default { + "peer-tor-address": { + name: "Peer Tor Address", + description: "The Tor address of the peer interface", + type: "pointer", + subtype: "package", + "package-id": "bitcoind", + target: "tor-address", + interface: "peer", + }, + "rpc-tor-address": { + name: "RPC Tor Address", + description: "The Tor address of the RPC interface", + type: "pointer", + subtype: "package", + "package-id": "bitcoind", + target: "tor-address", + interface: "rpc", + }, + rpc: { + type: "object", + name: "RPC Settings", + description: "RPC configuration options.", + spec: { + enable: { + type: "boolean", + name: "Enable", + description: "Allow remote RPC requests.", + default: true, + }, + username: { + type: "string", + nullable: false, + name: "Username", + description: "The username for connecting to Bitcoin over RPC.", + warning: + "You will need to restart all services that depend on Bitcoin.", + default: "bitcoin", + masked: true, + pattern: "^[a-zA-Z0-9_]+$", + "pattern-description": "Must be alphanumeric (can contain underscore).", + }, + password: { + type: "string", + nullable: false, + name: "RPC Password", + description: "The password for connecting to Bitcoin over RPC.", + warning: + "You will need to restart all services that depend on Bitcoin.", + default: { + charset: "a-z,2-7", + len: 20, + }, + pattern: "^[a-zA-Z0-9_]+$", + "pattern-description": "Must be alphanumeric (can contain underscore).", + copyable: true, + masked: true, + }, + advanced: { + type: "object", + name: "Advanced", + description: "Advanced RPC Settings", + spec: { + auth: { + name: "Authorization", + description: + "Username and hashed password for JSON-RPC connections. RPC clients connect using the usual http basic authentication.", + type: "list", + subtype: "string", + default: [], + spec: { + pattern: "^[a-zA-Z0-9_-]+:([0-9a-fA-F]{2})+\\$([0-9a-fA-F]{2})+$", + "pattern-description": + 'Each item must be of the form ":$".', + }, + range: "[0,*)", + }, + servertimeout: { + name: "Rpc Server Timeout", + description: + "Number of seconds after which an uncompleted RPC call will time out.", + type: "number", + nullable: false, + range: "[5,300]", + integral: true, + units: "seconds", + default: 30, + }, + threads: { + name: "Threads", + description: + "Set the number of threads for handling RPC calls. You may wish to increase this if you are making lots of calls via an integration.", + type: "number", + nullable: false, + default: 16, + range: "[1,64]", + integral: true, + units: undefined, + }, + workqueue: { + name: "Work Queue", + description: + "Set the depth of the work queue to service RPC calls. Determines how long the backlog of RPC requests can get before it just rejects new ones.", + type: "number", + nullable: false, + default: 128, + range: "[8,256]", + integral: true, + units: "requests", + }, + }, + }, + }, + }, + "zmq-enabled": { + type: "boolean", + name: "ZeroMQ Enabled", + description: + "The ZeroMQ interface is useful for some applications which might require data related to block and transaction events from Bitcoin Core. For example, LND requires ZeroMQ be enabled for LND to get the latest block data", + default: true, + }, + txindex: { + type: "boolean", + name: "Transaction Index", + description: + "By enabling Transaction Index (txindex) Bitcoin Core will build a complete transaction index. This allows Bitcoin Core to access any transaction with commands like `gettransaction`.", + default: true, + }, + coinstatsindex: { + type: "boolean", + name: "Coinstats Index", + description: + "Enabling Coinstats Index reduces the time for the gettxoutsetinfo RPC to complete at the cost of using additional disk space", + default: false, + }, + wallet: { + type: "object", + name: "Wallet", + description: "Wallet Settings", + spec: { + enable: { + name: "Enable Wallet", + description: "Load the wallet and enable wallet RPC calls.", + type: "boolean", + default: true, + }, + avoidpartialspends: { + name: "Avoid Partial Spends", + description: + "Group outputs by address, selecting all or none, instead of selecting on a per-output basis. This improves privacy at the expense of higher transaction fees.", + type: "boolean", + default: true, + }, + discardfee: { + name: "Discard Change Tolerance", + description: + "The fee rate (in BTC/kB) that indicates your tolerance for discarding change by adding it to the fee.", + type: "number", + nullable: false, + default: 0.0001, + range: "[0,.01]", + integral: false, + units: "BTC/kB", + }, + }, + }, + advanced: { + type: "object", + name: "Advanced", + description: "Advanced Settings", + spec: { + mempool: { + type: "object", + name: "Mempool", + description: "Mempool Settings", + spec: { + persistmempool: { + type: "boolean", + name: "Persist Mempool", + description: "Save the mempool on shutdown and load on restart.", + default: true, + }, + maxmempool: { + type: "number", + nullable: false, + name: "Max Mempool Size", + description: + "Keep the transaction memory pool below megabytes.", + range: "[1,*)", + integral: true, + units: "MiB", + default: 300, + }, + mempoolexpiry: { + type: "number", + nullable: false, + name: "Mempool Expiration", + description: + "Do not keep transactions in the mempool longer than hours.", + range: "[1,*)", + integral: true, + units: "Hr", + default: 336, + }, + mempoolfullrbf: { + name: "Enable Full RBF", + description: + "Policy for your node to use for relaying and mining unconfirmed transactions. For details, see https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-24.0.1.md#notice-of-new-option-for-transaction-replacement-policies", + type: "boolean", + default: true, + }, + permitbaremultisig: { + type: "boolean", + name: "Permit Bare Multisig", + description: "Relay non-P2SH multisig transactions", + default: true, + }, + datacarrier: { + type: "boolean", + name: "Relay OP_RETURN Transactions", + description: "Relay transactions with OP_RETURN outputs", + default: true, + }, + datacarriersize: { + type: "number", + nullable: false, + name: "Max OP_RETURN Size", + description: "Maximum size of data in OP_RETURN outputs to relay", + range: "[0,10000]", + integral: true, + units: "bytes", + default: 83, + }, + }, + }, + peers: { + type: "object", + name: "Peers", + description: "Peer Connection Settings", + spec: { + listen: { + type: "boolean", + name: "Make Public", + description: + "Allow other nodes to find your server on the network.", + default: true, + }, + onlyconnect: { + type: "boolean", + name: "Disable Peer Discovery", + description: "Only connect to specified peers.", + default: false, + }, + onlyonion: { + type: "boolean", + name: "Disable Clearnet", + description: "Only connect to peers over Tor.", + default: false, + }, + v2transport: { + type: "boolean", + name: "Use V2 P2P Transport Protocol", + description: + "Enable or disable the use of BIP324 V2 P2P transport protocol.", + default: false, + }, + addnode: { + name: "Add Nodes", + description: "Add addresses of nodes to connect to.", + type: "list", + subtype: "object", + range: "[0,*)", + default: [], + spec: { + spec: { + hostname: { + type: "string", + nullable: false, + name: "Hostname", + description: "Domain or IP address of bitcoin peer", + pattern: + "(^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))", + "pattern-description": + "Must be either a domain name, or an IPv4 or IPv6 address. Do not include protocol scheme (eg 'http://') or port.", + }, + port: { + type: "number", + nullable: true, + name: "Port", + description: + "Port that peer is listening on for inbound p2p connections", + range: "[0,65535]", + integral: true, + }, + }, + }, + }, + }, + }, + pruning: { + type: "union", + name: "Pruning Settings", + description: + "Blockchain Pruning Options\nReduce the blockchain size on disk\n", + warning: + "Disabling pruning will convert your node into a full archival node. This requires a resync of the entire blockchain, a process that may take several days.\n", + tag: { + id: "mode", + name: "Pruning Mode", + description: + "- Disabled: Disable pruning\n- Automatic: Limit blockchain size on disk to a certain number of megabytes\n", + "variant-names": { + disabled: "Disabled", + automatic: "Automatic", + }, + }, + variants: { + disabled: {}, + automatic: { + size: { + type: "number", + nullable: false, + name: "Max Chain Size", + description: "Limit of blockchain size on disk.", + warning: + "Increasing this value will require re-syncing your node.", + default: 550, + range: "[550,1000000)", + integral: true, + units: "MiB", + }, + }, + }, + default: "disabled", + }, + dbcache: { + type: "number", + nullable: true, + name: "Database Cache", + description: + "How much RAM to allocate for caching the TXO set. Higher values improve syncing performance, but increase your chance of using up all your system's memory or corrupting your database in the event of an ungraceful shutdown. Set this high but comfortably below your system's total RAM during IBD, then turn down to 450 (or leave blank) once the sync completes.", + warning: + "WARNING: Increasing this value results in a higher chance of ungraceful shutdowns, which can leave your node unusable if it happens during the initial block download. Use this setting with caution. Be sure to set this back to the default (450 or leave blank) once your node is synced. DO NOT press the STOP button if your dbcache is large. Instead, set this number back to the default, hit save, and wait for bitcoind to restart on its own.", + range: "(0,*)", + integral: true, + units: "MiB", + }, + blockfilters: { + type: "object", + name: "Block Filters", + description: "Settings for storing and serving compact block filters", + spec: { + blockfilterindex: { + type: "boolean", + name: "Compute Compact Block Filters (BIP158)", + description: + "Generate Compact Block Filters during initial sync (IBD) to enable 'getblockfilter' RPC. This is useful if dependent services need block filters to efficiently scan for addresses/transactions etc.", + default: true, + }, + peerblockfilters: { + type: "boolean", + name: "Serve Compact Block Filters to Peers (BIP157)", + description: + "Serve Compact Block Filters as a peer service to other nodes on the network. This is useful if you wish to connect an SPV client to your node to make it efficient to scan transactions without having to download all block data. 'Compute Compact Block Filters (BIP158)' is required.", + default: false, + }, + }, + }, + bloomfilters: { + type: "object", + name: "Bloom Filters (BIP37)", + description: "Setting for serving Bloom Filters", + spec: { + peerbloomfilters: { + type: "boolean", + name: "Serve Bloom Filters to Peers", + description: + "Peers have the option of setting filters on each connection they make after the version handshake has completed. Bloom filters are for clients implementing SPV (Simplified Payment Verification) that want to check that block headers connect together correctly, without needing to verify the full blockchain. The client must trust that the transactions in the chain are in fact valid. It is highly recommended AGAINST using for anything except Bisq integration.", + warning: + "This is ONLY for use with Bisq integration, please use Block Filters for all other applications.", + default: false, + }, + }, + }, + }, + }, +} diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/nostr.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/nostr.ts new file mode 100644 index 000000000..f5a93a918 --- /dev/null +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/nostr.ts @@ -0,0 +1,28 @@ +export default { + "tor-address": { + name: "Tor Address", + description: "The Tor address of the network interface", + type: "pointer", + subtype: "package", + "package-id": "nostr-wallet-connect", + target: "tor-address", + interface: "main", + }, + "lan-address": { + name: "LAN Address", + description: "The LAN address of the network interface", + type: "pointer", + subtype: "package", + "package-id": "nostr-wallet-connect", + target: "lan-address", + interface: "main", + }, + "nostr-relay": { + type: "string", + name: "Nostr Relay", + default: "wss://relay.getalby.com/v1", + description: "The Nostr Relay to use for Nostr Wallet Connect connections", + copyable: true, + nullable: false, + }, +} diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__snapshots__/transformConfigSpec.test.ts.snap b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__snapshots__/transformConfigSpec.test.ts.snap index 369e8fa4b..9eb6e97cf 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__snapshots__/transformConfigSpec.test.ts.snap +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__snapshots__/transformConfigSpec.test.ts.snap @@ -1,5 +1,521 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`transformConfigSpec transformConfigSpec(bitcoind) 1`] = ` +{ + "advanced": { + "description": "Advanced Settings", + "name": "Advanced", + "spec": { + "blockfilters": { + "description": "Settings for storing and serving compact block filters", + "name": "Block Filters", + "spec": { + "blockfilterindex": { + "default": true, + "description": "Generate Compact Block Filters during initial sync (IBD) to enable 'getblockfilter' RPC. This is useful if dependent services need block filters to efficiently scan for addresses/transactions etc.", + "disabled": false, + "immutable": false, + "name": "Compute Compact Block Filters (BIP158)", + "type": "toggle", + "warning": null, + }, + "peerblockfilters": { + "default": false, + "description": "Serve Compact Block Filters as a peer service to other nodes on the network. This is useful if you wish to connect an SPV client to your node to make it efficient to scan transactions without having to download all block data. 'Compute Compact Block Filters (BIP158)' is required.", + "disabled": false, + "immutable": false, + "name": "Serve Compact Block Filters to Peers (BIP157)", + "type": "toggle", + "warning": null, + }, + }, + "type": "object", + "warning": null, + }, + "bloomfilters": { + "description": "Setting for serving Bloom Filters", + "name": "Bloom Filters (BIP37)", + "spec": { + "peerbloomfilters": { + "default": false, + "description": "Peers have the option of setting filters on each connection they make after the version handshake has completed. Bloom filters are for clients implementing SPV (Simplified Payment Verification) that want to check that block headers connect together correctly, without needing to verify the full blockchain. The client must trust that the transactions in the chain are in fact valid. It is highly recommended AGAINST using for anything except Bisq integration.", + "disabled": false, + "immutable": false, + "name": "Serve Bloom Filters to Peers", + "type": "toggle", + "warning": "This is ONLY for use with Bisq integration, please use Block Filters for all other applications.", + }, + }, + "type": "object", + "warning": null, + }, + "dbcache": { + "default": null, + "description": "How much RAM to allocate for caching the TXO set. Higher values improve syncing performance, but increase your chance of using up all your system's memory or corrupting your database in the event of an ungraceful shutdown. Set this high but comfortably below your system's total RAM during IBD, then turn down to 450 (or leave blank) once the sync completes.", + "disabled": false, + "immutable": false, + "integer": true, + "max": null, + "min": null, + "name": "Database Cache", + "placeholder": null, + "required": false, + "step": null, + "type": "number", + "units": "MiB", + "warning": "WARNING: Increasing this value results in a higher chance of ungraceful shutdowns, which can leave your node unusable if it happens during the initial block download. Use this setting with caution. Be sure to set this back to the default (450 or leave blank) once your node is synced. DO NOT press the STOP button if your dbcache is large. Instead, set this number back to the default, hit save, and wait for bitcoind to restart on its own.", + }, + "mempool": { + "description": "Mempool Settings", + "name": "Mempool", + "spec": { + "datacarrier": { + "default": true, + "description": "Relay transactions with OP_RETURN outputs", + "disabled": false, + "immutable": false, + "name": "Relay OP_RETURN Transactions", + "type": "toggle", + "warning": null, + }, + "datacarriersize": { + "default": 83, + "description": "Maximum size of data in OP_RETURN outputs to relay", + "disabled": false, + "immutable": false, + "integer": true, + "max": 10000, + "min": null, + "name": "Max OP_RETURN Size", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "bytes", + "warning": null, + }, + "maxmempool": { + "default": 300, + "description": "Keep the transaction memory pool below megabytes.", + "disabled": false, + "immutable": false, + "integer": true, + "max": null, + "min": 1, + "name": "Max Mempool Size", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "MiB", + "warning": null, + }, + "mempoolexpiry": { + "default": 336, + "description": "Do not keep transactions in the mempool longer than hours.", + "disabled": false, + "immutable": false, + "integer": true, + "max": null, + "min": 1, + "name": "Mempool Expiration", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "Hr", + "warning": null, + }, + "mempoolfullrbf": { + "default": true, + "description": "Policy for your node to use for relaying and mining unconfirmed transactions. For details, see https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-24.0.1.md#notice-of-new-option-for-transaction-replacement-policies", + "disabled": false, + "immutable": false, + "name": "Enable Full RBF", + "type": "toggle", + "warning": null, + }, + "permitbaremultisig": { + "default": true, + "description": "Relay non-P2SH multisig transactions", + "disabled": false, + "immutable": false, + "name": "Permit Bare Multisig", + "type": "toggle", + "warning": null, + }, + "persistmempool": { + "default": true, + "description": "Save the mempool on shutdown and load on restart.", + "disabled": false, + "immutable": false, + "name": "Persist Mempool", + "type": "toggle", + "warning": null, + }, + }, + "type": "object", + "warning": null, + }, + "peers": { + "description": "Peer Connection Settings", + "name": "Peers", + "spec": { + "addnode": { + "default": [], + "description": "Add addresses of nodes to connect to.", + "disabled": false, + "maxLength": null, + "minLength": null, + "name": "Add Nodes", + "spec": { + "displayAs": null, + "spec": { + "hostname": { + "default": null, + "description": "Domain or IP address of bitcoin peer", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "name": "Hostname", + "patterns": [ + { + "description": "Must be either a domain name, or an IPv4 or IPv6 address. Do not include protocol scheme (eg 'http://') or port.", + "regex": "(^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))", + }, + ], + "placeholder": null, + "required": true, + "type": "text", + "warning": null, + }, + "port": { + "default": null, + "description": "Port that peer is listening on for inbound p2p connections", + "disabled": false, + "immutable": false, + "integer": true, + "max": 65535, + "min": null, + "name": "Port", + "placeholder": null, + "required": false, + "step": null, + "type": "number", + "units": null, + "warning": null, + }, + }, + "type": "object", + "uniqueBy": null, + }, + "type": "list", + "warning": null, + }, + "listen": { + "default": true, + "description": "Allow other nodes to find your server on the network.", + "disabled": false, + "immutable": false, + "name": "Make Public", + "type": "toggle", + "warning": null, + }, + "onlyconnect": { + "default": false, + "description": "Only connect to specified peers.", + "disabled": false, + "immutable": false, + "name": "Disable Peer Discovery", + "type": "toggle", + "warning": null, + }, + "onlyonion": { + "default": false, + "description": "Only connect to peers over Tor.", + "disabled": false, + "immutable": false, + "name": "Disable Clearnet", + "type": "toggle", + "warning": null, + }, + "v2transport": { + "default": false, + "description": "Enable or disable the use of BIP324 V2 P2P transport protocol.", + "disabled": false, + "immutable": false, + "name": "Use V2 P2P Transport Protocol", + "type": "toggle", + "warning": null, + }, + }, + "type": "object", + "warning": null, + }, + "pruning": { + "default": "disabled", + "description": "- Disabled: Disable pruning +- Automatic: Limit blockchain size on disk to a certain number of megabytes +", + "disabled": false, + "immutable": false, + "name": "Pruning Mode", + "required": true, + "type": "union", + "variants": { + "automatic": { + "name": "Automatic", + "spec": { + "size": { + "default": 550, + "description": "Limit of blockchain size on disk.", + "disabled": false, + "immutable": false, + "integer": true, + "max": 999999, + "min": 550, + "name": "Max Chain Size", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "MiB", + "warning": "Increasing this value will require re-syncing your node.", + }, + }, + }, + "disabled": { + "name": "Disabled", + "spec": {}, + }, + }, + "warning": null, + }, + }, + "type": "object", + "warning": null, + }, + "coinstatsindex": { + "default": false, + "description": "Enabling Coinstats Index reduces the time for the gettxoutsetinfo RPC to complete at the cost of using additional disk space", + "disabled": false, + "immutable": false, + "name": "Coinstats Index", + "type": "toggle", + "warning": null, + }, + "rpc": { + "description": "RPC configuration options.", + "name": "RPC Settings", + "spec": { + "advanced": { + "description": "Advanced RPC Settings", + "name": "Advanced", + "spec": { + "auth": { + "default": [], + "description": "Username and hashed password for JSON-RPC connections. RPC clients connect using the usual http basic authentication.", + "disabled": false, + "maxLength": null, + "minLength": null, + "name": "Authorization", + "spec": { + "generate": null, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "patterns": [ + { + "description": "Each item must be of the form ":$".", + "regex": "^[a-zA-Z0-9_-]+:([0-9a-fA-F]{2})+\\$([0-9a-fA-F]{2})+$", + }, + ], + "placeholder": null, + "type": "text", + }, + "type": "list", + "warning": null, + }, + "servertimeout": { + "default": 30, + "description": "Number of seconds after which an uncompleted RPC call will time out.", + "disabled": false, + "immutable": false, + "integer": true, + "max": 300, + "min": 5, + "name": "Rpc Server Timeout", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "seconds", + "warning": null, + }, + "threads": { + "default": 16, + "description": "Set the number of threads for handling RPC calls. You may wish to increase this if you are making lots of calls via an integration.", + "disabled": false, + "immutable": false, + "integer": true, + "max": 64, + "min": 1, + "name": "Threads", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": null, + "warning": null, + }, + "workqueue": { + "default": 128, + "description": "Set the depth of the work queue to service RPC calls. Determines how long the backlog of RPC requests can get before it just rejects new ones.", + "disabled": false, + "immutable": false, + "integer": true, + "max": 256, + "min": 8, + "name": "Work Queue", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "requests", + "warning": null, + }, + }, + "type": "object", + "warning": null, + }, + "enable": { + "default": true, + "description": "Allow remote RPC requests.", + "disabled": false, + "immutable": false, + "name": "Enable", + "type": "toggle", + "warning": null, + }, + "password": { + "default": { + "charset": "a-z,2-7", + "len": 20, + }, + "description": "The password for connecting to Bitcoin over RPC.", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": true, + "maxLength": null, + "minLength": null, + "name": "RPC Password", + "patterns": [ + { + "description": "Must be alphanumeric (can contain underscore).", + "regex": "^[a-zA-Z0-9_]+$", + }, + ], + "placeholder": null, + "required": true, + "type": "text", + "warning": "You will need to restart all services that depend on Bitcoin.", + }, + "username": { + "default": "bitcoin", + "description": "The username for connecting to Bitcoin over RPC.", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": true, + "maxLength": null, + "minLength": null, + "name": "Username", + "patterns": [ + { + "description": "Must be alphanumeric (can contain underscore).", + "regex": "^[a-zA-Z0-9_]+$", + }, + ], + "placeholder": null, + "required": true, + "type": "text", + "warning": "You will need to restart all services that depend on Bitcoin.", + }, + }, + "type": "object", + "warning": null, + }, + "txindex": { + "default": true, + "description": "By enabling Transaction Index (txindex) Bitcoin Core will build a complete transaction index. This allows Bitcoin Core to access any transaction with commands like \`gettransaction\`.", + "disabled": false, + "immutable": false, + "name": "Transaction Index", + "type": "toggle", + "warning": null, + }, + "wallet": { + "description": "Wallet Settings", + "name": "Wallet", + "spec": { + "avoidpartialspends": { + "default": true, + "description": "Group outputs by address, selecting all or none, instead of selecting on a per-output basis. This improves privacy at the expense of higher transaction fees.", + "disabled": false, + "immutable": false, + "name": "Avoid Partial Spends", + "type": "toggle", + "warning": null, + }, + "discardfee": { + "default": 0.0001, + "description": "The fee rate (in BTC/kB) that indicates your tolerance for discarding change by adding it to the fee.", + "disabled": false, + "immutable": false, + "integer": false, + "max": 0.01, + "min": null, + "name": "Discard Change Tolerance", + "placeholder": null, + "required": true, + "step": null, + "type": "number", + "units": "BTC/kB", + "warning": null, + }, + "enable": { + "default": true, + "description": "Load the wallet and enable wallet RPC calls.", + "disabled": false, + "immutable": false, + "name": "Enable Wallet", + "type": "toggle", + "warning": null, + }, + }, + "type": "object", + "warning": null, + }, + "zmq-enabled": { + "default": true, + "description": "The ZeroMQ interface is useful for some applications which might require data related to block and transaction events from Bitcoin Core. For example, LND requires ZeroMQ be enabled for LND to get the latest block data", + "disabled": false, + "immutable": false, + "name": "ZeroMQ Enabled", + "type": "toggle", + "warning": null, + }, +} +`; + exports[`transformConfigSpec transformConfigSpec(embassyPages) 1`] = ` { "homepage": { @@ -212,6 +728,28 @@ exports[`transformConfigSpec transformConfigSpec(embassyPages) 1`] = ` } `; +exports[`transformConfigSpec transformConfigSpec(nostr) 1`] = ` +{ + "nostr-relay": { + "default": "wss://relay.getalby.com/v1", + "description": "The Nostr Relay to use for Nostr Wallet Connect connections", + "disabled": false, + "generate": null, + "immutable": false, + "inputmode": "text", + "masked": false, + "maxLength": null, + "minLength": null, + "name": "Nostr Relay", + "patterns": [], + "placeholder": null, + "required": true, + "type": "text", + "warning": null, + }, +} +`; + exports[`transformConfigSpec transformConfigSpec(searNXG) 1`] = ` { "enable-metrics": { diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.test.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.test.ts index e3f463d03..79caef377 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.test.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.test.ts @@ -1,6 +1,8 @@ import { matchOldConfigSpec, transformConfigSpec } from "./transformConfigSpec" import fixtureEmbasyPagesConfig from "./__fixtures__/embasyPagesConfig" import searNXG from "./__fixtures__/searNXG" +import bitcoind from "./__fixtures__/bitcoind" +import nostr from "./__fixtures__/nostr" describe("transformConfigSpec", () => { test("matchOldConfigSpec(embassyPages.homepage.variants[web-page])", () => { @@ -20,4 +22,12 @@ describe("transformConfigSpec", () => { const spec = matchOldConfigSpec.unsafeCast(searNXG) expect(transformConfigSpec(spec)).toMatchSnapshot() }) + test("transformConfigSpec(bitcoind)", () => { + const spec = matchOldConfigSpec.unsafeCast(bitcoind) + expect(transformConfigSpec(spec)).toMatchSnapshot() + }) + test("transformConfigSpec(nostr)", () => { + const spec = matchOldConfigSpec.unsafeCast(nostr) + expect(transformConfigSpec(spec)).toMatchSnapshot() + }) }) diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts index 9a82d5c16..706e0b941 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/transformConfigSpec.ts @@ -285,7 +285,7 @@ function getListSpec( : [], minLength: null, maxLength: null, - masked: oldVal.spec.masked, + masked: oldVal.spec.masked || false, generate: null, inputmode: "text", placeholder: oldVal.spec.placeholder || null, @@ -301,7 +301,7 @@ function getListSpec( spec: transformConfigSpec( matchOldConfigSpec.unsafeCast(oldVal.spec.spec), ), - uniqueBy: oldVal.spec["unique-by"], + uniqueBy: oldVal.spec["unique-by"] || null, displayAs: oldVal.spec["display-as"] || null, }, } @@ -482,7 +482,7 @@ const matchOldListValueSpecObject = object( "unique-by": matchOldUniqueBy, // indicates whether duplicates can be permitted in the list "display-as": string, // this should be a handlebars template which can make use of the entire config which corresponds to 'spec' }, - ["display-as"], + ["display-as", "unique-by"], ) const matchOldListValueSpecString = object( { @@ -492,7 +492,7 @@ const matchOldListValueSpecString = object( "pattern-description": string, placeholder: string, }, - ["pattern", "pattern-description", "placeholder"], + ["pattern", "pattern-description", "placeholder", "copyable", "masked"], ) const matchOldListValueSpecEnum = object({ From dafa6385588f793429f3b6d4072509709e535931 Mon Sep 17 00:00:00 2001 From: Mariusz Kogen Date: Mon, 29 Jul 2024 19:25:01 +0200 Subject: [PATCH 104/125] fix SSH Key message (#2686) * fix SSH Key message * Update web/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.ts --------- Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com> --- .../ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.ts b/web/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.ts index 5e474db0f..6cef44cba 100644 --- a/web/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/ssh-keys/ssh-keys.page.ts @@ -80,7 +80,7 @@ const ADD_OPTIONS: Partial> = { label: 'SSH Key', data: { message: - 'Enter the SSH public key you would like to authorize for root access to your Embassy.', + 'Enter the SSH public key you would like to authorize for root access to your StartOS Server.', }, } From 08003c59b678180d95463097c3998e4da87e290b Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Mon, 29 Jul 2024 11:15:25 -0600 Subject: [PATCH 105/125] don't lazily unmount unless on error --- core/startos/src/context/setup.rs | 12 ------------ core/startos/src/disk/main.rs | 2 +- core/startos/src/disk/mount/guard.rs | 4 ++-- core/startos/src/disk/mount/util.rs | 13 ++++++++----- core/startos/src/init.rs | 2 +- core/startos/src/lxc/mod.rs | 6 +++++- 6 files changed, 17 insertions(+), 22 deletions(-) diff --git a/core/startos/src/context/setup.rs b/core/startos/src/context/setup.rs index de8dced07..999154977 100644 --- a/core/startos/src/context/setup.rs +++ b/core/startos/src/context/setup.rs @@ -100,18 +100,6 @@ impl SetupContext { .with_ctx(|_| (crate::ErrorKind::Filesystem, db_path.display().to_string()))?; Ok(db) } - #[instrument(skip_all)] - pub async fn secret_store(&self) -> Result { - init_postgres(&self.datadir).await?; - let secret_store = - PgPool::connect_with(PgConnectOptions::new().database("secrets").username("root")) - .await?; - sqlx::migrate!() - .run(&secret_store) - .await - .with_kind(crate::ErrorKind::Database)?; - Ok(secret_store) - } pub fn run_setup(&self, f: F) -> Result<(), Error> where diff --git a/core/startos/src/disk/main.rs b/core/startos/src/disk/main.rs index 3a13c5dca..73aca4010 100644 --- a/core/startos/src/disk/main.rs +++ b/core/startos/src/disk/main.rs @@ -168,7 +168,7 @@ pub async fn create_all_fs>( #[instrument(skip_all)] pub async fn unmount_fs>(guid: &str, datadir: P, name: &str) -> Result<(), Error> { - unmount(datadir.as_ref().join(name)).await?; + unmount(datadir.as_ref().join(name), false).await?; if !guid.ends_with("_UNENC") { Command::new("cryptsetup") .arg("-q") diff --git a/core/startos/src/disk/mount/guard.rs b/core/startos/src/disk/mount/guard.rs index d6e7e3da1..a2d577226 100644 --- a/core/startos/src/disk/mount/guard.rs +++ b/core/startos/src/disk/mount/guard.rs @@ -74,7 +74,7 @@ impl MountGuard { } pub async fn unmount(mut self, delete_mountpoint: bool) -> Result<(), Error> { if self.mounted { - unmount(&self.mountpoint).await?; + unmount(&self.mountpoint, false).await?; if delete_mountpoint { match tokio::fs::remove_dir(&self.mountpoint).await { Err(e) if e.raw_os_error() == Some(39) => Ok(()), // directory not empty @@ -96,7 +96,7 @@ impl Drop for MountGuard { fn drop(&mut self) { if self.mounted { let mountpoint = std::mem::take(&mut self.mountpoint); - tokio::spawn(async move { unmount(mountpoint).await.unwrap() }); + tokio::spawn(async move { unmount(mountpoint, true).await.unwrap() }); } } } diff --git a/core/startos/src/disk/mount/util.rs b/core/startos/src/disk/mount/util.rs index e93ceb7dd..674f33304 100644 --- a/core/startos/src/disk/mount/util.rs +++ b/core/startos/src/disk/mount/util.rs @@ -23,7 +23,7 @@ pub async fn bind, P1: AsRef>( .status() .await?; if is_mountpoint.success() { - unmount(dst.as_ref()).await?; + unmount(dst.as_ref(), true).await?; } tokio::fs::create_dir_all(&src).await?; tokio::fs::create_dir_all(&dst).await?; @@ -41,11 +41,14 @@ pub async fn bind, P1: AsRef>( } #[instrument(skip_all)] -pub async fn unmount>(mountpoint: P) -> Result<(), Error> { +pub async fn unmount>(mountpoint: P, lazy: bool) -> Result<(), Error> { tracing::debug!("Unmounting {}.", mountpoint.as_ref().display()); - tokio::process::Command::new("umount") - .arg("-Rl") - .arg(mountpoint.as_ref()) + let mut cmd = tokio::process::Command::new("umount"); + cmd.arg("-R"); + if lazy { + cmd.arg("-l"); + } + cmd.arg(mountpoint.as_ref()) .invoke(crate::ErrorKind::Filesystem) .await?; Ok(()) diff --git a/core/startos/src/init.rs b/core/startos/src/init.rs index 7b944ebe3..e6b7be598 100644 --- a/core/startos/src/init.rs +++ b/core/startos/src/init.rs @@ -65,7 +65,7 @@ pub async fn init_postgres(datadir: impl AsRef) -> Result<(), Error> { .await? .success() { - unmount("/var/lib/postgresql").await?; + unmount("/var/lib/postgresql", true).await?; } let exists = tokio::fs::metadata(&db_dir).await.is_ok(); if !exists { diff --git a/core/startos/src/lxc/mod.rs b/core/startos/src/lxc/mod.rs index 99f019d5a..8b8cab9a0 100644 --- a/core/startos/src/lxc/mod.rs +++ b/core/startos/src/lxc/mod.rs @@ -123,7 +123,11 @@ impl LxcManager { if !expected.contains(&ContainerId::try_from(container)?) { let rootfs_path = Path::new(LXC_CONTAINER_DIR).join(container).join("rootfs"); if tokio::fs::metadata(&rootfs_path).await.is_ok() { - unmount(Path::new(LXC_CONTAINER_DIR).join(container).join("rootfs")).await?; + unmount( + Path::new(LXC_CONTAINER_DIR).join(container).join("rootfs"), + true, + ) + .await?; if tokio_stream::wrappers::ReadDirStream::new( tokio::fs::read_dir(&rootfs_path).await?, ) From ccbb68aa0cdb7f88b2b50f16db336954293f56f2 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Mon, 29 Jul 2024 11:34:59 -0600 Subject: [PATCH 106/125] fix instructions on installed packages --- core/startos/src/net/static_server.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/core/startos/src/net/static_server.rs b/core/startos/src/net/static_server.rs index cd93a9d65..f1da91851 100644 --- a/core/startos/src/net/static_server.rs +++ b/core/startos/src/net/static_server.rs @@ -272,9 +272,8 @@ fn s9pk_router(ctx: RpcContext) -> Router { .route("/installed/:s9pk/*path", { let ctx = ctx.clone(); any( - |x::Path(s9pk): x::Path, - x::Path(path): x::Path, - x::Query(commitment): x::Query>, + |x::Path((s9pk, path)): x::Path<(String, PathBuf)>, + x::RawQuery(query): x::RawQuery, request: Request| async move { if_authorized(&ctx, request, |request| async { let s9pk = S9pk::deserialize( @@ -287,7 +286,12 @@ fn s9pk_router(ctx: RpcContext) -> Router { ) .await?, ), - commitment.as_ref(), + query + .as_deref() + .map(MerkleArchiveCommitment::from_query) + .and_then(|a| a.transpose()) + .transpose()? + .as_ref(), ) .await?; let (parts, _) = request.into_parts(); From 36cc9cc1ec14db6ebfd645cb33d70c7d6fdbfc42 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Mon, 29 Jul 2024 12:20:13 -0600 Subject: [PATCH 107/125] fix firmware checker --- core/startos/src/firmware.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/startos/src/firmware.rs b/core/startos/src/firmware.rs index 7e7b9d70f..a70cf9e47 100644 --- a/core/startos/src/firmware.rs +++ b/core/startos/src/firmware.rs @@ -13,8 +13,8 @@ use crate::util::Invoke; use crate::PLATFORM; /// Part of the Firmware, look there for more about -#[derive(Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] pub struct VersionMatcher { /// Strip this prefix on the version matcher semver_prefix: Option, @@ -27,8 +27,8 @@ pub struct VersionMatcher { /// Inside a file that is firmware.json, we /// wanted a structure that could help decide what to do /// for each of the firmware versions -#[derive(Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] pub struct Firmware { id: String, /// This is the platform(s) the firmware was built for @@ -49,6 +49,7 @@ pub fn display_firmware_update_result(result: RequiresReboot) { } } +#[instrument] pub async fn check_for_firmware_update() -> Result, Error> { let system_product_name = String::from_utf8( Command::new("dmidecode") @@ -118,6 +119,7 @@ pub async fn check_for_firmware_update() -> Result, Error> { /// that the firmware was the correct and updated for /// systems like the Pure System that a new firmware /// was released and the updates where pushed through the pure os. +#[instrument] pub async fn update_firmware(firmware: Firmware) -> Result<(), Error> { let id = &firmware.id; let firmware_dir = Path::new("/usr/lib/startos/firmware"); From 0bc6f972b234d7c8a5ed079cc7a34d9b37338f75 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Mon, 29 Jul 2024 13:00:48 -0600 Subject: [PATCH 108/125] reserialize getConfig response for backwards compatibility --- .../Adapters/Systems/SystemForEmbassy/__fixtures__/bitcoind.ts | 2 +- .../src/Adapters/Systems/SystemForEmbassy/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/bitcoind.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/bitcoind.ts index 9a643b39d..1134d198a 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/bitcoind.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/bitcoind.ts @@ -315,7 +315,7 @@ export default { }, }, variants: { - disabled: {}, + disabled: undefined, automatic: { size: { type: "number", diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index ff5a6ee74..a7a30c34a 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -577,7 +577,7 @@ export class SystemForEmbassy implements System { if (!method) throw new Error("Expecting that the method getConfig exists") return (await method(polyfillEffects(effects, this.manifest)).then( (x) => { - if ("result" in x) return x.result + if ("result" in x) return JSON.parse(JSON.stringify(x.result)) if ("error" in x) throw new Error("Error getting config: " + x.error) throw new Error("Error getting config: " + x["error-code"][1]) }, From bca75a3ea4dbf04efdbf2035bed3ff48fd01c963 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Mon, 29 Jul 2024 13:18:04 -0600 Subject: [PATCH 109/125] stop container before unmounting logs --- core/startos/src/lxc/mod.rs | 5 ++ .../src/service/persistent_container.rs | 56 ++++++++----------- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/core/startos/src/lxc/mod.rs b/core/startos/src/lxc/mod.rs index 8b8cab9a0..b8ce9c703 100644 --- a/core/startos/src/lxc/mod.rs +++ b/core/startos/src/lxc/mod.rs @@ -288,6 +288,11 @@ impl LxcContainer { #[instrument(skip_all)] pub async fn exit(mut self) -> Result<(), Error> { + Command::new("lxc-stop") + .arg("--name") + .arg(&**self.guid) + .invoke(ErrorKind::Lxc) + .await?; self.rpc_bind.take().unmount().await?; if let Some(log_mount) = self.log_mount.take() { log_mount.unmount(true).await?; diff --git a/core/startos/src/service/persistent_container.rs b/core/startos/src/service/persistent_container.rs index e1324b307..23f01605e 100644 --- a/core/startos/src/service/persistent_container.rs +++ b/core/startos/src/service/persistent_container.rs @@ -391,39 +391,31 @@ impl PersistentContainer { let overlays = self.overlays.clone(); let lxc_container = self.lxc_container.take(); self.destroyed = true; - Some( - async move { - dbg!( - async move { - let mut errs = ErrorCollection::new(); - if let Some((hdl, shutdown)) = rpc_server { - errs.handle(rpc_client.request(rpc::Exit, Empty {}).await); - shutdown.shutdown(); - errs.handle(hdl.await.with_kind(ErrorKind::Cancelled)); - } - for (_, volume) in volumes { - errs.handle(volume.unmount(true).await); - } - for (_, assets) in assets { - errs.handle(assets.unmount(true).await); - } - for (_, overlay) in std::mem::take(&mut *overlays.lock().await) { - errs.handle(overlay.unmount(true).await); - } - for (_, images) in images { - errs.handle(images.unmount().await); - } - errs.handle(js_mount.unmount(true).await); - if let Some(lxc_container) = lxc_container { - errs.handle(lxc_container.exit().await); - } - dbg!(errs.into_result()) - } - .await - ) + Some(async move { + let mut errs = ErrorCollection::new(); + if let Some((hdl, shutdown)) = rpc_server { + errs.handle(rpc_client.request(rpc::Exit, Empty {}).await); + shutdown.shutdown(); + errs.handle(hdl.await.with_kind(ErrorKind::Cancelled)); } - .map(|a| dbg!(a)), - ) + for (_, volume) in volumes { + errs.handle(volume.unmount(true).await); + } + for (_, assets) in assets { + errs.handle(assets.unmount(true).await); + } + for (_, overlay) in std::mem::take(&mut *overlays.lock().await) { + errs.handle(overlay.unmount(true).await); + } + for (_, images) in images { + errs.handle(images.unmount().await); + } + errs.handle(js_mount.unmount(true).await); + if let Some(lxc_container) = lxc_container { + errs.handle(lxc_container.exit().await); + } + errs.into_result() + }) } #[instrument(skip_all)] From 5c153c9e2121b810d5da562d722f318edd192941 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Mon, 29 Jul 2024 18:44:56 -0600 Subject: [PATCH 110/125] improve install performance --- core/models/src/errors.rs | 24 ++++++++ core/startos/src/install/mod.rs | 2 +- core/startos/src/registry/asset.rs | 42 +++++++++++++- core/startos/src/upload.rs | 92 +++++++++++++++--------------- 4 files changed, 111 insertions(+), 49 deletions(-) diff --git a/core/models/src/errors.rs b/core/models/src/errors.rs index 8bbc705ee..ee6b0ae12 100644 --- a/core/models/src/errors.rs +++ b/core/models/src/errors.rs @@ -490,6 +490,7 @@ where { fn with_kind(self, kind: ErrorKind) -> Result; fn with_ctx (ErrorKind, D), D: Display>(self, f: F) -> Result; + fn log_err(self) -> Option; } impl ResultExt for Result where @@ -516,6 +517,18 @@ where } }) } + + fn log_err(self) -> Option { + match self { + Ok(a) => Some(a), + Err(e) => { + let e: color_eyre::eyre::Error = e.into(); + tracing::error!("{e}"); + tracing::debug!("{e:?}"); + None + } + } + } } impl ResultExt for Result { fn with_kind(self, kind: ErrorKind) -> Result { @@ -539,6 +552,17 @@ impl ResultExt for Result { } }) } + + fn log_err(self) -> Option { + match self { + Ok(a) => Some(a), + Err(e) => { + tracing::error!("{e}"); + tracing::debug!("{e:?}"); + None + } + } + } } pub trait OptionExt diff --git a/core/startos/src/install/mod.rs b/core/startos/src/install/mod.rs index 8d4f65312..7a545a3aa 100644 --- a/core/startos/src/install/mod.rs +++ b/core/startos/src/install/mod.rs @@ -157,7 +157,7 @@ pub async fn install( .services .install( ctx.clone(), - || asset.deserialize_s9pk(ctx.client.clone()), + || asset.deserialize_s9pk_buffered(ctx.client.clone()), None::, None, ) diff --git a/core/startos/src/registry/asset.rs b/core/startos/src/registry/asset.rs index 7697a0c99..fb6dd59fc 100644 --- a/core/startos/src/registry/asset.rs +++ b/core/startos/src/registry/asset.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::sync::Arc; use chrono::{DateTime, Utc}; +use helpers::NonDetachingJoinHandle; use reqwest::Client; use serde::{Deserialize, Serialize}; use tokio::io::AsyncWrite; @@ -14,8 +15,9 @@ use crate::registry::signer::commitment::{Commitment, Digestable}; use crate::registry::signer::sign::{AnySignature, AnyVerifyingKey}; use crate::registry::signer::AcceptSigners; use crate::s9pk::merkle_archive::source::http::HttpSource; -use crate::s9pk::merkle_archive::source::Section; +use crate::s9pk::merkle_archive::source::{ArchiveSource, Section}; use crate::s9pk::S9pk; +use crate::upload::UploadingFile; #[derive(Debug, Deserialize, Serialize, TS)] #[serde(rename_all = "camelCase")] @@ -70,4 +72,42 @@ impl RegistryAsset { ) .await } + pub async fn deserialize_s9pk_buffered( + &self, + client: Client, + ) -> Result>>, Error> { + S9pk::deserialize( + &Arc::new(BufferedHttpSource::new(client, self.url.clone()).await?), + Some(&self.commitment), + ) + .await + } +} + +pub struct BufferedHttpSource { + _download: NonDetachingJoinHandle<()>, + file: UploadingFile, +} +impl BufferedHttpSource { + pub async fn new(client: Client, url: Url) -> Result { + let (mut handle, file) = UploadingFile::new().await?; + let response = client.get(url).send().await?; + Ok(Self { + _download: tokio::spawn(async move { handle.download(response).await }).into(), + file, + }) + } +} +impl ArchiveSource for BufferedHttpSource { + type FetchReader = ::FetchReader; + type FetchAllReader = ::FetchAllReader; + async fn size(&self) -> Option { + self.file.size().await + } + async fn fetch_all(&self) -> Result { + self.file.fetch_all().await + } + async fn fetch(&self, position: u64, size: u64) -> Result { + self.file.fetch(position, size).await + } } diff --git a/core/startos/src/upload.rs b/core/startos/src/upload.rs index 4735a63fb..20f294400 100644 --- a/core/startos/src/upload.rs +++ b/core/startos/src/upload.rs @@ -5,10 +5,12 @@ use std::task::Poll; use std::time::Duration; use axum::body::Body; +use axum::extract::Request; use axum::response::Response; -use futures::{ready, FutureExt, StreamExt}; +use bytes::Bytes; +use futures::{ready, FutureExt, Stream, StreamExt}; use http::header::CONTENT_LENGTH; -use http::StatusCode; +use http::{HeaderMap, StatusCode}; use imbl_value::InternedString; use tokio::fs::File; use tokio::io::{AsyncRead, AsyncSeek, AsyncSeekExt, AsyncWrite, AsyncWriteExt}; @@ -34,51 +36,7 @@ pub async fn upload( ctx, session, |request| async move { - let headers = request.headers(); - let content_length = match headers.get(CONTENT_LENGTH).map(|a| a.to_str()) { - None => { - return Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Body::from("Content-Length is required")) - .with_kind(ErrorKind::Network) - } - Some(Err(_)) => { - return Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Body::from("Invalid Content-Length")) - .with_kind(ErrorKind::Network) - } - Some(Ok(a)) => match a.parse::() { - Err(_) => { - return Response::builder() - .status(StatusCode::BAD_REQUEST) - .body(Body::from("Invalid Content-Length")) - .with_kind(ErrorKind::Network) - } - Ok(a) => a, - }, - }; - - handle - .progress - .send_modify(|p| p.expected_size = Some(content_length)); - - let mut body = request.into_body().into_data_stream(); - while let Some(next) = body.next().await { - if let Err(e) = async { - handle - .write_all(&next.map_err(|e| { - std::io::Error::new(std::io::ErrorKind::Other, e) - })?) - .await?; - Ok(()) - } - .await - { - handle.progress.send_if_modified(|p| p.handle_error(&e)); - break; - } - } + handle.upload(request).await; Response::builder() .status(StatusCode::NO_CONTENT) @@ -364,6 +322,46 @@ pub struct UploadHandle { file: File, progress: watch::Sender, } +impl UploadHandle { + pub async fn upload(&mut self, request: Request) { + self.process_headers(request.headers()); + self.process_body(request.into_body().into_data_stream()) + .await; + } + pub async fn download(&mut self, response: reqwest::Response) { + self.process_headers(response.headers()); + self.process_body(response.bytes_stream()).await; + } + fn process_headers(&mut self, headers: &HeaderMap) { + if let Some(content_length) = headers + .get(CONTENT_LENGTH) + .and_then(|a| a.to_str().log_err()) + .and_then(|a| a.parse::().log_err()) + { + self.progress + .send_modify(|p| p.expected_size = Some(content_length)); + } + } + async fn process_body>>( + &mut self, + mut body: impl Stream> + Unpin, + ) { + while let Some(next) = body.next().await { + if let Err(e) = async { + self.write_all( + &next.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?, + ) + .await?; + Ok(()) + } + .await + { + self.progress.send_if_modified(|p| p.handle_error(&e)); + break; + } + } + } +} #[pin_project::pinned_drop] impl PinnedDrop for UploadHandle { fn drop(self: Pin<&mut Self>) { From 46b3f83ce2486d6dbdd2d85494ce93fa74426328 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Mon, 29 Jul 2024 18:45:18 -0600 Subject: [PATCH 111/125] don't trim logs --- core/startos/src/logs.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/core/startos/src/logs.rs b/core/startos/src/logs.rs index 751dd243a..9cf234f5f 100644 --- a/core/startos/src/logs.rs +++ b/core/startos/src/logs.rs @@ -184,7 +184,13 @@ fn deserialize_log_message<'de, D: serde::de::Deserializer<'de>>( where E: serde::de::Error, { - Ok(v.trim().to_owned()) + Ok(v.to_owned()) + } + fn visit_string(self, v: String) -> Result + where + E: de::Error, + { + Ok(v) } fn visit_unit(self) -> Result where @@ -202,7 +208,7 @@ fn deserialize_log_message<'de, D: serde::de::Deserializer<'de>>( .flatten() .collect::, _>>()?, ) - .map(|s| s.trim().to_owned()) + .map(|s| s.to_owned()) .map_err(serde::de::Error::custom) } } From 1dd21f1f764e41c12a38d6ff3228f3883b86ea54 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Mon, 29 Jul 2024 18:45:49 -0600 Subject: [PATCH 112/125] fix config pointers --- container-runtime/package-lock.json | 1156 ++++++++++++++--- container-runtime/package.json | 4 +- .../Systems/SystemForEmbassy/index.ts | 193 ++- 3 files changed, 1086 insertions(+), 267 deletions(-) diff --git a/container-runtime/package-lock.json b/container-runtime/package-lock.json index eb7778c83..244ade9f4 100644 --- a/container-runtime/package-lock.json +++ b/container-runtime/package-lock.json @@ -15,7 +15,7 @@ "esbuild-plugin-resolve": "^2.0.0", "filebrowser": "^1.0.0", "isomorphic-fetch": "^3.0.0", - "jest": "^29.7.0", + "jsonpath": "^1.1.1", "lodash.merge": "^4.6.2", "node-fetch": "^3.1.0", "ts-matches": "^5.5.1", @@ -27,7 +27,9 @@ "@swc/cli": "^0.1.62", "@swc/core": "^1.3.65", "@types/jest": "^29.5.12", + "@types/jsonpath": "^0.2.4", "@types/node": "^20.11.13", + "jest": "^29.7.0", "prettier": "^3.2.5", "ts-jest": "^29.2.3", "typescript": ">5.2" @@ -64,6 +66,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" @@ -76,6 +79,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, "dependencies": { "@babel/highlight": "^7.24.7", "picocolors": "^1.0.0" @@ -88,6 +92,7 @@ "version": "7.24.9", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.9.tgz", "integrity": "sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -96,6 +101,7 @@ "version": "7.24.9", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", + "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -125,6 +131,7 @@ "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -140,12 +147,14 @@ "node_modules/@babel/core/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "bin": { "semver": "bin/semver.js" } @@ -154,6 +163,7 @@ "version": "7.24.10", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.10.tgz", "integrity": "sha512-o9HBZL1G2129luEUlG1hB4N/nlYNWHnpwlND9eOMclRqqu1YDy2sSYVCFUZwl8I1Gxh+QSRrP2vD7EpUmFVXxg==", + "dev": true, "dependencies": { "@babel/types": "^7.24.9", "@jridgewell/gen-mapping": "^0.3.5", @@ -168,6 +178,7 @@ "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz", "integrity": "sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==", + "dev": true, "dependencies": { "@babel/compat-data": "^7.24.8", "@babel/helper-validator-option": "^7.24.8", @@ -183,6 +194,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, "dependencies": { "yallist": "^3.0.2" } @@ -191,6 +203,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "bin": { "semver": "bin/semver.js" } @@ -198,12 +211,14 @@ "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true }, "node_modules/@babel/helper-environment-visitor": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, "dependencies": { "@babel/types": "^7.24.7" }, @@ -215,6 +230,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "dev": true, "dependencies": { "@babel/template": "^7.24.7", "@babel/types": "^7.24.7" @@ -227,6 +243,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "dev": true, "dependencies": { "@babel/types": "^7.24.7" }, @@ -238,6 +255,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, "dependencies": { "@babel/traverse": "^7.24.7", "@babel/types": "^7.24.7" @@ -250,6 +268,7 @@ "version": "7.24.9", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.9.tgz", "integrity": "sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw==", + "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.24.7", "@babel/helper-module-imports": "^7.24.7", @@ -268,6 +287,7 @@ "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -276,6 +296,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, "dependencies": { "@babel/traverse": "^7.24.7", "@babel/types": "^7.24.7" @@ -288,6 +309,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, "dependencies": { "@babel/types": "^7.24.7" }, @@ -299,6 +321,7 @@ "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -307,6 +330,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -315,6 +339,7 @@ "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -323,6 +348,7 @@ "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.8.tgz", "integrity": "sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==", + "dev": true, "dependencies": { "@babel/template": "^7.24.7", "@babel/types": "^7.24.8" @@ -335,6 +361,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", @@ -349,6 +376,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -360,6 +388,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -373,6 +402,7 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -380,12 +410,14 @@ "node_modules/@babel/highlight/node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true }, "node_modules/@babel/highlight/node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, "engines": { "node": ">=0.8.0" } @@ -394,6 +426,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, "engines": { "node": ">=4" } @@ -402,6 +435,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -413,6 +447,7 @@ "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.8.tgz", "integrity": "sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==", + "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -424,6 +459,7 @@ "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -435,6 +471,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -446,6 +483,7 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, @@ -457,6 +495,7 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -468,6 +507,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -479,6 +519,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.24.7" }, @@ -493,6 +534,7 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -504,6 +546,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -515,6 +558,7 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -526,6 +570,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -537,6 +582,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -548,6 +594,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -559,6 +606,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -573,6 +621,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.24.7" }, @@ -587,6 +636,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dev": true, "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/parser": "^7.24.7", @@ -600,6 +650,7 @@ "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.8.tgz", "integrity": "sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==", + "dev": true, "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/generator": "^7.24.8", @@ -620,6 +671,7 @@ "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -635,12 +687,14 @@ "node_modules/@babel/traverse/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "node_modules/@babel/types": { "version": "7.24.9", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.9.tgz", "integrity": "sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==", + "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.24.8", "@babel/helper-validator-identifier": "^7.24.7", @@ -653,7 +707,8 @@ "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true }, "node_modules/@iarna/toml": { "version": "2.2.5", @@ -663,6 +718,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", @@ -678,6 +734,7 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, "engines": { "node": ">=8" } @@ -686,6 +743,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -702,6 +760,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, "dependencies": { "@jest/console": "^29.7.0", "@jest/reporters": "^29.7.0", @@ -748,6 +807,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", @@ -762,6 +822,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, "dependencies": { "expect": "^29.7.0", "jest-snapshot": "^29.7.0" @@ -774,6 +835,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, "dependencies": { "jest-get-type": "^29.6.3" }, @@ -785,6 +847,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", @@ -801,6 +864,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -815,6 +879,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@jest/console": "^29.7.0", @@ -857,6 +922,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, "dependencies": { "@sinclair/typebox": "^0.27.8" }, @@ -868,6 +934,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", "callsites": "^3.0.0", @@ -881,6 +948,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, "dependencies": { "@jest/console": "^29.7.0", "@jest/types": "^29.6.3", @@ -895,6 +963,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, "dependencies": { "@jest/test-result": "^29.7.0", "graceful-fs": "^4.2.9", @@ -909,6 +978,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", @@ -934,6 +1004,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", @@ -950,6 +1021,7 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -963,6 +1035,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "engines": { "node": ">=6.0.0" } @@ -971,6 +1044,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, "engines": { "node": ">=6.0.0" } @@ -978,12 +1052,14 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -1064,7 +1140,8 @@ "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true }, "node_modules/@sindresorhus/is": { "version": "4.6.0", @@ -1081,6 +1158,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, "dependencies": { "type-detect": "4.0.8" } @@ -1089,6 +1167,7 @@ "version": "10.3.0", "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, "dependencies": { "@sinonjs/commons": "^3.0.0" } @@ -1228,6 +1307,7 @@ "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -1240,6 +1320,7 @@ "version": "7.6.8", "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, "dependencies": { "@babel/types": "^7.0.0" } @@ -1248,6 +1329,7 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" @@ -1257,6 +1339,7 @@ "version": "7.20.6", "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, "dependencies": { "@babel/types": "^7.20.7" } @@ -1276,6 +1359,7 @@ "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, "dependencies": { "@types/node": "*" } @@ -1288,12 +1372,14 @@ "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==" + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, "dependencies": { "@types/istanbul-lib-coverage": "*" } @@ -1302,6 +1388,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, "dependencies": { "@types/istanbul-lib-report": "*" } @@ -1316,6 +1403,13 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/jsonpath": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@types/jsonpath/-/jsonpath-0.2.4.tgz", + "integrity": "sha512-K3hxB8Blw0qgW6ExKgMbXQv2UPZBoE2GqLpVY+yr7nMD2Pq86lsuIzyAaiQ7eMqFL5B6di6pxSkogLJEyEHoGA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/keyv": { "version": "3.1.4", "dev": true, @@ -1326,6 +1420,7 @@ }, "node_modules/@types/node": { "version": "20.14.2", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~5.26.4" @@ -1342,12 +1437,14 @@ "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, "dependencies": { "@types/yargs-parser": "*" } @@ -1355,7 +1452,8 @@ "node_modules/@types/yargs-parser": { "version": "21.0.3", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true }, "node_modules/accepts": { "version": "1.3.8", @@ -1372,6 +1470,7 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, "dependencies": { "type-fest": "^0.21.3" }, @@ -1386,6 +1485,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "engines": { "node": ">=8" } @@ -1394,6 +1494,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -1408,6 +1509,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -1439,6 +1541,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, "dependencies": { "sprintf-js": "~1.0.2" } @@ -1457,6 +1560,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", @@ -1477,6 +1581,7 @@ "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", @@ -1492,6 +1597,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", @@ -1507,6 +1613,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "bin": { "semver": "bin/semver.js" } @@ -1515,6 +1622,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", @@ -1529,6 +1637,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", @@ -1551,6 +1660,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" @@ -1564,6 +1674,7 @@ }, "node_modules/balanced-match": { "version": "1.0.2", + "dev": true, "license": "MIT" }, "node_modules/bin-check": { @@ -1750,6 +1861,7 @@ }, "node_modules/braces": { "version": "3.0.3", + "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -1762,6 +1874,7 @@ "version": "4.23.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", + "dev": true, "funding": [ { "type": "opencollective", @@ -1805,6 +1918,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, "dependencies": { "node-int64": "^0.4.0" } @@ -1812,7 +1926,8 @@ "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true }, "node_modules/bytes": { "version": "3.1.2", @@ -1881,6 +1996,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, "engines": { "node": ">=6" } @@ -1889,6 +2005,7 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, "engines": { "node": ">=6" } @@ -1897,6 +2014,7 @@ "version": "1.0.30001643", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz", "integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==", + "dev": true, "funding": [ { "type": "opencollective", @@ -1916,6 +2034,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1931,6 +2050,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, "engines": { "node": ">=10" } @@ -1939,6 +2059,7 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, "funding": [ { "type": "github", @@ -1952,12 +2073,14 @@ "node_modules/cjs-module-lexer": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", - "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==" + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", + "dev": true }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -1982,6 +2105,7 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, "engines": { "iojs": ">= 1.0.0", "node": ">= 0.12.0" @@ -1990,12 +2114,14 @@ "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==" + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2006,7 +2132,8 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/commander": { "version": "7.2.0", @@ -2019,7 +2146,8 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true }, "node_modules/content-disposition": { "version": "0.5.4", @@ -2041,7 +2169,8 @@ "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true }, "node_modules/cookie": { "version": "0.6.0", @@ -2058,6 +2187,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", @@ -2127,6 +2257,7 @@ "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, @@ -2136,10 +2267,17 @@ } } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "license": "MIT" + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -2186,6 +2324,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, "engines": { "node": ">=8" } @@ -2194,6 +2333,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -2220,12 +2360,14 @@ "node_modules/electron-to-chromium": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.2.tgz", - "integrity": "sha512-kc4r3U3V3WLaaZqThjYz/Y6z8tJe+7K0bbjUVo3i+LWIypVdMx5nXCkwRe6SWbY6ILqLdc1rKcKmr3HoH7wjSQ==" + "integrity": "sha512-kc4r3U3V3WLaaZqThjYz/Y6z8tJe+7K0bbjUVo3i+LWIypVdMx5nXCkwRe6SWbY6ILqLdc1rKcKmr3HoH7wjSQ==", + "dev": true }, "node_modules/emittery": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, "engines": { "node": ">=12" }, @@ -2236,7 +2378,8 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "node_modules/encodeurl": { "version": "1.0.2", @@ -2257,6 +2400,7 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, "dependencies": { "is-arrayish": "^0.2.1" } @@ -2286,6 +2430,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, "engines": { "node": ">=6" } @@ -2305,6 +2450,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -2317,6 +2494,24 @@ "node": ">=4" } }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/etag": { "version": "1.8.1", "license": "MIT", @@ -2356,6 +2551,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, "engines": { "node": ">= 0.8.0" } @@ -2364,6 +2560,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", @@ -2456,7 +2653,14 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "license": "MIT" }, "node_modules/fastq": { "version": "1.17.1", @@ -2470,6 +2674,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, "dependencies": { "bser": "2.1.1" } @@ -2574,6 +2779,7 @@ }, "node_modules/fill-range": { "version": "7.1.1", + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -2602,6 +2808,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -2651,12 +2858,14 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -2677,6 +2886,7 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -2685,6 +2895,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -2710,6 +2921,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, "engines": { "node": ">=8.0.0" } @@ -2727,6 +2939,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2757,6 +2970,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2766,6 +2980,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2777,6 +2992,7 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, "engines": { "node": ">=4" } @@ -2818,12 +3034,14 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -2871,7 +3089,8 @@ "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true }, "node_modules/http-cache-semantics": { "version": "4.1.1", @@ -2906,6 +3125,7 @@ }, "node_modules/human-signals": { "version": "2.1.0", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.17.0" @@ -2944,6 +3164,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" @@ -2962,6 +3183,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, "engines": { "node": ">=0.8.19" } @@ -2971,6 +3193,7 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -2990,12 +3213,14 @@ "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true }, "node_modules/is-core-module": { "version": "2.15.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "dev": true, "dependencies": { "hasown": "^2.0.2" }, @@ -3018,6 +3243,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "engines": { "node": ">=8" } @@ -3026,6 +3252,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, "engines": { "node": ">=6" } @@ -3043,6 +3270,7 @@ }, "node_modules/is-number": { "version": "7.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -3066,6 +3294,7 @@ }, "node_modules/isexe": { "version": "2.0.0", + "dev": true, "license": "ISC" }, "node_modules/isomorphic-fetch": { @@ -3098,6 +3327,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, "engines": { "node": ">=8" } @@ -3106,6 +3336,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, "dependencies": { "@babel/core": "^7.23.9", "@babel/parser": "^7.23.9", @@ -3121,6 +3352,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", @@ -3134,6 +3366,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, "dependencies": { "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", @@ -3147,6 +3380,7 @@ "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -3162,12 +3396,14 @@ "node_modules/istanbul-lib-source-maps/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "node_modules/istanbul-lib-source-maps/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -3176,6 +3412,7 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" @@ -3228,6 +3465,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -3253,6 +3491,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, "dependencies": { "execa": "^5.0.0", "jest-util": "^29.7.0", @@ -3266,6 +3505,7 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -3279,6 +3519,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -3301,6 +3542,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, "engines": { "node": ">=10" }, @@ -3312,6 +3554,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, "engines": { "node": ">=8" }, @@ -3323,6 +3566,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, "dependencies": { "path-key": "^3.0.0" }, @@ -3334,6 +3578,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "engines": { "node": ">=8" } @@ -3342,6 +3587,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -3353,6 +3599,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "engines": { "node": ">=8" } @@ -3361,6 +3608,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -3375,6 +3623,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -3405,6 +3654,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/test-result": "^29.7.0", @@ -3437,6 +3687,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, "dependencies": { "@babel/core": "^7.11.6", "@jest/test-sequencer": "^29.7.0", @@ -3481,6 +3732,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", @@ -3495,6 +3747,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, "dependencies": { "detect-newline": "^3.0.0" }, @@ -3506,6 +3759,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", @@ -3521,6 +3775,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -3537,6 +3792,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -3545,6 +3801,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", @@ -3569,6 +3826,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, "dependencies": { "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" @@ -3581,6 +3839,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", @@ -3595,6 +3854,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", @@ -3614,6 +3874,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -3627,6 +3888,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, "engines": { "node": ">=6" }, @@ -3643,6 +3905,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -3651,6 +3914,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", @@ -3670,6 +3934,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, "dependencies": { "jest-regex-util": "^29.6.3", "jest-snapshot": "^29.7.0" @@ -3682,6 +3947,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, "dependencies": { "@jest/console": "^29.7.0", "@jest/environment": "^29.7.0", @@ -3713,6 +3979,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -3745,6 +4012,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", @@ -3775,6 +4043,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -3791,6 +4060,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", @@ -3807,6 +4077,7 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, "engines": { "node": ">=10" }, @@ -3818,6 +4089,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, "dependencies": { "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", @@ -3836,6 +4108,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", @@ -3850,6 +4123,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -3863,12 +4137,14 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true }, "node_modules/js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -3881,6 +4157,7 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, "bin": { "jsesc": "bin/jsesc" }, @@ -3896,12 +4173,14 @@ "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, "bin": { "json5": "lib/cli.js" }, @@ -3909,6 +4188,29 @@ "node": ">=6" } }, + "node_modules/jsonpath": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", + "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", + "license": "MIT", + "dependencies": { + "esprima": "1.2.2", + "static-eval": "2.0.2", + "underscore": "1.12.1" + } + }, + "node_modules/jsonpath/node_modules/esprima": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", + "integrity": "sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "dev": true, @@ -3921,6 +4223,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, "engines": { "node": ">=6" } @@ -3929,19 +4232,35 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, "engines": { "node": ">=6" } }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -3981,6 +4300,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, "dependencies": { "semver": "^7.5.3" }, @@ -4001,6 +4321,7 @@ "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, "dependencies": { "tmpl": "1.0.5" } @@ -4018,6 +4339,7 @@ }, "node_modules/merge-stream": { "version": "2.0.0", + "dev": true, "license": "MIT" }, "node_modules/merge2": { @@ -4037,6 +4359,7 @@ }, "node_modules/micromatch": { "version": "4.0.7", + "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -4075,6 +4398,7 @@ }, "node_modules/mimic-fn": { "version": "2.1.0", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4109,7 +4433,8 @@ "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true }, "node_modules/negotiator": { "version": "0.6.3", @@ -4154,17 +4479,20 @@ "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -4210,6 +4538,7 @@ }, "node_modules/once": { "version": "1.4.0", + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -4217,6 +4546,7 @@ }, "node_modules/onetime": { "version": "5.1.2", + "dev": true, "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" @@ -4228,6 +4558,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "license": "MIT", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/os-filter-obj": { "version": "2.0.0", "dev": true, @@ -4259,6 +4606,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -4273,6 +4621,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -4284,6 +4633,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "dependencies": { "p-try": "^2.0.0" }, @@ -4298,6 +4648,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, "engines": { "node": ">=6" } @@ -4306,6 +4657,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -4330,6 +4682,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "engines": { "node": ">=8" } @@ -4338,6 +4691,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -4353,7 +4707,8 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "node_modules/path-to-regexp": { "version": "0.1.7", @@ -4374,10 +4729,12 @@ "node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true }, "node_modules/picomatch": { "version": "2.3.1", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -4398,6 +4755,7 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, "engines": { "node": ">= 6" } @@ -4406,6 +4764,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, "dependencies": { "find-up": "^4.0.0" }, @@ -4413,6 +4772,14 @@ "node": ">=8" } }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prettier": { "version": "3.3.2", "dev": true, @@ -4431,6 +4798,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -4444,6 +4812,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, "engines": { "node": ">=10" }, @@ -4455,6 +4824,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" @@ -4492,6 +4862,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, "funding": [ { "type": "individual", @@ -4569,7 +4940,8 @@ "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true }, "node_modules/readable-stream": { "version": "3.6.2", @@ -4603,6 +4975,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -4611,6 +4984,7 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -4632,6 +5006,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, "dependencies": { "resolve-from": "^5.0.0" }, @@ -4643,6 +5018,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, "engines": { "node": ">=8" } @@ -4651,6 +5027,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, "engines": { "node": ">=10" } @@ -4721,6 +5098,7 @@ }, "node_modules/semver": { "version": "7.6.2", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -4849,15 +5227,18 @@ }, "node_modules/signal-exit": { "version": "3.0.7", + "dev": true, "license": "ISC" }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true }, "node_modules/slash": { "version": "3.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4897,6 +5278,7 @@ "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -4906,6 +5288,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -4913,12 +5296,14 @@ "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, "dependencies": { "escape-string-regexp": "^2.0.0" }, @@ -4930,10 +5315,20 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, "engines": { "node": ">=8" } }, + "node_modules/static-eval": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", + "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", + "license": "MIT", + "dependencies": { + "escodegen": "^1.8.1" + } + }, "node_modules/statuses": { "version": "2.0.1", "license": "MIT", @@ -4953,6 +5348,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" @@ -4965,6 +5361,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -4978,6 +5375,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -4989,6 +5387,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, "engines": { "node": ">=8" } @@ -5003,6 +5402,7 @@ }, "node_modules/strip-final-newline": { "version": "2.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5012,6 +5412,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, "engines": { "node": ">=8" }, @@ -5050,6 +5451,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -5061,6 +5463,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -5072,6 +5475,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", @@ -5085,6 +5489,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5094,6 +5499,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5104,18 +5510,21 @@ "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==" + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, "engines": { "node": ">=4" } }, "node_modules/to-regex-range": { "version": "5.0.1", + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -5218,10 +5627,23 @@ "version": "2.6.3", "license": "0BSD" }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, "engines": { "node": ">=4" } @@ -5230,6 +5652,7 @@ "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, "engines": { "node": ">=10" }, @@ -5260,8 +5683,15 @@ "node": ">=14.17" } }, + "node_modules/underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "5.26.5", + "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -5275,6 +5705,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -5316,6 +5747,7 @@ "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", @@ -5336,6 +5768,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, "dependencies": { "makeerror": "1.0.12" } @@ -5374,10 +5807,20 @@ "which": "bin/which" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -5392,12 +5835,14 @@ }, "node_modules/wrappy": { "version": "1.0.2", + "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" @@ -5410,6 +5855,7 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, "engines": { "node": ">=10" } @@ -5433,6 +5879,7 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -5450,6 +5897,7 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, "engines": { "node": ">=12" } @@ -5458,6 +5906,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, "engines": { "node": ">=10" }, @@ -5471,6 +5920,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, "requires": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" @@ -5480,6 +5930,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, "requires": { "@babel/highlight": "^7.24.7", "picocolors": "^1.0.0" @@ -5488,12 +5939,14 @@ "@babel/compat-data": { "version": "7.24.9", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.9.tgz", - "integrity": "sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng==" + "integrity": "sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng==", + "dev": true }, "@babel/core": { "version": "7.24.9", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", + "dev": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -5516,6 +5969,7 @@ "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, "requires": { "ms": "2.1.2" } @@ -5523,12 +5977,14 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true } } }, @@ -5536,6 +5992,7 @@ "version": "7.24.10", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.10.tgz", "integrity": "sha512-o9HBZL1G2129luEUlG1hB4N/nlYNWHnpwlND9eOMclRqqu1YDy2sSYVCFUZwl8I1Gxh+QSRrP2vD7EpUmFVXxg==", + "dev": true, "requires": { "@babel/types": "^7.24.9", "@jridgewell/gen-mapping": "^0.3.5", @@ -5547,6 +6004,7 @@ "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz", "integrity": "sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==", + "dev": true, "requires": { "@babel/compat-data": "^7.24.8", "@babel/helper-validator-option": "^7.24.8", @@ -5559,6 +6017,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, "requires": { "yallist": "^3.0.2" } @@ -5566,12 +6025,14 @@ "semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true }, "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true } } }, @@ -5579,6 +6040,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, "requires": { "@babel/types": "^7.24.7" } @@ -5587,6 +6049,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "dev": true, "requires": { "@babel/template": "^7.24.7", "@babel/types": "^7.24.7" @@ -5596,6 +6059,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "dev": true, "requires": { "@babel/types": "^7.24.7" } @@ -5604,6 +6068,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, "requires": { "@babel/traverse": "^7.24.7", "@babel/types": "^7.24.7" @@ -5613,6 +6078,7 @@ "version": "7.24.9", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.9.tgz", "integrity": "sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw==", + "dev": true, "requires": { "@babel/helper-environment-visitor": "^7.24.7", "@babel/helper-module-imports": "^7.24.7", @@ -5624,12 +6090,14 @@ "@babel/helper-plugin-utils": { "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", - "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==" + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "dev": true }, "@babel/helper-simple-access": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, "requires": { "@babel/traverse": "^7.24.7", "@babel/types": "^7.24.7" @@ -5639,6 +6107,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, "requires": { "@babel/types": "^7.24.7" } @@ -5646,22 +6115,26 @@ "@babel/helper-string-parser": { "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==" + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "dev": true }, "@babel/helper-validator-identifier": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==" + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true }, "@babel/helper-validator-option": { "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", - "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==" + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "dev": true }, "@babel/helpers": { "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.8.tgz", "integrity": "sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==", + "dev": true, "requires": { "@babel/template": "^7.24.7", "@babel/types": "^7.24.8" @@ -5671,6 +6144,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", @@ -5682,6 +6156,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -5690,6 +6165,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -5700,6 +6176,7 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "requires": { "color-name": "1.1.3" } @@ -5707,22 +6184,26 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -5732,12 +6213,14 @@ "@babel/parser": { "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.8.tgz", - "integrity": "sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==" + "integrity": "sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==", + "dev": true }, "@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -5746,6 +6229,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -5754,6 +6238,7 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.12.13" } @@ -5762,6 +6247,7 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -5770,6 +6256,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -5778,6 +6265,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.24.7" } @@ -5786,6 +6274,7 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -5794,6 +6283,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -5802,6 +6292,7 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.10.4" } @@ -5810,6 +6301,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -5818,6 +6310,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -5826,6 +6319,7 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.8.0" } @@ -5834,6 +6328,7 @@ "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.14.5" } @@ -5842,6 +6337,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.24.7" } @@ -5850,6 +6346,7 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dev": true, "requires": { "@babel/code-frame": "^7.24.7", "@babel/parser": "^7.24.7", @@ -5860,6 +6357,7 @@ "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.8.tgz", "integrity": "sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==", + "dev": true, "requires": { "@babel/code-frame": "^7.24.7", "@babel/generator": "^7.24.8", @@ -5877,6 +6375,7 @@ "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, "requires": { "ms": "2.1.2" } @@ -5884,7 +6383,8 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true } } }, @@ -5892,6 +6392,7 @@ "version": "7.24.9", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.9.tgz", "integrity": "sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==", + "dev": true, "requires": { "@babel/helper-string-parser": "^7.24.8", "@babel/helper-validator-identifier": "^7.24.7", @@ -5901,7 +6402,8 @@ "@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true }, "@iarna/toml": { "version": "2.2.5" @@ -5910,6 +6412,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, "requires": { "camelcase": "^5.3.1", "find-up": "^4.1.0", @@ -5921,12 +6424,14 @@ "@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==" + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true }, "@jest/console": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, "requires": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -5940,6 +6445,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, "requires": { "@jest/console": "^29.7.0", "@jest/reporters": "^29.7.0", @@ -5975,6 +6481,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, "requires": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", @@ -5986,6 +6493,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, "requires": { "expect": "^29.7.0", "jest-snapshot": "^29.7.0" @@ -5995,6 +6503,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, "requires": { "jest-get-type": "^29.6.3" } @@ -6003,6 +6512,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, "requires": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", @@ -6016,6 +6526,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, "requires": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -6027,6 +6538,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, "requires": { "@bcoe/v8-coverage": "^0.2.3", "@jest/console": "^29.7.0", @@ -6058,6 +6570,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, "requires": { "@sinclair/typebox": "^0.27.8" } @@ -6066,6 +6579,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, "requires": { "@jridgewell/trace-mapping": "^0.3.18", "callsites": "^3.0.0", @@ -6076,6 +6590,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, "requires": { "@jest/console": "^29.7.0", "@jest/types": "^29.6.3", @@ -6087,6 +6602,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, "requires": { "@jest/test-result": "^29.7.0", "graceful-fs": "^4.2.9", @@ -6098,6 +6614,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, "requires": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", @@ -6120,6 +6637,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, "requires": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", @@ -6133,6 +6651,7 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, "requires": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -6142,22 +6661,26 @@ "@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true }, "@jridgewell/set-array": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==" + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true }, "@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true }, "@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, "requires": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -6213,7 +6736,8 @@ "@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true }, "@sindresorhus/is": { "version": "4.6.0", @@ -6223,6 +6747,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, "requires": { "type-detect": "4.0.8" } @@ -6231,6 +6756,7 @@ "version": "10.3.0", "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, "requires": { "@sinonjs/commons": "^3.0.0" } @@ -6325,6 +6851,7 @@ "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, "requires": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -6337,6 +6864,7 @@ "version": "7.6.8", "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, "requires": { "@babel/types": "^7.0.0" } @@ -6345,6 +6873,7 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, "requires": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" @@ -6354,6 +6883,7 @@ "version": "7.20.6", "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, "requires": { "@babel/types": "^7.20.7" } @@ -6372,6 +6902,7 @@ "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, "requires": { "@types/node": "*" } @@ -6383,12 +6914,14 @@ "@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==" + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true }, "@types/istanbul-lib-report": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, "requires": { "@types/istanbul-lib-coverage": "*" } @@ -6397,6 +6930,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, "requires": { "@types/istanbul-lib-report": "*" } @@ -6411,6 +6945,12 @@ "pretty-format": "^29.0.0" } }, + "@types/jsonpath": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@types/jsonpath/-/jsonpath-0.2.4.tgz", + "integrity": "sha512-K3hxB8Blw0qgW6ExKgMbXQv2UPZBoE2GqLpVY+yr7nMD2Pq86lsuIzyAaiQ7eMqFL5B6di6pxSkogLJEyEHoGA==", + "dev": true + }, "@types/keyv": { "version": "3.1.4", "dev": true, @@ -6420,6 +6960,7 @@ }, "@types/node": { "version": "20.14.2", + "dev": true, "requires": { "undici-types": "~5.26.4" } @@ -6434,12 +6975,14 @@ "@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true }, "@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, "requires": { "@types/yargs-parser": "*" } @@ -6447,7 +6990,8 @@ "@types/yargs-parser": { "version": "21.0.3", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true }, "accepts": { "version": "1.3.8", @@ -6460,6 +7004,7 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, "requires": { "type-fest": "^0.21.3" } @@ -6467,12 +7012,14 @@ "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -6481,6 +7028,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "requires": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -6494,6 +7042,7 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, "requires": { "sprintf-js": "~1.0.2" } @@ -6511,6 +7060,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, "requires": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", @@ -6525,6 +7075,7 @@ "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", @@ -6537,6 +7088,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, "requires": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", @@ -6548,7 +7100,8 @@ "semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true } } }, @@ -6556,6 +7109,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, "requires": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", @@ -6567,6 +7121,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, "requires": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", @@ -6586,13 +7141,15 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, "requires": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" } }, "balanced-match": { - "version": "1.0.2" + "version": "1.0.2", + "dev": true }, "bin-check": { "version": "4.1.0", @@ -6708,6 +7265,7 @@ }, "braces": { "version": "3.0.3", + "dev": true, "requires": { "fill-range": "^7.1.1" } @@ -6716,6 +7274,7 @@ "version": "4.23.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", + "dev": true, "requires": { "caniuse-lite": "^1.0.30001640", "electron-to-chromium": "^1.4.820", @@ -6736,6 +7295,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, "requires": { "node-int64": "^0.4.0" } @@ -6743,7 +7303,8 @@ "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true }, "bytes": { "version": "3.1.2" @@ -6787,22 +7348,26 @@ "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true }, "caniuse-lite": { "version": "1.0.30001643", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz", - "integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==" + "integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==", + "dev": true }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -6811,22 +7376,26 @@ "char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==" + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true }, "ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==" + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true }, "cjs-module-lexer": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", - "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==" + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", + "dev": true }, "cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, "requires": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -6843,17 +7412,20 @@ "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==" + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true }, "collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==" + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "requires": { "color-name": "~1.1.4" } @@ -6861,7 +7433,8 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "commander": { "version": "7.2.0", @@ -6870,7 +7443,8 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true }, "content-disposition": { "version": "0.5.4", @@ -6884,7 +7458,8 @@ "convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true }, "cookie": { "version": "0.6.0" @@ -6896,6 +7471,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, "requires": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", @@ -6941,12 +7517,19 @@ "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, "requires": {} }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, "deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true }, "defer-to-connect": { "version": "2.0.1", @@ -6969,12 +7552,14 @@ "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==" + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true }, "diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==" + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true }, "ee-first": { "version": "1.1.1" @@ -6991,17 +7576,20 @@ "electron-to-chromium": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.2.tgz", - "integrity": "sha512-kc4r3U3V3WLaaZqThjYz/Y6z8tJe+7K0bbjUVo3i+LWIypVdMx5nXCkwRe6SWbY6ILqLdc1rKcKmr3HoH7wjSQ==" + "integrity": "sha512-kc4r3U3V3WLaaZqThjYz/Y6z8tJe+7K0bbjUVo3i+LWIypVdMx5nXCkwRe6SWbY6ILqLdc1rKcKmr3HoH7wjSQ==", + "dev": true }, "emittery": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==" + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "encodeurl": { "version": "1.0.2" @@ -7017,6 +7605,7 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, "requires": { "is-arrayish": "^0.2.1" } @@ -7036,7 +7625,8 @@ "escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==" + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true }, "escape-html": { "version": "1.0.3" @@ -7045,11 +7635,41 @@ "version": "5.0.0", "dev": true }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + } + } + }, "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, "etag": { "version": "1.8.1" }, @@ -7076,12 +7696,14 @@ "exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==" + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true }, "expect": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, "requires": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", @@ -7155,7 +7777,13 @@ "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, "fastq": { "version": "1.17.1", @@ -7168,6 +7796,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, "requires": { "bser": "2.1.1" } @@ -7236,6 +7865,7 @@ }, "fill-range": { "version": "7.1.1", + "dev": true, "requires": { "to-regex-range": "^5.0.1" } @@ -7256,6 +7886,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, "requires": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -7283,12 +7914,14 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "optional": true }, "function-bind": { @@ -7297,12 +7930,14 @@ "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true }, "get-intrinsic": { "version": "1.2.4", @@ -7317,7 +7952,8 @@ "get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==" + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true }, "get-stream": { "version": "3.0.0", @@ -7327,6 +7963,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -7340,6 +7977,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7349,6 +7987,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -7365,7 +8004,8 @@ "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true }, "gopd": { "version": "1.0.1", @@ -7393,12 +8033,14 @@ "graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true }, "has-property-descriptors": { "version": "1.0.2", @@ -7421,7 +8063,8 @@ "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true }, "http-cache-semantics": { "version": "4.1.1", @@ -7446,7 +8089,8 @@ } }, "human-signals": { - "version": "2.1.0" + "version": "2.1.0", + "dev": true }, "iconv-lite": { "version": "0.4.24", @@ -7462,6 +8106,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, "requires": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" @@ -7470,12 +8115,14 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -7490,12 +8137,14 @@ "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true }, "is-core-module": { "version": "2.15.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "dev": true, "requires": { "hasown": "^2.0.2" } @@ -7507,12 +8156,14 @@ "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true }, "is-generator-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==" + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true }, "is-glob": { "version": "4.0.3", @@ -7522,7 +8173,8 @@ } }, "is-number": { - "version": "7.0.0" + "version": "7.0.0", + "dev": true }, "is-plain-obj": { "version": "1.1.0", @@ -7533,7 +8185,8 @@ "dev": true }, "isexe": { - "version": "2.0.0" + "version": "2.0.0", + "dev": true }, "isomorphic-fetch": { "version": "3.0.0", @@ -7553,12 +8206,14 @@ "istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==" + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true }, "istanbul-lib-instrument": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, "requires": { "@babel/core": "^7.23.9", "@babel/parser": "^7.23.9", @@ -7571,6 +8226,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, "requires": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", @@ -7581,6 +8237,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, "requires": { "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", @@ -7591,6 +8248,7 @@ "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, "requires": { "ms": "2.1.2" } @@ -7598,12 +8256,14 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true } } }, @@ -7611,6 +8271,7 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, "requires": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" @@ -7653,6 +8314,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, "requires": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -7664,6 +8326,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, "requires": { "execa": "^5.0.0", "jest-util": "^29.7.0", @@ -7674,6 +8337,7 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, "requires": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -7684,6 +8348,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, "requires": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -7699,17 +8364,20 @@ "get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true }, "is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true }, "npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, "requires": { "path-key": "^3.0.0" } @@ -7717,12 +8385,14 @@ "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "requires": { "shebang-regex": "^3.0.0" } @@ -7730,12 +8400,14 @@ "shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "requires": { "isexe": "^2.0.0" } @@ -7746,6 +8418,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, "requires": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -7773,6 +8446,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, "requires": { "@jest/core": "^29.7.0", "@jest/test-result": "^29.7.0", @@ -7791,6 +8465,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, "requires": { "@babel/core": "^7.11.6", "@jest/test-sequencer": "^29.7.0", @@ -7820,6 +8495,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, "requires": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", @@ -7831,6 +8507,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, "requires": { "detect-newline": "^3.0.0" } @@ -7839,6 +8516,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, "requires": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", @@ -7851,6 +8529,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, "requires": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -7863,12 +8542,14 @@ "jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==" + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true }, "jest-haste-map": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, "requires": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", @@ -7888,6 +8569,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, "requires": { "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" @@ -7897,6 +8579,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, "requires": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", @@ -7908,6 +8591,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, "requires": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", @@ -7924,6 +8608,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, "requires": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -7934,17 +8619,20 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, "requires": {} }, "jest-regex-util": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==" + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true }, "jest-resolve": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, "requires": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", @@ -7961,6 +8649,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, "requires": { "jest-regex-util": "^29.6.3", "jest-snapshot": "^29.7.0" @@ -7970,6 +8659,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, "requires": { "@jest/console": "^29.7.0", "@jest/environment": "^29.7.0", @@ -7998,6 +8688,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, "requires": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -8027,6 +8718,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, "requires": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", @@ -8054,6 +8746,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, "requires": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -8067,6 +8760,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, "requires": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", @@ -8079,7 +8773,8 @@ "camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==" + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true } } }, @@ -8087,6 +8782,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, "requires": { "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", @@ -8102,6 +8798,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, "requires": { "@types/node": "*", "jest-util": "^29.7.0", @@ -8113,6 +8810,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -8122,12 +8820,14 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true }, "js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -8136,7 +8836,8 @@ "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true }, "json-buffer": { "version": "3.0.1", @@ -8145,12 +8846,31 @@ "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true }, "json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "jsonpath": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", + "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", + "requires": { + "esprima": "1.2.2", + "static-eval": "2.0.2", + "underscore": "1.12.1" + }, + "dependencies": { + "esprima": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", + "integrity": "sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==" + } + } }, "keyv": { "version": "4.5.4", @@ -8162,22 +8882,35 @@ "kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==" + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } }, "lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, "requires": { "p-locate": "^4.1.0" } @@ -8209,6 +8942,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, "requires": { "semver": "^7.5.3" } @@ -8223,6 +8957,7 @@ "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, "requires": { "tmpl": "1.0.5" } @@ -8234,7 +8969,8 @@ "version": "1.0.1" }, "merge-stream": { - "version": "2.0.0" + "version": "2.0.0", + "dev": true }, "merge2": { "version": "1.4.1", @@ -8245,6 +8981,7 @@ }, "micromatch": { "version": "4.0.7", + "dev": true, "requires": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -8263,7 +9000,8 @@ } }, "mimic-fn": { - "version": "2.1.0" + "version": "2.1.0", + "dev": true }, "mimic-response": { "version": "1.0.1", @@ -8282,7 +9020,8 @@ "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true }, "negotiator": { "version": "0.6.3" @@ -8301,17 +9040,20 @@ "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true }, "node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true }, "normalize-url": { "version": "6.1.0", @@ -8335,16 +9077,31 @@ }, "once": { "version": "1.4.0", + "dev": true, "requires": { "wrappy": "1" } }, "onetime": { "version": "5.1.2", + "dev": true, "requires": { "mimic-fn": "^2.1.0" } }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, "os-filter-obj": { "version": "2.0.0", "dev": true, @@ -8364,6 +9121,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "requires": { "yocto-queue": "^0.1.0" } @@ -8372,6 +9130,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, "requires": { "p-limit": "^2.2.0" }, @@ -8380,6 +9139,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "requires": { "p-try": "^2.0.0" } @@ -8389,12 +9149,14 @@ "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true }, "parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, "requires": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -8408,12 +9170,14 @@ "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true }, "path-key": { "version": "2.0.1", @@ -8422,7 +9186,8 @@ "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "path-to-regexp": { "version": "0.1.7" @@ -8434,10 +9199,12 @@ "picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true }, "picomatch": { - "version": "2.3.1" + "version": "2.3.1", + "dev": true }, "pify": { "version": "2.3.0", @@ -8446,16 +9213,23 @@ "pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==" + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true }, "pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, "requires": { "find-up": "^4.0.0" } }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==" + }, "prettier": { "version": "3.3.2", "dev": true @@ -8464,6 +9238,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, "requires": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -8473,7 +9248,8 @@ "ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true } } }, @@ -8481,6 +9257,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, "requires": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" @@ -8508,7 +9285,8 @@ "pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==" + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true }, "qs": { "version": "6.11.0", @@ -8539,7 +9317,8 @@ "react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true }, "readable-stream": { "version": "3.6.2", @@ -8560,12 +9339,14 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true }, "resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, "requires": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -8580,6 +9361,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, "requires": { "resolve-from": "^5.0.0" } @@ -8587,12 +9369,14 @@ "resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true }, "resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", - "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==" + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true }, "responselike": { "version": "2.0.1", @@ -8619,7 +9403,8 @@ "version": "2.1.2" }, "semver": { - "version": "7.6.2" + "version": "7.6.2", + "dev": true }, "semver-regex": { "version": "4.0.5", @@ -8699,15 +9484,18 @@ } }, "signal-exit": { - "version": "3.0.7" + "version": "3.0.7", + "dev": true }, "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true }, "slash": { - "version": "3.0.0" + "version": "3.0.0", + "dev": true }, "sort-keys": { "version": "1.1.2", @@ -8731,6 +9519,7 @@ "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -8739,19 +9528,22 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true } } }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true }, "stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, "requires": { "escape-string-regexp": "^2.0.0" }, @@ -8759,10 +9551,19 @@ "escape-string-regexp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true } } }, + "static-eval": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", + "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", + "requires": { + "escodegen": "^1.8.1" + } + }, "statuses": { "version": "2.0.1" }, @@ -8777,6 +9578,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, "requires": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" @@ -8786,6 +9588,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -8796,6 +9599,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "requires": { "ansi-regex": "^5.0.1" } @@ -8803,19 +9607,22 @@ "strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==" + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true }, "strip-eof": { "version": "1.0.0", "dev": true }, "strip-final-newline": { - "version": "2.0.0" + "version": "2.0.0", + "dev": true }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true }, "strip-outer": { "version": "2.0.0", @@ -8833,6 +9640,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -8840,12 +9648,14 @@ "supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, "requires": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", @@ -8856,6 +9666,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -8865,6 +9676,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -8874,15 +9686,18 @@ "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==" + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true }, "to-regex-range": { "version": "5.0.1", + "dev": true, "requires": { "is-number": "^7.0.0" } @@ -8931,15 +9746,25 @@ "tslib": { "version": "2.6.3" }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "requires": { + "prelude-ls": "~1.1.2" + } + }, "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true }, "type-fest": { "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==" + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true }, "type-is": { "version": "1.6.18", @@ -8952,8 +9777,14 @@ "version": "5.4.5", "dev": true }, + "underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" + }, "undici-types": { - "version": "5.26.5" + "version": "5.26.5", + "dev": true }, "unpipe": { "version": "1.0.0" @@ -8962,6 +9793,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "dev": true, "requires": { "escalade": "^3.1.2", "picocolors": "^1.0.1" @@ -8978,6 +9810,7 @@ "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, "requires": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", @@ -8991,6 +9824,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, "requires": { "makeerror": "1.0.12" } @@ -9018,10 +9852,16 @@ "isexe": "^2.0.0" } }, + "word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==" + }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "requires": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -9029,12 +9869,14 @@ } }, "wrappy": { - "version": "1.0.2" + "version": "1.0.2", + "dev": true }, "write-file-atomic": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, "requires": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" @@ -9043,7 +9885,8 @@ "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true }, "yallist": { "version": "2.1.2", @@ -9056,6 +9899,7 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, "requires": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -9069,12 +9913,14 @@ "yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true } } } diff --git a/container-runtime/package.json b/container-runtime/package.json index 24e6a6fe6..0a8e4afa8 100644 --- a/container-runtime/package.json +++ b/container-runtime/package.json @@ -24,7 +24,7 @@ "esbuild-plugin-resolve": "^2.0.0", "filebrowser": "^1.0.0", "isomorphic-fetch": "^3.0.0", - "jest": "^29.7.0", + "jsonpath": "^1.1.1", "lodash.merge": "^4.6.2", "node-fetch": "^3.1.0", "ts-matches": "^5.5.1", @@ -36,7 +36,9 @@ "@swc/cli": "^0.1.62", "@swc/core": "^1.3.65", "@types/jest": "^29.5.12", + "@types/jsonpath": "^0.2.4", "@types/node": "^20.11.13", + "jest": "^29.7.0", "prettier": "^3.2.5", "ts-jest": "^29.2.3", "typescript": ">5.2" diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts index a7a30c34a..ffdb02988 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts @@ -49,6 +49,7 @@ import { transformOldConfigToNew, } from "./transformConfigSpec" import { MainEffects } from "@start9labs/start-sdk/cjs/lib/StartSdk" +import { StorePath } from "@start9labs/start-sdk/cjs/lib/store/PathBuilder" type Optional = A | undefined | null function todo(): never { @@ -58,7 +59,7 @@ const execFile = promisify(childProcess.execFile) const MANIFEST_LOCATION = "/usr/lib/startos/package/embassyManifest.json" export const EMBASSY_JS_LOCATION = "/usr/lib/startos/package/embassy.js" -const EMBASSY_POINTER_PATH_PREFIX = "/embassyConfig" +const EMBASSY_POINTER_PATH_PREFIX = "/embassyConfig" as StorePath const matchSetResult = object( { @@ -597,6 +598,10 @@ export class SystemForEmbassy implements System { structuredClone(newConfigWithoutPointers as Record), ) await updateConfig(effects, this.manifest, spec, newConfig) + await effects.store.set({ + path: EMBASSY_POINTER_PATH_PREFIX, + value: newConfig, + }) const setConfigValue = this.manifest.config?.set if (!setConfigValue) return if (setConfigValue.type === "docker") { @@ -826,52 +831,6 @@ export class SystemForEmbassy implements System { })) as any } } - private async dependenciesCheck( - effects: Effects, - id: string, - oldConfig: unknown, - timeoutMs: number | null, - ): Promise { - const actionProcedure = this.manifest.dependencies?.[id]?.config?.check - if (!actionProcedure) return null - if (actionProcedure.type === "docker") { - const container = await DockerProcedureContainer.of( - effects, - this.manifest.id, - actionProcedure, - this.manifest.volumes, - ) - return JSON.parse( - ( - await container.execFail( - [ - actionProcedure.entrypoint, - ...actionProcedure.args, - JSON.stringify(oldConfig), - ], - timeoutMs, - ) - ).stdout.toString(), - ) - } else if (actionProcedure.type === "script") { - const moduleCode = await this.moduleCode - const method = moduleCode.dependencies?.[id]?.check - if (!method) - throw new Error( - `Expecting that the method dependency check ${id} exists`, - ) - return (await method( - polyfillEffects(effects, this.manifest), - oldConfig as any, - ).then((x) => { - if ("result" in x) return x.result - if ("error" in x) throw new Error("Error getting config: " + x.error) - throw new Error("Error getting config: " + x["error-code"][1]) - })) as any - } else { - return {} - } - } private async dependenciesAutoconfig( effects: Effects, id: string, @@ -957,84 +916,96 @@ type CleanConfigFromPointers = async function updateConfig( effects: Effects, manifest: Manifest, - spec: unknown, - mutConfigValue: unknown, + spec: OldConfigSpec, + mutConfigValue: Record, ) { - if (!dictionary([string, unknown]).test(spec)) return - if (!dictionary([string, unknown]).test(mutConfigValue)) return for (const key in spec) { const specValue = spec[key] - const newConfigValue = mutConfigValue[key] - if (matchSpec.test(specValue)) { - const updateObject = { spec: newConfigValue } + if (specValue.type === "object") { await updateConfig( effects, manifest, - { spec: specValue.spec }, - updateObject, + specValue.spec as OldConfigSpec, + mutConfigValue[key] as Record, ) - mutConfigValue[key] = updateObject.spec - } - if ( - matchVariants.test(specValue) && - object({ tag: object({ id: string }) }).test(newConfigValue) && - newConfigValue.tag.id in specValue.variants - ) { - // Not going to do anything on the variants... - } - if (!matchPointer.test(specValue)) continue - if (matchPointerConfig.test(specValue)) { - const configValue = (await effects.store.get({ - packageId: specValue["package-id"], - callback() {}, - path: `${EMBASSY_POINTER_PATH_PREFIX}${specValue.selector}` as any, - })) as any - mutConfigValue[key] = configValue - } - if (matchPointerPackage.test(specValue)) { - if (specValue.target === "tor-key") - throw new Error("This service uses an unsupported target TorKey") - - const specInterface = specValue.interface - const serviceInterfaceId = extractServiceInterfaceId( + } else if (specValue.type === "list" && specValue.subtype === "object") { + const list = mutConfigValue[key] as unknown[] + for (let val of list) { + await updateConfig( + effects, + manifest, + { ...(specValue.spec as any), type: "object" as const }, + val as Record, + ) + } + } else if (specValue.type === "union") { + const union = mutConfigValue[key] as Record + await updateConfig( + effects, manifest, - specInterface, + specValue.variants[union[specValue.tag.id] as string] as OldConfigSpec, + mutConfigValue[key] as Record, ) - if (!serviceInterfaceId) { - mutConfigValue[key] = "" - return - } - const filled = await utils - .getServiceInterface(effects, { + } else if ( + specValue.type === "pointer" && + specValue.subtype === "package" + ) { + if (specValue.target === "config") { + const jp = require("jsonpath") + const remoteConfig = await effects.store.get({ packageId: specValue["package-id"], - id: serviceInterfaceId, + callback: () => effects.restart(), + path: EMBASSY_POINTER_PATH_PREFIX, }) - .once() - .catch((x) => { - console.error("Could not get the service interface", x) - return null - }) - const catchFn = (fn: () => X) => { - try { - return fn() - } catch (e) { - return undefined + console.debug(remoteConfig) + const configValue = specValue.multi + ? jp.query(remoteConfig, specValue.selector) + : jp.query(remoteConfig, specValue.selector, 1)[0] + mutConfigValue[key] = configValue === undefined ? null : configValue + } else if (specValue.target === "tor-key") { + throw new Error("This service uses an unsupported target TorKey") + } else { + const specInterface = specValue.interface + const serviceInterfaceId = extractServiceInterfaceId( + manifest, + specInterface, + ) + if (!serviceInterfaceId) { + mutConfigValue[key] = "" + return } + const filled = await utils + .getServiceInterface(effects, { + packageId: specValue["package-id"], + id: serviceInterfaceId, + }) + .once() + .catch((x) => { + console.error("Could not get the service interface", x) + return null + }) + const catchFn = (fn: () => X) => { + try { + return fn() + } catch (e) { + return undefined + } + } + const url: string = + filled === null || filled.addressInfo === null + ? "" + : catchFn(() => + utils.hostnameInfoToAddress( + specValue.target === "lan-address" + ? filled.addressInfo!.localHostnames[0] || + filled.addressInfo!.onionHostnames[0] + : filled.addressInfo!.onionHostnames[0] || + filled.addressInfo!.localHostnames[0], + ), + ) || "" + mutConfigValue[key] = url } - const url: string = - filled === null || filled.addressInfo === null - ? "" - : catchFn(() => - utils.hostnameInfoToAddress( - specValue.target === "lan-address" - ? filled.addressInfo!.localHostnames[0] || - filled.addressInfo!.onionHostnames[0] - : filled.addressInfo!.onionHostnames[0] || - filled.addressInfo!.localHostnames[0], - ), - ) || "" - mutConfigValue[key] = url } } } From 290a15bbd9ac17665bbe95b708a4acc05b2989c1 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Mon, 29 Jul 2024 22:42:17 -0600 Subject: [PATCH 113/125] remove sourceVersion and minor cleanup --- web/projects/marketplace/src/types.ts | 3 +-- web/projects/ui/src/app/pages/init/init.service.ts | 4 +--- .../app/pages/server-routes/sideload/sideload.page.ts | 11 +++++------ .../src/app/services/api/embassy-live-api.service.ts | 4 +--- .../ui/src/app/services/patch-db/patch-db-source.ts | 2 +- web/projects/ui/src/app/services/state.service.ts | 2 +- 6 files changed, 10 insertions(+), 16 deletions(-) diff --git a/web/projects/marketplace/src/types.ts b/web/projects/marketplace/src/types.ts index e25aa59eb..9853cddbb 100644 --- a/web/projects/marketplace/src/types.ts +++ b/web/projects/marketplace/src/types.ts @@ -3,7 +3,6 @@ import { T } from '@start9labs/start-sdk' export type GetPackageReq = { id: string version: string | null - sourceVersion: null // @TODO what is this? otherVersions: 'short' } export type GetPackageRes = T.GetPackageResponse & { @@ -13,9 +12,9 @@ export type GetPackageRes = T.GetPackageResponse & { export type GetPackagesReq = { id: null version: null - sourceVersion: null otherVersions: 'short' } + export type GetPackagesRes = { [id: T.PackageId]: GetPackageRes } diff --git a/web/projects/ui/src/app/pages/init/init.service.ts b/web/projects/ui/src/app/pages/init/init.service.ts index 96bf656d3..c98c0b11c 100644 --- a/web/projects/ui/src/app/pages/init/init.service.ts +++ b/web/projects/ui/src/app/pages/init/init.service.ts @@ -58,9 +58,7 @@ export class InitService extends Observable { } }), catchError(e => { - // @TODO Alex this toast is presenting when we navigate away from init page. It seems other websockets exhibit the same behavior, but we never noticed because the error were not being caught and presented in this manner. It seems odd that unsubscribing from a websocket subject would be treated as an error. - // this.errorService.handleError(e) - + console.error(e) return EMPTY }), ) diff --git a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts index a0da8609d..9e09aca62 100644 --- a/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/sideload/sideload.page.ts @@ -138,8 +138,8 @@ export class SideloadPage { ).getUint32(0, false) await getPositions(start, end, file, positions, tocLength as any) - await this.getManifest(positions, file) - await this.getIcon(positions, file) + await this.getManifestV1(positions, file) + await this.getIconV1(positions, file) } async parseS9pkV2(file: File) { @@ -148,7 +148,7 @@ export class SideloadPage { this.toUpload.icon = await s9pk.icon() } - async getManifest(positions: Positions, file: Blob) { + private async getManifestV1(positions: Positions, file: Blob) { const data = await blobToBuffer( file.slice( Number(positions['manifest'][0]), @@ -158,12 +158,11 @@ export class SideloadPage { this.toUpload.manifest = await cbor.decode(data, true) } - async getIcon(positions: Positions, file: Blob) { - const contentType = '' // @TODO + private async getIconV1(positions: Positions, file: Blob) { const data = file.slice( Number(positions['icon'][0]), Number(positions['icon'][0]) + Number(positions['icon'][1]), - contentType, + '', ) this.toUpload.icon = await blobToDataURL(data) } diff --git a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts index 87be49e36..7780670c0 100644 --- a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -38,7 +38,7 @@ export class LiveApiService extends ApiService { @Inject(PATCH_CACHE) private readonly cache$: Observable>, ) { super() - ;(window as any).rpcClient = this + ; (window as any).rpcClient = this } // for sideloading packages @@ -321,7 +321,6 @@ export class LiveApiService extends ApiService { const params: GetPackageReq = { id, version: versionRange, - sourceVersion: null, otherVersions: 'short', } @@ -335,7 +334,6 @@ export class LiveApiService extends ApiService { const params: GetPackagesReq = { id: null, version: null, - sourceVersion: null, otherVersions: 'short', } diff --git a/web/projects/ui/src/app/services/patch-db/patch-db-source.ts b/web/projects/ui/src/app/services/patch-db/patch-db-source.ts index 0b80370d0..d5ef3364e 100644 --- a/web/projects/ui/src/app/services/patch-db/patch-db-source.ts +++ b/web/projects/ui/src/app/services/patch-db/patch-db-source.ts @@ -41,7 +41,7 @@ export class PatchDbSource extends Observable[]> { catchError((_, original$) => { this.state.retrigger() - // @TODO this is returning right away, but we need to wait until state emits again from the retrigger() above. + // @TODO Alex this is returning right away and crashing the browser, but we need to wait until state emits again from the retrigger() above. return this.state.pipe( filter(current => current === 'running'), take(1), diff --git a/web/projects/ui/src/app/services/state.service.ts b/web/projects/ui/src/app/services/state.service.ts index 33569a751..d1dd3b383 100644 --- a/web/projects/ui/src/app/services/state.service.ts +++ b/web/projects/ui/src/app/services/state.service.ts @@ -111,7 +111,7 @@ export class StateService extends Observable { ), ), ) - .subscribe() // @TODO shouldn't this be subscribed in app component with the others? Do we ever need to unsubscribe? + .subscribe() // @TODO Alex shouldn't this be subscribed in app component with the others? Do we ever need to unsubscribe? constructor() { super(subscriber => this.stream$.subscribe(subscriber)) From 89e327383ee5745dbabc13efc365fd8bcc31b08d Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Tue, 30 Jul 2024 09:09:35 -0600 Subject: [PATCH 114/125] remove file uploads from config --- .../form-control/form-control.component.html | 1 - .../form/form-file/form-file.component.html | 31 ------------- .../form/form-file/form-file.component.scss | 46 ------------------- .../form/form-file/form-file.component.ts | 11 ----- .../ui/src/app/components/form/form.module.ts | 2 - .../ui/src/app/modals/config.component.ts | 20 -------- .../app/services/api/embassy-api.service.ts | 2 - .../services/api/embassy-live-api.service.ts | 9 ---- .../services/api/embassy-mock-api.service.ts | 5 -- .../ui/src/app/services/form.service.ts | 15 ------ 10 files changed, 142 deletions(-) delete mode 100644 web/projects/ui/src/app/components/form/form-file/form-file.component.html delete mode 100644 web/projects/ui/src/app/components/form/form-file/form-file.component.scss delete mode 100644 web/projects/ui/src/app/components/form/form-file/form-file.component.ts diff --git a/web/projects/ui/src/app/components/form/form-control/form-control.component.html b/web/projects/ui/src/app/components/form/form-control/form-control.component.html index 614aae05c..731d64a63 100644 --- a/web/projects/ui/src/app/components/form/form-control/form-control.component.html +++ b/web/projects/ui/src/app/components/form/form-control/form-control.component.html @@ -1,7 +1,6 @@ - diff --git a/web/projects/ui/src/app/components/form/form-file/form-file.component.html b/web/projects/ui/src/app/components/form/form-file/form-file.component.html deleted file mode 100644 index 9e9c16613..000000000 --- a/web/projects/ui/src/app/components/form/form-file/form-file.component.html +++ /dev/null @@ -1,31 +0,0 @@ - - - -
-
- {{ spec.name }} - * - -
- - - Click or drop file here - -
-
Drop file here
-
-
diff --git a/web/projects/ui/src/app/components/form/form-file/form-file.component.scss b/web/projects/ui/src/app/components/form/form-file/form-file.component.scss deleted file mode 100644 index 2e314972d..000000000 --- a/web/projects/ui/src/app/components/form/form-file/form-file.component.scss +++ /dev/null @@ -1,46 +0,0 @@ -@import '@taiga-ui/core/styles/taiga-ui-local'; - -.template { - @include transition(opacity); - - width: 100%; - display: flex; - align-items: center; - padding: 0 0.5rem; - font: var(--tui-font-text-m); - font-weight: bold; - - &_hidden { - opacity: 0; - } -} - -.drop { - @include fullsize(); - @include transition(opacity); - display: flex; - align-items: center; - justify-content: space-around; - - &_hidden { - opacity: 0; - } -} - -.label { - display: flex; - align-items: center; - max-width: 50%; -} - -small { - max-width: 50%; - font-weight: normal; - color: var(--tui-text-02); - margin-left: auto; -} - -tui-tag { - z-index: 1; - margin: -0.25rem -0.25rem -0.25rem auto; -} diff --git a/web/projects/ui/src/app/components/form/form-file/form-file.component.ts b/web/projects/ui/src/app/components/form/form-file/form-file.component.ts deleted file mode 100644 index 52d340fa6..000000000 --- a/web/projects/ui/src/app/components/form/form-file/form-file.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component } from '@angular/core' -import { TuiFileLike } from '@taiga-ui/kit' -import { CT } from '@start9labs/start-sdk' -import { Control } from '../control' - -@Component({ - selector: 'form-file', - templateUrl: './form-file.component.html', - styleUrls: ['./form-file.component.scss'], -}) -export class FormFileComponent extends Control {} diff --git a/web/projects/ui/src/app/components/form/form.module.ts b/web/projects/ui/src/app/components/form/form.module.ts index b7a36cd1f..5417309a9 100644 --- a/web/projects/ui/src/app/components/form/form.module.ts +++ b/web/projects/ui/src/app/components/form/form.module.ts @@ -40,7 +40,6 @@ import { FormToggleComponent } from './form-toggle/form-toggle.component' import { FormTextareaComponent } from './form-textarea/form-textarea.component' import { FormNumberComponent } from './form-number/form-number.component' import { FormSelectComponent } from './form-select/form-select.component' -import { FormFileComponent } from './form-file/form-file.component' import { FormMultiselectComponent } from './form-multiselect/form-multiselect.component' import { FormUnionComponent } from './form-union/form-union.component' import { FormObjectComponent } from './form-object/form-object.component' @@ -96,7 +95,6 @@ import { HintPipe } from './hint.pipe' FormNumberComponent, FormSelectComponent, FormMultiselectComponent, - FormFileComponent, FormUnionComponent, FormObjectComponent, FormArrayComponent, diff --git a/web/projects/ui/src/app/modals/config.component.ts b/web/projects/ui/src/app/modals/config.component.ts index 194302f5f..60cef3141 100644 --- a/web/projects/ui/src/app/modals/config.component.ts +++ b/web/projects/ui/src/app/modals/config.component.ts @@ -203,8 +203,6 @@ export class ConfigModal { const loader = new Subscription() try { - await this.uploadFiles(config, loader) - if (hasCurrentDeps(this.pkgId, await getAllPackages(this.patchDb))) { await this.configureDeps(config, loader) } else { @@ -217,24 +215,6 @@ export class ConfigModal { } } - private async uploadFiles(config: Record, loader: Subscription) { - loader.unsubscribe() - loader.closed = false - - // TODO: Could be nested files - const keys = Object.keys(config).filter(key => config[key] instanceof File) - const message = `Uploading File${keys.length > 1 ? 's' : ''}...` - - if (!keys.length) return - - loader.add(this.loader.open(message).subscribe()) - - const hashes = await Promise.all( - keys.map(key => this.embassyApi.uploadFile(config[key])), - ) - keys.forEach((key, i) => (config[key] = hashes[i])) - } - private async configureDeps( config: Record, loader: Subscription, diff --git a/web/projects/ui/src/app/services/api/embassy-api.service.ts b/web/projects/ui/src/app/services/api/embassy-api.service.ts index 579292300..acd6780d1 100644 --- a/web/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-api.service.ts @@ -14,8 +14,6 @@ export abstract class ApiService { // for sideloading packages abstract uploadPackage(guid: string, body: Blob): Promise - abstract uploadFile(body: Blob): Promise - // for getting static files: ex icons, instructions, licenses abstract getStaticProxy( pkg: MarketplacePkg, diff --git a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts index 7780670c0..324936e8d 100644 --- a/web/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -52,15 +52,6 @@ export class LiveApiService extends ApiService { }) } - async uploadFile(body: Blob): Promise { - return this.httpRequest({ - method: Method.POST, - body, - url: `/rest/upload`, - responseType: 'text', - }) - } - // for getting static files: ex. instructions, licenses async getStaticProxy( diff --git a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts index e063fb6e4..f716a1863 100644 --- a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -1084,11 +1084,6 @@ export class MockApiService extends ApiService { } } - async uploadFile(body: Blob): Promise { - await pauseFor(2000) - return 'returnedhash' - } - private async initProgress(): Promise { const progress = JSON.parse(JSON.stringify(PROGRESS)) diff --git a/web/projects/ui/src/app/services/form.service.ts b/web/projects/ui/src/app/services/form.service.ts index 3cd0ee591..779fb1f53 100644 --- a/web/projects/ui/src/app/services/form.service.ts +++ b/web/projects/ui/src/app/services/form.service.ts @@ -122,11 +122,6 @@ export class FormService { return this.getListItem(spec, entry) }) return this.formBuilder.array(mapped, listValidators(spec)) - case 'file': - return this.formBuilder.control( - currentValue || null, - fileValidators(spec), - ) case 'union': const currentSelection = currentValue?.selection const isValid = !!spec.variants[currentSelection] @@ -259,16 +254,6 @@ function listValidators(spec: CT.ValueSpecList): ValidatorFn[] { return validators } -function fileValidators(spec: CT.ValueSpecFile): ValidatorFn[] { - const validators: ValidatorFn[] = [] - - if (spec.required) { - validators.push(Validators.required) - } - - return validators -} - export function numberInRange( min: number | null, max: number | null, From 7cd3f285ad8b747c7053ab1ce63f1051cdcf2316 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Tue, 30 Jul 2024 12:08:20 -0600 Subject: [PATCH 115/125] fix dependency autoconfig --- core/startos/src/dependencies.rs | 34 +++++++++++++++++++----- core/startos/src/service/dependencies.rs | 2 +- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/core/startos/src/dependencies.rs b/core/startos/src/dependencies.rs index 54e38f299..013648980 100644 --- a/core/startos/src/dependencies.rs +++ b/core/startos/src/dependencies.rs @@ -9,13 +9,13 @@ use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler}; use serde::{Deserialize, Serialize}; use tracing::instrument; use ts_rs::TS; -use url::Url; use crate::config::{Config, ConfigSpec, ConfigureContext}; -use crate::context::RpcContext; +use crate::context::{CliContext, RpcContext}; use crate::db::model::package::CurrentDependencies; use crate::prelude::*; use crate::rpc_continuations::Guid; +use crate::util::serde::HandlerExtSerde; use crate::util::PathOrUrl; use crate::Error; @@ -65,11 +65,20 @@ pub struct ConfigureParams { dependency_id: PackageId, } pub fn configure() -> ParentHandler { - ParentHandler::new().root_handler( - from_fn_async(configure_impl) - .with_inherited(|params, _| params) - .no_cli(), - ) + ParentHandler::new() + .root_handler( + from_fn_async(configure_impl) + .with_inherited(|params, _| params) + .no_display() + .with_call_remote::(), + ) + .subcommand( + "dry", + from_fn_async(configure_dry) + .with_inherited(|params, _| params) + .with_display_serializable() + .with_call_remote::(), + ) } pub async fn configure_impl( @@ -105,6 +114,17 @@ pub async fn configure_impl( Ok(()) } +pub async fn configure_dry( + ctx: RpcContext, + _: Empty, + ConfigureParams { + dependent_id, + dependency_id, + }: ConfigureParams, +) -> Result { + configure_logic(ctx.clone(), (dependent_id, dependency_id.clone())).await +} + #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ConfigDryRes { diff --git a/core/startos/src/service/dependencies.rs b/core/startos/src/service/dependencies.rs index 60fd76ad6..e8c6f07c4 100644 --- a/core/startos/src/service/dependencies.rs +++ b/core/startos/src/service/dependencies.rs @@ -38,7 +38,7 @@ impl ServiceActorSeed { ) .await .with_kind(ErrorKind::Dependency) - .map(|res| res.filter(|c| !c.is_empty())) + .map(|res| res.filter(|c| !c.is_empty() && Some(c) != remote_config.as_ref())) } } From 972ee8e42e6864fbd758f57c9f736c0792fa168e Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Wed, 31 Jul 2024 00:06:44 -0600 Subject: [PATCH 116/125] premake 5 versions --- core/startos/src/version/mod.rs | 22 +++++++++++++- core/startos/src/version/v0_3_6_alpha_3.rs | 35 ++++++++++++++++++++++ core/startos/src/version/v0_3_6_alpha_4.rs | 35 ++++++++++++++++++++++ core/startos/src/version/v0_3_6_alpha_5.rs | 35 ++++++++++++++++++++++ core/startos/src/version/v0_3_6_alpha_6.rs | 35 ++++++++++++++++++++++ core/startos/src/version/v0_3_6_alpha_7.rs | 35 ++++++++++++++++++++++ 6 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 core/startos/src/version/v0_3_6_alpha_3.rs create mode 100644 core/startos/src/version/v0_3_6_alpha_4.rs create mode 100644 core/startos/src/version/v0_3_6_alpha_5.rs create mode 100644 core/startos/src/version/v0_3_6_alpha_6.rs create mode 100644 core/startos/src/version/v0_3_6_alpha_7.rs diff --git a/core/startos/src/version/mod.rs b/core/startos/src/version/mod.rs index 39e6eae72..63c42314a 100644 --- a/core/startos/src/version/mod.rs +++ b/core/startos/src/version/mod.rs @@ -16,8 +16,13 @@ mod v0_3_5_2; mod v0_3_6_alpha_0; mod v0_3_6_alpha_1; mod v0_3_6_alpha_2; +mod v0_3_6_alpha_3; +mod v0_3_6_alpha_4; +mod v0_3_6_alpha_5; +mod v0_3_6_alpha_6; +mod v0_3_6_alpha_7; -pub type Current = v0_3_6_alpha_2::Version; +pub type Current = v0_3_6_alpha_2::Version; // VERSION_BUMP #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[serde(untagged)] @@ -30,6 +35,11 @@ enum Version { V0_3_6_alpha_0(Wrapper), V0_3_6_alpha_1(Wrapper), V0_3_6_alpha_2(Wrapper), + V0_3_6_alpha_3(Wrapper), + V0_3_6_alpha_4(Wrapper), + V0_3_6_alpha_5(Wrapper), + V0_3_6_alpha_6(Wrapper), + V0_3_6_alpha_7(Wrapper), Other(exver::Version), } @@ -52,6 +62,11 @@ impl Version { Version::V0_3_6_alpha_0(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_1(Wrapper(x)) => x.semver(), Version::V0_3_6_alpha_2(Wrapper(x)) => x.semver(), + Version::V0_3_6_alpha_3(Wrapper(x)) => x.semver(), + Version::V0_3_6_alpha_4(Wrapper(x)) => x.semver(), + Version::V0_3_6_alpha_5(Wrapper(x)) => x.semver(), + Version::V0_3_6_alpha_6(Wrapper(x)) => x.semver(), + Version::V0_3_6_alpha_7(Wrapper(x)) => x.semver(), Version::Other(x) => x.clone(), } } @@ -254,6 +269,11 @@ pub async fn init( Version::V0_3_6_alpha_0(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, Version::V0_3_6_alpha_1(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, Version::V0_3_6_alpha_2(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, + Version::V0_3_6_alpha_3(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, + Version::V0_3_6_alpha_4(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, + Version::V0_3_6_alpha_5(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, + Version::V0_3_6_alpha_6(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, + Version::V0_3_6_alpha_7(v) => v.0.migrate_to(&Current::new(), &db, &mut progress).await?, Version::Other(_) => { return Err(Error::new( eyre!("Cannot downgrade"), diff --git a/core/startos/src/version/v0_3_6_alpha_3.rs b/core/startos/src/version/v0_3_6_alpha_3.rs new file mode 100644 index 000000000..3f244a2a0 --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_3.rs @@ -0,0 +1,35 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_2, VersionT}; +use crate::db::model::Database; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref V0_3_6_alpha_3: exver::Version = exver::Version::new( + [0, 3, 6], + [PreReleaseSegment::String("alpha".into()), 3.into()] + ); +} + +#[derive(Clone, Debug)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_2::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> exver::Version { + V0_3_6_alpha_3.clone() + } + fn compat(&self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + async fn up(&self, _db: &TypedPatchDb) -> Result<(), Error> { + Ok(()) + } + async fn down(&self, _db: &TypedPatchDb) -> Result<(), Error> { + Ok(()) + } +} diff --git a/core/startos/src/version/v0_3_6_alpha_4.rs b/core/startos/src/version/v0_3_6_alpha_4.rs new file mode 100644 index 000000000..0a60764e0 --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_4.rs @@ -0,0 +1,35 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_3, VersionT}; +use crate::db::model::Database; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref V0_3_6_alpha_4: exver::Version = exver::Version::new( + [0, 3, 6], + [PreReleaseSegment::String("alpha".into()), 4.into()] + ); +} + +#[derive(Clone, Debug)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_3::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> exver::Version { + V0_3_6_alpha_4.clone() + } + fn compat(&self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + async fn up(&self, _db: &TypedPatchDb) -> Result<(), Error> { + Ok(()) + } + async fn down(&self, _db: &TypedPatchDb) -> Result<(), Error> { + Ok(()) + } +} diff --git a/core/startos/src/version/v0_3_6_alpha_5.rs b/core/startos/src/version/v0_3_6_alpha_5.rs new file mode 100644 index 000000000..1b921d78b --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_5.rs @@ -0,0 +1,35 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_4, VersionT}; +use crate::db::model::Database; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref V0_3_6_alpha_5: exver::Version = exver::Version::new( + [0, 3, 6], + [PreReleaseSegment::String("alpha".into()), 5.into()] + ); +} + +#[derive(Clone, Debug)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_4::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> exver::Version { + V0_3_6_alpha_5.clone() + } + fn compat(&self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + async fn up(&self, _db: &TypedPatchDb) -> Result<(), Error> { + Ok(()) + } + async fn down(&self, _db: &TypedPatchDb) -> Result<(), Error> { + Ok(()) + } +} diff --git a/core/startos/src/version/v0_3_6_alpha_6.rs b/core/startos/src/version/v0_3_6_alpha_6.rs new file mode 100644 index 000000000..df91246ae --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_6.rs @@ -0,0 +1,35 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_5, VersionT}; +use crate::db::model::Database; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref V0_3_6_alpha_6: exver::Version = exver::Version::new( + [0, 3, 6], + [PreReleaseSegment::String("alpha".into()), 6.into()] + ); +} + +#[derive(Clone, Debug)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_5::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> exver::Version { + V0_3_6_alpha_6.clone() + } + fn compat(&self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + async fn up(&self, _db: &TypedPatchDb) -> Result<(), Error> { + Ok(()) + } + async fn down(&self, _db: &TypedPatchDb) -> Result<(), Error> { + Ok(()) + } +} diff --git a/core/startos/src/version/v0_3_6_alpha_7.rs b/core/startos/src/version/v0_3_6_alpha_7.rs new file mode 100644 index 000000000..7aa63fb2e --- /dev/null +++ b/core/startos/src/version/v0_3_6_alpha_7.rs @@ -0,0 +1,35 @@ +use exver::{PreReleaseSegment, VersionRange}; + +use super::v0_3_5::V0_3_0_COMPAT; +use super::{v0_3_6_alpha_6, VersionT}; +use crate::db::model::Database; +use crate::prelude::*; + +lazy_static::lazy_static! { + static ref V0_3_6_alpha_7: exver::Version = exver::Version::new( + [0, 3, 6], + [PreReleaseSegment::String("alpha".into()), 7.into()] + ); +} + +#[derive(Clone, Debug)] +pub struct Version; + +impl VersionT for Version { + type Previous = v0_3_6_alpha_6::Version; + fn new() -> Self { + Version + } + fn semver(&self) -> exver::Version { + V0_3_6_alpha_7.clone() + } + fn compat(&self) -> &'static VersionRange { + &V0_3_0_COMPAT + } + async fn up(&self, _db: &TypedPatchDb) -> Result<(), Error> { + Ok(()) + } + async fn down(&self, _db: &TypedPatchDb) -> Result<(), Error> { + Ok(()) + } +} From 82ba5dad1b0efdd10d5729cdd47d699589efb336 Mon Sep 17 00:00:00 2001 From: Mariusz Kogen Date: Wed, 31 Jul 2024 09:11:37 +0200 Subject: [PATCH 117/125] version bump --- core/Cargo.lock | 2 +- core/startos/Cargo.toml | 2 +- core/startos/src/version/mod.rs | 2 +- web/package.json | 2 +- web/patchdb-ui-seed.json | 2 +- web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/Cargo.lock b/core/Cargo.lock index 9c7b79c0d..2108ac851 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -4961,7 +4961,7 @@ dependencies = [ [[package]] name = "start-os" -version = "0.3.6-alpha.2" +version = "0.3.6-alpha.3" dependencies = [ "aes", "async-compression", diff --git a/core/startos/Cargo.toml b/core/startos/Cargo.toml index 6969b5301..1e1cd4737 100644 --- a/core/startos/Cargo.toml +++ b/core/startos/Cargo.toml @@ -14,7 +14,7 @@ keywords = [ name = "start-os" readme = "README.md" repository = "https://github.com/Start9Labs/start-os" -version = "0.3.6-alpha.2" +version = "0.3.6-alpha.3" license = "MIT" [lib] diff --git a/core/startos/src/version/mod.rs b/core/startos/src/version/mod.rs index 63c42314a..be5c8d9a5 100644 --- a/core/startos/src/version/mod.rs +++ b/core/startos/src/version/mod.rs @@ -22,7 +22,7 @@ mod v0_3_6_alpha_5; mod v0_3_6_alpha_6; mod v0_3_6_alpha_7; -pub type Current = v0_3_6_alpha_2::Version; // VERSION_BUMP +pub type Current = v0_3_6_alpha_3::Version; // VERSION_BUMP #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[serde(untagged)] diff --git a/web/package.json b/web/package.json index 4a7047041..1697f1343 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "startos-ui", - "version": "0.3.6-alpha.2", + "version": "0.3.6-alpha.3", "author": "Start9 Labs, Inc", "homepage": "https://start9.com/", "license": "MIT", diff --git a/web/patchdb-ui-seed.json b/web/patchdb-ui-seed.json index f15a8d09b..0e15cd2cf 100644 --- a/web/patchdb-ui-seed.json +++ b/web/patchdb-ui-seed.json @@ -21,5 +21,5 @@ "ackInstructions": {}, "theme": "Dark", "widgets": [], - "ack-welcome": "0.3.6-alpha.2" + "ack-welcome": "0.3.6-alpha.3" } diff --git a/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html b/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html index 258c92f83..79fe3f8da 100644 --- a/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html +++ b/web/projects/ui/src/app/modals/os-welcome/os-welcome.page.html @@ -12,7 +12,7 @@

This Release

-

0.3.6-alpha.2

+

0.3.6-alpha.3

This is an ALPHA release! DO NOT use for production data!
Expect that any data you create or store on this version of the OS can be LOST FOREVER!
From 6168a006f436bb93701cae93d00ebea5dcb6f796 Mon Sep 17 00:00:00 2001 From: waterplea Date: Wed, 31 Jul 2024 11:57:56 +0400 Subject: [PATCH 118/125] fix: address TODOs and close dialogs upon state change --- web/projects/ui/src/app/app.providers.ts | 17 +++++++++++++++-- .../app/services/patch-db/patch-db-source.ts | 3 ++- .../ui/src/app/services/state.service.ts | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/web/projects/ui/src/app/app.providers.ts b/web/projects/ui/src/app/app.providers.ts index 00ff64690..101d402b3 100644 --- a/web/projects/ui/src/app/app.providers.ts +++ b/web/projects/ui/src/app/app.providers.ts @@ -1,14 +1,16 @@ -import { APP_INITIALIZER, Provider } from '@angular/core' +import { APP_INITIALIZER, inject, Provider } from '@angular/core' import { UntypedFormBuilder } from '@angular/forms' import { Router, RouteReuseStrategy } from '@angular/router' import { IonicRouteStrategy, IonNav } from '@ionic/angular' import { RELATIVE_URL, THEME, WorkspaceConfig } from '@start9labs/shared' -import { TUI_ICONS_PATH } from '@taiga-ui/core' +import { TUI_DIALOGS_CLOSE, TUI_ICONS_PATH } from '@taiga-ui/core' import { PatchDB } from 'patch-db-client' +import { filter, pairwise } from 'rxjs' import { PATCH_CACHE, PatchDbSource, } from 'src/app/services/patch-db/patch-db-source' +import { StateService } from 'src/app/services/state.service' import { ApiService } from './services/api/embassy-api.service' import { MockApiService } from './services/api/embassy-mock-api.service' import { LiveApiService } from './services/api/embassy-live-api.service' @@ -58,6 +60,17 @@ export const APP_PROVIDERS: Provider[] = [ provide: TUI_ICONS_PATH, useValue: (name: string) => `/assets/taiga-ui/icons/${name}.svg#${name}`, }, + { + provide: TUI_DIALOGS_CLOSE, + useFactory: () => + inject(StateService).pipe( + pairwise(), + filter( + ([prev, curr]) => + prev === 'running' && (curr === 'error' || curr === 'initializing'), + ), + ), + }, ] export function appInitializer( diff --git a/web/projects/ui/src/app/services/patch-db/patch-db-source.ts b/web/projects/ui/src/app/services/patch-db/patch-db-source.ts index d5ef3364e..00ab3d43e 100644 --- a/web/projects/ui/src/app/services/patch-db/patch-db-source.ts +++ b/web/projects/ui/src/app/services/patch-db/patch-db-source.ts @@ -5,6 +5,7 @@ import { bufferTime, catchError, filter, + skip, startWith, switchMap, take, @@ -41,8 +42,8 @@ export class PatchDbSource extends Observable[]> { catchError((_, original$) => { this.state.retrigger() - // @TODO Alex this is returning right away and crashing the browser, but we need to wait until state emits again from the retrigger() above. return this.state.pipe( + skip(1), // skipping previous value stored due to shareReplay filter(current => current === 'running'), take(1), switchMap(() => original$), diff --git a/web/projects/ui/src/app/services/state.service.ts b/web/projects/ui/src/app/services/state.service.ts index d1dd3b383..9eb8caf0a 100644 --- a/web/projects/ui/src/app/services/state.service.ts +++ b/web/projects/ui/src/app/services/state.service.ts @@ -111,7 +111,7 @@ export class StateService extends Observable { ), ), ) - .subscribe() // @TODO Alex shouldn't this be subscribed in app component with the others? Do we ever need to unsubscribe? + .subscribe() constructor() { super(subscriber => this.stream$.subscribe(subscriber)) From 0cfc43c4447aa9a2f5aaa4f156efe9017c2a00ed Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Thu, 1 Aug 2024 21:39:16 -0600 Subject: [PATCH 119/125] don't deprecate wifi --- .../server-routes/server-routing.module.ts | 2 +- .../server-show/server-show.page.html | 3 +- .../server-show/server-show.page.ts | 10 +- .../pages/server-routes/wifi/wifi.page.html | 242 +++++++++--------- .../app/pages/server-routes/wifi/wifi.page.ts | 6 + 5 files changed, 128 insertions(+), 135 deletions(-) diff --git a/web/projects/ui/src/app/pages/server-routes/server-routing.module.ts b/web/projects/ui/src/app/pages/server-routes/server-routing.module.ts index e7eb43aad..945f1b1c9 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-routing.module.ts +++ b/web/projects/ui/src/app/pages/server-routes/server-routing.module.ts @@ -76,7 +76,7 @@ const routes: Routes = [ import('./ssh-keys/ssh-keys.module').then(m => m.SSHKeysPageModule), }, { - path: 'wifi', + path: 'wireless', loadChildren: () => import('./wifi/wifi.module').then(m => m.WifiPageModule), }, diff --git a/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html b/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html index 01ecf897e..d6adc15d1 100644 --- a/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html +++ b/web/projects/ui/src/app/pages/server-routes/server-show/server-show.page.html @@ -50,8 +50,7 @@ - this.navCtrl.navigateForward(['wifi'], { relativeTo: this.route }), + this.navCtrl.navigateForward(['wireless'], { + relativeTo: this.route, + }), detail: true, disabled$: of(false), }, diff --git a/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html b/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html index 08387fcbc..1ddb44225 100644 --- a/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html +++ b/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html @@ -3,8 +3,8 @@ - WiFi Settings - + Wireless Settings + Refresh @@ -13,163 +13,149 @@ - - - - - - -

WiFi is deprecated

-

- WiFi will be eliminated from StartOS in version v0.4.0, expected soon. - Before then, we highly recommend switching your server to a direct, - Ethernet connection, which is faster and more reliable. If using - Ethernet is not possible for you, we recommend using a WiFi extender - instead. -

-
- - Learn More - - -
+ + + + Country - Country - - - - - - {{ wifi.country }} - {{ this.countries[wifi.country] }} - - Select Country - - - - - Saved Networks - - - - - - - - - - Available Networks - - - - - - - - - - - - - - Saved Networks - + -
- - {{ ssid.key }} - - - - + + + {{ wifi.country }} - {{ this.countries[wifi.country] }} + + Select Country
- Available Networks - + + + Saved Networks + + + + + + + + + + Available Networks + + + + + + + + + + + + + + Saved Networks + - {{ avWifi.ssid }} +
+ + {{ ssid.key }}
+ + Available Networks + + + {{ avWifi.ssid }} + + + + + + + + + + Join Another Network + +
- - - - Join Another Network - - -
-
+
+ +

No wireless interface detected.

+
+
diff --git a/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts b/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts index 0a1f6a7d9..5d722b482 100644 --- a/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts +++ b/web/projects/ui/src/app/pages/server-routes/wifi/wifi.page.ts @@ -9,11 +9,14 @@ import { WINDOW } from '@ng-web-apis/common' import { ErrorService, LoadingService, pauseFor } from '@start9labs/shared' import { CT } from '@start9labs/start-sdk' import { TuiDialogOptions } from '@taiga-ui/core' +import { PatchDB } from 'patch-db-client' import { FormComponent, FormContext } from 'src/app/components/form.component' import { RR } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' import { ConfigService } from 'src/app/services/config.service' +import { ConnectionService } from 'src/app/services/connection.service' import { FormDialogService } from 'src/app/services/form-dialog.service' +import { DataModel } from 'src/app/services/patch-db/data-model' export interface WiFiForm { ssid: string @@ -31,6 +34,7 @@ export class WifiPage { countries = require('../../../util/countries.json') as { [key: string]: string } + readonly hasWifi$ = this.patch.watch$('serverInfo', 'wifi', 'interface') constructor( private readonly api: ApiService, @@ -41,6 +45,8 @@ export class WifiPage { private readonly errorService: ErrorService, private readonly actionCtrl: ActionSheetController, private readonly config: ConfigService, + private readonly patch: PatchDB, + readonly connection$: ConnectionService, @Inject(WINDOW) private readonly windowRef: Window, ) {} From 512ed71fc36d3c74ef6b59118745d980d7e5dab5 Mon Sep 17 00:00:00 2001 From: J H Date: Mon, 5 Aug 2024 13:11:55 -0600 Subject: [PATCH 120/125] fixes: The case on the readonly that the path before doesn't exist, just let it --- core/startos/src/service/effects/dependency.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/core/startos/src/service/effects/dependency.rs b/core/startos/src/service/effects/dependency.rs index 34619ebd4..ad5ec2e9b 100644 --- a/core/startos/src/service/effects/dependency.rs +++ b/core/startos/src/service/effects/dependency.rs @@ -55,11 +55,8 @@ pub async fn mount( let subpath = subpath.unwrap_or_default(); let subpath = subpath.strip_prefix("/").unwrap_or(&subpath); let source = data_dir(&context.seed.ctx.datadir, &package_id, &volume_id).join(subpath); - if readonly && tokio::fs::metadata(&source).await.is_err() { - return Err(Error::new( - eyre!("{volume_id}/{} does not exist", subpath.display()), - ErrorKind::NotFound, - )); + if tokio::fs::metadata(&source).await.is_err() { + tokio::fs::create_dir_all(&source).await?; } let location = location.strip_prefix("/").unwrap_or(&location); let mountpoint = context From 93640bb08e1b67e94efd23ef57bc10b96866e647 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Tue, 6 Aug 2024 13:20:59 -0600 Subject: [PATCH 121/125] don't bail on error in dep config on startup --- core/startos/src/context/rpc.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/startos/src/context/rpc.rs b/core/startos/src/context/rpc.rs index 0d00abb3a..0db681d3b 100644 --- a/core/startos/src/context/rpc.rs +++ b/core/startos/src/context/rpc.rs @@ -297,7 +297,9 @@ impl RpcContext { for (package_id, package) in peek.as_public().as_package_data().as_entries()?.into_iter() { let package = package.clone(); let mut current_dependencies = package.as_current_dependencies().de()?; - compute_dependency_config_errs(self, &package_id, &mut current_dependencies).await?; + compute_dependency_config_errs(self, &package_id, &mut current_dependencies) + .await + .log_err(); updated_current_dependents.insert(package_id.clone(), current_dependencies); } self.db From 4427aeac54476088931fb8c6cf18298320cdff72 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Tue, 6 Aug 2024 13:21:12 -0600 Subject: [PATCH 122/125] fix asset mounts --- core/startos/src/service/persistent_container.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/core/startos/src/service/persistent_container.rs b/core/startos/src/service/persistent_container.rs index 23f01605e..c81322719 100644 --- a/core/startos/src/service/persistent_container.rs +++ b/core/startos/src/service/persistent_container.rs @@ -170,17 +170,17 @@ impl PersistentContainer { .arg(&mountpoint) .invoke(crate::ErrorKind::Filesystem) .await?; + let s9pk_asset_path = Path::new("assets").join(asset).with_extension("squashfs"); + let sqfs = s9pk + .as_archive() + .contents() + .get_path(&s9pk_asset_path) + .and_then(|e| e.as_file()) + .or_not_found(s9pk_asset_path.display())?; assets.insert( asset.clone(), MountGuard::mount( - &Bind::new( - asset_dir( - &ctx.datadir, - &s9pk.as_manifest().id, - &s9pk.as_manifest().version, - ) - .join(asset), - ), + &IdMapped::new(LoopDev::from(&**sqfs), 0, 100000, 65536), mountpoint, MountType::ReadWrite, ) From 0e8530172c46919749113cf40742234ef5b748fa Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Tue, 6 Aug 2024 13:59:14 -0600 Subject: [PATCH 123/125] fix config set dry --- core/startos/src/config/mod.rs | 70 +++++++++++++++++-- .../ui/src/app/modals/config.component.ts | 6 +- .../ui/src/app/services/api/api.types.ts | 2 +- .../services/api/embassy-mock-api.service.ts | 2 +- 4 files changed, 68 insertions(+), 12 deletions(-) diff --git a/core/startos/src/config/mod.rs b/core/startos/src/config/mod.rs index 01309a16f..22edd98f7 100644 --- a/core/startos/src/config/mod.rs +++ b/core/startos/src/config/mod.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeSet; use std::sync::Arc; use std::time::Duration; @@ -178,13 +179,68 @@ pub struct SetParams { // )] #[instrument(skip_all)] pub fn set() -> ParentHandler { - ParentHandler::new().root_handler( - from_fn_async(set_impl) - .with_metadata("sync_db", Value::Bool(true)) - .with_inherited(|set_params, id| (id, set_params)) - .no_display() - .with_call_remote::(), - ) + ParentHandler::new() + .root_handler( + from_fn_async(set_impl) + .with_metadata("sync_db", Value::Bool(true)) + .with_inherited(|set_params, id| (id, set_params)) + .no_display() + .with_call_remote::(), + ) + .subcommand( + "dry", + from_fn_async(set_dry) + .with_inherited(|set_params, id| (id, set_params)) + .no_display() + .with_call_remote::(), + ) +} + +pub async fn set_dry( + ctx: RpcContext, + _: Empty, + ( + id, + SetParams { + timeout, + config: StdinDeserializable(config), + }, + ): (PackageId, SetParams), +) -> Result, Error> { + let mut breakages = BTreeSet::new(); + + let procedure_id = Guid::new(); + + let db = ctx.db.peek().await; + for dep in db + .as_public() + .as_package_data() + .as_entries()? + .into_iter() + .filter_map( + |(k, v)| match v.as_current_dependencies().contains_key(&id) { + Ok(true) => Some(Ok(k)), + Ok(false) => None, + Err(e) => Some(Err(e)), + }, + ) + { + let dep_id = dep?; + + let Some(dependent) = &*ctx.services.get(&dep_id).await else { + continue; + }; + + if dependent + .dependency_config(procedure_id.clone(), id.clone(), config.clone()) + .await? + .is_some() + { + breakages.insert(dep_id); + } + } + + Ok(breakages) } #[derive(Default)] diff --git a/web/projects/ui/src/app/modals/config.component.ts b/web/projects/ui/src/app/modals/config.component.ts index 60cef3141..d0e8afa4d 100644 --- a/web/projects/ui/src/app/modals/config.component.ts +++ b/web/projects/ui/src/app/modals/config.component.ts @@ -6,7 +6,7 @@ import { isEmptyObject, LoadingService, } from '@start9labs/shared' -import { CT } from '@start9labs/start-sdk' +import { CT, T } from '@start9labs/start-sdk' import { TuiButtonModule } from '@taiga-ui/experimental' import { TuiDialogContext, @@ -245,11 +245,11 @@ export class ConfigModal { this.context.$implicit.complete() } - private async approveBreakages(breakages: Breakages): Promise { + private async approveBreakages(breakages: T.PackageId[]): Promise { const packages = await getAllPackages(this.patchDb) const message = 'As a result of this change, the following services will no longer work properly and may crash:
    ' - const content = `${message}${Object.keys(breakages).map( + const content = `${message}${breakages.map( id => `
  • ${getManifest(packages[id]).title}
  • `, )}
` const data: TuiPromptData = { content, yes: 'Continue', no: 'Cancel' } diff --git a/web/projects/ui/src/app/services/api/api.types.ts b/web/projects/ui/src/app/services/api/api.types.ts index faa4a072e..5742ba67f 100644 --- a/web/projects/ui/src/app/services/api/api.types.ts +++ b/web/projects/ui/src/app/services/api/api.types.ts @@ -230,7 +230,7 @@ export module RR { export type GetPackageConfigRes = { spec: CT.InputSpec; config: object } export type DrySetPackageConfigReq = { id: string; config: object } // package.config.set.dry - export type DrySetPackageConfigRes = Breakages + export type DrySetPackageConfigRes = T.PackageId[] export type SetPackageConfigReq = DrySetPackageConfigReq // package.config.set export type SetPackageConfigRes = null diff --git a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts index f716a1863..d44ef809a 100644 --- a/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/web/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -790,7 +790,7 @@ export class MockApiService extends ApiService { params: RR.DrySetPackageConfigReq, ): Promise { await pauseFor(2000) - return {} + return [] } async setPackageConfig( From d0c2dc53fe36b3a704bfd19f04a468560165bf3b Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Tue, 6 Aug 2024 14:27:05 -0600 Subject: [PATCH 124/125] fix dns in the overlay --- sdk/lib/util/Overlay.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sdk/lib/util/Overlay.ts b/sdk/lib/util/Overlay.ts index 4d6d36b34..93cb44238 100644 --- a/sdk/lib/util/Overlay.ts +++ b/sdk/lib/util/Overlay.ts @@ -26,6 +26,8 @@ export class Overlay { shared.push("run") } + fs.copyFile("/etc/resolv.conf", `${rootfs}/etc/resolv.conf`) + for (const dirPart of shared) { const from = `/${dirPart}` const to = `${rootfs}/${dirPart}` From 7c32404b6976f761a0001a4f2e9ab9e6900c4fb3 Mon Sep 17 00:00:00 2001 From: Aiden McClelland Date: Tue, 6 Aug 2024 14:40:49 -0600 Subject: [PATCH 125/125] fix test --- .../Adapters/Systems/SystemForEmbassy/__fixtures__/bitcoind.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/bitcoind.ts b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/bitcoind.ts index 1134d198a..9a643b39d 100644 --- a/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/bitcoind.ts +++ b/container-runtime/src/Adapters/Systems/SystemForEmbassy/__fixtures__/bitcoind.ts @@ -315,7 +315,7 @@ export default { }, }, variants: { - disabled: undefined, + disabled: {}, automatic: { size: { type: "number",

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 literal 0 HcmV?d00001 diff --git a/assets/create-vm/step-2.png b/assets/create-vm/step-2.png new file mode 100644 index 0000000000000000000000000000000000000000..020f3d7d178cf929b693778b1b69344519cddc13 GIT binary patch literal 47071 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 literal 0 HcmV?d00001 diff --git a/assets/create-vm/step-5.png b/assets/create-vm/step-5.png new file mode 100644 index 0000000000000000000000000000000000000000..d34cb16a73caeddcb13b67d29593d217812bd22f GIT binary patch literal 65075 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%0D% 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 Q2;iT%h>UQlpsvsV0Y+KGfB*mh literal 0 HcmV?d00001 diff --git a/assets/create-vm/step-7.png b/assets/create-vm/step-7.png new file mode 100644 index 0000000000000000000000000000000000000000..ad8eb9b81c5e54449f432fc300d14a1fc661057b GIT binary patch literal 65527 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 o+-~971$48EO_w6wAV_ye-qF|Jd(OG{ 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 literal 0 HcmV?d00001 diff --git a/assets/create-vm/step-9.png b/assets/create-vm/step-9.png new file mode 100644 index 0000000000000000000000000000000000000000..0427354903fa5fdb8e87527e84f4a1b001f4ae3b GIT binary patch literal 45452 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 echo 'Must be run as root' exit 1 @@ -77,20 +79,7 @@ umount /media/startos/next/boot if [ "$CHROOT_RES" -eq 0 ]; then if [ -h /media/startos/config/current.rootfs ] && [ -e /media/startos/config/current.rootfs ]; then - echo 'Pruning...' - current="$(readlink -f /media/startos/config/current.rootfs)" - needed=$(du -s --bytes /media/startos/next | awk '{print $1}') - while [[ "$(df -B1 --output=avail --sync /media/startos/images | tail -n1)" -lt "$needed" ]]; do - to_prune="$(ls -t1 /media/startos/images/*.rootfs | grep -v "$current" | tail -n1)" - if [ -e "$to_prune" ]; then - echo " Pruning $to_prune" - rm -rf "$to_prune" - else - >&2 echo "Not enough space and nothing to prune!" - exit 1 - fi - done - echo 'done.' + ${SOURCE_DIR}/prune-images $(du -s --bytes /media/startos/next | awk '{print $1}') fi echo 'Upgrading...' diff --git a/build/lib/scripts/prune-images b/build/lib/scripts/prune-images new file mode 100755 index 000000000..21861467f --- /dev/null +++ b/build/lib/scripts/prune-images @@ -0,0 +1,49 @@ +#!/bin/bash + +if [ "$UID" -ne 0 ]; then + >&2 echo 'Must be run as root' + exit 1 +fi + +POSITIONAL_ARGS=() + +while [[ $# -gt 0 ]]; do + case $1 in + -*|--*) + echo "Unknown option $1" + exit 1 + ;; + *) + POSITIONAL_ARGS+=("$1") # save positional arg + shift # past argument + ;; + esac +done + +set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters + +needed=$1 + +if [ -z "$needed" ]; then + >&2 echo "usage: $0 " + exit 1 +fi + +if [ -h /media/startos/config/current.rootfs ] && [ -e /media/startos/config/current.rootfs ]; then + echo 'Pruning...' + current="$(readlink -f /media/startos/config/current.rootfs)" + while [[ "$(df -B1 --output=avail --sync /media/startos/images | tail -n1)" -lt "$needed" ]]; do + to_prune="$(ls -t1 /media/startos/images/*.rootfs | grep -v "$current" | tail -n1)" + if [ -e "$to_prune" ]; then + echo " Pruning $to_prune" + rm -rf "$to_prune" + else + >&2 echo "Not enough space and nothing to prune!" + exit 1 + fi + done + echo 'done.' +else + >&2 echo 'No current.rootfs, not safe to prune' + exit 1 +fi \ No newline at end of file From c16d8a1da192af57a41098dab305330e0302d380 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Tue, 25 Jun 2024 20:58:24 -0600 Subject: [PATCH 052/125] fix setup wizard styles and remove diagnostic from angular.json (#2656) --- web/angular.json | 131 +----------------- .../src/app/pages/loading/loading.module.ts | 11 +- .../src/app/pages/loading/loading.page.html | 40 +++--- .../src/app/pages/loading/loading.page.scss | 9 ++ .../ui/src/app/pages/init/init.page.html | 2 +- 5 files changed, 42 insertions(+), 151 deletions(-) diff --git a/web/angular.json b/web/angular.json index 3a91dd00b..cf6c113a4 100644 --- a/web/angular.json +++ b/web/angular.json @@ -302,6 +302,7 @@ } ], "styles": [ + "node_modules/@taiga-ui/core/styles/taiga-ui-theme.less", "projects/shared/styles/variables.scss", "projects/shared/styles/global.scss", "projects/shared/styles/shared.scss", @@ -393,136 +394,6 @@ } } }, - "diagnostic-ui": { - "projectType": "application", - "schematics": {}, - "root": "projects/diagnostic-ui", - "sourceRoot": "projects/diagnostic-ui/src", - "prefix": "app", - "architect": { - "build": { - "builder": "@angular-devkit/build-angular:browser", - "options": { - "outputPath": "dist/raw/diagnostic-ui", - "index": "projects/diagnostic-ui/src/index.html", - "main": "projects/diagnostic-ui/src/main.ts", - "polyfills": "projects/diagnostic-ui/src/polyfills.ts", - "tsConfig": "projects/diagnostic-ui/tsconfig.json", - "inlineStyleLanguage": "scss", - "assets": [ - { - "glob": "**/*", - "input": "projects/shared/assets", - "output": "assets" - }, - { - "glob": "**/*.svg", - "input": "node_modules/ionicons/dist/ionicons/svg", - "output": "./svg" - } - ], - "styles": [ - "projects/shared/styles/variables.scss", - "projects/shared/styles/global.scss", - "projects/shared/styles/shared.scss", - "projects/diagnostic-ui/src/styles.scss" - ], - "scripts": [] - }, - "configurations": { - "production": { - "fileReplacements": [ - { - "replace": "projects/diagnostic-ui/src/environments/environment.ts", - "with": "projects/diagnostic-ui/src/environments/environment.prod.ts" - } - ], - "optimization": true, - "outputHashing": "all", - "sourceMap": false, - "namedChunks": false, - "aot": true, - "extractLicenses": true, - "vendorChunk": false, - "buildOptimizer": true, - "budgets": [ - { - "type": "initial", - "maximumWarning": "2mb", - "maximumError": "5mb" - } - ] - }, - "ci": { - "progress": false - }, - "development": { - "buildOptimizer": false, - "optimization": false, - "vendorChunk": true, - "extractLicenses": false, - "sourceMap": true, - "namedChunks": true - } - }, - "defaultConfiguration": "production" - }, - "serve": { - "builder": "@angular-devkit/build-angular:dev-server", - "options": { - "browserTarget": "diagnostic-ui:build" - }, - "configurations": { - "production": { - "browserTarget": "diagnostic-ui:build:production" - }, - "development": { - "browserTarget": "diagnostic-ui:build:development" - } - }, - "defaultConfiguration": "development" - }, - "extract-i18n": { - "builder": "@angular-devkit/build-angular:extract-i18n", - "options": { - "browserTarget": "diagnostic-ui:build" - } - }, - "lint": { - "builder": "@angular-eslint/builder:lint", - "options": { - "lintFilePatterns": [ - "projects/diagnostic-ui/src/**/*.ts", - "projects/diagnostic-ui/src/**/*.html" - ] - } - }, - "ionic-cordova-build": { - "builder": "@ionic/angular-toolkit:cordova-build", - "options": { - "browserTarget": "diagnostic-ui:build" - }, - "configurations": { - "production": { - "browserTarget": "diagnostic-ui:build:production" - } - } - }, - "ionic-cordova-serve": { - "builder": "@ionic/angular-toolkit:cordova-serve", - "options": { - "cordovaBuildTarget": "diagnostic-ui:ionic-cordova-build", - "devServerTarget": "diagnostic-ui:serve" - }, - "configurations": { - "production": { - "cordovaBuildTarget": "diagnostic-ui:ionic-cordova-build:production", - "devServerTarget": "diagnostic-ui:serve:production" - } - } - } - } - }, "marketplace": { "projectType": "library", "root": "projects/marketplace", diff --git a/web/projects/setup-wizard/src/app/pages/loading/loading.module.ts b/web/projects/setup-wizard/src/app/pages/loading/loading.module.ts index 2f3507941..498620697 100644 --- a/web/projects/setup-wizard/src/app/pages/loading/loading.module.ts +++ b/web/projects/setup-wizard/src/app/pages/loading/loading.module.ts @@ -1,12 +1,17 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { FormsModule } from '@angular/forms' +import { TuiProgressModule } from '@taiga-ui/kit' import { LoadingPage } from './loading.page' import { LoadingPageRoutingModule } from './loading-routing.module' +import { IonicModule } from '@ionic/angular' @NgModule({ - imports: [CommonModule, FormsModule, IonicModule, LoadingPageRoutingModule], + imports: [ + CommonModule, + IonicModule, + TuiProgressModule, + LoadingPageRoutingModule, + ], declarations: [LoadingPage], }) export class LoadingPageModule {} diff --git a/web/projects/setup-wizard/src/app/pages/loading/loading.page.html b/web/projects/setup-wizard/src/app/pages/loading/loading.page.html index 6c9ca41ab..94a666223 100644 --- a/web/projects/setup-wizard/src/app/pages/loading/loading.page.html +++ b/web/projects/setup-wizard/src/app/pages/loading/loading.page.html @@ -1,17 +1,23 @@ -