From 0d397ab5cc1c0ca8dde985c27f3234f05eb5fcdb Mon Sep 17 00:00:00 2001 From: pedrofaustino <1153411+pedrofaustino@users.noreply.github.com> Date: Fri, 14 Feb 2025 12:47:34 +0100 Subject: [PATCH 001/133] Display error messages if IPv4 or IPv6 address not found on nymtun0 (issue #5461) --- scripts/network_tunnel_manager.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/network_tunnel_manager.sh b/scripts/network_tunnel_manager.sh index 93e6f73ada..58eff34565 100644 --- a/scripts/network_tunnel_manager.sh +++ b/scripts/network_tunnel_manager.sh @@ -167,6 +167,8 @@ joke_through_tunnel() { else echo -e "${red}IPv4 connectivity is not working for $interface. verify your routing and NAT settings.${reset}" fi + else + echo -e "${red}no IPv4 address found on $interface. unable to fetch a joke via IPv4.${reset}" fi if [[ -n "$ipv6_address" ]]; then @@ -183,6 +185,8 @@ joke_through_tunnel() { else echo -e "${red}IPv6 connectivity is not working for $interface. verify your routing and NAT settings.${reset}" fi + else + echo -e "${red}no IPv6 address found on $interface. unable to fetch a joke via IPv6.${reset}" fi echo -e "${green}joke fetching processes completed for $interface.${reset}" From 63a8f96ea59f355335c1539dc22f31783dc155e9 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Wed, 19 Feb 2025 12:13:24 +0100 Subject: [PATCH 002/133] bump versions --- Cargo.lock | 16 ++++++++-------- clients/native/Cargo.toml | 2 +- clients/socks5/Cargo.toml | 2 +- explorer-api/Cargo.toml | 2 +- nym-api/Cargo.toml | 2 +- nym-node/Cargo.toml | 2 +- service-providers/network-requester/Cargo.toml | 2 +- tools/nym-cli/Cargo.toml | 2 +- tools/nymvisor/Cargo.toml | 2 +- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb3454c887..292aaa93d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2420,7 +2420,7 @@ dependencies = [ [[package]] name = "explorer-api" -version = "1.1.46" +version = "1.1.47" dependencies = [ "chrono", "clap", @@ -4747,7 +4747,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "nym-api" -version = "1.1.50" +version = "1.1.51" dependencies = [ "anyhow", "async-trait", @@ -4995,7 +4995,7 @@ dependencies = [ [[package]] name = "nym-cli" -version = "1.1.48" +version = "1.1.49" dependencies = [ "anyhow", "base64 0.22.1", @@ -5078,7 +5078,7 @@ dependencies = [ [[package]] name = "nym-client" -version = "1.1.48" +version = "1.1.49" dependencies = [ "bs58", "clap", @@ -6138,7 +6138,7 @@ dependencies = [ [[package]] name = "nym-network-requester" -version = "1.1.49" +version = "1.1.50" dependencies = [ "addr", "anyhow", @@ -6189,7 +6189,7 @@ dependencies = [ [[package]] name = "nym-node" -version = "1.5.0" +version = "1.6.0" dependencies = [ "anyhow", "arc-swap", @@ -6564,7 +6564,7 @@ dependencies = [ [[package]] name = "nym-socks5-client" -version = "1.1.48" +version = "1.1.49" dependencies = [ "bs58", "clap", @@ -7169,7 +7169,7 @@ dependencies = [ [[package]] name = "nymvisor" -version = "0.1.13" +version = "0.1.14" dependencies = [ "anyhow", "bytes", diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index 2b9b59bf4c..a3dfdc0661 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-client" -version = "1.1.48" +version = "1.1.49" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] description = "Implementation of the Nym Client" edition = "2021" diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index bf40164e8d..934c0b6db9 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-socks5-client" -version = "1.1.48" +version = "1.1.49" authors = ["Dave Hrycyszyn "] description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address" edition = "2021" diff --git a/explorer-api/Cargo.toml b/explorer-api/Cargo.toml index 42f4af8204..585dfd47bf 100644 --- a/explorer-api/Cargo.toml +++ b/explorer-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "explorer-api" -version = "1.1.46" +version = "1.1.47" edition = "2021" license.workspace = true diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index d3fb90b726..45eacae059 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-api" license = "GPL-3.0" -version = "1.1.50" +version = "1.1.51" authors.workspace = true edition = "2021" rust-version.workspace = true diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 35bf882631..6c65b81261 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node" -version = "1.5.0" +version = "1.6.0" authors.workspace = true repository.workspace = true homepage.workspace = true diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index 8ef8ae9c61..bbc7080819 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-network-requester" license = "GPL-3.0" -version = "1.1.49" +version = "1.1.50" authors.workspace = true edition.workspace = true rust-version = "1.70" diff --git a/tools/nym-cli/Cargo.toml b/tools/nym-cli/Cargo.toml index c8a946ea2e..9163feeb0b 100644 --- a/tools/nym-cli/Cargo.toml +++ b/tools/nym-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-cli" -version = "1.1.48" +version = "1.1.49" authors.workspace = true edition = "2021" license.workspace = true diff --git a/tools/nymvisor/Cargo.toml b/tools/nymvisor/Cargo.toml index b73d85012f..57b4de306d 100644 --- a/tools/nymvisor/Cargo.toml +++ b/tools/nymvisor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nymvisor" -version = "0.1.13" +version = "0.1.14" authors.workspace = true repository.workspace = true homepage.workspace = true From d4d576f3639cbc05aca95b5265eaa5b548d5dd79 Mon Sep 17 00:00:00 2001 From: helicopter-1 Date: Thu, 20 Feb 2025 21:28:47 +0100 Subject: [PATCH 003/133] Fix typos in CHANGELOG.md --- CHANGELOG.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 293f70002b..cf0aa8fc75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,7 +48,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - Downgrade harmless log message from info to debug ([#5403]) - Redirect from mixnode page to nodes page ([#5397]) - chore :update version of chain watcher and validator rewarder ([#5394]) -- bugfix: correctly handle ingore epoch roles flag ([#5390]) +- bugfix: correctly handle ignore epoch roles flag ([#5390]) - bugfix: terminate mixnet socket listener on shutdown ([#5389]) - feat: make client ignore dual mode nodes by default ([#5388]) - Handle ecash network errors differently ([#5378]) @@ -69,7 +69,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - Use expect in geodata test to give error message on failure ([#5314]) - feature: periodically remove stale gateway messages ([#5312]) - build(deps): bump the patch-updates group across 1 directory with 35 updates ([#5310]) -- Add dependabot assignes for the root cargo ecosystem ([#5297]) +- Add dependabot assigns for the root cargo ecosystem ([#5297]) - Move tun constants to network defaults ([#5286]) - Include IPINFO_API_TOKEN in nightly CI ([#5285]) - Nyx Chain Watcher ([#5274]) @@ -122,7 +122,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ## [2025.1-reeses] (2025-01-15) -- Feture/legacy alert ([#5346]) +- Feature, Future/legacy alert ([#5346]) - chore: readjusted --mode behaviour to fix the regression ([#5331]) - chore: apply 1.84 linter suggestions ([#5330]) - bugfix: make sure refresh data key matches bond info ([#5329]) @@ -202,7 +202,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ## [2024.14-crunch-patched] (2024-12-17) -- Fixes an issue to allow previously registred clients to connect to latest nym-nodes +- Fixes an issue to allow previously registered clients to connect to latest nym-nodes - Fixes compatibility issues between nym-nodes and older clients ## [2024.14-crunch] (2024-12-11) @@ -210,7 +210,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - Merge/release/2024.14-crunch ([#5242]) - bugfix: added explicit openapi servers to account for route prefixes ([#5237]) - Further config score adjustments ([#5225]) -- feature: remve any filtering on node semver ([#5224]) +- feature: remove any filtering on node semver ([#5224]) - Backport #5218 ([#5220]) - Derive serialize for UserAgent (#5210) ([#5217]) - dont consider legacy nodes for rewarded set selection ([#5215]) @@ -389,7 +389,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - bugfix/feature: added NymApiClient method to get all skimmed nodes ([#5062]) - Merge1/release/2024.13 magura ([#5061]) - added hacky routes to return nymnodes alongside legacy nodes ([#5051]) -- bugfix: mark migrated gateways as rewarded in the previous epoch in case theyre in the rewarded set ([#5049]) +- bugfix: mark migrated gateways as rewarded in the previous epoch in case they're, their, there in the rewarded set ([#5049]) - bugfix: adjust runtime storage migration ([#5047]) - bugfix: supersede 'cb13be27f8f61d9ae74d924e85d2e6787895eb14' by using… ([#5046]) - bugfix: restore default http port for nym-api ([#5045]) @@ -450,7 +450,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - Fix broken build after merge ([#4937]) - bugfix: correctly paginate through 'search_tx' endpoint ([#4936]) - Add more conversions for responses of authenticator messages ([#4929]) -- Directory Sevices v2.1 ([#4903]) +- Directory Services, Devices v2.1 ([#4903]) - Migrate Legacy Node (Frontend) ([#4826]) - Fix critical issues SI84 and SI85 from Cure53 ([#4758]) @@ -834,7 +834,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - Remove stale peers ([#4640]) - Add generic wg private network routing ([#4636]) - Feature/new node endpoints ([#4635]) -- standarised ContractBuildInformation and added it to all contracts ([#4631]) +- standardised ContractBuildInformation and added it to all contracts ([#4631]) - validate nym-node public ips on startup ([#4630]) - Bump defguard wg ([#4625]) - Fix cargo warnings ([#4624]) @@ -1455,7 +1455,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - clean-up nym-api startup arguments/flags to use clap 3 and its macro-derived arguments ([#2772]) - renamed all references to validator_api to nym_api - renamed all references to nymd to nyxd ([#2696]) -- all-binaries: standarised argument names (note: old names should still be accepted) ([#2762] +- all-binaries: standardised argument names (note: old names should still be accepted) ([#2762] ### Fixed @@ -1960,7 +1960,7 @@ The release also include some additional work for distributed key generation in - Explorer UI tests missing data-testid [\#903](https://github.com/nymtech/nym/pull/903) ([tommyv1987](https://github.com/tommyv1987)) - Fix up Nym-Wallet README.md [\#899](https://github.com/nymtech/nym/pull/899) ([tommyv1987](https://github.com/tommyv1987)) - Feature/batch delegator rewarding [\#898](https://github.com/nymtech/nym/pull/898) ([jstuczyn](https://github.com/jstuczyn)) -- Bug mapp nodemap [\#897](https://github.com/nymtech/nym/pull/897) ([Aid19801](https://github.com/Aid19801)) +- Bug map nodemap [\#897](https://github.com/nymtech/nym/pull/897) ([Aid19801](https://github.com/Aid19801)) - Bug fix/macos keyboard shortcuts [\#896](https://github.com/nymtech/nym/pull/896) ([fmtabbara](https://github.com/fmtabbara)) - Add a Mobile Nav to the Network Explorer [\#895](https://github.com/nymtech/nym/pull/895) ([Aid19801](https://github.com/Aid19801)) - Only use ts-rs in tests [\#894](https://github.com/nymtech/nym/pull/894) ([durch](https://github.com/durch)) From 26f97d3c34a74bec0c0218ea91fd2fb466d9ed73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 24 Feb 2025 10:59:06 +0000 Subject: [PATCH 004/133] dont query for ecash apis unless necessary (#5508) --- common/bandwidth-controller/src/lib.rs | 11 ++- common/bandwidth-controller/src/utils.rs | 85 ++++++++++++++++++------ nym-wallet/Cargo.lock | 30 ++++++--- 3 files changed, 90 insertions(+), 36 deletions(-) diff --git a/common/bandwidth-controller/src/lib.rs b/common/bandwidth-controller/src/lib.rs index a9b15db1fb..4d4d4d6952 100644 --- a/common/bandwidth-controller/src/lib.rs +++ b/common/bandwidth-controller/src/lib.rs @@ -105,26 +105,24 @@ impl BandwidthController { async fn get_aggregate_verification_key( &self, epoch_id: EpochId, - apis: &mut ApiClientsWrapper, + ecash_apis: &mut ApiClientsWrapper<'_, C>, ) -> Result where C: DkgQueryClient + Sync + Send, ::StorageError: Send + Sync + 'static, { - let ecash_apis = apis.get_or_init(epoch_id, &self.client).await?; get_aggregate_verification_key(&self.storage, epoch_id, ecash_apis).await } async fn get_coin_index_signatures( &self, epoch_id: EpochId, - apis: &mut ApiClientsWrapper, + ecash_apis: &mut ApiClientsWrapper<'_, C>, ) -> Result, BandwidthControllerError> where C: DkgQueryClient + Sync + Send, ::StorageError: Send + Sync + 'static, { - let ecash_apis = apis.get_or_init(epoch_id, &self.client).await?; get_coin_index_signatures(&self.storage, epoch_id, ecash_apis).await } @@ -132,13 +130,12 @@ impl BandwidthController { &self, epoch_id: EpochId, expiration_date: Date, - apis: &mut ApiClientsWrapper, + ecash_apis: &mut ApiClientsWrapper<'_, C>, ) -> Result, BandwidthControllerError> where C: DkgQueryClient + Sync + Send, ::StorageError: Send + Sync + 'static, { - let ecash_apis = apis.get_or_init(epoch_id, &self.client).await?; get_expiration_date_signatures(&self.storage, epoch_id, expiration_date, ecash_apis).await } @@ -154,7 +151,7 @@ impl BandwidthController { { let epoch_id = retrieved_ticketbook.ticketbook.epoch_id(); let expiration_date = retrieved_ticketbook.ticketbook.expiration_date(); - let mut api_clients = Default::default(); + let mut api_clients = ApiClientsWrapper::new(&self.client, epoch_id); let verification_key = self .get_aggregate_verification_key(epoch_id, &mut api_clients) diff --git a/common/bandwidth-controller/src/utils.rs b/common/bandwidth-controller/src/utils.rs index 9c15f5924b..2016849eb0 100644 --- a/common/bandwidth-controller/src/utils.rs +++ b/common/bandwidth-controller/src/utils.rs @@ -21,30 +21,67 @@ use rand::thread_rng; use std::fmt::Display; use std::future::Future; -// it really doesn't need the RwLock because it's never moved across tasks, -// but we need all the Send/Sync action -#[derive(Default)] -pub(crate) struct ApiClientsWrapper(Option>); - -impl ApiClientsWrapper { - pub(crate) async fn get_or_init( +pub(crate) trait EcashClientsProvider { + async fn try_get_ecash_clients( &mut self, + ) -> Result, BandwidthControllerError>; +} + +impl EcashClientsProvider for Vec { + async fn try_get_ecash_clients( + &mut self, + ) -> Result, BandwidthControllerError> { + Ok(self.clone()) + } +} + +impl EcashClientsProvider for &mut ApiClientsWrapper<'_, C> +where + C: DkgQueryClient + Sync + Send, +{ + async fn try_get_ecash_clients( + &mut self, + ) -> Result, BandwidthControllerError> { + self.clients().await + } +} + +pub(crate) enum ApiClientsWrapper<'a, C> { + Uninitialised { + query_client: &'a C, epoch_id: EpochId, - dkg_client: &C, - ) -> Result, BandwidthControllerError> + }, + Cached { + clients: Vec, + }, +} + +impl<'a, C> ApiClientsWrapper<'a, C> { + pub(crate) fn new(query_client: &'a C, epoch_id: EpochId) -> Self { + ApiClientsWrapper::Uninitialised { + query_client, + epoch_id, + } + } + + async fn clients(&mut self) -> Result, BandwidthControllerError> where C: DkgQueryClient + Sync + Send, { - if let Some(cached) = &self.0 { - return Ok(cached.clone()); + match self { + ApiClientsWrapper::Uninitialised { + query_client, + epoch_id, + } => { + let clients = all_ecash_api_clients(*query_client, *epoch_id).await?; + *self = ApiClientsWrapper::Cached { + clients: clients.clone(), + }; + + Ok(clients) + } + ApiClientsWrapper::Cached { clients } => Ok(clients.clone()), } - - let clients = all_ecash_api_clients(dkg_client, epoch_id).await?; - - // technically we don't have to be cloning all the clients here, but it's way simpler than - // dealing with locking and whatnot given the performance penalty is negligible - self.0 = Some(clients.clone()); - Ok(clients) } } @@ -76,7 +113,7 @@ where pub(crate) async fn get_aggregate_verification_key( storage: &St, epoch_id: EpochId, - ecash_apis: Vec, + mut ecash_apis: impl EcashClientsProvider, ) -> Result where St: Storage, @@ -90,6 +127,8 @@ where return Ok(stored); }; + let ecash_apis = ecash_apis.try_get_ecash_clients().await?; + let master_vk = query_random_apis_until_success( ecash_apis, |api| async move { api.api_client.master_verification_key(Some(epoch_id)).await }, @@ -115,7 +154,7 @@ where pub(crate) async fn get_coin_index_signatures( storage: &St, epoch_id: EpochId, - ecash_apis: Vec, + mut ecash_apis: impl EcashClientsProvider, ) -> Result, BandwidthControllerError> where St: Storage, @@ -129,6 +168,8 @@ where return Ok(stored); }; + let ecash_apis = ecash_apis.try_get_ecash_clients().await?; + let index_sigs = query_random_apis_until_success( ecash_apis, |api| async move { @@ -159,7 +200,7 @@ pub(crate) async fn get_expiration_date_signatures( storage: &St, epoch_id: EpochId, expiration_date: Date, - ecash_apis: Vec, + mut ecash_apis: impl EcashClientsProvider, ) -> Result, BandwidthControllerError> where St: Storage, @@ -173,6 +214,8 @@ where return Ok(stored); }; + let ecash_apis = ecash_apis.try_get_ecash_clients().await?; + let expiration_sigs = query_random_apis_until_success( ecash_apis, |api| async move { diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 588213bd09..e4e7d77ea2 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -165,6 +165,19 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" +[[package]] +name = "async-compression" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" +dependencies = [ + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-trait" version = "0.1.86" @@ -560,9 +573,9 @@ dependencies = [ [[package]] name = "celes" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39b9a21273925d7cc9e8a9a5f068122341336813c607014f5ef64f82b6acba58" +checksum = "54441489dce7026efc8f01d1aa996c23fa39dd615a953d0e934433a42f61dd30" dependencies = [ "serde", ] @@ -724,13 +737,12 @@ dependencies = [ [[package]] name = "colored" -version = "2.0.4" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ - "is-terminal", "lazy_static", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -3715,7 +3727,7 @@ dependencies = [ "base64 0.22.1", "bip32", "bip39", - "colored 2.0.4", + "colored 2.2.0", "cosmrs 0.21.1", "cosmwasm-std", "cw-controllers", @@ -3823,7 +3835,7 @@ dependencies = [ "base64 0.13.1", "bip39", "cfg-if", - "colored 2.0.4", + "colored 2.2.0", "cosmrs 0.21.1", "cosmwasm-std", "dirs 4.0.0", @@ -4758,6 +4770,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" dependencies = [ + "async-compression", "base64 0.22.1", "bytes", "encoding_rs", @@ -4790,6 +4803,7 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-rustls 0.25.0", + "tokio-util", "tower-service", "url", "wasm-bindgen", From 8f5457e698644e9b1fa66a5cc2d408ac9ce7f9ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 26 Feb 2025 09:47:57 +0000 Subject: [PATCH 005/133] feature: allow nym-nodes to understand future version of sphinx packets (#5496) (#5518) * use updated sphinx crate * updated outfox usage of keygen in tests * use x25519 in outfox * remove redundant constructor * adjusted key convertion traits --- Cargo.lock | 10 +- Cargo.toml | 2 +- common/crypto/Cargo.toml | 5 +- .../crypto/src/asymmetric/encryption/mod.rs | 118 +---- common/nymsphinx/Cargo.toml | 2 - common/nymsphinx/addressing/Cargo.toml | 2 +- .../anonymous-replies/src/requests.rs | 2 +- common/nymsphinx/framing/src/codec.rs | 23 +- common/nymsphinx/framing/src/processing.rs | 105 +++- common/nymsphinx/params/Cargo.toml | 4 +- common/nymsphinx/types/src/lib.rs | 24 +- common/topology/Cargo.toml | 2 +- common/topology/src/node.rs | 2 +- contracts/Cargo.lock | 487 ++++++------------ nym-node/nym-node-metrics/src/mixnet.rs | 33 +- .../src/prometheus_wrapper.rs | 36 +- .../handler/global_prometheus_updater/mod.rs | 9 + nym-node/src/node/mixnet/handler.rs | 21 +- nym-node/src/node/mixnet/shared/mod.rs | 39 +- nym-node/src/node/mod.rs | 2 +- nym-outfox/Cargo.toml | 2 +- nym-outfox/src/format.rs | 48 +- nym-outfox/src/packet.rs | 19 +- nym-outfox/tests/unittests.rs | 36 +- nym-wallet/Cargo.lock | 212 ++------ .../src/generic_scheme/coconut/types.rs | 12 - 26 files changed, 489 insertions(+), 768 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 292aaa93d7..0bac1e45d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6454,7 +6454,6 @@ dependencies = [ "chacha20", "chacha20poly1305", "criterion", - "curve25519-dalek 4.1.3", "fastrand 2.1.1", "getrandom 0.2.15", "log", @@ -6462,6 +6461,7 @@ dependencies = [ "rayon", "sphinx-packet", "thiserror 2.0.11", + "x25519-dalek", "zeroize", ] @@ -7999,7 +7999,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.13.0", "proc-macro2", "quote", "syn 2.0.90", @@ -9426,9 +9426,9 @@ dependencies = [ [[package]] name = "sphinx-packet" -version = "0.1.1" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dabeca95bf5fd0563d6be7ebcb1c6a9fcb135746a0ba9050c47dc68c8607e595" +checksum = "535f2c430778bf59c22249fcc1ed6d384129eb2f0f694706015d636c688f9ac6" dependencies = [ "aes", "arrayref", @@ -9442,11 +9442,11 @@ dependencies = [ "hkdf", "hmac", "lioness", - "log", "rand 0.8.5", "rand_distr", "sha2 0.10.8", "subtle 2.5.0", + "x25519-dalek", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d6b29494fb..a750d7dbc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -322,7 +322,7 @@ serde_with = "3.9.0" serde_yaml = "0.9.25" sha2 = "0.10.8" si-scale = "0.2.3" -sphinx-packet = "0.1.1" +sphinx-packet = "0.3.1" sqlx = "0.7.4" strum = "0.26" strum_macros = "0.26" diff --git a/common/crypto/Cargo.toml b/common/crypto/Cargo.toml index e840713541..ee8819585e 100644 --- a/common/crypto/Cargo.toml +++ b/common/crypto/Cargo.toml @@ -37,11 +37,10 @@ nym-pemstore = { path = "../../common/pemstore", version = "0.3.0" } rand_chacha = { workspace = true } [features] -default = ["sphinx"] +default = [] aead = ["dep:aead", "aead/std", "aes-gcm-siv", "generic-array"] serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"] asymmetric = ["x25519-dalek", "ed25519-dalek", "zeroize"] hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2"] stream_cipher = ["aes", "ctr", "cipher", "generic-array"] -sphinx = ["nym-sphinx-types/sphinx"] -outfox = ["nym-sphinx-types/outfox"] +sphinx = ["nym-sphinx-types/sphinx"] \ No newline at end of file diff --git a/common/crypto/src/asymmetric/encryption/mod.rs b/common/crypto/src/asymmetric/encryption/mod.rs index 7d7b988fc8..6bdeb284d4 100644 --- a/common/crypto/src/asymmetric/encryption/mod.rs +++ b/common/crypto/src/asymmetric/encryption/mod.rs @@ -202,6 +202,18 @@ impl PemStorableKey for PublicKey { } } +impl From for PublicKey { + fn from(public_key: x25519_dalek::PublicKey) -> Self { + PublicKey(public_key) + } +} + +impl From for x25519_dalek::PublicKey { + fn from(public_key: PublicKey) -> Self { + public_key.0 + } +} + #[derive(Zeroize, ZeroizeOnDrop)] pub struct PrivateKey(x25519_dalek::StaticSecret); @@ -308,109 +320,15 @@ impl PemStorableKey for PrivateKey { } } -// compatibility with sphinx keys: -#[cfg(feature = "sphinx")] -impl From for nym_sphinx_types::PublicKey { - fn from(key: PublicKey) -> Self { - nym_sphinx_types::PublicKey::from(key.to_bytes()) +impl From for PrivateKey { + fn from(secret: x25519_dalek::StaticSecret) -> Self { + PrivateKey(secret) } } -#[cfg(feature = "sphinx")] -impl<'a> From<&'a PublicKey> for nym_sphinx_types::PublicKey { - fn from(key: &'a PublicKey) -> Self { - nym_sphinx_types::PublicKey::from((*key).to_bytes()) - } -} - -#[cfg(feature = "sphinx")] -impl From for PublicKey { - fn from(pub_key: nym_sphinx_types::PublicKey) -> Self { - Self(x25519_dalek::PublicKey::from(*pub_key.as_bytes())) - } -} - -#[cfg(feature = "sphinx")] -impl From for nym_sphinx_types::PrivateKey { - fn from(key: PrivateKey) -> Self { - nym_sphinx_types::PrivateKey::from(key.to_bytes()) - } -} - -#[cfg(feature = "sphinx")] -impl<'a> From<&'a PrivateKey> for nym_sphinx_types::PrivateKey { - fn from(key: &'a PrivateKey) -> Self { - nym_sphinx_types::PrivateKey::from(key.to_bytes()) - } -} - -#[cfg(feature = "sphinx")] -impl From for PrivateKey { - fn from(private_key: nym_sphinx_types::PrivateKey) -> Self { - let private_key_bytes = private_key.to_bytes(); - assert_eq!(private_key_bytes.len(), PRIVATE_KEY_SIZE); - Self::from_bytes(&private_key_bytes).unwrap() - } -} - -#[cfg(test)] -mod sphinx_key_conversion { - use super::*; - use rand_chacha::rand_core::SeedableRng; - use rand_chacha::ChaCha20Rng; - - pub(super) fn test_rng() -> ChaCha20Rng { - let dummy_seed = [42u8; 32]; - ChaCha20Rng::from_seed(dummy_seed) - } - - const NUM_ITERATIONS: usize = 100; - - #[test] - fn works_for_forward_conversion() { - let mut rng = test_rng(); - - for _ in 0..NUM_ITERATIONS { - let keys = KeyPair::new(&mut rng); - let private = &keys.private_key; - let public = &keys.public_key; - - let dummy_remote = KeyPair::new(&mut rng); - let dh1 = private.diffie_hellman(&dummy_remote.public_key); - - let public_bytes = public.to_bytes(); - - let sphinx_private: nym_sphinx_types::PrivateKey = private.into(); - let recovered_private = PrivateKey::from(sphinx_private); - - let dh2 = recovered_private.diffie_hellman(&dummy_remote.public_key); - - let sphinx_public: nym_sphinx_types::PublicKey = public.into(); - let recovered_public = PublicKey::from(sphinx_public); - assert_eq!(public_bytes, recovered_public.to_bytes()); - - // even though the byte representation of the private key changed, the resultant DH is the same - // which is what matters - assert_eq!(dh1, dh2); - } - } - - #[test] - fn works_for_backward_conversion() { - for _ in 0..NUM_ITERATIONS { - let (sphinx_private, sphinx_public) = nym_sphinx_types::crypto::keygen(); - - let private_bytes = sphinx_private.to_bytes(); - let public_bytes = sphinx_public.as_bytes(); - - let private: PrivateKey = sphinx_private.into(); - let recovered_sphinx_private: nym_sphinx_types::PrivateKey = private.into(); - - let public: PublicKey = sphinx_public.into(); - let recovered_sphinx_public: nym_sphinx_types::PublicKey = public.into(); - assert_eq!(private_bytes, recovered_sphinx_private.to_bytes()); - assert_eq!(public_bytes, recovered_sphinx_public.as_bytes()); - } +impl AsRef for PrivateKey { + fn as_ref(&self) -> &x25519_dalek::StaticSecret { + &self.0 } } diff --git a/common/nymsphinx/Cargo.toml b/common/nymsphinx/Cargo.toml index 13ad8a8210..a82c9a4272 100644 --- a/common/nymsphinx/Cargo.toml +++ b/common/nymsphinx/Cargo.toml @@ -48,12 +48,10 @@ features = ["sync"] [features] default = ["sphinx"] sphinx = [ - "nym-crypto/sphinx", "nym-sphinx-params/sphinx", "nym-sphinx-types/sphinx", ] outfox = [ - "nym-crypto/outfox", "nym-sphinx-params/outfox", "nym-sphinx-types/outfox", ] diff --git a/common/nymsphinx/addressing/Cargo.toml b/common/nymsphinx/addressing/Cargo.toml index 4131dc751f..03ec4d94af 100644 --- a/common/nymsphinx/addressing/Cargo.toml +++ b/common/nymsphinx/addressing/Cargo.toml @@ -8,7 +8,7 @@ license = { workspace = true } repository = { workspace = true } [dependencies] -nym-crypto = { path = "../../crypto", features = ["asymmetric"] } # all addresses are expressed in terms on their crypto keys +nym-crypto = { path = "../../crypto", features = ["asymmetric", "sphinx"] } # all addresses are expressed in terms on their crypto keys nym-sphinx-types = { path = "../types", features = ["sphinx"] } # we need to be able to refer to some types defined inside sphinx crate serde = { workspace = true } # implementing serialization/deserialization for some types, like `Recipient` thiserror = { workspace = true } diff --git a/common/nymsphinx/anonymous-replies/src/requests.rs b/common/nymsphinx/anonymous-replies/src/requests.rs index 3945753495..7503f141e2 100644 --- a/common/nymsphinx/anonymous-replies/src/requests.rs +++ b/common/nymsphinx/anonymous-replies/src/requests.rs @@ -559,7 +559,7 @@ mod tests { let mut address_bytes = [0; NODE_ADDRESS_LENGTH]; rng.fill_bytes(&mut address_bytes); - let dummy_private = PrivateKey::new_with_rng(rng); + let dummy_private = PrivateKey::random_from_rng(rng); let pub_key = (&dummy_private).into(); Node { address: NodeAddressBytes::from_bytes(address_bytes), diff --git a/common/nymsphinx/framing/src/codec.rs b/common/nymsphinx/framing/src/codec.rs index 2abc29f91e..01d805c9e8 100644 --- a/common/nymsphinx/framing/src/codec.rs +++ b/common/nymsphinx/framing/src/codec.rs @@ -130,28 +130,33 @@ impl Decoder for NymCodec { mod packet_encoding { use super::*; use nym_sphinx_types::{ - crypto, Delay as SphinxDelay, Destination, DestinationAddressBytes, Node, NodeAddressBytes, - DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, NODE_ADDRESS_LENGTH, + Delay as SphinxDelay, Destination, DestinationAddressBytes, Node, NodeAddressBytes, + PrivateKey, DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, NODE_ADDRESS_LENGTH, }; + fn random_pubkey() -> nym_sphinx_types::PublicKey { + let private_key = PrivateKey::random(); + (&private_key).into() + } + fn make_valid_outfox_packet(size: PacketSize) -> NymPacket { - let (_, node1_pk) = crypto::keygen(); + let node1_pk = random_pubkey(); let node1 = Node::new( NodeAddressBytes::from_bytes([5u8; NODE_ADDRESS_LENGTH]), node1_pk, ); - let (_, node2_pk) = crypto::keygen(); + let node2_pk = random_pubkey(); let node2 = Node::new( NodeAddressBytes::from_bytes([4u8; NODE_ADDRESS_LENGTH]), node2_pk, ); - let (_, node3_pk) = crypto::keygen(); + let node3_pk = random_pubkey(); let node3 = Node::new( NodeAddressBytes::from_bytes([2u8; NODE_ADDRESS_LENGTH]), node3_pk, ); - let (_, node4_pk) = crypto::keygen(); + let node4_pk = random_pubkey(); let node4 = Node::new( NodeAddressBytes::from_bytes([2u8; NODE_ADDRESS_LENGTH]), node4_pk, @@ -170,17 +175,17 @@ mod packet_encoding { } fn make_valid_sphinx_packet(size: PacketSize) -> NymPacket { - let (_, node1_pk) = crypto::keygen(); + let node1_pk = random_pubkey(); let node1 = Node::new( NodeAddressBytes::from_bytes([5u8; NODE_ADDRESS_LENGTH]), node1_pk, ); - let (_, node2_pk) = crypto::keygen(); + let node2_pk = random_pubkey(); let node2 = Node::new( NodeAddressBytes::from_bytes([4u8; NODE_ADDRESS_LENGTH]), node2_pk, ); - let (_, node3_pk) = crypto::keygen(); + let node3_pk = random_pubkey(); let node3 = Node::new( NodeAddressBytes::from_bytes([2u8; NODE_ADDRESS_LENGTH]), node3_pk, diff --git a/common/nymsphinx/framing/src/processing.rs b/common/nymsphinx/framing/src/processing.rs index f9dd02dcfb..d00d4d797c 100644 --- a/common/nymsphinx/framing/src/processing.rs +++ b/common/nymsphinx/framing/src/processing.rs @@ -4,8 +4,10 @@ use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressE use nym_sphinx_params::{PacketSize, PacketType}; use nym_sphinx_types::{ Delay as SphinxDelay, DestinationAddressBytes, NodeAddressBytes, NymPacket, NymPacketError, - NymProcessedPacket, OutfoxError, PrivateKey, ProcessedPacket, SphinxError, + NymProcessedPacket, OutfoxError, PrivateKey, ProcessedPacketData, SphinxError, + Version as SphinxPacketVersion, }; +use std::fmt::Display; use thiserror::Error; use crate::packet::FramedNymPacket; @@ -13,12 +15,38 @@ use nym_metrics::nanos; use nym_sphinx_forwarding::packet::MixPacket; #[derive(Debug)] -pub enum MixProcessingResult { +pub enum MixProcessingResultData { /// Contains unwrapped data that should first get delayed before being sent to next hop. - ForwardHop(MixPacket, Option), + ForwardHop { + packet: MixPacket, + delay: Option, + }, /// Contains all data extracted out of the final hop packet that could be forwarded to the destination. - FinalHop(ProcessedFinalHop), + FinalHop { final_hop_data: ProcessedFinalHop }, +} + +#[derive(Debug, Copy, Clone)] +pub enum MixPacketVersion { + Outfox, + Sphinx(SphinxPacketVersion), +} + +impl Display for MixPacketVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + match self { + MixPacketVersion::Outfox => "outfox".fmt(f), + MixPacketVersion::Sphinx(sphinx_version) => { + write!(f, "sphinx-{}", sphinx_version.value()) + } + } + } +} + +#[derive(Debug)] +pub struct MixProcessingResult { + pub packet_version: MixPacketVersion, + pub processing_data: MixProcessingResultData, } type ForwardAck = MixPacket; @@ -107,37 +135,63 @@ fn perform_final_processing( ) -> Result { match packet { NymProcessedPacket::Sphinx(packet) => { - match packet { - ProcessedPacket::ForwardHop(packet, address, delay) => { - process_forward_hop(NymPacket::Sphinx(*packet), address, delay, packet_type) - } + let processing_data = match packet.data { + ProcessedPacketData::ForwardHop { + next_hop_packet, + next_hop_address, + delay, + } => process_forward_hop( + NymPacket::Sphinx(next_hop_packet), + next_hop_address, + delay, + packet_type, + ), // right now there's no use for the surb_id included in the header - probably it should get removed from the // sphinx all together? - ProcessedPacket::FinalHop(destination, _, payload) => process_final_hop( + ProcessedPacketData::FinalHop { + destination, + identifier: _, + payload, + } => process_final_hop( destination, payload.recover_plaintext()?, packet_size, packet_type, ), - } + }?; + + Ok(MixProcessingResult { + packet_version: MixPacketVersion::Sphinx(packet.version), + processing_data, + }) } NymProcessedPacket::Outfox(packet) => { let next_address = *packet.next_address(); let packet = packet.into_packet(); if packet.is_final_hop() { - process_final_hop( + let processing_data = process_final_hop( DestinationAddressBytes::from_bytes(next_address), packet.recover_plaintext()?.to_vec(), packet_size, packet_type, - ) + )?; + Ok(MixProcessingResult { + packet_version: MixPacketVersion::Outfox, + processing_data, + }) } else { - let mix_packet = MixPacket::new( + let packet = MixPacket::new( NymNodeRoutingAddress::try_from_bytes(&next_address)?, NymPacket::Outfox(packet), PacketType::Outfox, ); - Ok(MixProcessingResult::ForwardHop(mix_packet, None)) + Ok(MixProcessingResult { + packet_version: MixPacketVersion::Outfox, + processing_data: MixProcessingResultData::ForwardHop { + packet, + delay: None, + }, + }) } } } @@ -148,14 +202,16 @@ fn process_final_hop( payload: Vec, packet_size: PacketSize, packet_type: PacketType, -) -> Result { +) -> Result { let (forward_ack, message) = split_into_ack_and_message(payload, packet_size, packet_type)?; - Ok(MixProcessingResult::FinalHop(ProcessedFinalHop { - destination, - forward_ack, - message, - })) + Ok(MixProcessingResultData::FinalHop { + final_hop_data: ProcessedFinalHop { + destination, + forward_ack, + message, + }, + }) } fn split_into_ack_and_message( @@ -211,11 +267,14 @@ fn process_forward_hop( forward_address: NodeAddressBytes, delay: SphinxDelay, packet_type: PacketType, -) -> Result { +) -> Result { let next_hop_address = NymNodeRoutingAddress::try_from(forward_address)?; - let mix_packet = MixPacket::new(next_hop_address, packet, packet_type); - Ok(MixProcessingResult::ForwardHop(mix_packet, Some(delay))) + let packet = MixPacket::new(next_hop_address, packet, packet_type); + Ok(MixProcessingResultData::ForwardHop { + packet, + delay: Some(delay), + }) } // TODO: what more could we realistically test here? diff --git a/common/nymsphinx/params/Cargo.toml b/common/nymsphinx/params/Cargo.toml index 9c360942b0..8db26e7b1d 100644 --- a/common/nymsphinx/params/Cargo.toml +++ b/common/nymsphinx/params/Cargo.toml @@ -16,5 +16,5 @@ nym-sphinx-types = { path = "../types" } [features] default = ["sphinx"] -sphinx = ["nym-crypto/sphinx", "nym-sphinx-types/outfox"] -outfox = ["nym-crypto/outfox", "nym-sphinx-types/outfox"] +sphinx = ["nym-sphinx-types/outfox"] +outfox = ["nym-sphinx-types/outfox"] diff --git a/common/nymsphinx/types/src/lib.rs b/common/nymsphinx/types/src/lib.rs index 05232c0483..c9441dbc4d 100644 --- a/common/nymsphinx/types/src/lib.rs +++ b/common/nymsphinx/types/src/lib.rs @@ -1,14 +1,22 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::{array::TryFromSliceError, fmt}; +use thiserror::Error; + +#[cfg(feature = "outfox")] +use nym_outfox::packet::{OutfoxPacket, OutfoxProcessedPacket}; + +#[cfg(feature = "sphinx")] +use sphinx_packet::{SphinxPacket, SphinxPacketBuilder}; + #[cfg(feature = "outfox")] pub use nym_outfox::{ constants::MIN_PACKET_SIZE, constants::MIX_PARAMS_LEN, constants::OUTFOX_PACKET_OVERHEAD, error::OutfoxError, }; // re-exporting types and constants available in sphinx -#[cfg(feature = "outfox")] -use nym_outfox::packet::{OutfoxPacket, OutfoxProcessedPacket}; + #[cfg(feature = "sphinx")] pub use sphinx_packet::{ constants::{ @@ -21,12 +29,10 @@ pub use sphinx_packet::{ payload::{Payload, PAYLOAD_OVERHEAD_SIZE}, route::{Destination, DestinationAddressBytes, Node, NodeAddressBytes, SURBIdentifier}, surb::{SURBMaterial, SURB}, - Error as SphinxError, ProcessedPacket, + version::Version, + version::UPDATED_LEGACY_VERSION, + Error as SphinxError, ProcessedPacket, ProcessedPacketData, }; -#[cfg(feature = "sphinx")] -use sphinx_packet::{SphinxPacket, SphinxPacketBuilder}; -use std::{array::TryFromSliceError, fmt}; -use thiserror::Error; #[derive(Error, Debug)] pub enum NymPacketError { @@ -85,8 +91,12 @@ impl NymPacket { destination: &Destination, delays: &[Delay], ) -> Result { + // FIXME: + // for now explicitly use the legacy version until sufficient number of nodes + // understand both variants Ok(NymPacket::Sphinx( SphinxPacketBuilder::new() + .with_version(UPDATED_LEGACY_VERSION) .with_payload_size(size) .build_packet(message, route, destination, delays)?, )) diff --git a/common/topology/Cargo.toml b/common/topology/Cargo.toml index be031ed095..ba34832b04 100644 --- a/common/topology/Cargo.toml +++ b/common/topology/Cargo.toml @@ -27,7 +27,7 @@ wasm-bindgen = { workspace = true, optional = true } ## internal nym-config = { path = "../config" } -nym-crypto = { path = "../crypto", features = ["sphinx", "outfox"] } +nym-crypto = { path = "../crypto" } nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" } nym-sphinx-addressing = { path = "../nymsphinx/addressing" } nym-sphinx-types = { path = "../nymsphinx/types", features = [ diff --git a/common/topology/src/node.rs b/common/topology/src/node.rs index 81ab236f76..47193e6438 100644 --- a/common/topology/src/node.rs +++ b/common/topology/src/node.rs @@ -105,7 +105,7 @@ impl<'a> From<&'a RoutingNode> for SphinxNode { .try_into() .unwrap(); - SphinxNode::new(node_address_bytes, (&node.sphinx_key).into()) + SphinxNode::new(node_address_bytes, node.sphinx_key.into()) } } diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index a5c961c076..7a2e25bfb6 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -2,17 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - [[package]] name = "ahash" version = "0.7.8" @@ -35,21 +24,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.88" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e1496f8fb1fbf272686b8d37f523dab3e4a7443300055e74cdaa449f3114356" - -[[package]] -name = "arrayref" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" - -[[package]] -name = "autocfg" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] name = "base16ct" @@ -81,25 +58,13 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" -[[package]] -name = "blake2" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94cb07b0da6a73955f8fb85d24c466778e70cda767a568229b104f0264089330" -dependencies = [ - "byte-tools", - "crypto-mac", - "digest 0.8.1", - "opaque-debug 0.2.3", -] - [[package]] name = "block-buffer" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -108,7 +73,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -132,12 +97,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "byte-tools" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" - [[package]] name = "byteorder" version = "1.5.0" @@ -146,15 +105,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" [[package]] name = "camino" -version = "1.1.7" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" dependencies = [ "serde", ] @@ -179,7 +138,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 1.0.64", + "thiserror 1.0.69", ] [[package]] @@ -188,26 +147,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "chacha" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddf3c081b5fba1e5615640aae998e0fbd10c24cbd897ee39ed754a77601a4862" -dependencies = [ - "byteorder", - "keystream", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - [[package]] name = "coconut-test" version = "0.1.0" @@ -233,7 +172,7 @@ dependencies = [ "schemars", "serde", "subtle-encoding", - "thiserror 1.0.64", + "thiserror 1.0.69", ] [[package]] @@ -244,9 +183,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const_panic" -version = "0.2.8" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6051f239ecec86fde3410901ab7860d458d160371533842974fc61f96d15879b" +checksum = "2459fc9262a1aa204eb4b5764ad4f189caec88aea9634389c0a25f8be7f6265e" [[package]] name = "convert_case" @@ -265,9 +204,9 @@ checksum = "c533b66e502ecab30fec23d03eb54ab1d3ce120645a00493459f8510b7a9736f" dependencies = [ "digest 0.10.7", "ed25519-zebra", - "k256 0.13.3", + "k256 0.13.4", "rand_core 0.6.4", - "thiserror 1.0.64", + "thiserror 1.0.69", ] [[package]] @@ -289,7 +228,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "thiserror 1.0.64", + "thiserror 1.0.69", ] [[package]] @@ -320,7 +259,7 @@ dependencies = [ "serde", "serde-json-wasm", "sha2 0.10.8", - "thiserror 1.0.64", + "thiserror 1.0.69", ] [[package]] @@ -335,9 +274,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -348,9 +287,9 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" dependencies = [ - "generic-array 0.14.7", + "generic-array", "rand_core 0.6.4", - "subtle 2.4.1", + "subtle", "zeroize", ] @@ -360,9 +299,9 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ - "generic-array 0.14.7", + "generic-array", "rand_core 0.6.4", - "subtle 2.4.1", + "subtle", "zeroize", ] @@ -372,29 +311,10 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array 0.14.7", + "generic-array", "typenum", ] -[[package]] -name = "crypto-mac" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" -dependencies = [ - "generic-array 0.12.4", - "subtle 1.0.0", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - [[package]] name = "curve25519-dalek" version = "3.2.0" @@ -404,24 +324,23 @@ dependencies = [ "byteorder", "digest 0.9.0", "rand_core 0.5.1", - "subtle 2.4.1", + "subtle", "zeroize", ] [[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 2.4.1", + "subtle", "zeroize", ] @@ -448,7 +367,7 @@ dependencies = [ "cw-utils", "schemars", "serde", - "thiserror 1.0.64", + "thiserror 1.0.69", ] [[package]] @@ -467,7 +386,7 @@ dependencies = [ "prost", "schemars", "serde", - "thiserror 1.0.64", + "thiserror 1.0.69", ] [[package]] @@ -493,7 +412,7 @@ dependencies = [ "schemars", "semver", "serde", - "thiserror 1.0.64", + "thiserror 1.0.69", ] [[package]] @@ -508,7 +427,7 @@ dependencies = [ "schemars", "semver", "serde", - "thiserror 1.0.64", + "thiserror 1.0.69", ] [[package]] @@ -538,7 +457,7 @@ dependencies = [ "schemars", "semver", "serde", - "thiserror 1.0.64", + "thiserror 1.0.69", ] [[package]] @@ -553,7 +472,7 @@ dependencies = [ "cw20", "schemars", "serde", - "thiserror 1.0.64", + "thiserror 1.0.69", ] [[package]] @@ -570,7 +489,7 @@ dependencies = [ "cw3", "schemars", "serde", - "thiserror 1.0.64", + "thiserror 1.0.69", ] [[package]] @@ -622,7 +541,7 @@ dependencies = [ "nym-group-contract-common", "schemars", "serde", - "thiserror 1.0.64", + "thiserror 1.0.69", ] [[package]] @@ -665,22 +584,13 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "digest" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" -dependencies = [ - "generic-array 0.12.4", -] - [[package]] name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -692,14 +602,14 @@ dependencies = [ "block-buffer 0.10.4", "const-oid", "crypto-common", - "subtle 2.4.1", + "subtle", ] [[package]] name = "dyn-clone" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +checksum = "feeef44e73baff3a26d371801df019877a9866a8c493d315ab00177843314f35" [[package]] name = "ecdsa" @@ -743,12 +653,12 @@ 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", "rand_core 0.6.4", "serde", "sha2 0.10.8", - "subtle 2.4.1", + "subtle", "zeroize", ] @@ -769,9 +679,9 @@ dependencies = [ [[package]] name = "either" -version = "1.11.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "elliptic-curve" @@ -784,12 +694,12 @@ dependencies = [ "der 0.6.1", "digest 0.10.7", "ff 0.12.1", - "generic-array 0.14.7", + "generic-array", "group 0.12.1", "pkcs8 0.9.0", "rand_core 0.6.4", "sec1 0.3.0", - "subtle 2.4.1", + "subtle", "zeroize", ] @@ -803,20 +713,20 @@ dependencies = [ "crypto-bigint 0.5.5", "digest 0.10.7", "ff 0.13.0", - "generic-array 0.14.7", + "generic-array", "group 0.13.0", "pkcs8 0.10.2", "rand_core 0.6.4", "sec1 0.7.3", - "subtle 2.4.1", + "subtle", "zeroize", ] [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "ff" @@ -825,7 +735,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" dependencies = [ "rand_core 0.6.4", - "subtle 2.4.1", + "subtle", ] [[package]] @@ -835,14 +745,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ "rand_core 0.6.4", - "subtle 2.4.1", + "subtle", ] [[package]] name = "fiat-crypto" -version = "0.2.7" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c007b1ae3abe1cb6f85a16305acd418b7ca6343b953633fee2b76d8f108b830f" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "forward_ref" @@ -850,15 +760,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" -[[package]] -name = "generic-array" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" -dependencies = [ - "typenum", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -872,9 +773,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", @@ -889,7 +790,7 @@ checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" dependencies = [ "ff 0.12.1", "rand_core 0.6.4", - "subtle 2.4.1", + "subtle", ] [[package]] @@ -900,7 +801,7 @@ checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff 0.13.0", "rand_core 0.6.4", - "subtle 2.4.1", + "subtle", ] [[package]] @@ -914,9 +815,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "hex" @@ -924,15 +825,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - [[package]] name = "hmac" version = "0.12.1" @@ -960,21 +852,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", - "hashbrown 0.14.5", -] - -[[package]] -name = "inout" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" -dependencies = [ - "generic-array 0.14.7", + "hashbrown 0.15.2", ] [[package]] @@ -988,9 +871,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "k256" @@ -1006,9 +889,9 @@ dependencies = [ [[package]] name = "k256" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ "cfg-if", "ecdsa 0.16.9", @@ -1018,17 +901,11 @@ dependencies = [ "signature 2.2.0", ] -[[package]] -name = "keystream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28" - [[package]] name = "konst" -version = "0.3.9" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50a0ba6de5f7af397afff922f22c149ff605c766cd3269cf6c1cd5e466dbe3b9" +checksum = "4381b9b00c55f251f2ebe9473aef7c117e96828def1a7cb3bd3f0f903c6894e9" dependencies = [ "const_panic", "konst_kernel", @@ -1038,54 +915,30 @@ dependencies = [ [[package]] name = "konst_kernel" -version = "0.3.9" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be0a455a1719220fd6adf756088e1c69a85bf14b6a9e24537a5cc04f503edb2b" +checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c" dependencies = [ "typewit", ] [[package]] name = "konst_proc_macros" -version = "0.3.0" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e28ab1dc35e09d60c2b8c90d12a9a8d9666c876c10a3739a3196db0103b6043" +checksum = "00af7901ba50898c9e545c24d5c580c96a982298134e8037d8978b6594782c07" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" - -[[package]] -name = "libm" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" - -[[package]] -name = "lioness" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae926706ba42c425c9457121178330d75e273df2e82e28b758faf3de3a9acb9" -dependencies = [ - "arrayref", - "blake2", - "chacha", - "keystream", -] - -[[package]] -name = "log" -version = "0.4.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[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 = "mixnet-vesting-integration-tests" @@ -1109,16 +962,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" -[[package]] -name = "num-traits" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" -dependencies = [ - "autocfg", - "libm", -] - [[package]] name = "num_threads" version = "0.1.7" @@ -1140,7 +983,7 @@ dependencies = [ "nym-coconut-bandwidth-contract-common", "nym-multisig-contract-common", "serde", - "thiserror 1.0.64", + "thiserror 1.0.69", ] [[package]] @@ -1170,7 +1013,7 @@ dependencies = [ "nym-contracts-common", "nym-group-contract-common", "serde", - "thiserror 1.0.64", + "thiserror 1.0.69", ] [[package]] @@ -1240,7 +1083,7 @@ dependencies = [ "semver", "serde", "sylvia", - "thiserror 1.0.64", + "thiserror 1.0.69", ] [[package]] @@ -1289,7 +1132,7 @@ dependencies = [ "rand_chacha", "semver", "serde", - "thiserror 1.0.64", + "thiserror 1.0.69", "time", ] @@ -1348,7 +1191,6 @@ dependencies = [ name = "nym-sphinx-types" version = "0.2.0" dependencies = [ - "sphinx-packet", "thiserror 2.0.11", ] @@ -1370,7 +1212,7 @@ dependencies = [ "rand_chacha", "serde", "serde_json", - "thiserror 1.0.64", + "thiserror 1.0.69", ] [[package]] @@ -1388,15 +1230,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" - -[[package]] -name = "opaque-debug" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] name = "opaque-debug" @@ -1435,12 +1271,6 @@ dependencies = [ "spki 0.7.3", ] -[[package]] -name = "platforms" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7" - [[package]] name = "powerfmt" version = "0.2.0" @@ -1449,9 +1279,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "proc-macro-crate" @@ -1521,9 +1354,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -1564,16 +1397,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "rand_distr" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" -dependencies = [ - "num-traits", - "rand", -] - [[package]] name = "regex" version = "1.11.1" @@ -1621,29 +1444,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ "hmac", - "subtle 2.4.1", + "subtle", ] [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustversion" -version = "1.0.15" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" [[package]] name = "schemars" @@ -1677,9 +1500,9 @@ checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" dependencies = [ "base16ct 0.1.1", "der 0.6.1", - "generic-array 0.14.7", + "generic-array", "pkcs8 0.9.0", - "subtle 2.4.1", + "subtle", "zeroize", ] @@ -1691,9 +1514,9 @@ checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct 0.2.0", "der 0.7.9", - "generic-array 0.14.7", + "generic-array", "pkcs8 0.10.2", - "subtle 2.4.1", + "subtle", "zeroize", ] @@ -1757,9 +1580,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" dependencies = [ "itoa", "memchr", @@ -1788,7 +1611,7 @@ dependencies = [ "cfg-if", "cpufeatures", "digest 0.9.0", - "opaque-debug 0.3.1", + "opaque-debug", ] [[package]] @@ -1822,31 +1645,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "sphinx-packet" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dabeca95bf5fd0563d6be7ebcb1c6a9fcb135746a0ba9050c47dc68c8607e595" -dependencies = [ - "aes", - "arrayref", - "blake2", - "bs58 0.5.1", - "byteorder", - "chacha", - "ctr", - "curve25519-dalek 4.1.2", - "digest 0.10.7", - "hkdf", - "hmac", - "lioness", - "log", - "rand", - "rand_distr", - "sha2 0.10.8", - "subtle 2.4.1", -] - [[package]] name = "spki" version = "0.6.0" @@ -1869,15 +1667,9 @@ dependencies = [ [[package]] name = "subtle" -version = "1.0.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" - -[[package]] -name = "subtle" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "subtle-encoding" @@ -1945,11 +1737,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl 1.0.64", + "thiserror-impl 1.0.69", ] [[package]] @@ -1963,9 +1755,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", @@ -2018,9 +1810,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" dependencies = [ "tinyvec_macros", ] @@ -2033,9 +1825,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" @@ -2050,15 +1842,15 @@ dependencies = [ [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "typewit" -version = "1.9.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fb9ae6a3cafaf0a5d14c2302ca525f9ae8e07a0f0e6949de88d882c37a6e24" +checksum = "cb77c29baba9e4d3a6182d51fa75e3215c7fd1dab8f4ea9d107c716878e55fc0" dependencies = [ "typewit_proc_macros", ] @@ -2071,15 +1863,15 @@ checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" [[package]] name = "unicode-segmentation" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "vergen" @@ -2098,9 +1890,9 @@ dependencies = [ [[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 = "wasi" @@ -2123,17 +1915,38 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ - "curve25519-dalek 4.1.2", + "curve25519-dalek 4.1.3", "rand_core 0.6.4", "serde", "zeroize", ] [[package]] -name = "zeroize" -version = "1.7.0" +name = "zerocopy" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" dependencies = [ "zeroize_derive", ] diff --git a/nym-node/nym-node-metrics/src/mixnet.rs b/nym-node/nym-node-metrics/src/mixnet.rs index 32d84e509b..ee99510c38 100644 --- a/nym-node/nym-node-metrics/src/mixnet.rs +++ b/nym-node/nym-node-metrics/src/mixnet.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use dashmap::DashMap; +use std::fmt::Display; use std::net::{IpAddr, SocketAddr}; use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering}; use time::OffsetDateTime; @@ -52,7 +53,7 @@ impl MixingStats { self.ingress.senders.entry(source).or_default().malformed += 1; } - pub fn ingress_received_forward_packet(&self, source: IpAddr) { + pub fn ingress_received_forward_packet(&self, source: IpAddr, version: PacketKind) { self.ingress .forward_hop_packets_received .fetch_add(1, Ordering::Relaxed); @@ -62,9 +63,10 @@ impl MixingStats { .or_default() .forward_packets .received += 1; + *self.ingress.received_versions.entry(version).or_default() += 1; } - pub fn ingress_received_final_hop_packet(&self, source: IpAddr) { + pub fn ingress_received_final_hop_packet(&self, source: IpAddr, version: PacketKind) { self.ingress .final_hop_packets_received .fetch_add(1, Ordering::Relaxed); @@ -74,6 +76,7 @@ impl MixingStats { .or_default() .final_hop_packets .received += 1; + *self.ingress.received_versions.entry(version).or_default() += 1; } pub fn ingress_excessive_delay_packet(&self) { @@ -196,8 +199,30 @@ pub struct IngressRecipientStats { pub malformed: usize, } +#[derive(Debug, Default, Copy, Clone, Hash, PartialEq, Eq)] +pub enum PacketKind { + #[default] + Unknown, + Outfox, + Sphinx(u16), +} + +impl Display for PacketKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + match self { + PacketKind::Unknown => "unknown".fmt(f), + PacketKind::Outfox => "outfox".fmt(f), + PacketKind::Sphinx(sphinx_version) => { + write!(f, "sphinx-{sphinx_version}") + } + } + } +} + #[derive(Default)] pub struct IngressMixingStats { + received_versions: DashMap, + // forward hop packets (i.e. to mixnode) forward_hop_packets_received: AtomicUsize, @@ -248,6 +273,10 @@ impl IngressMixingStats { &self.senders } + pub fn packet_versions(&self) -> &DashMap { + &self.received_versions + } + pub fn remove_stale_sender(&self, sender: IpAddr) { self.senders.remove(&sender); } diff --git a/nym-node/nym-node-metrics/src/prometheus_wrapper.rs b/nym-node/nym-node-metrics/src/prometheus_wrapper.rs index cfee39ee3e..8fdfb5b488 100644 --- a/nym-node/nym-node-metrics/src/prometheus_wrapper.rs +++ b/nym-node/nym-node-metrics/src/prometheus_wrapper.rs @@ -1,6 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::mixnet::PacketKind; use nym_metrics::{metrics_registry, HistogramTimer, Metric}; use std::sync::LazyLock; use strum::{Display, EnumCount, EnumIter, EnumProperty, IntoEnumIterator}; @@ -28,6 +29,10 @@ const CLIENT_SESSION_DURATION_BUCKETS: &[f64] = &[ pub enum PrometheusMetric { // # MIXNET // ## INGRESS + #[strum(to_string = "mixnet_ingress_packet_version_{kind}")] + #[strum(props(help = "The number of ingress packets received with the particular version"))] + MixnetIngressPacketVersion { kind: PacketKind }, + #[strum(props(help = "The number of ingress forward hop sphinx packets received"))] MixnetIngressForwardPacketsReceived, @@ -178,7 +183,11 @@ impl PrometheusMetric { } fn is_complex(&self) -> bool { - matches!(self, PrometheusMetric::EntryClientSessionsDurations { .. }) + matches!( + self, + PrometheusMetric::EntryClientSessionsDurations { .. } + | PrometheusMetric::MixnetIngressPacketVersion { .. } + ) // match self { // PrometheusMetric::EntryClientSessionsDurations { .. } => true, // _ => false, @@ -190,6 +199,9 @@ impl PrometheusMetric { let help = self.help(); match self { + PrometheusMetric::MixnetIngressPacketVersion { .. } => { + Metric::new_int_gauge(&name, help) + } PrometheusMetric::MixnetIngressForwardPacketsReceived => { Metric::new_int_gauge(&name, help) } @@ -364,7 +376,7 @@ mod tests { // a sanity check for anyone adding new metrics. if this test fails, // make sure any methods on `PrometheusMetric` enum don't need updating // or require custom Display impl - assert_eq!(37, PrometheusMetric::COUNT) + assert_eq!(38, PrometheusMetric::COUNT) } #[test] @@ -385,6 +397,24 @@ mod tests { assert_eq!( "nym_node_entry_client_sessions_durations_vpn", parameterised - ) + ); + + let parameterised = PrometheusMetric::MixnetIngressPacketVersion { + kind: PacketKind::Outfox, + } + .to_string(); + assert_eq!( + "nym_node_mixnet_ingress_packet_version_outfox", + parameterised + ); + + let parameterised = PrometheusMetric::MixnetIngressPacketVersion { + kind: PacketKind::Sphinx(42), + } + .to_string(); + assert_eq!( + "nym_node_mixnet_ingress_packet_version_sphinx-42", + parameterised + ); } } diff --git a/nym-node/src/node/metrics/handler/global_prometheus_updater/mod.rs b/nym-node/src/node/metrics/handler/global_prometheus_updater/mod.rs index 81c61d9db8..16587ead63 100644 --- a/nym-node/src/node/metrics/handler/global_prometheus_updater/mod.rs +++ b/nym-node/src/node/metrics/handler/global_prometheus_updater/mod.rs @@ -43,6 +43,15 @@ impl OnUpdateMetricsHandler for PrometheusGlobalNodeMetricsRegistryUpdater { // # MIXNET // ## INGRESS + for version_entry in self.metrics.mixnet.ingress.packet_versions() { + self.prometheus_wrapper.set( + MixnetIngressPacketVersion { + kind: *version_entry.key(), + }, + *version_entry.value(), + ) + } + self.prometheus_wrapper.set( MixnetIngressForwardPacketsReceived, self.metrics.mixnet.ingress.forward_hop_packets_received() as i64, diff --git a/nym-node/src/node/mixnet/handler.rs b/nym-node/src/node/mixnet/handler.rs index 3c634d7c9c..51be743cd5 100644 --- a/nym-node/src/node/mixnet/handler.rs +++ b/nym-node/src/node/mixnet/handler.rs @@ -8,7 +8,7 @@ use nym_sphinx_forwarding::packet::MixPacket; use nym_sphinx_framing::codec::NymCodec; use nym_sphinx_framing::packet::FramedNymPacket; use nym_sphinx_framing::processing::{ - process_framed_packet, MixProcessingResult, ProcessedFinalHop, + process_framed_packet, MixProcessingResultData, ProcessedFinalHop, }; use nym_sphinx_types::Delay; use std::net::SocketAddr; @@ -44,7 +44,7 @@ impl ConnectionHandler { ConnectionHandler { shared: SharedData { processing_config: shared.processing_config, - sphinx_key: shared.sphinx_key.clone(), + sphinx_keys: shared.sphinx_keys.clone(), mixnet_forwarder: shared.mixnet_forwarder.clone(), final_hop: shared.final_hop.clone(), metrics: shared.metrics.clone(), @@ -135,7 +135,8 @@ impl ConnectionHandler { nanos!("handle_received_nym_packet", { // 1. attempt to unwrap the packet - let unwrapped_packet = process_framed_packet(packet, &self.shared.sphinx_key); + let unwrapped_packet = + process_framed_packet(packet, self.shared.sphinx_keys.private_key().as_ref()); // 2. increment our favourite metrics stats self.shared @@ -144,12 +145,14 @@ impl ConnectionHandler { // 3. forward the packet to the relevant sink (if enabled) match unwrapped_packet { Err(err) => trace!("failed to process received mix packet: {err}"), - Ok(MixProcessingResult::ForwardHop(forward_packet, delay)) => { - self.handle_forward_packet(forward_packet, delay); - } - Ok(MixProcessingResult::FinalHop(final_hop_data)) => { - self.handle_final_hop(final_hop_data).await; - } + Ok(processed_packet) => match processed_packet.processing_data { + MixProcessingResultData::ForwardHop { packet, delay } => { + self.handle_forward_packet(packet, delay); + } + MixProcessingResultData::FinalHop { final_hop_data } => { + self.handle_final_hop(final_hop_data).await; + } + }, } }) } diff --git a/nym-node/src/node/mixnet/shared/mod.rs b/nym-node/src/node/mixnet/shared/mod.rs index 8d9f68f528..0ff26dc0a0 100644 --- a/nym-node/src/node/mixnet/shared/mod.rs +++ b/nym-node/src/node/mixnet/shared/mod.rs @@ -7,9 +7,12 @@ use crate::node::mixnet::SharedFinalHopData; use nym_crypto::asymmetric::x25519; use nym_gateway::node::GatewayStorageError; use nym_mixnet_client::forwarder::{MixForwardingSender, PacketToForward}; +use nym_node_metrics::mixnet::PacketKind; use nym_node_metrics::NymNodeMetrics; use nym_sphinx_forwarding::packet::MixPacket; -use nym_sphinx_framing::processing::{MixProcessingResult, PacketProcessingError}; +use nym_sphinx_framing::processing::{ + MixPacketVersion, MixProcessingResult, MixProcessingResultData, PacketProcessingError, +}; use nym_sphinx_types::DestinationAddressBytes; use nym_task::ShutdownToken; use std::io; @@ -45,8 +48,7 @@ impl ProcessingConfig { // explicitly do NOT derive clone as we want to manually apply relevant suffixes to the task clients pub(crate) struct SharedData { pub(super) processing_config: ProcessingConfig, - // TODO: this type is not `Zeroize` : ( - pub(super) sphinx_key: Arc, + pub(super) sphinx_keys: Arc, // used for FORWARD mix packets and FINAL ack packets pub(super) mixnet_forwarder: MixForwardingSender, @@ -58,10 +60,17 @@ pub(crate) struct SharedData { pub(super) shutdown: ShutdownToken, } +fn convert_to_metrics_version(processed: MixPacketVersion) -> PacketKind { + match processed { + MixPacketVersion::Outfox => PacketKind::Outfox, + MixPacketVersion::Sphinx(sphinx_version) => PacketKind::Sphinx(sphinx_version.value()), + } +} + impl SharedData { pub(crate) fn new( processing_config: ProcessingConfig, - x25519_key: &x25519::PrivateKey, + x25519_keys: Arc, mixnet_forwarder: MixForwardingSender, final_hop: SharedFinalHopData, metrics: NymNodeMetrics, @@ -69,7 +78,7 @@ impl SharedData { ) -> Self { SharedData { processing_config, - sphinx_key: Arc::new(x25519_key.into()), + sphinx_keys: x25519_keys, mixnet_forwarder, final_hop, metrics, @@ -99,10 +108,18 @@ impl SharedData { processing_result: &Result, source: IpAddr, ) { - match processing_result { - Err(_) => self.metrics.mixnet.ingress_malformed_packet(source), - Ok(MixProcessingResult::ForwardHop(_, delay)) => { - self.metrics.mixnet.ingress_received_forward_packet(source); + let Ok(processing_result) = processing_result else { + self.metrics.mixnet.ingress_malformed_packet(source); + return; + }; + + let packet_version = convert_to_metrics_version(processing_result.packet_version); + + match processing_result.processing_data { + MixProcessingResultData::ForwardHop { delay, .. } => { + self.metrics + .mixnet + .ingress_received_forward_packet(source, packet_version); // check if the delay wasn't excessive if let Some(delay) = delay { @@ -111,10 +128,10 @@ impl SharedData { } } } - Ok(MixProcessingResult::FinalHop(_)) => { + MixProcessingResultData::FinalHop { .. } => { self.metrics .mixnet - .ingress_received_final_hop_packet(source); + .ingress_received_final_hop_packet(source, packet_version); } } } diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index 9b695cc94f..b803d772d0 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -993,7 +993,7 @@ impl NymNode { let shared = mixnet::SharedData::new( processing_config, - self.x25519_sphinx_keys.private_key(), + self.x25519_sphinx_keys.clone(), mix_packet_sender.clone(), final_hop_data, self.metrics.clone(), diff --git a/nym-outfox/Cargo.toml b/nym-outfox/Cargo.toml index 343dd35a51..0c3c3e8763 100644 --- a/nym-outfox/Cargo.toml +++ b/nym-outfox/Cargo.toml @@ -14,7 +14,7 @@ rayon = { workspace = true } blake3 = { workspace = true } zeroize = { workspace = true } chacha20 = { workspace = true, features = ["std"] } -curve25519-dalek = { workspace = true } +x25519-dalek = { workspace = true } chacha20poly1305 = { workspace = true } getrandom = { workspace = true, features = ["js"] } thiserror = { workspace = true } diff --git a/nym-outfox/src/format.rs b/nym-outfox/src/format.rs index 9dc9be9e48..413352d630 100644 --- a/nym-outfox/src/format.rs +++ b/nym-outfox/src/format.rs @@ -54,17 +54,6 @@ //! routing data for the layer, and the remaining Header; separately the master key is used to lion encrypt //! the payload. The process is repeated for each layer (from last to first) to construct the full message. -use chacha20poly1305::AeadInPlace; -use chacha20poly1305::ChaCha20Poly1305; -use chacha20poly1305::KeyInit; - -use chacha20poly1305::Tag; -use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE; -use curve25519_dalek::montgomery::MontgomeryPoint; -use curve25519_dalek::scalar::Scalar; - -use std::ops::Range; - use crate::constants::groupelementbytes; use crate::constants::tagbytes; use crate::constants::DEFAULT_HOPS; @@ -75,6 +64,11 @@ use crate::constants::ROUTING_INFORMATION_LENGTH_BY_STAGE; use crate::constants::TAGBYTES; use crate::error::OutfoxError; use crate::lion::*; +use chacha20poly1305::AeadInPlace; +use chacha20poly1305::ChaCha20Poly1305; +use chacha20poly1305::KeyInit; +use chacha20poly1305::Tag; +use std::ops::Range; /// A structure that holds mix packet construction parameters. These incluse the length /// of the routing information at each hop, the number of hops, and the payload length. @@ -218,13 +212,11 @@ impl MixStageParameters { pub fn encode_mix_layer( &self, buffer: &mut [u8], - user_secret_key: &[u8], - node_pub_key: &[u8], + user_secret_key: &x25519_dalek::StaticSecret, + mix_public_key: x25519_dalek::PublicKey, destination: &[u8; 32], - ) -> Result { + ) -> Result { let routing_data = destination; - let mix_public_key = MontgomeryPoint(node_pub_key.try_into()?); - let user_secret_key = Scalar::from_bytes_mod_order(user_secret_key.try_into()?); if buffer.len() != self.incoming_packet_length() { return Err(OutfoxError::LenMismatch { @@ -240,14 +232,14 @@ impl MixStageParameters { }); } - let user_public_key = (ED25519_BASEPOINT_TABLE * &user_secret_key).to_montgomery(); - let shared_key = user_secret_key * mix_public_key; + let user_public_key = x25519_dalek::PublicKey::from(user_secret_key); + let shared_key = user_secret_key.diffie_hellman(&mix_public_key); // Copy rounting data into buffer buffer[self.routing_data_range()].copy_from_slice(routing_data); // Perform the AEAD - let header_aead_key = ChaCha20Poly1305::new_from_slice(&shared_key.0[..])?; + let header_aead_key = ChaCha20Poly1305::new_from_slice(shared_key.as_bytes())?; let nonce = [0u8; 12]; let tag = header_aead_key @@ -258,10 +250,10 @@ impl MixStageParameters { buffer[self.tag_range()].copy_from_slice(&tag[..]); // Copy own public key into buffer - buffer[self.pub_element_range()].copy_from_slice(&user_public_key.0[..]); + buffer[self.pub_element_range()].copy_from_slice(user_public_key.as_bytes()); // Do a round of LION on the payload - lion_transform_encrypt(&mut buffer[self.payload_range()], &shared_key.0)?; + lion_transform_encrypt(&mut buffer[self.payload_range()], shared_key.as_bytes())?; Ok(shared_key) } @@ -269,12 +261,9 @@ impl MixStageParameters { pub fn decode_mix_layer( &self, buffer: &mut [u8], - mix_secret_key: &[u8], + mix_secret_key: &x25519_dalek::StaticSecret, ) -> Result, OutfoxError> { // Check the length of the incoming buffer is correct. - - let mix_secret_key = Scalar::from_bytes_mod_order(mix_secret_key.try_into()?); - if buffer.len() != self.incoming_packet_length() { return Err(OutfoxError::LenMismatch { expected: buffer.len(), @@ -283,11 +272,12 @@ impl MixStageParameters { } // Derive the shared key for this packet - let user_public_key = MontgomeryPoint(buffer[self.pub_element_range()].try_into()?); - let shared_key = mix_secret_key * user_public_key; + let user_public_key_bytes: [u8; 32] = buffer[self.pub_element_range()].try_into()?; + let user_public_key = x25519_dalek::PublicKey::from(user_public_key_bytes); + let shared_key = mix_secret_key.diffie_hellman(&user_public_key); // Compute the AEAD and check the Tag, if wrong return Err - let header_aead_key = ChaCha20Poly1305::new_from_slice(&shared_key.0[..])?; + let header_aead_key = ChaCha20Poly1305::new_from_slice(shared_key.as_bytes())?; let nonce = [0; 12]; let tag_bytes = buffer[self.tag_range()].to_vec(); @@ -304,7 +294,7 @@ impl MixStageParameters { let routing_data = buffer[self.routing_data_range()].to_vec(); // Do a round of LION on the payload - lion_transform_decrypt(&mut buffer[self.payload_range()], &shared_key.0)?; + lion_transform_decrypt(&mut buffer[self.payload_range()], shared_key.as_bytes())?; Ok(routing_data) } diff --git a/nym-outfox/src/packet.rs b/nym-outfox/src/packet.rs index 720ed6071a..97a1562177 100644 --- a/nym-outfox/src/packet.rs +++ b/nym-outfox/src/packet.rs @@ -1,17 +1,14 @@ -use std::{array::TryFromSliceError, collections::VecDeque, ops::Range}; - use crate::{ constants::{DEFAULT_HOPS, MAGIC_SLICE, MIN_PACKET_SIZE, MIX_PARAMS_LEN}, error::OutfoxError, format::{MixCreationParameters, MixStageParameters}, }; - -use rand::{rngs::OsRng, RngCore}; use sphinx_packet::{ crypto::PrivateKey, packet::builder::DEFAULT_PAYLOAD_SIZE, route::{Destination, Node}, }; +use std::{array::TryFromSliceError, collections::VecDeque, ops::Range}; #[derive(Debug)] pub struct OutfoxPacket { @@ -90,8 +87,7 @@ impl OutfoxPacket { destination: &Destination, packet_size: Option, ) -> Result { - let mut secret_key = [0; 32]; - OsRng.fill_bytes(&mut secret_key); + let secret_key = x25519_dalek::StaticSecret::random(); let packet_size = packet_size.unwrap_or(DEFAULT_PAYLOAD_SIZE); let packet_size = if packet_size < MIN_PACKET_SIZE { MIN_PACKET_SIZE @@ -110,7 +106,7 @@ impl OutfoxPacket { stage_params.encode_mix_layer( &mut buffer[range], &secret_key, - route.last().unwrap().pub_key.as_bytes(), + route.last().unwrap().pub_key, destination.address.as_bytes_ref(), )?; @@ -130,11 +126,11 @@ impl OutfoxPacket { // We know that we'll always get 4 nodes, so we can unwrap here let processing_node = nodes.last().unwrap(); let destination_node = nodes.first().unwrap(); - OsRng.fill_bytes(&mut secret_key); + let secret_key = x25519_dalek::StaticSecret::random(); stage_params.encode_mix_layer( &mut buffer[range], &secret_key, - processing_node.pub_key.as_bytes(), + processing_node.pub_key, destination_node.address.as_bytes_ref(), )?; } @@ -168,7 +164,7 @@ impl OutfoxPacket { pub fn decode_mix_layer( &mut self, layer: usize, - mix_secret_key: &[u8; 32], + mix_secret_key: &x25519_dalek::StaticSecret, ) -> Result, OutfoxError> { let (range, params) = self.stage_params(layer); let routing_data = @@ -198,7 +194,6 @@ impl OutfoxPacket { &mut self, mix_secret_key: &PrivateKey, ) -> Result<[u8; 32], OutfoxError> { - let mix_secret_key = mix_secret_key.to_bytes(); let routing_lenght_by_stage = self .mix_params() .routing_information_length_by_stage @@ -210,7 +205,7 @@ impl OutfoxPacket { break; } } - self.decode_mix_layer(layer, &mix_secret_key)?; + self.decode_mix_layer(layer, mix_secret_key)?; self.update_routing_information(layer)?; let (range, stage_params) = self.mix_params().get_stage_params(layer); let routing_bytes = &self.payload()[range][stage_params.routing_data_range()]; diff --git a/nym-outfox/tests/unittests.rs b/nym-outfox/tests/unittests.rs index 08ffdd8a43..6a9e928d8c 100644 --- a/nym-outfox/tests/unittests.rs +++ b/nym-outfox/tests/unittests.rs @@ -9,11 +9,9 @@ mod tests { repeat_with(|| fastrand::u8(..)).take(n).collect() } - use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE; - use curve25519_dalek::scalar::Scalar; use nym_outfox::packet::OutfoxPacket; use sphinx_packet::constants::NODE_ADDRESS_LENGTH; - use sphinx_packet::crypto::PublicKey; + use sphinx_packet::crypto::{PrivateKey, PublicKey}; use sphinx_packet::route::Destination; use sphinx_packet::route::DestinationAddressBytes; use sphinx_packet::route::Node; @@ -22,6 +20,12 @@ mod tests { use nym_outfox::format::*; use nym_outfox::lion::*; + pub fn keygen() -> (PrivateKey, PublicKey) { + let private_key = PrivateKey::random(); + let public_key = PublicKey::from(&private_key); + (private_key, public_key) + } + #[test] fn test_encode_decode() { let mix_params = MixStageParameters { @@ -30,11 +34,9 @@ mod tests { payload_length_bytes: 1024, // 1kb }; - let user_secret = randombytes(32); - let mix_secret = randombytes(32); - let mix_secret_scalar = - Scalar::from_bytes_mod_order(mix_secret.clone().try_into().unwrap()); - let mix_public_key = (ED25519_BASEPOINT_TABLE * &mix_secret_scalar).to_montgomery(); + let user_secret = x25519_dalek::StaticSecret::random(); + let mix_secret = x25519_dalek::StaticSecret::random(); + let mix_public_key = x25519_dalek::PublicKey::from(&mix_secret); let routing = [0; 32]; let destination = [0; 32]; @@ -52,7 +54,7 @@ mod tests { .encode_mix_layer( &mut new_buffer[..], &user_secret, - node.pub_key.as_bytes(), + node.pub_key, &destination, ) .unwrap(); @@ -93,23 +95,23 @@ mod tests { #[test] fn test_packet_params_short() { - let (node1_pk, node1_pub) = sphinx_packet::crypto::keygen(); + let (node1_pk, node1_pub) = keygen(); let node1 = Node::new( NodeAddressBytes::from_bytes([0u8; NODE_ADDRESS_LENGTH]), node1_pub, ); - let (node2_pk, node2_pub) = sphinx_packet::crypto::keygen(); + let (node2_pk, node2_pub) = keygen(); let node2 = Node::new( NodeAddressBytes::from_bytes([1u8; NODE_ADDRESS_LENGTH]), node2_pub, ); - let (node3_pk, node3_pub) = sphinx_packet::crypto::keygen(); + let (node3_pk, node3_pub) = keygen(); let node3 = Node::new( NodeAddressBytes::from_bytes([2u8; NODE_ADDRESS_LENGTH]), node3_pub, ); - let (gateway_pk, gateway_pub) = sphinx_packet::crypto::keygen(); + let (gateway_pk, gateway_pub) = keygen(); let gateway = Node::new( NodeAddressBytes::from_bytes([3u8; NODE_ADDRESS_LENGTH]), gateway_pub, @@ -149,23 +151,23 @@ mod tests { #[test] fn test_packet_params_long() { - let (node1_pk, node1_pub) = sphinx_packet::crypto::keygen(); + let (node1_pk, node1_pub) = keygen(); let node1 = Node::new( NodeAddressBytes::from_bytes([0u8; NODE_ADDRESS_LENGTH]), node1_pub, ); - let (node2_pk, node2_pub) = sphinx_packet::crypto::keygen(); + let (node2_pk, node2_pub) = keygen(); let node2 = Node::new( NodeAddressBytes::from_bytes([1u8; NODE_ADDRESS_LENGTH]), node2_pub, ); - let (node3_pk, node3_pub) = sphinx_packet::crypto::keygen(); + let (node3_pk, node3_pub) = keygen(); let node3 = Node::new( NodeAddressBytes::from_bytes([2u8; NODE_ADDRESS_LENGTH]), node3_pub, ); - let (gateway_pk, gateway_pub) = sphinx_packet::crypto::keygen(); + let (gateway_pk, gateway_pub) = keygen(); let gateway = Node::new( NodeAddressBytes::from_bytes([3u8; NODE_ADDRESS_LENGTH]), gateway_pub, diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index e4e7d77ea2..070d93ead9 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -30,7 +30,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ "crypto-common", - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -55,7 +55,7 @@ dependencies = [ "cipher", "ctr", "ghash", - "subtle 2.5.0", + "subtle", ] [[package]] @@ -154,17 +154,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2e554a8638bdc1e4eae9984845306cc95f8a9208ba8d49c3859fd958b46774d" dependencies = [ "base64ct", - "blake2 0.10.6", + "blake2", "cpufeatures", "password-hash", ] -[[package]] -name = "arrayref" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" - [[package]] name = "async-compression" version = "0.4.18" @@ -319,7 +313,7 @@ dependencies = [ "ripemd", "secp256k1", "sha2 0.10.8", - "subtle 2.5.0", + "subtle", "zeroize", ] @@ -355,18 +349,6 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" -[[package]] -name = "blake2" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94cb07b0da6a73955f8fb85d24c466778e70cda767a568229b104f0264089330" -dependencies = [ - "byte-tools", - "crypto-mac", - "digest 0.8.1", - "opaque-debug 0.2.3", -] - [[package]] name = "blake2" version = "0.10.6" @@ -388,7 +370,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -397,7 +379,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -412,7 +394,7 @@ dependencies = [ "rand_core 0.6.4", "serde", "serdect 0.3.0-pre.0", - "subtle 2.5.0", + "subtle", "zeroize", ] @@ -469,12 +451,6 @@ version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" -[[package]] -name = "byte-tools" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" - [[package]] name = "bytemuck" version = "1.13.1" @@ -621,16 +597,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "chacha" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddf3c081b5fba1e5615640aae998e0fbd10c24cbd897ee39ed754a77601a4862" -dependencies = [ - "byteorder", - "keystream", -] - [[package]] name = "cipher" version = "0.4.4" @@ -985,9 +951,9 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4c2f4e1afd912bc40bfd6fed5d9dc1f288e0ba01bfcc835cc5bc3eb13efe15" dependencies = [ - "generic-array 0.14.7", + "generic-array", "rand_core 0.6.4", - "subtle 2.5.0", + "subtle", "zeroize", ] @@ -997,21 +963,11 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array 0.14.7", + "generic-array", "rand_core 0.6.4", "typenum", ] -[[package]] -name = "crypto-mac" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" -dependencies = [ - "generic-array 0.12.4", - "subtle 1.0.0", -] - [[package]] name = "cssparser" version = "0.27.2" @@ -1067,7 +1023,7 @@ dependencies = [ "byteorder", "digest 0.9.0", "rand_core 0.5.1", - "subtle 2.5.0", + "subtle", "zeroize", ] @@ -1085,7 +1041,7 @@ dependencies = [ "platforms", "rustc_version", "serde", - "subtle 2.5.0", + "subtle", "zeroize", ] @@ -1295,22 +1251,13 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "digest" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" -dependencies = [ - "generic-array 0.12.4", -] - [[package]] name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -1322,7 +1269,7 @@ dependencies = [ "block-buffer 0.10.4", "const-oid", "crypto-common", - "subtle 2.5.0", + "subtle", ] [[package]] @@ -1487,7 +1434,7 @@ dependencies = [ "rand_core 0.6.4", "serde", "sha2 0.10.8", - "subtle 2.5.0", + "subtle", "zeroize", ] @@ -1522,13 +1469,13 @@ dependencies = [ "crypto-bigint", "digest 0.10.7", "ff", - "generic-array 0.14.7", + "generic-array", "group", "pkcs8", "rand_core 0.6.4", "sec1", "serdect 0.2.0", - "subtle 2.5.0", + "subtle", "zeroize", ] @@ -1641,7 +1588,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ "rand_core 0.6.4", - "subtle 2.5.0", + "subtle", ] [[package]] @@ -1921,15 +1868,6 @@ dependencies = [ "windows 0.48.0", ] -[[package]] -name = "generic-array" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" -dependencies = [ - "typenum", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -1983,7 +1921,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" dependencies = [ - "opaque-debug 0.3.0", + "opaque-debug", "polyval", ] @@ -2106,7 +2044,7 @@ checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", "rand_core 0.6.4", - "subtle 2.5.0", + "subtle", ] [[package]] @@ -2334,15 +2272,6 @@ dependencies = [ "webpki-roots 0.25.4", ] -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - [[package]] name = "hmac" version = "0.12.1" @@ -2825,7 +2754,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ - "generic-array 0.14.7", + "generic-array", ] [[package]] @@ -2982,12 +2911,6 @@ dependencies = [ "signature", ] -[[package]] -name = "keystream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28" - [[package]] name = "kuchiki" version = "0.8.1" @@ -3012,12 +2935,6 @@ version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" -[[package]] -name = "libm" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" - [[package]] name = "line-wrap" version = "0.1.1" @@ -3039,18 +2956,6 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" -[[package]] -name = "lioness" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae926706ba42c425c9457121178330d75e273df2e82e28b758faf3de3a9acb9" -dependencies = [ - "arrayref", - "blake2 0.8.1", - "chacha", - "keystream", -] - [[package]] name = "litemap" version = "0.7.4" @@ -3321,7 +3226,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -3436,7 +3340,7 @@ dependencies = [ "rand 0.8.5", "serde", "sha2 0.9.9", - "subtle 2.5.0", + "subtle", "thiserror 2.0.11", "zeroize", ] @@ -3657,7 +3561,6 @@ dependencies = [ name = "nym-sphinx-types" version = "0.2.0" dependencies = [ - "sphinx-packet", "thiserror 2.0.11", ] @@ -3667,7 +3570,7 @@ version = "0.1.0" dependencies = [ "aes-gcm", "argon2", - "generic-array 0.14.7", + "generic-array", "getrandom 0.2.10", "rand 0.8.5", "serde", @@ -3933,12 +3836,6 @@ version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" -[[package]] -name = "opaque-debug" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" - [[package]] name = "opaque-debug" version = "0.3.0" @@ -4076,7 +3973,7 @@ checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", "rand_core 0.6.4", - "subtle 2.5.0", + "subtle", ] [[package]] @@ -4366,7 +4263,7 @@ checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb" dependencies = [ "cfg-if", "cpufeatures", - "opaque-debug 0.3.0", + "opaque-debug", "universal-hash", ] @@ -4617,16 +4514,6 @@ dependencies = [ "getrandom 0.2.10", ] -[[package]] -name = "rand_distr" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" -dependencies = [ - "num-traits", - "rand 0.8.5", -] - [[package]] name = "rand_hc" version = "0.2.0" @@ -4830,7 +4717,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ "hmac", - "subtle 2.5.0", + "subtle", ] [[package]] @@ -4939,7 +4826,7 @@ dependencies = [ "ring", "rustls-pki-types", "rustls-webpki 0.102.4", - "subtle 2.5.0", + "subtle", "zeroize", ] @@ -5092,10 +4979,10 @@ checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", "der", - "generic-array 0.14.7", + "generic-array", "pkcs8", "serdect 0.2.0", - "subtle 2.5.0", + "subtle", "zeroize", ] @@ -5346,7 +5233,7 @@ dependencies = [ "cfg-if", "cpufeatures", "digest 0.9.0", - "opaque-debug 0.3.0", + "opaque-debug", ] [[package]] @@ -5463,31 +5350,6 @@ dependencies = [ "system-deps 5.0.0", ] -[[package]] -name = "sphinx-packet" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dabeca95bf5fd0563d6be7ebcb1c6a9fcb135746a0ba9050c47dc68c8607e595" -dependencies = [ - "aes", - "arrayref", - "blake2 0.8.1", - "bs58", - "byteorder", - "chacha", - "ctr", - "curve25519-dalek 4.1.2", - "digest 0.10.7", - "hkdf", - "hmac", - "lioness", - "log", - "rand 0.8.5", - "rand_distr", - "sha2 0.10.8", - "subtle 2.5.0", -] - [[package]] name = "spin" version = "0.9.8" @@ -5595,12 +5457,6 @@ dependencies = [ "syn 2.0.96", ] -[[package]] -name = "subtle" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" - [[package]] name = "subtle" version = "2.5.0" @@ -5991,7 +5847,7 @@ dependencies = [ "serde_repr", "sha2 0.10.8", "signature", - "subtle 2.5.0", + "subtle", "subtle-encoding", "tendermint-proto 0.34.0", "time", @@ -6021,7 +5877,7 @@ dependencies = [ "serde_repr", "sha2 0.10.8", "signature", - "subtle 2.5.0", + "subtle", "subtle-encoding", "tendermint-proto 0.40.1", "time", @@ -6094,7 +5950,7 @@ dependencies = [ "serde", "serde_bytes", "serde_json", - "subtle 2.5.0", + "subtle", "subtle-encoding", "tendermint 0.40.1", "tendermint-config", @@ -6558,7 +6414,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ "crypto-common", - "subtle 2.5.0", + "subtle", ] [[package]] diff --git a/wasm/zknym-lib/src/generic_scheme/coconut/types.rs b/wasm/zknym-lib/src/generic_scheme/coconut/types.rs index 8370fcc636..875de418cd 100644 --- a/wasm/zknym-lib/src/generic_scheme/coconut/types.rs +++ b/wasm/zknym-lib/src/generic_scheme/coconut/types.rs @@ -156,15 +156,3 @@ pub struct UnblindableShare { pub issuer_key_bs58: String, pub blinded_share_bs58: String, } - -#[wasm_bindgen] -impl UnblindableShare { - #[wasm_bindgen(constructor)] - pub fn new(issuer_index: u64, issuer_key_bs58: String, blinded_share_bs58: String) -> Self { - UnblindableShare { - issuer_index, - issuer_key_bs58, - blinded_share_bs58, - } - } -} From 80b395cd8ef92cac04f969bc8f6d414117a376a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 26 Feb 2025 09:48:05 +0000 Subject: [PATCH 006/133] feat: use ct_eq for checking bearer token (#5501) (#5519) --- Cargo.lock | 1 + common/http-api-common/Cargo.toml | 1 + common/http-api-common/src/middleware/bearer_auth.rs | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 0bac1e45d9..90e0495ee2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5888,6 +5888,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "subtle 2.6.1", "tower 0.5.2", "tracing", "utoipa", diff --git a/common/http-api-common/Cargo.toml b/common/http-api-common/Cargo.toml index e00af2e4ef..e8cf08b103 100644 --- a/common/http-api-common/Cargo.toml +++ b/common/http-api-common/Cargo.toml @@ -20,6 +20,7 @@ mime = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true serde_yaml = { workspace = true } +subtle.workspace = true tower = { workspace = true } tracing.workspace = true utoipa = { workspace = true, optional = true } diff --git a/common/http-api-common/src/middleware/bearer_auth.rs b/common/http-api-common/src/middleware/bearer_auth.rs index 68867f32a3..bb334ab944 100644 --- a/common/http-api-common/src/middleware/bearer_auth.rs +++ b/common/http-api-common/src/middleware/bearer_auth.rs @@ -7,6 +7,7 @@ use axum::{extract::Request, response::Response}; use futures::future::BoxFuture; use std::sync::Arc; use std::task::{Context, Poll}; +use subtle::ConstantTimeEq; use tower::{Layer, Service}; use tracing::{debug, instrument, trace}; use zeroize::Zeroizing; @@ -76,7 +77,7 @@ impl RequireAuth { return Err("`Authorization` header must contain non-empty `Bearer` token"); } - if self.bearer_token.as_str() != bearer_token { + if bool::from(self.bearer_token.as_bytes().ct_ne(bearer_token.as_bytes())) { return Err("`Authorization` header does not contain the correct `Bearer` token"); } From 2cb3817b2ccd63d38496fee8ecc7e14b7919e4f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 26 Feb 2025 09:48:13 +0000 Subject: [PATCH 007/133] feat: add config option for maximum number of client connections (#5513) --- gateway/src/config.rs | 3 +++ .../node/client_handling/websocket/listener.rs | 16 ++++++++++++++++ gateway/src/node/mod.rs | 1 + nym-node/src/config/gateway_tasks.rs | 5 +++++ nym-node/src/config/helpers.rs | 1 + 5 files changed, 26 insertions(+) diff --git a/gateway/src/config.rs b/gateway/src/config.rs index 66d6180f31..02d58ad143 100644 --- a/gateway/src/config.rs +++ b/gateway/src/config.rs @@ -97,6 +97,9 @@ pub struct Debug { /// Specifies maximum age of stored messages before they are removed from the storage pub stale_messages_max_age: Duration, + /// The maximum number of client connections the gateway will keep open at once. + pub maximum_open_connections: usize, + pub zk_nym_tickets: ZkNymTicketHandlerDebug, } diff --git a/gateway/src/node/client_handling/websocket/listener.rs b/gateway/src/node/client_handling/websocket/listener.rs index b5eebc3cab..2d358840e7 100644 --- a/gateway/src/node/client_handling/websocket/listener.rs +++ b/gateway/src/node/client_handling/websocket/listener.rs @@ -7,11 +7,14 @@ use nym_task::TaskClient; use rand::rngs::OsRng; use std::net::SocketAddr; use std::process; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; use tokio::task::JoinHandle; use tracing::*; pub struct Listener { address: SocketAddr, + maximum_open_connections: usize, shared_state: CommonHandlerState, shutdown: TaskClient, } @@ -19,11 +22,13 @@ pub struct Listener { impl Listener { pub(crate) fn new( address: SocketAddr, + maximum_open_connections: usize, shared_state: CommonHandlerState, shutdown: TaskClient, ) -> Self { Listener { address, + maximum_open_connections, shared_state, shutdown, } @@ -41,6 +46,8 @@ impl Listener { } }; + let open_connections = Arc::new(AtomicUsize::new(0)); + while !self.shutdown.is_shutdown() { tokio::select! { biased; @@ -52,6 +59,12 @@ impl Listener { Ok((socket, remote_addr)) => { let shutdown = self.shutdown.fork(format!("websocket_handler_{remote_addr}")); trace!("received a socket connection from {remote_addr}"); + + if open_connections.fetch_add(1, Ordering::SeqCst) >= self.maximum_open_connections { + warn!("connection limit exceeded ({}). can't accept request from {remote_addr}", self.maximum_open_connections); + continue; + } + // TODO: I think we *REALLY* need a mechanism for having a maximum number of connected // clients or spawned tokio tasks -> perhaps a worker system? let handle = FreshHandler::new( @@ -61,12 +74,15 @@ impl Listener { remote_addr, shutdown, ); + let open_connections = open_connections.clone(); tokio::spawn(async move { // TODO: refactor it similarly to the mixnet listener on the nym-node let metrics_ref = handle.shared_state.metrics.clone(); metrics_ref.network.new_ingress_websocket_client(); + open_connections.fetch_add(1, Ordering::SeqCst); handle.start_handling().await; metrics_ref.network.disconnected_ingress_websocket_client(); + open_connections.fetch_sub(1, Ordering::SeqCst); }); } Err(err) => warn!("failed to get client: {err}"), diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 1dd6f2c217..9323bb572b 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -262,6 +262,7 @@ impl GatewayTasksBuilder { Ok(websocket::Listener::new( self.config.gateway.websocket_bind_address, + self.config.debug.maximum_open_connections, shared_state, self.shutdown.fork("websocket"), )) diff --git a/nym-node/src/config/gateway_tasks.rs b/nym-node/src/config/gateway_tasks.rs index 9f814f2bf5..1a4c7454ac 100644 --- a/nym-node/src/config/gateway_tasks.rs +++ b/nym-node/src/config/gateway_tasks.rs @@ -47,6 +47,9 @@ pub struct Debug { /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. pub message_retrieval_limit: i64, + /// The maximum number of client connections the gateway will keep open at once. + pub maximum_open_connections: usize, + pub stale_messages: StaleMessageDebug, pub client_bandwidth: ClientBandwidthDebug, @@ -56,12 +59,14 @@ pub struct Debug { impl Debug { const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; + const DEFAULT_MAXIMUM_OPEN_CONNECTIONS: usize = 8192; } impl Default for Debug { fn default() -> Self { Debug { message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + maximum_open_connections: Self::DEFAULT_MAXIMUM_OPEN_CONNECTIONS, stale_messages: Default::default(), client_bandwidth: Default::default(), zk_nym_tickets: Default::default(), diff --git a/nym-node/src/config/helpers.rs b/nym-node/src/config/helpers.rs index 2afd58301c..f3380ef223 100644 --- a/nym-node/src/config/helpers.rs +++ b/nym-node/src/config/helpers.rs @@ -40,6 +40,7 @@ fn ephemeral_gateway_config(config: &Config) -> nym_gateway::config::Config { .stale_messages .cleaner_run_interval, stale_messages_max_age: config.gateway_tasks.debug.stale_messages.max_age, + maximum_open_connections: config.gateway_tasks.debug.maximum_open_connections, zk_nym_tickets: nym_gateway::config::ZkNymTicketHandlerDebug { revocation_bandwidth_penalty: config .gateway_tasks From 8ba53229971f11be13c853a1a9557bea020795bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 26 Feb 2025 09:48:21 +0000 Subject: [PATCH 008/133] bugfix: bound check when recovering a reply SURB (#5502) --- common/nymsphinx/anonymous-replies/src/reply_surb.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/common/nymsphinx/anonymous-replies/src/reply_surb.rs b/common/nymsphinx/anonymous-replies/src/reply_surb.rs index 3ac7af8571..90bd892d78 100644 --- a/common/nymsphinx/anonymous-replies/src/reply_surb.rs +++ b/common/nymsphinx/anonymous-replies/src/reply_surb.rs @@ -19,6 +19,9 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum ReplySurbError { + #[error("did not receive enough data to recover a reply SURB")] + TooShort, + #[error("tried to use reply SURB with an unpadded message")] UnpaddedMessageError, @@ -131,7 +134,9 @@ impl ReplySurb { } pub fn from_bytes(bytes: &[u8]) -> Result { - // TODO: introduce bound checks to guard us against out of bound reads + if bytes.len() <= SurbEncryptionKeySize::USIZE { + return Err(ReplySurbError::TooShort); + } let encryption_key = SurbEncryptionKey::try_from_bytes(&bytes[..SurbEncryptionKeySize::USIZE])?; From 69b24485001b4cea9e345d974dc480dc5fe2f081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 26 Feb 2025 10:02:55 +0000 Subject: [PATCH 009/133] chore: removed all old coconut code (#5500) --- Cargo.lock | 28 +- Cargo.toml | 1 - common/nymcoconut/Cargo.toml | 48 -- common/nymcoconut/README.md | 1 - common/nymcoconut/benches/benchmarks.rs | 360 --------- common/nymcoconut/src/elgamal.rs | 354 --------- common/nymcoconut/src/error.rs | 69 -- common/nymcoconut/src/impls/clone.rs | 15 - common/nymcoconut/src/impls/mod.rs | 2 - common/nymcoconut/src/impls/serde.rs | 57 -- common/nymcoconut/src/lib.rs | 56 -- common/nymcoconut/src/proofs/mod.rs | 619 --------------- common/nymcoconut/src/scheme/aggregation.rs | 432 ----------- common/nymcoconut/src/scheme/double_use.rs | 79 -- common/nymcoconut/src/scheme/issuance.rs | 660 ---------------- common/nymcoconut/src/scheme/keygen.rs | 722 ------------------ common/nymcoconut/src/scheme/mod.rs | 672 ---------------- common/nymcoconut/src/scheme/setup.rs | 91 --- common/nymcoconut/src/scheme/verification.rs | 432 ----------- common/nymcoconut/src/tests/e2e.rs | 84 -- common/nymcoconut/src/tests/helpers.rs | 186 ----- common/nymcoconut/src/tests/mod.rs | 3 - common/nymcoconut/src/traits.rs | 88 --- common/nymcoconut/src/utils.rs | 382 --------- nym-api/Cargo.toml | 1 - nym-api/src/ecash/dkg/controller/keys.rs | 14 +- nym-api/src/ecash/keys/mod.rs | 17 - nym-api/src/ecash/keys/persistence.rs | 36 +- .../src/client.rs | 26 - .../src/http/router/api/v1/openapi.rs | 42 - wasm/zknym-lib/Cargo.toml | 1 - wasm/zknym-lib/src/error.rs | 6 - .../src/generic_scheme/coconut/mod.rs | 338 -------- .../src/generic_scheme/coconut/types.rs | 158 ---- wasm/zknym-lib/src/generic_scheme/mod.rs | 1 - wasm/zknym-lib/src/lib.rs | 4 - wasm/zknym-lib/src/vpn_api_client/client.rs | 35 +- wasm/zknym-lib/src/vpn_api_client/types.rs | 16 - 38 files changed, 4 insertions(+), 6132 deletions(-) delete mode 100644 common/nymcoconut/Cargo.toml delete mode 100644 common/nymcoconut/README.md delete mode 100644 common/nymcoconut/benches/benchmarks.rs delete mode 100644 common/nymcoconut/src/elgamal.rs delete mode 100644 common/nymcoconut/src/error.rs delete mode 100644 common/nymcoconut/src/impls/clone.rs delete mode 100644 common/nymcoconut/src/impls/mod.rs delete mode 100644 common/nymcoconut/src/impls/serde.rs delete mode 100644 common/nymcoconut/src/lib.rs delete mode 100644 common/nymcoconut/src/proofs/mod.rs delete mode 100644 common/nymcoconut/src/scheme/aggregation.rs delete mode 100644 common/nymcoconut/src/scheme/double_use.rs delete mode 100644 common/nymcoconut/src/scheme/issuance.rs delete mode 100644 common/nymcoconut/src/scheme/keygen.rs delete mode 100644 common/nymcoconut/src/scheme/mod.rs delete mode 100644 common/nymcoconut/src/scheme/setup.rs delete mode 100644 common/nymcoconut/src/scheme/verification.rs delete mode 100644 common/nymcoconut/src/tests/e2e.rs delete mode 100644 common/nymcoconut/src/tests/helpers.rs delete mode 100644 common/nymcoconut/src/tests/mod.rs delete mode 100644 common/nymcoconut/src/traits.rs delete mode 100644 common/nymcoconut/src/utils.rs delete mode 100644 wasm/zknym-lib/src/generic_scheme/coconut/mod.rs delete mode 100644 wasm/zknym-lib/src/generic_scheme/coconut/types.rs diff --git a/Cargo.lock b/Cargo.lock index 90e0495ee2..d9ee2ed197 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4775,7 +4775,6 @@ dependencies = [ "nym-api-requests", "nym-bandwidth-controller", "nym-bin-common", - "nym-coconut", "nym-coconut-dkg-common", "nym-compact-ecash", "nym-config", @@ -5258,30 +5257,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "nym-coconut" -version = "0.5.0" -dependencies = [ - "bls12_381", - "bs58", - "criterion", - "digest 0.9.0", - "doc-comment", - "ff", - "getrandom 0.2.15", - "group", - "itertools 0.13.0", - "nym-dkg", - "nym-pemstore", - "rand 0.8.5", - "rand_chacha 0.3.1", - "serde", - "serde_derive", - "sha2 0.9.9", - "thiserror 2.0.11", - "zeroize", -] - [[package]] name = "nym-coconut-bandwidth-contract-common" version = "0.1.0" @@ -5888,7 +5863,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "subtle 2.6.1", + "subtle 2.5.0", "tower 0.5.2", "tracing", "utoipa", @@ -12202,7 +12177,6 @@ dependencies = [ "getrandom 0.2.15", "js-sys", "nym-bin-common", - "nym-coconut", "nym-compact-ecash", "nym-credentials", "nym-crypto", diff --git a/Cargo.toml b/Cargo.toml index a750d7dbc7..c1fee9aca5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,7 +66,6 @@ members = [ "common/nym-id", "common/nym-metrics", "common/nym_offline_compact_ecash", - "common/nymcoconut", "common/nymsphinx", "common/nymsphinx/acknowledgements", "common/nymsphinx/addressing", diff --git a/common/nymcoconut/Cargo.toml b/common/nymcoconut/Cargo.toml deleted file mode 100644 index c4266b3459..0000000000 --- a/common/nymcoconut/Cargo.toml +++ /dev/null @@ -1,48 +0,0 @@ -[package] -name = "nym-coconut" -version = "0.5.0" -authors = ["Jedrzej Stuczynski ", "Ania Piotrowska "] -edition = "2021" -license.workspace = true - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -bls12_381 = { workspace = true, default-features = false, features = ["pairings", "alloc", "experimental"] } -itertools = { workspace = true } -digest = "0.9" -rand = { workspace = true } -thiserror = { workspace = true } -serde = { workspace = true } -serde_derive = { workspace = true } -bs58 = { workspace = true } -sha2 = "0.9" -zeroize = { workspace = true, optional = true } - -nym-dkg = { path = "../dkg" } -nym-pemstore = { path = "../pemstore" } - -[dependencies.ff] -workspace = true -default-features = false - -[dependencies.group] -workspace = true -default-features = false - -[dev-dependencies] -criterion = { workspace = true, features = ["html_reports"] } -doc-comment = { workspace = true } -rand_chacha = { workspace = true } - -[[bench]] -name = "benchmarks" -harness = false - -[features] -key-zeroize = ["zeroize", "bls12_381/zeroize"] -default = [] - - -[target.'cfg(target_env = "wasm32-unknown-unknown")'.dependencies] -getrandom = { version="0.2", features=["js"] } diff --git a/common/nymcoconut/README.md b/common/nymcoconut/README.md deleted file mode 100644 index 2433e80e34..0000000000 --- a/common/nymcoconut/README.md +++ /dev/null @@ -1 +0,0 @@ -This project was partially funded through the NGI0 PET Fund, a fund established by NL.net with financial support from the European Commission's NGI programme, under the aegis of DG Communications Networks, Content and Technology under grant agreement No 825310. \ No newline at end of file diff --git a/common/nymcoconut/benches/benchmarks.rs b/common/nymcoconut/benches/benchmarks.rs deleted file mode 100644 index 42fb36dc82..0000000000 --- a/common/nymcoconut/benches/benchmarks.rs +++ /dev/null @@ -1,360 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use bls12_381::{multi_miller_loop, G1Affine, G1Projective, G2Affine, G2Prepared, Scalar}; -use criterion::{criterion_group, criterion_main, Criterion}; -use ff::Field; -use group::{Curve, Group}; -use nym_coconut::{ - aggregate_signature_shares_and_verify, aggregate_verification_keys, blind_sign, - prepare_blind_sign, prove_bandwidth_credential, random_scalars_refs, setup, ttp_keygen, - verify_credential, verify_partial_blind_signature, Attribute, BlindedSignature, Parameters, - Signature, SignatureShare, VerificationKey, -}; -use rand::seq::SliceRandom; -use std::ops::Neg; -use std::time::Duration; - -#[allow(unused)] -fn double_pairing(g11: &G1Affine, g21: &G2Affine, g12: &G1Affine, g22: &G2Affine) { - let gt1 = bls12_381::pairing(g11, g21); - let gt2 = bls12_381::pairing(g12, g22); - assert_eq!(gt1, gt2) -} - -#[allow(unused)] -fn multi_miller_pairing_affine(g11: &G1Affine, g21: &G2Affine, g12: &G1Affine, g22: &G2Affine) { - let miller_loop_result = multi_miller_loop(&[ - (g11, &G2Prepared::from(*g21)), - (&g12.neg(), &G2Prepared::from(*g22)), - ]); - assert!(bool::from( - miller_loop_result.final_exponentiation().is_identity() - )) -} - -#[allow(unused)] -fn bench_pairings(c: &mut Criterion) { - let mut rng = rand::thread_rng(); - - let g1 = G1Affine::generator(); - let g2 = G2Affine::generator(); - let r = Scalar::random(&mut rng); - let s = Scalar::random(&mut rng); - - let g11 = (g1 * r).to_affine(); - let g21 = (g2 * s).to_affine(); - let g21_prep = G2Prepared::from(g21); - - let g12 = (g1 * s).to_affine(); - let g22 = (g2 * r).to_affine(); - let g22_prep = G2Prepared::from(g22); - - c.bench_function("double pairing", |b| { - b.iter(|| double_pairing(&g11, &g21, &g12, &g22)) - }); - - c.bench_function("multi miller in affine", |b| { - b.iter(|| multi_miller_pairing_affine(&g11, &g21, &g12, &g22)) - }); - - c.bench_function("multi miller with prepared g2", |b| { - b.iter(|| multi_miller_pairing_with_prepared(&g11, &g21_prep, &g12, &g22_prep)) - }); - - c.bench_function("multi miller with semi-prepared g2", |b| { - b.iter(|| multi_miller_pairing_with_semi_prepared(&g11, &g21, &g12, &g22_prep)) - }); -} - -#[allow(unused)] -fn multi_miller_pairing_with_prepared( - g11: &G1Affine, - g21: &G2Prepared, - g12: &G1Affine, - g22: &G2Prepared, -) { - let miller_loop_result = multi_miller_loop(&[(g11, g21), (&g12.neg(), g22)]); - assert!(bool::from( - miller_loop_result.final_exponentiation().is_identity() - )) -} - -// the case of being able to prepare G2 generator -#[allow(unused)] -fn multi_miller_pairing_with_semi_prepared( - g11: &G1Affine, - g21: &G2Affine, - g12: &G1Affine, - g22: &G2Prepared, -) { - let miller_loop_result = - multi_miller_loop(&[(g11, &G2Prepared::from(*g21)), (&g12.neg(), g22)]); - assert!(bool::from( - miller_loop_result.final_exponentiation().is_identity() - )) -} - -#[allow(clippy::too_many_arguments)] -fn unblind_and_aggregate( - params: &Parameters, - blinded_signatures: &[BlindedSignature], - partial_verification_keys: &[VerificationKey], - private_attributes: &[&Attribute], - public_attributes: &[&Attribute], - commitment_hash: &G1Projective, - pedersen_commitments_openings: &[Scalar], - verification_key: &VerificationKey, -) -> Signature { - // Unblind all partial signatures - let unblinded_signatures: Vec = blinded_signatures - .iter() - .zip(partial_verification_keys.iter()) - .map(|(signature, partial_verification_key)| { - signature - .unblind_and_verify( - params, - partial_verification_key, - private_attributes, - public_attributes, - commitment_hash, - pedersen_commitments_openings, - ) - .unwrap() - }) - .collect(); - - let unblinded_signature_shares: Vec = unblinded_signatures - .iter() - .enumerate() - .map(|(idx, signature)| SignatureShare::new(*signature, (idx + 1) as u64)) - .collect(); - - let mut attributes = vec![]; - attributes.extend_from_slice(private_attributes); - attributes.extend_from_slice(public_attributes); - aggregate_signature_shares_and_verify( - params, - verification_key, - &attributes, - &unblinded_signature_shares, - ) - .unwrap() -} - -struct BenchCase { - num_authorities: u64, - threshold_p: f32, - num_public_attrs: u32, - num_private_attrs: u32, -} - -impl BenchCase { - fn threshold(&self) -> u64 { - (self.num_authorities as f32 * self.threshold_p).round() as u64 - } - - fn num_attrs(&self) -> u32 { - self.num_public_attrs + self.num_private_attrs - } -} - -fn bench_coconut(c: &mut Criterion) { - let mut group = c.benchmark_group("benchmark-coconut"); - group.measurement_time(Duration::from_secs(1000)); - let case = BenchCase { - num_authorities: 100, - threshold_p: 0.7, - num_public_attrs: 2, - num_private_attrs: 2, - }; - - let params = setup(case.num_public_attrs + case.num_private_attrs).unwrap(); - - random_scalars_refs!(public_attributes, params, case.num_public_attrs as usize); - let serial_number = params.random_scalar(); - let binding_number = params.random_scalar(); - let private_attributes = vec![&serial_number, &binding_number]; - - // The prepare blind sign is performed by the user - let (pedersen_commitments_openings, blind_sign_request) = - prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap(); - - // CLIENT BENCHMARK: Data needed to ask for a credential - // Let's benchmark the operations the client has to perform - // to ask for a credential - group.bench_function( - format!( - "[Client] prepare_blind_sign_{}_authorities_{}_attributes_{}_threshold", - case.num_authorities, - case.num_attrs(), - case.threshold_p, - ), - |b| { - b.iter(|| prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap()) - }, - ); - - // keys for the validators - let coconut_keypairs = ttp_keygen(¶ms, case.threshold(), case.num_authorities).unwrap(); - - // VALIDATOR BENCHMARK: Issue partial credential - // we pick only one key pair, as we want to validate how much does it - // take for a single validator to issue a partial credential - let mut rng = rand::thread_rng(); - let keypair = coconut_keypairs.choose(&mut rng).unwrap(); - - group.bench_function( - format!( - "[Validator] compute_single_blind_sign_for_credential_with_{}_attributes", - case.num_attrs(), - ), - |b| { - b.iter(|| { - blind_sign( - ¶ms, - keypair.secret_key(), - &blind_sign_request, - &public_attributes, - ) - .unwrap() - }) - }, - ); - - // computing all partial credentials - // NOTE: in reality, each validator computes only single signature - let mut blinded_signatures = Vec::new(); - for keypair in coconut_keypairs.iter() { - let blinded_signature = blind_sign( - ¶ms, - keypair.secret_key(), - &blind_sign_request, - &public_attributes, - ) - .unwrap(); - blinded_signatures.push(blinded_signature) - } - - let verification_keys: Vec = coconut_keypairs - .iter() - .map(|keypair| keypair.verification_key().clone()) - .collect(); - - // verify a random partial blind signature - let rand_idx = 1; - let random_blind_signature = blinded_signatures.get(rand_idx).unwrap(); - let partial_verification_key = verification_keys.get(rand_idx).unwrap(); - - group.bench_function( - format!( - "verify_partial_blind_signature_{}_private_attributes_{}_public_attributes", - case.num_private_attrs, case.num_public_attrs - ), - |b| { - b.iter(|| { - verify_partial_blind_signature( - ¶ms, - blind_sign_request.get_private_attributes_pedersen_commitments(), - &public_attributes, - random_blind_signature, - partial_verification_key, - ) - }) - }, - ); - - // Lets bench worse case, ie aggregating all - let indices: Vec = (1..=case.num_authorities).collect(); - // aggregate verification keys - let aggr_verification_key = - aggregate_verification_keys(&verification_keys, Some(&indices)).unwrap(); - - // CLIENT OPERATION: Unblind partial singatures and aggregate into single signature - let aggregated_signature = unblind_and_aggregate( - ¶ms, - &blinded_signatures, - &verification_keys, - &private_attributes, - &public_attributes, - &blind_sign_request.get_commitment_hash(), - &pedersen_commitments_openings, - &aggr_verification_key, - ); - - // CLIENT BENCHMARK: aggregate all partial credentials - group.bench_function( - format!( - "[Client] unblind_and_aggregate_partial_credentials_{}_authorities_{}_attributes_{}_threshold", - case.num_authorities, - case.num_attrs(), - case.threshold_p, - ), - |b| { - b.iter(|| { - unblind_and_aggregate( - ¶ms, - &blinded_signatures, - &verification_keys, - &private_attributes, - &public_attributes, - &blind_sign_request.get_commitment_hash(), - &pedersen_commitments_openings, - &aggr_verification_key) - }) - }, - ); - - // CLIENT OPERATION: Randomize credentials and generate any cryptographic material to verify them - let theta = prove_bandwidth_credential( - ¶ms, - &aggr_verification_key, - &aggregated_signature, - &serial_number, - &binding_number, - ) - .unwrap(); - - // CLIENT BENCHMARK - group.bench_function( - format!( - "[Client] randomize_and_prove_credential_{}_authorities_{}_attributes_{}_threshold", - case.num_authorities, - case.num_attrs(), - case.threshold_p, - ), - |b| { - b.iter(|| { - prove_bandwidth_credential( - ¶ms, - &aggr_verification_key, - &aggregated_signature, - &serial_number, - &binding_number, - ) - .unwrap() - }) - }, - ); - - // VERIFIER OPERATION - // Verify credentials - verify_credential(¶ms, &aggr_verification_key, &theta, &public_attributes); - - // VERIFICATION BENCHMARK - group.bench_function( - format!( - "[Verifier] verify_credentials_{}_authorities_{}_attributes_{}_threshold", - case.num_authorities, - case.num_attrs(), - case.threshold_p, - ), - |b| { - b.iter(|| { - verify_credential(¶ms, &aggr_verification_key, &theta, &public_attributes) - }) - }, - ); -} -criterion_group!(benches, bench_coconut); -criterion_main!(benches); diff --git a/common/nymcoconut/src/elgamal.rs b/common/nymcoconut/src/elgamal.rs deleted file mode 100644 index 1b814f0bc5..0000000000 --- a/common/nymcoconut/src/elgamal.rs +++ /dev/null @@ -1,354 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use core::ops::{Deref, Mul}; - -use bls12_381::{G1Projective, Scalar}; -use group::Curve; -use serde_derive::{Deserialize, Serialize}; - -use crate::error::{CoconutError, Result}; -use crate::scheme::setup::Parameters; -use crate::traits::{Base58, Bytable}; -use crate::utils::{try_deserialize_g1_projective, try_deserialize_scalar}; -use crate::Attribute; - -/// Type alias for the ephemeral key generated during ElGamal encryption -pub type EphemeralKey = Scalar; - -/// Two G1 points representing ElGamal ciphertext -#[derive(Debug)] -#[cfg_attr(test, derive(PartialEq, Eq))] -pub struct Ciphertext(pub(crate) G1Projective, pub(crate) G1Projective); - -impl TryFrom<&[u8]> for Ciphertext { - type Error = CoconutError; - - fn try_from(bytes: &[u8]) -> Result { - if bytes.len() != 96 { - return Err(CoconutError::Deserialization(format!( - "Ciphertext must be exactly 96 bytes, got {}", - bytes.len() - ))); - } - - // safety: we just checked for the length so the unwraps are fine - #[allow(clippy::unwrap_used)] - let c1_bytes: &[u8; 48] = &bytes[..48].try_into().unwrap(); - #[allow(clippy::unwrap_used)] - let c2_bytes: &[u8; 48] = &bytes[48..].try_into().unwrap(); - - let c1 = try_deserialize_g1_projective( - c1_bytes, - CoconutError::Deserialization("Failed to deserialize compressed c1".to_string()), - )?; - let c2 = try_deserialize_g1_projective( - c2_bytes, - CoconutError::Deserialization("Failed to deserialize compressed c2".to_string()), - )?; - - Ok(Ciphertext(c1, c2)) - } -} - -impl Ciphertext { - pub fn c1(&self) -> &G1Projective { - &self.0 - } - - pub fn c2(&self) -> &G1Projective { - &self.1 - } - - pub fn to_bytes(&self) -> [u8; 96] { - let mut bytes = [0u8; 96]; - bytes[..48].copy_from_slice(&self.0.to_affine().to_compressed()); - bytes[48..].copy_from_slice(&self.1.to_affine().to_compressed()); - bytes - } - - pub fn from_bytes(bytes: &[u8]) -> Result { - Ciphertext::try_from(bytes) - } -} - -/// PrivateKey used in the ElGamal encryption scheme to recover the plaintext -#[derive(Debug)] -#[cfg_attr(test, derive(PartialEq, Eq))] -pub struct PrivateKey(pub(crate) Scalar); - -impl PrivateKey { - /// Decrypt takes the ElGamal encryption of a message and returns a point on the G1 curve - /// that represents original h^m. - pub fn decrypt(&self, ciphertext: &Ciphertext) -> G1Projective { - let (c1, c2) = &(ciphertext.0, ciphertext.1); - - // (gamma^k * h^m) / (g1^{d * k}) | note: gamma = g1^d - c2 - c1 * self.0 - } - - pub fn public_key(&self, params: &Parameters) -> PublicKey { - PublicKey(params.gen1() * self.0) - } - - pub fn to_bytes(&self) -> [u8; 32] { - self.0.to_bytes() - } - - pub fn from_bytes(bytes: &[u8; 32]) -> Result { - try_deserialize_scalar( - bytes, - CoconutError::Deserialization( - "Failed to deserialize ElGamal private key - it was not in the canonical form" - .to_string(), - ), - ) - .map(PrivateKey) - } -} - -impl Bytable for PrivateKey { - fn to_byte_vec(&self) -> Vec { - self.to_bytes().to_vec() - } - - fn try_from_byte_slice(slice: &[u8]) -> Result { - let received = slice.len(); - let Ok(arr) = slice.try_into() else { - return Err(CoconutError::UnexpectedArrayLength { - typ: "elgamal::PrivateKey".to_string(), - received, - expected: 32, - }); - }; - - PrivateKey::from_bytes(arr) - } -} - -impl Base58 for PrivateKey {} - -// TODO: perhaps be more explicit and apart from gamma also store generator and group order? -/// PublicKey used in the ElGamal encryption scheme to produce the ciphertext -#[derive(Debug, Clone)] -#[cfg_attr(test, derive(PartialEq, Eq))] -pub struct PublicKey(G1Projective); - -impl PublicKey { - /// Encrypt encrypts the given message in the form of h^m, - /// where h is a point on the G1 curve using the given public key. - /// The random k is returned alongside the encryption - /// as it is required by the Coconut Scheme to create proofs of knowledge. - pub fn encrypt( - &self, - params: &Parameters, - h: &G1Projective, - msg: &Scalar, - ) -> (Ciphertext, EphemeralKey) { - let k = params.random_scalar(); - // c1 = g1^k - let c1 = params.gen1() * k; - // c2 = gamma^k * h^m - let c2 = self.0 * k + h * msg; - - (Ciphertext(c1, c2), k) - } - - pub fn to_bytes(&self) -> [u8; 48] { - self.0.to_affine().to_compressed() - } - - pub fn from_bytes(bytes: &[u8; 48]) -> Result { - try_deserialize_g1_projective( - bytes, - CoconutError::Deserialization( - "Failed to deserialize compressed ElGamal public key".to_string(), - ), - ) - .map(PublicKey) - } -} - -impl Bytable for PublicKey { - fn to_byte_vec(&self) -> Vec { - self.to_bytes().into() - } - - fn try_from_byte_slice(slice: &[u8]) -> Result { - let received = slice.len(); - let Ok(arr) = slice.try_into() else { - return Err(CoconutError::UnexpectedArrayLength { - typ: "elgamal::PublicKey".to_string(), - received, - expected: 48, - }); - }; - - PublicKey::from_bytes(arr) - } -} - -impl TryFrom<&[u8]> for PublicKey { - type Error = CoconutError; - - fn try_from(slice: &[u8]) -> Result { - PublicKey::try_from_byte_slice(slice) - } -} - -impl Base58 for PublicKey {} - -impl Deref for PublicKey { - type Target = G1Projective; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl<'a> Mul<&'a Scalar> for &PublicKey { - type Output = G1Projective; - - fn mul(self, rhs: &'a Scalar) -> Self::Output { - self.0 * rhs - } -} - -#[derive(Serialize, Deserialize)] -/// A convenient wrapper for both keys of the ElGamal keypair -pub struct ElGamalKeyPair { - private_key: PrivateKey, - public_key: PublicKey, -} - -impl ElGamalKeyPair { - pub fn public_key(&self) -> &PublicKey { - &self.public_key - } - - pub fn private_key(&self) -> &PrivateKey { - &self.private_key - } -} - -/// Generate a fresh ElGamal keypair using the group generator specified by the provided [Parameters] -pub fn elgamal_keygen(params: &Parameters) -> ElGamalKeyPair { - let private_key = params.random_scalar(); - let gamma = params.gen1() * private_key; - - ElGamalKeyPair { - private_key: PrivateKey(private_key), - public_key: PublicKey(gamma), - } -} - -pub fn compute_attribute_encryption( - params: &Parameters, - private_attributes: &[&Attribute], - pub_key: &PublicKey, - commitment_hash: G1Projective, -) -> (Vec, Vec) { - private_attributes - .iter() - .map(|m| pub_key.encrypt(params, &commitment_hash, m)) - .unzip() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn keygen() { - let params = Parameters::default(); - let keypair = super::elgamal_keygen(¶ms); - - let expected = params.gen1() * keypair.private_key.0; - let gamma = keypair.public_key.0; - assert_eq!( - expected, gamma, - "Public key, gamma, should be equal to g1^d, where d is the private key" - ); - } - - #[test] - fn encryption() { - let params = Parameters::default(); - let keypair = super::elgamal_keygen(¶ms); - - let r = params.random_scalar(); - let h = params.gen1() * r; - let m = params.random_scalar(); - - let (ciphertext, ephemeral_key) = keypair.public_key.encrypt(¶ms, &h, &m); - - let expected_c1 = params.gen1() * ephemeral_key; - assert_eq!(expected_c1, ciphertext.0, "c1 should be equal to g1^k"); - - let expected_c2 = keypair.public_key.0 * ephemeral_key + h * m; - assert_eq!( - expected_c2, ciphertext.1, - "c2 should be equal to gamma^k * h^m" - ); - } - - #[test] - fn decryption() { - let params = Parameters::default(); - let keypair = super::elgamal_keygen(¶ms); - - let r = params.random_scalar(); - let h = params.gen1() * r; - let m = params.random_scalar(); - - let (ciphertext, _) = keypair.public_key.encrypt(¶ms, &h, &m); - let dec = keypair.private_key.decrypt(&ciphertext); - - let expected = h * m; - assert_eq!( - expected, dec, - "after ElGamal decryption, original h^m should be obtained" - ); - } - - #[test] - fn private_key_bytes_roundtrip() { - let params = Parameters::default(); - let private_key = PrivateKey(params.random_scalar()); - let bytes = private_key.to_bytes(); - - // also make sure it is equivalent to the internal scalar's bytes - assert_eq!(private_key.0.to_bytes(), bytes); - assert_eq!(private_key, PrivateKey::from_bytes(&bytes).unwrap()) - } - - #[test] - fn public_key_bytes_roundtrip() { - let params = Parameters::default(); - let r = params.random_scalar(); - let public_key = PublicKey(params.gen1() * r); - let bytes = public_key.to_bytes(); - - // also make sure it is equivalent to the internal g1 compressed bytes - assert_eq!(public_key.0.to_affine().to_compressed(), bytes); - assert_eq!(public_key, PublicKey::from_bytes(&bytes).unwrap()) - } - - #[test] - fn ciphertext_bytes_roundtrip() { - let params = Parameters::default(); - let r = params.random_scalar(); - let s = params.random_scalar(); - let ciphertext = Ciphertext(params.gen1() * r, params.gen1() * s); - let bytes = ciphertext.to_bytes(); - - // also make sure it is equivalent to the internal g1 compressed bytes concatenated - let expected_bytes = [ - ciphertext.0.to_affine().to_compressed(), - ciphertext.1.to_affine().to_compressed(), - ] - .concat(); - assert_eq!(expected_bytes, bytes); - assert_eq!(ciphertext, Ciphertext::try_from(&bytes[..]).unwrap()) - } -} diff --git a/common/nymcoconut/src/error.rs b/common/nymcoconut/src/error.rs deleted file mode 100644 index d502c76c95..0000000000 --- a/common/nymcoconut/src/error.rs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use thiserror::Error; - -/// A `Result` alias where the `Err` case is `coconut_rs::Error`. -pub type Result = std::result::Result; - -#[derive(Error, Debug)] -pub enum CoconutError { - #[error("Setup error: {0}")] - Setup(String), - - #[error("encountered error during keygen")] - Keygen, - - #[error("Issuance related error: {0}")] - Issuance(String), - - #[error("Tried to prepare blind sign request for higher than specified number of attributes (max: {}, requested: {})", max, requested)] - IssuanceMaxAttributes { max: usize, requested: usize }, - - #[error("Interpolation error: {0}")] - Interpolation(String), - - #[error("Aggregation error: {0}")] - Aggregation(String), - - #[error("Unblind error: {0}")] - Unblind(String), - - #[error("Verification error: {0}")] - Verification(String), - - #[error("Deserialization error: {0}")] - Deserialization(String), - - #[error( - "Deserailization error, expected at least {} bytes, got {}", - min, - actual - )] - DeserializationMinLength { min: usize, actual: usize }, - - #[error("Tried to deserialize {object} with bytes of invalid length. Expected {actual} < {object} or {modulus_target} % {modulus} == 0")] - DeserializationInvalidLength { - actual: usize, - target: usize, - modulus_target: usize, - modulus: usize, - object: String, - }, - - #[error("received an array of unexpected size for deserialization of {typ}. got {received} but expected {expected}")] - UnexpectedArrayLength { - typ: String, - received: usize, - expected: usize, - }, - - #[error("failed to decode the base58 representation: {0}")] - Base58DecodingFailure(#[from] bs58::decode::Error), - - #[error("failed to deserialize scalar from the received bytes - it might not have been canonically encoded")] - ScalarDeserializationFailure, - - #[error("failed to deserialize G1Projective point from the received bytes - it might not have been canonically encoded")] - G1ProjectiveDeserializationFailure, -} diff --git a/common/nymcoconut/src/impls/clone.rs b/common/nymcoconut/src/impls/clone.rs deleted file mode 100644 index 88ee6664c0..0000000000 --- a/common/nymcoconut/src/impls/clone.rs +++ /dev/null @@ -1,15 +0,0 @@ -use crate::{BlindSignRequest, BlindedSignature, Bytable, VerifyCredentialRequest}; - -macro_rules! impl_clone { - ($struct:ident) => { - impl Clone for $struct { - fn clone(&self) -> Self { - Self::try_from_byte_slice(&self.to_byte_vec()).unwrap() - } - } - }; -} - -impl_clone!(BlindSignRequest); -impl_clone!(BlindedSignature); -impl_clone!(VerifyCredentialRequest); diff --git a/common/nymcoconut/src/impls/mod.rs b/common/nymcoconut/src/impls/mod.rs deleted file mode 100644 index efacad272d..0000000000 --- a/common/nymcoconut/src/impls/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod clone; -mod serde; diff --git a/common/nymcoconut/src/impls/serde.rs b/common/nymcoconut/src/impls/serde.rs deleted file mode 100644 index 7d1e020cb1..0000000000 --- a/common/nymcoconut/src/impls/serde.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::elgamal::PrivateKey; -use crate::scheme::SecretKey; -use crate::{ - Base58, BlindSignRequest, BlindedSignature, PublicKey, Signature, VerificationKey, - VerifyCredentialRequest, -}; -use serde::de::Unexpected; -use serde::{de::Error, de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; -use std::fmt; - -macro_rules! impl_serde { - ($struct:ident, $visitor:ident) => { - pub struct $visitor {} - - impl Serialize for $struct { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.to_bs58()) - } - } - - impl<'de> Visitor<'de> for $visitor { - type Value = $struct; - - fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(formatter, "A base58 encoded struct") - } - - fn visit_str(self, s: &str) -> Result { - match $struct::try_from_bs58(s) { - Ok(x) => Ok(x), - Err(_) => Err(Error::invalid_value(Unexpected::Str(s), &self)), - } - } - } - - impl<'de> Deserialize<'de> for $struct { - fn deserialize(deserializer: D) -> Result<$struct, D::Error> - where - D: Deserializer<'de>, - { - deserializer.deserialize_str($visitor {}) - } - } - }; -} - -impl_serde!(SecretKey, V1); -impl_serde!(VerificationKey, V2); -impl_serde!(PublicKey, V3); -impl_serde!(PrivateKey, V4); -impl_serde!(BlindSignRequest, V5); -impl_serde!(BlindedSignature, V6); -impl_serde!(Signature, V7); -impl_serde!(VerifyCredentialRequest, V8); diff --git a/common/nymcoconut/src/lib.rs b/common/nymcoconut/src/lib.rs deleted file mode 100644 index dbcbda5b39..0000000000 --- a/common/nymcoconut/src/lib.rs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -#![warn(clippy::expect_used)] -#![warn(clippy::unwrap_used)] - -pub use bls12_381::Scalar; -pub use elgamal::elgamal_keygen; -pub use elgamal::ElGamalKeyPair; -pub use elgamal::PublicKey; -pub use error::CoconutError; -pub use scheme::aggregation::aggregate_key_shares; -pub use scheme::aggregation::aggregate_signature_shares; -pub use scheme::aggregation::aggregate_signature_shares_and_verify; -pub use scheme::aggregation::aggregate_verification_keys; -pub use scheme::issuance::blind_sign; -pub use scheme::issuance::prepare_blind_sign; -pub use scheme::issuance::sign; -pub use scheme::issuance::verify_partial_blind_signature; -pub use scheme::issuance::BlindSignRequest; -pub use scheme::keygen::keygen; -pub use scheme::keygen::ttp_keygen; -pub use scheme::keygen::KeyPair; -pub use scheme::keygen::SecretKey; -pub use scheme::keygen::VerificationKey; -pub use scheme::keygen::VerificationKeyShare; -pub use scheme::setup::setup; -pub use scheme::setup::Parameters; -pub use scheme::verification::check_vk_pairing; -pub use scheme::verification::prove_bandwidth_credential; -pub use scheme::verification::verify; -pub use scheme::verification::verify_credential; -pub use scheme::verification::BlindedSerialNumber; -pub use scheme::verification::VerifyCredentialRequest; -pub use scheme::BlindedSignature; -pub use scheme::Signature; -pub use scheme::SignatureShare; -pub use scheme::SignerIndex; -pub use traits::Base58; -pub use traits::Bytable; -pub use utils::hash_to_scalar; - -pub mod elgamal; -mod error; -mod impls; -mod proofs; -mod scheme; -pub mod tests; -mod traits; -pub mod utils; - -pub type Attribute = bls12_381::Scalar; -pub type PrivateAttribute = Attribute; -pub type PublicAttribute = Attribute; - -pub use bls12_381::G1Projective; diff --git a/common/nymcoconut/src/proofs/mod.rs b/common/nymcoconut/src/proofs/mod.rs deleted file mode 100644 index 17a8416c59..0000000000 --- a/common/nymcoconut/src/proofs/mod.rs +++ /dev/null @@ -1,619 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -// TODO: look at https://crates.io/crates/merlin to perhaps use it instead? - -use std::borrow::Borrow; - -use bls12_381::{G1Projective, G2Projective, Scalar}; -use digest::generic_array::typenum::Unsigned; -use digest::Digest; -use group::GroupEncoding; -use itertools::izip; -use sha2::Sha256; - -use crate::error::{CoconutError, Result}; -use crate::scheme::issuance::compute_hash; -use crate::scheme::setup::Parameters; -use crate::scheme::VerificationKey; -use crate::utils::{try_deserialize_scalar, try_deserialize_scalar_vec}; -use crate::Attribute; - -// as per the reference python implementation -type ChallengeDigest = Sha256; - -#[derive(Debug)] -#[cfg_attr(test, derive(PartialEq, Eq))] -pub struct ProofCmCs { - challenge: Scalar, - response_opening: Scalar, - response_openings: Vec, - response_attributes: Vec, -} - -// note: this is slightly different from the reference python implementation -// as we omit the unnecessary string conversion. Instead we concatenate byte -// representations together and hash that. -// note2: G1 and G2 elements are using their compressed representations -// and as per the bls12-381 library all elements are using big-endian form -/// Generates a Scalar [or Fp] challenge by hashing a number of elliptic curve points. -fn compute_challenge(iter: I) -> Scalar -where - D: Digest, - I: Iterator, - B: AsRef<[u8]>, -{ - let mut h = D::new(); - for point_representation in iter { - h.update(point_representation); - } - let digest = h.finalize(); - - // TODO: I don't like the 0 padding here (though it's what we've been using before, - // but we never had a security audit anyway...) - // instead we could maybe use the `from_bytes` variant and adding some suffix - // when computing the digest until we produce a valid scalar. - let mut bytes = [0u8; 64]; - let pad_size = 64usize - .checked_sub(D::OutputSize::to_usize()) - .unwrap_or_default(); - - bytes[pad_size..].copy_from_slice(&digest); - - Scalar::from_bytes_wide(&bytes) -} - -fn produce_response(witness: &Scalar, challenge: &Scalar, secret: &Scalar) -> Scalar { - witness - challenge * secret -} - -// note: it's caller's responsibility to ensure witnesses.len() = secrets.len() -fn produce_responses(witnesses: &[Scalar], challenge: &Scalar, secrets: &[S]) -> Vec -where - S: Borrow, -{ - debug_assert_eq!(witnesses.len(), secrets.len()); - - witnesses - .iter() - .zip(secrets.iter()) - .map(|(w, x)| produce_response(w, challenge, x.borrow())) - .collect() -} - -impl ProofCmCs { - /// Construct non-interactive zero-knowledge proof of correctness of the ciphertexts and the commitment - /// using the Fiat-Shamir heuristic. - pub(crate) fn construct( - params: &Parameters, - commitment: &G1Projective, - commitment_opening: &Scalar, - commitments: &[G1Projective], - pedersen_commitments_openings: &[Scalar], - private_attributes: &[&Attribute], - public_attributes: &[&Attribute], - ) -> Self { - // note: this is only called from `prepare_blind_sign` that already checks - // whether private attributes are non-empty and whether we don't have too many - // attributes in total to sign. - // we also know, due to the single call place, that ephemeral_keys.len() == private_attributes.len() - - // witness creation - let witness_commitment_opening = params.random_scalar(); - let witness_pedersen_commitments_openings = - params.n_random_scalars(pedersen_commitments_openings.len()); - let witness_attributes = params.n_random_scalars(private_attributes.len()); - - // recompute h - let h = compute_hash(*commitment, public_attributes); - let hs_bytes = params - .gen_hs() - .iter() - .map(|h| h.to_bytes()) - .collect::>(); - - let g1 = params.gen1(); - - // compute commitments - - // zkp commitment for the attributes commitment cm - // Ccm = (wr * g1) + (wm[0] * hs[0]) + ... + (wm[i] * hs[i]) - let commitment_attributes = g1 * witness_commitment_opening - + witness_attributes - .iter() - .zip(params.gen_hs().iter()) - .map(|(wm_i, hs_i)| hs_i * wm_i) - .sum::(); - - // zkp commitments for the individual attributes - let commitments_attributes = witness_pedersen_commitments_openings - .iter() - .zip(witness_attributes.iter()) - .map(|(o_j, m_j)| g1 * o_j + h * m_j) - .collect::>(); - - let commitments_bytes = commitments - .iter() - .map(|cm| cm.to_bytes()) - .collect::>(); - - let commitments_attributes_bytes = commitments_attributes - .iter() - .map(|cm| cm.to_bytes()) - .collect::>(); - - // compute challenge - let challenge = compute_challenge::( - std::iter::once(params.gen1().to_bytes().as_ref()) - .chain(hs_bytes.iter().map(|hs| hs.as_ref())) - .chain(std::iter::once(h.to_bytes().as_ref())) - .chain(std::iter::once(commitment.to_bytes().as_ref())) - .chain(commitments_bytes.iter().map(|cm| cm.as_ref())) - .chain(std::iter::once(commitment_attributes.to_bytes().as_ref())) - .chain(commitments_attributes_bytes.iter().map(|cm| cm.as_ref())), - ); - - // Responses - let response_opening = - produce_response(&witness_commitment_opening, &challenge, commitment_opening); - let response_openings = produce_responses( - &witness_pedersen_commitments_openings, - &challenge, - &pedersen_commitments_openings.iter().collect::>(), - ); - let response_attributes = - produce_responses(&witness_attributes, &challenge, private_attributes); - - ProofCmCs { - challenge, - response_opening, - response_openings, - response_attributes, - } - } - - pub(crate) fn verify( - &self, - params: &Parameters, - commitment: &G1Projective, - commitments: &[G1Projective], - public_attributes: &[&Attribute], - ) -> bool { - if self.response_attributes.len() != commitments.len() { - return false; - } - - // recompute h - let h = compute_hash(*commitment, public_attributes); - let g1 = params.gen1(); - - let hs_bytes = params - .gen_hs() - .iter() - .map(|h| h.to_bytes()) - .collect::>(); - - // recompute witnesses commitments - // Cw = (cm * c) + (rr * g1) + (rm[0] * hs[0]) + ... + (rm[n] * hs[n]) - let commitment_attributes = (commitment - - public_attributes - .iter() - .zip(params.gen_hs().iter().skip(self.response_attributes.len())) - .map(|(&pub_attr, hs)| hs * pub_attr) - .sum::()) - * self.challenge - + g1 * self.response_opening - + self - .response_attributes - .iter() - .zip(params.gen_hs().iter()) - .map(|(res_attr, hs)| hs * res_attr) - .sum::(); - - let commitments_attributes = izip!( - commitments.iter(), - self.response_openings.iter(), - self.response_attributes.iter() - ) - .map(|(cm_j, r_o_j, r_m_j)| cm_j * self.challenge + g1 * r_o_j + h * r_m_j) - .collect::>(); - - let commitments_bytes = commitments - .iter() - .map(|cm| cm.to_bytes()) - .collect::>(); - - let commitments_attributes_bytes = commitments_attributes - .iter() - .map(|cm| cm.to_bytes()) - .collect::>(); - - // re-compute the challenge - let challenge = compute_challenge::( - std::iter::once(params.gen1().to_bytes().as_ref()) - .chain(hs_bytes.iter().map(|hs| hs.as_ref())) - .chain(std::iter::once(h.to_bytes().as_ref())) - .chain(std::iter::once(commitment.to_bytes().as_ref())) - .chain(commitments_bytes.iter().map(|cm| cm.as_ref())) - .chain(std::iter::once(commitment_attributes.to_bytes().as_ref())) - .chain(commitments_attributes_bytes.iter().map(|cm| cm.as_ref())), - ); - - challenge == self.challenge - } - - // challenge || response opening || openings len || response openings || attributes len || - // response attributes - pub(crate) fn to_bytes(&self) -> Vec { - let openings_len = self.response_openings.len() as u64; - let attributes_len = self.response_attributes.len() as u64; - - let mut bytes = Vec::with_capacity(16 + (2 + openings_len + attributes_len) as usize * 32); - - bytes.extend_from_slice(&self.challenge.to_bytes()); - bytes.extend_from_slice(&self.response_opening.to_bytes()); - - bytes.extend_from_slice(&openings_len.to_le_bytes()); - for ro in &self.response_openings { - bytes.extend_from_slice(&ro.to_bytes()); - } - - bytes.extend_from_slice(&attributes_len.to_le_bytes()); - for rm in &self.response_attributes { - bytes.extend_from_slice(&rm.to_bytes()); - } - - bytes - } - - pub(crate) fn from_bytes(bytes: &[u8]) -> Result { - // at the very minimum there must be a single attribute being proven - if bytes.len() < 32 * 4 + 16 || (bytes.len() - 16) % 32 != 0 { - return Err(CoconutError::Deserialization( - "tried to deserialize proof of commitments with bytes of invalid length" - .to_string(), - )); - } - - let mut idx = 0; - // safety: bound checked + constant offset - #[allow(clippy::unwrap_used)] - let challenge_bytes = bytes[idx..idx + 32].try_into().unwrap(); - idx += 32; - // safety: bound checked + constant offset - #[allow(clippy::unwrap_used)] - let response_opening_bytes = bytes[idx..idx + 32].try_into().unwrap(); - idx += 32; - - let challenge = try_deserialize_scalar( - &challenge_bytes, - CoconutError::Deserialization("Failed to deserialize challenge".to_string()), - )?; - - let response_opening = try_deserialize_scalar( - &response_opening_bytes, - CoconutError::Deserialization( - "Failed to deserialize the response to the random".to_string(), - ), - )?; - - // safety: bound checked + constant offset - #[allow(clippy::unwrap_used)] - let ro_len = u64::from_le_bytes(bytes[idx..idx + 8].try_into().unwrap()); - idx += 8; - if bytes[idx..].len() < ro_len as usize * 32 + 8 { - return Err( - CoconutError::Deserialization( - "tried to deserialize proof of ciphertexts and commitment with insufficient number of bytes provided".to_string()), - ); - } - - let ro_end = idx + ro_len as usize * 32; - let response_openings = try_deserialize_scalar_vec( - ro_len, - &bytes[idx..ro_end], - CoconutError::Deserialization("Failed to deserialize openings response".to_string()), - )?; - - // safety: bound checked + constant offset - #[allow(clippy::unwrap_used)] - let rm_len = u64::from_le_bytes(bytes[ro_end..ro_end + 8].try_into().unwrap()); - let response_attributes = try_deserialize_scalar_vec( - rm_len, - &bytes[ro_end + 8..], - CoconutError::Deserialization("Failed to deserialize attributes response".to_string()), - )?; - - Ok(ProofCmCs { - challenge, - response_opening, - response_openings, - response_attributes, - }) - } -} - -#[derive(Debug, PartialEq, Eq)] -pub struct ProofKappaZeta { - // c - challenge: Scalar, - - // responses - response_serial_number: Scalar, - response_binding_number: Scalar, - response_blinder: Scalar, -} - -impl ProofKappaZeta { - pub(crate) fn construct( - params: &Parameters, - verification_key: &VerificationKey, - serial_number: &Attribute, - binding_number: &Attribute, - blinding_factor: &Scalar, - blinded_message: &G2Projective, - blinded_serial_number: &G2Projective, - ) -> Self { - // create the witnesses - let witness_blinder = params.random_scalar(); - let witness_serial_number = params.random_scalar(); - let witness_binding_number = params.random_scalar(); - let witness_attributes = [witness_serial_number, witness_binding_number]; - - let beta_bytes = verification_key - .beta_g2 - .iter() - .map(|beta_i| beta_i.to_bytes()) - .collect::>(); - - // witnesses commitments - // Aw = g2 * wt + alpha + beta[0] * wm[0] + ... + beta[i] * wm[i] - let commitment_kappa = params.gen2() * witness_blinder - + verification_key.alpha - + witness_attributes - .iter() - .zip(verification_key.beta_g2.iter()) - .map(|(wm_i, beta_i)| beta_i * wm_i) - .sum::(); - - // zeta is the public value associated with the serial number - let commitment_zeta = params.gen2() * witness_serial_number; - - let challenge = compute_challenge::( - std::iter::once(params.gen2().to_bytes().as_ref()) - .chain(std::iter::once(blinded_message.to_bytes().as_ref())) - .chain(std::iter::once(blinded_serial_number.to_bytes().as_ref())) - .chain(std::iter::once(verification_key.alpha.to_bytes().as_ref())) - .chain(beta_bytes.iter().map(|b| b.as_ref())) - .chain(std::iter::once(commitment_kappa.to_bytes().as_ref())) - .chain(std::iter::once(commitment_zeta.to_bytes().as_ref())), - ); - - // responses - let response_blinder = produce_response(&witness_blinder, &challenge, blinding_factor); - let response_serial_number = - produce_response(&witness_serial_number, &challenge, serial_number); - let response_binding_number = - produce_response(&witness_binding_number, &challenge, binding_number); - - ProofKappaZeta { - challenge, - response_serial_number, - response_binding_number, - response_blinder, - } - } - - pub(crate) fn private_attributes_len(&self) -> usize { - 2 - } - - pub(crate) fn verify( - &self, - params: &Parameters, - verification_key: &VerificationKey, - kappa: &G2Projective, - zeta: &G2Projective, - ) -> bool { - let beta_bytes = verification_key - .beta_g2 - .iter() - .map(|beta_i| beta_i.to_bytes()) - .collect::>(); - - let response_attributes = [self.response_serial_number, self.response_binding_number]; - // re-compute witnesses commitments - // Aw = (c * kappa) + (rt * g2) + ((1 - c) * alpha) + (rm[0] * beta[0]) + ... + (rm[i] * beta[i]) - let commitment_kappa = kappa * self.challenge - + params.gen2() * self.response_blinder - + verification_key.alpha * (Scalar::one() - self.challenge) - + response_attributes - .iter() - .zip(verification_key.beta_g2.iter()) - .map(|(priv_attr, beta_i)| beta_i * priv_attr) - .sum::(); - - // zeta is the public value associated with the serial number - let commitment_zeta = zeta * self.challenge + params.gen2() * self.response_serial_number; - - // compute the challenge - let challenge = compute_challenge::( - std::iter::once(params.gen2().to_bytes().as_ref()) - .chain(std::iter::once(kappa.to_bytes().as_ref())) - .chain(std::iter::once(zeta.to_bytes().as_ref())) - .chain(std::iter::once(verification_key.alpha.to_bytes().as_ref())) - .chain(beta_bytes.iter().map(|b| b.as_ref())) - .chain(std::iter::once(commitment_kappa.to_bytes().as_ref())) - .chain(std::iter::once(commitment_zeta.to_bytes().as_ref())), - ); - - challenge == self.challenge - } - - // challenge || response serial number || response binding number || repose blinder - pub(crate) fn to_bytes(&self) -> Vec { - let attributes_len = 2; // because we have serial number and the binding number - let mut bytes = Vec::with_capacity((1 + attributes_len + 1) as usize * 32); - - bytes.extend_from_slice(&self.challenge.to_bytes()); - bytes.extend_from_slice(&self.response_serial_number.to_bytes()); - bytes.extend_from_slice(&self.response_binding_number.to_bytes()); - - bytes.extend_from_slice(&self.response_blinder.to_bytes()); - - bytes - } - - pub(crate) fn from_bytes(bytes: &[u8]) -> Result { - // at the very minimum there must be a single attribute being proven - if bytes.len() != 128 { - return Err(CoconutError::DeserializationInvalidLength { - actual: bytes.len(), - modulus_target: bytes.len(), - modulus: 32, - object: "kappa and zeta".to_string(), - target: 32 * 4, - }); - } - - // safety: bound checked + constant offset - #[allow(clippy::unwrap_used)] - let challenge_bytes = bytes[..32].try_into().unwrap(); - let challenge = try_deserialize_scalar( - &challenge_bytes, - CoconutError::Deserialization("Failed to deserialize challenge".to_string()), - )?; - - // safety: bound checked + constant offset - #[allow(clippy::unwrap_used)] - let serial_number_bytes = &bytes[32..64].try_into().unwrap(); - let response_serial_number = try_deserialize_scalar( - serial_number_bytes, - CoconutError::Deserialization("failed to deserialize the serial number".to_string()), - )?; - - // safety: bound checked + constant offset - #[allow(clippy::unwrap_used)] - let binding_number_bytes = &bytes[64..96].try_into().unwrap(); - let response_binding_number = try_deserialize_scalar( - binding_number_bytes, - CoconutError::Deserialization("failed to deserialize the binding number".to_string()), - )?; - - // safety: bound checked + constant offset - #[allow(clippy::unwrap_used)] - let blinder_bytes = bytes[96..].try_into().unwrap(); - let response_blinder = try_deserialize_scalar( - &blinder_bytes, - CoconutError::Deserialization("failed to deserialize the blinder".to_string()), - )?; - - Ok(ProofKappaZeta { - challenge, - response_serial_number, - response_binding_number, - response_blinder, - }) - } -} - -// proof builder: -// - commitment -// - challenge -// - responses - -#[cfg(test)] -mod tests { - use super::*; - use crate::scheme::keygen::keygen; - use crate::scheme::setup::setup; - use crate::scheme::verification::{compute_kappa, compute_zeta}; - use crate::tests::helpers::random_scalars_refs; - use group::Group; - use rand::thread_rng; - - #[test] - fn proof_cm_cs_bytes_roundtrip() { - let mut rng = thread_rng(); - - let params = setup(1).unwrap(); - let cm = G1Projective::random(&mut rng); - let r = params.random_scalar(); - let cms: [G1Projective; 1] = [G1Projective::random(&mut rng)]; - let rs = params.n_random_scalars(1); - random_scalars_refs!(private_attributes, params, 1); - - // 0 public 1 private - let pi_s = ProofCmCs::construct(¶ms, &cm, &r, &cms, &rs, &private_attributes, &[]); - - let bytes = pi_s.to_bytes(); - assert_eq!(ProofCmCs::from_bytes(&bytes).unwrap(), pi_s); - - let params = setup(2).unwrap(); - let cm = G1Projective::random(&mut rng); - let r = params.random_scalar(); - let cms: [G1Projective; 2] = [ - G1Projective::random(&mut rng), - G1Projective::random(&mut rng), - ]; - let rs = params.n_random_scalars(2); - random_scalars_refs!(private_attributes, params, 2); - - // 0 public 2 privates - let pi_s = ProofCmCs::construct(¶ms, &cm, &r, &cms, &rs, &private_attributes, &[]); - - let bytes = pi_s.to_bytes(); - assert_eq!(ProofCmCs::from_bytes(&bytes).unwrap(), pi_s); - } - - #[test] - fn proof_kappa_zeta_bytes_roundtrip() { - let params = setup(4).unwrap(); - - let keypair = keygen(¶ms); - - // we don't care about 'correctness' of the proof. only whether we can correctly recover it from bytes - let serial_number = ¶ms.random_scalar(); - let binding_number = ¶ms.random_scalar(); - let private_attributes = vec![serial_number, binding_number]; - - let r = params.random_scalar(); - let kappa = compute_kappa(¶ms, keypair.verification_key(), &private_attributes, r); - let zeta = compute_zeta(¶ms, serial_number); - - // 0 public 2 private - let pi_v = ProofKappaZeta::construct( - ¶ms, - keypair.verification_key(), - serial_number, - binding_number, - &r, - &kappa, - &zeta, - ); - - let proof_bytes = pi_v.to_bytes(); - - let proof_from_bytes = ProofKappaZeta::from_bytes(&proof_bytes).unwrap(); - assert_eq!(proof_from_bytes, pi_v); - - // 2 public 2 private - let params = setup(4).unwrap(); - let keypair = keygen(¶ms); - - let pi_v = ProofKappaZeta::construct( - ¶ms, - keypair.verification_key(), - serial_number, - binding_number, - &r, - &kappa, - &zeta, - ); - - let proof_bytes = pi_v.to_bytes(); - - let proof_from_bytes = ProofKappaZeta::from_bytes(&proof_bytes).unwrap(); - assert_eq!(proof_from_bytes, pi_v); - } -} diff --git a/common/nymcoconut/src/scheme/aggregation.rs b/common/nymcoconut/src/scheme/aggregation.rs deleted file mode 100644 index e954e5b8b0..0000000000 --- a/common/nymcoconut/src/scheme/aggregation.rs +++ /dev/null @@ -1,432 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use core::iter::Sum; -use core::ops::Mul; - -use bls12_381::{G2Prepared, G2Projective, Scalar}; -use group::Curve; -use itertools::Itertools; - -use crate::error::{CoconutError, Result}; -use crate::scheme::verification::check_bilinear_pairing; -use crate::scheme::{PartialSignature, Signature, SignatureShare, SignerIndex, VerificationKey}; -use crate::utils::perform_lagrangian_interpolation_at_origin; -use crate::{Attribute, Parameters, VerificationKeyShare}; - -pub(crate) trait Aggregatable: Sized { - fn aggregate(aggregatable: &[Self], indices: Option<&[SignerIndex]>) -> Result; - - fn check_unique_indices(indices: &[SignerIndex]) -> bool { - // if aggregation is a threshold one, all indices should be unique - indices.iter().unique_by(|&index| index).count() == indices.len() - } -} - -// includes `VerificationKey` -impl Aggregatable for T -where - T: Sum, - for<'a> T: Sum<&'a T>, - for<'a> &'a T: Mul, -{ - fn aggregate(aggregatable: &[T], indices: Option<&[u64]>) -> Result { - if aggregatable.is_empty() { - return Err(CoconutError::Aggregation("Empty set of values".to_string())); - } - - if let Some(indices) = indices { - if !Self::check_unique_indices(indices) { - return Err(CoconutError::Aggregation("Non-unique indices".to_string())); - } - perform_lagrangian_interpolation_at_origin(indices, aggregatable) - } else { - // non-threshold - Ok(aggregatable.iter().sum()) - } - } -} - -impl Aggregatable for PartialSignature { - fn aggregate(sigs: &[PartialSignature], indices: Option<&[u64]>) -> Result { - let h = sigs - .first() - .ok_or_else(|| CoconutError::Aggregation("Empty set of signatures".to_string()))? - .sig1(); - - // TODO: is it possible to avoid this allocation? - let sigmas = sigs.iter().map(|sig| *sig.sig2()).collect::>(); - let aggr_sigma = Aggregatable::aggregate(&sigmas, indices)?; - - Ok(Signature(*h, aggr_sigma)) - } -} - -/// Ensures all provided verification keys were generated to verify the same number of attributes. -fn check_same_key_size(keys: &[VerificationKey]) -> bool { - keys.iter().map(|vk| vk.beta_g1.len()).all_equal() - && keys.iter().map(|vk| vk.beta_g2.len()).all_equal() -} - -pub fn aggregate_verification_keys( - keys: &[VerificationKey], - indices: Option<&[SignerIndex]>, -) -> Result { - if !check_same_key_size(keys) { - return Err(CoconutError::Aggregation( - "Verification keys are of different sizes".to_string(), - )); - } - Aggregatable::aggregate(keys, indices) -} - -pub fn aggregate_key_shares(shares: &[VerificationKeyShare]) -> Result { - let (keys, indices): (Vec<_>, Vec<_>) = shares - .iter() - .map(|share| (share.key.clone(), share.index)) - .unzip(); - - aggregate_verification_keys(&keys, Some(&indices)) -} - -pub fn aggregate_signatures( - signatures: &[PartialSignature], - indices: Option<&[SignerIndex]>, -) -> Result { - Aggregatable::aggregate(signatures, indices) -} - -pub fn aggregate_signatures_and_verify( - params: &Parameters, - verification_key: &VerificationKey, - attributes: &[&Attribute], - signatures: &[PartialSignature], - indices: Option<&[SignerIndex]>, -) -> Result { - // aggregate the signature - let signature = aggregate_signatures(signatures, indices)?; - - // Verify the signature - let alpha = verification_key.alpha; - - let tmp = attributes - .iter() - .zip(verification_key.beta_g2.iter()) - .map(|(&attr, beta_i)| beta_i * attr) - .sum::(); - - if bool::from(signature.0.is_identity()) { - return Err(CoconutError::Aggregation( - "Verification of the aggregated signature failed - h is an identity point".to_string(), - )); - } - if !check_bilinear_pairing( - &signature.0.to_affine(), - &G2Prepared::from((alpha + tmp).to_affine()), - &signature.1.to_affine(), - params.prepared_miller_g2(), - ) { - return Err(CoconutError::Aggregation( - "Verification of the aggregated signature failed".to_string(), - )); - } - Ok(signature) -} - -pub fn aggregate_signature_shares(shares: &[SignatureShare]) -> Result { - let (signatures, indices): (Vec<_>, Vec<_>) = shares - .iter() - .map(|share| (*share.signature(), share.index())) - .unzip(); - - aggregate_signatures(&signatures, Some(&indices)) -} - -pub fn aggregate_signature_shares_and_verify( - params: &Parameters, - verification_key: &VerificationKey, - attributes: &[&Attribute], - shares: &[SignatureShare], -) -> Result { - let (signatures, indices): (Vec<_>, Vec<_>) = shares - .iter() - .map(|share| (*share.signature(), share.index())) - .unzip(); - - aggregate_signatures_and_verify( - params, - verification_key, - attributes, - &signatures, - Some(&indices), - ) -} - -#[cfg(test)] -mod tests { - use crate::scheme::issuance::sign; - use crate::scheme::keygen::ttp_keygen; - use crate::scheme::verification::verify; - use crate::tests::helpers::random_scalars_refs; - use bls12_381::G1Projective; - use group::Group; - - use super::*; - - #[test] - fn key_aggregation_works_for_any_subset_of_keys() { - let params = Parameters::new(2).unwrap(); - let keypairs = ttp_keygen(¶ms, 3, 5).unwrap(); - - let vks = keypairs - .into_iter() - .map(|keypair| keypair.verification_key().clone()) - .collect::>(); - - let aggr_vk1 = aggregate_verification_keys(&vks[..3], Some(&[1, 2, 3])).unwrap(); - let aggr_vk2 = aggregate_verification_keys(&vks[2..], Some(&[3, 4, 5])).unwrap(); - - assert_eq!(aggr_vk1, aggr_vk2); - - // TODO: should those two actually work or not? - // aggregating threshold+1 - let aggr_more = aggregate_verification_keys(&vks[1..], Some(&[2, 3, 4, 5])).unwrap(); - assert_eq!(aggr_vk1, aggr_more); - - // aggregating all - let aggr_all = aggregate_verification_keys(&vks, Some(&[1, 2, 3, 4, 5])).unwrap(); - assert_eq!(aggr_all, aggr_vk1); - - // not taking enough points (threshold was 3) - let aggr_not_enough = aggregate_verification_keys(&vks[..2], Some(&[1, 2])).unwrap(); - assert_ne!(aggr_not_enough, aggr_vk1); - - // taking wrong index - let aggr_bad = aggregate_verification_keys(&vks[2..], Some(&[42, 123, 100])).unwrap(); - assert_ne!(aggr_vk1, aggr_bad); - } - - #[test] - fn key_aggregation_doesnt_work_for_empty_set_of_keys() { - let keys: Vec = vec![]; - assert!(aggregate_verification_keys(&keys, None).is_err()); - } - - #[test] - fn key_aggregation_doesnt_work_if_indices_have_invalid_length() { - let keys = vec![VerificationKey::identity(3)]; - - assert!(aggregate_verification_keys(&keys, Some(&[])).is_err()); - assert!(aggregate_verification_keys(&keys, Some(&[1, 2])).is_err()); - } - - #[test] - fn key_aggregation_doesnt_work_for_non_unique_indices() { - let keys = vec![VerificationKey::identity(3), VerificationKey::identity(3)]; - - assert!(aggregate_verification_keys(&keys, Some(&[1, 1])).is_err()); - } - - #[test] - fn key_aggregation_doesnt_work_for_keys_of_different_size() { - let keys = vec![VerificationKey::identity(3), VerificationKey::identity(1)]; - - assert!(aggregate_verification_keys(&keys, None).is_err()) - } - - #[test] - fn signature_aggregation_works_for_any_subset_of_signatures() { - let params = Parameters::new(2).unwrap(); - random_scalars_refs!(attributes, params, 2); - - let keypairs = ttp_keygen(¶ms, 3, 5).unwrap(); - - let (sks, vks): (Vec<_>, Vec<_>) = keypairs - .into_iter() - .map(|keypair| { - ( - keypair.secret_key().clone(), - keypair.verification_key().clone(), - ) - }) - .unzip(); - - let sigs = sks - .iter() - .map(|sk| sign(sk, &attributes).unwrap()) - .collect::>(); - - // aggregating (any) threshold works - let aggr_vk_1 = aggregate_verification_keys(&vks[..3], Some(&[1, 2, 3])).unwrap(); - let aggr_sig1 = aggregate_signatures_and_verify( - ¶ms, - &aggr_vk_1, - &attributes, - &sigs[..3], - Some(&[1, 2, 3]), - ) - .unwrap(); - - let aggr_vk_2 = aggregate_verification_keys(&vks[2..], Some(&[3, 4, 5])).unwrap(); - let aggr_sig2 = aggregate_signatures_and_verify( - ¶ms, - &aggr_vk_1, - &attributes, - &sigs[2..], - Some(&[3, 4, 5]), - ) - .unwrap(); - assert_eq!(aggr_sig1, aggr_sig2); - - // verify credential for good measure - assert!(verify(¶ms, &aggr_vk_1, &attributes, &aggr_sig1)); - assert!(verify(¶ms, &aggr_vk_2, &attributes, &aggr_sig2)); - - // aggregating threshold+1 works - let aggr_vk_more = aggregate_verification_keys(&vks[1..], Some(&[2, 3, 4, 5])).unwrap(); - let aggr_more = aggregate_signatures_and_verify( - ¶ms, - &aggr_vk_more, - &attributes, - &sigs[1..], - Some(&[2, 3, 4, 5]), - ) - .unwrap(); - assert_eq!(aggr_sig1, aggr_more); - - // aggregating all - let aggr_vk_all = aggregate_verification_keys(&vks, Some(&[1, 2, 3, 4, 5])).unwrap(); - let aggr_all = aggregate_signatures_and_verify( - ¶ms, - &aggr_vk_all, - &attributes, - &sigs, - Some(&[1, 2, 3, 4, 5]), - ) - .unwrap(); - assert_eq!(aggr_all, aggr_sig1); - - // not taking enough points (threshold was 3) should fail - let aggr_vk_not_enough = aggregate_verification_keys(&vks[..2], Some(&[1, 2])).unwrap(); - let aggr_not_enough = aggregate_signatures_and_verify( - ¶ms, - &aggr_vk_not_enough, - &attributes, - &sigs[..2], - Some(&[1, 2]), - ) - .unwrap(); - assert_ne!(aggr_not_enough, aggr_sig1); - - // taking wrong index should fail - let aggr_vk_bad = aggregate_verification_keys(&vks[2..], Some(&[1, 2, 3])).unwrap(); - assert!(aggregate_signatures_and_verify( - ¶ms, - &aggr_vk_bad, - &attributes, - &sigs[2..], - Some(&[42, 123, 100]), - ) - .is_err()); - } - - fn random_signature() -> Signature { - let mut rng = rand::thread_rng(); - Signature( - G1Projective::random(&mut rng), - G1Projective::random(&mut rng), - ) - } - - #[test] - fn signature_aggregation_doesnt_work_for_empty_set_of_signatures() { - let signatures: Vec = vec![]; - let params = Parameters::new(2).unwrap(); - random_scalars_refs!(attributes, params, 2); - let keypairs = ttp_keygen(¶ms, 3, 5).unwrap(); - - let (_, vks): (Vec<_>, Vec<_>) = keypairs - .into_iter() - .map(|keypair| { - ( - keypair.secret_key().clone(), - keypair.verification_key().clone(), - ) - }) - .unzip(); - - let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap(); - assert!(aggregate_signatures_and_verify( - ¶ms, - &aggr_vk_all, - &attributes, - &signatures, - None - ) - .is_err()); - } - - #[test] - fn signature_aggregation_doesnt_work_if_indices_have_invalid_length() { - let signatures = vec![random_signature()]; - let params = Parameters::new(2).unwrap(); - random_scalars_refs!(attributes, params, 2); - let keypairs = ttp_keygen(¶ms, 3, 5).unwrap(); - let (_, vks): (Vec<_>, Vec<_>) = keypairs - .into_iter() - .map(|keypair| { - ( - keypair.secret_key().clone(), - keypair.verification_key().clone(), - ) - }) - .unzip(); - let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap(); - - assert!(aggregate_signatures_and_verify( - ¶ms, - &aggr_vk_all, - &attributes, - &signatures, - Some(&[]) - ) - .is_err()); - assert!(aggregate_signatures_and_verify( - ¶ms, - &aggr_vk_all, - &attributes, - &signatures, - Some(&[1, 2]), - ) - .is_err()); - } - - #[test] - fn signature_aggregation_doesnt_work_for_non_unique_indices() { - let signatures = vec![random_signature(), random_signature()]; - let params = Parameters::new(2).unwrap(); - random_scalars_refs!(attributes, params, 2); - let keypairs = ttp_keygen(¶ms, 3, 5).unwrap(); - let (_, vks): (Vec<_>, Vec<_>) = keypairs - .into_iter() - .map(|keypair| { - ( - keypair.secret_key().clone(), - keypair.verification_key().clone(), - ) - }) - .unzip(); - let aggr_vk_all = aggregate_verification_keys(&vks, None).unwrap(); - - assert!(aggregate_signatures_and_verify( - ¶ms, - &aggr_vk_all, - &attributes, - &signatures, - Some(&[1, 1]), - ) - .is_err()); - } - - // TODO: test for aggregating non-threshold keys -} diff --git a/common/nymcoconut/src/scheme/double_use.rs b/common/nymcoconut/src/scheme/double_use.rs deleted file mode 100644 index f0a6a1eed3..0000000000 --- a/common/nymcoconut/src/scheme/double_use.rs +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2022-2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::error::{CoconutError, Result}; -use crate::traits::{Base58, Bytable}; -use crate::utils::try_deserialize_g2_projective; -use bls12_381::{G2Affine, G2Projective}; -use group::Curve; - -use std::fmt::{Debug, Formatter}; -use std::ops::Deref; - -#[derive(PartialEq, Eq, Clone, Copy)] -pub struct BlindedSerialNumber(G2Projective); - -// use custom Debug implementation to show base58 encoding (rather than raw curve elements) -impl Debug for BlindedSerialNumber { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("BlindedSerialNumber") - .field(&self.to_bs58()) - .finish() - } -} - -impl From for BlindedSerialNumber { - fn from(value: G2Projective) -> Self { - BlindedSerialNumber(value) - } -} - -impl From for BlindedSerialNumber { - fn from(value: G2Affine) -> Self { - BlindedSerialNumber(value.into()) - } -} - -impl Deref for BlindedSerialNumber { - type Target = G2Projective; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl TryFrom<&[u8]> for BlindedSerialNumber { - type Error = CoconutError; - - fn try_from(bytes: &[u8]) -> Result { - if bytes.len() != 96 { - return Err( - CoconutError::Deserialization( - format!("Tried to deserialize blinded serial number with incorrect number of bytes, expected 96, got {}", bytes.len()), - )); - } - - // safety: we've just made a check for 96 bytes - #[allow(clippy::unwrap_used)] - let inner = try_deserialize_g2_projective( - &bytes.try_into().unwrap(), - CoconutError::Deserialization( - "failed to deserialize the blinded serial number (zeta)".to_string(), - ), - )?; - - Ok(BlindedSerialNumber(inner)) - } -} - -impl Bytable for BlindedSerialNumber { - fn to_byte_vec(&self) -> Vec { - self.0.to_affine().to_compressed().to_vec() - } - - fn try_from_byte_slice(slice: &[u8]) -> Result { - Self::try_from(slice) - } -} - -impl Base58 for BlindedSerialNumber {} diff --git a/common/nymcoconut/src/scheme/issuance.rs b/common/nymcoconut/src/scheme/issuance.rs deleted file mode 100644 index 24a4c85bbf..0000000000 --- a/common/nymcoconut/src/scheme/issuance.rs +++ /dev/null @@ -1,660 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use std::ops::Neg; - -use bls12_381::{multi_miller_loop, G1Affine, G1Projective, G2Prepared, Scalar}; -use group::{Curve, Group, GroupEncoding}; - -use crate::error::{CoconutError, Result}; -use crate::proofs::ProofCmCs; -use crate::scheme::keygen::VerificationKey; -use crate::scheme::setup::Parameters; -use crate::scheme::BlindedSignature; -use crate::scheme::SecretKey; -use crate::Attribute; -use crate::Signature; - -// TODO: possibly completely remove those two functions. -// They only exist to have a simpler and smaller code snippets to test -// basic functionalities. -use crate::traits::{Base58, Bytable}; -use crate::utils::{hash_g1, try_deserialize_g1_projective}; - -// TODO NAMING: double check this one -// Lambda -#[derive(Debug)] -#[cfg_attr(test, derive(PartialEq, Eq))] -pub struct BlindSignRequest { - // cm - commitment: G1Projective, - // h - commitment_hash: G1Projective, - // c - private_attributes_commitments: Vec, - // pi_s - pi_s: ProofCmCs, -} - -impl TryFrom<&[u8]> for BlindSignRequest { - type Error = CoconutError; - - fn try_from(bytes: &[u8]) -> Result { - if bytes.len() < 48 + 48 + 8 + 48 { - return Err(CoconutError::DeserializationMinLength { - min: 48 + 48 + 8 + 48, - actual: bytes.len(), - }); - } - - let mut j = 0; - let commitment_bytes_len = 48; - let commitment_hash_bytes_len = 48; - - // safety: we made bound check and we're using constant offest - #[allow(clippy::unwrap_used)] - let cm_bytes = bytes[..j + commitment_bytes_len].try_into().unwrap(); - let commitment = try_deserialize_g1_projective( - &cm_bytes, - CoconutError::Deserialization( - "Failed to deserialize compressed commitment".to_string(), - ), - )?; - j += commitment_bytes_len; - - // safety: we made bound check and we're using constant offest - #[allow(clippy::unwrap_used)] - let cm_hash_bytes = bytes[j..j + commitment_hash_bytes_len].try_into().unwrap(); - let commitment_hash = try_deserialize_g1_projective( - &cm_hash_bytes, - CoconutError::Deserialization( - "Failed to deserialize compressed commitment hash".to_string(), - ), - )?; - j += commitment_hash_bytes_len; - - // safety: we made bound check and we're using constant offest - #[allow(clippy::unwrap_used)] - let c_len = u64::from_le_bytes(bytes[j..j + 8].try_into().unwrap()); - j += 8; - if bytes[j..].len() < c_len as usize * 48 { - return Err(CoconutError::DeserializationMinLength { - min: c_len as usize * 48, - actual: bytes[56..].len(), - }); - } - - let mut private_attributes_commitments = Vec::with_capacity(c_len as usize); - for i in 0..c_len as usize { - let start = j + i * 48; - let end = start + 48; - - if bytes.len() < end { - return Err(CoconutError::Deserialization( - "Failed to deserialize compressed commitment".to_string(), - )); - } - - // safety: we made bound check and we're using constant offest - #[allow(clippy::unwrap_used)] - let private_attributes_commitment_bytes = bytes[start..end].try_into().unwrap(); - let private_attributes_commitment = try_deserialize_g1_projective( - &private_attributes_commitment_bytes, - CoconutError::Deserialization( - "Failed to deserialize compressed commitment".to_string(), - ), - )?; - - private_attributes_commitments.push(private_attributes_commitment) - } - - let pi_s = ProofCmCs::from_bytes(&bytes[j + c_len as usize * 48..])?; - - Ok(BlindSignRequest { - commitment, - commitment_hash, - private_attributes_commitments, - pi_s, - }) - } -} - -impl Bytable for BlindSignRequest { - fn to_byte_vec(&self) -> Vec { - let cm_bytes = self.commitment.to_affine().to_compressed(); - let cm_hash_bytes = self.commitment_hash.to_affine().to_compressed(); - let c_len = self.private_attributes_commitments.len() as u64; - let proof_bytes = self.pi_s.to_bytes(); - - let mut bytes = Vec::with_capacity(48 + 48 + 8 + c_len as usize * 48 + proof_bytes.len()); - - bytes.extend_from_slice(&cm_bytes); - bytes.extend_from_slice(&cm_hash_bytes); - bytes.extend_from_slice(&c_len.to_le_bytes()); - for c in &self.private_attributes_commitments { - bytes.extend_from_slice(&c.to_affine().to_compressed()); - } - - bytes.extend_from_slice(&proof_bytes); - - bytes - } - - fn try_from_byte_slice(slice: &[u8]) -> Result { - BlindSignRequest::from_bytes(slice) - } -} - -impl Base58 for BlindSignRequest {} - -impl BlindSignRequest { - fn verify_proof(&self, params: &Parameters, public_attributes: &[&Attribute]) -> bool { - self.pi_s.verify( - params, - &self.commitment, - &self.private_attributes_commitments, - public_attributes, - ) - } - - pub fn verify_commitment_hash(&self, public_attributes: &[&Attribute]) -> bool { - self.commitment_hash == compute_hash(self.commitment, public_attributes) - } - - pub fn get_commitment_hash(&self) -> G1Projective { - self.commitment_hash - } - - pub fn get_private_attributes_pedersen_commitments(&self) -> &[G1Projective] { - &self.private_attributes_commitments - } - - pub fn to_bytes(&self) -> Vec { - self.to_byte_vec() - } - - pub fn from_bytes(bytes: &[u8]) -> Result { - BlindSignRequest::try_from(bytes) - } - - pub fn num_private_attributes(&self) -> usize { - self.private_attributes_commitments.len() - } -} - -pub fn compute_attributes_commitment( - params: &Parameters, - private_attributes: &[&Attribute], - public_attributes: &[&Attribute], - hs: &[G1Affine], -) -> (Scalar, G1Projective) { - let commitment_opening = params.random_scalar(); - - // Produces h0 ^ m0 * h1^m1 * .... * hn^mn - // where m0, m1, ...., mn are attributes - let attr_cm = private_attributes - .iter() - .chain(public_attributes.iter()) - .zip(hs) - .map(|(&m, h)| h * m) - .sum::(); - - // Produces g1^r * h0 ^ m0 * h1^m1 * .... * hn^mn - let commitment = params.gen1() * commitment_opening + attr_cm; - (commitment_opening, commitment) -} - -pub fn compute_pedersen_commitments_for_private_attributes( - params: &Parameters, - private_attributes: &[&Attribute], - h: &G1Projective, -) -> (Vec, Vec) { - // Generate openings for Pedersen commitment for each private attribute - let commitments_openings = params.n_random_scalars(private_attributes.len()); - - // Compute Pedersen commitment for each private attribute - let pedersen_commitments = commitments_openings - .iter() - .zip(private_attributes.iter()) - .map(|(o_j, &m_j)| params.gen1() * o_j + h * m_j) - .collect::>(); - - (commitments_openings, pedersen_commitments) -} - -pub fn compute_hash(commitment: G1Projective, public_attributes: &[&Attribute]) -> G1Projective { - let mut buff = Vec::new(); - buff.extend_from_slice(commitment.to_bytes().as_ref()); - for attr in public_attributes { - buff.extend_from_slice(attr.to_bytes().as_ref()); - } - hash_g1(buff) -} - -/// Builds cryptographic material required for blind sign. -pub fn prepare_blind_sign( - params: &Parameters, - private_attributes: &[&Attribute], - public_attributes: &[&Attribute], -) -> Result<(Vec, BlindSignRequest)> { - if private_attributes.is_empty() { - return Err(CoconutError::Issuance( - "Tried to prepare blind sign request for an empty set of private attributes" - .to_string(), - )); - } - - let hs = params.gen_hs(); - if private_attributes.len() + public_attributes.len() > hs.len() { - return Err(CoconutError::IssuanceMaxAttributes { - max: hs.len(), - requested: private_attributes.len() + public_attributes.len(), - }); - } - - let mut commitment_hash; - let mut commitment; - let mut commitment_opening; - - loop { - // Compute the attributes commitment - let (c_opening, c) = - compute_attributes_commitment(params, private_attributes, public_attributes, hs); - commitment_opening = c_opening; - commitment = c; - - // Compute the commitment hash - commitment_hash = compute_hash(commitment, public_attributes); - - // Check if the commitment hash is not the identity point - if !bool::from(commitment_hash.is_identity()) { - break; - } - } - - let (pedersen_commitments_openings, pedersen_commitments) = - compute_pedersen_commitments_for_private_attributes( - params, - private_attributes, - &commitment_hash, - ); - - let pi_s = ProofCmCs::construct( - params, - &commitment, - &commitment_opening, - &pedersen_commitments, - &pedersen_commitments_openings, - private_attributes, - public_attributes, - ); - - Ok(( - pedersen_commitments_openings, - BlindSignRequest { - commitment, - commitment_hash, - private_attributes_commitments: pedersen_commitments, - pi_s, - }, - )) -} - -pub fn blind_sign( - params: &Parameters, - signing_secret_key: &SecretKey, - blind_sign_request: &BlindSignRequest, - public_attributes: &[&Attribute], -) -> Result { - let num_private = blind_sign_request.private_attributes_commitments.len(); - let hs = params.gen_hs(); - - if num_private + public_attributes.len() > hs.len() { - return Err(CoconutError::IssuanceMaxAttributes { - max: hs.len(), - requested: num_private + public_attributes.len(), - }); - } - - // Verify the commitment hash - let h = compute_hash(blind_sign_request.commitment, public_attributes); - if bool::from(blind_sign_request.commitment_hash.is_identity()) { - return Err(CoconutError::Issuance( - "Commitment hash should not be an identity point".to_string(), - )); - } - if !(h == blind_sign_request.commitment_hash) { - return Err(CoconutError::Issuance( - "Failed to verify the commitment hash".to_string(), - )); - } - - // Verify the ZK proof - if !blind_sign_request.verify_proof(params, public_attributes) { - return Err(CoconutError::Issuance( - "Failed to verify the proof of knowledge".to_string(), - )); - } - - // in python implementation there are n^2 G1 multiplications, let's do it with a single one instead. - // i.e. compute h ^ (pub_m[0] * y[m + 1] + ... + pub_m[n] * y[m + n]) directly (where m is number of PRIVATE attributes) - // rather than ((h ^ pub_m[0]) ^ y[m + 1] , (h ^ pub_m[1]) ^ y[m + 2] , ...).sum() separately - let signed_public = h * public_attributes - .iter() - .zip(signing_secret_key.ys.iter().skip(num_private)) - .map(|(&attr, yi)| attr * yi) - .sum::(); - - // h ^ x + c[0] ^ y[0] + ... c[m] ^ y[m] + h ^ (pub_m[0] * y[m + 1] + ... + pub_m[n] * y[m + n]) - let sig = blind_sign_request - .private_attributes_commitments - .iter() - .zip(signing_secret_key.ys.iter()) - .map(|(c, yi)| c * yi) - .chain(std::iter::once(h * signing_secret_key.x)) - .chain(std::iter::once(signed_public)) - .sum(); - - Ok(BlindedSignature(h, sig)) -} - -/// Verifies a partial blind signature using the provided parameters and validator's verification key. -/// -/// # Arguments -/// -/// * `params` - A reference to the cryptographic parameters. -/// * `blind_sign_request` - A reference to the blind signature request signed by the client. -/// * `public_attributes` - A reference to the public attributes included in the client's request. -/// * `blind_sig` - A reference to the issued partial blinded signature to be verified. -/// * `partial_verification_key` - A reference to the validator's partial verification key. -/// -/// # Returns -/// -/// A boolean indicating whether the partial blind signature is valid (`true`) or not (`false`). -/// -/// # Remarks -/// -/// This function verifies the correctness and validity of a partial blind signature using -/// the provided cryptographic parameters, blind signature request, blinded signature, -/// and partial verification key. -/// It calculates pairings based on the provided values and checks whether the partial blind signature -/// is consistent with the verification key and commitments in the blind signature request. -/// The function returns `true` if the partial blind signature is valid, and `false` otherwise. -pub fn verify_partial_blind_signature( - params: &Parameters, - private_attribute_commitments: &[G1Projective], - public_attributes: &[&Attribute], - blind_sig: &BlindedSignature, - partial_verification_key: &VerificationKey, -) -> bool { - let num_private_attributes = private_attribute_commitments.len(); - if num_private_attributes + public_attributes.len() > partial_verification_key.beta_g2.len() { - return false; - } - if bool::from(blind_sig.0.is_identity()) { - return false; - } - - // TODO: we're losing some memory here due to extra allocation, - // but worst-case scenario (given SANE amount of attributes), it's just few kb at most - let c_neg = blind_sig.1.to_affine().neg(); - let g2_prep = params.prepared_miller_g2(); - - let mut terms = vec![ - // (c^{-1}, g2) - (c_neg, g2_prep.clone()), - // (s, alpha) - ( - blind_sig.0.to_affine(), - G2Prepared::from(partial_verification_key.alpha.to_affine()), - ), - ]; - - // for each private attribute, add (cm_i, beta_i) to the miller terms - for (private_attr_commit, beta_g2) in private_attribute_commitments - .iter() - .zip(&partial_verification_key.beta_g2) - { - // (cm_i, beta_i) - terms.push(( - private_attr_commit.to_affine(), - G2Prepared::from(beta_g2.to_affine()), - )) - } - - // for each public attribute, add (s^pub_j, beta_{priv + j}) to the miller terms - for (&pub_attr, beta_g2) in public_attributes.iter().zip( - partial_verification_key - .beta_g2 - .iter() - .skip(num_private_attributes), - ) { - // (s^pub_j, beta_j) - terms.push(( - (blind_sig.0 * pub_attr).to_affine(), - G2Prepared::from(beta_g2.to_affine()), - )) - } - - // get the references to all the terms to get the arguments the miller loop expects - #[allow(clippy::map_identity)] - let terms_refs = terms.iter().map(|(g1, g2)| (g1, g2)).collect::>(); - - // since checking whether e(a, b) == e(c, d) - // is equivalent to checking e(a, b) • e(c, d)^{-1} == id - // and thus to e(a, b) • e(c^{-1}, d) == id - // - // compute e(c^{-1}, g2) • e(s, alpha) • e(cm_0, beta_0) • e(cm_i, beta_i) • (s^pub_0, beta_{i+1}) (s^pub_j, beta_{i + j}) - multi_miller_loop(&terms_refs) - .final_exponentiation() - .is_identity() - .into() -} - -/// Creates a Coconut Signature under a given secret key on a set of public attributes only. -pub fn sign(secret_key: &SecretKey, public_attributes: &[&Attribute]) -> Result { - if public_attributes.len() > secret_key.ys.len() { - return Err(CoconutError::IssuanceMaxAttributes { - max: secret_key.ys.len(), - requested: public_attributes.len(), - }); - } - - //Serialize the array structure of the public attributes into a byte array - let mut serialized_attributes = Vec::new(); - //Prepend the length of the entire array (in bytes) - let array_len = public_attributes.len() as u64; - serialized_attributes.extend_from_slice(&array_len.to_le_bytes()); - //Serialize each attribute with its length - for &attribute in public_attributes.iter() { - let attr_bytes = attribute.to_bytes(); - let attr_len = attr_bytes.len() as u64; - - // Prefix the attribute with its length - serialized_attributes.extend_from_slice(&attr_len.to_le_bytes()); - serialized_attributes.extend_from_slice(&attr_bytes); - } - - //Hash the resulting byte array to derive the point H - let h = hash_g1(serialized_attributes); - - // x + m0 * y0 + m1 * y1 + ... mn * yn - let exponent = secret_key.x - + public_attributes - .iter() - .zip(secret_key.ys.iter()) - .map(|(&m_i, y_i)| m_i * y_i) - .sum::(); - - let sig2 = h * exponent; - Ok(Signature(h, sig2)) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::scheme::keygen::keygen; - use crate::tests::helpers::random_scalars_refs; - - #[test] - fn blind_sign_request_bytes_roundtrip() { - // 0 public and 1 private attribute - let params = Parameters::new(1).unwrap(); - random_scalars_refs!(private_attributes, params, 1); - random_scalars_refs!(public_attributes, params, 0); - - let (_commitments_openings, lambda) = - prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap(); - - let bytes = lambda.to_bytes(); - assert_eq!( - BlindSignRequest::try_from(bytes.as_slice()).unwrap(), - lambda - ); - - // 2 public and 2 private attributes - let params = Parameters::new(4).unwrap(); - random_scalars_refs!(private_attributes, params, 2); - random_scalars_refs!(public_attributes, params, 2); - - let (_commitments_openings, lambda) = - prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap(); - - let bytes = lambda.to_bytes(); - assert_eq!( - BlindSignRequest::try_from(bytes.as_slice()).unwrap(), - lambda - ); - } - - #[test] - fn test_prepare_blind_sign_non_identity_commitment_hash() { - let params = Parameters::new(1).unwrap(); - random_scalars_refs!(private_attributes, params, 1); - random_scalars_refs!(public_attributes, params, 0); - - // Call the function to prepare the blind sign - let result = prepare_blind_sign(¶ms, &private_attributes, &public_attributes); - - // Ensure the result is Ok - assert!(result.is_ok(), "prepare_blind_sign should succeed"); - - let (_, blind_sign_request) = result.unwrap(); - - // Ensure the commitment_hash is not the identity point - assert!( - !bool::from(blind_sign_request.commitment_hash.is_identity()), - "commitment_hash should not be the identity point" - ); - } - - #[test] - fn test_blind_sign_with_identity_commitment_hash() { - let params = Parameters::new(1).unwrap(); - random_scalars_refs!(private_attributes, params, 1); - random_scalars_refs!(public_attributes, params, 0); - - // Call the function to prepare the blind sign - let (_commitments_openings, blind_sign_request) = - prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap(); - let blind_sign_request = BlindSignRequest { - commitment_hash: G1Projective::identity(), - ..blind_sign_request // This copies the other fields from the existing instance - }; - - let signing_secret_key = SecretKey { - x: params.random_scalar(), - ys: vec![params.random_scalar()], - }; - - // Call blind_sign and ensure it returns an error due to identity commitment hash - let result = blind_sign( - ¶ms, - &signing_secret_key, - &blind_sign_request, - &public_attributes, - ); - - // The result should be an error - assert!( - result.is_err(), - "blind_sign should return an error when commitment_hash is the identity point" - ); - } - - #[test] - fn successful_verify_partial_blind_signature() { - let params = Parameters::new(4).unwrap(); - random_scalars_refs!(private_attributes, params, 2); - random_scalars_refs!(public_attributes, params, 2); - - let (_commitments_openings, request) = - prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap(); - - let validator_keypair = keygen(¶ms); - let blind_sig = blind_sign( - ¶ms, - validator_keypair.secret_key(), - &request, - &public_attributes, - ) - .unwrap(); - - assert!(verify_partial_blind_signature( - ¶ms, - &request.private_attributes_commitments, - &public_attributes, - &blind_sig, - validator_keypair.verification_key() - )); - } - - #[test] - fn successful_verify_partial_blind_signature_no_public_attributes() { - let params = Parameters::new(4).unwrap(); - random_scalars_refs!(private_attributes, params, 2); - - let (_commitments_openings, request) = - prepare_blind_sign(¶ms, &private_attributes, &[]).unwrap(); - - let validator_keypair = keygen(¶ms); - let blind_sig = blind_sign(¶ms, validator_keypair.secret_key(), &request, &[]).unwrap(); - - assert!(verify_partial_blind_signature( - ¶ms, - &request.private_attributes_commitments, - &[], - &blind_sig, - validator_keypair.verification_key() - )); - } - - #[test] - fn fail_verify_partial_blind_signature_with_wrong_key() { - let params = Parameters::new(4).unwrap(); - random_scalars_refs!(private_attributes, params, 2); - random_scalars_refs!(public_attributes, params, 2); - - let (_commitments_openings, request) = - prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap(); - - let validator_keypair = keygen(¶ms); - let validator2_keypair = keygen(¶ms); - let blind_sig = blind_sign( - ¶ms, - validator_keypair.secret_key(), - &request, - &public_attributes, - ) - .unwrap(); - - // this assertion should fail, as we try to verify with a wrong validator key - assert!(!verify_partial_blind_signature( - ¶ms, - &request.private_attributes_commitments, - &public_attributes, - &blind_sig, - validator2_keypair.verification_key() - ),); - } -} diff --git a/common/nymcoconut/src/scheme/keygen.rs b/common/nymcoconut/src/scheme/keygen.rs deleted file mode 100644 index f80c131143..0000000000 --- a/common/nymcoconut/src/scheme/keygen.rs +++ /dev/null @@ -1,722 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use core::borrow::Borrow; -use core::iter::Sum; -use core::ops::{Add, Mul}; - -use bls12_381::{G1Projective, G2Projective, Scalar}; -use group::Curve; -use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; -use serde_derive::{Deserialize, Serialize}; - -use crate::error::{CoconutError, Result}; -use crate::scheme::aggregation::aggregate_verification_keys; -use crate::scheme::setup::Parameters; -use crate::scheme::SignerIndex; -use crate::traits::Bytable; -use crate::utils::{ - try_deserialize_g1_projective, try_deserialize_g2_projective, try_deserialize_scalar, - try_deserialize_scalar_vec, Polynomial, -}; -use crate::Base58; - -#[derive(Debug)] -#[cfg_attr(test, derive(PartialEq, Eq, Clone))] -#[cfg_attr( - feature = "key-zeroize", - derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop) -)] -pub struct SecretKey { - pub(crate) x: Scalar, - pub(crate) ys: Vec, -} - -impl PemStorableKey for SecretKey { - type Error = CoconutError; - - fn pem_type() -> &'static str { - "COCONUT SECRET KEY" - } - - fn to_bytes(&self) -> Vec { - self.to_bytes() - } - - fn from_bytes(bytes: &[u8]) -> std::result::Result { - Self::from_bytes(bytes) - } -} - -impl TryFrom<&[u8]> for SecretKey { - type Error = CoconutError; - - fn try_from(bytes: &[u8]) -> Result { - // There should be x and at least one y - if bytes.len() < 32 * 2 + 8 || (bytes.len() - 8) % 32 != 0 { - return Err(CoconutError::DeserializationInvalidLength { - actual: bytes.len(), - modulus_target: bytes.len() - 8, - target: 32 * 2 + 8, - modulus: 32, - object: "secret key".to_string(), - }); - } - - // this conversion will not fail as we are taking the same length of data - #[allow(clippy::unwrap_used)] - let x_bytes: [u8; 32] = bytes[..32].try_into().unwrap(); - #[allow(clippy::unwrap_used)] - let ys_len = u64::from_le_bytes(bytes[32..40].try_into().unwrap()); - let actual_ys_len = (bytes.len() - 40) / 32; - - if ys_len as usize != actual_ys_len { - return Err(CoconutError::Deserialization(format!( - "Tried to deserialize secret key with inconsistent ys len (expected {ys_len}, got {actual_ys_len})" - ))); - } - - let x = try_deserialize_scalar( - &x_bytes, - CoconutError::Deserialization("Failed to deserialize secret key scalar".to_string()), - )?; - let ys = try_deserialize_scalar_vec( - ys_len, - &bytes[40..], - CoconutError::Deserialization("Failed to deserialize secret key scalars".to_string()), - )?; - - Ok(SecretKey { x, ys }) - } -} - -impl SecretKey { - /// Following a (distributed) key generation process, scalar values can be obtained - /// outside of the normal key generation process. - pub fn create_from_raw(x: Scalar, ys: Vec) -> Self { - Self { x, ys } - } - - /// Extract the Scalar copy of the underlying secrets. - /// The caller of this function must exercise extreme care to not misuse the data and ensuring it gets zeroized - pub fn hazmat_to_raw(&self) -> (Scalar, Vec) { - (self.x, self.ys.clone()) - } - - pub fn size(&self) -> usize { - self.ys.len() - } - - /// Derive verification key using this secret key. - pub fn verification_key(&self, params: &Parameters) -> VerificationKey { - let g1 = params.gen1(); - let g2 = params.gen2(); - VerificationKey { - alpha: g2 * self.x, - beta_g1: self.ys.iter().map(|y| g1 * y).collect(), - beta_g2: self.ys.iter().map(|y| g2 * y).collect(), - } - } - - // x || ys.len() || ys - pub fn to_bytes(&self) -> Vec { - let ys_len = self.ys.len(); - let mut bytes = Vec::with_capacity(8 + (ys_len + 1) * 32); - - bytes.extend_from_slice(&self.x.to_bytes()); - bytes.extend_from_slice(&ys_len.to_le_bytes()); - for y in self.ys.iter() { - bytes.extend_from_slice(&y.to_bytes()) - } - bytes - } - - pub fn from_bytes(bytes: &[u8]) -> Result { - SecretKey::try_from(bytes) - } -} - -impl Bytable for SecretKey { - fn to_byte_vec(&self) -> Vec { - self.to_bytes() - } - - fn try_from_byte_slice(slice: &[u8]) -> Result { - SecretKey::try_from(slice) - } -} - -impl Base58 for SecretKey {} - -// TODO: perhaps change points to affine representation -// to make verification slightly more efficient? -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct VerificationKey { - // TODO add gen2 as per the paper or imply it from the fact library is using bls381? - pub(crate) alpha: G2Projective, - pub(crate) beta_g1: Vec, - pub(crate) beta_g2: Vec, -} - -impl PemStorableKey for VerificationKey { - type Error = CoconutError; - - fn pem_type() -> &'static str { - "COCONUT VERIFICATION KEY" - } - - fn to_bytes(&self) -> Vec { - self.to_bytes() - } - - fn from_bytes(bytes: &[u8]) -> std::result::Result { - Self::from_bytes(bytes) - } -} - -impl TryFrom<&[u8]> for VerificationKey { - type Error = CoconutError; - - fn try_from(bytes: &[u8]) -> Result { - // There should be at least alpha, one betaG1 and one betaG2 and their length - if bytes.len() < 96 * 2 + 48 + 8 || (bytes.len() - 8 - 96) % (96 + 48) != 0 { - return Err(CoconutError::DeserializationInvalidLength { - actual: bytes.len(), - modulus_target: bytes.len() - 8 - 96, - target: 96 * 2 + 48 + 8, - modulus: 96 + 48, - object: "verification key".to_string(), - }); - } - - // this conversion will not fail as we are taking the same length of data - #[allow(clippy::unwrap_used)] - let alpha_bytes: [u8; 96] = bytes[..96].try_into().unwrap(); - #[allow(clippy::unwrap_used)] - let betas_len = u64::from_le_bytes(bytes[96..104].try_into().unwrap()); - - let actual_betas_len = (bytes.len() - 104) / (96 + 48); - - if betas_len as usize != actual_betas_len { - return Err( - CoconutError::Deserialization( - format!("Tried to deserialize verification key with inconsistent betas len (expected {betas_len}, got {actual_betas_len})" - ))); - } - - let alpha = try_deserialize_g2_projective( - &alpha_bytes, - CoconutError::Deserialization( - "Failed to deserialize verification key G2 point (alpha)".to_string(), - ), - )?; - - let mut beta_g1 = Vec::with_capacity(betas_len as usize); - let mut beta_g1_end: u64 = 0; - for i in 0..betas_len { - let start = (104 + i * 48) as usize; - let end = start + 48; - // we're using a constant 48 byte offset (which is the size of G1 compressed) so unwrap is fine - #[allow(clippy::unwrap_used)] - let beta_i_bytes = bytes[start..end].try_into().unwrap(); - let beta_i = try_deserialize_g1_projective( - &beta_i_bytes, - CoconutError::Deserialization( - "Failed to deserialize verification key G1 point (beta)".to_string(), - ), - )?; - - beta_g1_end = end as u64; - beta_g1.push(beta_i) - } - - let mut beta_g2 = Vec::with_capacity(betas_len as usize); - for i in 0..betas_len { - let start = (beta_g1_end + i * 96) as usize; - let end = start + 96; - // we're using a constant 96 byte offset (which is the size of G2 compressed) so unwrap is fine - #[allow(clippy::unwrap_used)] - let beta_i_bytes = bytes[start..end].try_into().unwrap(); - let beta_i = try_deserialize_g2_projective( - &beta_i_bytes, - CoconutError::Deserialization( - "Failed to deserialize verification key G2 point (beta)".to_string(), - ), - )?; - - beta_g2.push(beta_i) - } - - Ok(VerificationKey { - alpha, - beta_g1, - beta_g2, - }) - } -} - -impl<'b> Add<&'b VerificationKey> for VerificationKey { - type Output = VerificationKey; - - #[inline] - fn add(self, rhs: &'b VerificationKey) -> VerificationKey { - // If you're trying to add two keys together that were created - // for different number of attributes, just panic as it's a - // nonsense operation. - assert_eq!( - self.beta_g1.len(), - rhs.beta_g1.len(), - "trying to add verification keys generated for different number of attributes [G1]" - ); - - assert_eq!( - self.beta_g2.len(), - rhs.beta_g2.len(), - "trying to add verification keys generated for different number of attributes [G2]" - ); - - assert_eq!( - self.beta_g1.len(), - self.beta_g2.len(), - "this key is incorrect - the number of elements G1 and G2 does not match" - ); - - assert_eq!( - rhs.beta_g1.len(), - rhs.beta_g2.len(), - "they key you want to add is incorrect - the number of elements G1 and G2 does not match" - ); - - VerificationKey { - alpha: self.alpha + rhs.alpha, - beta_g1: self - .beta_g1 - .iter() - .zip(rhs.beta_g1.iter()) - .map(|(self_beta_g1, rhs_beta_g1)| self_beta_g1 + rhs_beta_g1) - .collect(), - beta_g2: self - .beta_g2 - .iter() - .zip(rhs.beta_g2.iter()) - .map(|(self_beta_g2, rhs_beta_g2)| self_beta_g2 + rhs_beta_g2) - .collect(), - } - } -} - -impl Mul for &VerificationKey { - type Output = VerificationKey; - - #[inline] - fn mul(self, rhs: Scalar) -> Self::Output { - VerificationKey { - alpha: self.alpha * rhs, - beta_g1: self.beta_g1.iter().map(|b_i| b_i * rhs).collect(), - beta_g2: self.beta_g2.iter().map(|b_i| b_i * rhs).collect(), - } - } -} - -impl Sum for VerificationKey -where - T: Borrow, -{ - #[inline] - fn sum(iter: I) -> Self - where - I: Iterator, - { - let mut peekable = iter.peekable(); - let head_attributes = match peekable.peek() { - Some(head) => head.borrow().beta_g2.len(), - None => { - // TODO: this is a really weird edge case. You're trying to sum an EMPTY iterator - // of VerificationKey. So should it panic here or just return some nonsense value? - return VerificationKey::identity(0); - } - }; - - peekable.fold(VerificationKey::identity(head_attributes), |acc, item| { - acc + item.borrow() - }) - } -} - -impl VerificationKey { - /// Create a (kinda) identity verification key using specified - /// number of 'beta' elements - pub(crate) fn identity(beta_size: usize) -> Self { - VerificationKey { - alpha: G2Projective::identity(), - beta_g1: vec![G1Projective::identity(); beta_size], - beta_g2: vec![G2Projective::identity(); beta_size], - } - } - - pub fn aggregate(sigs: &[Self], indices: Option<&[SignerIndex]>) -> Result { - aggregate_verification_keys(sigs, indices) - } - - pub fn alpha(&self) -> &G2Projective { - &self.alpha - } - - pub fn beta_g1(&self) -> &Vec { - &self.beta_g1 - } - - pub fn beta_g2(&self) -> &Vec { - &self.beta_g2 - } - - pub fn to_bytes(&self) -> Vec { - let beta_g1_len = self.beta_g1.len(); - let beta_g2_len = self.beta_g2.len(); - let mut bytes = Vec::with_capacity(96 + 8 + beta_g1_len * 48 + beta_g2_len * 96); - - bytes.extend_from_slice(&self.alpha.to_affine().to_compressed()); - - bytes.extend_from_slice(&beta_g1_len.to_le_bytes()); - - for beta_g1 in self.beta_g1.iter() { - bytes.extend_from_slice(&beta_g1.to_affine().to_compressed()) - } - - for beta_g2 in self.beta_g2.iter() { - bytes.extend_from_slice(&beta_g2.to_affine().to_compressed()) - } - - bytes - } - - pub fn from_bytes(bytes: &[u8]) -> Result { - VerificationKey::try_from(bytes) - } -} - -impl Bytable for VerificationKey { - fn to_byte_vec(&self) -> Vec { - self.to_bytes() - } - - fn try_from_byte_slice(slice: &[u8]) -> Result { - VerificationKey::try_from(slice) - } -} - -impl Base58 for VerificationKey {} - -#[derive(Debug, Clone)] -pub struct VerificationKeyShare { - pub key: VerificationKey, - pub index: SignerIndex, -} - -impl From<(VerificationKey, SignerIndex)> for VerificationKeyShare { - fn from(value: (VerificationKey, SignerIndex)) -> Self { - VerificationKeyShare { - key: value.0, - index: value.1, - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Eq, Clone))] -pub struct KeyPair { - secret_key: SecretKey, - verification_key: VerificationKey, - - /// Optional index value specifying polynomial point used during threshold key generation. - pub index: Option, -} - -impl From for (SecretKey, VerificationKey) { - fn from(value: KeyPair) -> Self { - (value.secret_key, value.verification_key) - } -} - -impl PemStorableKeyPair for KeyPair { - type PrivatePemKey = SecretKey; - type PublicPemKey = VerificationKey; - - fn private_key(&self) -> &Self::PrivatePemKey { - &self.secret_key - } - - fn public_key(&self) -> &Self::PublicPemKey { - &self.verification_key - } - - fn from_keys(secret_key: Self::PrivatePemKey, verification_key: Self::PublicPemKey) -> Self { - Self::from_keys(secret_key, verification_key) - } -} - -impl KeyPair { - const MARKER_BYTES: &'static [u8] = b"coconutkeypair"; - - pub fn from_keys(secret_key: SecretKey, verification_key: VerificationKey) -> Self { - Self { - secret_key, - verification_key, - index: None, - } - } - - pub fn secret_key(&self) -> &SecretKey { - &self.secret_key - } - - pub fn verification_key(&self) -> &VerificationKey { - &self.verification_key - } - - pub fn to_verification_key_share(&self) -> Option { - self.index.map(|index| VerificationKeyShare { - key: self.verification_key.clone(), - index, - }) - } - - pub fn to_bytes(&self) -> Vec { - // Schema is coconutkeypair[14]|secret_key_len[8]|secret_key[secret_key_len]|verification_key_len[8]|verification_key[verification_key_len]|signer_index[8] - optional - self.to_byte_vec() - } - - pub fn from_bytes(bytes: &[u8]) -> Result { - KeyPair::try_from_byte_slice(bytes) - } -} - -impl Bytable for KeyPair { - fn to_byte_vec(&self) -> Vec { - // Schema is coconutkeypair[14]|secret_key_len[8]|secret_key[secret_key_len]|verification_key_len[8]|verification_key[verification_key_len]|signer_index[8] - optional - let mut byts = vec![]; - let secret_key_bytes = self.secret_key.to_bytes(); - let secret_key_len = (secret_key_bytes.len() as u64).to_le_bytes(); - let verification_key_bytes = self.verification_key.to_bytes(); - let verification_key_len = (verification_key_bytes.len() as u64).to_le_bytes(); - byts.extend_from_slice(Self::MARKER_BYTES); - byts.extend_from_slice(&secret_key_len); - byts.extend_from_slice(&secret_key_bytes); - byts.extend_from_slice(&verification_key_len); - byts.extend_from_slice(&verification_key_bytes); - if let Some(index) = self.index { - byts.extend_from_slice(&index.to_le_bytes()) - } - byts - } - - fn try_from_byte_slice(slice: &[u8]) -> Result { - KeyPair::try_from(slice) - } -} - -impl Base58 for KeyPair {} - -impl TryFrom<&[u8]> for KeyPair { - type Error = CoconutError; - - fn try_from(bytes: &[u8]) -> Result { - let header_len = Self::MARKER_BYTES.len(); - - // we must be able to at the very least read the length of secret key which is past the header - // and is 8 bytes long - if bytes.len() < header_len + 8 { - return Err(CoconutError::DeserializationMinLength { - min: header_len + 8, - actual: bytes.len(), - }); - } - - // safety: we made bound check and we're using constant offest - #[allow(clippy::unwrap_used)] - let secret_key_len = - u64::from_le_bytes(bytes[header_len..header_len + 8].try_into().unwrap()) as usize; - let secret_key_start = header_len + 8; - - let secret_key = - SecretKey::try_from(&bytes[secret_key_start..secret_key_start + secret_key_len])?; - - // we must be able to read the length of verification key - if bytes.len() < secret_key_start + secret_key_len + 8 { - return Err(CoconutError::DeserializationMinLength { - min: secret_key_start + secret_key_len + 8, - actual: bytes.len(), - }); - } - - // safety: we made bound check - #[allow(clippy::unwrap_used)] - let verification_key_len = u64::from_le_bytes( - bytes[secret_key_start + secret_key_len..secret_key_start + secret_key_len + 8] - .try_into() - .unwrap(), - ) as usize; - let verification_key_start = secret_key_start + secret_key_len + 8; - - let verification_key = VerificationKey::try_from( - &bytes[verification_key_start..verification_key_start + verification_key_len], - )?; - let consumed_bytes = verification_key_start + verification_key_len; - let index = if consumed_bytes < bytes.len() && [consumed_bytes..].len() == 8 { - #[allow(clippy::unwrap_used)] - Some(u64::from_le_bytes( - bytes[consumed_bytes..consumed_bytes + 8] - .try_into() - .unwrap(), - )) - } else { - None - }; - Ok(KeyPair { - secret_key, - verification_key, - index, - }) - } -} - -/// Generate a single Coconut keypair ((x, y0, y1...), (g2^x, g2^y0, ...)). -/// -/// It is not suitable for threshold credentials as all subsequent calls to `keygen` generate keys -/// that are independent of each other. -pub fn keygen(params: &Parameters) -> KeyPair { - let attributes = params.gen_hs().len(); - - let x = params.random_scalar(); - let ys = params.n_random_scalars(attributes); - - let secret_key = SecretKey { x, ys }; - let verification_key = secret_key.verification_key(params); - - KeyPair { - secret_key, - verification_key, - index: None, - } -} - -/// Generate Coconut keypairs. -/// -/// Generate a set of n Coconut keypairs [((x, y0, y1...), (g2^x, g2^y0, ...)), ...], -/// such that they support threshold aggregation by `threshold` number of parties. -/// It is expected that this procedure is executed by a Trusted Third Party. -pub fn ttp_keygen( - params: &Parameters, - threshold: u64, - num_authorities: u64, -) -> Result> { - if threshold == 0 { - return Err(CoconutError::Setup( - "Tried to generate threshold keys with a 0 threshold value".to_string(), - )); - } - - if threshold > num_authorities { - return Err( - CoconutError::Setup( - "Tried to generate threshold keys for threshold value being higher than number of the signing authorities".to_string(), - )); - } - - let attributes = params.gen_hs().len(); - - // generate polynomials - let v = Polynomial::new_random(params, threshold - 1); - let ws = (0..attributes) - .map(|_| Polynomial::new_random(params, threshold - 1)) - .collect::>(); - - // TODO: potentially if we had some known authority identifier we could use that instead - // of the increasing (1,2,3,...) sequence - let polynomial_indices = (1..=num_authorities).collect::>(); - - // generate polynomial shares - let x = polynomial_indices - .iter() - .map(|&id| v.evaluate(&Scalar::from(id))); - let ys = polynomial_indices.iter().map(|&id| { - ws.iter() - .map(|w| w.evaluate(&Scalar::from(id))) - .collect::>() - }); - - // finally set the keys - let secret_keys = x.zip(ys).map(|(x, ys)| SecretKey { x, ys }); - - let keypairs = secret_keys - .zip(polynomial_indices.iter()) - .map(|(secret_key, index)| { - let verification_key = secret_key.verification_key(params); - KeyPair { - secret_key, - verification_key, - index: Some(*index), - } - }) - .collect(); - - Ok(keypairs) -} - -#[cfg(test)] -mod tests { - use crate::scheme::setup::setup; - - use super::*; - - #[test] - fn keypair_bytes_roundtrip() { - let params1 = setup(1).unwrap(); - let params5 = setup(5).unwrap(); - - let keypair1 = keygen(¶ms1); - let keypair5 = keygen(¶ms5); - - let bytes1 = keypair1.to_bytes(); - let bytes5 = keypair5.to_bytes(); - - assert_eq!(KeyPair::from_bytes(&bytes1).unwrap(), keypair1); - assert_eq!(KeyPair::from_bytes(&bytes5).unwrap(), keypair5); - } - - #[test] - fn secret_key_bytes_roundtrip() { - let params1 = setup(1).unwrap(); - let params5 = setup(5).unwrap(); - - let keypair1 = keygen(¶ms1); - let keypair5 = keygen(¶ms5); - - let bytes1 = keypair1.secret_key.to_bytes(); - let bytes5 = keypair5.secret_key.to_bytes(); - - assert_eq!(SecretKey::from_bytes(&bytes1).unwrap(), keypair1.secret_key); - assert_eq!(SecretKey::from_bytes(&bytes5).unwrap(), keypair5.secret_key); - } - - #[test] - fn verification_key_bytes_roundtrip() { - let params1 = setup(1).unwrap(); - let params5 = setup(5).unwrap(); - - let keypair1 = &keygen(¶ms1); - let keypair5 = &keygen(¶ms5); - - let bytes1: Vec = keypair1.verification_key.to_bytes(); - let bytes5: Vec = keypair5.verification_key.to_bytes(); - - assert_eq!( - VerificationKey::try_from(bytes1.as_slice()).unwrap(), - keypair1.verification_key - ); - assert_eq!( - VerificationKey::try_from(bytes5.as_slice()).unwrap(), - keypair5.verification_key - ); - } -} diff --git a/common/nymcoconut/src/scheme/mod.rs b/common/nymcoconut/src/scheme/mod.rs deleted file mode 100644 index b811b6e250..0000000000 --- a/common/nymcoconut/src/scheme/mod.rs +++ /dev/null @@ -1,672 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -// TODO: implement https://crates.io/crates/signature traits? - -use bls12_381::{G1Projective, G2Prepared, G2Projective, Scalar}; -use group::Curve; - -pub use keygen::{SecretKey, VerificationKey}; - -use crate::error::{CoconutError, Result}; -use crate::scheme::setup::Parameters; -use crate::scheme::verification::check_bilinear_pairing; -use crate::traits::{Base58, Bytable}; -use crate::utils::try_deserialize_g1_projective; -use crate::Attribute; - -pub mod aggregation; -pub mod double_use; -pub mod issuance; -pub mod keygen; -pub mod setup; -pub mod verification; - -pub type SignerIndex = u64; - -// (h, s) -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Signature(pub(crate) G1Projective, pub(crate) G1Projective); - -pub type PartialSignature = Signature; - -impl TryFrom<&[u8]> for Signature { - type Error = CoconutError; - - fn try_from(bytes: &[u8]) -> Result { - if bytes.len() != 96 { - return Err(CoconutError::Deserialization(format!( - "Signature must be exactly 96 bytes, got {}", - bytes.len() - ))); - } - - // safety: we just checked for the length so the unwraps are fine - #[allow(clippy::expect_used)] - let sig1_bytes: &[u8; 48] = &bytes[..48].try_into().expect("Slice size != 48"); - #[allow(clippy::expect_used)] - let sig2_bytes: &[u8; 48] = &bytes[48..].try_into().expect("Slice size != 48"); - - let sig1 = try_deserialize_g1_projective( - sig1_bytes, - CoconutError::Deserialization("Failed to deserialize compressed sig1".to_string()), - )?; - - let sig2 = try_deserialize_g1_projective( - sig2_bytes, - CoconutError::Deserialization("Failed to deserialize compressed sig2".to_string()), - )?; - - Ok(Signature(sig1, sig2)) - } -} - -impl Signature { - pub(crate) fn sig1(&self) -> &G1Projective { - &self.0 - } - - pub(crate) fn sig2(&self) -> &G1Projective { - &self.1 - } - - pub fn randomise_simple(&self, params: &Parameters) -> Signature { - let r = params.random_scalar(); - Signature(self.0 * r, self.1 * r) - } - - pub fn randomise(&self, params: &Parameters) -> (Signature, Scalar) { - let r = params.random_scalar(); - let r_prime = params.random_scalar(); - let h_prime = self.0 * r_prime; - let s_prime = (self.1 * r_prime) + (h_prime * r); - (Signature(h_prime, s_prime), r) - } - - pub fn to_bytes(self) -> [u8; 96] { - let mut bytes = [0u8; 96]; - bytes[..48].copy_from_slice(&self.0.to_affine().to_compressed()); - bytes[48..].copy_from_slice(&self.1.to_affine().to_compressed()); - bytes - } - - pub fn from_bytes(bytes: &[u8]) -> Result { - Signature::try_from(bytes) - } - - pub fn verify( - &self, - params: &Parameters, - partial_verification_key: &VerificationKey, - private_attributes: &[&Attribute], - public_attributes: &[&Attribute], - commitment_hash: &G1Projective, - ) -> Result<()> { - // Verify the commitment hash - if bool::from(self.0.is_identity()) { - return Err(CoconutError::Verification( - "Commitment hash should not be an identity point".to_string(), - )); - } - if !(commitment_hash == &self.0) { - return Err(CoconutError::Verification( - "Verification of commitment hash from signature failed".to_string(), - )); - } - - let alpha = partial_verification_key.alpha; - - let signed_attributes = private_attributes - .iter() - .chain(public_attributes.iter()) - .zip(partial_verification_key.beta_g2.iter()) - .map(|(&attr, beta_i)| beta_i * attr) - .sum::(); - - // Verify the signature share - if !check_bilinear_pairing( - &self.0.to_affine(), - &G2Prepared::from((alpha + signed_attributes).to_affine()), - &self.1.to_affine(), - params.prepared_miller_g2(), - ) { - return Err(CoconutError::Unblind( - "Verification of signature share failed".to_string(), - )); - } - - Ok(()) - } -} - -impl Bytable for Signature { - fn to_byte_vec(&self) -> Vec { - self.to_bytes().to_vec() - } - - fn try_from_byte_slice(slice: &[u8]) -> Result { - Signature::from_bytes(slice) - } -} - -impl Base58 for Signature {} - -#[derive(Debug, PartialEq, Eq)] -pub struct BlindedSignature(G1Projective, G1Projective); - -impl Bytable for BlindedSignature { - fn to_byte_vec(&self) -> Vec { - self.to_bytes().to_vec() - } - - fn try_from_byte_slice(slice: &[u8]) -> Result { - Self::from_bytes(slice) - } -} - -impl Base58 for BlindedSignature {} - -impl TryFrom<&[u8]> for BlindedSignature { - type Error = CoconutError; - - fn try_from(bytes: &[u8]) -> Result { - if bytes.len() != 96 { - return Err(CoconutError::Deserialization(format!( - "BlindedSignature must be exactly 96 bytes, got {}", - bytes.len() - ))); - } - - // safety: we just checked for the length so the unwraps are fine - #[allow(clippy::expect_used)] - let h_bytes: &[u8; 48] = &bytes[..48].try_into().expect("Slice size != 48"); - #[allow(clippy::expect_used)] - let sig_bytes: &[u8; 48] = &bytes[48..].try_into().expect("Slice size != 48"); - - let h = try_deserialize_g1_projective( - h_bytes, - CoconutError::Deserialization("Failed to deserialize compressed h".to_string()), - )?; - let sig = try_deserialize_g1_projective( - sig_bytes, - CoconutError::Deserialization("Failed to deserialize compressed sig".to_string()), - )?; - - Ok(BlindedSignature(h, sig)) - } -} - -impl BlindedSignature { - pub fn unblind( - &self, - partial_verification_key: &VerificationKey, - pedersen_commitments_openings: &[Scalar], - ) -> Signature { - // parse the signature - let h = &self.0; - let c = &self.1; - let blinding_removers = partial_verification_key - .beta_g1 - .iter() - .zip(pedersen_commitments_openings.iter()) - .map(|(beta, opening)| beta * opening) - .sum::(); - - let unblinded_c = c - blinding_removers; - - Signature(*h, unblinded_c) - } - - pub fn unblind_and_verify( - &self, - params: &Parameters, - partial_verification_key: &VerificationKey, - private_attributes: &[&Attribute], - public_attributes: &[&Attribute], - commitment_hash: &G1Projective, - pedersen_commitments_openings: &[Scalar], - ) -> Result { - let unblinded = self.unblind(partial_verification_key, pedersen_commitments_openings); - unblinded.verify( - params, - partial_verification_key, - private_attributes, - public_attributes, - commitment_hash, - )?; - Ok(unblinded) - } - - pub fn to_bytes(&self) -> [u8; 96] { - let mut bytes = [0u8; 96]; - bytes[..48].copy_from_slice(&self.0.to_affine().to_compressed()); - bytes[48..].copy_from_slice(&self.1.to_affine().to_compressed()); - bytes - } - - pub fn from_bytes(bytes: &[u8]) -> Result { - BlindedSignature::try_from(bytes) - } -} - -// perhaps this should take signature by reference? we'll see how it goes -#[derive(Clone, Copy)] -pub struct SignatureShare { - signature: Signature, - index: SignerIndex, -} - -impl From<(Signature, SignerIndex)> for SignatureShare { - fn from(value: (Signature, SignerIndex)) -> Self { - SignatureShare { - signature: value.0, - index: value.1, - } - } -} - -impl SignatureShare { - pub fn new(signature: Signature, index: SignerIndex) -> Self { - SignatureShare { signature, index } - } - - pub fn signature(&self) -> &Signature { - &self.signature - } - - pub fn index(&self) -> SignerIndex { - self.index - } - - // pub fn aggregate(shares: &[Self]) -> Result { - // aggregate_signature_shares(shares) - // } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::hash_to_scalar; - use crate::scheme::aggregation::{ - aggregate_signatures_and_verify, aggregate_verification_keys, - }; - use crate::scheme::issuance::{blind_sign, compute_hash, prepare_blind_sign, sign}; - use crate::scheme::keygen::{keygen, ttp_keygen}; - use crate::scheme::verification::{prove_bandwidth_credential, verify, verify_credential}; - use crate::tests::helpers::random_scalars_refs; - - #[test] - fn unblind_returns_error_if_integrity_check_on_commitment_hash_fails() { - let params = Parameters::new(2).unwrap(); - random_scalars_refs!(private_attributes, params, 2); - - let (_commitments_openings, lambda) = - prepare_blind_sign(¶ms, &private_attributes, &[]).unwrap(); - - let keypair1 = keygen(¶ms); - - let sig1 = blind_sign(¶ms, keypair1.secret_key(), &lambda, &[]).unwrap(); - - let wrong_commitment_opening = params.random_scalar(); - let wrong_commitment = params.gen1() * wrong_commitment_opening; - let fake_commitment_hash = compute_hash(wrong_commitment, &[]); - let wrong_commitments_openings = params.n_random_scalars(private_attributes.len()); - - assert!(sig1 - .unblind_and_verify( - ¶ms, - keypair1.verification_key(), - &private_attributes, - &[], - &fake_commitment_hash, - &wrong_commitments_openings, - ) - .is_err()); - } - - #[test] - fn unblind_returns_error_if_signature_verification_fails() { - let params = Parameters::new(2).unwrap(); - let p = [hash_to_scalar("Attribute1"), hash_to_scalar("Attribute2")]; - let private_attributes = vec![&p[0], &p[1]]; - - let p2 = [hash_to_scalar("Attribute3"), hash_to_scalar("Attribute4")]; - let private_attributes2 = vec![&p2[0], &p2[1]]; - - let (commitments_openings, lambda) = - prepare_blind_sign(¶ms, &private_attributes, &[]).unwrap(); - - let keypair1 = keygen(¶ms); - - let sig1 = blind_sign(¶ms, keypair1.secret_key(), &lambda, &[]).unwrap(); - - assert!(sig1 - .unblind_and_verify( - ¶ms, - keypair1.verification_key(), - &private_attributes2, - &[], - &lambda.get_commitment_hash(), - &commitments_openings, - ) - .is_err()); - } - - #[test] - fn verification_on_two_private_attributes() { - let params = Parameters::new(2).unwrap(); - let serial_number = params.random_scalar(); - let binding_number = params.random_scalar(); - let private_attributes = vec![&serial_number, &binding_number]; - - let keypair1 = keygen(¶ms); - let keypair2 = keygen(¶ms); - - let (commitments_openings, lambda) = - prepare_blind_sign(¶ms, &private_attributes, &[]).unwrap(); - - let sig1 = blind_sign(¶ms, keypair1.secret_key(), &lambda, &[]) - .unwrap() - .unblind_and_verify( - ¶ms, - keypair1.verification_key(), - &private_attributes, - &[], - &lambda.get_commitment_hash(), - &commitments_openings, - ) - .unwrap(); - - let sig2 = blind_sign(¶ms, keypair2.secret_key(), &lambda, &[]) - .unwrap() - .unblind_and_verify( - ¶ms, - keypair2.verification_key(), - &private_attributes, - &[], - &lambda.get_commitment_hash(), - &commitments_openings, - ) - .unwrap(); - - let theta1 = prove_bandwidth_credential( - ¶ms, - keypair1.verification_key(), - &sig1, - &serial_number, - &binding_number, - ) - .unwrap(); - - let theta2 = prove_bandwidth_credential( - ¶ms, - keypair2.verification_key(), - &sig2, - &serial_number, - &binding_number, - ) - .unwrap(); - - assert!(verify_credential( - ¶ms, - keypair1.verification_key(), - &theta1, - &[], - )); - - assert!(verify_credential( - ¶ms, - keypair2.verification_key(), - &theta2, - &[], - )); - - assert!(!verify_credential( - ¶ms, - keypair1.verification_key(), - &theta2, - &[], - )); - } - - #[test] - fn verification_on_two_public_attributes() { - let params = Parameters::new(2).unwrap(); - random_scalars_refs!(attributes, params, 2); - - let keypair1 = keygen(¶ms); - let keypair2 = keygen(¶ms); - let sig1 = sign(keypair1.secret_key(), &attributes).unwrap(); - let sig2 = sign(keypair2.secret_key(), &attributes).unwrap(); - - assert!(verify( - ¶ms, - keypair1.verification_key(), - &attributes, - &sig1, - )); - - assert!(!verify( - ¶ms, - keypair2.verification_key(), - &attributes, - &sig1, - )); - - assert!(!verify( - ¶ms, - keypair1.verification_key(), - &attributes, - &sig2, - )); - } - - #[test] - fn verification_on_two_public_and_two_private_attributes() { - let params = Parameters::new(4).unwrap(); - random_scalars_refs!(public_attributes, params, 2); - - let serial_number = params.random_scalar(); - let binding_number = params.random_scalar(); - let private_attributes = vec![&serial_number, &binding_number]; - - let keypair1 = keygen(¶ms); - let keypair2 = keygen(¶ms); - - let (commitments_openings, lambda) = - prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap(); - - let sig1 = blind_sign(¶ms, keypair1.secret_key(), &lambda, &public_attributes) - .unwrap() - .unblind_and_verify( - ¶ms, - keypair1.verification_key(), - &private_attributes, - &public_attributes, - &lambda.get_commitment_hash(), - &commitments_openings, - ) - .unwrap(); - - let sig2 = blind_sign(¶ms, keypair2.secret_key(), &lambda, &public_attributes) - .unwrap() - .unblind_and_verify( - ¶ms, - keypair2.verification_key(), - &private_attributes, - &public_attributes, - &lambda.get_commitment_hash(), - &commitments_openings, - ) - .unwrap(); - - let theta1 = prove_bandwidth_credential( - ¶ms, - keypair1.verification_key(), - &sig1, - &serial_number, - &binding_number, - ) - .unwrap(); - - let theta2 = prove_bandwidth_credential( - ¶ms, - keypair2.verification_key(), - &sig2, - &serial_number, - &binding_number, - ) - .unwrap(); - - assert!(verify_credential( - ¶ms, - keypair1.verification_key(), - &theta1, - &public_attributes, - )); - - assert!(verify_credential( - ¶ms, - keypair2.verification_key(), - &theta2, - &public_attributes, - )); - - assert!(!verify_credential( - ¶ms, - keypair1.verification_key(), - &theta2, - &public_attributes, - )); - } - - #[test] - fn verification_on_two_public_and_two_private_attributes_from_two_signers() { - let params = Parameters::new(4).unwrap(); - random_scalars_refs!(public_attributes, params, 2); - - let serial_number = params.random_scalar(); - let binding_number = params.random_scalar(); - let private_attributes = vec![&serial_number, &binding_number]; - - let keypairs = ttp_keygen(¶ms, 2, 3).unwrap(); - - let (commitments_openings, lambda) = - prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap(); - - let sigs = keypairs - .iter() - .map(|keypair| { - blind_sign(¶ms, keypair.secret_key(), &lambda, &public_attributes) - .unwrap() - .unblind_and_verify( - ¶ms, - keypair.verification_key(), - &private_attributes, - &public_attributes, - &lambda.get_commitment_hash(), - &commitments_openings, - ) - .unwrap() - }) - .collect::>(); - - let vks = keypairs - .into_iter() - .map(|keypair| keypair.verification_key().clone()) - .collect::>(); - - let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len()); - attributes.extend_from_slice(&private_attributes); - attributes.extend_from_slice(&public_attributes); - - let aggr_vk = aggregate_verification_keys(&vks[..2], Some(&[1, 2])).unwrap(); - let aggr_sig = aggregate_signatures_and_verify( - ¶ms, - &aggr_vk, - &attributes, - &sigs[..2], - Some(&[1, 2]), - ) - .unwrap(); - - let theta = prove_bandwidth_credential( - ¶ms, - &aggr_vk, - &aggr_sig, - &serial_number, - &binding_number, - ) - .unwrap(); - - assert!(verify_credential( - ¶ms, - &aggr_vk, - &theta, - &public_attributes, - )); - - // taking different subset of keys and credentials - let aggr_vk = aggregate_verification_keys(&vks[1..], Some(&[2, 3])).unwrap(); - let aggr_sig = aggregate_signatures_and_verify( - ¶ms, - &aggr_vk, - &attributes, - &sigs[1..], - Some(&[2, 3]), - ) - .unwrap(); - - let theta = prove_bandwidth_credential( - ¶ms, - &aggr_vk, - &aggr_sig, - &serial_number, - &binding_number, - ) - .unwrap(); - - assert!(verify_credential( - ¶ms, - &aggr_vk, - &theta, - &public_attributes, - )); - } - - #[test] - fn signature_bytes_roundtrip() { - let params = Parameters::default(); - let r = params.random_scalar(); - let s = params.random_scalar(); - let signature = Signature(params.gen1() * r, params.gen1() * s); - let bytes = signature.to_bytes(); - - // also make sure it is equivalent to the internal g1 compressed bytes concatenated - let expected_bytes = [ - signature.0.to_affine().to_compressed(), - signature.1.to_affine().to_compressed(), - ] - .concat(); - assert_eq!(expected_bytes, bytes); - assert_eq!(signature, Signature::try_from(&bytes[..]).unwrap()) - } - - #[test] - fn blinded_signature_bytes_roundtrip() { - let params = Parameters::default(); - let r = params.random_scalar(); - let s = params.random_scalar(); - let blinded_sig = BlindedSignature(params.gen1() * r, params.gen1() * s); - let bytes = blinded_sig.to_bytes(); - - // also make sure it is equivalent to the internal g1 compressed bytes concatenated - let expected_bytes = [ - blinded_sig.0.to_affine().to_compressed(), - blinded_sig.1.to_affine().to_compressed(), - ] - .concat(); - assert_eq!(expected_bytes, bytes); - assert_eq!(blinded_sig, BlindedSignature::try_from(&bytes[..]).unwrap()) - } -} diff --git a/common/nymcoconut/src/scheme/setup.rs b/common/nymcoconut/src/scheme/setup.rs deleted file mode 100644 index 65c994ec8f..0000000000 --- a/common/nymcoconut/src/scheme/setup.rs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use bls12_381::{G1Affine, G2Affine, G2Prepared, Scalar}; -use ff::Field; -use group::Curve; -use rand::thread_rng; - -use crate::error::{CoconutError, Result}; -use crate::utils::hash_g1; - -/// System-wide parameters used for the protocol -#[derive(Clone)] -pub struct Parameters { - /// Generator of the G1 group - g1: G1Affine, - - /// Additional generators of the G1 group - hs: Vec, - - /// Generator of the G2 group - g2: G2Affine, - - /// Precomputed G2 generator used for the miller loop - _g2_prepared_miller: G2Prepared, -} - -impl Parameters { - pub fn new(num_attributes: u32) -> Result { - if num_attributes == 0 { - return Err(CoconutError::Setup( - "Tried to setup the scheme for 0 attributes".to_string(), - )); - } - - let hs = (1..=num_attributes) - .map(|i| hash_g1(format!("h{i}")).to_affine()) - .collect(); - - Ok(Parameters { - g1: G1Affine::generator(), - hs, - g2: G2Affine::generator(), - _g2_prepared_miller: G2Prepared::from(G2Affine::generator()), - }) - } - - pub fn gen1(&self) -> &G1Affine { - &self.g1 - } - - pub fn gen2(&self) -> &G2Affine { - &self.g2 - } - - pub(crate) fn prepared_miller_g2(&self) -> &G2Prepared { - &self._g2_prepared_miller - } - - pub fn gen_hs(&self) -> &[G1Affine] { - &self.hs - } - - pub fn random_scalar(&self) -> Scalar { - // lazily-initialized thread-local random number generator, seeded by the system - let mut rng = thread_rng(); - Scalar::random(&mut rng) - } - - pub fn n_random_scalars(&self, n: usize) -> Vec { - (0..n).map(|_| self.random_scalar()).collect() - } -} - -pub fn setup(num_attributes: u32) -> Result { - Parameters::new(num_attributes) -} - -// for ease of use in tests requiring params -// TODO: not sure if this will have to go away when tests require some specific number of generators -#[cfg(test)] -impl Default for Parameters { - fn default() -> Self { - Parameters { - g1: G1Affine::generator(), - hs: Vec::new(), - g2: G2Affine::generator(), - _g2_prepared_miller: G2Prepared::from(G2Affine::generator()), - } - } -} diff --git a/common/nymcoconut/src/scheme/verification.rs b/common/nymcoconut/src/scheme/verification.rs deleted file mode 100644 index 149c60d3e3..0000000000 --- a/common/nymcoconut/src/scheme/verification.rs +++ /dev/null @@ -1,432 +0,0 @@ -// Copyright 2021-2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::error::{CoconutError, Result}; -use crate::proofs::ProofKappaZeta; -use crate::scheme::setup::Parameters; -use crate::scheme::Signature; -use crate::scheme::VerificationKey; -use crate::traits::{Base58, Bytable}; -use crate::utils::try_deserialize_g2_projective; -use crate::Attribute; -use bls12_381::{multi_miller_loop, G1Affine, G2Prepared, G2Projective, Scalar}; -use core::ops::Neg; -use group::{Curve, Group}; - -pub use crate::scheme::double_use::BlindedSerialNumber; - -// TODO NAMING: this whole thing -// Theta -#[derive(Debug, PartialEq, Eq)] -pub struct VerifyCredentialRequest { - // blinded_message (kappa) - pub blinded_message: G2Projective, - // blinded serial number (zeta) - pub blinded_serial_number: BlindedSerialNumber, - // sigma - pub credential: Signature, - // pi_v - pub pi_v: ProofKappaZeta, -} - -impl TryFrom<&[u8]> for VerifyCredentialRequest { - type Error = CoconutError; - - fn try_from(bytes: &[u8]) -> Result { - if bytes.len() < 288 { - return Err( - CoconutError::Deserialization( - format!("Tried to deserialize theta with insufficient number of bytes, expected >= 288, got {}", bytes.len()), - )); - } - - // safety: we just checked for the length so the unwraps are fine - #[allow(clippy::unwrap_used)] - let blinded_message_bytes = bytes[..96].try_into().unwrap(); - let blinded_message = try_deserialize_g2_projective( - &blinded_message_bytes, - CoconutError::Deserialization( - "failed to deserialize the blinded message (kappa)".to_string(), - ), - )?; - - let blinded_serial_number_bytes = &bytes[96..192]; - let blinded_serial_number = - BlindedSerialNumber::try_from_byte_slice(blinded_serial_number_bytes)?; - - let credential = Signature::try_from(&bytes[192..288])?; - - let pi_v = ProofKappaZeta::from_bytes(&bytes[288..])?; - - Ok(VerifyCredentialRequest { - blinded_message, - blinded_serial_number, - credential, - pi_v, - }) - } -} - -impl VerifyCredentialRequest { - fn verify_proof(&self, params: &Parameters, verification_key: &VerificationKey) -> bool { - self.pi_v.verify( - params, - verification_key, - &self.blinded_message, - &self.blinded_serial_number, - ) - } - - pub fn has_blinded_serial_number(&self, blinded_serial_number_bs58: &str) -> Result { - let blinded_serial_number = BlindedSerialNumber::try_from_bs58(blinded_serial_number_bs58)?; - let ret = self.blinded_serial_number.eq(&blinded_serial_number); - Ok(ret) - } - - // blinded message (kappa) || blinded serial number (zeta) || credential || pi_v - pub fn to_bytes(&self) -> Vec { - let blinded_message_bytes = self.blinded_message.to_affine().to_compressed(); - let blinded_serial_number_bytes = self.blinded_serial_number.to_affine().to_compressed(); - let credential_bytes = self.credential.to_bytes(); - let proof_bytes = self.pi_v.to_bytes(); - - let mut bytes = Vec::with_capacity(288 + proof_bytes.len()); - bytes.extend_from_slice(&blinded_message_bytes); - bytes.extend_from_slice(&blinded_serial_number_bytes); - bytes.extend_from_slice(&credential_bytes); - bytes.extend_from_slice(&proof_bytes); - - bytes - } - - pub fn from_bytes(bytes: &[u8]) -> Result { - VerifyCredentialRequest::try_from(bytes) - } - - pub fn blinded_serial_number(&self) -> BlindedSerialNumber { - self.blinded_serial_number - } - - pub fn blinded_serial_number_bs58(&self) -> String { - self.blinded_serial_number.to_bs58() - } -} - -impl Bytable for VerifyCredentialRequest { - fn to_byte_vec(&self) -> Vec { - self.to_bytes() - } - - fn try_from_byte_slice(slice: &[u8]) -> Result { - VerifyCredentialRequest::try_from(slice) - } -} - -impl Base58 for VerifyCredentialRequest {} - -pub fn compute_kappa( - params: &Parameters, - verification_key: &VerificationKey, - private_attributes: &[&Attribute], - blinding_factor: Scalar, -) -> G2Projective { - params.gen2() * blinding_factor - + verification_key.alpha - + private_attributes - .iter() - .zip(verification_key.beta_g2.iter()) - .map(|(&priv_attr, beta_i)| beta_i * priv_attr) - .sum::() -} - -pub fn compute_zeta(params: &Parameters, serial_number: &Attribute) -> G2Projective { - params.gen2() * serial_number -} - -pub fn prove_bandwidth_credential( - params: &Parameters, - verification_key: &VerificationKey, - signature: &Signature, - serial_number: &Attribute, - binding_number: &Attribute, -) -> Result { - if verification_key.beta_g2.len() < 2 { - return Err( - CoconutError::Verification( - format!("Tried to prove a credential for higher than supported by the provided verification key number of attributes (max: {}, requested: 2)", - verification_key.beta_g2.len() - ))); - } - - // Randomize the signature - let (signature_prime, sign_blinding_factor) = signature.randomise(params); - - // blinded_message : kappa in the paper. - // Value kappa is needed since we want to show a signature sigma'. - // In order to verify sigma' we need both the verification key vk and the message m. - // However, we do not want to reveal m to whomever we are showing the signature. - // Thus, we need kappa which allows us to verify sigma'. In particular, - // kappa is computed on m as input, but thanks to the use or random value r, - // it does not reveal any information about m. - let private_attributes = [serial_number, binding_number]; - let blinded_message = compute_kappa( - params, - verification_key, - &private_attributes, - sign_blinding_factor, - ); - - // zeta is a commitment to the serial number (i.e., a public value associated with the serial number) - let blinded_serial_number = compute_zeta(params, serial_number); - - let pi_v = ProofKappaZeta::construct( - params, - verification_key, - serial_number, - binding_number, - &sign_blinding_factor, - &blinded_message, - &blinded_serial_number, - ); - - Ok(VerifyCredentialRequest { - blinded_message, - blinded_serial_number: blinded_serial_number.into(), - credential: signature_prime, - pi_v, - }) -} - -/// Checks whether e(P, Q) * e(-R, S) == id -pub fn check_bilinear_pairing(p: &G1Affine, q: &G2Prepared, r: &G1Affine, s: &G2Prepared) -> bool { - // checking e(P, Q) * e(-R, S) == id - // is equivalent to checking e(P, Q) == e(R, S) - // but requires only a single final exponentiation rather than two of them - // and therefore, as seen via benchmarks.rs, is almost 50% faster - // (1.47ms vs 2.45ms, tested on R9 5900X) - - let multi_miller = multi_miller_loop(&[(p, q), (&r.neg(), s)]); - multi_miller.final_exponentiation().is_identity().into() -} - -pub fn check_vk_pairing( - params: &Parameters, - dkg_values: &[G2Projective], - vk: &VerificationKey, -) -> bool { - let values_len = dkg_values.len(); - if values_len == 0 || values_len - 1 != vk.beta_g1.len() || values_len - 1 != vk.beta_g2.len() { - return false; - } - - // safety: we made an explicit check for if the length of the slice is 0, thus unwrap here is fine - #[allow(clippy::unwrap_used)] - if &vk.alpha != *dkg_values.first().as_ref().unwrap() { - return false; - } - let dkg_betas = &dkg_values[1..]; - if dkg_betas - .iter() - .zip(vk.beta_g2.iter()) - .any(|(dkg_beta, vk_beta)| dkg_beta != vk_beta) - { - return false; - } - if vk.beta_g1.iter().zip(vk.beta_g2.iter()).any(|(g1, g2)| { - !check_bilinear_pairing( - params.gen1(), - &G2Prepared::from(g2.to_affine()), - &g1.to_affine(), - params.prepared_miller_g2(), - ) - }) { - return false; - } - - true -} - -pub fn verify_credential( - params: &Parameters, - verification_key: &VerificationKey, - theta: &VerifyCredentialRequest, - public_attributes: &[&Attribute], -) -> bool { - if public_attributes.len() + theta.pi_v.private_attributes_len() - > verification_key.beta_g2.len() - { - return false; - } - - if !theta.verify_proof(params, verification_key) { - return false; - } - - let kappa = if public_attributes.is_empty() { - theta.blinded_message - } else { - let signed_public_attributes = public_attributes - .iter() - .zip( - verification_key - .beta_g2 - .iter() - .skip(theta.pi_v.private_attributes_len()), - ) - .map(|(&pub_attr, beta_i)| beta_i * pub_attr) - .sum::(); - - theta.blinded_message + signed_public_attributes - }; - - check_bilinear_pairing( - &theta.credential.0.to_affine(), - &G2Prepared::from(kappa.to_affine()), - &(theta.credential.1).to_affine(), - params.prepared_miller_g2(), - ) && !bool::from(theta.credential.0.is_identity()) -} - -// Used in tests only -pub fn verify( - params: &Parameters, - verification_key: &VerificationKey, - public_attributes: &[&Attribute], - sig: &Signature, -) -> bool { - let kappa = (verification_key.alpha - + public_attributes - .iter() - .zip(verification_key.beta_g2.iter()) - .map(|(&m_i, b_i)| b_i * m_i) - .sum::()) - .to_affine(); - - check_bilinear_pairing( - &sig.0.to_affine(), - &G2Prepared::from(kappa), - &sig.1.to_affine(), - params.prepared_miller_g2(), - ) && !bool::from(sig.0.is_identity()) -} - -#[cfg(test)] -mod tests { - use crate::scheme::issuance::sign; - use crate::scheme::keygen::keygen; - use crate::scheme::setup::setup; - - use super::*; - - #[test] - fn vk_pairing() { - let params = setup(2).unwrap(); - let keypair = keygen(¶ms); - let vk = keypair.verification_key(); - - let mut dkg_values = vec![vk.alpha]; - dkg_values.append(&mut vk.beta_g2.clone()); - assert!(check_vk_pairing(¶ms, &dkg_values, vk)); - } - - #[test] - fn theta_bytes_roundtrip() { - let params = setup(2).unwrap(); - - let keypair = keygen(¶ms); - let r = params.random_scalar(); - let s = params.random_scalar(); - - let signature = Signature(params.gen1() * r, params.gen1() * s); - let serial_number = params.random_scalar(); - let binding_number = params.random_scalar(); - - let theta = prove_bandwidth_credential( - ¶ms, - keypair.verification_key(), - &signature, - &serial_number, - &binding_number, - ) - .unwrap(); - - let bytes = theta.to_bytes(); - assert_eq!( - VerifyCredentialRequest::try_from(bytes.as_slice()).unwrap(), - theta - ); - } - - #[test] - fn reject_forged_signature_via_linear_combination() { - // This test checks if the protocol correctly rejects forged signatures created - // by linear combinations of valid signatures. The verification for forged - // signatures should fail. - let params = Parameters::new(4).unwrap(); - - let scalar_2 = Scalar::one() + Scalar::one(); - let scalar_2_inv = Scalar::invert(&scalar_2).unwrap(); - - //#1 - let a = params.random_scalar(); - let zero = Scalar::zero(); - let a_zero = vec![&a, &zero]; - let zero_a = vec![&zero, &a]; - - let validator_keypair = keygen(¶ms); - - //#2 - let sig_a_zero = sign(validator_keypair.secret_key(), &a_zero).unwrap(); - let sig_zero_a = sign(validator_keypair.secret_key(), &zero_a).unwrap(); - - assert!(verify( - ¶ms, - validator_keypair.verification_key(), - &a_zero, - &sig_a_zero - )); - assert!(verify( - ¶ms, - validator_keypair.verification_key(), - &zero_a, - &sig_zero_a - )); - - //#3 - let h0 = sig_a_zero.0; - // Removed unnecessary references - let h1 = scalar_2_inv * sig_a_zero.1 + scalar_2_inv * sig_zero_a.1; - let forged_signature = Signature(h0, h1); - let a_half = a * scalar_2_inv; - let new_plaintext = vec![&a_half, &a_half]; - - // The forged signature should not pass verification - assert!(!verify( - ¶ms, - validator_keypair.verification_key(), - &new_plaintext, - &forged_signature - )); - - //#4 - let scalar_3 = Scalar::one() + Scalar::one() + Scalar::one(); - let scalar_4 = Scalar::one() + Scalar::one() + Scalar::one() + Scalar::one(); - let scalar_4_inv = Scalar::invert(&scalar_4).unwrap(); - let scalar_3_over_4 = scalar_3 * scalar_4_inv; - - // Removed unnecessary references - let h1_2 = scalar_4_inv * sig_a_zero.1 + scalar_3_over_4 * sig_zero_a.1; - let forged_signature_2 = Signature(h0, h1_2); - let a_quarter = a * scalar_4_inv; - let a_3_over_4 = a * scalar_3_over_4; - let new_plaintext_2 = vec![&a_quarter, &a_3_over_4]; - - // The second forged signature should also not pass verification - assert!(!verify( - ¶ms, - validator_keypair.verification_key(), - &new_plaintext_2, - &forged_signature_2 - )); - } -} diff --git a/common/nymcoconut/src/tests/e2e.rs b/common/nymcoconut/src/tests/e2e.rs deleted file mode 100644 index b9cc397845..0000000000 --- a/common/nymcoconut/src/tests/e2e.rs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2022 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::random_scalars_refs; -use crate::tests::helpers::tests::generate_dkg_keys; -use crate::{ - aggregate_verification_keys, setup, tests::helpers::*, ttp_keygen, verify_credential, - CoconutError, VerificationKey, -}; - -#[test] -fn keygen() -> Result<(), CoconutError> { - let params = setup(5)?; - let node_indices = vec![15u64, 248, 33521]; - - random_scalars_refs!(public_attributes, params, 2); - - // generate_keys - let coconut_keypairs = ttp_keygen(¶ms, 2, 3)?; - - let verification_keys: Vec = coconut_keypairs - .iter() - .map(|keypair| keypair.verification_key().clone()) - .collect(); - - // aggregate verification keys - let verification_key = aggregate_verification_keys(&verification_keys, Some(&node_indices))?; - - // Generate cryptographic material to verify them - let theta = theta_from_keys_and_attributes( - ¶ms, - &coconut_keypairs, - &node_indices, - &public_attributes, - )?; - - // Verify credentials - assert!(verify_credential( - ¶ms, - &verification_key, - &theta, - &public_attributes, - )); - - Ok(()) -} - -#[test] -#[ignore] // expensive test -fn dkg() -> Result<(), CoconutError> { - let params = setup(5)?; - let node_indices = vec![15u64, 248, 33521]; - - random_scalars_refs!(public_attributes, params, 2); - - // generate_keys - let coconut_keypairs = generate_dkg_keys(5, &node_indices); - - let verification_keys: Vec = coconut_keypairs - .iter() - .map(|keypair| keypair.verification_key().clone()) - .collect(); - - // aggregate verification keys - let verification_key = aggregate_verification_keys(&verification_keys, Some(&node_indices))?; - - // Generate cryptographic material to verify them - let theta = theta_from_keys_and_attributes( - ¶ms, - &coconut_keypairs, - &node_indices, - &public_attributes, - )?; - - // Verify credentials - assert!(verify_credential( - ¶ms, - &verification_key, - &theta, - &public_attributes, - )); - - Ok(()) -} diff --git a/common/nymcoconut/src/tests/helpers.rs b/common/nymcoconut/src/tests/helpers.rs deleted file mode 100644 index f01081bbf1..0000000000 --- a/common/nymcoconut/src/tests/helpers.rs +++ /dev/null @@ -1,186 +0,0 @@ -// Copyright 2022 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::*; -use itertools::izip; -use std::fmt::Debug; - -// unwraps are fine in the test code -#[allow(clippy::unwrap_used)] -pub fn theta_from_keys_and_attributes( - params: &Parameters, - coconut_keypairs: &Vec, - indices: &[scheme::SignerIndex], - public_attributes: &[&PublicAttribute], -) -> Result { - let serial_number = params.random_scalar(); - let binding_number = params.random_scalar(); - let private_attributes = vec![&serial_number, &binding_number]; - - // generate commitment - let (commitments_openings, blind_sign_request) = - prepare_blind_sign(params, &private_attributes, public_attributes)?; - - let verification_keys: Vec = coconut_keypairs - .iter() - .map(|keypair| keypair.verification_key().clone()) - .collect(); - - // aggregate verification keys - let verification_key = aggregate_verification_keys(&verification_keys, Some(indices))?; - - // generate blinded signatures - let mut blinded_signatures = Vec::new(); - - for keypair in coconut_keypairs { - let blinded_signature = blind_sign( - params, - keypair.secret_key(), - &blind_sign_request, - public_attributes, - )?; - blinded_signatures.push(blinded_signature) - } - - // Unblind - let unblinded_signatures: Vec<(scheme::SignerIndex, Signature)> = izip!( - indices.iter(), - blinded_signatures.iter(), - verification_keys.iter() - ) - .map(|(idx, s, vk)| { - ( - *idx, - s.unblind_and_verify( - params, - vk, - &private_attributes, - public_attributes, - &blind_sign_request.get_commitment_hash(), - &commitments_openings, - ) - .unwrap(), - ) - }) - .collect(); - - // Aggregate signatures - let signature_shares: Vec = unblinded_signatures - .iter() - .map(|(idx, signature)| SignatureShare::new(*signature, *idx)) - .collect(); - - let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len()); - attributes.extend_from_slice(&private_attributes); - attributes.extend_from_slice(public_attributes); - - // Randomize credentials and generate any cryptographic material to verify them - let signature = aggregate_signature_shares_and_verify( - params, - &verification_key, - &attributes, - &signature_shares, - )?; - - // Generate cryptographic material to verify them - let theta = prove_bandwidth_credential( - params, - &verification_key, - &signature, - &serial_number, - &binding_number, - )?; - - Ok(theta) -} - -// unwraps are fine in the test code -#[allow(clippy::unwrap_used)] -pub fn transpose_matrix(matrix: Vec>) -> Vec> { - if matrix.is_empty() { - return vec![]; - } - let len = matrix[0].len(); - let mut iters: Vec<_> = matrix.into_iter().map(|d| d.into_iter()).collect(); - (0..len) - .map(|_| { - iters - .iter_mut() - .map(|it| it.next().unwrap()) - .collect::>() - }) - .collect::>() -} - -#[macro_export] -macro_rules! random_scalars_refs { - ( $x: ident, $params: expr, $n: expr ) => { - let _vec = $params.n_random_scalars($n); - #[allow(clippy::map_identity)] - let $x = _vec.iter().collect::>(); - }; -} - -pub use random_scalars_refs; - -#[cfg(test)] -pub mod tests { - use super::*; - use bls12_381::Scalar; - use nym_dkg::{bte::decrypt_share, combine_shares, Dealing, NodeIndex}; - use rand_chacha::rand_core::SeedableRng; - - pub fn generate_dkg_secrets(node_indices: &[NodeIndex]) -> Vec { - let dummy_seed = [42u8; 32]; - let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); - let params = nym_dkg::bte::setup(); - - // the simplest possible case - let threshold = 2; - - let mut receivers = std::collections::BTreeMap::new(); - let mut full_keys = Vec::new(); - for index in node_indices { - let (dk, pk) = nym_dkg::bte::keygen(¶ms, &mut rng); - receivers.insert(*index, *pk.public_key()); - full_keys.push((dk, pk)) - } - let dealings = node_indices - .iter() - .map(|&dealer_index| { - Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0 - }) - .collect::>(); - let mut derived_secrets = Vec::new(); - for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() { - let shares = dealings - .iter() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) - .collect(); - - let recovered_secret = - combine_shares(shares, &receivers.keys().copied().collect::>()).unwrap(); - - derived_secrets.push(recovered_secret) - } - derived_secrets - } - pub fn generate_dkg_keys(num_attributes: u32, node_indices: &[NodeIndex]) -> Vec { - let params = Parameters::new(num_attributes).unwrap(); - let mut all_secrets = vec![]; - for _ in 0..num_attributes { - let secrets = generate_dkg_secrets(node_indices); - all_secrets.push(secrets); - } - let signers = transpose_matrix(all_secrets); - signers - .into_iter() - .map(|mut secrets| { - let x = secrets.pop().unwrap(); - let sk = SecretKey::create_from_raw(x, secrets); - let vk = sk.verification_key(¶ms); - KeyPair::from_keys(sk, vk) - }) - .collect() - } -} diff --git a/common/nymcoconut/src/tests/mod.rs b/common/nymcoconut/src/tests/mod.rs deleted file mode 100644 index c13b7a3b20..0000000000 --- a/common/nymcoconut/src/tests/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -#[cfg(test)] -mod e2e; -pub mod helpers; diff --git a/common/nymcoconut/src/traits.rs b/common/nymcoconut/src/traits.rs deleted file mode 100644 index b2aa13944c..0000000000 --- a/common/nymcoconut/src/traits.rs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2021-2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -#![warn(clippy::expect_used)] -#![warn(clippy::unwrap_used)] - -use crate::CoconutError; -use bls12_381::{G1Affine, G1Projective, Scalar}; -use group::GroupEncoding; - -pub trait Bytable -where - Self: Sized, -{ - fn to_byte_vec(&self) -> Vec; - - fn try_from_byte_slice(slice: &[u8]) -> Result; -} - -pub trait Base58 -where - Self: Bytable, -{ - fn try_from_bs58>(x: S) -> Result { - let bs58_decoded = &bs58::decode(x.as_ref()).into_vec()?; - Self::try_from_byte_slice(bs58_decoded) - } - fn to_bs58(&self) -> String { - bs58::encode(self.to_byte_vec()).into_string() - } -} - -impl Bytable for Scalar { - fn to_byte_vec(&self) -> Vec { - self.to_bytes().to_vec() - } - - fn try_from_byte_slice(slice: &[u8]) -> Result { - let received = slice.len(); - let Ok(arr) = slice.try_into() else { - return Err(CoconutError::UnexpectedArrayLength { - typ: "Scalar".to_string(), - received, - expected: 32, - }); - }; - - let maybe_scalar = Scalar::from_bytes(arr); - if maybe_scalar.is_none().into() { - Err(CoconutError::ScalarDeserializationFailure) - } else { - // safety: this unwrap is fine as we've just checked the element is not none - #[allow(clippy::unwrap_used)] - Ok(maybe_scalar.unwrap()) - } - } -} - -impl Base58 for Scalar {} - -impl Bytable for G1Projective { - fn to_byte_vec(&self) -> Vec { - self.to_bytes().as_ref().to_vec() - } - - fn try_from_byte_slice(slice: &[u8]) -> Result { - let received = slice.len(); - let arr: Result<[u8; 48], _> = slice.try_into(); - let Ok(bytes) = arr else { - return Err(CoconutError::UnexpectedArrayLength { - typ: "G1Projective".to_string(), - received, - expected: 48, - }); - }; - - let maybe_g1 = G1Affine::from_compressed(&bytes); - if maybe_g1.is_none().into() { - Err(CoconutError::G1ProjectiveDeserializationFailure) - } else { - // safety: this unwrap is fine as we've just checked the element is not none - #[allow(clippy::unwrap_used)] - Ok(maybe_g1.unwrap().into()) - } - } -} - -impl Base58 for G1Projective {} diff --git a/common/nymcoconut/src/utils.rs b/common/nymcoconut/src/utils.rs deleted file mode 100644 index d12707408c..0000000000 --- a/common/nymcoconut/src/utils.rs +++ /dev/null @@ -1,382 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use core::iter::Sum; -use core::ops::Mul; - -use bls12_381::hash_to_curve::{ExpandMsgXmd, HashToCurve, HashToField}; -use bls12_381::{G1Affine, G1Projective, G2Affine, G2Projective, Scalar}; -use ff::Field; - -use crate::error::{CoconutError, Result}; -use crate::scheme::setup::Parameters; -use crate::scheme::SignerIndex; - -pub struct Polynomial { - coefficients: Vec, -} - -impl Polynomial { - // for polynomial of degree n, we generate n+1 values - // (for example for degree 1, like y = x + 2, we need [2,1]) - pub fn new_random(params: &Parameters, degree: u64) -> Self { - Polynomial { - coefficients: params.n_random_scalars((degree + 1) as usize), - } - } - - /// Evaluates the polynomial at point x. - pub fn evaluate(&self, x: &Scalar) -> Scalar { - if self.coefficients.is_empty() { - Scalar::zero() - // if x is zero then we can ignore most of the expensive computation and - // just return the last term of the polynomial - } else if x.is_zero().into() { - // we checked that coefficients are not empty so unwrap here is fine - #[allow(clippy::unwrap_used)] - *self.coefficients.first().unwrap() - } else { - self.coefficients - .iter() - .enumerate() - // coefficient[n] * x ^ n - .map(|(i, coefficient)| coefficient * x.pow(&[i as u64, 0, 0, 0])) - .sum() - } - } -} - -#[inline] -fn generate_lagrangian_coefficients_at_origin(points: &[u64]) -> Vec { - let x = Scalar::zero(); - - points - .iter() - .enumerate() - .map(|(i, point_i)| { - let mut numerator = Scalar::one(); - let mut denominator = Scalar::one(); - let xi = Scalar::from(*point_i); - - for (j, point_j) in points.iter().enumerate() { - if j != i { - let xj = Scalar::from(*point_j); - - // numerator = (x - xs[0]) * ... * (x - xs[j]), j != i - numerator *= x - xj; - - // denominator = (xs[i] - x[0]) * ... * (xs[i] - x[j]), j != i - denominator *= xi - xj; - } - } - // numerator / denominator - numerator * denominator.invert().unwrap() - }) - .collect() -} - -/// Performs a Lagrange interpolation at the origin for a polynomial defined by `points` and `values`. -/// It can be used for Scalars, G1 and G2 points. -pub(crate) fn perform_lagrangian_interpolation_at_origin( - points: &[SignerIndex], - values: &[T], -) -> Result -where - T: Sum, - for<'a> &'a T: Mul, -{ - if points.is_empty() || values.is_empty() { - return Err(CoconutError::Interpolation( - "Tried to perform lagrangian interpolation for an empty set of coordinates".to_string(), - )); - } - - if points.len() != values.len() { - return Err(CoconutError::Interpolation( - "Tried to perform lagrangian interpolation for an incomplete set of coordinates" - .to_string(), - )); - } - - let coefficients = generate_lagrangian_coefficients_at_origin(points); - - Ok(coefficients - .into_iter() - .zip(values.iter()) - .map(|(coeff, val)| val * coeff) - .sum()) -} - -// A temporary way of hashing particular message into G1. -// Implementation idea was taken from `threshold_crypto`: -// https://github.com/poanetwork/threshold_crypto/blob/7709462f2df487ada3bb3243060504b5881f2628/src/lib.rs#L691 -// Eventually it should get replaced by, most likely, the osswu map -// method once ideally it's implemented inside the pairing crate. - -// note: I have absolutely no idea what are the correct domains for those. I just used whatever -// was given in the test vectors of `Hashing to Elliptic Curves draft-irtf-cfrg-hash-to-curve-11` - -// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#appendix-J.9.1 -const G1_HASH_DOMAIN: &[u8] = b"QUUX-V01-CS02-with-BLS12381G1_XMD:SHA-256_SSWU_RO_"; - -// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#appendix-K.1 -const SCALAR_HASH_DOMAIN: &[u8] = b"QUUX-V01-CS02-with-expander"; - -pub fn hash_g1>(msg: M) -> G1Projective { - >>::hash_to_curve(msg, G1_HASH_DOMAIN) -} - -pub fn hash_to_scalar>(msg: M) -> Scalar { - let mut output = vec![Scalar::zero()]; - - Scalar::hash_to_field::>( - msg.as_ref(), - SCALAR_HASH_DOMAIN, - &mut output, - ); - output[0] -} - -pub fn try_deserialize_scalar_vec( - expected_len: u64, - bytes: &[u8], - err: CoconutError, -) -> Result> { - if bytes.len() != expected_len as usize * 32 { - return Err(err); - } - - let mut out = Vec::with_capacity(expected_len as usize); - for i in 0..expected_len as usize { - // we just checked we have exactly the amount of bytes we need and thus the unwrap is fine - #[allow(clippy::unwrap_used)] - let s_bytes = bytes[i * 32..(i + 1) * 32].try_into().unwrap(); - let s = match Into::>::into(Scalar::from_bytes(&s_bytes)) { - None => return Err(err), - Some(scalar) => scalar, - }; - out.push(s) - } - - Ok(out) -} - -pub fn try_deserialize_scalar(bytes: &[u8; 32], err: CoconutError) -> Result { - Into::>::into(Scalar::from_bytes(bytes)).ok_or(err) -} - -pub fn try_deserialize_g1_projective(bytes: &[u8; 48], err: CoconutError) -> Result { - Into::>::into(G1Affine::from_compressed(bytes)) - .ok_or(err) - .map(G1Projective::from) -} - -pub fn try_deserialize_g2_projective(bytes: &[u8; 96], err: CoconutError) -> Result { - Into::>::into(G2Affine::from_compressed(bytes)) - .ok_or(err) - .map(G2Projective::from) -} - -// use core::fmt; -// #[cfg(feature = "serde")] -// use serde::de::Visitor; -// #[cfg(feature = "serde")] -// use serde::{self, Deserialize, Deserializer, Serialize, Serializer}; -// -// // #[cfg(feature = "serde")] -// #[serde(remote = "Scalar")] -// pub(crate) struct ScalarDef(pub Scalar); -// -// // #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -// -// impl Serialize for ScalarDef { -// fn serialize(&self, serializer: S) -> core::result::Result -// where -// S: Serializer, -// { -// use serde::ser::SerializeTuple; -// let mut tup = serializer.serialize_tuple(32)?; -// for byte in self.0.to_bytes().iter() { -// tup.serialize_element(byte)?; -// } -// tup.end() -// } -// } -// -// impl<'de> Deserialize<'de> for ScalarDef { -// fn deserialize(deserializer: D) -> core::result::Result -// where -// D: Deserializer<'de>, -// { -// struct ScalarVisitor; -// -// impl<'de> Visitor<'de> for ScalarVisitor { -// type Value = ScalarDef; -// -// fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { -// formatter.write_str("a 32-byte canonical bls12_381 scalar") -// } -// -// fn visit_seq(self, mut seq: A) -> core::result::Result -// where -// A: serde::de::SeqAccess<'de>, -// { -// let mut bytes = [0u8; 32]; -// for i in 0..32 { -// bytes[i] = seq -// .next_element()? -// .ok_or_else(|| serde::de::Error::invalid_length(i, &"expected 32 bytes"))?; -// } -// -// let res = Scalar::from_bytes(&bytes); -// if res.is_some().into() { -// Ok(ScalarDef(res.unwrap())) -// } else { -// Err(serde::de::Error::custom( -// &"scalar was not canonically encoded", -// )) -// } -// } -// } -// -// deserializer.deserialize_tuple(32, ScalarVisitor) -// } -// } -// -// #[cfg(feature = "serde")] -// pub(crate) struct G1ProjectiveSerdeHelper(Scalar); -// -// #[cfg(feature = "serde")] -// pub(crate) struct G2ProjectiveSerdeHelper(Scalar); - -#[cfg(test)] -mod tests { - use rand::RngCore; - - use super::*; - - #[test] - fn polynomial_evaluation() { - // y = 42 (it should be 42 regardless of x) - let poly = Polynomial { - coefficients: vec![Scalar::from(42)], - }; - - assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(1))); - assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(0))); - assert_eq!(Scalar::from(42), poly.evaluate(&Scalar::from(10))); - - // y = x + 10, at x = 2 (exp: 12) - let poly = Polynomial { - coefficients: vec![Scalar::from(10), Scalar::from(1)], - }; - - assert_eq!(Scalar::from(12), poly.evaluate(&Scalar::from(2))); - - // y = x^4 - 5x^2 + 2x - 3, at x = 3 (exp: 39) - let poly = Polynomial { - coefficients: vec![ - (-Scalar::from(3)), - Scalar::from(2), - (-Scalar::from(5)), - Scalar::zero(), - Scalar::from(1), - ], - }; - - assert_eq!(Scalar::from(39), poly.evaluate(&Scalar::from(3))); - - // empty polynomial - let poly = Polynomial { - coefficients: vec![], - }; - - // should always be 0 - assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(1))); - assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(0))); - assert_eq!(Scalar::from(0), poly.evaluate(&Scalar::from(10))); - } - - #[test] - fn performing_lagrangian_scalar_interpolation_at_origin() { - // x^2 + 3 - // x, f(x): - // 1, 4, - // 2, 7, - // 3, 12, - let points = vec![1, 2, 3]; - let values = vec![Scalar::from(4), Scalar::from(7), Scalar::from(12)]; - - assert_eq!( - Scalar::from(3), - perform_lagrangian_interpolation_at_origin(&points, &values).unwrap() - ); - - // x^3 + 3x^2 - 5x + 11 - // x, f(x): - // 1, 10 - // 2, 21 - // 3, 50 - // 4, 103 - let points = vec![1, 2, 3, 4]; - let values = vec![ - Scalar::from(10), - Scalar::from(21), - Scalar::from(50), - Scalar::from(103), - ]; - - assert_eq!( - Scalar::from(11), - perform_lagrangian_interpolation_at_origin(&points, &values).unwrap() - ); - - // more points than it is required - // x^2 + x + 10 - // x, f(x) - // 1, 12 - // 2, 16 - // 3, 22 - // 4, 30 - // 5, 40 - let points = vec![1, 2, 3, 4, 5]; - let values = vec![ - Scalar::from(12), - Scalar::from(16), - Scalar::from(22), - Scalar::from(30), - Scalar::from(40), - ]; - - assert_eq!( - Scalar::from(10), - perform_lagrangian_interpolation_at_origin(&points, &values).unwrap() - ); - } - - #[test] - fn hash_g1_sanity_check() { - let mut rng = rand::thread_rng(); - let mut msg1 = [0u8; 1024]; - rng.fill_bytes(&mut msg1); - let mut msg2 = [0u8; 1024]; - rng.fill_bytes(&mut msg2); - - assert_eq!(hash_g1(msg1), hash_g1(msg1)); - assert_eq!(hash_g1(msg2), hash_g1(msg2)); - assert_ne!(hash_g1(msg1), hash_g1(msg2)); - } - - #[test] - fn hash_scalar_sanity_check() { - let mut rng = rand::thread_rng(); - let mut msg1 = [0u8; 1024]; - rng.fill_bytes(&mut msg1); - let mut msg2 = [0u8; 1024]; - rng.fill_bytes(&mut msg2); - - assert_eq!(hash_to_scalar(msg1), hash_to_scalar(msg1)); - assert_eq!(hash_to_scalar(msg2), hash_to_scalar(msg2)); - assert_ne!(hash_to_scalar(msg1), hash_to_scalar(msg2)); - } -} diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 45eacae059..a6830e21c1 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -105,7 +105,6 @@ nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet nym-vesting-contract-common = { path = "../common/cosmwasm-smart-contracts/vesting-contract" } nym-contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common", features = ["naive_float", "utoipa"] } nym-multisig-contract-common = { path = "../common/cosmwasm-smart-contracts/multisig-contract" } -nym-coconut = { path = "../common/nymcoconut", features = ["key-zeroize"] } nym-sphinx = { path = "../common/nymsphinx" } nym-pemstore = { path = "../common/pemstore" } nym-task = { path = "../common/task" } diff --git a/nym-api/src/ecash/dkg/controller/keys.rs b/nym-api/src/ecash/dkg/controller/keys.rs index 474d6ab0b1..bfbaa8c28d 100644 --- a/nym-api/src/ecash/dkg/controller/keys.rs +++ b/nym-api/src/ecash/dkg/controller/keys.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::ecash::client::Client; -use crate::ecash::keys::{KeyPairWithEpoch, LegacyCoconutKeyWithEpoch}; +use crate::ecash::keys::KeyPairWithEpoch; use crate::support::{config, nyxd}; use anyhow::{anyhow, bail, Context}; use nym_coconut_dkg_common::types::{EpochId, EpochState}; @@ -43,24 +43,12 @@ pub(crate) fn load_ecash_keypair_if_exists( return Ok(None); } - // first attempt to load ecash keys directly, - // if that fails fallback to coconut keys and perform migration if let Ok(ecash_key) = nym_pemstore::load_key::(&config.storage_paths.ecash_key_path) { return Ok(Some(ecash_key)); } - if let Ok(legacy_coconut_key) = - nym_pemstore::load_key::(&config.storage_paths.ecash_key_path) - { - let migrated_key: KeyPairWithEpoch = legacy_coconut_key.into(); - nym_pemstore::store_key(&migrated_key, &config.storage_paths.ecash_key_path) - .context("migrated key storage failure")?; - - return Ok(Some(migrated_key)); - } - bail!("ecash key load failure") } diff --git a/nym-api/src/ecash/keys/mod.rs b/nym-api/src/ecash/keys/mod.rs index 87d01865e2..bf315f8b1a 100644 --- a/nym-api/src/ecash/keys/mod.rs +++ b/nym-api/src/ecash/keys/mod.rs @@ -24,23 +24,6 @@ pub struct KeyPairWithEpoch { pub(crate) issued_for_epoch: EpochId, } -impl From for KeyPairWithEpoch { - fn from(value: LegacyCoconutKeyWithEpoch) -> Self { - let (x, ys) = value.secret_key.hazmat_to_raw(); - let sk = nym_compact_ecash::SecretKeyAuth::create_from_raw(x, ys); - - KeyPairWithEpoch { - keys: sk.into(), - issued_for_epoch: value.issued_for_epoch, - } - } -} - -pub struct LegacyCoconutKeyWithEpoch { - pub(crate) secret_key: nym_coconut::SecretKey, - pub(crate) issued_for_epoch: EpochId, -} - impl KeyPairWithEpoch { pub(crate) fn new(keys: nym_compact_ecash::KeyPairAuth, issued_for_epoch: EpochId) -> Self { KeyPairWithEpoch { diff --git a/nym-api/src/ecash/keys/persistence.rs b/nym-api/src/ecash/keys/persistence.rs index 976be0dfd6..b45b8a7e2d 100644 --- a/nym-api/src/ecash/keys/persistence.rs +++ b/nym-api/src/ecash/keys/persistence.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::ecash::keys::{KeyPairWithEpoch, LegacyCoconutKeyWithEpoch}; +use crate::ecash::keys::KeyPairWithEpoch; use nym_coconut_dkg_common::types::EpochId; use nym_compact_ecash::{error::CompactEcashError, scheme::keygen::SecretKeyAuth, KeyPairAuth}; use nym_pemstore::traits::PemStorableKey; @@ -41,37 +41,3 @@ impl PemStorableKey for KeyPairWithEpoch { }) } } - -impl PemStorableKey for LegacyCoconutKeyWithEpoch { - // that's not the best error for this, but it felt like an overkill to define a dedicated struct just for this purpose - type Error = nym_coconut::CoconutError; - - fn pem_type() -> &'static str { - "COCONUT KEY WITH EPOCH" - } - - fn to_bytes(&self) -> Vec { - let mut bytes = self.issued_for_epoch.to_be_bytes().to_vec(); - bytes.append(&mut self.secret_key.to_bytes()); - bytes - } - - fn from_bytes(bytes: &[u8]) -> Result { - if bytes.len() <= mem::size_of::() { - return Err(nym_coconut::CoconutError::DeserializationMinLength { - min: mem::size_of::(), - actual: bytes.len(), - }); - } - let epoch_id = EpochId::from_be_bytes([ - bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], - ]); - - let sk = nym_coconut::SecretKey::from_bytes(&bytes[mem::size_of::()..])?; - - Ok(LegacyCoconutKeyWithEpoch { - secret_key: sk, - issued_for_epoch: epoch_id, - }) - } -} diff --git a/nym-credential-proxy/nym-credential-proxy-requests/src/client.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/client.rs index b168bca4cb..c55dd4f914 100644 --- a/nym-credential-proxy/nym-credential-proxy-requests/src/client.rs +++ b/nym-credential-proxy/nym-credential-proxy-requests/src/client.rs @@ -145,30 +145,4 @@ impl NymVpnApiClient for VpnApiClient { parse_response(res, false).await } - - // async fn get_bandwidth_voucher_blinded_shares( - // &self, - // blind_sign_request: BlindSignRequest, - // ) -> Result { - // let req = self.inner.create_post_request( - // &["/api", "/v1", "/bandwidth-voucher", "/obtain"], - // NO_PARAMS, - // &BandwidthVoucherRequest { blind_sign_request }, - // ); - // - // let fut = req.bearer_auth(&self.bearer_token).send(); - // - // // the only reason for that target lock is so that I could call this method from an ephemeral test - // // running in non-wasm mode (since I wanted to use tokio) - // - // #[cfg(target_arch = "wasm32")] - // let res = wasmtimer::tokio::timeout(std::time::Duration::from_secs(5), fut) - // .await - // .map_err(|_timeout| HttpClientError::RequestTimeout)??; - // - // #[cfg(not(target_arch = "wasm32"))] - // let res = fut.await?; - // - // parse_response(res, false).await - // } } diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/openapi.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/openapi.rs index 812392056a..d2ede701cd 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/openapi.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/openapi.rs @@ -9,48 +9,6 @@ use utoipa::openapi::security::{Http, HttpAuthScheme, SecurityScheme}; use utoipa::{Modify, OpenApi}; use utoipa_swagger_ui::SwaggerUi; -/* -#[derive(OpenApi)] -#[openapi( - info(title = "Nym VPN Api"), - paths( - api::v1::freepass::generate_freepass, - api::v1::bandwidth_voucher::obtain_bandwidth_voucher_shares, - api::v1::bandwidth_voucher::obtain_async_bandwidth_voucher_shares, - api::v1::bandwidth_voucher::current_deposit, - api::v1::bandwidth_voucher::prehashed_public_attributes, - api::v1::bandwidth_voucher::partial_verification_keys, - api::v1::bandwidth_voucher::master_verification_key, - api::v1::bandwidth_voucher::current_epoch, - api::v1::bandwidth_voucher::shares::query_for_shares_by_id, - ), - components( - schemas( - api::Output, - api::OutputParams, - api_requests::v1::ErrorResponse, - api_requests::v1::freepass::models::FreepassCredentialResponse, - api_requests::v1::freepass::models::FreepassQueryParams, - api_requests::v1::bandwidth_voucher::models::DepositResponse, - api_requests::v1::bandwidth_voucher::models::AttributesResponse, - api_requests::v1::bandwidth_voucher::models::BandwidthVoucherResponse, - api_requests::v1::bandwidth_voucher::models::BandwidthVoucherAsyncResponse, - api_requests::v1::bandwidth_voucher::models::PartialVerificationKeysResponse, - api_requests::v1::bandwidth_voucher::models::CurrentEpochResponse, - api_requests::v1::bandwidth_voucher::models::CredentialShare, - api_requests::v1::bandwidth_voucher::models::PartialVerificationKey, - api_requests::v1::bandwidth_voucher::models::MasterVerificationKeyResponse, - api_requests::v1::bandwidth_voucher::models::BandwidthVoucherAsyncRequest, - api_requests::v1::bandwidth_voucher::models::BandwidthVoucherRequest, - api_requests::v1::bandwidth_voucher::models::BlindSignRequestJsonSchemaWrapper - ), - responses(RequestError), - ), - modifiers(&SecurityAddon), -)] -pub(crate) struct ApiDoc; - */ - #[derive(OpenApi)] #[openapi( info(title = "Nym Credential Proxy Api"), diff --git a/wasm/zknym-lib/Cargo.toml b/wasm/zknym-lib/Cargo.toml index 75a203b727..572e26c6bd 100644 --- a/wasm/zknym-lib/Cargo.toml +++ b/wasm/zknym-lib/Cargo.toml @@ -31,7 +31,6 @@ rand = { workspace = true } nym-bin-common = { path = "../../common/bin-common" } -nym-coconut = { path = "../../common/nymcoconut", features = ["key-zeroize"] } nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } nym-credentials = { path = "../../common/credentials" } nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } diff --git a/wasm/zknym-lib/src/error.rs b/wasm/zknym-lib/src/error.rs index 37c49aa672..f232daddf3 100644 --- a/wasm/zknym-lib/src/error.rs +++ b/wasm/zknym-lib/src/error.rs @@ -7,12 +7,6 @@ use wasm_utils::wasm_error; #[derive(Debug, Error)] pub enum ZkNymError { - #[error("[coconut] cryptographic failure: {source}")] - CoconutFailure { - #[from] - source: nym_coconut::CoconutError, - }, - #[error("[ecash] cryptographic failure: {source}")] EcashFailure { #[from] diff --git a/wasm/zknym-lib/src/generic_scheme/coconut/mod.rs b/wasm/zknym-lib/src/generic_scheme/coconut/mod.rs deleted file mode 100644 index 5ac1b00dd7..0000000000 --- a/wasm/zknym-lib/src/generic_scheme/coconut/mod.rs +++ /dev/null @@ -1,338 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use super::coconut::types::{ - BlindSignRequestData, BlindSignRequestWrapper, BlindedCredentialWrapper, - CredentialShareWrapper, CredentialWrapper, KeyPairWrapper, ParametersWrapper, ScalarsWrapper, - VerificationKeyShareWrapper, VerificationKeyWrapper, VerifyCredentialRequestWrapper, -}; -use crate::error::ZkNymError; -use crate::GLOBAL_COCONUT_PARAMS; -use nym_coconut::{hash_to_scalar, Parameters, SignerIndex}; -use serde::{Deserialize, Serialize}; -use std::sync::OnceLock; -use tsify::Tsify; -use wasm_bindgen::prelude::wasm_bindgen; - -pub mod types; - -// works under the assumption of having 4 attributes in the underlying credential(s) -const DEFAULT_ATTRIBUTES: u32 = 4; - -pub fn default_bandwidth_credential_params() -> &'static Parameters { - static BANDWIDTH_CREDENTIAL_PARAMS: OnceLock = OnceLock::new(); - BANDWIDTH_CREDENTIAL_PARAMS.get_or_init(|| Parameters::new(DEFAULT_ATTRIBUTES).unwrap()) -} - -// attempt to extract appropriate system parameters in the following order: -// 1. attempt to get explicit provided value -// 2. then try a globally set value -// 3. finally fallback to sane default: the bandwidth credential params -pub(crate) fn get_params(explicit_params: &Option) -> &Parameters { - if let Some(explicit) = explicit_params.as_ref() { - return explicit; - } - - if let Some(global) = GLOBAL_COCONUT_PARAMS.get() { - return global; - } - - default_bandwidth_credential_params() -} - -#[derive(Tsify, Debug, Copy, Clone, Serialize, Deserialize, Default)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[serde(rename_all = "camelCase")] -pub struct SetupOpts { - #[tsify(optional)] - pub num_attributes: Option, - - #[tsify(optional)] - pub set_global: Option, -} - -#[wasm_bindgen(js_name = "coconutSetup")] -pub fn setup(opts: SetupOpts) -> Result { - let num_attributes = opts.num_attributes.unwrap_or(DEFAULT_ATTRIBUTES); - - let params = nym_coconut::setup(num_attributes)?; - - if let Some(true) = opts.set_global { - GLOBAL_COCONUT_PARAMS - .set(params.clone()) - .map_err(|_| ZkNymError::GlobalParamsAlreadySet)?; - } - - Ok(params.into()) -} - -#[wasm_bindgen(js_name = "coconutKeygen")] -pub fn keygen(parameters: Option) -> KeyPairWrapper { - let params = get_params(¶meters); - nym_coconut::keygen(params).into() -} - -#[derive(Tsify, Debug, Clone, Copy, Serialize, Deserialize)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[serde(rename_all = "camelCase")] -pub struct TtpKeygenOpts { - pub threshold: u64, - - pub authorities: u64, -} - -#[wasm_bindgen(js_name = "coconutTtpKeygen")] -pub fn ttp_keygen( - opts: TtpKeygenOpts, - parameters: Option, -) -> Result, ZkNymError> { - let params = get_params(¶meters); - - let keys = nym_coconut::ttp_keygen(params, opts.threshold, opts.authorities)?; - Ok(keys.into_iter().map(Into::into).collect()) -} - -#[wasm_bindgen(js_name = "coconutSignSimple")] -pub fn sign_simple( - attributes: Vec, - keys: &KeyPairWrapper, -) -> Result { - let public_attributes = attributes - .into_iter() - .map(hash_to_scalar) - .collect::>(); - let attributes_ref = public_attributes.iter().collect::>(); - - nym_coconut::sign(keys.secret_key(), &attributes_ref) - .map(Into::into) - .map_err(Into::into) -} - -#[wasm_bindgen(js_name = "coconutPrepareBlindSign")] -pub fn prepare_blind_sign( - private_attributes: Vec, - public_attributes: Vec, - parameters: Option, -) -> Result { - let params = get_params(¶meters); - - let public_attributes = public_attributes - .into_iter() - .map(hash_to_scalar) - .collect::>(); - let public_attributes_ref = public_attributes.iter().collect::>(); - - let private_attributes = private_attributes - .into_iter() - .map(hash_to_scalar) - .collect::>(); - let private_attributes_ref = private_attributes.iter().collect::>(); - let (pedersen_commitments_openings, blind_sign_request) = - nym_coconut::prepare_blind_sign(params, &private_attributes_ref, &public_attributes_ref)?; - - Ok(BlindSignRequestData { - blind_sign_request, - pedersen_commitments_openings, - }) -} - -#[wasm_bindgen(js_name = "coconutBlindSign")] -pub fn blind_sign( - keys: &KeyPairWrapper, - blind_sign_request: &BlindSignRequestWrapper, - public_attributes: Vec, - parameters: Option, -) -> Result { - let params = get_params(¶meters); - - let public_attributes = public_attributes - .into_iter() - .map(hash_to_scalar) - .collect::>(); - let public_attributes_ref = public_attributes.iter().collect::>(); - - nym_coconut::blind_sign( - params, - keys.secret_key(), - blind_sign_request, - &public_attributes_ref, - ) - .map(Into::into) - .map_err(Into::into) -} - -#[wasm_bindgen(js_name = "coconutUnblindSignatureShare")] -pub fn unblind_signature_share( - blinded_signature: &BlindedCredentialWrapper, - partial_verification_key: &VerificationKeyWrapper, - pedersen_commitments_openings: &ScalarsWrapper, -) -> CredentialWrapper { - BlindedCredentialWrapper::unblind( - blinded_signature, - partial_verification_key, - pedersen_commitments_openings, - ) -} - -#[wasm_bindgen(js_name = "coconutUnblindAndVerifySignatureShare")] -pub fn unblind_and_verify_signature_share( - blinded_signature: &BlindedCredentialWrapper, - partial_verification_key: &VerificationKeyWrapper, - request: &BlindSignRequestData, - private_attributes: Vec, - public_attributes: Vec, - parameters: Option, -) -> Result { - BlindedCredentialWrapper::unblind_and_verify( - blinded_signature, - partial_verification_key, - request, - private_attributes, - public_attributes, - parameters, - ) -} - -#[wasm_bindgen(js_name = "coconutAggregateSignatureShares")] -pub fn aggregate_signature_shares( - shares: Vec, -) -> Result { - let shares = shares.into_iter().map(Into::into).collect::>(); - - nym_coconut::aggregate_signature_shares(&shares) - .map(Into::into) - .map_err(Into::into) -} - -#[wasm_bindgen(js_name = "coconutAggregateSignatureSharesAndVerify")] -pub fn aggregate_signature_shares_and_verify( - verification_key: &VerificationKeyWrapper, - parameters: Option, - private_attributes: Vec, - public_attributes: Vec, - shares: Vec, -) -> Result { - let params = get_params(¶meters); - - let public_attributes = public_attributes - .into_iter() - .map(hash_to_scalar) - .collect::>(); - - let private_attributes = private_attributes - .into_iter() - .map(hash_to_scalar) - .collect::>(); - - let attributes = private_attributes - .iter() - .chain(public_attributes.iter()) - .collect::>(); - let shares = shares.into_iter().map(Into::into).collect::>(); - - nym_coconut::aggregate_signature_shares_and_verify( - params, - verification_key, - &attributes, - &shares, - ) - .map(Into::into) - .map_err(Into::into) -} - -#[wasm_bindgen(js_name = "coconutAggregateVerificationKeyShares")] -pub fn aggregate_verification_key_shares( - shares: Vec, -) -> Result { - let shares = shares.into_iter().map(Into::into).collect::>(); - - nym_coconut::aggregate_key_shares(&shares) - .map(Into::into) - .map_err(Into::into) -} - -#[wasm_bindgen(js_name = "coconutAggregateVerificationKeys")] -pub fn aggregate_verification_keys( - keys: Vec, - indices: Vec, -) -> Result { - let keys = keys.into_iter().map(Into::into).collect::>(); - - nym_coconut::aggregate_verification_keys(&keys, Some(&indices)) - .map(Into::into) - .map_err(Into::into) -} - -#[wasm_bindgen(js_name = "coconutProveBandwidthCredential")] -pub fn prove_bandwidth_credential( - verification_key: &VerificationKeyWrapper, - credential: &CredentialWrapper, - serial_number: String, - binding_number: String, - parameters: Option, -) -> Result { - let params = get_params(¶meters); - - nym_coconut::prove_bandwidth_credential( - params, - verification_key, - credential, - &hash_to_scalar(serial_number), - &hash_to_scalar(binding_number), - ) - .map(Into::into) - .map_err(Into::into) -} - -#[wasm_bindgen(js_name = "coconutVerifyCredential")] -pub fn verify_credential( - verification_key: &VerificationKeyWrapper, - verification_request: &VerifyCredentialRequestWrapper, - public_attributes: Vec, - parameters: Option, -) -> bool { - let params = get_params(¶meters); - - let public_attributes = public_attributes - .into_iter() - .map(hash_to_scalar) - .collect::>(); - let attributes_ref = public_attributes.iter().collect::>(); - - nym_coconut::verify_credential( - params, - verification_key, - verification_request, - &attributes_ref, - ) -} - -#[wasm_bindgen(js_name = "coconutVerifySimple")] -pub fn verify_simple( - verification_key: &VerificationKeyWrapper, - attributes: Vec, - credential: &CredentialWrapper, - parameters: Option, -) -> bool { - let params = get_params(¶meters); - - let public_attributes = attributes - .into_iter() - .map(hash_to_scalar) - .collect::>(); - let attributes_ref = public_attributes.iter().collect::>(); - - nym_coconut::verify(params, verification_key, &attributes_ref, credential) -} - -#[wasm_bindgen(js_name = "coconutSimpleRandomiseCredential")] -pub fn simple_randomise_credential( - credential: &CredentialWrapper, - parameters: Option, -) -> CredentialWrapper { - let params = get_params(¶meters); - - CredentialWrapper { - inner: credential.inner.randomise_simple(params), - } -} diff --git a/wasm/zknym-lib/src/generic_scheme/coconut/types.rs b/wasm/zknym-lib/src/generic_scheme/coconut/types.rs deleted file mode 100644 index 875de418cd..0000000000 --- a/wasm/zknym-lib/src/generic_scheme/coconut/types.rs +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::error::ZkNymError; -use crate::{data_pointer_clone, wasm_wrapper, wasm_wrapper_bs58}; -use nym_coconut::{ - hash_to_scalar, Base58, BlindSignRequest, BlindedSignature, KeyPair, Parameters, Scalar, - SecretKey, Signature, SignatureShare, SignerIndex, VerificationKey, VerificationKeyShare, - VerifyCredentialRequest, -}; -use serde::{Deserialize, Serialize}; -use std::ops::Deref; -use std::str::FromStr; -use tsify::Tsify; -use wasm_bindgen::prelude::wasm_bindgen; -use zeroize::{Zeroize, ZeroizeOnDrop}; - -wasm_wrapper!(Parameters, ParametersWrapper); -wasm_wrapper_bs58!(Signature, CredentialWrapper); -wasm_wrapper_bs58!(BlindedSignature, BlindedCredentialWrapper); -wasm_wrapper!(SignatureShare, CredentialShareWrapper); -wasm_wrapper_bs58!(Scalar, ScalarWrapper); - -wasm_wrapper!(KeyPair, KeyPairWrapper); -wasm_wrapper!(SecretKey, SecretKeyWrapper); -wasm_wrapper!(BlindSignRequest, BlindSignRequestWrapper); -wasm_wrapper_bs58!(VerificationKey, VerificationKeyWrapper); -wasm_wrapper_bs58!(VerifyCredentialRequest, VerifyCredentialRequestWrapper); -wasm_wrapper!(VerificationKeyShare, VerificationKeyShareWrapper); - -data_pointer_clone!(VerificationKeyShareWrapper); -data_pointer_clone!(CredentialShareWrapper); -data_pointer_clone!(BlindSignRequestWrapper); - -#[wasm_bindgen] -impl BlindedCredentialWrapper { - pub fn unblind( - &self, - partial_verification_key: &VerificationKeyWrapper, - pedersen_commitments_openings: &ScalarsWrapper, - ) -> CredentialWrapper { - self.inner - .unblind(partial_verification_key, pedersen_commitments_openings) - .into() - } - - #[wasm_bindgen(js_name = "unblindAndVerify")] - pub fn unblind_and_verify( - &self, - partial_verification_key: &VerificationKeyWrapper, - request: &BlindSignRequestData, - private_attributes: Vec, - public_attributes: Vec, - parameters: Option, - ) -> Result { - let params = super::get_params(¶meters); - - let public_attributes = public_attributes - .into_iter() - .map(hash_to_scalar) - .collect::>(); - let public_attributes_ref = public_attributes.iter().collect::>(); - - let private_attributes = private_attributes - .into_iter() - .map(hash_to_scalar) - .collect::>(); - let private_attributes_ref = private_attributes.iter().collect::>(); - - let unblinded_signature = self.inner.unblind_and_verify( - params, - partial_verification_key, - &private_attributes_ref, - &public_attributes_ref, - &request.blind_sign_request.get_commitment_hash(), - &request.pedersen_commitments_openings, - )?; - - Ok(unblinded_signature.into()) - } -} - -#[wasm_bindgen] -impl CredentialWrapper { - #[wasm_bindgen(js_name = "intoShare")] - pub fn into_share(self, index: SignerIndex) -> CredentialShareWrapper { - CredentialShareWrapper { - inner: SignatureShare::new(self.inner, index), - } - } -} - -#[wasm_bindgen] -impl KeyPairWrapper { - #[wasm_bindgen(js_name = "verificationKey")] - pub fn verification_key(&self) -> VerificationKeyWrapper { - self.inner.verification_key().clone().into() - } - - pub fn index(&self) -> Option { - self.inner.index - } - - #[wasm_bindgen(js_name = "verificationKeyShare")] - pub fn verification_key_share(&self) -> Option { - self.inner.to_verification_key_share().map(Into::into) - } -} - -#[wasm_bindgen] -pub struct BlindSignRequestData { - pub(crate) blind_sign_request: BlindSignRequest, - pub(crate) pedersen_commitments_openings: Vec, -} - -#[wasm_bindgen] -impl BlindSignRequestData { - #[wasm_bindgen(js_name = "blindSignRequest")] - pub fn blind_sign_request(&self) -> BlindSignRequestWrapper { - self.blind_sign_request.clone().into() - } - - #[wasm_bindgen(js_name = "pedersenCommitmentsOpenings")] - pub fn pedersen_commitments_openings(&self) -> ScalarsWrapper { - ScalarsWrapper(self.pedersen_commitments_openings.clone()) - } -} - -#[wasm_bindgen] -#[derive(Zeroize, ZeroizeOnDrop)] -pub struct ScalarsWrapper(pub(crate) Vec); - -impl Deref for ScalarsWrapper { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[derive( - Tsify, Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq, Zeroize, ZeroizeOnDrop, -)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[serde(rename_all = "camelCase")] -pub struct KeypairWrapper { - pub private_key: String, - pub public_key: String, -} - -#[derive(Tsify, Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[serde(rename_all = "camelCase")] -pub struct UnblindableShare { - pub issuer_index: u64, - pub issuer_key_bs58: String, - pub blinded_share_bs58: String, -} diff --git a/wasm/zknym-lib/src/generic_scheme/mod.rs b/wasm/zknym-lib/src/generic_scheme/mod.rs index ad786c7841..0fa97dc952 100644 --- a/wasm/zknym-lib/src/generic_scheme/mod.rs +++ b/wasm/zknym-lib/src/generic_scheme/mod.rs @@ -1,5 +1,4 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -pub mod coconut; pub mod ecash; diff --git a/wasm/zknym-lib/src/lib.rs b/wasm/zknym-lib/src/lib.rs index 540a386ee8..4d0b342232 100644 --- a/wasm/zknym-lib/src/lib.rs +++ b/wasm/zknym-lib/src/lib.rs @@ -7,8 +7,6 @@ // due to the code generated by Tsify #![allow(clippy::empty_docs)] -use nym_coconut::Parameters; -use std::sync::OnceLock; use wasm_bindgen::prelude::*; pub mod bandwidth_voucher; @@ -22,8 +20,6 @@ pub mod types; // I mostly got it, so I could test the whole thing end to end pub(crate) mod vpn_api_client; -pub(crate) static GLOBAL_COCONUT_PARAMS: OnceLock = OnceLock::new(); - #[wasm_bindgen(start)] // #[cfg(target_arch = "wasm32")] pub fn main() { diff --git a/wasm/zknym-lib/src/vpn_api_client/client.rs b/wasm/zknym-lib/src/vpn_api_client/client.rs index b6ac1ccbe8..846ec11b64 100644 --- a/wasm/zknym-lib/src/vpn_api_client/client.rs +++ b/wasm/zknym-lib/src/vpn_api_client/client.rs @@ -4,11 +4,9 @@ use super::NymVpnApiClientError; use crate::error::ZkNymError; use crate::vpn_api_client::types::{ - AttributesResponse, BandwidthVoucherRequest, BandwidthVoucherResponse, - MasterVerificationKeyResponse, PartialVerificationKeysResponse, + AttributesResponse, MasterVerificationKeyResponse, PartialVerificationKeysResponse, }; use async_trait::async_trait; -use nym_coconut::BlindSignRequest; pub use nym_http_api_client::Client; use nym_http_api_client::{parse_response, ApiClient, PathSegments, NO_PARAMS}; use reqwest::IntoUrl; @@ -76,11 +74,6 @@ pub trait NymVpnApiClient { ]) .await } - - async fn get_bandwidth_voucher_blinded_shares( - &self, - blind_sign_request: BlindSignRequest, - ) -> Result; } #[async_trait(?Send)] @@ -108,30 +101,4 @@ impl NymVpnApiClient for VpnApiClient { parse_response(res, false).await } - - async fn get_bandwidth_voucher_blinded_shares( - &self, - blind_sign_request: BlindSignRequest, - ) -> Result { - let req = self.inner.create_post_request( - &["/api", "/v1", "/bandwidth-voucher", "/obtain"], - NO_PARAMS, - &BandwidthVoucherRequest { blind_sign_request }, - ); - - let fut = req.bearer_auth(&self.bearer_token).send(); - - // the only reason for that target lock is so that I could call this method from an ephemeral test - // running in non-wasm mode (since I wanted to use tokio) - - #[cfg(target_arch = "wasm32")] - let res = wasmtimer::tokio::timeout(std::time::Duration::from_secs(5), fut) - .await - .map_err(|_timeout| HttpClientError::RequestTimeout)??; - - #[cfg(not(target_arch = "wasm32"))] - let res = fut.await?; - - parse_response(res, false).await - } } diff --git a/wasm/zknym-lib/src/vpn_api_client/types.rs b/wasm/zknym-lib/src/vpn_api_client/types.rs index cdb8d47534..89da067fc8 100644 --- a/wasm/zknym-lib/src/vpn_api_client/types.rs +++ b/wasm/zknym-lib/src/vpn_api_client/types.rs @@ -3,27 +3,11 @@ // just copied over from dot com repo -use nym_coconut::BlindSignRequest; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use tsify::Tsify; use uuid::Uuid; -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct BandwidthVoucherRequest { - /// base58 encoded blind sign request - pub blind_sign_request: BlindSignRequest, -} - -#[derive(Tsify, Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[serde(rename_all = "camelCase")] -pub struct BandwidthVoucherResponse { - pub epoch_id: u64, - pub shares: Vec, -} - #[derive(Tsify, Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)] #[tsify(into_wasm_abi, from_wasm_abi)] #[serde(rename_all = "camelCase")] From 65175fee0974ab816bb50bf8709e96ab2118902d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 26 Feb 2025 10:52:09 +0000 Subject: [PATCH 010/133] merge #5512 again after reverting due to incorrect rebase (#5520) * setup workspace global lints to prevent needless panics * removed sources of panic in nym-crypto, nym-node and nym-api * adjusted test code --- Cargo.toml | 10 ++++++ clippy.toml | 1 + common/crypto/Cargo.toml | 5 ++- common/crypto/src/hmac.rs | 14 +++++--- common/crypto/src/shared_key.rs | 19 +++++++--- common/crypto/src/symmetric/stream_cipher.rs | 13 +++---- .../acknowledgements/src/identifier.rs | 10 ++++-- nym-api/Cargo.toml | 3 ++ nym-api/build.rs | 3 ++ nym-api/src/ecash/comm.rs | 4 ++- nym-api/src/ecash/dkg/controller/mod.rs | 5 ++- nym-api/src/ecash/dkg/dealing.rs | 3 +- nym-api/src/ecash/dkg/key_derivation.rs | 4 +++ nym-api/src/ecash/state/mod.rs | 5 +-- nym-api/src/ecash/storage/helpers.rs | 6 ++++ nym-api/src/ecash/tests/mod.rs | 20 +++++------ nym-api/src/epoch_operations/helpers.rs | 3 ++ .../rewarded_set_assignment.rs | 2 ++ nym-api/src/network_monitor/monitor/mod.rs | 2 ++ .../src/network_monitor/monitor/preparer.rs | 4 +++ .../src/network_monitor/monitor/processor.rs | 3 ++ .../src/network_monitor/monitor/receiver.rs | 16 ++++++--- nym-api/src/network_monitor/monitor/sender.rs | 2 ++ nym-api/src/network_monitor/test_route/mod.rs | 1 + nym-api/src/node_status_api/models.rs | 5 +++ nym-api/src/node_status_api/uptime_updater.rs | 7 ++-- nym-api/src/node_status_api/utils.rs | 1 + .../src/nym_contract_cache/cache/refresher.rs | 1 + .../nym_nodes/handlers/unstable/helpers.rs | 5 ++- nym-api/src/support/cli/run.rs | 2 +- nym-api/src/support/config/mod.rs | 2 ++ nym-api/src/support/nyxd/mod.rs | 17 ++++----- nym-api/src/support/storage/mod.rs | 35 ++++++++++--------- nym-node/Cargo.toml | 4 +++ nym-node/src/cli/commands/sign.rs | 2 ++ nym-node/src/node/metrics/aggregator.rs | 3 ++ .../handler/global_prometheus_updater/mod.rs | 2 ++ .../metrics/handler/legacy_packet_data.rs | 2 ++ .../metrics/handler/mixnet_data_cleaner.rs | 2 ++ .../handler/pending_egress_packets_updater.rs | 2 ++ 40 files changed, 181 insertions(+), 69 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c1fee9aca5..bf65a433fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -437,3 +437,13 @@ opt-level = 'z' [profile.release.package.mix-fetch-wasm] # lto = true opt-level = 'z' + +[workspace.lints.clippy] +unwrap_used = "deny" +expect_used = "deny" +todo = "deny" +dbg_macro = "deny" +exit = "deny" +panic = "deny" +unimplemented = "deny" +unreachable = "deny" \ No newline at end of file diff --git a/clippy.toml b/clippy.toml index 0358cdb508..9b4ca613ab 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,2 +1,3 @@ allow-unwrap-in-tests = true allow-expect-in-tests = true +allow-panic-in-tests = true \ No newline at end of file diff --git a/common/crypto/Cargo.toml b/common/crypto/Cargo.toml index ee8819585e..6e1f8cfb12 100644 --- a/common/crypto/Cargo.toml +++ b/common/crypto/Cargo.toml @@ -43,4 +43,7 @@ serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde" asymmetric = ["x25519-dalek", "ed25519-dalek", "zeroize"] hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2"] stream_cipher = ["aes", "ctr", "cipher", "generic-array"] -sphinx = ["nym-sphinx-types/sphinx"] \ No newline at end of file +sphinx = ["nym-sphinx-types/sphinx"] + +[lints] +workspace = true \ No newline at end of file diff --git a/common/crypto/src/hmac.rs b/common/crypto/src/hmac.rs index c2678eb0bd..f878df238c 100644 --- a/common/crypto/src/hmac.rs +++ b/common/crypto/src/hmac.rs @@ -16,8 +16,11 @@ pub fn compute_keyed_hmac(key: &[u8], data: &[u8]) -> HmacOutput where D: Digest + BlockSizeUser, { - let mut hmac = SimpleHmac::::new_from_slice(key) - .expect("HMAC was instantiated with a key of an invalid size!"); + // SAFETY: hmac is fine with keys of any size; if they're smaller than the block size of the underlying + // digest, they're padded with 0. if they're larger they're hashed and padded + // the reason for `Result` return type is due to the trait definition + #[allow(clippy::unwrap_used)] + let mut hmac = SimpleHmac::::new_from_slice(key).unwrap(); hmac.update(data); hmac.finalize() } @@ -27,8 +30,11 @@ pub fn recompute_keyed_hmac_and_verify_tag(key: &[u8], data: &[u8], tag: &[u8 where D: Digest + BlockSizeUser, { - let mut hmac = SimpleHmac::::new_from_slice(key) - .expect("HMAC was instantiated with a key of an invalid size!"); + // SAFETY: hmac is fine with keys of any size; if they're smaller than the block size of the underlying + // digest, they're padded with 0. if they're larger they're hashed and padded + // the reason for `Result` return type is due to the trait definition + #[allow(clippy::unwrap_used)] + let mut hmac = SimpleHmac::::new_from_slice(key).unwrap(); hmac.update(data); let tag_arr = Output::::from_slice(tag); diff --git a/common/crypto/src/shared_key.rs b/common/crypto/src/shared_key.rs index f90e99feb2..def5709339 100644 --- a/common/crypto/src/shared_key.rs +++ b/common/crypto/src/shared_key.rs @@ -27,12 +27,16 @@ where // after performing diffie-hellman we don't care about the private component anymore let dh_result = ephemeral_keypair.private_key().diffie_hellman(remote_key); - // there is no reason for this to fail as our okm is expected to be only C::KeySize bytes + // SAFETY: while this is a relatively weak assumption, it's unlikely that any stream cipher has `C::key_size()` + // larger than 255 * chunk_size of the digest (so for example keys larger than 8160 bytes if sh256 is used) + #[allow(clippy::expect_used)] let okm = hkdf::extract_then_expand::(None, &dh_result, None, C::key_size()) .expect("somehow too long okm was provided"); - let derived_shared_key = - Key::::from_exact_iter(okm).expect("okm was expanded to incorrect length!"); + // SAFETY: the generated okm has exactly `C::key_size()` elements, + // so this call is safe + #[allow(clippy::unwrap_used)] + let derived_shared_key = Key::::from_exact_iter(okm).unwrap(); (ephemeral_keypair, derived_shared_key) } @@ -48,9 +52,14 @@ where { let dh_result = local_key.diffie_hellman(remote_key); - // there is no reason for this to fail as our okm is expected to be only C::KeySize bytes + // SAFETY: while this is a relatively weak assumption, it's unlikely that any stream cipher has `C::key_size()` + // larger than 255 * chunk_size of the digest (so for example keys larger than 8160 bytes if sh256 is used) + #[allow(clippy::expect_used)] let okm = hkdf::extract_then_expand::(None, &dh_result, None, C::key_size()) .expect("somehow too long okm was provided"); - Key::::from_exact_iter(okm).expect("okm was expanded to incorrect length!") + // SAFETY: the generated okm has exactly `C::key_size()` elements, + // so this call is safe + #[allow(clippy::unwrap_used)] + Key::::from_exact_iter(okm).unwrap() } diff --git a/common/crypto/src/symmetric/stream_cipher.rs b/common/crypto/src/symmetric/stream_cipher.rs index 4bd21eb031..758f32617c 100644 --- a/common/crypto/src/symmetric/stream_cipher.rs +++ b/common/crypto/src/symmetric/stream_cipher.rs @@ -60,20 +60,15 @@ where Iv::::default() } -pub fn iv_from_slice(b: &[u8]) -> &IV +pub fn try_iv_from_slice(b: &[u8]) -> Option<&IV> where C: IvSizeUser, { if b.len() != C::iv_size() { - // `from_slice` would have caused a panic about this issue anyway. - // Now we at least have slightly more information - panic!( - "Tried to convert {} bytes to IV. Expected {}", - b.len(), - C::iv_size() - ) + None + } else { + Some(IV::::from_slice(b)) } - IV::::from_slice(b) } // TODO: there's really no way to use more parts of the keystream if it was required at some point. diff --git a/common/nymsphinx/acknowledgements/src/identifier.rs b/common/nymsphinx/acknowledgements/src/identifier.rs index abe1d58368..544e9be959 100644 --- a/common/nymsphinx/acknowledgements/src/identifier.rs +++ b/common/nymsphinx/acknowledgements/src/identifier.rs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::AckKey; -use nym_crypto::symmetric::stream_cipher::{self, encrypt, iv_from_slice, random_iv, IvSizeUser}; +use nym_crypto::symmetric::stream_cipher::{ + self, encrypt, random_iv, try_iv_from_slice, IvSizeUser, +}; use nym_sphinx_params::{AckEncryptionAlgorithm, SerializedFragmentIdentifier, FRAG_ID_LEN}; use rand::{CryptoRng, RngCore}; @@ -25,7 +27,11 @@ pub fn recover_identifier( iv_id_ciphertext: &[u8], ) -> Option { let iv_size = AckEncryptionAlgorithm::iv_size(); - let iv = iv_from_slice::(&iv_id_ciphertext[..iv_size]); + if iv_id_ciphertext.len() < FRAG_ID_LEN + iv_size { + return None; + } + + let iv = try_iv_from_slice::(&iv_id_ciphertext[..iv_size])?; let id = stream_cipher::decrypt::( key.inner(), diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index a6830e21c1..4db77f3ccd 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -141,3 +141,6 @@ cw3 = { workspace = true } cw-utils = { workspace = true } rand_chacha = { workspace = true } sha2 = "0.9" + +[lints] +workspace = true \ No newline at end of file diff --git a/nym-api/build.rs b/nym-api/build.rs index d6e0c45770..a2c0377698 100644 --- a/nym-api/build.rs +++ b/nym-api/build.rs @@ -1,6 +1,9 @@ use sqlx::{Connection, FromRow, SqliteConnection}; use std::env; +// it's fine if compilation fails +#[allow(clippy::unwrap_used)] +#[allow(clippy::expect_used)] #[tokio::main] async fn main() { let out_dir = env::var("OUT_DIR").unwrap(); diff --git a/nym-api/src/ecash/comm.rs b/nym-api/src/ecash/comm.rs index fd70c592eb..ef5deceb83 100644 --- a/nym-api/src/ecash/comm.rs +++ b/nym-api/src/ecash/comm.rs @@ -47,6 +47,8 @@ impl CachedEpoch { let now = OffsetDateTime::now_utc(); let validity_duration = if let Some(epoch_finish) = epoch.deadline { + // SAFETY: values set in our contract are valid unix timestamps + #[allow(clippy::unwrap_used)] let state_end = OffsetDateTime::from_unix_timestamp(epoch_finish.seconds() as i64).unwrap(); let until_epoch_state_end = state_end - now; @@ -103,7 +105,7 @@ impl APICommunicationChannel for QueryCommunicationChannel { drop(guard); let guard = self.update_epoch_cache().await?; - return Ok(guard.current_epoch.epoch_id); + Ok(guard.current_epoch.epoch_id) } // TODO: perhaps this should be returning a ReadGuard instead? diff --git a/nym-api/src/ecash/dkg/controller/mod.rs b/nym-api/src/ecash/dkg/controller/mod.rs index 25cba6b48d..56d380bb1d 100644 --- a/nym-api/src/ecash/dkg/controller/mod.rs +++ b/nym-api/src/ecash/dkg/controller/mod.rs @@ -189,7 +189,10 @@ impl DkgController { self.state.clear_previous_epoch(epoch_id); // SAFETY: we just accessed this item in an immutable way, thus it MUST exist so the unwrap is fine - self.state.in_progress_state_mut(epoch_id).unwrap().entered = true; + #[allow(clippy::unwrap_used)] + { + self.state.in_progress_state_mut(epoch_id).unwrap().entered = true; + } } // so at this point we don't need to be polling the contract so often anymore, but we can't easily diff --git a/nym-api/src/ecash/dkg/dealing.rs b/nym-api/src/ecash/dkg/dealing.rs index 31da8a0ea5..ee7ad5b5a3 100644 --- a/nym-api/src/ecash/dkg/dealing.rs +++ b/nym-api/src/ecash/dkg/dealing.rs @@ -204,8 +204,9 @@ impl DkgController { chunk_dealing(*dealing_index, dealing.to_bytes(), Self::DEALING_CHUNK_SIZE); for chunk_index in needs_resubmission { // this is a hard failure, panic level, actually. - // because we have already committed to dealings of particular size + // because we have already committed to dealings of particular size, // yet we don't have relevant chunks after chunking + #[allow(clippy::expect_used)] let chunk = chunks .remove(chunk_index) .expect("chunking specification has changed mid-exchange!"); diff --git a/nym-api/src/ecash/dkg/key_derivation.rs b/nym-api/src/ecash/dkg/key_derivation.rs index 46818547d4..41cfd11d36 100644 --- a/nym-api/src/ecash/dkg/key_derivation.rs +++ b/nym-api/src/ecash/dkg/key_derivation.rs @@ -177,6 +177,8 @@ impl DkgController { // SAFETY: // since this share appears as 'verified' on the chain, it means the consensus of dealers confirmed its validity // and thus they must have been able to parse it, so the unwrap/expect here is fine + // (unless quorum of validators is malicious, but at that point we have bigger problems...) + #[allow(clippy::expect_used)] Ok(Some( VerificationKeyAuth::try_from_bs58(&share.share) .expect("failed to deserialize VERIFIED key"), @@ -415,6 +417,7 @@ impl DkgController { // SAFETY: combining shares can only fail if we have different number shares and indices // however, we returned an explicit error if decryption of any share failed and thus we know those values must match + #[allow(clippy::unwrap_used)] let secret = combine_shares(shares, &all_dealers).unwrap(); if derived_x.is_none() { derived_x = Some(secret) @@ -426,6 +429,7 @@ impl DkgController { // SAFETY: // we know we had a non-empty map of dealings and thus, at the very least, we must have derived a single secret // (i.e. the x-element) + #[allow(clippy::unwrap_used)] let sk = SecretKeyAuth::create_from_raw(derived_x.unwrap(), derived_secrets); let derived_vk = sk.verification_key(); diff --git a/nym-api/src/ecash/state/mod.rs b/nym-api/src/ecash/state/mod.rs index 5d3c6412af..4ade072fb6 100644 --- a/nym-api/src/ecash/state/mod.rs +++ b/nym-api/src/ecash/state/mod.rs @@ -140,6 +140,9 @@ impl EcashState { } } + // whilst we normally don't want to panic, this one would only occur at startup, + // if some logical invariants got broken (which have to be fixed in code anyway) + #[allow(clippy::panic)] pub(crate) fn spawn_background_cleaner(&mut self) { match std::mem::take(&mut self.background_cleaner_state) { BackgroundCleanerState::WaitingStartup(cleaner) => { @@ -147,8 +150,6 @@ impl EcashState { _handle: cleaner.start(), } } - // whilst we normally don't want to panic, this one would only occur at startup, - // if some logical invariants got broken (which have to be fixed in code anyway) _ => panic!("attempted to spawn background cleaner more than once"), } } diff --git a/nym-api/src/ecash/storage/helpers.rs b/nym-api/src/ecash/storage/helpers.rs index c0be8a9183..c7b8a0d507 100644 --- a/nym-api/src/ecash/storage/helpers.rs +++ b/nym-api/src/ecash/storage/helpers.rs @@ -13,6 +13,9 @@ struct StorageBorrowedSerdeWrapper<'a, T>(&'a T); #[derive(Serialize, Deserialize)] struct StorageSerdeWrapper(T); +// SAFETY: we're not using custom serialiser for AnnotatedCoinIndexSignature +// and we're within bound limits +#[allow(clippy::unwrap_used)] pub(crate) fn serialise_coin_index_signatures(sigs: &[AnnotatedCoinIndexSignature]) -> Vec { storage_serialiser() .serialize(&StorageBorrowedSerdeWrapper(&sigs)) @@ -28,6 +31,9 @@ pub(crate) fn deserialise_coin_index_signatures( Ok(de.0) } +// SAFETY: we're not using custom serialiser for AnnotatedExpirationDateSignature +// and we're within bound limits +#[allow(clippy::unwrap_used)] pub(crate) fn serialise_expiration_date_signatures( sigs: &[AnnotatedExpirationDateSignature], ) -> Vec { diff --git a/nym-api/src/ecash/tests/mod.rs b/nym-api/src/ecash/tests/mod.rs index 9a71802f5c..c31ecc2ede 100644 --- a/nym-api/src/ecash/tests/mod.rs +++ b/nym-api/src/ecash/tests/mod.rs @@ -428,27 +428,27 @@ impl FakeChainState { ); let epoch_id = self.dkg_contract.epoch.epoch_id; let Some(shares) = self.dkg_contract.verification_shares.get_mut(&epoch_id) else { - unimplemented!("no shares for epoch") + panic!("no shares for epoch") }; let Some(share) = shares.get_mut(owner.as_str()) else { - unimplemented!("no shares for owner") + panic!("no shares for owner") }; share.verified = true } - other => unimplemented!("unimplemented exec of {other:?}"), + other => panic!("unimplemented exec of {other:?}"), } } // TODO: make it return a result fn execute_contract_msg(&mut self, contract: &String, msg: &Binary, sender: MessageInfo) { if contract == &self.group_contract.address { - unimplemented!("group contract exec") + panic!("group contract exec") } if contract == &self.multisig_contract.address { - unimplemented!("multisig contract exec") + panic!("multisig contract exec") } if contract == &self.ecash_contract.address { - unimplemented!("bandwidth contract exec") + panic!("bandwidth contract exec") } if contract == self.dkg_contract.address.as_ref() { return self.execute_dkg_contract(sender, msg); @@ -467,7 +467,7 @@ impl FakeChainState { let sender = mock_info(sender_address.as_ref(), funds); self.execute_contract_msg(contract_addr, msg, sender) } - other => unimplemented!("unimplemented wasm proposal for {other:?}"), + other => panic!("unimplemented wasm proposal for {other:?}"), } } @@ -477,7 +477,7 @@ impl FakeChainState { CosmosMsg::Wasm(wasm_msg) => { self.execute_wasm_msg(wasm_msg, Addr::unchecked(sender_address.as_ref())) } - other => unimplemented!("unimplemented proposal for {other:?}"), + other => panic!("unimplemented proposal for {other:?}"), }; } } @@ -907,7 +907,7 @@ impl super::client::Client for DummyClient { }; if proposal.status != cw3::Status::Passed { - unimplemented!("proposal hasn't been passed") + panic!("proposal hasn't been passed") } proposal.status = cw3::Status::Executed; @@ -954,7 +954,7 @@ impl super::client::Client for DummyClient { if !epoch_dealers.contains_key(self.validator_address.as_ref()) { epoch_dealers.insert(self.validator_address.to_string(), dealer_details); } else { - unimplemented!("already registered") + panic!("already registered") } let transaction_hash = guard._counters.next_tx_hash(); diff --git a/nym-api/src/epoch_operations/helpers.rs b/nym-api/src/epoch_operations/helpers.rs index ed9abf30ce..d21fd1aadb 100644 --- a/nym-api/src/epoch_operations/helpers.rs +++ b/nym-api/src/epoch_operations/helpers.rs @@ -105,8 +105,11 @@ impl EpochAdvancer { let standby_node_work_factor = global_rewarding_params.standby_node_work(); // SANITY CHECK: + // SAFETY: 0 decimal places is within the range of `Decimal` + #[allow(clippy::unwrap_used)] let standby_share = Decimal::from_atomics(nodes.standby.len() as u128, 0).unwrap() * standby_node_work_factor; + #[allow(clippy::unwrap_used)] let active_share = Decimal::from_atomics(nodes.active_set_size() as u128, 0).unwrap() * active_node_work_factor; let total_work = standby_share + active_share; diff --git a/nym-api/src/epoch_operations/rewarded_set_assignment.rs b/nym-api/src/epoch_operations/rewarded_set_assignment.rs index c2fda62dd6..40c1124b05 100644 --- a/nym-api/src/epoch_operations/rewarded_set_assignment.rs +++ b/nym-api/src/epoch_operations/rewarded_set_assignment.rs @@ -139,6 +139,7 @@ impl EpochAdvancer { let mut layer2 = Vec::new(); let mut layer3 = Vec::new(); + #[allow(clippy::panic)] for (i, mix) in mixnodes_vec.iter().enumerate() { match i % 3 { 0 => layer1.push(*mix), @@ -207,6 +208,7 @@ impl EpochAdvancer { let mut with_performance = Vec::new(); // SAFETY: the cache MUST HAVE been initialised before now + #[allow(clippy::unwrap_used)] let described_cache = self.described_cache.get().await.unwrap(); let Some(status_cache) = self.status_cache.node_annotations().await else { diff --git a/nym-api/src/network_monitor/monitor/mod.rs b/nym-api/src/network_monitor/monitor/mod.rs index 0145245197..360a946638 100644 --- a/nym-api/src/network_monitor/monitor/mod.rs +++ b/nym-api/src/network_monitor/monitor/mod.rs @@ -229,6 +229,8 @@ impl Monitor { // ideally we would blacklist all nodes regardless of the result so we would not use them anymore // however, currently we have huge imbalance of gateways to mixnodes so we might accidentally // discard working gateway because it was paired with broken mixnode + // SAFETY: the results is subset of candidates so the entry must exist + #[allow(clippy::unwrap_used)] if *results.get(&candidate.id()).unwrap() { // if the path is fully working, blacklist those nodes so we wouldn't construct // any other path through any of those nodes diff --git a/nym-api/src/network_monitor/monitor/preparer.rs b/nym-api/src/network_monitor/monitor/preparer.rs index 7ae2547844..7ee9888478 100644 --- a/nym-api/src/network_monitor/monitor/preparer.rs +++ b/nym-api/src/network_monitor/monitor/preparer.rs @@ -157,6 +157,7 @@ impl PacketPreparer { self.contract_cache.wait_for_initial_values().await; self.described_cache.naive_wait_for_initial_values().await; + #[allow(clippy::expect_used)] let described_nodes = self .described_cache .get() @@ -374,6 +375,7 @@ impl PacketPreparer { .collect::>(); // the unwrap on `min()` is fine as we know the iterator is not empty + #[allow(clippy::unwrap_used)] let most_available = *[ rand_l1.len(), rand_l2.len(), @@ -427,6 +429,7 @@ impl PacketPreparer { // 1. the topology is definitely valid (otherwise we wouldn't be here) // 2. the recipient is specified (by calling **mix**_tester) // 3. the test message is not too long, i.e. when serialized it will fit in a single sphinx packet + #[allow(clippy::unwrap_used)] let mix_packets = plaintexts .into_iter() .map(|p| tester.wrap_plaintext_data(p, &topology, None).unwrap()) @@ -492,6 +495,7 @@ impl PacketPreparer { ) -> PreparedPackets { let (mixnodes, gateways) = self.all_legacy_mixnodes_and_gateways().await; + #[allow(clippy::expect_used)] let descriptions = self .described_cache .get() diff --git a/nym-api/src/network_monitor/monitor/processor.rs b/nym-api/src/network_monitor/monitor/processor.rs index 7a65115d78..b69f919f74 100644 --- a/nym-api/src/network_monitor/monitor/processor.rs +++ b/nym-api/src/network_monitor/monitor/processor.rs @@ -169,6 +169,9 @@ where where R: Sync + Send + 'static, { + // this panic could only be triggered by incorrect startup sequence and shouldn't affect + // the binary beyond that + #[allow(clippy::expect_used)] let mut receiver_task = self .receiver_task .take() diff --git a/nym-api/src/network_monitor/monitor/receiver.rs b/nym-api/src/network_monitor/monitor/receiver.rs index 91a8c67811..a317b9985b 100644 --- a/nym-api/src/network_monitor/monitor/receiver.rs +++ b/nym-api/src/network_monitor/monitor/receiver.rs @@ -8,7 +8,7 @@ use futures::StreamExt; use nym_crypto::asymmetric::identity; use nym_gateway_client::{AcknowledgementReceiver, MixnetMessageReceiver}; use nym_task::TaskClient; -use tracing::trace; +use tracing::{error, trace}; pub(crate) type GatewayClientUpdateSender = mpsc::UnboundedSender; pub(crate) type GatewayClientUpdateReceiver = mpsc::UnboundedReceiver; @@ -52,9 +52,9 @@ impl PacketReceiver { } fn process_gateway_messages(&self, messages: GatewayMessages) { - self.processor_sender - .unbounded_send(messages) - .expect("packet processor seems to have crashed!"); + if self.processor_sender.unbounded_send(messages).is_err() { + error!("packet processor seems to have crashed!") + } } pub(crate) async fn run(&mut self, mut shutdown: TaskClient) { @@ -66,7 +66,13 @@ impl PacketReceiver { } // unwrap here is fine as it can only return a `None` if the PacketSender has died // and if that was the case, then the entire monitor is already in an undefined state - update = self.clients_updater.next() => self.process_gateway_update(update.unwrap()), + update = self.clients_updater.next() => { + if let Some(update) = update { + self.process_gateway_update(update) + } else { + error!("UpdateHandler: Client stream ended!"); + } + }, Some((_gateway_id, messages)) = self.gateways_reader.next() => { self.process_gateway_messages(messages) } diff --git a/nym-api/src/network_monitor/monitor/sender.rs b/nym-api/src/network_monitor/monitor/sender.rs index ca2743b97f..fa6ba454c9 100644 --- a/nym-api/src/network_monitor/monitor/sender.rs +++ b/nym-api/src/network_monitor/monitor/sender.rs @@ -246,6 +246,8 @@ impl PacketSender { trace!("Sending {} packets...", mix_packets.len()); if mix_packets.len() == 1 { + // SAFETY: we just explicitly checked we have 1 message + #[allow(clippy::unwrap_used)] client.send_mix_packet(mix_packets.pop().unwrap()).await?; } else { client.batch_send_mix_packets(mix_packets).await?; diff --git a/nym-api/src/network_monitor/test_route/mod.rs b/nym-api/src/network_monitor/test_route/mod.rs index 1032baf5db..d1384e851b 100644 --- a/nym-api/src/network_monitor/test_route/mod.rs +++ b/nym-api/src/network_monitor/test_route/mod.rs @@ -104,6 +104,7 @@ impl TestRoute { // the unwrap here is fine as the failure can only occur due to serialization and we're not // using any custom implementations + #[allow(clippy::unwrap_used)] NymApiTestMessageExt::new(self.id, ROUTE_TESTING_TEST_NONCE) .mix_plaintexts(mix, count as u32) .unwrap() diff --git a/nym-api/src/node_status_api/models.rs b/nym-api/src/node_status_api/models.rs index 19120e88b8..53f5d3b7d5 100644 --- a/nym-api/src/node_status_api/models.rs +++ b/nym-api/src/node_status_api/models.rs @@ -125,6 +125,8 @@ impl TryFrom for Uptime { impl From for Performance { fn from(uptime: Uptime) -> Self { + // SAFETY: uptime has a valid range to be transformed into a `Performance` + #[allow(clippy::unwrap_used)] Performance::from_percentage_value(uptime.0 as u64).unwrap() } } @@ -481,6 +483,9 @@ pub enum NymApiStorageError { // this one would never be returned to users since it's only possible on startup #[error("failed to perform startup SQL migration - {0}")] StartupMigrationFailure(#[from] sqlx::migrate::MigrateError), + + #[error("{value} is not a valid unix timestamp")] + InvalidTimestampProvided { value: i64 }, } impl From for NymApiStorageError { diff --git a/nym-api/src/node_status_api/uptime_updater.rs b/nym-api/src/node_status_api/uptime_updater.rs index affca9f7f2..a68979ced1 100644 --- a/nym-api/src/node_status_api/uptime_updater.rs +++ b/nym-api/src/node_status_api/uptime_updater.rs @@ -8,7 +8,8 @@ use crate::node_status_api::ONE_DAY; use crate::storage::NymApiStorage; use nym_task::{TaskClient, TaskManager}; use std::time::Duration; -use time::{OffsetDateTime, PrimitiveDateTime, Time}; +use time::macros::time; +use time::{OffsetDateTime, PrimitiveDateTime}; use tokio::time::{interval_at, Instant}; use tracing::error; use tracing::{info, trace, warn}; @@ -75,18 +76,20 @@ impl HistoricalUptimeUpdater { // nodes update for different days // the unwrap is fine as 23:00:00 is a valid time - let update_time = Time::from_hms(23, 0, 0).unwrap(); + let update_time = time!(23:00:00); let now = OffsetDateTime::now_utc(); // is the current time within 0:00 - 22:59:59 or 23:00 - 23:59:59 ? let update_date = if now.hour() < 23 { now.date() } else { // the unwrap is fine as (**PRESUMABLY**) we're not running this code in the year 9999 + #[allow(clippy::unwrap_used)] now.date().next_day().unwrap() }; let update_datetime = PrimitiveDateTime::new(update_date, update_time).assume_utc(); // the unwrap here is fine as we're certain `update_datetime` is in the future and thus the // resultant Duration is positive + #[allow(clippy::unwrap_used)] let time_left: Duration = (update_datetime - now).try_into().unwrap(); info!( diff --git a/nym-api/src/node_status_api/utils.rs b/nym-api/src/node_status_api/utils.rs index 4d86be5faa..2604254e92 100644 --- a/nym-api/src/node_status_api/utils.rs +++ b/nym-api/src/node_status_api/utils.rs @@ -109,6 +109,7 @@ impl NodeUptimes { // the unwraps in Uptime::from_ratio are fine because it's impossible for us to have more "up" results // than total test runs as we just bounded them + #[allow(clippy::unwrap_used)] NodeUptimes { most_recent: most_recent.try_into().unwrap(), last_hour: Uptime::from_uptime_sum(last_hour_sum, last_hour_test_runs).unwrap(), diff --git a/nym-api/src/nym_contract_cache/cache/refresher.rs b/nym-api/src/nym_contract_cache/cache/refresher.rs index e4f57dae63..787d299de5 100644 --- a/nym-api/src/nym_contract_cache/cache/refresher.rs +++ b/nym-api/src/nym_contract_cache/cache/refresher.rs @@ -128,6 +128,7 @@ impl NymContractCacheRefresher { .collect(); let mut gateways = Vec::with_capacity(gateway_bonds.len()); + #[allow(clippy::panic)] for bond in gateway_bonds { // we explicitly panic here because that value MUST exist. // if it doesn't, we messed up the migration and we have big problems diff --git a/nym-api/src/nym_nodes/handlers/unstable/helpers.rs b/nym-api/src/nym_nodes/handlers/unstable/helpers.rs index 38c981a97a..3940d99237 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/helpers.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/helpers.rs @@ -47,5 +47,8 @@ impl LegacyAnnotation for GatewayBondAnnotated { pub(crate) fn refreshed_at( iter: impl IntoIterator, ) -> OffsetDateTimeJsonSchemaWrapper { - iter.into_iter().min().unwrap().into() + iter.into_iter() + .min() + .unwrap_or(OffsetDateTime::UNIX_EPOCH) + .into() } diff --git a/nym-api/src/support/cli/run.rs b/nym-api/src/support/cli/run.rs index c506d32508..836f048930 100644 --- a/nym-api/src/support/cli/run.rs +++ b/nym-api/src/support/cli/run.rs @@ -108,7 +108,7 @@ pub(crate) struct Args { async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result { let task_manager = TaskManager::new(TASK_MANAGER_TIMEOUT_S); - let nyxd_client = nyxd::Client::new(config); + let nyxd_client = nyxd::Client::new(config)?; let connected_nyxd = config.get_nyxd_url(); let nym_network_details = NymNetworkDetails::new_from_env(); let network_details = NetworkDetails::new(connected_nyxd.to_string(), nym_network_details); diff --git a/nym-api/src/support/config/mod.rs b/nym-api/src/support/config/mod.rs index 75793726f4..0868f908c5 100644 --- a/nym-api/src/support/config/mod.rs +++ b/nym-api/src/support/config/mod.rs @@ -268,6 +268,8 @@ pub struct Base { impl Base { pub fn new_default>(id: S) -> Self { + // SAFETY: the provided hardcoded value is well-formed + #[allow(clippy::expect_used)] let default_validator: Url = DEFAULT_LOCAL_VALIDATOR .parse() .expect("default local validator is malformed!"); diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index 180d1810e4..01a32decec 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -4,7 +4,7 @@ use crate::ecash::error::EcashError; use crate::epoch_operations::RewardedNodeWithParams; use crate::support::config::Config; -use anyhow::Result; +use anyhow::{Context, Result}; use async_trait::async_trait; use cw3::{ProposalResponse, VoteResponse}; use cw4::MemberResponse; @@ -99,12 +99,13 @@ pub enum ClientInner { } impl Client { - pub(crate) fn new(config: &Config) -> Self { + pub(crate) fn new(config: &Config) -> anyhow::Result { let details = NymNetworkDetails::new_from_env(); let nyxd_url = config.get_nyxd_url(); - let client_config = nyxd::Config::try_from_nym_network_details(&details) - .expect("failed to construct valid validator client config with the provided network"); + let client_config = nyxd::Config::try_from_nym_network_details(&details).context( + "failed to construct valid validator client config with the provided network", + )?; let inner = if let Some(mnemonic) = config.get_mnemonic() { ClientInner::Signing( @@ -113,18 +114,18 @@ impl Client { nyxd_url.as_str(), mnemonic.clone(), ) - .expect("Failed to connect to nyxd!"), + .context("Failed to connect to nyxd!")?, ) } else { ClientInner::Query( QueryHttpRpcNyxdClient::connect(client_config, nyxd_url.as_str()) - .expect("Failed to connect to nyxd!"), + .context("Failed to connect to nyxd!")?, ) }; - Client { + Ok(Client { inner: Arc::new(RwLock::new(inner)), - } + }) } pub(crate) async fn read(&self) -> RwLockReadGuard<'_, ClientInner> { diff --git a/nym-api/src/support/storage/mod.rs b/nym-api/src/support/storage/mod.rs index e1d6b729c7..cc10d55e76 100644 --- a/nym-api/src/support/storage/mod.rs +++ b/nym-api/src/support/storage/mod.rs @@ -225,9 +225,9 @@ impl NymApiStorage { .get_monitor_runs_count(day_ago, now.unix_timestamp()) .await?; - let mixnode_identity = self.manager.get_mixnode_identity_key(mix_id).await?.expect( - "The node doesn't have an identity even though we have status information on it!", - ); + let Some(mixnode_identity) = self.manager.get_mixnode_identity_key(mix_id).await? else { + return Err(NymApiStorageError::DatabaseInconsistency { reason: format!("The node {mix_id} doesn't have an identity even though we have status information on it!") }); + }; Ok(MixnodeStatusReport::construct_from_last_day_reports( now, @@ -262,13 +262,9 @@ impl NymApiStorage { .get_monitor_runs_count(day_ago, now.unix_timestamp()) .await?; - let gateway_identity = self - .manager - .get_gateway_identity_key(node_id) - .await? - .expect( - "The node doesn't have an identity even though we have status information on it!", - ); + let Some(gateway_identity) = self.manager.get_gateway_identity_key(node_id).await? else { + return Err(NymApiStorageError::DatabaseInconsistency { reason: format!("The node {node_id} doesn't have an identity even though we have status information on it!") }); + }; Ok(GatewayStatusReport::construct_from_last_day_reports( now, @@ -290,10 +286,9 @@ impl NymApiStorage { return Err(NymApiStorageError::MixnodeUptimeHistoryNotFound { mix_id }); } - let mixnode_identity = - self.manager.get_mixnode_identity_key(mix_id).await?.expect( - "The node doesn't have an identity even though we have uptime history for it!", - ); + let Some(mixnode_identity) = self.manager.get_mixnode_identity_key(mix_id).await? else { + return Err(NymApiStorageError::DatabaseInconsistency { reason: format!("The node {mix_id} doesn't have an identity even though we uptime history for it!") }); + }; Ok(MixnodeUptimeHistory::new(mix_id, mixnode_identity, history)) } @@ -536,6 +531,10 @@ impl NymApiStorage { start: i64, end: i64, ) -> Result, NymApiStorageError> { + let Ok(end_timestamp) = OffsetDateTime::from_unix_timestamp(end) else { + return Err(NymApiStorageError::InvalidTimestampProvided { value: end }); + }; + if (end - start) as u64 != ONE_DAY.as_secs() { warn!("Our current interval length breaks the 24h length assumption") } @@ -553,7 +552,7 @@ impl NymApiStorage { .into_iter() .map(|statuses| { MixnodeStatusReport::construct_from_last_day_reports( - OffsetDateTime::from_unix_timestamp(end).unwrap(), + end_timestamp, statuses.mix_id, statuses.identity, statuses.statuses, @@ -579,6 +578,10 @@ impl NymApiStorage { start: i64, end: i64, ) -> Result, NymApiStorageError> { + let Ok(end_timestamp) = OffsetDateTime::from_unix_timestamp(end) else { + return Err(NymApiStorageError::InvalidTimestampProvided { value: end }); + }; + if (end - start) as u64 != ONE_DAY.as_secs() { warn!("Our current interval length breaks the 24h length assumption") } @@ -596,7 +599,7 @@ impl NymApiStorage { .into_iter() .map(|statuses| { GatewayStatusReport::construct_from_last_day_reports( - OffsetDateTime::from_unix_timestamp(end).unwrap(), + end_timestamp, statuses.node_id, statuses.identity, statuses.statuses, diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 6c65b81261..a794ca4e98 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -98,3 +98,7 @@ nym-ip-packet-router = { path = "../service-providers/ip-packet-router" } [build-dependencies] # temporary bonding information v1 (to grab and parse nym-mixnode and nym-gateway package versions) cargo_metadata = { workspace = true } + + +[lints] +workspace = true \ No newline at end of file diff --git a/nym-node/src/cli/commands/sign.rs b/nym-node/src/cli/commands/sign.rs index ab22077396..0b9fc56d5b 100644 --- a/nym-node/src/cli/commands/sign.rs +++ b/nym-node/src/cli/commands/sign.rs @@ -68,6 +68,8 @@ fn print_signed_contract_msg( println!("{}", output.format(&sign_output)); } +// SAFETY: clippy ArgGroup ensures only a single branch is actually called +#[allow(clippy::unreachable)] pub async fn execute(args: Args) -> Result<(), NymNodeError> { let config = try_load_current_config(args.config.config_path()).await?; let identity_keypair = diff --git a/nym-node/src/node/metrics/aggregator.rs b/nym-node/src/node/metrics/aggregator.rs index 3b3cc17eb3..7d67e925c2 100644 --- a/nym-node/src/node/metrics/aggregator.rs +++ b/nym-node/src/node/metrics/aggregator.rs @@ -45,6 +45,9 @@ impl MetricsAggregator { self.event_sender.clone() } + // we must panic here to terminate as soon as possible, because the underlying code + // has to be resolved as it implies some serious logic bugs + #[allow(clippy::panic)] pub fn register_handler(&mut self, handler: H, update_interval: impl Into>) where H: MetricsHandler, diff --git a/nym-node/src/node/metrics/handler/global_prometheus_updater/mod.rs b/nym-node/src/node/metrics/handler/global_prometheus_updater/mod.rs index 16587ead63..af7e907d48 100644 --- a/nym-node/src/node/metrics/handler/global_prometheus_updater/mod.rs +++ b/nym-node/src/node/metrics/handler/global_prometheus_updater/mod.rs @@ -226,6 +226,8 @@ impl OnUpdateMetricsHandler for PrometheusGlobalNodeMetricsRegistryUpdater { impl MetricsHandler for PrometheusGlobalNodeMetricsRegistryUpdater { type Events = GlobalPrometheusData; + // SAFETY: `PrometheusNodeMetricsRegistryUpdater` doesn't have any associated events + #[allow(clippy::panic)] async fn handle_event(&mut self, _event: Self::Events) { panic!("this should have never been called! MetricsHandler has been incorrectly called on PrometheusNodeMetricsRegistryUpdater") } diff --git a/nym-node/src/node/metrics/handler/legacy_packet_data.rs b/nym-node/src/node/metrics/handler/legacy_packet_data.rs index 6240993797..d45536e587 100644 --- a/nym-node/src/node/metrics/handler/legacy_packet_data.rs +++ b/nym-node/src/node/metrics/handler/legacy_packet_data.rs @@ -62,6 +62,8 @@ impl OnUpdateMetricsHandler for LegacyMixingStatsUpdater { impl MetricsHandler for LegacyMixingStatsUpdater { type Events = LegacyMixingData; + // SAFETY: `LegacyMixingStatsUpdater` doesn't have any associated events + #[allow(clippy::panic)] async fn handle_event(&mut self, _event: Self::Events) { panic!("this should have never been called! MetricsHandler has been incorrectly called on LegacyMixingStatsUpdater") } diff --git a/nym-node/src/node/metrics/handler/mixnet_data_cleaner.rs b/nym-node/src/node/metrics/handler/mixnet_data_cleaner.rs index 83c2041b5b..0c4db73333 100644 --- a/nym-node/src/node/metrics/handler/mixnet_data_cleaner.rs +++ b/nym-node/src/node/metrics/handler/mixnet_data_cleaner.rs @@ -101,6 +101,8 @@ impl OnUpdateMetricsHandler for MixnetMetricsCleaner { impl MetricsHandler for MixnetMetricsCleaner { type Events = StaleMixnetMetrics; + // SAFETY: `MixnetMetricsCleaner` doesn't have any associated events + #[allow(clippy::panic)] async fn handle_event(&mut self, _event: Self::Events) { panic!("this should have never been called! MetricsHandler has been incorrectly called on MixnetMetricsCleaner") } diff --git a/nym-node/src/node/metrics/handler/pending_egress_packets_updater.rs b/nym-node/src/node/metrics/handler/pending_egress_packets_updater.rs index d1fde40f70..f3a134cc37 100644 --- a/nym-node/src/node/metrics/handler/pending_egress_packets_updater.rs +++ b/nym-node/src/node/metrics/handler/pending_egress_packets_updater.rs @@ -54,6 +54,8 @@ impl OnUpdateMetricsHandler for PendingEgressPacketsUpdater { impl MetricsHandler for PendingEgressPacketsUpdater { type Events = PendingEgressPackets; + // SAFETY: `PendingEgressPacketsUpdater` doesn't have any associated events + #[allow(clippy::panic)] async fn handle_event(&mut self, _event: Self::Events) { panic!("this should have never been called! MetricsHandler has been incorrectly called on PendingEgressPacketsUpdater") } From e6f6e1342f2e39a4b043776e18114d59b10102a6 Mon Sep 17 00:00:00 2001 From: benedetta davico <46782255+benedettadavico@users.noreply.github.com> Date: Wed, 26 Feb 2025 12:25:46 +0100 Subject: [PATCH 011/133] Update ns-api version --- nym-node-status-api/nym-node-status-api/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nym-node-status-api/nym-node-status-api/Cargo.toml b/nym-node-status-api/nym-node-status-api/Cargo.toml index 7068e61f25..1e41d4d41d 100644 --- a/nym-node-status-api/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/nym-node-status-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node-status-api" -version = "1.0.0-rc.8" +version = "1.0.2" authors.workspace = true repository.workspace = true homepage.workspace = true From 41788095558ecfabebd7a1566ab6a176b22c18c0 Mon Sep 17 00:00:00 2001 From: Yana Date: Wed, 26 Feb 2025 18:53:08 +0200 Subject: [PATCH 012/133] Make "Memo" visible per default on send NYM --- nym-wallet/src/components/Send/SendInputModal.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/nym-wallet/src/components/Send/SendInputModal.tsx b/nym-wallet/src/components/Send/SendInputModal.tsx index 0ac77f4b28..c5bd398e85 100644 --- a/nym-wallet/src/components/Send/SendInputModal.tsx +++ b/nym-wallet/src/components/Send/SendInputModal.tsx @@ -121,6 +121,19 @@ export const SendInputModal = ({ initialValue={amount?.amount} denom={denom} /> + onMemoChange(e.target.value)} + value={memo} + error={!memoIsValid} + placeholder="Optional" + helperText={ + !memoIsValid ? ' The text is invalid, only alphanumeric characters and white spaces are allowed' : undefined + } + InputLabelProps={{ shrink: true }} + fullWidth + /> {error} From 856dbfe1ac0ff34c1ed5e1d395d8e40431ebd28d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Thu, 27 Feb 2025 15:21:55 +0100 Subject: [PATCH 013/133] IPR request types v8 (#5498) * IPR v8 request/response types * Remove signature for when we use sender tags * Remove unused * Address some review comments * Update license to GPL-3.0 for IPR Since the IPR can run as a binary, make sure it's license is GPL-3.0 * update cargo deny * Add back support for v6 * Tidy responses * Clippy * Fix compilation * Conversions * Conversions * Split response conversion * request split * Complete conversion switch * Remove commented out code * rustfmt * Remove unused conversions * Remove unused TryFrom * use from --- Cargo.lock | 3 +- common/ip-packet-requests/src/lib.rs | 18 +- .../src/{v7/signature.rs => sign.rs} | 27 +- .../ip-packet-requests/src/v6/conversion.rs | 69 -- common/ip-packet-requests/src/v6/mod.rs | 1 - .../ip-packet-requests/src/v7/conversion.rs | 125 --- common/ip-packet-requests/src/v7/mod.rs | 2 - common/ip-packet-requests/src/v7/request.rs | 86 +- common/ip-packet-requests/src/v8/mod.rs | 4 + common/ip-packet-requests/src/v8/request.rs | 536 ++++++++++ common/ip-packet-requests/src/v8/response.rs | 259 +++++ deny.toml | 1 + sdk/rust/nym-sdk/src/mixnet/native_client.rs | 19 +- .../authenticator/src/mixnet_listener.rs | 33 +- service-providers/ip-packet-router/Cargo.toml | 3 +- .../ip-packet-router/src/cli/add_gateway.rs | 2 +- .../cli/ecash/import_coin_index_signatures.rs | 2 +- .../src/cli/ecash/import_credential.rs | 2 +- .../import_expiration_date_signatures.rs | 2 +- .../ecash/import_master_verification_key.rs | 2 +- .../ip-packet-router/src/cli/ecash/mod.rs | 2 +- .../src/cli/ecash/show_ticketbooks.rs | 2 +- .../ip-packet-router/src/cli/list_gateways.rs | 2 +- .../src/cli/switch_gateway.rs | 2 +- .../ip-packet-router/src/clients/client_id.rs | 58 ++ .../{ => clients}/connected_client_handler.rs | 86 +- .../src/clients/connected_clients.rs | 257 +++++ .../ip-packet-router/src/clients/mod.rs | 12 + .../ip-packet-router/src/config/helpers.rs | 2 +- .../ip-packet-router/src/config/mod.rs | 1 + .../src/config/old_config_v1.rs | 3 +- .../src/config/persistence.rs | 2 +- .../ip-packet-router/src/config/template.rs | 2 +- .../ip-packet-router/src/constants.rs | 3 + .../ip-packet-router/src/error.rs | 11 +- .../ip-packet-router/src/ip_packet_router.rs | 22 +- service-providers/ip-packet-router/src/lib.rs | 23 +- .../ip-packet-router/src/main.rs | 3 + .../ip-packet-router/src/messages/mod.rs | 22 + .../ip-packet-router/src/messages/request.rs | 144 +++ .../src/messages/request/v6.rs | 100 ++ .../src/messages/request/v7.rs | 100 ++ .../src/messages/request/v8.rs | 180 ++++ .../ip-packet-router/src/messages/response.rs | 190 ++++ .../src/messages/response/v6.rs | 188 ++++ .../src/messages/response/v7.rs | 188 ++++ .../src/messages/response/v8.rs | 183 ++++ .../ip-packet-router/src/mixnet_client.rs | 3 + .../ip-packet-router/src/mixnet_listener.rs | 925 +++++------------- .../ip-packet-router/src/non_linux_dummy.rs | 4 +- .../src/request_filter/exit_policy/mod.rs | 2 +- .../src/request_filter/mod.rs | 2 +- .../ip-packet-router/src/tun_listener.rs | 42 +- .../src/util/create_message.rs | 15 +- 54 files changed, 2939 insertions(+), 1038 deletions(-) rename common/ip-packet-requests/src/{v7/signature.rs => sign.rs} (62%) delete mode 100644 common/ip-packet-requests/src/v6/conversion.rs delete mode 100644 common/ip-packet-requests/src/v7/conversion.rs create mode 100644 common/ip-packet-requests/src/v8/mod.rs create mode 100644 common/ip-packet-requests/src/v8/request.rs create mode 100644 common/ip-packet-requests/src/v8/response.rs create mode 100644 service-providers/ip-packet-router/src/clients/client_id.rs rename service-providers/ip-packet-router/src/{ => clients}/connected_client_handler.rs (62%) create mode 100644 service-providers/ip-packet-router/src/clients/connected_clients.rs create mode 100644 service-providers/ip-packet-router/src/clients/mod.rs create mode 100644 service-providers/ip-packet-router/src/messages/mod.rs create mode 100644 service-providers/ip-packet-router/src/messages/request.rs create mode 100644 service-providers/ip-packet-router/src/messages/request/v6.rs create mode 100644 service-providers/ip-packet-router/src/messages/request/v7.rs create mode 100644 service-providers/ip-packet-router/src/messages/request/v8.rs create mode 100644 service-providers/ip-packet-router/src/messages/response.rs create mode 100644 service-providers/ip-packet-router/src/messages/response/v6.rs create mode 100644 service-providers/ip-packet-router/src/messages/response/v7.rs create mode 100644 service-providers/ip-packet-router/src/messages/response/v8.rs diff --git a/Cargo.lock b/Cargo.lock index d9ee2ed197..dac0f0ff20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5955,7 +5955,6 @@ dependencies = [ "reqwest 0.12.4", "serde", "serde_json", - "tap", "thiserror 2.0.11", "time", "tokio", @@ -6289,7 +6288,7 @@ dependencies = [ [[package]] name = "nym-node-status-api" -version = "1.0.0-rc.8" +version = "1.0.2" dependencies = [ "ammonia", "anyhow", diff --git a/common/ip-packet-requests/src/lib.rs b/common/ip-packet-requests/src/lib.rs index 1da258c256..977a277446 100644 --- a/common/ip-packet-requests/src/lib.rs +++ b/common/ip-packet-requests/src/lib.rs @@ -2,24 +2,18 @@ use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use std::net::{Ipv4Addr, Ipv6Addr}; -// The current version of the protocol. -// The idea here is that we add new request response types at least one version before we start -// using them. -// Also, depending on the version in the client connect message the IPR could respond with a -// matching older version. -pub use v6::request; -pub use v6::response; - pub mod codec; +pub mod sign; pub mod v6; pub mod v7; +pub mod v8; // version 3: initial version // version 4: IPv6 support // version 5: Add severity level to info response // version 6: Increase the available IPs // version 7: Add signature support (for the future) -pub const CURRENT_VERSION: u8 = 6; +// version 8: Anonymous sends #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct IpPair { @@ -45,3 +39,9 @@ fn make_bincode_serializer() -> impl bincode::Options { .with_big_endian() .with_varint_encoding() } + +fn generate_random() -> u64 { + use rand::RngCore; + let mut rng = rand::rngs::OsRng; + rng.next_u64() +} diff --git a/common/ip-packet-requests/src/v7/signature.rs b/common/ip-packet-requests/src/sign.rs similarity index 62% rename from common/ip-packet-requests/src/v7/signature.rs rename to common/ip-packet-requests/src/sign.rs index e11de693b8..e678a9dc90 100644 --- a/common/ip-packet-requests/src/v7/signature.rs +++ b/common/ip-packet-requests/src/sign.rs @@ -1,6 +1,7 @@ use std::time::Duration; -use nym_crypto::asymmetric::identity; +use nym_crypto::asymmetric::ed25519; +use time::OffsetDateTime; // For reply protection, if a request is older than this, it will be rejected const MAX_REQUEST_AGE: Duration = Duration::from_secs(10); @@ -22,29 +23,37 @@ pub enum SignatureError { #[error("signature verification failed")] VerificationFailed { message: String, - error: identity::SignatureError, + error: ed25519::SignatureError, }, } pub trait SignedRequest { - fn identity(&self) -> &identity::PublicKey; + fn identity(&self) -> Option<&ed25519::PublicKey>; - fn request(&self) -> Result, SignatureError>; + fn request_as_bytes(&self) -> Result, SignatureError>; - fn signature(&self) -> Option<&identity::Signature>; + fn signature(&self) -> Option<&ed25519::Signature>; - fn timestamp(&self) -> time::OffsetDateTime; + fn timestamp(&self) -> OffsetDateTime; fn verify(&self) -> Result<(), SignatureError> { + let identity = match self.identity() { + Some(identity) => identity, + None => { + // If we are not revealing our identity, we don't need to verify anything + return Ok(()); + } + }; + if let Some(signature) = self.signature() { // First check that the request is recent enough - if time::OffsetDateTime::now_utc() - self.timestamp() > MAX_REQUEST_AGE { + if OffsetDateTime::now_utc() - self.timestamp() > MAX_REQUEST_AGE { return Err(SignatureError::RequestOutOfDate); } - let request_as_bytes = self.request()?; + let request_as_bytes = self.request_as_bytes()?; - self.identity() + identity .verify(request_as_bytes, signature) .map_err(|error| SignatureError::VerificationFailed { message: "signature verification failed".to_string(), diff --git a/common/ip-packet-requests/src/v6/conversion.rs b/common/ip-packet-requests/src/v6/conversion.rs deleted file mode 100644 index be2327b2e6..0000000000 --- a/common/ip-packet-requests/src/v6/conversion.rs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::{v6, v7}; - -impl From for v6::response::StaticConnectFailureReason { - fn from(failure: v7::response::StaticConnectFailureReason) -> Self { - match failure { - v7::response::StaticConnectFailureReason::RequestedIpAlreadyInUse => { - v6::response::StaticConnectFailureReason::RequestedIpAlreadyInUse - } - v7::response::StaticConnectFailureReason::RequestedNymAddressAlreadyInUse => { - v6::response::StaticConnectFailureReason::RequestedNymAddressAlreadyInUse - } - v7::response::StaticConnectFailureReason::OutOfDateTimestamp => { - v6::response::StaticConnectFailureReason::Other("out of date timestamp".to_string()) - } - v7::response::StaticConnectFailureReason::Other(reason) => { - v6::response::StaticConnectFailureReason::Other(reason) - } - } - } -} - -impl From for v6::response::DynamicConnectFailureReason { - fn from(failure: v7::response::DynamicConnectFailureReason) -> Self { - match failure { - v7::response::DynamicConnectFailureReason::RequestedNymAddressAlreadyInUse => { - v6::response::DynamicConnectFailureReason::RequestedNymAddressAlreadyInUse - } - v7::response::DynamicConnectFailureReason::NoAvailableIp => { - v6::response::DynamicConnectFailureReason::NoAvailableIp - } - v7::response::DynamicConnectFailureReason::Other(err) => { - v6::response::DynamicConnectFailureReason::Other(err) - } - } - } -} - -impl From for v6::response::InfoResponseReply { - fn from(reply: v7::response::InfoResponseReply) -> Self { - match reply { - v7::response::InfoResponseReply::Generic { msg } => { - v6::response::InfoResponseReply::Generic { msg } - } - v7::response::InfoResponseReply::VersionMismatch { - request_version, - response_version, - } => v6::response::InfoResponseReply::VersionMismatch { - request_version, - response_version, - }, - v7::response::InfoResponseReply::ExitPolicyFilterCheckFailed { dst } => { - v6::response::InfoResponseReply::ExitPolicyFilterCheckFailed { dst } - } - } - } -} - -impl From for v6::response::InfoLevel { - fn from(level: v7::response::InfoLevel) -> Self { - match level { - v7::response::InfoLevel::Info => v6::response::InfoLevel::Info, - v7::response::InfoLevel::Warn => v6::response::InfoLevel::Warn, - v7::response::InfoLevel::Error => v6::response::InfoLevel::Error, - } - } -} diff --git a/common/ip-packet-requests/src/v6/mod.rs b/common/ip-packet-requests/src/v6/mod.rs index ae7394c02d..8f3f1e3955 100644 --- a/common/ip-packet-requests/src/v6/mod.rs +++ b/common/ip-packet-requests/src/v6/mod.rs @@ -1,4 +1,3 @@ -pub mod conversion; pub mod request; pub mod response; diff --git a/common/ip-packet-requests/src/v7/conversion.rs b/common/ip-packet-requests/src/v7/conversion.rs deleted file mode 100644 index 04b0244b05..0000000000 --- a/common/ip-packet-requests/src/v7/conversion.rs +++ /dev/null @@ -1,125 +0,0 @@ -use time::OffsetDateTime; - -use crate::{v6, v7}; - -impl From for v7::request::IpPacketRequest { - fn from(ip_packet_request: v6::request::IpPacketRequest) -> Self { - Self { - version: 7, - data: ip_packet_request.data.into(), - } - } -} - -impl From for v7::request::IpPacketRequestData { - fn from(ip_packet_request_data: v6::request::IpPacketRequestData) -> Self { - match ip_packet_request_data { - v6::request::IpPacketRequestData::StaticConnect(r) => { - v7::request::IpPacketRequestData::StaticConnect( - v7::request::SignedStaticConnectRequest { - request: r.into(), - signature: None, - }, - ) - } - v6::request::IpPacketRequestData::DynamicConnect(r) => { - v7::request::IpPacketRequestData::DynamicConnect( - v7::request::SignedDynamicConnectRequest { - request: r.into(), - signature: None, - }, - ) - } - v6::request::IpPacketRequestData::Disconnect(r) => { - v7::request::IpPacketRequestData::Disconnect(v7::request::SignedDisconnectRequest { - request: r.into(), - signature: None, - }) - } - v6::request::IpPacketRequestData::Data(r) => { - v7::request::IpPacketRequestData::Data(r.into()) - } - v6::request::IpPacketRequestData::Ping(r) => { - v7::request::IpPacketRequestData::Ping(r.into()) - } - v6::request::IpPacketRequestData::Health(r) => { - v7::request::IpPacketRequestData::Health(r.into()) - } - } - } -} - -impl From for v7::request::StaticConnectRequest { - fn from(static_connect_request: v6::request::StaticConnectRequest) -> Self { - Self { - request_id: static_connect_request.request_id, - ips: static_connect_request.ips, - reply_to: static_connect_request.reply_to, - reply_to_hops: static_connect_request.reply_to_hops, - reply_to_avg_mix_delays: static_connect_request.reply_to_avg_mix_delays, - buffer_timeout: static_connect_request.buffer_timeout, - timestamp: OffsetDateTime::now_utc(), - } - } -} - -#[allow(deprecated)] -impl From for v7::request::DynamicConnectRequest { - fn from(dynamic_connect_request: v6::request::DynamicConnectRequest) -> Self { - Self { - request_id: dynamic_connect_request.request_id, - reply_to: dynamic_connect_request.reply_to, - reply_to_hops: dynamic_connect_request.reply_to_hops, - reply_to_avg_mix_delays: dynamic_connect_request.reply_to_avg_mix_delays, - buffer_timeout: dynamic_connect_request.buffer_timeout, - timestamp: OffsetDateTime::now_utc(), - } - } -} - -impl From for v7::request::SignedDisconnectRequest { - fn from(disconnect_request: v6::request::DisconnectRequest) -> Self { - Self { - request: disconnect_request.into(), - signature: None, - } - } -} - -impl From for v7::request::DisconnectRequest { - fn from(disconnect_request: v6::request::DisconnectRequest) -> Self { - Self { - request_id: disconnect_request.request_id, - reply_to: disconnect_request.reply_to, - timestamp: OffsetDateTime::now_utc(), - } - } -} - -impl From for v7::request::DataRequest { - fn from(data_request: v6::request::DataRequest) -> Self { - Self { - ip_packets: data_request.ip_packets, - } - } -} - -impl From for v7::request::PingRequest { - fn from(ping_request: v6::request::PingRequest) -> Self { - Self { - request_id: ping_request.request_id, - reply_to: ping_request.reply_to, - timestamp: OffsetDateTime::now_utc(), - } - } -} - -impl From for v7::request::HealthRequest { - fn from(health_request: v6::request::HealthRequest) -> Self { - Self { - request_id: health_request.request_id, - reply_to: health_request.reply_to, - timestamp: OffsetDateTime::now_utc(), - } - } -} diff --git a/common/ip-packet-requests/src/v7/mod.rs b/common/ip-packet-requests/src/v7/mod.rs index e0e61133f1..f15da6711a 100644 --- a/common/ip-packet-requests/src/v7/mod.rs +++ b/common/ip-packet-requests/src/v7/mod.rs @@ -1,6 +1,4 @@ -pub mod conversion; pub mod request; pub mod response; -pub mod signature; pub const VERSION: u8 = 7; diff --git a/common/ip-packet-requests/src/v7/request.rs b/common/ip-packet-requests/src/v7/request.rs index 43b71bb362..2313c16942 100644 --- a/common/ip-packet-requests/src/v7/request.rs +++ b/common/ip-packet-requests/src/v7/request.rs @@ -1,22 +1,18 @@ +use std::fmt; + use nym_crypto::asymmetric::identity; use nym_sphinx::addressing::clients::Recipient; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; -use crate::{make_bincode_serializer, IpPair}; - -use super::{ - signature::{SignatureError, SignedRequest}, - VERSION, +use crate::{ + sign::{SignatureError, SignedRequest}, + IpPair, }; -fn generate_random() -> u64 { - use rand::RngCore; - let mut rng = rand::rngs::OsRng; - rng.next_u64() -} +use super::VERSION; -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct IpPacketRequest { pub version: u8, pub data: IpPacketRequestData, @@ -30,7 +26,7 @@ impl IpPacketRequest { reply_to_avg_mix_delays: Option, buffer_timeout: Option, ) -> (Self, u64) { - let request_id = generate_random(); + let request_id = crate::generate_random(); ( Self { version: VERSION, @@ -58,7 +54,7 @@ impl IpPacketRequest { reply_to_avg_mix_delays: Option, buffer_timeout: Option, ) -> (Self, u64) { - let request_id = generate_random(); + let request_id = crate::generate_random(); ( Self { version: VERSION, @@ -79,7 +75,7 @@ impl IpPacketRequest { } pub fn new_disconnect_request(reply_to: Recipient) -> (Self, u64) { - let request_id = generate_random(); + let request_id = crate::generate_random(); ( Self { version: VERSION, @@ -104,7 +100,7 @@ impl IpPacketRequest { } pub fn new_ping(reply_to: Recipient) -> (Self, u64) { - let request_id = generate_random(); + let request_id = crate::generate_random(); ( Self { version: VERSION, @@ -119,7 +115,7 @@ impl IpPacketRequest { } pub fn new_health_request(reply_to: Recipient) -> (Self, u64) { - let request_id = generate_random(); + let request_id = crate::generate_random(); ( Self { version: VERSION, @@ -155,16 +151,27 @@ impl IpPacketRequest { } } + pub fn verify(&self) -> Result<(), SignatureError> { + match &self.data { + IpPacketRequestData::StaticConnect(request) => request.verify(), + IpPacketRequestData::DynamicConnect(request) => request.verify(), + IpPacketRequestData::Disconnect(request) => request.verify(), + IpPacketRequestData::Data(_) => Ok(()), + IpPacketRequestData::Ping(_) => Ok(()), + IpPacketRequestData::Health(_) => Ok(()), + } + } + pub fn to_bytes(&self) -> Result, bincode::Error> { use bincode::Options; - make_bincode_serializer().serialize(self) + crate::make_bincode_serializer().serialize(self) } pub fn from_reconstructed_message( message: &nym_sphinx::receiver::ReconstructedMessage, ) -> Result { use bincode::Options; - make_bincode_serializer().deserialize(&message.message) + crate::make_bincode_serializer().deserialize(&message.message) } } @@ -179,6 +186,19 @@ pub enum IpPacketRequestData { Health(HealthRequest), } +impl fmt::Display for IpPacketRequestData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + IpPacketRequestData::StaticConnect(_) => write!(f, "StaticConnect"), + IpPacketRequestData::DynamicConnect(_) => write!(f, "DynamicConnect"), + IpPacketRequestData::Disconnect(_) => write!(f, "Disconnect"), + IpPacketRequestData::Data(_) => write!(f, "Data"), + IpPacketRequestData::Ping(_) => write!(f, "Ping"), + IpPacketRequestData::Health(_) => write!(f, "Health"), + } + } +} + impl IpPacketRequestData { pub fn add_signature(&mut self, signature: identity::Signature) -> Option { match self { @@ -202,9 +222,9 @@ impl IpPacketRequestData { pub fn signable_request(&self) -> Option, SignatureError>> { match self { - IpPacketRequestData::StaticConnect(request) => Some(request.request()), - IpPacketRequestData::DynamicConnect(request) => Some(request.request()), - IpPacketRequestData::Disconnect(request) => Some(request.request()), + IpPacketRequestData::StaticConnect(request) => Some(request.request_as_bytes()), + IpPacketRequestData::DynamicConnect(request) => Some(request.request_as_bytes()), + IpPacketRequestData::Disconnect(request) => Some(request.request_as_bytes()), IpPacketRequestData::Data(_) => None, IpPacketRequestData::Ping(_) => None, IpPacketRequestData::Health(_) => None, @@ -242,7 +262,7 @@ pub struct StaticConnectRequest { impl StaticConnectRequest { pub fn to_bytes(&self) -> Result, bincode::Error> { use bincode::Options; - make_bincode_serializer().serialize(self) + crate::make_bincode_serializer().serialize(self) } } @@ -253,11 +273,11 @@ pub struct SignedStaticConnectRequest { } impl SignedRequest for SignedStaticConnectRequest { - fn identity(&self) -> &identity::PublicKey { - self.request.reply_to.identity() + fn identity(&self) -> Option<&identity::PublicKey> { + Some(self.request.reply_to.identity()) } - fn request(&self) -> Result, SignatureError> { + fn request_as_bytes(&self) -> Result, SignatureError> { self.request .to_bytes() .map_err(|error| SignatureError::RequestSerializationError { @@ -306,7 +326,7 @@ pub struct DynamicConnectRequest { impl DynamicConnectRequest { pub fn to_bytes(&self) -> Result, bincode::Error> { use bincode::Options; - make_bincode_serializer().serialize(self) + crate::make_bincode_serializer().serialize(self) } } @@ -317,11 +337,11 @@ pub struct SignedDynamicConnectRequest { } impl SignedRequest for SignedDynamicConnectRequest { - fn identity(&self) -> &identity::PublicKey { - self.request.reply_to.identity() + fn identity(&self) -> Option<&identity::PublicKey> { + Some(self.request.reply_to.identity()) } - fn request(&self) -> Result, SignatureError> { + fn request_as_bytes(&self) -> Result, SignatureError> { self.request .to_bytes() .map_err(|error| SignatureError::RequestSerializationError { @@ -355,7 +375,7 @@ pub struct DisconnectRequest { impl DisconnectRequest { pub fn to_bytes(&self) -> Result, bincode::Error> { use bincode::Options; - make_bincode_serializer().serialize(self) + crate::make_bincode_serializer().serialize(self) } } @@ -366,11 +386,11 @@ pub struct SignedDisconnectRequest { } impl SignedRequest for SignedDisconnectRequest { - fn identity(&self) -> &identity::PublicKey { - self.request.reply_to.identity() + fn identity(&self) -> Option<&identity::PublicKey> { + Some(self.request.reply_to.identity()) } - fn request(&self) -> Result, SignatureError> { + fn request_as_bytes(&self) -> Result, SignatureError> { self.request .to_bytes() .map_err(|error| SignatureError::RequestSerializationError { diff --git a/common/ip-packet-requests/src/v8/mod.rs b/common/ip-packet-requests/src/v8/mod.rs new file mode 100644 index 0000000000..70d1147b3a --- /dev/null +++ b/common/ip-packet-requests/src/v8/mod.rs @@ -0,0 +1,4 @@ +pub mod request; +pub mod response; + +pub const VERSION: u8 = 8; diff --git a/common/ip-packet-requests/src/v8/request.rs b/common/ip-packet-requests/src/v8/request.rs new file mode 100644 index 0000000000..15cf47827f --- /dev/null +++ b/common/ip-packet-requests/src/v8/request.rs @@ -0,0 +1,536 @@ +use std::{fmt, sync::Arc}; + +use nym_crypto::asymmetric::ed25519; +use nym_sphinx::addressing::Recipient; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +use crate::{ + sign::{SignatureError, SignedRequest}, + IpPair, +}; + +use super::VERSION; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct IpPacketRequest { + pub version: u8, + pub data: IpPacketRequestData, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum IpPacketRequestData { + Data(DataRequest), + Control(Box), +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum ControlRequest { + StaticConnect(SignedStaticConnectRequest), + DynamicConnect(SignedDynamicConnectRequest), + Disconnect(SignedDisconnectRequest), + Ping(PingRequest), + Health(HealthRequest), +} + +// A data request is when the client wants to send an IP packet to a destination. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct DataRequest { + pub ip_packets: bytes::Bytes, +} + +// A static connect request is when the client provides the internal IP address it will use on the +// ip packet router. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct StaticConnectRequest { + pub request_id: u64, + + // The requested internal IP addresses. + pub ips: IpPair, + + // The maximum time in milliseconds the IPR should wait when filling up a mix packet + // with ip packets. + pub buffer_timeout: Option, + + // Timestamp of when the request was sent by the client. + pub timestamp: OffsetDateTime, + + pub sender: SentBy, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct SignedStaticConnectRequest { + pub request: StaticConnectRequest, + pub signature: Option, +} + +// A dynamic connect request is when the client does not provide the internal IP address it will use +// on the ip packet router, and instead requests one to be assigned to it. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct DynamicConnectRequest { + pub request_id: u64, + + // The maximum time in milliseconds the IPR should wait when filling up a mix packet + // with ip packets. + pub buffer_timeout: Option, + + // Timestamp of when the request was sent by the client. + pub timestamp: OffsetDateTime, + + pub sender: SentBy, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct SignedDynamicConnectRequest { + pub request: DynamicConnectRequest, + pub signature: Option, +} + +// A disconnect request is when the client wants to disconnect from the ip packet router and free +// up the allocated IP address. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct DisconnectRequest { + pub request_id: u64, + + // Timestamp of when the request was sent by the client. + pub timestamp: OffsetDateTime, + + pub sender: SentBy, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct SignedDisconnectRequest { + pub request: DisconnectRequest, + pub signature: Option, +} + +// A ping request is when the client wants to check if the ip packet router is still alive. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct PingRequest { + pub request_id: u64, + + pub sender: SentBy, + + // Timestamp of when the request was sent by the client. + pub timestamp: OffsetDateTime, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct HealthRequest { + pub request_id: u64, + + pub sender: SentBy, + + // Timestamp of when the request was sent by the client. + pub timestamp: OffsetDateTime, +} + +// When constructing the request, use this return address. It has the keypair to be able to sign +// the request if we reveal the sender. +#[derive(Clone, Debug)] +pub enum ReturnAddress { + AnonymousSenderTag, + NymAddress { + reply_to: Box, + signing_keypair: Arc, + }, +} + +// The serialized sender field in the request, that does not contain the keypair. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum SentBy { + AnonymousSenderTag, + NymAddress(Box), +} + +impl IpPacketRequest { + pub fn new_connect_request( + ips: Option, + buffer_timeout: Option, + return_address: ReturnAddress, + ) -> Result<(Self, u64), SignatureError> { + let request_id = rand::random(); + let timestamp = OffsetDateTime::now_utc(); + let sender = return_address.clone().into(); + let request = if let Some(ips) = ips { + let request = StaticConnectRequest { + request_id, + ips, + buffer_timeout, + timestamp, + sender, + }; + let signature = return_address + .signing_key() + .map(|keypair| { + request + .to_bytes() + .map(|bytes| keypair.private_key().sign(bytes)) + }) + .transpose()?; + ControlRequest::StaticConnect(SignedStaticConnectRequest { request, signature }) + } else { + let request = DynamicConnectRequest { + request_id, + buffer_timeout, + timestamp, + sender, + }; + let signature = return_address + .signing_key() + .map(|keypair| { + request + .to_bytes() + .map(|bytes| keypair.private_key().sign(bytes)) + }) + .transpose()?; + ControlRequest::DynamicConnect(SignedDynamicConnectRequest { request, signature }) + }; + let request = Self { + version: VERSION, + data: IpPacketRequestData::Control(Box::new(request)), + }; + Ok((request, request_id)) + } + + pub fn new_disconnect_request( + return_address: ReturnAddress, + ) -> Result<(Self, u64), SignatureError> { + let request_id = rand::random(); + let timestamp = OffsetDateTime::now_utc(); + let sender = return_address.clone().into(); + let request = DisconnectRequest { + request_id, + timestamp, + sender, + }; + let signature = return_address + .signing_key() + .map(|keypair| { + request + .to_bytes() + .map(|bytes| keypair.private_key().sign(bytes)) + }) + .transpose()?; + let request = Self { + version: VERSION, + data: IpPacketRequestData::Control(Box::new(ControlRequest::Disconnect( + SignedDisconnectRequest { request, signature }, + ))), + }; + Ok((request, request_id)) + } + + pub fn new_data_request(ip_packets: bytes::Bytes) -> Self { + Self { + version: VERSION, + data: IpPacketRequestData::Data(DataRequest { ip_packets }), + } + } + + pub fn new_ping(return_address: ReturnAddress) -> (Self, u64) { + let request_id = rand::random(); + let timestamp = OffsetDateTime::now_utc(); + let sender = return_address.into(); + let ping_request = PingRequest { + request_id, + sender, + timestamp, + }; + let request = Self { + version: VERSION, + data: IpPacketRequestData::Control(Box::new(ControlRequest::Ping(ping_request))), + }; + (request, request_id) + } + + pub fn new_health_request(return_address: ReturnAddress) -> (Self, u64) { + let request_id = rand::random(); + let timestamp = OffsetDateTime::now_utc(); + let sender = return_address.into(); + let health_request = HealthRequest { + request_id, + sender, + timestamp, + }; + let request = Self { + version: VERSION, + data: IpPacketRequestData::Control(Box::new(ControlRequest::Health(health_request))), + }; + (request, request_id) + } + + pub fn id(&self) -> Option { + match self.data { + IpPacketRequestData::Control(ref c) => Some(c.id()), + IpPacketRequestData::Data(_) => None, + } + } + + pub fn verify(&self) -> Result<(), SignatureError> { + match &self.data { + IpPacketRequestData::Control(c) => c.verify(), + IpPacketRequestData::Data(_) => Ok(()), + } + } + + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + crate::make_bincode_serializer().serialize(self) + } + + pub fn from_reconstructed_message( + message: &nym_sphinx::receiver::ReconstructedMessage, + ) -> Result { + use bincode::Options; + crate::make_bincode_serializer().deserialize(&message.message) + } +} + +impl fmt::Display for IpPacketRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "IpPacketRequest {{ version: {}, data: {} }}", + self.version, self.data + ) + } +} + +impl fmt::Display for IpPacketRequestData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + IpPacketRequestData::Data(_) => write!(f, "Data"), + IpPacketRequestData::Control(c) => write!(f, "Control({})", c), + } + } +} + +impl ControlRequest { + fn id(&self) -> u64 { + match self { + ControlRequest::StaticConnect(request) => request.request.request_id, + ControlRequest::DynamicConnect(request) => request.request.request_id, + ControlRequest::Disconnect(request) => request.request.request_id, + ControlRequest::Ping(request) => request.request_id, + ControlRequest::Health(request) => request.request_id, + } + } + + fn verify(&self) -> Result<(), SignatureError> { + match self { + ControlRequest::StaticConnect(request) => request.verify(), + ControlRequest::DynamicConnect(request) => request.verify(), + ControlRequest::Disconnect(request) => request.verify(), + ControlRequest::Ping(_) => Ok(()), + ControlRequest::Health(_) => Ok(()), + } + } +} + +impl fmt::Display for ControlRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ControlRequest::StaticConnect(_) => write!(f, "StaticConnect"), + ControlRequest::DynamicConnect(_) => write!(f, "DynamicConnect"), + ControlRequest::Disconnect(_) => write!(f, "Disconnect"), + ControlRequest::Ping(_) => write!(f, "Ping"), + ControlRequest::Health(_) => write!(f, "Health"), + } + } +} + +impl StaticConnectRequest { + pub fn to_bytes(&self) -> Result, SignatureError> { + use bincode::Options; + crate::make_bincode_serializer() + .serialize(self) + .map_err(|error| SignatureError::RequestSerializationError { + message: "failed to serialize request to binary".to_string(), + error, + }) + } +} + +impl SignedRequest for SignedStaticConnectRequest { + fn request_as_bytes(&self) -> Result, SignatureError> { + self.request.to_bytes() + } + + fn timestamp(&self) -> OffsetDateTime { + self.request.timestamp + } + + fn identity(&self) -> Option<&ed25519::PublicKey> { + self.request.sender.identity() + } + + fn signature(&self) -> Option<&ed25519::Signature> { + self.signature.as_ref() + } +} + +impl DynamicConnectRequest { + pub fn to_bytes(&self) -> Result, SignatureError> { + use bincode::Options; + crate::make_bincode_serializer() + .serialize(self) + .map_err(|error| SignatureError::RequestSerializationError { + message: "failed to serialize request to binary".to_string(), + error, + }) + } +} + +impl SignedRequest for SignedDynamicConnectRequest { + fn request_as_bytes(&self) -> Result, SignatureError> { + self.request.to_bytes() + } + + fn timestamp(&self) -> OffsetDateTime { + self.request.timestamp + } + + fn identity(&self) -> Option<&ed25519::PublicKey> { + self.request.sender.identity() + } + + fn signature(&self) -> Option<&ed25519::Signature> { + self.signature.as_ref() + } +} + +impl DisconnectRequest { + pub fn to_bytes(&self) -> Result, SignatureError> { + use bincode::Options; + crate::make_bincode_serializer() + .serialize(self) + .map_err(|error| SignatureError::RequestSerializationError { + message: "failed to serialize request to binary".to_string(), + error, + }) + } +} + +impl SignedRequest for SignedDisconnectRequest { + fn request_as_bytes(&self) -> Result, SignatureError> { + self.request.to_bytes() + } + + fn timestamp(&self) -> OffsetDateTime { + self.request.timestamp + } + + fn identity(&self) -> Option<&ed25519::PublicKey> { + self.request.sender.identity() + } + + fn signature(&self) -> Option<&ed25519::Signature> { + self.signature.as_ref() + } +} + +impl SentBy { + fn identity(&self) -> Option<&ed25519::PublicKey> { + match self { + SentBy::AnonymousSenderTag => None, + SentBy::NymAddress(recipient) => Some(recipient.identity()), + } + } +} + +impl From for SentBy { + fn from(recipient: Recipient) -> Self { + SentBy::NymAddress(Box::new(recipient)) + } +} + +impl ReturnAddress { + fn signing_key(&self) -> Option<&ed25519::KeyPair> { + match self { + ReturnAddress::AnonymousSenderTag => None, + ReturnAddress::NymAddress { + signing_keypair, .. + } => Some(signing_keypair.as_ref()), + } + } +} + +impl From for SentBy { + fn from(return_address: ReturnAddress) -> Self { + match return_address { + ReturnAddress::AnonymousSenderTag => SentBy::AnonymousSenderTag, + ReturnAddress::NymAddress { reply_to, .. } => SentBy::NymAddress(reply_to), + } + } +} + +#[cfg(test)] +mod tests { + use time::macros::datetime; + + use super::*; + use std::net::{Ipv4Addr, Ipv6Addr}; + use std::str::FromStr; + + #[test] + fn check_size_of_request() { + let connect = IpPacketRequest { + version: 4, + data: IpPacketRequestData::Control(Box::new(ControlRequest::StaticConnect( + SignedStaticConnectRequest { + request: StaticConnectRequest { + request_id: 123, + ips: IpPair::new( + Ipv4Addr::from_str("10.0.0.1").unwrap(), + Ipv6Addr::from_str("fc00::1").unwrap(), + ), + buffer_timeout: None, + timestamp: datetime!(2024-01-01 12:59:59.5 UTC), + sender: SentBy::AnonymousSenderTag, + }, + signature: None, + }, + ))), + }; + assert_eq!(connect.to_bytes().unwrap().len(), 42); + } + + #[test] + fn check_size_of_data() { + let data = IpPacketRequest { + version: 4, + data: IpPacketRequestData::Data(DataRequest { + ip_packets: bytes::Bytes::from(vec![1u8; 32]), + }), + }; + assert_eq!(data.to_bytes().unwrap().len(), 35); + } + + #[test] + fn serialize_and_deserialize_data_request() { + let data = IpPacketRequest { + version: 4, + data: IpPacketRequestData::Data(DataRequest { + ip_packets: bytes::Bytes::from(vec![1, 2, 4, 2, 5]), + }), + }; + + let serialized = data.to_bytes().unwrap(); + let deserialized = IpPacketRequest::from_reconstructed_message( + &nym_sphinx::receiver::ReconstructedMessage { + message: serialized, + sender_tag: None, + }, + ) + .unwrap(); + + assert_eq!(deserialized.version, 4); + assert_eq!( + deserialized.data, + IpPacketRequestData::Data(DataRequest { + ip_packets: bytes::Bytes::from(vec![1, 2, 4, 2, 5]), + }) + ); + } +} diff --git a/common/ip-packet-requests/src/v8/response.rs b/common/ip-packet-requests/src/v8/response.rs new file mode 100644 index 0000000000..a919d2110f --- /dev/null +++ b/common/ip-packet-requests/src/v8/response.rs @@ -0,0 +1,259 @@ +use nym_bin_common::build_information::BinaryBuildInformationOwned; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, IpPair}; + +use super::VERSION; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct IpPacketResponse { + pub version: u8, + pub data: IpPacketResponseData, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum IpPacketResponseData { + Data(DataResponse), + Control(Box), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DataResponse { + pub ip_packet: bytes::Bytes, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum ControlResponse { + // Response for a static connect request + StaticConnect(StaticConnectResponse), + + // Response for a dynamic connect request + DynamicConnect(DynamicConnectResponse), + + // Response for a disconnect initiqated by the client + Disconnect(DisconnectResponse), + + // Message from the server that the client got disconnected without the client initiating it + UnrequestedDisconnect(UnrequestedDisconnect), + + // Response to ping request + Pong(PongResponse), + + // Response for a health request + Health(Box), + + // Info response. This can be anything from informative messages to errors + Info(InfoResponse), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct StaticConnectResponse { + pub request_id: u64, + pub reply: StaticConnectResponseReply, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum StaticConnectResponseReply { + Success, + Failure(StaticConnectFailureReason), +} + +#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] +pub enum StaticConnectFailureReason { + #[error("requested ip address is already in use")] + RequestedIpAlreadyInUse, + + #[error("client is already connected")] + ClientAlreadyConnected, + + #[error("request timestamp is out of date")] + OutOfDateTimestamp, + + #[error("{0}")] + Other(String), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DynamicConnectResponse { + pub request_id: u64, + pub reply: DynamicConnectResponseReply, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum DynamicConnectResponseReply { + Success(DynamicConnectSuccess), + Failure(DynamicConnectFailureReason), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DynamicConnectSuccess { + pub ips: IpPair, +} + +#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] +pub enum DynamicConnectFailureReason { + #[error("client is already connected")] + ClientAlreadyConnected, + + #[error("no available ip address")] + NoAvailableIp, + + #[error("{0}")] + Other(String), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DisconnectResponse { + pub request_id: u64, + pub reply: DisconnectResponseReply, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum DisconnectResponseReply { + Success, + Failure(DisconnectFailureReason), +} + +#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] +pub enum DisconnectFailureReason { + #[error("client is not connected")] + ClientNotConnected, + + #[error("{0}")] + Other(String), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct UnrequestedDisconnect { + pub reason: UnrequestedDisconnectReason, +} + +#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] +pub enum UnrequestedDisconnectReason { + #[error("client mixnet traffic timeout")] + ClientMixnetTrafficTimeout, + + #[error("client tun traffic timeout")] + ClientTunTrafficTimeout, + + #[error("{0}")] + Other(String), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PongResponse { + pub request_id: u64, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct HealthResponse { + pub request_id: u64, + pub reply: HealthResponseReply, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct HealthResponseReply { + // Return the binary build information of the IPR + pub build_info: BinaryBuildInformationOwned, + + // Return if the IPR has performed a successful routing test. + pub routable: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct InfoResponse { + pub request_id: u64, + pub reply: InfoResponseReply, + pub level: InfoLevel, +} + +#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] +pub enum InfoResponseReply { + #[error("{msg}")] + Generic { msg: String }, + + #[error( + "version mismatch: response is v{request_version} and response is v{response_version}" + )] + VersionMismatch { + request_version: u8, + response_version: u8, + }, + + #[error("destination failed exit policy filter check: {dst}")] + ExitPolicyFilterCheckFailed { dst: String }, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum InfoLevel { + Info, + Warn, + Error, +} + +impl IpPacketResponse { + pub fn new_ip_packet(ip_packet: bytes::Bytes) -> Self { + Self { + version: VERSION, + data: IpPacketResponseData::Data(DataResponse { ip_packet }), + } + } + + pub fn id(&self) -> Option { + match &self.data { + IpPacketResponseData::Data(_) => None, + IpPacketResponseData::Control(response) => response.id(), + } + } + + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + make_bincode_serializer().serialize(self) + } + + pub fn from_reconstructed_message( + message: &nym_sphinx::receiver::ReconstructedMessage, + ) -> Result { + use bincode::Options; + make_bincode_serializer().deserialize(&message.message) + } +} + +impl IpPacketResponseData { + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + make_bincode_serializer().serialize(self) + } +} + +impl ControlResponse { + fn id(&self) -> Option { + match self { + ControlResponse::StaticConnect(response) => Some(response.request_id), + ControlResponse::DynamicConnect(response) => Some(response.request_id), + ControlResponse::Disconnect(response) => Some(response.request_id), + ControlResponse::UnrequestedDisconnect(_) => None, + ControlResponse::Pong(response) => Some(response.request_id), + ControlResponse::Health(response) => Some(response.request_id), + ControlResponse::Info(response) => Some(response.request_id), + } + } +} + +impl StaticConnectResponseReply { + pub fn is_success(&self) -> bool { + match self { + StaticConnectResponseReply::Success => true, + StaticConnectResponseReply::Failure(_) => false, + } + } +} + +impl DynamicConnectResponseReply { + pub fn is_success(&self) -> bool { + match self { + DynamicConnectResponseReply::Success(_) => true, + DynamicConnectResponseReply::Failure(_) => false, + } + } +} diff --git a/deny.toml b/deny.toml index 4da5bf5c59..5bd402997e 100644 --- a/deny.toml +++ b/deny.toml @@ -122,6 +122,7 @@ exceptions = [ { allow = ["GPL-3.0"], crate = "nym-network-requester" }, { allow = ["GPL-3.0"], crate = "nym-node" }, { allow = ["GPL-3.0"], crate = "nym-validator-rewarder" }, + { allow = ["GPL-3.0"], crate = "nym-ip-packet-router" }, ] # Some crates don't have (easily) machine readable licensing information, diff --git a/sdk/rust/nym-sdk/src/mixnet/native_client.rs b/sdk/rust/nym-sdk/src/mixnet/native_client.rs index 33e0a7023b..fe542d9eb7 100644 --- a/sdk/rust/nym-sdk/src/mixnet/native_client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/native_client.rs @@ -3,7 +3,7 @@ use crate::mixnet::traits::MixnetMessageSender; use crate::{Error, Result}; use async_trait::async_trait; use futures::{ready, Stream, StreamExt}; -use log::error; +use log::{debug, error}; use nym_client_core::client::base_client::GatewayConnection; use nym_client_core::client::mix_traffic::ClientRequestSender; use nym_client_core::client::{ @@ -12,7 +12,7 @@ use nym_client_core::client::{ received_buffer::ReconstructedMessagesReceiver, }; use nym_client_core::config::ForgetMe; -use nym_crypto::asymmetric::identity; +use nym_crypto::asymmetric::ed25519; use nym_gateway_requests::ClientRequest; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::{params::PacketType, receiver::ReconstructedMessage}; @@ -32,7 +32,7 @@ pub struct MixnetClient { /// The nym address of this connected client. pub(crate) nym_address: Recipient, - pub(crate) identity_keys: Arc, + pub(crate) identity_keys: Arc, /// Input to the client from the users perspective. This can be either data to send or control /// messages. @@ -67,7 +67,7 @@ impl MixnetClient { #[allow(clippy::too_many_arguments)] pub(crate) fn new( nym_address: Recipient, - identity_keys: Arc, + identity_keys: Arc, client_input: ClientInput, client_output: ClientOutput, client_state: ClientState, @@ -125,8 +125,13 @@ impl MixnetClient { self.client_request_sender.clone() } + /// Get the client's identity keys. + pub fn identity_keypair(&self) -> Arc { + self.identity_keys.clone() + } + /// Sign a message with the client's private identity key. - pub fn sign(&self, data: &[u8]) -> identity::Signature { + pub fn sign(&self, data: &[u8]) -> ed25519::Signature { self.identity_keys.private_key().sign(data) } @@ -274,7 +279,9 @@ impl Stream for MixnetClient { } Poll::Ready(Some(next)) } else { - error!("the reconstructed messages vector is empty - please let the developers know if you see this message"); + // I *think* this happens for SURBs, but I'm not 100% sure. Nonetheless it's + // beneign, but let's log it here anyway as a reminder + debug!("the reconstructed messages vector is empty"); cx.waker().wake_by_ref(); Poll::Pending } diff --git a/service-providers/authenticator/src/mixnet_listener.rs b/service-providers/authenticator/src/mixnet_listener.rs index fb3e57378a..dcd3c8e31e 100644 --- a/service-providers/authenticator/src/mixnet_listener.rs +++ b/service-providers/authenticator/src/mixnet_listener.rs @@ -30,7 +30,9 @@ use nym_credential_verification::{ use nym_credentials_interface::CredentialSpendingData; use nym_crypto::asymmetric::x25519::KeyPair; use nym_gateway_requests::models::CredentialSpendingRequest; -use nym_sdk::mixnet::{InputMessage, MixnetMessageSender, Recipient, TransmissionLane}; +use nym_sdk::mixnet::{ + AnonymousSenderTag, InputMessage, MixnetMessageSender, Recipient, TransmissionLane, +}; use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; use nym_sphinx::receiver::ReconstructedMessage; use nym_task::TaskHandle; @@ -757,9 +759,13 @@ impl MixnetListener { } // When an incoming mixnet message triggers a response that we send back. - async fn handle_response(&self, response: Vec, recipient: Recipient) -> Result<()> { - let input_message = - InputMessage::new_regular(recipient, response, TransmissionLane::General, None); + async fn handle_response( + &self, + response: Vec, + recipient: Recipient, + sender_tag: Option, + ) -> Result<()> { + let input_message = create_input_message(recipient, sender_tag, response); self.mixnet_client .send(input_message) .await @@ -782,9 +788,10 @@ impl MixnetListener { } msg = self.mixnet_client.next() => { if let Some(msg) = msg { + let sender_tag = msg.sender_tag; match self.on_reconstructed_message(msg).await { Ok((response, recipient)) => { - if let Err(err) = self.handle_response(response, recipient).await { + if let Err(err) = self.handle_response(response, recipient, sender_tag).await { log::error!("Mixnet listener failed to handle response: {err}"); } } @@ -857,3 +864,19 @@ fn deserialize_request(reconstructed: &ReconstructedMessage) -> Result, + response_packet: Vec, +) -> InputMessage { + let lane = TransmissionLane::General; + let packet_type = None; + if let Some(reply_to_tag) = reply_to_tag { + log::debug!("Creating message using SURB"); + InputMessage::new_reply(reply_to_tag, response_packet, lane, packet_type) + } else { + log::debug!("Creating message using nym_address"); + InputMessage::new_regular(nym_address, response_packet, lane, packet_type) + } +} diff --git a/service-providers/ip-packet-router/Cargo.toml b/service-providers/ip-packet-router/Cargo.toml index 9a322dcaec..fc7303a872 100644 --- a/service-providers/ip-packet-router/Cargo.toml +++ b/service-providers/ip-packet-router/Cargo.toml @@ -6,7 +6,7 @@ repository.workspace = true homepage.workspace = true documentation.workspace = true edition.workspace = true -license.workspace = true +license = "GPL-3.0" [dependencies] anyhow.workspace = true @@ -38,7 +38,6 @@ rand = { workspace = true } reqwest.workspace = true serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -tap.workspace = true thiserror = { workspace = true } time = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "net", "io-util"] } diff --git a/service-providers/ip-packet-router/src/cli/add_gateway.rs b/service-providers/ip-packet-router/src/cli/add_gateway.rs index 251e2cb1a3..ef23413c68 100644 --- a/service-providers/ip-packet-router/src/cli/add_gateway.rs +++ b/service-providers/ip-packet-router/src/cli/add_gateway.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use crate::cli::CliIpPacketRouterClient; use nym_bin_common::output_format::OutputFormat; diff --git a/service-providers/ip-packet-router/src/cli/ecash/import_coin_index_signatures.rs b/service-providers/ip-packet-router/src/cli/ecash/import_coin_index_signatures.rs index a06f9e70b8..0a9a53dab5 100644 --- a/service-providers/ip-packet-router/src/cli/ecash/import_coin_index_signatures.rs +++ b/service-providers/ip-packet-router/src/cli/ecash/import_coin_index_signatures.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use crate::cli::CliIpPacketRouterClient; use nym_client_core::cli_helpers::client_import_coin_index_signatures::{ diff --git a/service-providers/ip-packet-router/src/cli/ecash/import_credential.rs b/service-providers/ip-packet-router/src/cli/ecash/import_credential.rs index d37a8fa066..85209b933e 100644 --- a/service-providers/ip-packet-router/src/cli/ecash/import_credential.rs +++ b/service-providers/ip-packet-router/src/cli/ecash/import_credential.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use crate::cli::CliIpPacketRouterClient; use nym_client_core::cli_helpers::client_import_credential::{ diff --git a/service-providers/ip-packet-router/src/cli/ecash/import_expiration_date_signatures.rs b/service-providers/ip-packet-router/src/cli/ecash/import_expiration_date_signatures.rs index 7c338d05cb..28ea667345 100644 --- a/service-providers/ip-packet-router/src/cli/ecash/import_expiration_date_signatures.rs +++ b/service-providers/ip-packet-router/src/cli/ecash/import_expiration_date_signatures.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use crate::cli::CliIpPacketRouterClient; use nym_client_core::cli_helpers::client_import_expiration_date_signatures::{ diff --git a/service-providers/ip-packet-router/src/cli/ecash/import_master_verification_key.rs b/service-providers/ip-packet-router/src/cli/ecash/import_master_verification_key.rs index 654be2ea18..bff9d3ea62 100644 --- a/service-providers/ip-packet-router/src/cli/ecash/import_master_verification_key.rs +++ b/service-providers/ip-packet-router/src/cli/ecash/import_master_verification_key.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use crate::cli::CliIpPacketRouterClient; use nym_client_core::cli_helpers::client_import_master_verification_key::{ diff --git a/service-providers/ip-packet-router/src/cli/ecash/mod.rs b/service-providers/ip-packet-router/src/cli/ecash/mod.rs index 33a4ddd667..e06a563f84 100644 --- a/service-providers/ip-packet-router/src/cli/ecash/mod.rs +++ b/service-providers/ip-packet-router/src/cli/ecash/mod.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use clap::{Args, Subcommand}; use nym_client_core::cli_helpers::client_import_coin_index_signatures::CommonClientImportCoinIndexSignaturesArgs; diff --git a/service-providers/ip-packet-router/src/cli/ecash/show_ticketbooks.rs b/service-providers/ip-packet-router/src/cli/ecash/show_ticketbooks.rs index b3abea30f7..0c9ebad722 100644 --- a/service-providers/ip-packet-router/src/cli/ecash/show_ticketbooks.rs +++ b/service-providers/ip-packet-router/src/cli/ecash/show_ticketbooks.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use crate::cli::CliIpPacketRouterClient; use nym_bin_common::output_format::OutputFormat; diff --git a/service-providers/ip-packet-router/src/cli/list_gateways.rs b/service-providers/ip-packet-router/src/cli/list_gateways.rs index 3050233c87..8ddb9a985b 100644 --- a/service-providers/ip-packet-router/src/cli/list_gateways.rs +++ b/service-providers/ip-packet-router/src/cli/list_gateways.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use crate::cli::CliIpPacketRouterClient; use nym_bin_common::output_format::OutputFormat; diff --git a/service-providers/ip-packet-router/src/cli/switch_gateway.rs b/service-providers/ip-packet-router/src/cli/switch_gateway.rs index 4a365a4bdb..cb53983fb8 100644 --- a/service-providers/ip-packet-router/src/cli/switch_gateway.rs +++ b/service-providers/ip-packet-router/src/cli/switch_gateway.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use crate::cli::CliIpPacketRouterClient; use nym_client_core::cli_helpers::client_switch_gateway::{ diff --git a/service-providers/ip-packet-router/src/clients/client_id.rs b/service-providers/ip-packet-router/src/clients/client_id.rs new file mode 100644 index 0000000000..038efbda76 --- /dev/null +++ b/service-providers/ip-packet-router/src/clients/client_id.rs @@ -0,0 +1,58 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use std::fmt; + +use nym_ip_packet_requests::v8::request::SentBy; +use nym_sdk::mixnet::{AnonymousSenderTag, Recipient}; + +use crate::error::{IpPacketRouterError, Result}; + +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum ConnectedClientId { + AnonymousSenderTag(AnonymousSenderTag), + NymAddress(Box), +} + +impl ConnectedClientId { + pub(crate) fn into_nym_address(self) -> Result { + match self { + ConnectedClientId::NymAddress(nym_address) => Ok(*nym_address), + ConnectedClientId::AnonymousSenderTag(_) => Err(IpPacketRouterError::InvalidReplyTo), + } + } +} + +impl fmt::Display for ConnectedClientId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ConnectedClientId::NymAddress(nym_address) => write!(f, "{nym_address}"), + ConnectedClientId::AnonymousSenderTag(tag) => write!(f, "{tag}"), + } + } +} + +impl From for ConnectedClientId { + fn from(nym_address: Recipient) -> Self { + ConnectedClientId::NymAddress(Box::new(nym_address)) + } +} + +impl From for ConnectedClientId { + fn from(tag: AnonymousSenderTag) -> Self { + ConnectedClientId::AnonymousSenderTag(tag) + } +} + +impl TryFrom<(SentBy, Option)> for ConnectedClientId { + type Error = IpPacketRouterError; + + fn try_from((sent_by, sender_tag): (SentBy, Option)) -> Result { + match sent_by { + SentBy::NymAddress(nym_address) => Ok(ConnectedClientId::NymAddress(nym_address)), + SentBy::AnonymousSenderTag => sender_tag + .map(ConnectedClientId::AnonymousSenderTag) + .ok_or(IpPacketRouterError::InvalidReplyTo), + } + } +} diff --git a/service-providers/ip-packet-router/src/connected_client_handler.rs b/service-providers/ip-packet-router/src/clients/connected_client_handler.rs similarity index 62% rename from service-providers/ip-packet-router/src/connected_client_handler.rs rename to service-providers/ip-packet-router/src/clients/connected_client_handler.rs index a1904eba5f..a865a3a75f 100644 --- a/service-providers/ip-packet-router/src/connected_client_handler.rs +++ b/service-providers/ip-packet-router/src/clients/connected_client_handler.rs @@ -1,14 +1,25 @@ -// Copyright 2024 - Nym Technologies SA +// Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use std::time::Duration; + use bytes::Bytes; -use nym_ip_packet_requests::codec::MultiIpPacketCodec; -use nym_sdk::mixnet::{MixnetMessageSender, Recipient}; +use nym_ip_packet_requests::{ + codec::MultiIpPacketCodec, v6::response::IpPacketResponse as IpPacketResponseV6, + v7::response::IpPacketResponse as IpPacketResponseV7, + v8::response::IpPacketResponse as IpPacketResponseV8, +}; +use nym_sdk::mixnet::MixnetMessageSender; +use tokio::{ + sync::{mpsc, oneshot}, + time::interval, +}; use crate::{ + clients::ConnectedClientId, constants::CLIENT_HANDLER_ACTIVITY_TIMEOUT, error::{IpPacketRouterError, Result}, - mixnet_listener::SupportedClientVersion, + messages::ClientVersion, util::create_message::create_input_message, }; @@ -19,17 +30,17 @@ use crate::{ // This handler is spawned as a task, and it listens to IP packets passed from the tun_listener, // encodes it, and then sends to mixnet. pub(crate) struct ConnectedClientHandler { - // The address of the client that this handler is connected to - nym_address: Recipient, + // The client that sent the packets + sent_by: ConnectedClientId, // Channel to receive packets from the tun_listener - forward_from_tun_rx: tokio::sync::mpsc::UnboundedReceiver>, + forward_from_tun_rx: mpsc::UnboundedReceiver>, // Channel to send packets to the mixnet mixnet_client_sender: nym_sdk::mixnet::MixnetClientSender, // Channel to receive close signal - close_rx: tokio::sync::oneshot::Receiver<()>, + close_rx: oneshot::Receiver<()>, // Interval to check for activity timeout activity_timeout: tokio::time::Interval, @@ -38,31 +49,33 @@ pub(crate) struct ConnectedClientHandler { encoder: MultiIpPacketCodec, // The version of the client - client_version: SupportedClientVersion, + client_version: ClientVersion, } impl ConnectedClientHandler { pub(crate) fn start( - reply_to: Recipient, - buffer_timeout: std::time::Duration, - client_version: SupportedClientVersion, + client_id: ConnectedClientId, + buffer_timeout: Duration, + client_version: ClientVersion, mixnet_client_sender: nym_sdk::mixnet::MixnetClientSender, ) -> ( - tokio::sync::mpsc::UnboundedSender>, - tokio::sync::oneshot::Sender<()>, + mpsc::UnboundedSender>, + oneshot::Sender<()>, tokio::task::JoinHandle<()>, ) { - let (close_tx, close_rx) = tokio::sync::oneshot::channel(); - let (forward_from_tun_tx, forward_from_tun_rx) = tokio::sync::mpsc::unbounded_channel(); + log::debug!("Starting connected client handler for: {}", client_id); + log::debug!("client version: {:?}", client_version); + let (close_tx, close_rx) = oneshot::channel(); + let (forward_from_tun_tx, forward_from_tun_rx) = mpsc::unbounded_channel(); // Reset so that we don't get the first tick immediately - let mut activity_timeout = tokio::time::interval(CLIENT_HANDLER_ACTIVITY_TIMEOUT); + let mut activity_timeout = interval(CLIENT_HANDLER_ACTIVITY_TIMEOUT); activity_timeout.reset(); let encoder = MultiIpPacketCodec::new(buffer_timeout); let connected_client_handler = ConnectedClientHandler { - nym_address: reply_to, + sent_by: client_id, forward_from_tun_rx, mixnet_client_sender, close_rx, @@ -80,20 +93,18 @@ impl ConnectedClientHandler { (forward_from_tun_tx, close_tx, handle) } - async fn send_packets_to_mixnet(&mut self, packets: Bytes) -> Result<()> { - let response_packet = match self.client_version { - SupportedClientVersion::V6 => { - nym_ip_packet_requests::v6::response::IpPacketResponse::new_ip_packet(packets) - .to_bytes() - } - SupportedClientVersion::V7 => { - nym_ip_packet_requests::v7::response::IpPacketResponse::new_ip_packet(packets) - .to_bytes() - } + async fn create_ip_packet(&self, packets: Bytes) -> Result> { + match self.client_version { + ClientVersion::V6 => IpPacketResponseV6::new_ip_packet(packets).to_bytes(), + ClientVersion::V7 => IpPacketResponseV7::new_ip_packet(packets).to_bytes(), + ClientVersion::V8 => IpPacketResponseV8::new_ip_packet(packets).to_bytes(), } - .map_err(|err| IpPacketRouterError::FailedToSerializeResponsePacket { source: err })?; + .map_err(|err| IpPacketRouterError::FailedToSerializeResponsePacket { source: err }) + } - let input_message = create_input_message(self.nym_address, response_packet); + async fn send_packets_to_mixnet(&mut self, packets: Bytes) -> Result<()> { + let response_packet = self.create_ip_packet(packets).await?; + let input_message = create_input_message(&self.sent_by, response_packet); self.mixnet_client_sender .send(input_message) @@ -123,17 +134,20 @@ impl ConnectedClientHandler { loop { tokio::select! { _ = &mut self.close_rx => { - log::info!("client handler stopping: received close: {}", self.nym_address); + log::info!("client handler stopping: received close: {}", self.sent_by); break; }, _ = self.activity_timeout.tick() => { - log::info!("client handler stopping: activity timeout: {}", self.nym_address); + log::info!("client handler stopping: activity timeout: {}", self.sent_by); break; }, - Some(packets) = self.encoder.buffer_timeout() => { - if let Err(err) = self.handle_buffer_timeout(packets).await { - log::error!("client handler: failed to handle buffer timeout: {err}"); - } + packets = self.encoder.buffer_timeout() => match packets { + Some(packets) => { + if let Err(err) = self.handle_buffer_timeout(packets).await { + log::error!("client handler: failed to handle buffer timeout: {err}"); + } + }, + None => log::trace!("no packets to send"), }, packet = self.forward_from_tun_rx.recv() => match packet { Some(packet) => { diff --git a/service-providers/ip-packet-router/src/clients/connected_clients.rs b/service-providers/ip-packet-router/src/clients/connected_clients.rs new file mode 100644 index 0000000000..1957ddaa15 --- /dev/null +++ b/service-providers/ip-packet-router/src/clients/connected_clients.rs @@ -0,0 +1,257 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use std::{ + collections::HashMap, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + sync::Arc, + time::Instant, +}; + +use nym_ip_packet_requests::IpPair; +use tokio::sync::{mpsc, oneshot, RwLock}; + +use crate::{ + constants::CLIENT_MIXNET_INACTIVITY_TIMEOUT, + error::{IpPacketRouterError, Result}, + tun_listener, +}; + +use super::ConnectedClientId; + +pub(crate) struct ConnectedClients { + // The set of connected clients + clients_ipv4_mapping: HashMap, + clients_ipv6_mapping: HashMap, + + // Notify the tun listener when a new client connects or disconnects + tun_listener_connected_client_tx: mpsc::UnboundedSender, +} + +impl ConnectedClients { + pub(crate) fn new() -> (Self, tun_listener::ConnectedClientsListener) { + let (connected_client_tx, connected_client_rx) = mpsc::unbounded_channel(); + ( + Self { + clients_ipv4_mapping: Default::default(), + clients_ipv6_mapping: Default::default(), + tun_listener_connected_client_tx: connected_client_tx, + }, + tun_listener::ConnectedClientsListener::new(connected_client_rx), + ) + } + + pub(crate) fn is_ip_connected(&self, ips: &IpPair) -> bool { + self.clients_ipv4_mapping.contains_key(&ips.ipv4) + || self.clients_ipv6_mapping.contains_key(&ips.ipv6) + } + + pub(crate) fn get_client_from_ip_mut(&mut self, ip: &IpAddr) -> Option<&mut ConnectedClient> { + match ip { + IpAddr::V4(ip) => self.clients_ipv4_mapping.get_mut(ip), + IpAddr::V6(ip) => self.clients_ipv6_mapping.get_mut(ip), + } + } + + pub(crate) fn is_client_connected(&self, client_id: &ConnectedClientId) -> bool { + self.clients_ipv4_mapping + .values() + .any(|client| client.client_id == *client_id) + } + + //fn lookup_ip_from_client_id(&self, client_id: &ConnectedClientId) -> Option { + // self.clients_ipv4_mapping + // .iter() + // .find_map(|(ipv4, connected_client)| { + // if connected_client.client_id == *client_id { + // Some(IpPair::new(*ipv4, connected_client.ipv6)) + // } else { + // None + // } + // }) + //} + // + //fn lookup_client(&self, client_id: &ConnectedClientId) -> Option<&ConnectedClient> { + // self.clients_ipv4_mapping + // .values() + // .find(|connected_client| connected_client.client_id == *client_id) + //} + + pub(crate) fn connect( + &mut self, + ips: IpPair, + client_id: ConnectedClientId, + forward_from_tun_tx: mpsc::UnboundedSender>, + close_tx: oneshot::Sender<()>, + handle: tokio::task::JoinHandle<()>, + ) { + // The map of connected clients that the mixnet listener keeps track of. It monitors + // activity and disconnects clients that have been inactive for too long. + let client = ConnectedClient { + client_id: client_id.clone(), + ipv6: ips.ipv6, + last_activity: Arc::new(RwLock::new(Instant::now())), + close_tx: Arc::new(CloseTx { + client_id, + inner: Some(close_tx), + }), + handle: Arc::new(handle), + }; + + log::info!("Inserting {} and {}", ips.ipv4, ips.ipv6); + self.clients_ipv4_mapping.insert(ips.ipv4, client.clone()); + self.clients_ipv6_mapping.insert(ips.ipv6, client); + + // Send the connected client info to the tun listener, which will use it to forward packets + // to the connected client handler. + self.tun_listener_connected_client_tx + .send(ConnectedClientEvent::Connect(Box::new(ConnectEvent { + ips, + forward_from_tun_tx, + }))) + .inspect_err(|err| { + log::error!("Failed to send connected client event: {err}"); + }) + .ok(); + } + + pub(crate) async fn update_activity(&mut self, ips: &IpPair) -> Result<()> { + if let Some(client) = self.clients_ipv4_mapping.get(&ips.ipv4) { + *client.last_activity.write().await = Instant::now(); + Ok(()) + } else { + Err(IpPacketRouterError::FailedToUpdateClientActivity) + } + } + + // Identify connected client handlers that have stopped without being told to stop + pub(crate) fn get_finished_client_handlers(&mut self) -> Vec<(IpPair, ConnectedClientId)> { + self.clients_ipv4_mapping + .iter_mut() + .filter_map(|(ip, connected_client)| { + if connected_client.handle.is_finished() { + Some(( + IpPair::new(*ip, connected_client.ipv6), + connected_client.client_id.clone(), + )) + } else { + None + } + }) + .collect() + } + + pub(crate) async fn get_inactive_clients(&mut self) -> Vec<(IpPair, ConnectedClientId)> { + let now = Instant::now(); + let mut ret = vec![]; + for (ip, connected_client) in self.clients_ipv4_mapping.iter() { + if now.duration_since(*connected_client.last_activity.read().await) + > CLIENT_MIXNET_INACTIVITY_TIMEOUT + { + ret.push(( + IpPair::new(*ip, connected_client.ipv6), + connected_client.client_id.clone(), + )) + } + } + ret + } + + pub(crate) fn disconnect_stopped_client_handlers( + &mut self, + stopped_clients: Vec<(IpPair, ConnectedClientId)>, + ) { + for (ips, _) in &stopped_clients { + log::info!("Disconnect stopped client: {ips}"); + self.clients_ipv4_mapping.remove(&ips.ipv4); + self.clients_ipv6_mapping.remove(&ips.ipv6); + self.tun_listener_connected_client_tx + .send(ConnectedClientEvent::Disconnect(DisconnectEvent(*ips))) + .inspect_err(|err| { + log::error!("Failed to send disconnect event: {err}"); + }) + .ok(); + } + } + + pub(crate) fn disconnect_inactive_clients( + &mut self, + inactive_clients: Vec<(IpPair, ConnectedClientId)>, + ) { + for (ips, _) in &inactive_clients { + log::info!("Disconnect inactive client: {ips}"); + self.clients_ipv4_mapping.remove(&ips.ipv4); + self.clients_ipv6_mapping.remove(&ips.ipv6); + self.tun_listener_connected_client_tx + .send(ConnectedClientEvent::Disconnect(DisconnectEvent(*ips))) + .inspect_err(|err| { + log::error!("Failed to send disconnect event: {err}"); + }) + .ok(); + } + } + + pub(crate) fn find_new_ip(&self) -> Option { + crate::util::generate_new_ip::find_new_ips( + &self.clients_ipv4_mapping, + &self.clients_ipv6_mapping, + ) + } +} + +pub(crate) struct CloseTx { + // pub(crate) nym_address: Recipient, + pub(crate) client_id: ConnectedClientId, + // Send to connected clients listener to stop. This is option only because we need to take + // ownership of it when the client is dropped. + pub(crate) inner: Option>, +} + +#[derive(Clone)] +pub(crate) struct ConnectedClient { + // The nym address of the connected client that we are communicating with on the other side of + // the mixnet + // pub(crate) nym_address: Recipient, + pub(crate) client_id: ConnectedClientId, + + // The assigned IPv6 address of this client + pub(crate) ipv6: Ipv6Addr, + + // Keep track of last activity so we can disconnect inactive clients + pub(crate) last_activity: Arc>, + + // Channel to send close signal to the connected client handler + // This is currently unused because the disconnect command it not yet implemented. + #[allow(unused)] + pub(crate) close_tx: Arc, + + // Handle for the connected client handler + pub(crate) handle: Arc>, +} + +impl ConnectedClient { + pub(crate) async fn update_activity(&self) { + *self.last_activity.write().await = Instant::now(); + } +} + +impl Drop for CloseTx { + fn drop(&mut self) { + log::debug!("signal to close client: {}", self.client_id); + if let Some(close_tx) = self.inner.take() { + close_tx.send(()).ok(); + } + } +} + +pub(crate) enum ConnectedClientEvent { + Disconnect(DisconnectEvent), + Connect(Box), +} + +pub(crate) struct DisconnectEvent(pub(crate) IpPair); + +pub(crate) struct ConnectEvent { + pub(crate) ips: IpPair, + pub(crate) forward_from_tun_tx: mpsc::UnboundedSender>, +} diff --git a/service-providers/ip-packet-router/src/clients/mod.rs b/service-providers/ip-packet-router/src/clients/mod.rs new file mode 100644 index 0000000000..fc6b39a359 --- /dev/null +++ b/service-providers/ip-packet-router/src/clients/mod.rs @@ -0,0 +1,12 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +mod client_id; +mod connected_client_handler; +mod connected_clients; + +pub(crate) use client_id::ConnectedClientId; +pub(crate) use connected_client_handler::ConnectedClientHandler; +pub(crate) use connected_clients::{ + ConnectEvent, ConnectedClientEvent, ConnectedClients, DisconnectEvent, +}; diff --git a/service-providers/ip-packet-router/src/config/helpers.rs b/service-providers/ip-packet-router/src/config/helpers.rs index e6abda1d98..6dd7209b87 100644 --- a/service-providers/ip-packet-router/src/config/helpers.rs +++ b/service-providers/ip-packet-router/src/config/helpers.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use crate::config::default_config_filepath; use crate::config::old_config_v1::ConfigV1; diff --git a/service-providers/ip-packet-router/src/config/mod.rs b/service-providers/ip-packet-router/src/config/mod.rs index 1c1e155959..1f7aad3cc2 100644 --- a/service-providers/ip-packet-router/src/config/mod.rs +++ b/service-providers/ip-packet-router/src/config/mod.rs @@ -199,6 +199,7 @@ impl Default for IpPacketRouter { fn default() -> Self { IpPacketRouter { disable_poisson_rate: true, + #[allow(clippy::expect_used)] upstream_exit_policy_url: Some( mainnet::EXIT_POLICY_URL .parse() diff --git a/service-providers/ip-packet-router/src/config/old_config_v1.rs b/service-providers/ip-packet-router/src/config/old_config_v1.rs index 34399af4cf..b7cddacf00 100644 --- a/service-providers/ip-packet-router/src/config/old_config_v1.rs +++ b/service-providers/ip-packet-router/src/config/old_config_v1.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use crate::config::persistence::IpPacketRouterPaths; use crate::config::Config; @@ -76,6 +76,7 @@ impl Default for IpPacketRouterV1 { fn default() -> Self { IpPacketRouterV1 { disable_poisson_rate: true, + #[allow(clippy::expect_used)] upstream_exit_policy_url: Some( mainnet::EXIT_POLICY_URL .parse() diff --git a/service-providers/ip-packet-router/src/config/persistence.rs b/service-providers/ip-packet-router/src/config/persistence.rs index 7f60a98f8c..2bc1cdfc6e 100644 --- a/service-providers/ip-packet-router/src/config/persistence.rs +++ b/service-providers/ip-packet-router/src/config/persistence.rs @@ -1,5 +1,5 @@ // Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use nym_client_core::config::disk_persistence::CommonClientPaths; use serde::{Deserialize, Serialize}; diff --git a/service-providers/ip-packet-router/src/config/template.rs b/service-providers/ip-packet-router/src/config/template.rs index ab5db7bbe8..5338b4b497 100644 --- a/service-providers/ip-packet-router/src/config/template.rs +++ b/service-providers/ip-packet-router/src/config/template.rs @@ -1,5 +1,5 @@ // Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only pub(crate) const CONFIG_TEMPLATE: &str = // While using normal toml marshalling would have been way simpler with less overhead, diff --git a/service-providers/ip-packet-router/src/constants.rs b/service-providers/ip-packet-router/src/constants.rs index bfee848df2..128afa271b 100644 --- a/service-providers/ip-packet-router/src/constants.rs +++ b/service-providers/ip-packet-router/src/constants.rs @@ -1,3 +1,6 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + use std::time::Duration; // We routinely check if any clients needs to be disconnected at this interval diff --git a/service-providers/ip-packet-router/src/error.rs b/service-providers/ip-packet-router/src/error.rs index 7c8a4590d5..0a81bde0ed 100644 --- a/service-providers/ip-packet-router/src/error.rs +++ b/service-providers/ip-packet-router/src/error.rs @@ -1,8 +1,12 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + use std::net::SocketAddr; pub use nym_client_core::error::ClientCoreError; use nym_exit_policy::PolicyError; use nym_id::NymIdError; +use nym_ip_packet_requests::sign::SignatureError; #[derive(thiserror::Error, Debug)] pub enum IpPacketRouterError { @@ -90,12 +94,13 @@ pub enum IpPacketRouterError { EmptyPacket, #[error("failed to verify request: {source}")] - FailedToVerifyRequest { - source: nym_ip_packet_requests::v7::signature::SignatureError, - }, + FailedToVerifyRequest { source: SignatureError }, #[error("client is connected with an invalid version: {version}")] InvalidConnectedClientVersion { version: u8 }, + + #[error("invalid reply-to address in the response")] + InvalidReplyTo, } pub type Result = std::result::Result; diff --git a/service-providers/ip-packet-router/src/ip_packet_router.rs b/service-providers/ip-packet-router/src/ip_packet_router.rs index a0f185cc76..4e976e96f9 100644 --- a/service-providers/ip-packet-router/src/ip_packet_router.rs +++ b/service-providers/ip-packet-router/src/ip_packet_router.rs @@ -1,3 +1,6 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + #![cfg_attr(not(target_os = "linux"), allow(dead_code))] #![cfg_attr(not(target_os = "linux"), allow(unused_imports))] @@ -11,11 +14,7 @@ use nym_client_core::{ use nym_sdk::mixnet::Recipient; use nym_task::{TaskClient, TaskHandle}; -use crate::{ - config::Config, - error::IpPacketRouterError, - request_filter::{self, RequestFilter}, -}; +use crate::{config::Config, error::IpPacketRouterError, request_filter::RequestFilter}; pub struct OnStartData { // to add more fields as required @@ -127,7 +126,10 @@ impl IpPacketRouter { pub async fn run_service_provider(self) -> Result<(), IpPacketRouterError> { // Used to notify tasks to shutdown. Not all tasks fully supports this (yet). - use crate::{mixnet_listener, tun_listener}; + use crate::{ + clients::ConnectedClients, mixnet_listener::MixnetListener, + request_filter::RequestFilter, tun_listener::TunListener, + }; let task_handle: TaskHandle = self.shutdown.map(Into::into).unwrap_or_default(); // Connect to the mixnet @@ -157,19 +159,19 @@ impl IpPacketRouter { // Channel used by the IpPacketRouter to signal connected and disconnected clients to the // TunListener - let (connected_clients, connected_clients_rx) = mixnet_listener::ConnectedClients::new(); + let (connected_clients, connected_clients_rx) = ConnectedClients::new(); - let tun_listener = tun_listener::TunListener { + let tun_listener = TunListener { tun_reader, task_client: task_handle.get_handle(), connected_clients: connected_clients_rx, }; tun_listener.start(); - let request_filter = request_filter::RequestFilter::new(&self.config).await?; + let request_filter = RequestFilter::new(&self.config).await?; request_filter.start_update_tasks().await; - let mixnet_listener = mixnet_listener::MixnetListener { + let mixnet_listener = MixnetListener { _config: self.config, request_filter: request_filter.clone(), tun_writer, diff --git a/service-providers/ip-packet-router/src/lib.rs b/service-providers/ip-packet-router/src/lib.rs index cb4a84ec65..fe66577b22 100644 --- a/service-providers/ip-packet-router/src/lib.rs +++ b/service-providers/ip-packet-router/src/lib.rs @@ -1,17 +1,26 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] +#![warn(clippy::panic)] #![cfg_attr(not(target_os = "linux"), allow(dead_code))] #![cfg_attr(not(target_os = "linux"), allow(unused_imports))] -pub use crate::config::Config; -pub use ip_packet_router::{IpPacketRouter, OnStartData}; - pub mod config; -mod connected_client_handler; -mod constants; pub mod error; +pub mod request_filter; + +pub(crate) mod messages; +pub(crate) mod non_linux_dummy; + +mod clients; +mod constants; mod ip_packet_router; mod mixnet_client; mod mixnet_listener; -pub(crate) mod non_linux_dummy; -pub mod request_filter; mod tun_listener; mod util; + +pub use crate::config::Config; +pub use ip_packet_router::{IpPacketRouter, OnStartData}; diff --git a/service-providers/ip-packet-router/src/main.rs b/service-providers/ip-packet-router/src/main.rs index c8f9fd0e1b..17e5040ab3 100644 --- a/service-providers/ip-packet-router/src/main.rs +++ b/service-providers/ip-packet-router/src/main.rs @@ -1,3 +1,6 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + #[cfg(target_os = "linux")] mod cli; diff --git a/service-providers/ip-packet-router/src/messages/mod.rs b/service-providers/ip-packet-router/src/messages/mod.rs new file mode 100644 index 0000000000..a695fc5e2f --- /dev/null +++ b/service-providers/ip-packet-router/src/messages/mod.rs @@ -0,0 +1,22 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub(crate) mod request; +pub(crate) mod response; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum ClientVersion { + V6, + V7, + V8, +} + +impl ClientVersion { + pub(crate) fn into_u8(self) -> u8 { + match self { + ClientVersion::V6 => 6, + ClientVersion::V7 => 7, + ClientVersion::V8 => 8, + } + } +} diff --git a/service-providers/ip-packet-router/src/messages/request.rs b/service-providers/ip-packet-router/src/messages/request.rs new file mode 100644 index 0000000000..3b781275a7 --- /dev/null +++ b/service-providers/ip-packet-router/src/messages/request.rs @@ -0,0 +1,144 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +mod v6; +mod v7; +mod v8; + +use nym_ip_packet_requests::{ + v6::request::IpPacketRequest as IpPacketRequestV6, + v7::request::IpPacketRequest as IpPacketRequestV7, + v8::request::IpPacketRequest as IpPacketRequestV8, IpPair, +}; +use nym_sdk::mixnet::ReconstructedMessage; +use std::fmt; + +use crate::{clients::ConnectedClientId, error::IpPacketRouterError}; + +use super::ClientVersion; + +// The internal representation of the request after deserialization, valid for all versions +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum IpPacketRequest { + Data(DataRequest), + Control(ControlRequest), +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct DataRequest { + pub(crate) version: ClientVersion, + pub(crate) ip_packets: bytes::Bytes, +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum ControlRequest { + StaticConnect(StaticConnectRequest), + DynamicConnect(DynamicConnectRequest), + Disconnect(DisconnectRequest), + Ping(PingRequest), + Health(HealthRequest), +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct StaticConnectRequest { + pub(crate) version: ClientVersion, + pub(crate) request_id: u64, + pub(crate) sent_by: ConnectedClientId, + pub(crate) ips: IpPair, + pub(crate) buffer_timeout: Option, +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct DynamicConnectRequest { + pub(crate) version: ClientVersion, + pub(crate) request_id: u64, + pub(crate) sent_by: ConnectedClientId, + pub(crate) buffer_timeout: Option, +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct DisconnectRequest { + pub(crate) version: ClientVersion, + pub(crate) request_id: u64, + pub(crate) sent_by: ConnectedClientId, +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct PingRequest { + pub(crate) version: ClientVersion, + pub(crate) request_id: u64, + pub(crate) sent_by: ConnectedClientId, +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct HealthRequest { + pub(crate) version: ClientVersion, + pub(crate) request_id: u64, + pub(crate) sent_by: ConnectedClientId, +} + +impl TryFrom<&ReconstructedMessage> for IpPacketRequest { + type Error = IpPacketRouterError; + + fn try_from(reconstructed: &ReconstructedMessage) -> Result { + let request_version = *reconstructed + .message + .first() + .ok_or(IpPacketRouterError::EmptyPacket)?; + + match request_version { + 6 => { + let request_v6 = IpPacketRequestV6::from_reconstructed_message(reconstructed) + .map_err( + |source| IpPacketRouterError::FailedToDeserializeTaggedPacket { source }, + )?; + Ok(IpPacketRequest::from(request_v6)) + } + 7 => { + let request_v7 = IpPacketRequestV7::from_reconstructed_message(reconstructed) + .map_err( + |source| IpPacketRouterError::FailedToDeserializeTaggedPacket { source }, + )?; + request_v7 + .verify() + .map_err(|source| IpPacketRouterError::FailedToVerifyRequest { source })?; + Ok(IpPacketRequest::from(request_v7)) + } + 8 => { + let request_v8 = IpPacketRequestV8::from_reconstructed_message(reconstructed) + .map_err( + |source| IpPacketRouterError::FailedToDeserializeTaggedPacket { source }, + )?; + request_v8 + .verify() + .map_err(|source| IpPacketRouterError::FailedToVerifyRequest { source })?; + IpPacketRequest::try_from((request_v8, reconstructed.sender_tag)) + } + _ => { + log::info!("Received packet with invalid version: v{request_version}"); + Err(IpPacketRouterError::InvalidPacketVersion(request_version)) + } + } + } +} + +impl fmt::Display for IpPacketRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + IpPacketRequest::Data(_) => write!(f, "Data"), + IpPacketRequest::Control(control) => write!(f, "{control}"), + } + } +} + +impl fmt::Display for ControlRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ControlRequest::StaticConnect(_) => write!(f, "StaticConnect"), + ControlRequest::DynamicConnect(_) => write!(f, "DynamicConnect"), + ControlRequest::Disconnect(_) => write!(f, "Disconnect"), + ControlRequest::Ping(_) => write!(f, "Ping"), + ControlRequest::Health(_) => write!(f, "Health"), + } + } +} diff --git a/service-providers/ip-packet-router/src/messages/request/v6.rs b/service-providers/ip-packet-router/src/messages/request/v6.rs new file mode 100644 index 0000000000..d6b26475c7 --- /dev/null +++ b/service-providers/ip-packet-router/src/messages/request/v6.rs @@ -0,0 +1,100 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_ip_packet_requests::v6::request::{ + DataRequest as DataRequestV6, DisconnectRequest as DisconnectRequestV6, + DynamicConnectRequest as DynamicConnectRequestV6, HealthRequest as HealthRequestV6, + IpPacketRequest as IpPacketRequestV6, IpPacketRequestData as IpPacketRequestDataV6, + PingRequest as PingRequestV6, StaticConnectRequest as StaticConnectRequestV6, +}; + +use super::{ + ClientVersion, ControlRequest, DataRequest, DisconnectRequest, DynamicConnectRequest, + HealthRequest, IpPacketRequest, PingRequest, StaticConnectRequest, +}; + +impl From for IpPacketRequest { + fn from(request: IpPacketRequestV6) -> Self { + let version = ClientVersion::V6; + match request.data { + IpPacketRequestDataV6::Data(inner) => Self::Data((inner, version).into()), + IpPacketRequestDataV6::StaticConnect(inner) => { + Self::Control(ControlRequest::StaticConnect((inner, version).into())) + } + IpPacketRequestDataV6::DynamicConnect(inner) => { + Self::Control(ControlRequest::DynamicConnect((inner, version).into())) + } + IpPacketRequestDataV6::Disconnect(inner) => { + Self::Control(ControlRequest::Disconnect((inner, version).into())) + } + IpPacketRequestDataV6::Ping(inner) => { + Self::Control(ControlRequest::Ping((inner, version).into())) + } + IpPacketRequestDataV6::Health(inner) => { + Self::Control(ControlRequest::Health((inner, version).into())) + } + } + } +} + +impl From<(DataRequestV6, ClientVersion)> for DataRequest { + fn from((request, version): (DataRequestV6, ClientVersion)) -> Self { + Self { + version, + ip_packets: request.ip_packets, + } + } +} + +impl From<(StaticConnectRequestV6, ClientVersion)> for StaticConnectRequest { + fn from((request, version): (StaticConnectRequestV6, ClientVersion)) -> Self { + Self { + version, + request_id: request.request_id, + sent_by: request.reply_to.into(), + ips: request.ips, + buffer_timeout: request.buffer_timeout, + } + } +} + +impl From<(DynamicConnectRequestV6, ClientVersion)> for DynamicConnectRequest { + fn from((request, version): (DynamicConnectRequestV6, ClientVersion)) -> Self { + Self { + version, + request_id: request.request_id, + sent_by: request.reply_to.into(), + buffer_timeout: request.buffer_timeout, + } + } +} + +impl From<(DisconnectRequestV6, ClientVersion)> for DisconnectRequest { + fn from((request, version): (DisconnectRequestV6, ClientVersion)) -> Self { + Self { + version, + request_id: request.request_id, + sent_by: request.reply_to.into(), + } + } +} + +impl From<(PingRequestV6, ClientVersion)> for PingRequest { + fn from((request, version): (PingRequestV6, ClientVersion)) -> Self { + Self { + version, + request_id: request.request_id, + sent_by: request.reply_to.into(), + } + } +} + +impl From<(HealthRequestV6, ClientVersion)> for HealthRequest { + fn from((request, version): (HealthRequestV6, ClientVersion)) -> Self { + Self { + version, + request_id: request.request_id, + sent_by: request.reply_to.into(), + } + } +} diff --git a/service-providers/ip-packet-router/src/messages/request/v7.rs b/service-providers/ip-packet-router/src/messages/request/v7.rs new file mode 100644 index 0000000000..1582de66c2 --- /dev/null +++ b/service-providers/ip-packet-router/src/messages/request/v7.rs @@ -0,0 +1,100 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_ip_packet_requests::v7::request::{ + DataRequest as DataRequestV7, DisconnectRequest as DisconnectRequestV7, + DynamicConnectRequest as DynamicConnectRequestV7, HealthRequest as HealthRequestV7, + IpPacketRequest as IpPacketRequestV7, IpPacketRequestData as IpPacketRequestDataV7, + PingRequest as PingRequestV7, StaticConnectRequest as StaticConnectRequestV7, +}; + +use super::{ + ClientVersion, ControlRequest, DataRequest, DisconnectRequest, DynamicConnectRequest, + HealthRequest, IpPacketRequest, PingRequest, StaticConnectRequest, +}; + +impl From for IpPacketRequest { + fn from(request: IpPacketRequestV7) -> Self { + let version = ClientVersion::V7; + match request.data { + IpPacketRequestDataV7::Data(inner) => Self::Data((inner, version).into()), + IpPacketRequestDataV7::StaticConnect(inner) => Self::Control( + ControlRequest::StaticConnect((inner.request, version).into()), + ), + IpPacketRequestDataV7::DynamicConnect(inner) => Self::Control( + ControlRequest::DynamicConnect((inner.request, version).into()), + ), + IpPacketRequestDataV7::Disconnect(inner) => { + Self::Control(ControlRequest::Disconnect((inner.request, version).into())) + } + IpPacketRequestDataV7::Ping(inner) => { + Self::Control(ControlRequest::Ping((inner, version).into())) + } + IpPacketRequestDataV7::Health(inner) => { + Self::Control(ControlRequest::Health((inner, version).into())) + } + } + } +} + +impl From<(DataRequestV7, ClientVersion)> for DataRequest { + fn from((request, version): (DataRequestV7, ClientVersion)) -> Self { + Self { + version, + ip_packets: request.ip_packets, + } + } +} + +impl From<(StaticConnectRequestV7, ClientVersion)> for StaticConnectRequest { + fn from((request, version): (StaticConnectRequestV7, ClientVersion)) -> Self { + Self { + version, + request_id: request.request_id, + sent_by: request.reply_to.into(), + ips: request.ips, + buffer_timeout: request.buffer_timeout, + } + } +} + +impl From<(DynamicConnectRequestV7, ClientVersion)> for DynamicConnectRequest { + fn from((request, version): (DynamicConnectRequestV7, ClientVersion)) -> Self { + Self { + version, + request_id: request.request_id, + sent_by: request.reply_to.into(), + buffer_timeout: request.buffer_timeout, + } + } +} + +impl From<(DisconnectRequestV7, ClientVersion)> for DisconnectRequest { + fn from((request, version): (DisconnectRequestV7, ClientVersion)) -> Self { + Self { + version, + request_id: request.request_id, + sent_by: request.reply_to.into(), + } + } +} + +impl From<(PingRequestV7, ClientVersion)> for PingRequest { + fn from((request, version): (PingRequestV7, ClientVersion)) -> Self { + Self { + version, + request_id: request.request_id, + sent_by: request.reply_to.into(), + } + } +} + +impl From<(HealthRequestV7, ClientVersion)> for HealthRequest { + fn from((request, version): (HealthRequestV7, ClientVersion)) -> Self { + Self { + version, + request_id: request.request_id, + sent_by: request.reply_to.into(), + } + } +} diff --git a/service-providers/ip-packet-router/src/messages/request/v8.rs b/service-providers/ip-packet-router/src/messages/request/v8.rs new file mode 100644 index 0000000000..e7190faae7 --- /dev/null +++ b/service-providers/ip-packet-router/src/messages/request/v8.rs @@ -0,0 +1,180 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_ip_packet_requests::v8::request::{ + ControlRequest as ControlRequestV8, DataRequest as DataRequestV8, + DisconnectRequest as DisconnectRequestV8, DynamicConnectRequest as DynamicConnectRequestV8, + HealthRequest as HealthRequestV8, IpPacketRequest as IpPacketRequestV8, + IpPacketRequestData as IpPacketRequestDataV8, PingRequest as PingRequestV8, + StaticConnectRequest as StaticConnectRequestV8, +}; +use nym_sdk::mixnet::AnonymousSenderTag; + +use crate::error::IpPacketRouterError; + +use super::{ + ClientVersion, ConnectedClientId, ControlRequest, DataRequest, DisconnectRequest, + DynamicConnectRequest, HealthRequest, IpPacketRequest, PingRequest, StaticConnectRequest, +}; + +impl TryFrom<(IpPacketRequestV8, Option)> for IpPacketRequest { + type Error = IpPacketRouterError; + + fn try_from( + (request, sender_tag): (IpPacketRequestV8, Option), + ) -> Result { + let version = ClientVersion::V8; + Ok(match request.data { + IpPacketRequestDataV8::Data(inner) => Self::Data((inner, version).into()), + IpPacketRequestDataV8::Control(inner) => { + Self::Control((*inner, sender_tag, version).try_into()?) + } + }) + } +} + +impl From<(DataRequestV8, ClientVersion)> for DataRequest { + fn from((request, version): (DataRequestV8, ClientVersion)) -> Self { + Self { + version, + ip_packets: request.ip_packets, + } + } +} + +impl TryFrom<(ControlRequestV8, Option, ClientVersion)> for ControlRequest { + type Error = IpPacketRouterError; + + fn try_from( + (request, sender_tag, version): ( + ControlRequestV8, + Option, + ClientVersion, + ), + ) -> Result { + Ok(match request { + ControlRequestV8::StaticConnect(inner) => { + ControlRequest::StaticConnect((inner.request, sender_tag, version).try_into()?) + } + ControlRequestV8::DynamicConnect(inner) => { + ControlRequest::DynamicConnect((inner.request, sender_tag, version).try_into()?) + } + ControlRequestV8::Disconnect(inner) => { + ControlRequest::Disconnect((inner.request, sender_tag, version).try_into()?) + } + ControlRequestV8::Ping(inner) => { + ControlRequest::Ping((inner, sender_tag, version).try_into()?) + } + ControlRequestV8::Health(inner) => { + ControlRequest::Health((inner, sender_tag, version).try_into()?) + } + }) + } +} + +impl + TryFrom<( + StaticConnectRequestV8, + Option, + ClientVersion, + )> for StaticConnectRequest +{ + type Error = IpPacketRouterError; + + fn try_from( + (request, sender_tag, version): ( + StaticConnectRequestV8, + Option, + ClientVersion, + ), + ) -> Result { + Ok(Self { + version, + request_id: request.request_id, + sent_by: ConnectedClientId::try_from((request.sender, sender_tag))?, + ips: request.ips, + buffer_timeout: request.buffer_timeout, + }) + } +} + +impl + TryFrom<( + DynamicConnectRequestV8, + Option, + ClientVersion, + )> for DynamicConnectRequest +{ + type Error = IpPacketRouterError; + + fn try_from( + (request, sender_tag, version): ( + DynamicConnectRequestV8, + Option, + ClientVersion, + ), + ) -> Result { + Ok(Self { + version, + request_id: request.request_id, + sent_by: ConnectedClientId::try_from((request.sender, sender_tag))?, + buffer_timeout: request.buffer_timeout, + }) + } +} + +impl + TryFrom<( + DisconnectRequestV8, + Option, + ClientVersion, + )> for DisconnectRequest +{ + type Error = IpPacketRouterError; + + fn try_from( + (request, sender_tag, version): ( + DisconnectRequestV8, + Option, + ClientVersion, + ), + ) -> Result { + Ok(Self { + version, + request_id: request.request_id, + sent_by: ConnectedClientId::try_from((request.sender, sender_tag))?, + }) + } +} + +impl TryFrom<(PingRequestV8, Option, ClientVersion)> for PingRequest { + type Error = IpPacketRouterError; + + fn try_from( + (request, sender_tag, version): (PingRequestV8, Option, ClientVersion), + ) -> Result { + Ok(Self { + version, + request_id: request.request_id, + sent_by: ConnectedClientId::try_from((request.sender, sender_tag))?, + }) + } +} + +impl TryFrom<(HealthRequestV8, Option, ClientVersion)> for HealthRequest { + type Error = IpPacketRouterError; + + fn try_from( + (request, sender_tag, version): ( + HealthRequestV8, + Option, + ClientVersion, + ), + ) -> Result { + Ok(Self { + version, + request_id: request.request_id, + sent_by: ConnectedClientId::try_from((request.sender, sender_tag))?, + }) + } +} diff --git a/service-providers/ip-packet-router/src/messages/response.rs b/service-providers/ip-packet-router/src/messages/response.rs new file mode 100644 index 0000000000..3743323166 --- /dev/null +++ b/service-providers/ip-packet-router/src/messages/response.rs @@ -0,0 +1,190 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +mod v6; +mod v7; +mod v8; + +use nym_bin_common::build_information::BinaryBuildInformationOwned; +use nym_ip_packet_requests::{ + v6::response::IpPacketResponse as IpPacketResponseV6, + v7::response::IpPacketResponse as IpPacketResponseV7, + v8::response::IpPacketResponse as IpPacketResponseV8, IpPair, +}; + +use crate::{ + clients::ConnectedClientId, + error::{IpPacketRouterError, Result}, +}; + +use super::ClientVersion; + +pub(crate) struct VersionedResponse { + pub(crate) version: ClientVersion, + pub(crate) reply_to: ConnectedClientId, + pub(crate) response: Response, +} + +#[derive(Debug, Clone)] +pub(crate) enum Response { + StaticConnect { + request_id: u64, + reply: StaticConnectResponse, + }, + DynamicConnect { + request_id: u64, + reply: DynamicConnectResponse, + }, + // Disconnect is not yet implemented + #[allow(unused)] + Disconnect { + request_id: u64, + reply: DisconnectResponse, + }, + Pong { + request_id: u64, + }, + Health { + request_id: u64, + reply: Box, + }, + Info { + request_id: u64, + reply: InfoResponse, + }, +} + +#[derive(Debug, Clone)] +pub(crate) enum StaticConnectResponse { + Success, + Failure(StaticConnectFailureReason), +} + +#[derive(thiserror::Error, Debug, Clone)] +pub(crate) enum StaticConnectFailureReason { + #[error("requested ip address is already in use")] + RequestedIpAlreadyInUse, + + #[error("client already connected")] + ClientAlreadyConnected, + + #[allow(unused)] + #[error("request timestamp is out of date")] + OutOfDateTimestamp, + + #[allow(unused)] + #[error("{0}")] + Other(String), +} + +#[derive(Debug, Clone)] +pub(crate) enum DynamicConnectResponse { + Success(DynamicConnectSuccess), + Failure(DynamicConnectFailureReason), +} + +#[derive(Debug, Clone)] +pub(crate) struct DynamicConnectSuccess { + pub(crate) ips: IpPair, +} + +#[derive(Clone, Debug, thiserror::Error)] +pub(crate) enum DynamicConnectFailureReason { + #[error("client already connected")] + ClientAlreadyConnected, + + #[error("no available ip address")] + NoAvailableIp, + + #[allow(unused)] + #[error("{0}")] + Other(String), +} + +// Disconnect is not yet implemented +#[allow(unused)] +#[derive(Debug, Clone)] +pub(crate) enum DisconnectResponse { + Success, + Failure(DisconnectFailureReason), +} + +// Disconnect is not yet implemented +#[allow(unused)] +#[derive(Debug, Clone, thiserror::Error)] +pub(crate) enum DisconnectFailureReason { + #[error("requested client is not currently connected")] + ClientNotConnected, + + #[error("{0}")] + Other(String), +} + +#[derive(Debug, Clone)] +pub(crate) struct HealthResponse { + pub(crate) build_info: BinaryBuildInformationOwned, + pub(crate) routable: Option, +} + +impl VersionedResponse { + pub(crate) fn try_into_bytes(self) -> Result> { + match self.version { + ClientVersion::V6 => IpPacketResponseV6::try_from(self)?.to_bytes(), + ClientVersion::V7 => IpPacketResponseV7::try_from(self)?.to_bytes(), + ClientVersion::V8 => IpPacketResponseV8::from(self).to_bytes(), + } + .map_err(|err| IpPacketRouterError::FailedToSerializeResponsePacket { source: err }) + } +} + +#[derive(Clone, Debug)] +pub(crate) struct InfoResponse { + pub(crate) reply: InfoResponseReply, + pub(crate) level: InfoLevel, +} + +#[derive(Clone, Debug, thiserror::Error)] +pub(crate) enum InfoResponseReply { + #[allow(unused)] + #[error("{msg}")] + Generic { msg: String }, + + #[allow(unused)] + #[error( + "version mismatch: response is v{request_version} and response is v{response_version}" + )] + VersionMismatch { + request_version: u8, + response_version: u8, + }, + + #[error("destination failed exit policy filter check: {dst}")] + ExitPolicyFilterCheckFailed { dst: String }, +} + +#[derive(Clone, Debug)] +pub(crate) enum InfoLevel { + #[allow(unused)] + Info, + Warn, + #[allow(unused)] + Error, +} + +impl From for StaticConnectResponse { + fn from(failure: StaticConnectFailureReason) -> Self { + StaticConnectResponse::Failure(failure) + } +} + +impl From for DynamicConnectResponse { + fn from(success: DynamicConnectSuccess) -> Self { + DynamicConnectResponse::Success(success) + } +} + +impl From for DynamicConnectResponse { + fn from(failure: DynamicConnectFailureReason) -> Self { + DynamicConnectResponse::Failure(failure) + } +} diff --git a/service-providers/ip-packet-router/src/messages/response/v6.rs b/service-providers/ip-packet-router/src/messages/response/v6.rs new file mode 100644 index 0000000000..290f78971b --- /dev/null +++ b/service-providers/ip-packet-router/src/messages/response/v6.rs @@ -0,0 +1,188 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_ip_packet_requests::v6::response::{ + DisconnectFailureReason as DisconnectFailureReasonV6, + DisconnectResponse as DisconnectResponseV6, + DisconnectResponseReply as DisconnectResponseReplyV6, + DynamicConnectFailureReason as DynamicConnectFailureReasonV6, + DynamicConnectResponse as DynamicConnectResponseV6, + DynamicConnectResponseReply as DynamicConnectResponseReplyV6, + DynamicConnectSuccess as DynamicConnectSuccessV6, HealthResponse as HealthResponseV6, + HealthResponseReply as HealthResponseReplyV6, InfoLevel as InfoLevelV6, + InfoResponse as InfoResponseV6, InfoResponseReply as InfoResponseReplyV6, + IpPacketResponse as IpPacketResponseV6, IpPacketResponseData as IpPacketResponseDataV6, + PongResponse as PongResponseV6, StaticConnectFailureReason as StaticConnectFailureReasonV6, + StaticConnectResponse as StaticConnectResponseV6, + StaticConnectResponseReply as StaticConnectResponseReplyV6, +}; + +use crate::error::IpPacketRouterError; + +use super::{ + DisconnectFailureReason, DisconnectResponse, DynamicConnectFailureReason, + DynamicConnectResponse, DynamicConnectSuccess, HealthResponse, InfoLevel, InfoResponseReply, + Response, StaticConnectFailureReason, StaticConnectResponse, VersionedResponse, +}; + +impl TryFrom for IpPacketResponseV6 { + type Error = IpPacketRouterError; + + fn try_from(response: VersionedResponse) -> std::result::Result { + let version = response.version.into_u8(); + let reply_to = response.reply_to.into_nym_address()?; + let data = match response.response { + Response::StaticConnect { request_id, reply } => { + IpPacketResponseDataV6::StaticConnect(StaticConnectResponseV6 { + request_id, + reply_to, + reply: reply.into(), + }) + } + Response::DynamicConnect { request_id, reply } => { + IpPacketResponseDataV6::DynamicConnect(DynamicConnectResponseV6 { + request_id, + reply_to, + reply: reply.into(), + }) + } + Response::Disconnect { request_id, reply } => { + IpPacketResponseDataV6::Disconnect(DisconnectResponseV6 { + request_id, + reply_to, + reply: reply.into(), + }) + } + Response::Pong { request_id } => IpPacketResponseDataV6::Pong(PongResponseV6 { + request_id, + reply_to, + }), + Response::Health { request_id, reply } => { + IpPacketResponseDataV6::Health(HealthResponseV6 { + request_id, + reply_to, + reply: (*reply).into(), + }) + } + Response::Info { request_id, reply } => IpPacketResponseDataV6::Info(InfoResponseV6 { + request_id, + reply_to, + reply: reply.reply.into(), + level: reply.level.into(), + }), + }; + Ok(IpPacketResponseV6 { version, data }) + } +} + +impl From for StaticConnectResponseReplyV6 { + fn from(response: StaticConnectResponse) -> Self { + match response { + StaticConnectResponse::Success => StaticConnectResponseReplyV6::Success, + StaticConnectResponse::Failure(err) => { + StaticConnectResponseReplyV6::Failure(err.into()) + } + } + } +} + +impl From for StaticConnectFailureReasonV6 { + fn from(reason: StaticConnectFailureReason) -> Self { + match reason { + StaticConnectFailureReason::RequestedIpAlreadyInUse => { + StaticConnectFailureReasonV6::RequestedIpAlreadyInUse + } + StaticConnectFailureReason::ClientAlreadyConnected => { + StaticConnectFailureReasonV6::RequestedNymAddressAlreadyInUse + } + StaticConnectFailureReason::OutOfDateTimestamp => { + StaticConnectFailureReasonV6::Other("unexpected timestamp".to_string()) + } + StaticConnectFailureReason::Other(err) => StaticConnectFailureReasonV6::Other(err), + } + } +} + +impl From for DynamicConnectResponseReplyV6 { + fn from(response: DynamicConnectResponse) -> Self { + match response { + DynamicConnectResponse::Success(DynamicConnectSuccess { ips }) => { + DynamicConnectResponseReplyV6::Success(DynamicConnectSuccessV6 { ips }) + } + DynamicConnectResponse::Failure(err) => { + DynamicConnectResponseReplyV6::Failure(err.into()) + } + } + } +} + +impl From for DynamicConnectFailureReasonV6 { + fn from(reason: DynamicConnectFailureReason) -> Self { + match reason { + DynamicConnectFailureReason::ClientAlreadyConnected => { + DynamicConnectFailureReasonV6::RequestedNymAddressAlreadyInUse + } + DynamicConnectFailureReason::NoAvailableIp => { + DynamicConnectFailureReasonV6::NoAvailableIp + } + DynamicConnectFailureReason::Other(err) => DynamicConnectFailureReasonV6::Other(err), + } + } +} + +impl From for DisconnectResponseReplyV6 { + fn from(response: DisconnectResponse) -> Self { + match response { + DisconnectResponse::Success => DisconnectResponseReplyV6::Success, + DisconnectResponse::Failure(err) => DisconnectResponseReplyV6::Failure(err.into()), + } + } +} + +impl From for DisconnectFailureReasonV6 { + fn from(reason: DisconnectFailureReason) -> Self { + match reason { + DisconnectFailureReason::ClientNotConnected => { + DisconnectFailureReasonV6::RequestedNymAddressNotConnected + } + DisconnectFailureReason::Other(err) => DisconnectFailureReasonV6::Other(err), + } + } +} + +impl From for HealthResponseReplyV6 { + fn from(response: HealthResponse) -> Self { + HealthResponseReplyV6 { + build_info: response.build_info, + routable: response.routable, + } + } +} + +impl From for InfoResponseReplyV6 { + fn from(reply: InfoResponseReply) -> Self { + match reply { + InfoResponseReply::Generic { msg } => InfoResponseReplyV6::Generic { msg }, + InfoResponseReply::VersionMismatch { + request_version, + response_version, + } => InfoResponseReplyV6::VersionMismatch { + request_version, + response_version, + }, + InfoResponseReply::ExitPolicyFilterCheckFailed { dst } => { + InfoResponseReplyV6::ExitPolicyFilterCheckFailed { dst } + } + } + } +} + +impl From for InfoLevelV6 { + fn from(level: InfoLevel) -> Self { + match level { + InfoLevel::Info => InfoLevelV6::Info, + InfoLevel::Warn => InfoLevelV6::Warn, + InfoLevel::Error => InfoLevelV6::Error, + } + } +} diff --git a/service-providers/ip-packet-router/src/messages/response/v7.rs b/service-providers/ip-packet-router/src/messages/response/v7.rs new file mode 100644 index 0000000000..60a3aa6d8a --- /dev/null +++ b/service-providers/ip-packet-router/src/messages/response/v7.rs @@ -0,0 +1,188 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_ip_packet_requests::v7::response::{ + DisconnectFailureReason as DisconnectFailureReasonV7, + DisconnectResponse as DisconnectResponseV7, + DisconnectResponseReply as DisconnectResponseReplyV7, + DynamicConnectFailureReason as DynamicConnectFailureReasonV7, + DynamicConnectResponse as DynamicConnectResponseV7, + DynamicConnectResponseReply as DynamicConnectResponseReplyV7, + DynamicConnectSuccess as DynamicConnectSuccessV7, HealthResponse as HealthResponseV7, + HealthResponseReply as HealthResponseReplyV7, InfoLevel as InfoLevelV7, + InfoResponse as InfoResponseV7, InfoResponseReply as InfoResponseReplyV7, + IpPacketResponse as IpPacketResponseV7, IpPacketResponseData as IpPacketResponseDataV7, + PongResponse as PongResponseV7, StaticConnectFailureReason as StaticConnectFailureReasonV7, + StaticConnectResponse as StaticConnectResponseV7, + StaticConnectResponseReply as StaticConnectResponseReplyV7, +}; + +use crate::error::IpPacketRouterError; + +use super::{ + DisconnectFailureReason, DisconnectResponse, DynamicConnectFailureReason, + DynamicConnectResponse, DynamicConnectSuccess, HealthResponse, InfoLevel, InfoResponseReply, + Response, StaticConnectFailureReason, StaticConnectResponse, VersionedResponse, +}; + +impl TryFrom for IpPacketResponseV7 { + type Error = IpPacketRouterError; + + fn try_from(response: VersionedResponse) -> std::result::Result { + let version = response.version.into_u8(); + let reply_to = response.reply_to.into_nym_address()?; + let data = match response.response { + Response::StaticConnect { request_id, reply } => { + IpPacketResponseDataV7::StaticConnect(StaticConnectResponseV7 { + request_id, + reply_to, + reply: reply.into(), + }) + } + Response::DynamicConnect { request_id, reply } => { + IpPacketResponseDataV7::DynamicConnect(DynamicConnectResponseV7 { + request_id, + reply_to, + reply: reply.into(), + }) + } + Response::Disconnect { request_id, reply } => { + IpPacketResponseDataV7::Disconnect(DisconnectResponseV7 { + request_id, + reply_to, + reply: reply.into(), + }) + } + Response::Pong { request_id } => IpPacketResponseDataV7::Pong(PongResponseV7 { + request_id, + reply_to, + }), + Response::Health { request_id, reply } => { + IpPacketResponseDataV7::Health(HealthResponseV7 { + request_id, + reply_to, + reply: (*reply).into(), + }) + } + Response::Info { request_id, reply } => IpPacketResponseDataV7::Info(InfoResponseV7 { + request_id, + reply_to, + reply: reply.reply.into(), + level: reply.level.into(), + }), + }; + Ok(IpPacketResponseV7 { version, data }) + } +} + +impl From for StaticConnectResponseReplyV7 { + fn from(response: StaticConnectResponse) -> Self { + match response { + StaticConnectResponse::Success => StaticConnectResponseReplyV7::Success, + StaticConnectResponse::Failure(err) => { + StaticConnectResponseReplyV7::Failure(err.into()) + } + } + } +} + +impl From for StaticConnectFailureReasonV7 { + fn from(reason: StaticConnectFailureReason) -> Self { + match reason { + StaticConnectFailureReason::RequestedIpAlreadyInUse => { + StaticConnectFailureReasonV7::RequestedIpAlreadyInUse + } + StaticConnectFailureReason::ClientAlreadyConnected => { + StaticConnectFailureReasonV7::RequestedNymAddressAlreadyInUse + } + StaticConnectFailureReason::OutOfDateTimestamp => { + StaticConnectFailureReasonV7::OutOfDateTimestamp + } + StaticConnectFailureReason::Other(err) => StaticConnectFailureReasonV7::Other(err), + } + } +} + +impl From for DynamicConnectResponseReplyV7 { + fn from(response: DynamicConnectResponse) -> Self { + match response { + DynamicConnectResponse::Success(DynamicConnectSuccess { ips }) => { + DynamicConnectResponseReplyV7::Success(DynamicConnectSuccessV7 { ips }) + } + DynamicConnectResponse::Failure(err) => { + DynamicConnectResponseReplyV7::Failure(err.into()) + } + } + } +} + +impl From for DynamicConnectFailureReasonV7 { + fn from(reason: DynamicConnectFailureReason) -> Self { + match reason { + DynamicConnectFailureReason::ClientAlreadyConnected => { + DynamicConnectFailureReasonV7::RequestedNymAddressAlreadyInUse + } + DynamicConnectFailureReason::NoAvailableIp => { + DynamicConnectFailureReasonV7::NoAvailableIp + } + DynamicConnectFailureReason::Other(err) => DynamicConnectFailureReasonV7::Other(err), + } + } +} + +impl From for DisconnectResponseReplyV7 { + fn from(response: DisconnectResponse) -> Self { + match response { + DisconnectResponse::Success => DisconnectResponseReplyV7::Success, + DisconnectResponse::Failure(err) => DisconnectResponseReplyV7::Failure(err.into()), + } + } +} + +impl From for DisconnectFailureReasonV7 { + fn from(reason: DisconnectFailureReason) -> Self { + match reason { + DisconnectFailureReason::ClientNotConnected => { + DisconnectFailureReasonV7::RequestedNymAddressNotConnected + } + DisconnectFailureReason::Other(err) => DisconnectFailureReasonV7::Other(err), + } + } +} + +impl From for HealthResponseReplyV7 { + fn from(response: HealthResponse) -> Self { + HealthResponseReplyV7 { + build_info: response.build_info, + routable: response.routable, + } + } +} + +impl From for InfoResponseReplyV7 { + fn from(reply: InfoResponseReply) -> Self { + match reply { + InfoResponseReply::Generic { msg } => InfoResponseReplyV7::Generic { msg }, + InfoResponseReply::VersionMismatch { + request_version, + response_version, + } => InfoResponseReplyV7::VersionMismatch { + request_version, + response_version, + }, + InfoResponseReply::ExitPolicyFilterCheckFailed { dst } => { + InfoResponseReplyV7::ExitPolicyFilterCheckFailed { dst } + } + } + } +} + +impl From for InfoLevelV7 { + fn from(level: InfoLevel) -> Self { + match level { + InfoLevel::Info => InfoLevelV7::Info, + InfoLevel::Warn => InfoLevelV7::Warn, + InfoLevel::Error => InfoLevelV7::Error, + } + } +} diff --git a/service-providers/ip-packet-router/src/messages/response/v8.rs b/service-providers/ip-packet-router/src/messages/response/v8.rs new file mode 100644 index 0000000000..5ad984e355 --- /dev/null +++ b/service-providers/ip-packet-router/src/messages/response/v8.rs @@ -0,0 +1,183 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_ip_packet_requests::v8::response::{ + ControlResponse as ControlResponseV8, DisconnectFailureReason as DisconnectFailureReasonV8, + DisconnectResponse as DisconnectResponseV8, + DisconnectResponseReply as DisconnectResponseReplyV8, + DynamicConnectFailureReason as DynamicConnectFailureReasonV8, + DynamicConnectResponse as DynamicConnectResponseV8, + DynamicConnectResponseReply as DynamicConnectResponseReplyV8, + DynamicConnectSuccess as DynamicConnectSuccessV8, HealthResponse as HealthResponseV8, + HealthResponseReply as HealthResponseReplyV8, InfoLevel as InfoLevelV8, + InfoResponse as InfoResponseV8, InfoResponseReply as InfoResponseReplyV8, + IpPacketResponse as IpPacketResponseV8, IpPacketResponseData as IpPacketResponseDataV8, + PongResponse as PongResponseV8, StaticConnectFailureReason as StaticConnectFailureReasonV8, + StaticConnectResponse as StaticConnectResponseV8, + StaticConnectResponseReply as StaticConnectResponseReplyV8, +}; + +use super::{ + DisconnectFailureReason, DisconnectResponse, DynamicConnectFailureReason, + DynamicConnectResponse, DynamicConnectSuccess, HealthResponse, InfoLevel, InfoResponseReply, + Response, StaticConnectFailureReason, StaticConnectResponse, VersionedResponse, +}; + +impl From for IpPacketResponseV8 { + fn from(response: VersionedResponse) -> Self { + let version = response.version.into_u8(); + let data = + match response.response { + Response::StaticConnect { request_id, reply } => IpPacketResponseDataV8::Control( + Box::new(ControlResponseV8::StaticConnect(StaticConnectResponseV8 { + request_id, + reply: reply.into(), + })), + ), + Response::DynamicConnect { request_id, reply } => { + IpPacketResponseDataV8::Control(Box::new(ControlResponseV8::DynamicConnect( + DynamicConnectResponseV8 { + request_id, + reply: reply.into(), + }, + ))) + } + Response::Disconnect { request_id, reply } => IpPacketResponseDataV8::Control( + Box::new(ControlResponseV8::Disconnect(DisconnectResponseV8 { + request_id, + reply: reply.into(), + })), + ), + Response::Pong { request_id } => IpPacketResponseDataV8::Control(Box::new( + ControlResponseV8::Pong(PongResponseV8 { request_id }), + )), + Response::Health { request_id, reply } => IpPacketResponseDataV8::Control( + Box::new(ControlResponseV8::Health(Box::new(HealthResponseV8 { + request_id, + reply: (*reply).into(), + }))), + ), + Response::Info { request_id, reply } => IpPacketResponseDataV8::Control(Box::new( + ControlResponseV8::Info(InfoResponseV8 { + request_id, + reply: reply.reply.into(), + level: reply.level.into(), + }), + )), + }; + + IpPacketResponseV8 { version, data } + } +} + +impl From for StaticConnectResponseReplyV8 { + fn from(reply: StaticConnectResponse) -> Self { + match reply { + StaticConnectResponse::Success => StaticConnectResponseReplyV8::Success, + StaticConnectResponse::Failure(err) => { + StaticConnectResponseReplyV8::Failure(err.into()) + } + } + } +} + +impl From for StaticConnectFailureReasonV8 { + fn from(reason: StaticConnectFailureReason) -> Self { + match reason { + StaticConnectFailureReason::RequestedIpAlreadyInUse => { + StaticConnectFailureReasonV8::RequestedIpAlreadyInUse + } + StaticConnectFailureReason::ClientAlreadyConnected => { + StaticConnectFailureReasonV8::ClientAlreadyConnected + } + StaticConnectFailureReason::OutOfDateTimestamp => { + StaticConnectFailureReasonV8::OutOfDateTimestamp + } + StaticConnectFailureReason::Other(err) => StaticConnectFailureReasonV8::Other(err), + } + } +} + +impl From for DynamicConnectResponseReplyV8 { + fn from(reply: DynamicConnectResponse) -> Self { + match reply { + DynamicConnectResponse::Success(DynamicConnectSuccess { ips }) => { + DynamicConnectResponseReplyV8::Success(DynamicConnectSuccessV8 { ips }) + } + DynamicConnectResponse::Failure(err) => { + DynamicConnectResponseReplyV8::Failure(err.into()) + } + } + } +} + +impl From for DynamicConnectFailureReasonV8 { + fn from(reason: DynamicConnectFailureReason) -> Self { + match reason { + DynamicConnectFailureReason::ClientAlreadyConnected => { + DynamicConnectFailureReasonV8::ClientAlreadyConnected + } + DynamicConnectFailureReason::NoAvailableIp => { + DynamicConnectFailureReasonV8::NoAvailableIp + } + DynamicConnectFailureReason::Other(err) => DynamicConnectFailureReasonV8::Other(err), + } + } +} + +impl From for DisconnectResponseReplyV8 { + fn from(reply: DisconnectResponse) -> Self { + match reply { + DisconnectResponse::Success => DisconnectResponseReplyV8::Success, + DisconnectResponse::Failure(err) => DisconnectResponseReplyV8::Failure(err.into()), + } + } +} + +impl From for DisconnectFailureReasonV8 { + fn from(reason: DisconnectFailureReason) -> Self { + match reason { + DisconnectFailureReason::ClientNotConnected => { + DisconnectFailureReasonV8::ClientNotConnected + } + DisconnectFailureReason::Other(err) => DisconnectFailureReasonV8::Other(err), + } + } +} + +impl From for HealthResponseReplyV8 { + fn from(response: HealthResponse) -> Self { + HealthResponseReplyV8 { + build_info: response.build_info, + routable: response.routable, + } + } +} + +impl From for InfoResponseReplyV8 { + fn from(reply: InfoResponseReply) -> Self { + match reply { + InfoResponseReply::Generic { msg } => InfoResponseReplyV8::Generic { msg }, + InfoResponseReply::VersionMismatch { + request_version, + response_version, + } => InfoResponseReplyV8::VersionMismatch { + request_version, + response_version, + }, + InfoResponseReply::ExitPolicyFilterCheckFailed { dst } => { + InfoResponseReplyV8::ExitPolicyFilterCheckFailed { dst } + } + } + } +} + +impl From for InfoLevelV8 { + fn from(level: InfoLevel) -> Self { + match level { + InfoLevel::Info => InfoLevelV8::Info, + InfoLevel::Warn => InfoLevelV8::Warn, + InfoLevel::Error => InfoLevelV8::Error, + } + } +} diff --git a/service-providers/ip-packet-router/src/mixnet_client.rs b/service-providers/ip-packet-router/src/mixnet_client.rs index 178832a164..43f5f00038 100644 --- a/service-providers/ip-packet-router/src/mixnet_client.rs +++ b/service-providers/ip-packet-router/src/mixnet_client.rs @@ -1,3 +1,6 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + use nym_client_core::{config::disk_persistence::CommonClientPaths, TopologyProvider}; use nym_sdk::{GatewayTransceiver, NymNetworkDetails}; use nym_task::TaskClient; diff --git a/service-providers/ip-packet-router/src/mixnet_listener.rs b/service-providers/ip-packet-router/src/mixnet_listener.rs index 2bd5fdeaaa..293718831c 100644 --- a/service-providers/ip-packet-router/src/mixnet_listener.rs +++ b/service-providers/ip-packet-router/src/mixnet_listener.rs @@ -1,389 +1,37 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + use bytes::{Bytes, BytesMut}; use futures::StreamExt; -use nym_ip_packet_requests::v7::response::{ - DynamicConnectFailureReason, InfoLevel, InfoResponseReply, StaticConnectFailureReason, -}; -use nym_ip_packet_requests::{ - codec::MultiIpPacketCodec, - v6, - v7::{ - self, - request::{ - DataRequest, DisconnectRequest, DynamicConnectRequest, IpPacketRequest, - IpPacketRequestData, StaticConnectRequest, - }, - signature::SignedRequest, - }, - IpPair, -}; -use nym_sdk::mixnet::{MixnetMessageSender, Recipient}; +use nym_ip_packet_requests::codec::MultiIpPacketCodec; +use nym_sdk::mixnet::MixnetMessageSender; use nym_sphinx::receiver::ReconstructedMessage; use nym_task::TaskHandle; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; -use std::sync::Arc; -use std::{collections::HashMap, net::SocketAddr}; -use tap::TapFallible; +use std::{net::SocketAddr, time::Duration}; use tokio::io::AsyncWriteExt; -use tokio::sync::RwLock; use tokio_util::codec::Decoder; use crate::{ + clients::{ConnectedClientHandler, ConnectedClients}, config::Config, - connected_client_handler, - constants::{CLIENT_MIXNET_INACTIVITY_TIMEOUT, DISCONNECT_TIMER_INTERVAL}, + constants::DISCONNECT_TIMER_INTERVAL, error::{IpPacketRouterError, Result}, - request_filter::{self}, - tun_listener, - util::generate_new_ip, - util::{ - create_message::create_input_message, - parse_ip::{parse_packet, ParsedPacket}, + messages::{ + request::{ + ControlRequest, DataRequest, DisconnectRequest, DynamicConnectRequest, HealthRequest, + IpPacketRequest, PingRequest, StaticConnectRequest, + }, + response::{ + DynamicConnectFailureReason, DynamicConnectSuccess, HealthResponse, InfoLevel, + InfoResponse, InfoResponseReply, Response, StaticConnectFailureReason, + StaticConnectResponse, VersionedResponse, + }, + ClientVersion, }, + request_filter::RequestFilter, + util::parse_ip::ParsedPacket, }; -pub(crate) struct ConnectedClients { - // The set of connected clients - clients_ipv4_mapping: HashMap, - clients_ipv6_mapping: HashMap, - - // Notify the tun listener when a new client connects or disconnects - tun_listener_connected_client_tx: tokio::sync::mpsc::UnboundedSender, -} - -impl ConnectedClients { - pub(crate) fn new() -> (Self, tun_listener::ConnectedClientsListener) { - let (connected_client_tx, connected_client_rx) = tokio::sync::mpsc::unbounded_channel(); - ( - Self { - clients_ipv4_mapping: Default::default(), - clients_ipv6_mapping: Default::default(), - tun_listener_connected_client_tx: connected_client_tx, - }, - tun_listener::ConnectedClientsListener::new(connected_client_rx), - ) - } - - fn is_ip_connected(&self, ips: &IpPair) -> bool { - self.clients_ipv4_mapping.contains_key(&ips.ipv4) - || self.clients_ipv6_mapping.contains_key(&ips.ipv6) - } - - fn get_client_from_ip_mut(&mut self, ip: &IpAddr) -> Option<&mut ConnectedClient> { - match ip { - IpAddr::V4(ip) => self.clients_ipv4_mapping.get_mut(ip), - IpAddr::V6(ip) => self.clients_ipv6_mapping.get_mut(ip), - } - } - - fn is_nym_address_connected(&self, nym_address: &Recipient) -> bool { - self.clients_ipv4_mapping - .values() - .any(|client| client.nym_address == *nym_address) - } - - fn lookup_ip_from_nym_address(&self, nym_address: &Recipient) -> Option { - self.clients_ipv4_mapping - .iter() - .find_map(|(ipv4, connected_client)| { - if connected_client.nym_address == *nym_address { - Some(IpPair::new(*ipv4, connected_client.ipv6)) - } else { - None - } - }) - } - - #[allow(dead_code)] - fn lookup_client_from_nym_address(&self, nym_address: &Recipient) -> Option<&ConnectedClient> { - self.clients_ipv4_mapping - .iter() - .find_map(|(_, connected_client)| { - if connected_client.nym_address == *nym_address { - Some(connected_client) - } else { - None - } - }) - } - - fn connect( - &mut self, - ips: IpPair, - nym_address: Recipient, - forward_from_tun_tx: tokio::sync::mpsc::UnboundedSender>, - close_tx: tokio::sync::oneshot::Sender<()>, - handle: tokio::task::JoinHandle<()>, - ) { - // The map of connected clients that the mixnet listener keeps track of. It monitors - // activity and disconnects clients that have been inactive for too long. - let client = ConnectedClient { - nym_address, - ipv6: ips.ipv6, - last_activity: Arc::new(RwLock::new(std::time::Instant::now())), - _close_tx: Arc::new(CloseTx { - nym_address, - inner: Some(close_tx), - }), - handle: Arc::new(handle), - }; - log::info!("Inserting {} and {}", ips.ipv4, ips.ipv6); - self.clients_ipv4_mapping.insert(ips.ipv4, client.clone()); - self.clients_ipv6_mapping.insert(ips.ipv6, client); - // Send the connected client info to the tun listener, which will use it to forward packets - // to the connected client handler. - self.tun_listener_connected_client_tx - .send(ConnectedClientEvent::Connect(Box::new(ConnectEvent { - ips, - forward_from_tun_tx, - }))) - .tap_err(|err| { - log::error!("Failed to send connected client event: {err}"); - }) - .ok(); - } - - async fn update_activity(&mut self, ips: &IpPair) -> Result<()> { - if let Some(client) = self.clients_ipv4_mapping.get(&ips.ipv4) { - *client.last_activity.write().await = std::time::Instant::now(); - Ok(()) - } else { - Err(IpPacketRouterError::FailedToUpdateClientActivity) - } - } - - // Identify connected client handlers that have stopped without being told to stop - fn get_finished_client_handlers(&mut self) -> Vec<(IpPair, Recipient)> { - self.clients_ipv4_mapping - .iter_mut() - .filter_map(|(ip, connected_client)| { - if connected_client.handle.is_finished() { - Some(( - IpPair::new(*ip, connected_client.ipv6), - connected_client.nym_address, - )) - } else { - None - } - }) - .collect() - } - - async fn get_inactive_clients(&mut self) -> Vec<(IpPair, Recipient)> { - let now = std::time::Instant::now(); - let mut ret = vec![]; - for (ip, connected_client) in self.clients_ipv4_mapping.iter() { - if now.duration_since(*connected_client.last_activity.read().await) - > CLIENT_MIXNET_INACTIVITY_TIMEOUT - { - ret.push(( - IpPair::new(*ip, connected_client.ipv6), - connected_client.nym_address, - )) - } - } - ret - } - - fn disconnect_stopped_client_handlers(&mut self, stopped_clients: Vec<(IpPair, Recipient)>) { - for (ips, _) in &stopped_clients { - log::info!("Disconnect stopped client: {ips}"); - self.clients_ipv4_mapping.remove(&ips.ipv4); - self.clients_ipv6_mapping.remove(&ips.ipv6); - self.tun_listener_connected_client_tx - .send(ConnectedClientEvent::Disconnect(DisconnectEvent(*ips))) - .tap_err(|err| { - log::error!("Failed to send disconnect event: {err}"); - }) - .ok(); - } - } - - fn disconnect_inactive_clients(&mut self, inactive_clients: Vec<(IpPair, Recipient)>) { - for (ips, _) in &inactive_clients { - log::info!("Disconnect inactive client: {ips}"); - self.clients_ipv4_mapping.remove(&ips.ipv4); - self.clients_ipv6_mapping.remove(&ips.ipv6); - self.tun_listener_connected_client_tx - .send(ConnectedClientEvent::Disconnect(DisconnectEvent(*ips))) - .tap_err(|err| { - log::error!("Failed to send disconnect event: {err}"); - }) - .ok(); - } - } - - fn find_new_ip(&self) -> Option { - generate_new_ip::find_new_ips(&self.clients_ipv4_mapping, &self.clients_ipv6_mapping) - } -} - -pub(crate) struct CloseTx { - pub(crate) nym_address: Recipient, - // Send to connected clients listener to stop. This is option only because we need to take - // ownership of it when the client is dropped. - pub(crate) inner: Option>, -} - -#[derive(Clone)] -pub(crate) struct ConnectedClient { - // The nym address of the connected client that we are communicating with on the other side of - // the mixnet - pub(crate) nym_address: Recipient, - - // The assigned IPv6 address of this client - pub(crate) ipv6: Ipv6Addr, - - // Keep track of last activity so we can disconnect inactive clients - pub(crate) last_activity: Arc>, - - pub(crate) _close_tx: Arc, - - // Handle for the connected client handler - pub(crate) handle: Arc>, -} - -impl ConnectedClient { - async fn update_activity(&self) { - *self.last_activity.write().await = std::time::Instant::now(); - } -} - -impl Drop for CloseTx { - fn drop(&mut self) { - log::debug!("signal to close client: {}", self.nym_address); - if let Some(close_tx) = self.inner.take() { - close_tx.send(()).ok(); - } - } -} - -type PacketHandleResult = Result>; - -#[derive(Debug, Clone)] -enum Response { - V6(v6::response::IpPacketResponse), - V7(v7::response::IpPacketResponse), -} - -impl Response { - fn recipient(&self) -> Option<&Recipient> { - match self { - Response::V6(response) => response.recipient(), - Response::V7(response) => response.recipient(), - } - } - - fn new_static_connect_success( - request_id: u64, - reply_to: Recipient, - client_version: SupportedClientVersion, - ) -> Self { - match client_version { - SupportedClientVersion::V6 => Response::V6( - v6::response::IpPacketResponse::new_static_connect_success(request_id, reply_to), - ), - SupportedClientVersion::V7 => Response::V7( - v7::response::IpPacketResponse::new_static_connect_success(request_id, reply_to), - ), - } - } - - fn new_static_connect_failure( - request_id: u64, - reply_to: Recipient, - reason: StaticConnectFailureReason, - client_version: SupportedClientVersion, - ) -> Self { - match client_version { - SupportedClientVersion::V6 => { - Response::V6(v6::response::IpPacketResponse::new_static_connect_failure( - request_id, - reply_to, - reason.into(), - )) - } - SupportedClientVersion::V7 => { - Response::V7(v7::response::IpPacketResponse::new_static_connect_failure( - request_id, reply_to, reason, - )) - } - } - } - - fn new_dynamic_connect_success( - request_id: u64, - reply_to: Recipient, - ips: IpPair, - client_version: SupportedClientVersion, - ) -> Self { - match client_version { - SupportedClientVersion::V6 => { - Response::V6(v6::response::IpPacketResponse::new_dynamic_connect_success( - request_id, reply_to, ips, - )) - } - SupportedClientVersion::V7 => { - Response::V7(v7::response::IpPacketResponse::new_dynamic_connect_success( - request_id, reply_to, ips, - )) - } - } - } - - fn new_dynamic_connect_failure( - request_id: u64, - reply_to: Recipient, - reason: DynamicConnectFailureReason, - client_version: SupportedClientVersion, - ) -> Self { - match client_version { - SupportedClientVersion::V6 => { - Response::V6(v6::response::IpPacketResponse::new_dynamic_connect_failure( - request_id, - reply_to, - reason.into(), - )) - } - SupportedClientVersion::V7 => { - Response::V7(v7::response::IpPacketResponse::new_dynamic_connect_failure( - request_id, reply_to, reason, - )) - } - } - } - - fn new_data_info_response( - reply_to: Recipient, - reply: InfoResponseReply, - level: InfoLevel, - client_version: SupportedClientVersion, - ) -> Self { - match client_version { - SupportedClientVersion::V6 => { - Response::V6(v6::response::IpPacketResponse::new_data_info_response( - reply_to, - reply.into(), - level.into(), - )) - } - SupportedClientVersion::V7 => Response::V7( - v7::response::IpPacketResponse::new_data_info_response(reply_to, reply, level), - ), - } - } - - fn to_bytes(&self) -> Result> { - match self { - Response::V6(response) => response.to_bytes(), - Response::V7(response) => response.to_bytes(), - } - .map_err(|err| { - log::error!("Failed to serialize response packet"); - IpPacketRouterError::FailedToSerializeResponsePacket { source: err } - }) - } -} - #[cfg(not(target_os = "linux"))] type TunDevice = crate::non_linux_dummy::DummyDevice; @@ -396,7 +44,7 @@ pub(crate) struct MixnetListener { pub(crate) _config: Config, // The request filter that we use to check if a packet should be forwarded - pub(crate) request_filter: request_filter::RequestFilter, + pub(crate) request_filter: RequestFilter, // The TUN device that we use to send and receive packets from the internet pub(crate) tun_writer: tokio::io::WriteHalf, @@ -414,192 +62,23 @@ pub(crate) struct MixnetListener { // #[cfg(target_os = "linux")] impl MixnetListener { - // Receving a static connect request from a client with an IP provided that we assign to them, - // if it's available. If it's not available, we send a failure response. - async fn on_static_connect_request( - &mut self, - connect_request: StaticConnectRequest, - client_version: SupportedClientVersion, - ) -> PacketHandleResult { - log::info!( - "Received static connect request from {sender_address}", - sender_address = connect_request.reply_to - ); - - let request_id = connect_request.request_id; - let requested_ips = connect_request.ips; - let reply_to = connect_request.reply_to; - // TODO: add to connect request - let buffer_timeout = nym_ip_packet_requests::codec::BUFFER_TIMEOUT; - // TODO: ignoring reply_to_avg_mix_delays for now - - // Check that the IP is available in the set of connected clients - let is_ip_taken = self.connected_clients.is_ip_connected(&requested_ips); - - // Check that the nym address isn't already registered - let is_nym_address_taken = self.connected_clients.is_nym_address_connected(&reply_to); - - match (is_ip_taken, is_nym_address_taken) { - (true, true) => { - log::info!("Connecting an already connected client"); - if self - .connected_clients - .update_activity(&requested_ips) - .await - .is_err() - { - log::error!("Failed to update activity for client"); - }; - Ok(Some(Response::new_static_connect_success( - request_id, - reply_to, - client_version, - ))) - } - (false, false) => { - log::info!("Connecting a new client"); - - // Spawn the ConnectedClientHandler for the new client - let (forward_from_tun_tx, close_tx, handle) = - connected_client_handler::ConnectedClientHandler::start( - reply_to, - buffer_timeout, - client_version, - self.mixnet_client.split_sender(), - ); - - // Register the new client in the set of connected clients - self.connected_clients.connect( - requested_ips, - reply_to, - forward_from_tun_tx, - close_tx, - handle, - ); - Ok(Some(Response::new_static_connect_success( - request_id, - reply_to, - client_version, - ))) - } - (true, false) => { - log::info!("Requested IP is not available"); - Ok(Some(Response::new_static_connect_failure( - request_id, - reply_to, - StaticConnectFailureReason::RequestedIpAlreadyInUse, - client_version, - ))) - } - (false, true) => { - log::info!("Nym address is already registered"); - Ok(Some(Response::new_static_connect_failure( - request_id, - reply_to, - StaticConnectFailureReason::RequestedNymAddressAlreadyInUse, - client_version, - ))) - } - } - } - - async fn on_dynamic_connect_request( - &mut self, - connect_request: DynamicConnectRequest, - client_version: SupportedClientVersion, - ) -> PacketHandleResult { - log::info!( - "Received dynamic connect request from {sender_address}", - sender_address = connect_request.reply_to - ); - - let request_id = connect_request.request_id; - let reply_to = connect_request.reply_to; - // TODO: add to connect request - let buffer_timeout = nym_ip_packet_requests::codec::BUFFER_TIMEOUT; - // TODO: ignoring reply_to_avg_mix_delays for now - - // Check if it's the same client connecting again, then we just reuse the same IP - // TODO: this is problematic. Until we sign connect requests this means you can spam people - // with return traffic - - if let Some(existing_ips) = self.connected_clients.lookup_ip_from_nym_address(&reply_to) { - log::info!("Found existing client for nym address"); - if self - .connected_clients - .update_activity(&existing_ips) - .await - .is_err() - { - log::error!("Failed to update activity for client"); - } - return Ok(Some(Response::new_dynamic_connect_success( - request_id, - reply_to, - existing_ips, - client_version, - ))); - } - - let Some(new_ips) = self.connected_clients.find_new_ip() else { - log::info!("No available IP address"); - return Ok(Some(Response::new_dynamic_connect_failure( - request_id, - reply_to, - DynamicConnectFailureReason::NoAvailableIp, - client_version, - ))); - }; - - // Spawn the ConnectedClientHandler for the new client - let (forward_from_tun_tx, close_tx, handle) = - connected_client_handler::ConnectedClientHandler::start( - reply_to, - buffer_timeout, - client_version, - self.mixnet_client.split_sender(), - ); - - // Register the new client in the set of connected clients - self.connected_clients - .connect(new_ips, reply_to, forward_from_tun_tx, close_tx, handle); - Ok(Some(Response::new_dynamic_connect_success( - request_id, - reply_to, - new_ips, - client_version, - ))) - } - - fn on_disconnect_request( - &self, - _disconnect_request: DisconnectRequest, - _client_version: SupportedClientVersion, - ) -> PacketHandleResult { - log::info!("Received disconnect request: not implemented, dropping"); - Ok(None) - } - async fn handle_packet( &mut self, ip_packet: &Bytes, - client_version: SupportedClientVersion, + version: ClientVersion, ) -> PacketHandleResult { log::trace!("Received data request"); // We don't forward packets that we are not able to parse. BUT, there might be a good // reason to still forward them. // - // For example, if we are running in a mode where we are only supposed to forward - // packets to a specific destination, we might want to forward them anyway. - // // TODO: look into this let ParsedPacket { packet_type, src_addr, dst_addr, dst, - } = parse_packet(ip_packet)?; + } = crate::util::parse_ip::parse_packet(ip_packet)?; let dst_str = dst.map_or(dst_addr.to_string(), |dst| dst.to_string()); log::debug!("Received packet: {packet_type}: {src_addr} -> {dst_str}"); @@ -621,14 +100,19 @@ impl MixnetListener { Ok(None) } else { log::info!("Denied filter check: {dst}"); - Ok(Some(Response::new_data_info_response( - connected_client.nym_address, - InfoResponseReply::ExitPolicyFilterCheckFailed { - dst: dst.to_string(), + Ok(Some(VersionedResponse { + version, + reply_to: connected_client.client_id.clone(), + response: Response::Info { + request_id: 0, + reply: InfoResponse { + reply: InfoResponseReply::ExitPolicyFilterCheckFailed { + dst: dst.to_string(), + }, + level: InfoLevel::Warn, + }, }, - InfoLevel::Warn, - client_version, - ))) + })) } } else { // If the client is not connected, just drop the packet silently @@ -640,19 +124,213 @@ impl MixnetListener { async fn on_data_request( &mut self, data_request: DataRequest, - client_version: SupportedClientVersion, ) -> Result> { let mut responses = Vec::new(); let mut decoder = MultiIpPacketCodec::new(nym_ip_packet_requests::codec::BUFFER_TIMEOUT); let mut bytes = BytesMut::new(); bytes.extend_from_slice(&data_request.ip_packets); while let Ok(Some(packet)) = decoder.decode(&mut bytes) { - let result = self.handle_packet(&packet, client_version).await; + let result = self.handle_packet(&packet, data_request.version).await; responses.push(result); } Ok(responses) } + // Receving a static connect request from a client with an IP provided that we assign to them, + // if it's available. If it's not available, we send a failure response. + async fn on_static_connect_request( + &mut self, + connect_request: StaticConnectRequest, + ) -> PacketHandleResult { + log::info!( + "Received static connect request from {}", + connect_request.sent_by + ); + + let version = connect_request.version; + let sent_by = connect_request.sent_by; + let request_id = connect_request.request_id; + let requested_ips = connect_request.ips; + let buffer_timeout = connect_request + .buffer_timeout + .map(Duration::from_millis) + .unwrap_or(nym_ip_packet_requests::codec::BUFFER_TIMEOUT); + + // Check that the IP is available in the set of connected clients + let is_ip_taken = self.connected_clients.is_ip_connected(&requested_ips); + + // Check that the client_id address isn't already registered + let is_client_id_taken = self.connected_clients.is_client_connected(&sent_by); + + let response = match (is_ip_taken, is_client_id_taken) { + (true, true) => { + log::info!("Connecting an already connected client"); + if self + .connected_clients + .update_activity(&requested_ips) + .await + .is_err() + { + log::error!("Failed to update activity for client"); + }; + Response::StaticConnect { + request_id, + reply: StaticConnectResponse::Success, + } + } + (false, false) => { + log::info!("Connecting a new client"); + + // Spawn the ConnectedClientHandler for the new client + let (forward_from_tun_tx, close_tx, handle) = ConnectedClientHandler::start( + sent_by.clone(), + buffer_timeout, + version, + self.mixnet_client.split_sender(), + ); + + // Register the new client in the set of connected clients + self.connected_clients.connect( + requested_ips, + sent_by.clone(), + forward_from_tun_tx, + close_tx, + handle, + ); + Response::StaticConnect { + request_id, + reply: StaticConnectResponse::Success, + } + } + (true, false) => { + log::info!("Requested IP is not available"); + Response::StaticConnect { + request_id, + reply: StaticConnectFailureReason::RequestedIpAlreadyInUse.into(), + } + } + (false, true) => { + log::info!("Nym address is already registered"); + Response::StaticConnect { + request_id, + reply: StaticConnectFailureReason::ClientAlreadyConnected.into(), + } + } + }; + + Ok(Some(VersionedResponse { + version, + reply_to: sent_by, + response, + })) + } + + async fn on_dynamic_connect_request( + &mut self, + connect_request: DynamicConnectRequest, + ) -> PacketHandleResult { + log::info!( + "Received dynamic connect request from {}", + connect_request.sent_by + ); + + let version = connect_request.version; + let request_id = connect_request.request_id; + let reply_to = connect_request.sent_by; + let buffer_timeout = connect_request + .buffer_timeout + .map(Duration::from_millis) + .unwrap_or(nym_ip_packet_requests::codec::BUFFER_TIMEOUT); + + if self.connected_clients.is_client_connected(&reply_to) { + return Ok(Some(VersionedResponse { + version, + reply_to, + response: Response::DynamicConnect { + request_id, + reply: DynamicConnectFailureReason::ClientAlreadyConnected.into(), + }, + })); + } + + let Some(new_ips) = self.connected_clients.find_new_ip() else { + log::info!("No available IP address"); + return Ok(Some(VersionedResponse { + version, + reply_to, + response: Response::DynamicConnect { + request_id, + reply: DynamicConnectFailureReason::NoAvailableIp.into(), + }, + })); + }; + + // Spawn the ConnectedClientHandler for the new client + let (forward_from_tun_tx, close_tx, handle) = ConnectedClientHandler::start( + reply_to.clone(), + buffer_timeout, + version, + self.mixnet_client.split_sender(), + ); + + // Register the new client in the set of connected clients + self.connected_clients.connect( + new_ips, + reply_to.clone(), + forward_from_tun_tx, + close_tx, + handle, + ); + + Ok(Some(VersionedResponse { + version, + reply_to, + response: Response::DynamicConnect { + request_id, + reply: DynamicConnectSuccess { ips: new_ips }.into(), + }, + })) + } + + fn on_disconnect_request(&self, _disconnect_request: DisconnectRequest) -> PacketHandleResult { + log::info!("Received disconnect request: not implemented, dropping"); + Ok(None) + } + + async fn on_ping_request(&self, ping_request: PingRequest) -> PacketHandleResult { + Ok(Some(VersionedResponse { + version: ping_request.version, + reply_to: ping_request.sent_by, + response: Response::Pong { + request_id: ping_request.request_id, + }, + })) + } + + async fn on_health_request(&self, health_request: HealthRequest) -> PacketHandleResult { + Ok(Some(VersionedResponse { + version: health_request.version, + reply_to: health_request.sent_by, + response: Response::Health { + request_id: health_request.request_id, + reply: Box::new(HealthResponse { + build_info: nym_bin_common::bin_info_owned!(), + routable: None, + }), + }, + })) + } + + async fn on_control_request(&mut self, control_request: ControlRequest) -> PacketHandleResult { + match control_request { + ControlRequest::StaticConnect(r) => self.on_static_connect_request(r).await, + ControlRequest::DynamicConnect(r) => self.on_dynamic_connect_request(r).await, + ControlRequest::Disconnect(r) => self.on_disconnect_request(r), + ControlRequest::Ping(r) => self.on_ping_request(r).await, + ControlRequest::Health(r) => self.on_health_request(r).await, + } + } + fn on_version_mismatch( &self, _version: u8, @@ -669,52 +347,27 @@ impl MixnetListener { reconstructed: ReconstructedMessage, ) -> Result> { log::debug!( - "Received message with sender_tag: {:?}", - reconstructed.sender_tag + "Received message with sender_tag: {}", + reconstructed + .sender_tag + .map(|tag| tag.to_string()) + .unwrap_or("missing".to_owned()) ); - let (request, client_version) = match deserialize_request(&reconstructed) { + // First deserialize the request + let request = match IpPacketRequest::try_from(&reconstructed) { Err(IpPacketRouterError::InvalidPacketVersion(version)) => { + log::debug!("Received packet with invalid version: v{version}"); return Ok(vec![self.on_version_mismatch(version, &reconstructed)]); } req => req, }?; - match request.data { - IpPacketRequestData::StaticConnect(signed_connect_request) => { - verify_signed_request(&signed_connect_request, client_version)?; - let connect_request = signed_connect_request.request; - Ok(vec![ - self.on_static_connect_request(connect_request, client_version) - .await, - ]) - } - IpPacketRequestData::DynamicConnect(signed_connect_request) => { - verify_signed_request(&signed_connect_request, client_version)?; - let connect_request = signed_connect_request.request; - Ok(vec![ - self.on_dynamic_connect_request(connect_request, client_version) - .await, - ]) - } - IpPacketRequestData::Disconnect(signed_disconnect_request) => { - verify_signed_request(&signed_disconnect_request, client_version)?; - let disconnect_request = signed_disconnect_request.request; - Ok(vec![ - self.on_disconnect_request(disconnect_request, client_version) - ]) - } - IpPacketRequestData::Data(data_request) => { - self.on_data_request(data_request, client_version).await - } - IpPacketRequestData::Ping(_) => { - log::info!("Received ping request: not implemented, dropping"); - Ok(vec![]) - } - IpPacketRequestData::Health(_) => { - log::info!("Received health request: not implemented, dropping"); - Ok(vec![]) - } + log::debug!("Received request: {request}"); + + match request { + IpPacketRequest::Data(request) => self.on_data_request(request).await, + IpPacketRequest::Control(request) => Ok(vec![self.on_control_request(request).await]), } } @@ -738,15 +391,12 @@ impl MixnetListener { // When an incoming mixnet message triggers a response that we send back, such as during // connect handshake. - async fn handle_response(&self, response: Response) -> Result<()> { - let Some(recipient) = response.recipient() else { - log::error!("No recipient in response packet, this should NOT happen!"); - return Err(IpPacketRouterError::NoRecipientInResponse); - }; + async fn handle_response(&self, response: VersionedResponse) -> Result<()> { + let send_to = response.reply_to.clone(); + let response_bytes = response.try_into_bytes()?; + let input_message = + crate::util::create_message::create_input_message(&send_to, response_bytes); - let response_packet = response.to_bytes()?; - - let input_message = create_input_message(*recipient, response_packet); self.mixnet_client .send(input_message) .await @@ -807,77 +457,4 @@ impl MixnetListener { } } -fn deserialize_request( - reconstructed: &ReconstructedMessage, -) -> Result<(IpPacketRequest, SupportedClientVersion)> { - let request_version = *reconstructed - .message - .first() - .ok_or(IpPacketRouterError::EmptyPacket)?; - - // Check version of the request and convert to the latest version if necessary - let request = match request_version { - 6 => nym_ip_packet_requests::v6::request::IpPacketRequest::from_reconstructed_message( - reconstructed, - ) - .map_err(|err| IpPacketRouterError::FailedToDeserializeTaggedPacket { source: err }) - .map(|r| r.into()), - 7 => nym_ip_packet_requests::v7::request::IpPacketRequest::from_reconstructed_message( - reconstructed, - ) - .map_err(|err| IpPacketRouterError::FailedToDeserializeTaggedPacket { source: err }), - _ => { - log::info!("Received packet with invalid version: v{request_version}"); - Err(IpPacketRouterError::InvalidPacketVersion(request_version)) - } - }; - - let Some(request_version) = SupportedClientVersion::new(request_version) else { - return Err(IpPacketRouterError::InvalidPacketVersion(request_version)); - }; - - // Tag the request with the version of the request - request.map(|r| (r, request_version)) -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub(crate) enum SupportedClientVersion { - V6, - V7, -} - -impl SupportedClientVersion { - fn new(request_version: u8) -> Option { - match request_version { - 6 => Some(SupportedClientVersion::V6), - 7 => Some(SupportedClientVersion::V7), - _ => None, - } - } -} - -fn verify_signed_request( - request: &impl SignedRequest, - client_version: SupportedClientVersion, -) -> Result<()> { - if let Err(err) = request.verify() { - // If the client is V6, we don't care about missing signature - if client_version == SupportedClientVersion::V6 { - return Ok(()); - } - return Err(IpPacketRouterError::FailedToVerifyRequest { source: err }); - } - Ok(()) -} - -pub(crate) enum ConnectedClientEvent { - Disconnect(DisconnectEvent), - Connect(Box), -} - -pub(crate) struct DisconnectEvent(pub(crate) IpPair); - -pub(crate) struct ConnectEvent { - pub(crate) ips: IpPair, - pub(crate) forward_from_tun_tx: tokio::sync::mpsc::UnboundedSender>, -} +pub(crate) type PacketHandleResult = Result>; diff --git a/service-providers/ip-packet-router/src/non_linux_dummy.rs b/service-providers/ip-packet-router/src/non_linux_dummy.rs index 4369f72689..abac0ad1d3 100644 --- a/service-providers/ip-packet-router/src/non_linux_dummy.rs +++ b/service-providers/ip-packet-router/src/non_linux_dummy.rs @@ -1,5 +1,5 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only use std::io::Error; use std::pin::Pin; diff --git a/service-providers/ip-packet-router/src/request_filter/exit_policy/mod.rs b/service-providers/ip-packet-router/src/request_filter/exit_policy/mod.rs index 9dcecbdea3..3ebc68e774 100644 --- a/service-providers/ip-packet-router/src/request_filter/exit_policy/mod.rs +++ b/service-providers/ip-packet-router/src/request_filter/exit_policy/mod.rs @@ -1,5 +1,5 @@ // Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use std::net::SocketAddr; diff --git a/service-providers/ip-packet-router/src/request_filter/mod.rs b/service-providers/ip-packet-router/src/request_filter/mod.rs index 599a00a1d2..5e2f359570 100644 --- a/service-providers/ip-packet-router/src/request_filter/mod.rs +++ b/service-providers/ip-packet-router/src/request_filter/mod.rs @@ -1,5 +1,5 @@ // Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: GPL-3.0-only use crate::config::Config; use crate::error::IpPacketRouterError; diff --git a/service-providers/ip-packet-router/src/tun_listener.rs b/service-providers/ip-packet-router/src/tun_listener.rs index ce3a70ca68..0d4a99e3b9 100644 --- a/service-providers/ip-packet-router/src/tun_listener.rs +++ b/service-providers/ip-packet-router/src/tun_listener.rs @@ -1,3 +1,6 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + use std::collections::HashMap; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; @@ -5,34 +8,27 @@ use nym_ip_packet_requests::IpPair; use nym_task::TaskClient; #[cfg(target_os = "linux")] use tokio::io::AsyncReadExt; +use tokio::sync::mpsc; -use crate::{ - error::Result, - mixnet_listener::{self}, - util::parse_ip::parse_dst_addr, -}; +use crate::clients::{ConnectEvent, ConnectedClientEvent, DisconnectEvent}; +use crate::{error::Result, util::parse_ip::parse_dst_addr}; // The TUN listener keeps a local map of the connected clients that has its state updated by the // mixnet listener. Basically it's just so that we don't have to have mutexes around shared state. // It's even ok if this is slightly out of date -pub(crate) struct ConnectedClientMirror { - pub(crate) forward_from_tun_tx: tokio::sync::mpsc::UnboundedSender>, - pub(crate) ips: IpPair, +struct ConnectedClientMirror { + forward_from_tun_tx: mpsc::UnboundedSender>, + ips: IpPair, } pub(crate) struct ConnectedClientsListener { clients_ipv4: HashMap, clients_ipv6: HashMap, - connected_client_rx: - tokio::sync::mpsc::UnboundedReceiver, + connected_client_rx: mpsc::UnboundedReceiver, } impl ConnectedClientsListener { - pub(crate) fn new( - connected_client_rx: tokio::sync::mpsc::UnboundedReceiver< - mixnet_listener::ConnectedClientEvent, - >, - ) -> Self { + pub(crate) fn new(connected_client_rx: mpsc::UnboundedReceiver) -> Self { ConnectedClientsListener { clients_ipv4: HashMap::new(), clients_ipv6: HashMap::new(), @@ -40,17 +36,17 @@ impl ConnectedClientsListener { } } - pub(crate) fn get(&self, ip: &IpAddr) -> Option<&ConnectedClientMirror> { + fn get(&self, ip: &IpAddr) -> Option<&ConnectedClientMirror> { match ip { IpAddr::V4(ip) => self.clients_ipv4.get(ip), IpAddr::V6(ip) => self.clients_ipv6.get(ip), } } - pub(crate) fn update(&mut self, event: mixnet_listener::ConnectedClientEvent) { + pub(crate) fn update(&mut self, event: ConnectedClientEvent) { match event { - mixnet_listener::ConnectedClientEvent::Connect(connected_event) => { - let mixnet_listener::ConnectEvent { + ConnectedClientEvent::Connect(connected_event) => { + let ConnectEvent { ips, forward_from_tun_tx, } = *connected_event; @@ -70,9 +66,7 @@ impl ConnectedClientsListener { }, ); } - mixnet_listener::ConnectedClientEvent::Disconnect( - mixnet_listener::DisconnectEvent(ips), - ) => { + ConnectedClientEvent::Disconnect(DisconnectEvent(ips)) => { log::trace!("Disconnect client: {ips}"); self.clients_ipv4.remove(&ips.ipv4); self.clients_ipv6.remove(&ips.ipv6); @@ -106,9 +100,7 @@ impl TunListener { if forward_from_tun_tx.send(packet).is_err() { log::warn!("Failed to forward packet to connected client {dst_addr}: disconnecting it from tun listener"); self.connected_clients - .update(mixnet_listener::ConnectedClientEvent::Disconnect( - mixnet_listener::DisconnectEvent(*ips), - )); + .update(ConnectedClientEvent::Disconnect(DisconnectEvent(*ips))); } } else { log::info!( diff --git a/service-providers/ip-packet-router/src/util/create_message.rs b/service-providers/ip-packet-router/src/util/create_message.rs index dca63c21df..c2800c0516 100644 --- a/service-providers/ip-packet-router/src/util/create_message.rs +++ b/service-providers/ip-packet-router/src/util/create_message.rs @@ -1,11 +1,20 @@ -use nym_sdk::mixnet::{InputMessage, Recipient}; +use nym_sdk::mixnet::InputMessage; use nym_task::connections::TransmissionLane; +use crate::clients::ConnectedClientId; + pub(crate) fn create_input_message( - nym_address: Recipient, + recipient: &ConnectedClientId, response_packet: Vec, ) -> InputMessage { let lane = TransmissionLane::General; let packet_type = None; - InputMessage::new_regular(nym_address, response_packet, lane, packet_type) + match recipient { + ConnectedClientId::NymAddress(recipient) => { + InputMessage::new_regular(**recipient, response_packet, lane, packet_type) + } + ConnectedClientId::AnonymousSenderTag(tag) => { + InputMessage::new_reply(*tag, response_packet, lane, packet_type) + } + } } From f13ce6bf2de9db8673eb64b55ffbef7ce025e241 Mon Sep 17 00:00:00 2001 From: Jack Wampler Date: Thu, 27 Feb 2025 09:05:10 -0700 Subject: [PATCH 014/133] HickoryDnsResolver use a shared instance by default to limit fd use (#5523) --- Cargo.lock | 8 +-- Cargo.toml | 4 +- common/http-api-client/src/dns.rs | 100 ++++++++++++++++++++++++++---- 3 files changed, 95 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dac0f0ff20..6cc16f142c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3162,9 +3162,9 @@ dependencies = [ [[package]] name = "hickory-resolver" -version = "0.24.3" +version = "0.24.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf287bde7b776e85d7188e6e5db7cf410a2f9531fe82817eb87feed034c8d14" +checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e" dependencies = [ "cfg-if", "futures-util", @@ -9912,9 +9912,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c65998313f8e17d0d553d28f91a0df93e4dbbbf770279c7bc21ca0f09ea1a1f6" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" dependencies = [ "filetime", "libc", diff --git a/Cargo.toml b/Cargo.toml index bf65a433fa..14e86aadfc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -256,7 +256,7 @@ handlebars = "3.5.5" headers = "0.4.0" hex = "0.4.3" hex-literal = "0.3.3" -hickory-resolver = "0.24.3" +hickory-resolver = "0.24.4" hkdf = "0.12.3" hmac = "0.12.1" http = "1" @@ -329,7 +329,7 @@ subtle-encoding = "0.5" syn = "1" sysinfo = "0.33.0" tap = "1.0.1" -tar = "0.4.43" +tar = "0.4.44" tempfile = "3.15" thiserror = "2.0" time = "0.3.37" diff --git a/common/http-api-client/src/dns.rs b/common/http-api-client/src/dns.rs index d35d72487b..7e0519de98 100644 --- a/common/http-api-client/src/dns.rs +++ b/common/http-api-client/src/dns.rs @@ -13,15 +13,18 @@ use crate::ClientBuilder; -use std::{net::SocketAddr, sync::Arc}; +use std::{ + net::SocketAddr, + sync::{Arc, LazyLock}, +}; -use hickory_resolver::lookup_ip::LookupIp; use hickory_resolver::{ config::{LookupIpStrategy, NameServerConfigGroup, ResolverConfig, ResolverOpts}, error::ResolveError, lookup_ip::LookupIpIntoIter, TokioAsyncResolver, }; +use hickory_resolver::{error::ResolveErrorKind, lookup_ip::LookupIp}; use once_cell::sync::OnceCell; use reqwest::dns::{Addrs, Name, Resolve, Resolving}; use tracing::warn; @@ -38,6 +41,14 @@ struct SocketAddrs { iter: LookupIpIntoIter, } +// n.b. static items do not call [`Drop`] on program termination, so this won't be deallocated. +// this is fine, as the OS can deallocate the terminated program faster than we can free memory +// but tools like valgrind might report "memory leaks" as it isn't obvious this is intentional. +static SHARED_RESOLVER: LazyLock = LazyLock::new(|| { + tracing::debug!("Initializing shared DNS resolver"); + HickoryDnsResolver::default() +}); + #[derive(Debug, thiserror::Error)] #[error("hickory-dns resolver error: {hickory_error}")] /// Error occurring while resolving a hostname into an IP address. @@ -47,29 +58,62 @@ pub struct HickoryDnsError { } /// Wrapper around an `AsyncResolver`, which implements the `Resolve` trait. +/// +/// Typical use involves instantiating using the `Default` implementation and then resolving using +/// methods or trait implementations. +/// +/// The default initialization uses a shared underlying `AsyncResolver`. If a thread local resolver +/// is required use `thread_resolver()` to build a resolver with an independently instantiated +/// internal `AsyncResolver`. #[derive(Debug, Default, Clone)] pub struct HickoryDnsResolver { - /// Since we might not have been called in the context of a - /// Tokio Runtime in initialization, so we must delay the actual - /// construction of the resolver. + // Since we might not have been called in the context of a + // Tokio Runtime in initialization, so we must delay the actual + // construction of the resolver. state: Arc>, fallback: Arc>, + dont_use_shared: bool, } impl Resolve for HickoryDnsResolver { fn resolve(&self, name: Name) -> Resolving { let resolver = self.state.clone(); let fallback = self.fallback.clone(); + let independent = self.dont_use_shared; Box::pin(async move { - let resolver = resolver.get_or_try_init(new_resolver)?; + let resolver = resolver.get_or_try_init(|| { + // using a closure here is slightly gross, but this makes sure that if the + // lazy-init returns an error it can be handled by the client + if independent { + new_resolver() + } else { + Ok(SHARED_RESOLVER.state.get_or_try_init(new_resolver)?.clone()) + } + })?; // try the primary DNS resolver that we set up (DoH or DoT or whatever) let lookup = match resolver.lookup_ip(name.as_str()).await { Ok(res) => res, Err(e) => { // on failure use the fall back system configured DNS resolver - warn!("primary DNS failed w/ error {e}: using system fallback"); - let resolver = fallback.get_or_try_init(new_resolver_system)?; + match e.kind() { + ResolveErrorKind::NoRecordsFound { .. } => {} + _ => { + warn!("primary DNS failed w/ error {e}: using system fallback"); + } + } + let resolver = fallback.get_or_try_init(|| { + // using a closure here is slightly gross, but this makes sure that if the + // lazy-init returns an error it can be handled by the client + if independent { + new_resolver_system() + } else { + Ok(SHARED_RESOLVER + .fallback + .get_or_try_init(new_resolver_system)? + .clone()) + } + })?; resolver.lookup_ip(name.as_str()).await? } }; @@ -93,21 +137,55 @@ impl Iterator for SocketAddrs { impl HickoryDnsResolver { /// Attempt to resolve a domain name to a set of ['IpAddr']s pub async fn resolve_str(&self, name: &str) -> Result { - let resolver = self.state.get_or_try_init(new_resolver)?; + let resolver = self.state.get_or_try_init(|| self.new_resolver())?; // try the primary DNS resolver that we set up (DoH or DoT or whatever) let lookup = match resolver.lookup_ip(name).await { Ok(res) => res, Err(e) => { // on failure use the fall back system configured DNS resolver - warn!("primary DNS failed w/ error {e}: using system fallback"); - let resolver = self.fallback.get_or_try_init(new_resolver_system)?; + match e.kind() { + ResolveErrorKind::NoRecordsFound { .. } => {} + _ => { + warn!("primary DNS failed w/ error {e}: using system fallback"); + } + } + let resolver = self + .fallback + .get_or_try_init(|| self.new_resolver_system())?; resolver.lookup_ip(name).await? } }; Ok(lookup) } + + /// Create a (lazy-initialized) resolver that is not shared across threads. + pub fn thread_resolver() -> Self { + Self { + dont_use_shared: true, + ..Default::default() + } + } + + fn new_resolver(&self) -> Result { + if self.dont_use_shared { + new_resolver() + } else { + Ok(SHARED_RESOLVER.state.get_or_try_init(new_resolver)?.clone()) + } + } + + fn new_resolver_system(&self) -> Result { + if self.dont_use_shared { + new_resolver_system() + } else { + Ok(SHARED_RESOLVER + .fallback + .get_or_try_init(new_resolver_system)? + .clone()) + } + } } /// Create a new resolver with a custom DoT based configuration. The options are overridden to look From 40dd7dc95edf1665c3cd1ea1eb317f5748f67d16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Fri, 28 Feb 2025 10:55:30 +0100 Subject: [PATCH 015/133] Add RUSTUP_PERMIT_COPY_RENAME to ci-build (#5533) --- .github/workflows/ci-build-upload-binaries.yml | 1 + .github/workflows/ci-build-vpn-api-wasm.yml | 1 + .github/workflows/ci-build.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/ci-build-upload-binaries.yml b/.github/workflows/ci-build-upload-binaries.yml index 928cd92a52..a3e45d811f 100644 --- a/.github/workflows/ci-build-upload-binaries.yml +++ b/.github/workflows/ci-build-upload-binaries.yml @@ -26,6 +26,7 @@ jobs: runs-on: ${{ matrix.platform }} env: CARGO_TERM_COLOR: always + RUSTUP_PERMIT_COPY_RENAME: 1 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/ci-build-vpn-api-wasm.yml b/.github/workflows/ci-build-vpn-api-wasm.yml index a4abd8fa3e..76694ecca0 100644 --- a/.github/workflows/ci-build-vpn-api-wasm.yml +++ b/.github/workflows/ci-build-vpn-api-wasm.yml @@ -12,6 +12,7 @@ jobs: runs-on: arc-ubuntu-22.04 env: CARGO_TERM_COLOR: always + RUSTUP_PERMIT_COPY_RENAME: 1 steps: - name: Check out repository code uses: actions/checkout@v4 diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 72ee5560ec..314b8176d8 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -37,6 +37,7 @@ jobs: env: CARGO_TERM_COLOR: always IPINFO_API_TOKEN: ${{ secrets.IPINFO_API_TOKEN }} + RUSTUP_PERMIT_COPY_RENAME: 1 steps: - name: Install Dependencies (Linux) run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools protobuf-compiler From 128f69a5d67a9e87d1691d9cf9d3a9365fdcc5bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Fri, 28 Feb 2025 13:04:53 +0100 Subject: [PATCH 016/133] Simplify IPR v8 (#5532) * Purge stuff from v8 * Adapt to v8 changes * Use protocol in ipr header * Remove commented out code * Remove unused error --- Cargo.lock | 4 + common/ip-packet-requests/Cargo.toml | 1 + common/ip-packet-requests/src/v8/request.rs | 384 ++++-------------- common/ip-packet-requests/src/v8/response.rs | 66 +-- .../Cargo.toml | 4 + .../src/lib.rs | 103 +++++ service-providers/ip-packet-router/Cargo.toml | 1 + .../ip-packet-router/src/clients/client_id.rs | 14 - .../ip-packet-router/src/error.rs | 16 +- .../ip-packet-router/src/messages/request.rs | 25 +- .../src/messages/request/v8.rs | 170 +++----- .../ip-packet-router/src/messages/response.rs | 2 +- .../src/messages/response/v8.rs | 88 ++-- 13 files changed, 311 insertions(+), 567 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6cc16f142c..e45cc5e1f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5913,6 +5913,7 @@ dependencies = [ "bytes", "nym-bin-common", "nym-crypto", + "nym-service-provider-requests-common", "nym-sphinx", "rand 0.8.5", "serde", @@ -5944,6 +5945,7 @@ dependencies = [ "nym-network-defaults", "nym-network-requester", "nym-sdk", + "nym-service-provider-requests-common", "nym-service-providers-common", "nym-sphinx", "nym-task", @@ -6517,7 +6519,9 @@ dependencies = [ name = "nym-service-provider-requests-common" version = "0.1.0" dependencies = [ + "bincode", "serde", + "thiserror 2.0.11", ] [[package]] diff --git a/common/ip-packet-requests/Cargo.toml b/common/ip-packet-requests/Cargo.toml index 60581a4278..cd9663dc90 100644 --- a/common/ip-packet-requests/Cargo.toml +++ b/common/ip-packet-requests/Cargo.toml @@ -13,6 +13,7 @@ bincode = { workspace = true } bytes = { workspace = true } nym-bin-common = { path = "../bin-common" } nym-crypto = { path = "../crypto" } +nym-service-provider-requests-common = { path = "../service-provider-requests-common" } nym-sphinx = { path = "../nymsphinx" } rand = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/common/ip-packet-requests/src/v8/request.rs b/common/ip-packet-requests/src/v8/request.rs index 15cf47827f..c5e49e83b7 100644 --- a/common/ip-packet-requests/src/v8/request.rs +++ b/common/ip-packet-requests/src/v8/request.rs @@ -1,20 +1,14 @@ -use std::{fmt, sync::Arc}; +use std::fmt; -use nym_crypto::asymmetric::ed25519; -use nym_sphinx::addressing::Recipient; +use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; -use crate::{ - sign::{SignatureError, SignedRequest}, - IpPair, -}; - use super::VERSION; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct IpPacketRequest { - pub version: u8, + pub protocol: Protocol, pub data: IpPacketRequestData, } @@ -26,9 +20,8 @@ pub enum IpPacketRequestData { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum ControlRequest { - StaticConnect(SignedStaticConnectRequest), - DynamicConnect(SignedDynamicConnectRequest), - Disconnect(SignedDisconnectRequest), + Connect(ConnectRequest), + Disconnect(DisconnectRequest), Ping(PingRequest), Health(HealthRequest), } @@ -39,35 +32,10 @@ pub struct DataRequest { pub ip_packets: bytes::Bytes, } -// A static connect request is when the client provides the internal IP address it will use on the -// ip packet router. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct StaticConnectRequest { - pub request_id: u64, - - // The requested internal IP addresses. - pub ips: IpPair, - - // The maximum time in milliseconds the IPR should wait when filling up a mix packet - // with ip packets. - pub buffer_timeout: Option, - - // Timestamp of when the request was sent by the client. - pub timestamp: OffsetDateTime, - - pub sender: SentBy, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct SignedStaticConnectRequest { - pub request: StaticConnectRequest, - pub signature: Option, -} - // A dynamic connect request is when the client does not provide the internal IP address it will use // on the ip packet router, and instead requests one to be assigned to it. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct DynamicConnectRequest { +pub struct ConnectRequest { pub request_id: u64, // The maximum time in milliseconds the IPR should wait when filling up a mix packet @@ -76,14 +44,6 @@ pub struct DynamicConnectRequest { // Timestamp of when the request was sent by the client. pub timestamp: OffsetDateTime, - - pub sender: SentBy, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct SignedDynamicConnectRequest { - pub request: DynamicConnectRequest, - pub signature: Option, } // A disconnect request is when the client wants to disconnect from the ip packet router and free @@ -94,14 +54,6 @@ pub struct DisconnectRequest { // Timestamp of when the request was sent by the client. pub timestamp: OffsetDateTime, - - pub sender: SentBy, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct SignedDisconnectRequest { - pub request: DisconnectRequest, - pub signature: Option, } // A ping request is when the client wants to check if the ip packet router is still alive. @@ -109,8 +61,6 @@ pub struct SignedDisconnectRequest { pub struct PingRequest { pub request_id: u64, - pub sender: SentBy, - // Timestamp of when the request was sent by the client. pub timestamp: OffsetDateTime, } @@ -119,142 +69,89 @@ pub struct PingRequest { pub struct HealthRequest { pub request_id: u64, - pub sender: SentBy, - // Timestamp of when the request was sent by the client. pub timestamp: OffsetDateTime, } -// When constructing the request, use this return address. It has the keypair to be able to sign -// the request if we reveal the sender. -#[derive(Clone, Debug)] -pub enum ReturnAddress { - AnonymousSenderTag, - NymAddress { - reply_to: Box, - signing_keypair: Arc, - }, -} - -// The serialized sender field in the request, that does not contain the keypair. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub enum SentBy { - AnonymousSenderTag, - NymAddress(Box), -} - impl IpPacketRequest { - pub fn new_connect_request( - ips: Option, - buffer_timeout: Option, - return_address: ReturnAddress, - ) -> Result<(Self, u64), SignatureError> { + pub fn new_connect_request(buffer_timeout: Option) -> (Self, u64) { + let protocol = Protocol { + version: VERSION, + service_provider_type: ServiceProviderType::IpPacketRouter, + }; let request_id = rand::random(); let timestamp = OffsetDateTime::now_utc(); - let sender = return_address.clone().into(); - let request = if let Some(ips) = ips { - let request = StaticConnectRequest { - request_id, - ips, - buffer_timeout, - timestamp, - sender, - }; - let signature = return_address - .signing_key() - .map(|keypair| { - request - .to_bytes() - .map(|bytes| keypair.private_key().sign(bytes)) - }) - .transpose()?; - ControlRequest::StaticConnect(SignedStaticConnectRequest { request, signature }) - } else { - let request = DynamicConnectRequest { - request_id, - buffer_timeout, - timestamp, - sender, - }; - let signature = return_address - .signing_key() - .map(|keypair| { - request - .to_bytes() - .map(|bytes| keypair.private_key().sign(bytes)) - }) - .transpose()?; - ControlRequest::DynamicConnect(SignedDynamicConnectRequest { request, signature }) + let connect = ConnectRequest { + request_id, + buffer_timeout, + timestamp, }; let request = Self { - version: VERSION, - data: IpPacketRequestData::Control(Box::new(request)), + protocol, + data: IpPacketRequestData::Control(Box::new(ControlRequest::Connect(connect))), }; - Ok((request, request_id)) + (request, request_id) } - pub fn new_disconnect_request( - return_address: ReturnAddress, - ) -> Result<(Self, u64), SignatureError> { + pub fn new_disconnect_request() -> (Self, u64) { + let protocol = Protocol { + version: VERSION, + service_provider_type: ServiceProviderType::IpPacketRouter, + }; let request_id = rand::random(); let timestamp = OffsetDateTime::now_utc(); - let sender = return_address.clone().into(); - let request = DisconnectRequest { + let disconnect = DisconnectRequest { request_id, timestamp, - sender, }; - let signature = return_address - .signing_key() - .map(|keypair| { - request - .to_bytes() - .map(|bytes| keypair.private_key().sign(bytes)) - }) - .transpose()?; let request = Self { - version: VERSION, - data: IpPacketRequestData::Control(Box::new(ControlRequest::Disconnect( - SignedDisconnectRequest { request, signature }, - ))), + protocol, + data: IpPacketRequestData::Control(Box::new(ControlRequest::Disconnect(disconnect))), }; - Ok((request, request_id)) + (request, request_id) } pub fn new_data_request(ip_packets: bytes::Bytes) -> Self { Self { - version: VERSION, + protocol: Protocol { + version: VERSION, + service_provider_type: ServiceProviderType::IpPacketRouter, + }, data: IpPacketRequestData::Data(DataRequest { ip_packets }), } } - pub fn new_ping(return_address: ReturnAddress) -> (Self, u64) { + pub fn new_ping() -> (Self, u64) { + let protocol = Protocol { + version: VERSION, + service_provider_type: ServiceProviderType::IpPacketRouter, + }; let request_id = rand::random(); let timestamp = OffsetDateTime::now_utc(); - let sender = return_address.into(); let ping_request = PingRequest { request_id, - sender, timestamp, }; let request = Self { - version: VERSION, + protocol, data: IpPacketRequestData::Control(Box::new(ControlRequest::Ping(ping_request))), }; (request, request_id) } - pub fn new_health_request(return_address: ReturnAddress) -> (Self, u64) { + pub fn new_health_request() -> (Self, u64) { + let protocol = Protocol { + version: VERSION, + service_provider_type: ServiceProviderType::IpPacketRouter, + }; let request_id = rand::random(); let timestamp = OffsetDateTime::now_utc(); - let sender = return_address.into(); let health_request = HealthRequest { request_id, - sender, timestamp, }; let request = Self { - version: VERSION, + protocol, data: IpPacketRequestData::Control(Box::new(ControlRequest::Health(health_request))), }; (request, request_id) @@ -267,13 +164,6 @@ impl IpPacketRequest { } } - pub fn verify(&self) -> Result<(), SignatureError> { - match &self.data { - IpPacketRequestData::Control(c) => c.verify(), - IpPacketRequestData::Data(_) => Ok(()), - } - } - pub fn to_bytes(&self) -> Result, bincode::Error> { use bincode::Options; crate::make_bincode_serializer().serialize(self) @@ -292,7 +182,7 @@ impl fmt::Display for IpPacketRequest { write!( f, "IpPacketRequest {{ version: {}, data: {} }}", - self.version, self.data + self.protocol.version, self.data ) } } @@ -309,30 +199,18 @@ impl fmt::Display for IpPacketRequestData { impl ControlRequest { fn id(&self) -> u64 { match self { - ControlRequest::StaticConnect(request) => request.request.request_id, - ControlRequest::DynamicConnect(request) => request.request.request_id, - ControlRequest::Disconnect(request) => request.request.request_id, + ControlRequest::Connect(request) => request.request_id, + ControlRequest::Disconnect(request) => request.request_id, ControlRequest::Ping(request) => request.request_id, ControlRequest::Health(request) => request.request_id, } } - - fn verify(&self) -> Result<(), SignatureError> { - match self { - ControlRequest::StaticConnect(request) => request.verify(), - ControlRequest::DynamicConnect(request) => request.verify(), - ControlRequest::Disconnect(request) => request.verify(), - ControlRequest::Ping(_) => Ok(()), - ControlRequest::Health(_) => Ok(()), - } - } } impl fmt::Display for ControlRequest { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - ControlRequest::StaticConnect(_) => write!(f, "StaticConnect"), - ControlRequest::DynamicConnect(_) => write!(f, "DynamicConnect"), + ControlRequest::Connect(_) => write!(f, "Connect"), ControlRequest::Disconnect(_) => write!(f, "Disconnect"), ControlRequest::Ping(_) => write!(f, "Ping"), ControlRequest::Health(_) => write!(f, "Health"), @@ -340,128 +218,17 @@ impl fmt::Display for ControlRequest { } } -impl StaticConnectRequest { - pub fn to_bytes(&self) -> Result, SignatureError> { +impl ConnectRequest { + pub fn to_bytes(&self) -> Result, bincode::Error> { use bincode::Options; - crate::make_bincode_serializer() - .serialize(self) - .map_err(|error| SignatureError::RequestSerializationError { - message: "failed to serialize request to binary".to_string(), - error, - }) - } -} - -impl SignedRequest for SignedStaticConnectRequest { - fn request_as_bytes(&self) -> Result, SignatureError> { - self.request.to_bytes() - } - - fn timestamp(&self) -> OffsetDateTime { - self.request.timestamp - } - - fn identity(&self) -> Option<&ed25519::PublicKey> { - self.request.sender.identity() - } - - fn signature(&self) -> Option<&ed25519::Signature> { - self.signature.as_ref() - } -} - -impl DynamicConnectRequest { - pub fn to_bytes(&self) -> Result, SignatureError> { - use bincode::Options; - crate::make_bincode_serializer() - .serialize(self) - .map_err(|error| SignatureError::RequestSerializationError { - message: "failed to serialize request to binary".to_string(), - error, - }) - } -} - -impl SignedRequest for SignedDynamicConnectRequest { - fn request_as_bytes(&self) -> Result, SignatureError> { - self.request.to_bytes() - } - - fn timestamp(&self) -> OffsetDateTime { - self.request.timestamp - } - - fn identity(&self) -> Option<&ed25519::PublicKey> { - self.request.sender.identity() - } - - fn signature(&self) -> Option<&ed25519::Signature> { - self.signature.as_ref() + crate::make_bincode_serializer().serialize(self) } } impl DisconnectRequest { - pub fn to_bytes(&self) -> Result, SignatureError> { + pub fn to_bytes(&self) -> Result, bincode::Error> { use bincode::Options; - crate::make_bincode_serializer() - .serialize(self) - .map_err(|error| SignatureError::RequestSerializationError { - message: "failed to serialize request to binary".to_string(), - error, - }) - } -} - -impl SignedRequest for SignedDisconnectRequest { - fn request_as_bytes(&self) -> Result, SignatureError> { - self.request.to_bytes() - } - - fn timestamp(&self) -> OffsetDateTime { - self.request.timestamp - } - - fn identity(&self) -> Option<&ed25519::PublicKey> { - self.request.sender.identity() - } - - fn signature(&self) -> Option<&ed25519::Signature> { - self.signature.as_ref() - } -} - -impl SentBy { - fn identity(&self) -> Option<&ed25519::PublicKey> { - match self { - SentBy::AnonymousSenderTag => None, - SentBy::NymAddress(recipient) => Some(recipient.identity()), - } - } -} - -impl From for SentBy { - fn from(recipient: Recipient) -> Self { - SentBy::NymAddress(Box::new(recipient)) - } -} - -impl ReturnAddress { - fn signing_key(&self) -> Option<&ed25519::KeyPair> { - match self { - ReturnAddress::AnonymousSenderTag => None, - ReturnAddress::NymAddress { - signing_keypair, .. - } => Some(signing_keypair.as_ref()), - } - } -} - -impl From for SentBy { - fn from(return_address: ReturnAddress) -> Self { - match return_address { - ReturnAddress::AnonymousSenderTag => SentBy::AnonymousSenderTag, - ReturnAddress::NymAddress { reply_to, .. } => SentBy::NymAddress(reply_to), - } + crate::make_bincode_serializer().serialize(self) } } @@ -470,47 +237,44 @@ mod tests { use time::macros::datetime; use super::*; - use std::net::{Ipv4Addr, Ipv6Addr}; - use std::str::FromStr; #[test] fn check_size_of_request() { let connect = IpPacketRequest { - version: 4, - data: IpPacketRequestData::Control(Box::new(ControlRequest::StaticConnect( - SignedStaticConnectRequest { - request: StaticConnectRequest { - request_id: 123, - ips: IpPair::new( - Ipv4Addr::from_str("10.0.0.1").unwrap(), - Ipv6Addr::from_str("fc00::1").unwrap(), - ), - buffer_timeout: None, - timestamp: datetime!(2024-01-01 12:59:59.5 UTC), - sender: SentBy::AnonymousSenderTag, - }, - signature: None, - }, - ))), + protocol: Protocol { + version: 4, + service_provider_type: ServiceProviderType::IpPacketRouter, + }, + data: IpPacketRequestData::Control(Box::new(ControlRequest::Connect(ConnectRequest { + request_id: 123, + buffer_timeout: None, + timestamp: datetime!(2024-01-01 12:59:59.5 UTC), + }))), }; - assert_eq!(connect.to_bytes().unwrap().len(), 42); + assert_eq!(connect.to_bytes().unwrap().len(), 21); } #[test] fn check_size_of_data() { let data = IpPacketRequest { - version: 4, + protocol: Protocol { + version: 4, + service_provider_type: ServiceProviderType::IpPacketRouter, + }, data: IpPacketRequestData::Data(DataRequest { ip_packets: bytes::Bytes::from(vec![1u8; 32]), }), }; - assert_eq!(data.to_bytes().unwrap().len(), 35); + assert_eq!(data.to_bytes().unwrap().len(), 36); } #[test] fn serialize_and_deserialize_data_request() { let data = IpPacketRequest { - version: 4, + protocol: Protocol { + version: 4, + service_provider_type: ServiceProviderType::IpPacketRouter, + }, data: IpPacketRequestData::Data(DataRequest { ip_packets: bytes::Bytes::from(vec![1, 2, 4, 2, 5]), }), @@ -525,7 +289,11 @@ mod tests { ) .unwrap(); - assert_eq!(deserialized.version, 4); + assert_eq!(deserialized.protocol.version, 4); + assert_eq!( + deserialized.protocol.service_provider_type, + ServiceProviderType::IpPacketRouter + ); assert_eq!( deserialized.data, IpPacketRequestData::Data(DataRequest { diff --git a/common/ip-packet-requests/src/v8/response.rs b/common/ip-packet-requests/src/v8/response.rs index a919d2110f..946a8621be 100644 --- a/common/ip-packet-requests/src/v8/response.rs +++ b/common/ip-packet-requests/src/v8/response.rs @@ -24,11 +24,8 @@ pub struct DataResponse { #[derive(Clone, Debug, Serialize, Deserialize)] pub enum ControlResponse { - // Response for a static connect request - StaticConnect(StaticConnectResponse), - - // Response for a dynamic connect request - DynamicConnect(DynamicConnectResponse), + // Response for a connect request + Connect(ConnectResponse), // Response for a disconnect initiqated by the client Disconnect(DisconnectResponse), @@ -47,51 +44,24 @@ pub enum ControlResponse { } #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct StaticConnectResponse { +pub struct ConnectResponse { pub request_id: u64, - pub reply: StaticConnectResponseReply, + pub reply: ConnectResponseReply, } #[derive(Clone, Debug, Serialize, Deserialize)] -pub enum StaticConnectResponseReply { - Success, - Failure(StaticConnectFailureReason), -} - -#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] -pub enum StaticConnectFailureReason { - #[error("requested ip address is already in use")] - RequestedIpAlreadyInUse, - - #[error("client is already connected")] - ClientAlreadyConnected, - - #[error("request timestamp is out of date")] - OutOfDateTimestamp, - - #[error("{0}")] - Other(String), +pub enum ConnectResponseReply { + Success(ConnectSuccess), + Failure(ConnectFailureReason), } #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct DynamicConnectResponse { - pub request_id: u64, - pub reply: DynamicConnectResponseReply, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum DynamicConnectResponseReply { - Success(DynamicConnectSuccess), - Failure(DynamicConnectFailureReason), -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct DynamicConnectSuccess { +pub struct ConnectSuccess { pub ips: IpPair, } #[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)] -pub enum DynamicConnectFailureReason { +pub enum ConnectFailureReason { #[error("client is already connected")] ClientAlreadyConnected, @@ -229,8 +199,7 @@ impl IpPacketResponseData { impl ControlResponse { fn id(&self) -> Option { match self { - ControlResponse::StaticConnect(response) => Some(response.request_id), - ControlResponse::DynamicConnect(response) => Some(response.request_id), + ControlResponse::Connect(response) => Some(response.request_id), ControlResponse::Disconnect(response) => Some(response.request_id), ControlResponse::UnrequestedDisconnect(_) => None, ControlResponse::Pong(response) => Some(response.request_id), @@ -240,20 +209,11 @@ impl ControlResponse { } } -impl StaticConnectResponseReply { +impl ConnectResponseReply { pub fn is_success(&self) -> bool { match self { - StaticConnectResponseReply::Success => true, - StaticConnectResponseReply::Failure(_) => false, - } - } -} - -impl DynamicConnectResponseReply { - pub fn is_success(&self) -> bool { - match self { - DynamicConnectResponseReply::Success(_) => true, - DynamicConnectResponseReply::Failure(_) => false, + ConnectResponseReply::Success(_) => true, + ConnectResponseReply::Failure(_) => false, } } } diff --git a/common/service-provider-requests-common/Cargo.toml b/common/service-provider-requests-common/Cargo.toml index f88fdcc0e1..63aef2eb76 100644 --- a/common/service-provider-requests-common/Cargo.toml +++ b/common/service-provider-requests-common/Cargo.toml @@ -12,3 +12,7 @@ readme.workspace = true [dependencies] serde = { workspace = true, features = ["derive"] } +thiserror.workspace = true + +[dev-dependencies] +bincode.workspace = true diff --git a/common/service-provider-requests-common/src/lib.rs b/common/service-provider-requests-common/src/lib.rs index 3b294baba7..2840a9321c 100644 --- a/common/service-provider-requests-common/src/lib.rs +++ b/common/service-provider-requests-common/src/lib.rs @@ -1,8 +1,16 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::fmt; + use serde::{Deserialize, Serialize}; +#[derive(thiserror::Error, Debug)] +pub enum ProtocolError { + #[error("invalid service provider type: {0}")] + InvalidServiceProviderType(u8), +} + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[repr(u8)] pub enum ServiceProviderType { @@ -11,8 +19,103 @@ pub enum ServiceProviderType { Authenticator = 2, } +impl fmt::Display for ServiceProviderType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NetworkRequester => write!(f, "NetworkRequester"), + Self::IpPacketRouter => write!(f, "IpPacketRouter"), + Self::Authenticator => write!(f, "Authenticator"), + } + } +} + +impl TryFrom for ServiceProviderType { + type Error = ProtocolError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::NetworkRequester), + 1 => Ok(Self::IpPacketRouter), + 2 => Ok(Self::Authenticator), + _ => Err(ProtocolError::InvalidServiceProviderType(value)), + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Protocol { pub version: u8, pub service_provider_type: ServiceProviderType, } + +impl TryFrom<&[u8; 2]> for Protocol { + type Error = ProtocolError; + + fn try_from(bytes: &[u8; 2]) -> Result { + let version = bytes[0]; + let service_provider_type = ServiceProviderType::try_from(bytes[1]) + .map_err(|_| ProtocolError::InvalidServiceProviderType(bytes[1]))?; + + Ok(Self { + version, + service_provider_type, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bincode::Options; + + fn make_bincode_serializer() -> impl bincode::Options { + bincode::DefaultOptions::new() + .with_big_endian() + .with_varint_encoding() + } + + #[test] + fn protocol_serialization() { + let protocol = Protocol { + version: 4, + service_provider_type: ServiceProviderType::NetworkRequester, + }; + + let serialized = make_bincode_serializer().serialize(&protocol).unwrap(); + let deserialized: Protocol = make_bincode_serializer().deserialize(&serialized).unwrap(); + + assert_eq!(protocol, deserialized); + } + + #[test] + fn compact_serialization() { + let protocol = Protocol { + version: 4, + service_provider_type: ServiceProviderType::NetworkRequester, + }; + + let serialized = make_bincode_serializer().serialize(&protocol).unwrap(); + assert_eq!(serialized.len(), 2); + } + + #[test] + fn protocol_deserialization() { + let bytes = [4, ServiceProviderType::NetworkRequester as u8]; + let deserialized = Protocol::try_from(&bytes).unwrap(); + + let expected = Protocol { + version: 4, + service_provider_type: ServiceProviderType::NetworkRequester, + }; + + assert_eq!(expected, deserialized); + } + + #[test] + fn invalid_protocol_deserialization() { + let bytes = [4, 3]; + let deserialized = Protocol::try_from(&bytes); + + assert!(deserialized.is_err()); + } +} diff --git a/service-providers/ip-packet-router/Cargo.toml b/service-providers/ip-packet-router/Cargo.toml index fc7303a872..dd7d953714 100644 --- a/service-providers/ip-packet-router/Cargo.toml +++ b/service-providers/ip-packet-router/Cargo.toml @@ -27,6 +27,7 @@ nym-ip-packet-requests = { path = "../../common/ip-packet-requests" } nym-network-defaults = { path = "../../common/network-defaults" } nym-network-requester = { path = "../network-requester" } nym-sdk = { path = "../../sdk/rust/nym-sdk" } +nym-service-provider-requests-common = { path = "../../common/service-provider-requests-common" } nym-service-providers-common = { path = "../common" } nym-sphinx = { path = "../../common/nymsphinx" } nym-task = { path = "../../common/task" } diff --git a/service-providers/ip-packet-router/src/clients/client_id.rs b/service-providers/ip-packet-router/src/clients/client_id.rs index 038efbda76..5add99a724 100644 --- a/service-providers/ip-packet-router/src/clients/client_id.rs +++ b/service-providers/ip-packet-router/src/clients/client_id.rs @@ -3,7 +3,6 @@ use std::fmt; -use nym_ip_packet_requests::v8::request::SentBy; use nym_sdk::mixnet::{AnonymousSenderTag, Recipient}; use crate::error::{IpPacketRouterError, Result}; @@ -43,16 +42,3 @@ impl From for ConnectedClientId { ConnectedClientId::AnonymousSenderTag(tag) } } - -impl TryFrom<(SentBy, Option)> for ConnectedClientId { - type Error = IpPacketRouterError; - - fn try_from((sent_by, sender_tag): (SentBy, Option)) -> Result { - match sent_by { - SentBy::NymAddress(nym_address) => Ok(ConnectedClientId::NymAddress(nym_address)), - SentBy::AnonymousSenderTag => sender_tag - .map(ConnectedClientId::AnonymousSenderTag) - .ok_or(IpPacketRouterError::InvalidReplyTo), - } - } -} diff --git a/service-providers/ip-packet-router/src/error.rs b/service-providers/ip-packet-router/src/error.rs index 0a81bde0ed..49965b8ad8 100644 --- a/service-providers/ip-packet-router/src/error.rs +++ b/service-providers/ip-packet-router/src/error.rs @@ -7,6 +7,7 @@ pub use nym_client_core::error::ClientCoreError; use nym_exit_policy::PolicyError; use nym_id::NymIdError; use nym_ip_packet_requests::sign::SignatureError; +use nym_service_provider_requests_common::{ProtocolError, ServiceProviderType}; #[derive(thiserror::Error, Debug)] pub enum IpPacketRouterError { @@ -96,11 +97,20 @@ pub enum IpPacketRouterError { #[error("failed to verify request: {source}")] FailedToVerifyRequest { source: SignatureError }, - #[error("client is connected with an invalid version: {version}")] - InvalidConnectedClientVersion { version: u8 }, - #[error("invalid reply-to address in the response")] InvalidReplyTo, + + #[error("missing sender tag in the request")] + MissingSenderTag, + + #[error("unsupported response: {0}")] + UnsupportedResponse(String), + + #[error("invalid service provider type: {0}")] + InvalidServiceProviderType(ServiceProviderType), + + #[error("failed to deserialize protocol: {source}")] + FailedToDeserializeProtocol { source: ProtocolError }, } pub type Result = std::result::Result; diff --git a/service-providers/ip-packet-router/src/messages/request.rs b/service-providers/ip-packet-router/src/messages/request.rs index 3b781275a7..665abe024a 100644 --- a/service-providers/ip-packet-router/src/messages/request.rs +++ b/service-providers/ip-packet-router/src/messages/request.rs @@ -11,6 +11,7 @@ use nym_ip_packet_requests::{ v8::request::IpPacketRequest as IpPacketRequestV8, IpPair, }; use nym_sdk::mixnet::ReconstructedMessage; +use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; use std::fmt; use crate::{clients::ConnectedClientId, error::IpPacketRouterError}; @@ -83,9 +84,23 @@ impl TryFrom<&ReconstructedMessage> for IpPacketRequest { fn try_from(reconstructed: &ReconstructedMessage) -> Result { let request_version = *reconstructed .message - .first() + .first_chunk::<2>() .ok_or(IpPacketRouterError::EmptyPacket)?; + // With version v8 and onwards, the type of the service provider is included in the + // header. + if request_version[0] >= 8 { + let protocol = Protocol::try_from(&request_version) + .map_err(|source| IpPacketRouterError::FailedToDeserializeProtocol { source })?; + + if protocol.service_provider_type != ServiceProviderType::IpPacketRouter { + return Err(IpPacketRouterError::InvalidServiceProviderType( + protocol.service_provider_type, + )); + } + } + + let request_version = request_version[0]; match request_version { 6 => { let request_v6 = IpPacketRequestV6::from_reconstructed_message(reconstructed) @@ -109,10 +124,10 @@ impl TryFrom<&ReconstructedMessage> for IpPacketRequest { .map_err( |source| IpPacketRouterError::FailedToDeserializeTaggedPacket { source }, )?; - request_v8 - .verify() - .map_err(|source| IpPacketRouterError::FailedToVerifyRequest { source })?; - IpPacketRequest::try_from((request_v8, reconstructed.sender_tag)) + let sender_tag = reconstructed + .sender_tag + .ok_or(IpPacketRouterError::MissingSenderTag)?; + Ok(IpPacketRequest::from((request_v8, sender_tag))) } _ => { log::info!("Received packet with invalid version: v{request_version}"); diff --git a/service-providers/ip-packet-router/src/messages/request/v8.rs b/service-providers/ip-packet-router/src/messages/request/v8.rs index e7190faae7..d54eb22d7c 100644 --- a/service-providers/ip-packet-router/src/messages/request/v8.rs +++ b/service-providers/ip-packet-router/src/messages/request/v8.rs @@ -2,34 +2,27 @@ // SPDX-License-Identifier: GPL-3.0-only use nym_ip_packet_requests::v8::request::{ - ControlRequest as ControlRequestV8, DataRequest as DataRequestV8, - DisconnectRequest as DisconnectRequestV8, DynamicConnectRequest as DynamicConnectRequestV8, + ConnectRequest as ConnectRequestV8, ControlRequest as ControlRequestV8, + DataRequest as DataRequestV8, DisconnectRequest as DisconnectRequestV8, HealthRequest as HealthRequestV8, IpPacketRequest as IpPacketRequestV8, IpPacketRequestData as IpPacketRequestDataV8, PingRequest as PingRequestV8, - StaticConnectRequest as StaticConnectRequestV8, }; use nym_sdk::mixnet::AnonymousSenderTag; -use crate::error::IpPacketRouterError; - use super::{ - ClientVersion, ConnectedClientId, ControlRequest, DataRequest, DisconnectRequest, - DynamicConnectRequest, HealthRequest, IpPacketRequest, PingRequest, StaticConnectRequest, + ClientVersion, ControlRequest, DataRequest, DisconnectRequest, DynamicConnectRequest, + HealthRequest, IpPacketRequest, PingRequest, }; -impl TryFrom<(IpPacketRequestV8, Option)> for IpPacketRequest { - type Error = IpPacketRouterError; - - fn try_from( - (request, sender_tag): (IpPacketRequestV8, Option), - ) -> Result { +impl From<(IpPacketRequestV8, AnonymousSenderTag)> for IpPacketRequest { + fn from((request, sender_tag): (IpPacketRequestV8, AnonymousSenderTag)) -> Self { let version = ClientVersion::V8; - Ok(match request.data { + match request.data { IpPacketRequestDataV8::Data(inner) => Self::Data((inner, version).into()), IpPacketRequestDataV8::Control(inner) => { - Self::Control((*inner, sender_tag, version).try_into()?) + Self::Control((*inner, sender_tag, version).into()) } - }) + } } } @@ -42,139 +35,72 @@ impl From<(DataRequestV8, ClientVersion)> for DataRequest { } } -impl TryFrom<(ControlRequestV8, Option, ClientVersion)> for ControlRequest { - type Error = IpPacketRouterError; - - fn try_from( - (request, sender_tag, version): ( - ControlRequestV8, - Option, - ClientVersion, - ), - ) -> Result { - Ok(match request { - ControlRequestV8::StaticConnect(inner) => { - ControlRequest::StaticConnect((inner.request, sender_tag, version).try_into()?) - } - ControlRequestV8::DynamicConnect(inner) => { - ControlRequest::DynamicConnect((inner.request, sender_tag, version).try_into()?) +impl From<(ControlRequestV8, AnonymousSenderTag, ClientVersion)> for ControlRequest { + fn from( + (request, sender_tag, version): (ControlRequestV8, AnonymousSenderTag, ClientVersion), + ) -> Self { + match request { + ControlRequestV8::Connect(inner) => { + ControlRequest::DynamicConnect((inner, sender_tag, version).into()) } ControlRequestV8::Disconnect(inner) => { - ControlRequest::Disconnect((inner.request, sender_tag, version).try_into()?) + ControlRequest::Disconnect((inner, sender_tag, version).into()) } ControlRequestV8::Ping(inner) => { - ControlRequest::Ping((inner, sender_tag, version).try_into()?) + ControlRequest::Ping((inner, sender_tag, version).into()) } ControlRequestV8::Health(inner) => { - ControlRequest::Health((inner, sender_tag, version).try_into()?) + ControlRequest::Health((inner, sender_tag, version).into()) } - }) + } } } -impl - TryFrom<( - StaticConnectRequestV8, - Option, - ClientVersion, - )> for StaticConnectRequest -{ - type Error = IpPacketRouterError; - - fn try_from( - (request, sender_tag, version): ( - StaticConnectRequestV8, - Option, - ClientVersion, - ), - ) -> Result { - Ok(Self { +impl From<(ConnectRequestV8, AnonymousSenderTag, ClientVersion)> for DynamicConnectRequest { + fn from( + (request, sender_tag, version): (ConnectRequestV8, AnonymousSenderTag, ClientVersion), + ) -> Self { + Self { version, request_id: request.request_id, - sent_by: ConnectedClientId::try_from((request.sender, sender_tag))?, - ips: request.ips, + sent_by: sender_tag.into(), buffer_timeout: request.buffer_timeout, - }) + } } } -impl - TryFrom<( - DynamicConnectRequestV8, - Option, - ClientVersion, - )> for DynamicConnectRequest -{ - type Error = IpPacketRouterError; - - fn try_from( - (request, sender_tag, version): ( - DynamicConnectRequestV8, - Option, - ClientVersion, - ), - ) -> Result { - Ok(Self { +impl From<(DisconnectRequestV8, AnonymousSenderTag, ClientVersion)> for DisconnectRequest { + fn from( + (request, sender_tag, version): (DisconnectRequestV8, AnonymousSenderTag, ClientVersion), + ) -> Self { + Self { version, request_id: request.request_id, - sent_by: ConnectedClientId::try_from((request.sender, sender_tag))?, - buffer_timeout: request.buffer_timeout, - }) + sent_by: sender_tag.into(), + } } } -impl - TryFrom<( - DisconnectRequestV8, - Option, - ClientVersion, - )> for DisconnectRequest -{ - type Error = IpPacketRouterError; - - fn try_from( - (request, sender_tag, version): ( - DisconnectRequestV8, - Option, - ClientVersion, - ), - ) -> Result { - Ok(Self { +impl From<(PingRequestV8, AnonymousSenderTag, ClientVersion)> for PingRequest { + fn from( + (request, sender_tag, version): (PingRequestV8, AnonymousSenderTag, ClientVersion), + ) -> Self { + Self { version, request_id: request.request_id, - sent_by: ConnectedClientId::try_from((request.sender, sender_tag))?, - }) + sent_by: sender_tag.into(), + } } } -impl TryFrom<(PingRequestV8, Option, ClientVersion)> for PingRequest { - type Error = IpPacketRouterError; - - fn try_from( - (request, sender_tag, version): (PingRequestV8, Option, ClientVersion), - ) -> Result { - Ok(Self { +impl From<(HealthRequestV8, AnonymousSenderTag, ClientVersion)> for HealthRequest { + fn from( + (request, sender_tag, version): (HealthRequestV8, AnonymousSenderTag, ClientVersion), + ) -> Self { + Self { version, request_id: request.request_id, - sent_by: ConnectedClientId::try_from((request.sender, sender_tag))?, - }) - } -} - -impl TryFrom<(HealthRequestV8, Option, ClientVersion)> for HealthRequest { - type Error = IpPacketRouterError; - - fn try_from( - (request, sender_tag, version): ( - HealthRequestV8, - Option, - ClientVersion, - ), - ) -> Result { - Ok(Self { - version, - request_id: request.request_id, - sent_by: ConnectedClientId::try_from((request.sender, sender_tag))?, - }) + sent_by: sender_tag.into(), + } } } diff --git a/service-providers/ip-packet-router/src/messages/response.rs b/service-providers/ip-packet-router/src/messages/response.rs index 3743323166..6e60d41433 100644 --- a/service-providers/ip-packet-router/src/messages/response.rs +++ b/service-providers/ip-packet-router/src/messages/response.rs @@ -131,7 +131,7 @@ impl VersionedResponse { match self.version { ClientVersion::V6 => IpPacketResponseV6::try_from(self)?.to_bytes(), ClientVersion::V7 => IpPacketResponseV7::try_from(self)?.to_bytes(), - ClientVersion::V8 => IpPacketResponseV8::from(self).to_bytes(), + ClientVersion::V8 => IpPacketResponseV8::try_from(self)?.to_bytes(), } .map_err(|err| IpPacketRouterError::FailedToSerializeResponsePacket { source: err }) } diff --git a/service-providers/ip-packet-router/src/messages/response/v8.rs b/service-providers/ip-packet-router/src/messages/response/v8.rs index 5ad984e355..3f73479a05 100644 --- a/service-providers/ip-packet-router/src/messages/response/v8.rs +++ b/service-providers/ip-packet-router/src/messages/response/v8.rs @@ -2,46 +2,44 @@ // SPDX-License-Identifier: GPL-3.0-only use nym_ip_packet_requests::v8::response::{ + ConnectFailureReason as ConnectFailureReasonV8, ConnectResponse as ConnectResponseV8, + ConnectResponseReply as ConnectResponseReplyV8, ConnectSuccess as ConnectSuccessV8, ControlResponse as ControlResponseV8, DisconnectFailureReason as DisconnectFailureReasonV8, DisconnectResponse as DisconnectResponseV8, - DisconnectResponseReply as DisconnectResponseReplyV8, - DynamicConnectFailureReason as DynamicConnectFailureReasonV8, - DynamicConnectResponse as DynamicConnectResponseV8, - DynamicConnectResponseReply as DynamicConnectResponseReplyV8, - DynamicConnectSuccess as DynamicConnectSuccessV8, HealthResponse as HealthResponseV8, + DisconnectResponseReply as DisconnectResponseReplyV8, HealthResponse as HealthResponseV8, HealthResponseReply as HealthResponseReplyV8, InfoLevel as InfoLevelV8, InfoResponse as InfoResponseV8, InfoResponseReply as InfoResponseReplyV8, IpPacketResponse as IpPacketResponseV8, IpPacketResponseData as IpPacketResponseDataV8, - PongResponse as PongResponseV8, StaticConnectFailureReason as StaticConnectFailureReasonV8, - StaticConnectResponse as StaticConnectResponseV8, - StaticConnectResponseReply as StaticConnectResponseReplyV8, + PongResponse as PongResponseV8, }; +use crate::error::IpPacketRouterError; + use super::{ DisconnectFailureReason, DisconnectResponse, DynamicConnectFailureReason, DynamicConnectResponse, DynamicConnectSuccess, HealthResponse, InfoLevel, InfoResponseReply, - Response, StaticConnectFailureReason, StaticConnectResponse, VersionedResponse, + Response, VersionedResponse, }; -impl From for IpPacketResponseV8 { - fn from(response: VersionedResponse) -> Self { +impl TryFrom for IpPacketResponseV8 { + type Error = IpPacketRouterError; + + fn try_from(response: VersionedResponse) -> Result { let version = response.version.into_u8(); let data = match response.response { - Response::StaticConnect { request_id, reply } => IpPacketResponseDataV8::Control( - Box::new(ControlResponseV8::StaticConnect(StaticConnectResponseV8 { + Response::StaticConnect { .. } => { + return Err(IpPacketRouterError::UnsupportedResponse(format!( + "Static connect response is not supported in version {}", + version + ))) + } + Response::DynamicConnect { request_id, reply } => IpPacketResponseDataV8::Control( + Box::new(ControlResponseV8::Connect(ConnectResponseV8 { request_id, reply: reply.into(), })), ), - Response::DynamicConnect { request_id, reply } => { - IpPacketResponseDataV8::Control(Box::new(ControlResponseV8::DynamicConnect( - DynamicConnectResponseV8 { - request_id, - reply: reply.into(), - }, - ))) - } Response::Disconnect { request_id, reply } => IpPacketResponseDataV8::Control( Box::new(ControlResponseV8::Disconnect(DisconnectResponseV8 { request_id, @@ -66,61 +64,29 @@ impl From for IpPacketResponseV8 { )), }; - IpPacketResponseV8 { version, data } + Ok(IpPacketResponseV8 { version, data }) } } -impl From for StaticConnectResponseReplyV8 { - fn from(reply: StaticConnectResponse) -> Self { - match reply { - StaticConnectResponse::Success => StaticConnectResponseReplyV8::Success, - StaticConnectResponse::Failure(err) => { - StaticConnectResponseReplyV8::Failure(err.into()) - } - } - } -} - -impl From for StaticConnectFailureReasonV8 { - fn from(reason: StaticConnectFailureReason) -> Self { - match reason { - StaticConnectFailureReason::RequestedIpAlreadyInUse => { - StaticConnectFailureReasonV8::RequestedIpAlreadyInUse - } - StaticConnectFailureReason::ClientAlreadyConnected => { - StaticConnectFailureReasonV8::ClientAlreadyConnected - } - StaticConnectFailureReason::OutOfDateTimestamp => { - StaticConnectFailureReasonV8::OutOfDateTimestamp - } - StaticConnectFailureReason::Other(err) => StaticConnectFailureReasonV8::Other(err), - } - } -} - -impl From for DynamicConnectResponseReplyV8 { +impl From for ConnectResponseReplyV8 { fn from(reply: DynamicConnectResponse) -> Self { match reply { DynamicConnectResponse::Success(DynamicConnectSuccess { ips }) => { - DynamicConnectResponseReplyV8::Success(DynamicConnectSuccessV8 { ips }) - } - DynamicConnectResponse::Failure(err) => { - DynamicConnectResponseReplyV8::Failure(err.into()) + ConnectResponseReplyV8::Success(ConnectSuccessV8 { ips }) } + DynamicConnectResponse::Failure(err) => ConnectResponseReplyV8::Failure(err.into()), } } } -impl From for DynamicConnectFailureReasonV8 { +impl From for ConnectFailureReasonV8 { fn from(reason: DynamicConnectFailureReason) -> Self { match reason { DynamicConnectFailureReason::ClientAlreadyConnected => { - DynamicConnectFailureReasonV8::ClientAlreadyConnected + ConnectFailureReasonV8::ClientAlreadyConnected } - DynamicConnectFailureReason::NoAvailableIp => { - DynamicConnectFailureReasonV8::NoAvailableIp - } - DynamicConnectFailureReason::Other(err) => DynamicConnectFailureReasonV8::Other(err), + DynamicConnectFailureReason::NoAvailableIp => ConnectFailureReasonV8::NoAvailableIp, + DynamicConnectFailureReason::Other(err) => ConnectFailureReasonV8::Other(err), } } } From b709d3ba0b836e7970c70f8e0d062c487ff5d287 Mon Sep 17 00:00:00 2001 From: Fran Arbanas Date: Fri, 28 Feb 2025 14:01:33 +0100 Subject: [PATCH 017/133] Fix/pull from harbor (#5521) * fix: pull from harbor instead of dockerhub * add remaining * add comments saying that these changes will only work with VPN --- nym-api.dockerfile | 3 ++- nym-credential-proxy/nym-credential-proxy/Dockerfile | 5 +++-- nym-network-monitor.dockerfile | 3 ++- nym-node-status-api/nym-node-status-agent/Dockerfile | 7 ++++--- nym-node-status-api/nym-node-status-api/Dockerfile | 5 +++-- nym-node-status-api/nym-node-status-api/Dockerfile.dev | 2 +- nym-node/Dockerfile | 5 +++-- nym-validator-rewarder/Dockerfile | 5 +++-- nyx-chain-watcher/Dockerfile | 5 +++-- 9 files changed, 24 insertions(+), 16 deletions(-) diff --git a/nym-api.dockerfile b/nym-api.dockerfile index 7ee95379dc..5701fbb395 100644 --- a/nym-api.dockerfile +++ b/nym-api.dockerfile @@ -1,4 +1,5 @@ -FROM rust:latest AS builder +# this will only work with VPN, otherwise remove the harbor part +FROM harbor.nymte.ch/dockerhub/rust:latest AS builder COPY ./ /usr/src/nym WORKDIR /usr/src/nym/nym-api diff --git a/nym-credential-proxy/nym-credential-proxy/Dockerfile b/nym-credential-proxy/nym-credential-proxy/Dockerfile index b6da10f4f5..e0fbc7fc05 100644 --- a/nym-credential-proxy/nym-credential-proxy/Dockerfile +++ b/nym-credential-proxy/nym-credential-proxy/Dockerfile @@ -1,4 +1,5 @@ -FROM rust:latest AS builder +# this will only work with VPN, otherwise remove the harbor part +FROM harbor.nymte.ch/dockerhub/rust:latest AS builder COPY ./ /usr/src/nym WORKDIR /usr/src/nym/nym-credential-proxy/nym-credential-proxy @@ -24,7 +25,7 @@ RUN cargo build --release # see https://github.com/nymtech/nym/blob/develop/nym-credential-proxy/nym-credential-proxy/src/cli.rs for details #------------------------------------------------------------------- -FROM ubuntu:24.04 +FROM harbor.nymte.ch/dockerhub/ubuntu:24.04 RUN apt update && apt install -yy curl ca-certificates diff --git a/nym-network-monitor.dockerfile b/nym-network-monitor.dockerfile index f25b93156e..22a2ca0c1d 100644 --- a/nym-network-monitor.dockerfile +++ b/nym-network-monitor.dockerfile @@ -1,4 +1,5 @@ -FROM rust:latest AS builder +# this will only work with VPN, otherwise remove the harbor part +FROM harbor.nymte.ch/dockerhub/rust:latest AS builder COPY ./ /usr/src/nym WORKDIR /usr/src/nym/nym-network-monitor diff --git a/nym-node-status-api/nym-node-status-agent/Dockerfile b/nym-node-status-api/nym-node-status-agent/Dockerfile index b50ce8be3d..fcab377e26 100644 --- a/nym-node-status-api/nym-node-status-agent/Dockerfile +++ b/nym-node-status-api/nym-node-status-agent/Dockerfile @@ -1,4 +1,5 @@ -FROM rust:latest AS builder +# this will only work with VPN, otherwise remove the harbor part +FROM harbor.nymte.ch/dockerhub/rust:latest AS builder ARG GIT_REF=main @@ -27,7 +28,7 @@ RUN cargo build --release # see https://github.com/nymtech/nym/blob/develop/nym-node-status-agent/src/cli.rs for details #------------------------------------------------------------------- -FROM ubuntu:24.04 +FROM harbor.nymte.ch/dockerhub/ubuntu:24.04 RUN apt-get update && apt-get install -y ca-certificates @@ -37,4 +38,4 @@ COPY --from=builder /usr/src/nym/target/release/nym-node-status-agent ./ COPY --from=builder /usr/src/nym-vpn-client/nym-vpn-core/target/release/nym-gateway-probe ./ ENV NODE_STATUS_AGENT_PROBE_PATH=/nym/nym-gateway-probe -ENTRYPOINT [ "/nym/nym-node-status-agent", "run-probe" ] \ No newline at end of file +ENTRYPOINT [ "/nym/nym-node-status-agent", "run-probe" ] diff --git a/nym-node-status-api/nym-node-status-api/Dockerfile b/nym-node-status-api/nym-node-status-api/Dockerfile index 035f186c65..6eeaf8c6ee 100644 --- a/nym-node-status-api/nym-node-status-api/Dockerfile +++ b/nym-node-status-api/nym-node-status-api/Dockerfile @@ -1,4 +1,5 @@ -FROM rust:latest AS builder +# this will only work with VPN, otherwise remove the harbor part +FROM harbor.nymte.ch/dockerhub/rust:latest AS builder COPY ./ /usr/src/nym WORKDIR /usr/src/nym/nym-node-status-api @@ -26,7 +27,7 @@ RUN cargo build --release # see https://github.com/nymtech/nym/blob/develop/nym-node-status-api/src/cli.rs for details #------------------------------------------------------------------- -FROM ubuntu:24.04 +FROM harbor.nymte.ch/dockerhub/ubuntu:24.04 RUN apt-get update && apt-get install -y ca-certificates diff --git a/nym-node-status-api/nym-node-status-api/Dockerfile.dev b/nym-node-status-api/nym-node-status-api/Dockerfile.dev index 2967a6e605..3811074734 100644 --- a/nym-node-status-api/nym-node-status-api/Dockerfile.dev +++ b/nym-node-status-api/nym-node-status-api/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM ubuntu:22.04 +FROM harbor.nymte.ch/dockerhub/ubuntu:22.04 RUN apt-get update && apt-get install -y ca-certificates diff --git a/nym-node/Dockerfile b/nym-node/Dockerfile index 53faa4ce3f..d7ef3012f2 100644 --- a/nym-node/Dockerfile +++ b/nym-node/Dockerfile @@ -1,4 +1,5 @@ -FROM rust:latest AS builder +# this will only work with VPN, otherwise remove the harbor part +FROM harbor.nymte.ch/dockerhub/rust:latest AS builder COPY ./ /usr/src/nym WORKDIR /usr/src/nym/nym-node @@ -65,7 +66,7 @@ RUN cargo build --release # see https://github.com/nymtech/nym/blob/develop/nym-node/src/env.rs for details #------------------------------------------------------------------- -FROM ubuntu:24.04 +FROM harbor.nymte.ch/dockerhub/ubuntu:24.04 WORKDIR /nym diff --git a/nym-validator-rewarder/Dockerfile b/nym-validator-rewarder/Dockerfile index e4543a16a7..c690adb9c6 100644 --- a/nym-validator-rewarder/Dockerfile +++ b/nym-validator-rewarder/Dockerfile @@ -1,4 +1,5 @@ -FROM rust:latest AS builder +# this will only work with VPN, otherwise remove the harbor part +FROM harbor.nymte.ch/dockerhub/rust:latest AS builder COPY ./ /usr/src/nym WORKDIR /usr/src/nym/nym-validator-rewarder @@ -16,7 +17,7 @@ RUN cargo build --release # see https://github.com/nymtech/nym/blob/develop/nym-validator-rewarder/src/cli/mod.rs for details #------------------------------------------------------------------- -FROM ubuntu:24.04 +FROM harbor.nymte.ch/dockerhub/ubuntu:24.04 RUN apt-get update && apt-get install -y ca-certificates diff --git a/nyx-chain-watcher/Dockerfile b/nyx-chain-watcher/Dockerfile index e4419d4664..57076b18b7 100644 --- a/nyx-chain-watcher/Dockerfile +++ b/nyx-chain-watcher/Dockerfile @@ -1,4 +1,5 @@ -FROM rust:latest AS builder +# this will only work with VPN, otherwise remove the harbor part +FROM harbor.nymte.ch/dockerhub/rust:latest AS builder COPY ./ /usr/src/nym WORKDIR /usr/src/nym/nyx-chain-watcher @@ -21,7 +22,7 @@ RUN cargo build --release # and https://github.com/nymtech/nym/blob/develop/nyx-chain-watcher/src/env.rs for env vars #------------------------------------------------------------------- -FROM ubuntu:24.04 +FROM harbor.nymte.ch/dockerhub/ubuntu:24.04 RUN apt update && apt install -yy curl ca-certificates From f52f07f6eca6f990dde0e5f8335552a6e6035496 Mon Sep 17 00:00:00 2001 From: mfahampshire Date: Mon, 3 Mar 2025 07:39:17 +0000 Subject: [PATCH 018/133] Max/tcp proxy bin sdk readme (#5354) * removed old todos * add bin files to proxy * add readme to sdk * fmt --- Cargo.lock | 2 + sdk/rust/nym-sdk/Cargo.toml | 13 +++++ sdk/rust/nym-sdk/README.md | 8 +++ .../nym-sdk/src/tcp_proxy/bin/proxy_client.rs | 54 +++++++++++++++++++ .../nym-sdk/src/tcp_proxy/bin/proxy_server.rs | 37 +++++++++++++ 5 files changed, 114 insertions(+) create mode 100644 sdk/rust/nym-sdk/README.md create mode 100644 sdk/rust/nym-sdk/src/tcp_proxy/bin/proxy_client.rs create mode 100644 sdk/rust/nym-sdk/src/tcp_proxy/bin/proxy_server.rs diff --git a/Cargo.lock b/Cargo.lock index 654fb80097..1d72eae859 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6539,6 +6539,7 @@ dependencies = [ "bip39", "bytecodec", "bytes", + "clap", "dashmap", "dirs", "dotenvy", @@ -6562,6 +6563,7 @@ dependencies = [ "nym-socks5-client-core", "nym-socks5-requests", "nym-sphinx", + "nym-sphinx-addressing", "nym-statistics-common", "nym-task", "nym-topology", diff --git a/sdk/rust/nym-sdk/Cargo.toml b/sdk/rust/nym-sdk/Cargo.toml index 7a50e11982..f30e244e87 100644 --- a/sdk/rust/nym-sdk/Cargo.toml +++ b/sdk/rust/nym-sdk/Cargo.toml @@ -6,6 +6,14 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[bin]] +name = "nym-proxy-server" +path = "src/tcp_proxy/bin/proxy_server.rs" + +[[bin]] +name = "nym-proxy-client" +path = "src/tcp_proxy/bin/proxy_client.rs" + [dependencies] async-trait = { workspace = true } bip39 = { workspace = true } @@ -33,6 +41,10 @@ nym-validator-client = { path = "../../../common/client-libs/validator-client", nym-socks5-requests = { path = "../../../common/socks5/requests" } nym-ordered-buffer = { path = "../../../common/socks5/ordered-buffer" } nym-service-providers-common = { path = "../../../service-providers/common" } +nym-sphinx-addressing = { path = "../../../common/nymsphinx/addressing" } +nym-bin-common = { path = "../../../common/bin-common", features = [ + "basic_tracing", +] } bytecodec = { workspace = true } httpcodec = { workspace = true } bytes = { workspace = true } @@ -48,6 +60,7 @@ url = { workspace = true } toml = { workspace = true } # tcpproxy dependencies +clap = { workspace = true, features = ["derive"] } anyhow.workspace = true dashmap.workspace = true tokio.workspace = true diff --git a/sdk/rust/nym-sdk/README.md b/sdk/rust/nym-sdk/README.md new file mode 100644 index 0000000000..16f1989414 --- /dev/null +++ b/sdk/rust/nym-sdk/README.md @@ -0,0 +1,8 @@ +# Nym Rust SDK + +This repo contains several components: +- `mixnet`: exposes Nym Client builders and methods. This is useful if you want to interact directly with the Client, or build transport abstractions. +- `tcp_proxy`: exposes functionality to set up client/server instances that expose a localhost TcpSocket to read/write to like a 'normal' socket connection. `tcp_proxy/bin/` contains standalone `nym-proxy-client` and `nym-proxy-server` binaries. +- `clientpool`: a configurable pool of ephemeral Nym Clients which can be created as a background process and quickly grabbed. + +Documentation can be found [here](https://nym.com/docs/developers/rust). diff --git a/sdk/rust/nym-sdk/src/tcp_proxy/bin/proxy_client.rs b/sdk/rust/nym-sdk/src/tcp_proxy/bin/proxy_client.rs new file mode 100644 index 0000000000..3b6d24da1d --- /dev/null +++ b/sdk/rust/nym-sdk/src/tcp_proxy/bin/proxy_client.rs @@ -0,0 +1,54 @@ +use anyhow::Result; +use clap::Parser; +use nym_sdk::tcp_proxy; +use nym_sphinx_addressing::Recipient; + +#[derive(Parser, Debug)] +struct Args { + /// Send timeout in seconds + #[clap(long, default_value_t = 30)] + close_timeout: u64, + + /// Nym address of the NymProxyServer e.g. EjYsntVxxBJrcRugiX5VnbKMbg7gyBGSp9SLt7RgeVFV.EzRtVdHCHoP2ho3DJgKMisMQ3zHkqMtAFAW4pxsq7Y2a@Hs463Wh5LtWZU@NyAmt4trcCbNVsuUhry1wpEXpVnAAfn + #[clap(short, long)] + server_address: String, + + /// Listen address + #[clap(long, default_value = "127.0.0.1")] + listen_address: String, + + /// Listen port + #[clap(long, default_value = "8080")] + listen_port: String, + + /// Optional env filepath - if none is supplied then the proxy defaults to using mainnet else just use a path to one of the supplied files in envs/ e.g. ./envs/sandbox.env + #[clap(short, long)] + env_path: Option, + + /// How many clients to have running in reserve for quick access by incoming connections + #[clap(long, default_value_t = 2)] + client_pool_reserve: usize, +} + +#[tokio::main] +async fn main() -> Result<()> { + nym_bin_common::logging::setup_tracing_logger(); + let args = Args::parse(); + + let nym_addr: Recipient = + Recipient::try_from_base58_string(&args.server_address).expect("Invalid server address"); + + let proxy_client = tcp_proxy::NymProxyClient::new( + nym_addr, + &args.listen_address, + &args.listen_port, + args.close_timeout, + args.env_path.clone(), + args.client_pool_reserve, + ) + .await?; + + proxy_client.run().await.unwrap(); + + Ok(()) +} diff --git a/sdk/rust/nym-sdk/src/tcp_proxy/bin/proxy_server.rs b/sdk/rust/nym-sdk/src/tcp_proxy/bin/proxy_server.rs new file mode 100644 index 0000000000..44686b438a --- /dev/null +++ b/sdk/rust/nym-sdk/src/tcp_proxy/bin/proxy_server.rs @@ -0,0 +1,37 @@ +use anyhow::Result; +use clap::Parser; +use nym_sdk::tcp_proxy; + +#[derive(Parser, Debug)] +struct Args { + /// Upstream address of the server process we want to proxy traffic to e.g. 127.0.0.1:9067 + #[clap(short, long)] + upstream_tcp_address: String, + + /// Config directory + #[clap(short, long, default_value = "/tmp/nym-tcp-proxy-server")] + config_dir: String, + + /// Optional env filepath - if none is supplied then the proxy defaults to using mainnet else just use a path to one of the supplied files in envs/ e.g. ./envs/sandbox.env + #[clap(short, long)] + env_path: Option, +} + +#[tokio::main] +async fn main() -> Result<()> { + nym_bin_common::logging::setup_logging(); + + let args = Args::parse(); + + let home_dir = dirs::home_dir().expect("Unable to get home directory"); + let conf_path = format!("{}{}", home_dir.display(), args.config_dir); + + let mut proxy_server = tcp_proxy::NymProxyServer::new( + &args.upstream_tcp_address, + &conf_path, + args.env_path.clone(), + ) + .await?; + + proxy_server.run_with_shutdown().await +} From 4f7124e6619e5f3c8d66fca7b15c8b389cbdc715 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 3 Mar 2025 10:47:40 +0000 Subject: [PATCH 019/133] Feature/chain status api (#5539) * nym-api endpoint to return latest block information * attached chain health to health query * fixed serde casing * one of the most nastiest work arounds in test code --- Cargo.lock | 2 + .../validator-client/src/nyxd/mod.rs | 2 +- nym-api/Cargo.toml | 1 + nym-api/nym-api-requests/Cargo.toml | 1 + nym-api/nym-api-requests/src/models.rs | 16 + nym-api/src/ecash/tests/mod.rs | 34 +- nym-api/src/network/handlers.rs | 29 +- nym-api/src/network/models.rs | 291 ++++++++++++++++++ nym-api/src/node_status_api/models.rs | 10 + nym-api/src/status/handlers.rs | 37 ++- nym-api/src/support/cli/run.rs | 6 +- nym-api/src/support/config/mod.rs | 3 + nym-api/src/support/http/state.rs | 90 +++++- nym-api/src/support/nyxd/mod.rs | 11 +- nym-wallet/Cargo.lock | 5 +- 15 files changed, 517 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e45cc5e1f7..2aaf965a5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4814,6 +4814,7 @@ dependencies = [ "sha2 0.9.9", "sqlx", "tempfile", + "tendermint 0.40.1", "thiserror 2.0.11", "time", "tokio", @@ -4838,6 +4839,7 @@ dependencies = [ "cosmwasm-std", "ecdsa", "getset", + "humantime-serde", "nym-compact-ecash", "nym-config", "nym-contracts-common", diff --git a/common/client-libs/validator-client/src/nyxd/mod.rs b/common/client-libs/validator-client/src/nyxd/mod.rs index 14a91a4459..458ba45c27 100644 --- a/common/client-libs/validator-client/src/nyxd/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/mod.rs @@ -28,7 +28,6 @@ use nym_network_defaults::{ChainDetails, NymNetworkDetails}; use serde::{de::DeserializeOwned, Serialize}; use std::fmt::Debug; use std::time::SystemTime; -use tendermint_rpc::endpoint::block::Response as BlockResponse; use tendermint_rpc::endpoint::*; use tendermint_rpc::{Error as TendermintRpcError, Order}; use url::Url; @@ -63,6 +62,7 @@ pub use cw3; pub use cw4; pub use cw_controllers; pub use fee::{gas_price::GasPrice, GasAdjustable, GasAdjustment}; +pub use tendermint_rpc::endpoint::block::Response as BlockResponse; pub use tendermint_rpc::{ endpoint::{tx::Response as TxResponse, validators::Response as ValidatorResponse}, query::Query, diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 4db77f3ccd..f95baabd00 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -46,6 +46,7 @@ tokio-stream = { workspace = true } tokio-util = { workspace = true } url = { workspace = true } +tendermint = { workspace = true } ts-rs = { workspace = true, optional = true } anyhow = { workspace = true } diff --git a/nym-api/nym-api-requests/Cargo.toml b/nym-api/nym-api-requests/Cargo.toml index 3d66ad2bb9..9ca0fe6ea4 100644 --- a/nym-api/nym-api-requests/Cargo.toml +++ b/nym-api/nym-api-requests/Cargo.toml @@ -13,6 +13,7 @@ cosmwasm-std = { workspace = true } getset = { workspace = true } schemars = { workspace = true, features = ["preserve_order"] } serde = { workspace = true, features = ["derive"] } +humantime-serde = { workspace = true } serde_json = { workspace = true } sha2.workspace = true tendermint = { workspace = true } diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index ba5d6a94b2..2558613c8c 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -1215,6 +1215,7 @@ impl From for WireguardDetails { #[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct ApiHealthResponse { pub status: ApiStatus, + pub chain_status: ChainStatus, pub uptime: u64, } @@ -1224,10 +1225,25 @@ pub enum ApiStatus { Up, } +#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum ChainStatus { + Synced, + Unknown, + Stalled { + #[serde( + serialize_with = "humantime_serde::serialize", + deserialize_with = "humantime_serde::deserialize" + )] + approximate_amount: Duration, + }, +} + impl ApiHealthResponse { pub fn new_healthy(uptime: Duration) -> Self { ApiHealthResponse { status: ApiStatus::Up, + chain_status: ChainStatus::Synced, uptime: uptime.as_secs(), } } diff --git a/nym-api/src/ecash/tests/mod.rs b/nym-api/src/ecash/tests/mod.rs index c31ecc2ede..e1f0c38083 100644 --- a/nym-api/src/ecash/tests/mod.rs +++ b/nym-api/src/ecash/tests/mod.rs @@ -14,7 +14,8 @@ use crate::nym_contract_cache::cache::NymContractCache; use crate::status::ApiStatusState; use crate::support::caching::cache::SharedCache; use crate::support::config; -use crate::support::http::state::{AppState, ForcedRefresh}; +use crate::support::http::state::{AppState, ChainStatusCache, ForcedRefresh}; +use crate::support::nyxd::Client; use crate::support::storage::NymApiStorage; use async_trait::async_trait; use axum::Router; @@ -46,7 +47,7 @@ use nym_coconut_dkg_common::types::{ use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; use nym_compact_ecash::BlindedSignature; use nym_compact_ecash::{ttp_keygen, VerificationKeyAuth}; -use nym_config::defaults::NymNetworkDetails; +use nym_config::defaults::{NymNetworkDetails, ValidatorDetails}; use nym_contracts_common::IdentityKey; use nym_credentials::IssuanceTicketBook; use nym_credentials_interface::TicketType; @@ -70,6 +71,7 @@ use std::ops::Deref; use std::str::FromStr; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; +use std::time::Duration; use tempfile::{tempdir, TempDir}; use time::Date; use tokio::sync::RwLock; @@ -1264,8 +1266,14 @@ struct TestFixture { } impl TestFixture { - fn build_app_state(storage: NymApiStorage, ecash_state: EcashState) -> AppState { + fn build_app_state( + storage: NymApiStorage, + ecash_state: EcashState, + nyxd_client: Client, + ) -> AppState { AppState { + nyxd_client, + chain_status_cache: ChainStatusCache::new(Duration::from_secs(42)), forced_refresh: ForcedRefresh::new(true), nym_contract_cache: NymContractCache::new(), node_status_cache: NodeStatusCache::new(), @@ -1337,12 +1345,22 @@ impl TestFixture { TaskClient::dummy(), ); + // ideally this would have been generic, but that's way too much work + // since then `AppState` would have had to be made generic + // also, this is such a disgusting workaround to make it 'work'. yuck + let mut dummy = NymNetworkDetails::new_empty(); + dummy.endpoints = vec![ValidatorDetails::new( + "http://127.0.0.1:26657", + Some("http://why-do-we-even-need-api-url-set-here.wtf"), + None, + )]; + dummy.export_to_env(); + let another_fake_nyxd_client = Client::new(&config).unwrap(); + TestFixture { - axum: TestServer::new( - Router::new() - .nest("/v1/ecash", ecash_routes()) - .with_state(Self::build_app_state(storage.clone(), ecash_state)), - ) + axum: TestServer::new(Router::new().nest("/v1/ecash", ecash_routes()).with_state( + Self::build_app_state(storage.clone(), ecash_state, another_fake_nyxd_client), + )) .unwrap(), storage, chain_state, diff --git a/nym-api/src/network/handlers.rs b/nym-api/src/network/handlers.rs index 708a1cf86f..a308a9fa82 100644 --- a/nym-api/src/network/handlers.rs +++ b/nym-api/src/network/handlers.rs @@ -1,9 +1,11 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::network::models::{ContractInformation, NetworkDetails}; +use crate::network::models::{ChainStatusResponse, ContractInformation, NetworkDetails}; +use crate::node_status_api::models::AxumResult; use crate::support::http::state::AppState; -use axum::{extract, Router}; +use axum::extract::State; +use axum::{extract, Json, Router}; use nym_contracts_common::ContractBuildInformation; use std::collections::HashMap; use tower_http::compression::CompressionLayer; @@ -12,6 +14,7 @@ use utoipa::ToSchema; pub(crate) fn nym_network_routes() -> Router { Router::new() .route("/details", axum::routing::get(network_details)) + .route("/chain-status", axum::routing::get(chain_status)) .route("/nym-contracts", axum::routing::get(nym_contracts)) .route( "/nym-contracts-detailed", @@ -34,6 +37,28 @@ async fn network_details( state.network_details().to_owned().into() } +#[utoipa::path( + tag = "network", + get, + path = "/v1/network/chain-status", + responses( + (status = 200, body = ChainStatusResponse) + ) +)] +async fn chain_status(State(state): State) -> AxumResult> { + let chain_status = state + .chain_status_cache + .get_or_refresh(&state.nyxd_client) + .await?; + + let connected_nyxd = state.network_details.connected_nyxd; + + Ok(Json(ChainStatusResponse { + connected_nyxd, + status: chain_status, + })) +} + // it's used for schema generation so dead_code is fine #[allow(dead_code)] #[derive(ToSchema)] diff --git a/nym-api/src/network/models.rs b/nym-api/src/network/models.rs index 1819ce024d..066b473921 100644 --- a/nym-api/src/network/models.rs +++ b/nym-api/src/network/models.rs @@ -1,7 +1,9 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::network::models::tendermint_types::{AbciInfo, BlockHeader, BlockId}; use nym_config::defaults::NymNetworkDetails; +use nym_validator_client::nyxd::BlockResponse; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; @@ -27,3 +29,292 @@ pub struct ContractInformation { pub(crate) address: Option, pub(crate) details: Option, } + +#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct ChainStatusResponse { + pub connected_nyxd: String, + pub status: ChainStatus, +} + +#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct ChainStatus { + pub abci: AbciInfo, + pub latest_block: BlockInfo, +} + +#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct BlockInfo { + pub block_id: BlockId, + pub block: FullBlockInfo, + // if necessary we might put block data here later too +} + +impl From for BlockInfo { + fn from(value: BlockResponse) -> Self { + BlockInfo { + block_id: value.block_id.into(), + block: FullBlockInfo { + header: value.block.header.into(), + }, + } + } +} + +#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct FullBlockInfo { + pub header: BlockHeader, +} + +// copy tendermint types definitions whilst deriving schema types on them and dropping unwanted fields +pub mod tendermint_types { + use nym_validator_client::nyxd::Hash; + use schemars::JsonSchema; + use serde::{Deserialize, Serialize}; + use tendermint::abci::response::Info; + use tendermint::block; + use tendermint::block::header::Version; + use utoipa::ToSchema; + + #[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] + pub struct AbciInfo { + /// Some arbitrary information. + pub data: String, + + /// The application software semantic version. + pub version: String, + + /// The application protocol version. + pub app_version: u64, + + /// The latest block for which the app has called [`Commit`]. + pub last_block_height: u64, + + /// The latest result of [`Commit`]. + pub last_block_app_hash: String, + } + + impl From for AbciInfo { + fn from(value: Info) -> Self { + AbciInfo { + data: value.data, + version: value.version, + app_version: value.app_version, + last_block_height: value.last_block_height.value(), + last_block_app_hash: value.last_block_app_hash.to_string(), + } + } + } + + /// `Version` contains the protocol version for the blockchain and the + /// application. + /// + /// + #[derive( + Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, ToSchema, + )] + pub struct HeaderVersion { + /// Block version + pub block: u64, + + /// App version + pub app: u64, + } + + impl From for HeaderVersion { + fn from(value: Version) -> Self { + HeaderVersion { + block: value.block, + app: value.app, + } + } + } + + /// Block identifiers which contain two distinct Merkle roots of the block, + /// as well as the number of parts in the block. + /// + /// + /// + /// Default implementation is an empty Id as defined by the Go implementation in + /// . + /// + /// If the Hash is empty in BlockId, the BlockId should be empty (encoded to None). + /// This is implemented outside of this struct. Use the Default trait to check for an empty BlockId. + /// See: + #[derive( + Serialize, + Deserialize, + Copy, + Clone, + Debug, + Default, + Hash, + Eq, + PartialEq, + PartialOrd, + Ord, + JsonSchema, + ToSchema, + )] + pub struct BlockId { + /// The block's main hash is the Merkle root of all the fields in the + /// block header. + #[schemars(with = "String")] + #[schema(value_type = String)] + pub hash: Hash, + + /// Parts header (if available) is used for secure gossipping of the block + /// during consensus. It is the Merkle root of the complete serialized block + /// cut into parts. + /// + /// PartSet is used to split a byteslice of data into parts (pieces) for + /// transmission. By splitting data into smaller parts and computing a + /// Merkle root hash on the list, you can verify that a part is + /// legitimately part of the complete data, and the part can be forwarded + /// to other peers before all the parts are known. In short, it's a fast + /// way to propagate a large file over a gossip network. + /// + /// + /// + /// PartSetHeader in protobuf is defined as never nil using the gogoproto + /// annotations. This does not translate to Rust, but we can indicate this + /// in the domain type. + pub part_set_header: PartSetHeader, + } + + impl From for BlockId { + fn from(value: block::Id) -> Self { + BlockId { + hash: value.hash, + part_set_header: value.part_set_header.into(), + } + } + } + + /// Block parts header + #[derive( + Clone, + Copy, + Debug, + Default, + Hash, + Eq, + PartialEq, + PartialOrd, + Ord, + Deserialize, + Serialize, + JsonSchema, + ToSchema, + )] + #[non_exhaustive] + pub struct PartSetHeader { + /// Number of parts in this block + pub total: u32, + + /// Hash of the parts set header, + #[schemars(with = "String")] + #[schema(value_type = String)] + pub hash: Hash, + } + + impl From for PartSetHeader { + fn from(value: block::parts::Header) -> Self { + PartSetHeader { + total: value.total, + hash: value.hash, + } + } + } + + /// Block `Header` values contain metadata about the block and about the + /// consensus, as well as commitments to the data in the current block, the + /// previous block, and the results returned by the application. + /// + /// + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] + pub struct BlockHeader { + /// Header version + pub version: HeaderVersion, + + /// Chain ID + pub chain_id: String, + + /// Current block height + pub height: u64, + + /// Current timestamp + #[schemars(with = "String")] + #[schema(value_type = String)] + pub time: tendermint::Time, + + /// Previous block info + pub last_block_id: Option, + + /// Commit from validators from the last block + #[schemars(with = "Option")] + #[schema(value_type = Option)] + pub last_commit_hash: Option, + + /// Merkle root of transaction hashes + #[schemars(with = "Option")] + #[schema(value_type = Option)] + pub data_hash: Option, + + /// Validators for the current block + #[schemars(with = "String")] + #[schema(value_type = String)] + pub validators_hash: Hash, + + /// Validators for the next block + #[schemars(with = "String")] + #[schema(value_type = String)] + pub next_validators_hash: Hash, + + /// Consensus params for the current block + #[schemars(with = "String")] + #[schema(value_type = String)] + pub consensus_hash: Hash, + + /// State after txs from the previous block + #[schemars(with = "String")] + #[schema(value_type = String)] + pub app_hash: Hash, + + /// Root hash of all results from the txs from the previous block + #[schemars(with = "Option")] + #[schema(value_type = Option)] + pub last_results_hash: Option, + + /// Hash of evidence included in the block + #[schemars(with = "Option")] + #[schema(value_type = Option)] + pub evidence_hash: Option, + + /// Original proposer of the block + #[serde(with = "nym_serde_helpers::hex")] + #[schemars(with = "String")] + #[schema(value_type = String)] + pub proposer_address: Vec, + } + + impl From for BlockHeader { + fn from(value: block::Header) -> Self { + BlockHeader { + version: value.version.into(), + chain_id: value.chain_id.to_string(), + height: value.height.value(), + time: value.time, + last_block_id: value.last_block_id.map(Into::into), + last_commit_hash: value.last_commit_hash, + data_hash: value.data_hash, + validators_hash: value.validators_hash, + next_validators_hash: value.next_validators_hash, + consensus_hash: value.consensus_hash, + app_hash: Hash::try_from(value.app_hash.as_bytes().to_vec()).unwrap_or_default(), + last_results_hash: value.last_results_hash, + evidence_hash: value.evidence_hash, + proposer_address: value.proposer_address.as_bytes().to_vec(), + } + } + } +} diff --git a/nym-api/src/node_status_api/models.rs b/nym-api/src/node_status_api/models.rs index 53f5d3b7d5..b2af9a6c52 100644 --- a/nym-api/src/node_status_api/models.rs +++ b/nym-api/src/node_status_api/models.rs @@ -14,6 +14,7 @@ use nym_contracts_common::NaiveFloat; use nym_mixnet_contract_common::reward_params::Performance; use nym_mixnet_contract_common::{IdentityKey, NodeId}; use nym_serde_helpers::date::DATE_FORMAT; +use nym_validator_client::nyxd::error::NyxdError; use reqwest::StatusCode; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -452,6 +453,15 @@ impl From for AxumErrorResponse { } } +impl From for AxumErrorResponse { + fn from(value: NyxdError) -> Self { + Self { + message: RequestError::new(value.to_string()), + status: StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + #[derive(Debug, Error)] pub enum NymApiStorageError { #[error("could not find status report associated with mixnode {mix_id}")] diff --git a/nym-api/src/status/handlers.rs b/nym-api/src/status/handlers.rs index 8b177cadd6..1a280eb5df 100644 --- a/nym-api/src/status/handlers.rs +++ b/nym-api/src/status/handlers.rs @@ -7,9 +7,13 @@ use crate::support::http::state::AppState; use axum::extract::State; use axum::Json; use axum::Router; -use nym_api_requests::models::{ApiHealthResponse, SignerInformationResponse}; +use nym_api_requests::models::{ + ApiHealthResponse, ApiStatus, ChainStatus, SignerInformationResponse, +}; use nym_bin_common::build_information::BinaryBuildInformationOwned; use nym_compact_ecash::Base58; +use std::time::Duration; +use time::OffsetDateTime; pub(crate) fn api_status_routes() -> Router { Router::new() @@ -29,9 +33,34 @@ pub(crate) fn api_status_routes() -> Router { (status = 200, body = ApiHealthResponse) ) )] -async fn health(State(state): State) -> Json { - let uptime = state.startup_time.elapsed(); - let health = ApiHealthResponse::new_healthy(uptime); +async fn health(State(state): State) -> Json { + const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(5 * 60); + + let uptime = state.api_status.startup_time.elapsed(); + let chain_status = match state + .chain_status_cache + .get_or_refresh(&state.nyxd_client) + .await + { + Ok(res) => { + let now = OffsetDateTime::now_utc(); + let block_time: OffsetDateTime = res.latest_block.block.header.time.into(); + let diff = now - block_time; + if diff > CHAIN_STALL_THRESHOLD { + ChainStatus::Stalled { + approximate_amount: diff.unsigned_abs(), + } + } else { + ChainStatus::Synced + } + } + Err(_) => ChainStatus::Unknown, + }; + let health = ApiHealthResponse { + status: ApiStatus::Up, + chain_status, + uptime: uptime.as_secs(), + }; Json(health) } diff --git a/nym-api/src/support/cli/run.rs b/nym-api/src/support/cli/run.rs index 836f048930..9ab4828e65 100644 --- a/nym-api/src/support/cli/run.rs +++ b/nym-api/src/support/cli/run.rs @@ -19,9 +19,9 @@ use crate::nym_contract_cache::cache::NymContractCache; use crate::status::{ApiStatusState, SignerState}; use crate::support::caching::cache::SharedCache; use crate::support::config::helpers::try_load_current_config; -use crate::support::config::Config; +use crate::support::config::{Config, DEFAULT_CHAIN_STATUS_CACHE_TTL}; use crate::support::http::state::{ - AppState, ForcedRefresh, ShutdownHandles, TASK_MANAGER_TIMEOUT_S, + AppState, ChainStatusCache, ForcedRefresh, ShutdownHandles, TASK_MANAGER_TIMEOUT_S, }; use crate::support::http::RouterBuilder; use crate::support::nyxd; @@ -191,6 +191,8 @@ async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result>; #[derive(Clone)] pub(crate) struct AppState { + // ideally this would have been made generic to make tests easier, + // however, it'd be a way bigger change (I tried) + pub(crate) nyxd_client: Client, + pub(crate) chain_status_cache: ChainStatusCache, + pub(crate) forced_refresh: ForcedRefresh, pub(crate) nym_contract_cache: NymContractCache, pub(crate) node_status_cache: NodeStatusCache, @@ -127,6 +134,87 @@ impl ForcedRefresh { } } +#[derive(Clone)] +pub(crate) struct ChainStatusCache { + cache_ttl: Duration, + inner: Arc>>, +} + +impl ChainStatusCache { + pub(crate) fn new(cache_ttl: Duration) -> Self { + ChainStatusCache { + cache_ttl, + inner: Arc::new(Default::default()), + } + } +} + +struct ChainStatusCacheInner { + last_refreshed_at: OffsetDateTime, + cache_value: ChainStatus, +} + +impl ChainStatusCacheInner { + fn is_valid(&self, ttl: Duration) -> bool { + if self.last_refreshed_at + ttl > OffsetDateTime::now_utc() { + return true; + } + false + } +} + +impl ChainStatusCache { + pub(crate) async fn get_or_refresh( + &self, + client: &Client, + ) -> Result { + if let Some(cached) = self.check_cache().await { + return Ok(cached); + } + + self.refresh(client).await + } + + async fn check_cache(&self) -> Option { + let guard = self.inner.read().await; + let inner = guard.as_ref()?; + if inner.is_valid(self.cache_ttl) { + return Some(inner.cache_value.clone()); + } + None + } + + async fn refresh(&self, client: &Client) -> Result { + // 1. attempt to get write lock permit + let mut guard = self.inner.write().await; + + // 2. check if another task hasn't already updated the cache whilst we were waiting for the permit + if let Some(cached) = guard.as_ref() { + if cached.is_valid(self.cache_ttl) { + return Ok(cached.cache_value.clone()); + } + } + + // 3. attempt to query the chain for the chain data + let abci = client.abci_info().await?; + let block = client + .block_info(abci.last_block_height.value() as u32) + .await?; + + let status = ChainStatus { + abci: abci.into(), + latest_block: block.into(), + }; + + *guard = Some(ChainStatusCacheInner { + last_refreshed_at: OffsetDateTime::now_utc(), + cache_value: status.clone(), + }); + + Ok(status) + } +} + impl AppState { pub(crate) fn nym_contract_cache(&self) -> &NymContractCache { &self.nym_contract_cache diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index 01a32decec..889ffc0acd 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -44,7 +44,7 @@ use nym_validator_client::nyxd::{ PagedMixnetQueryClient, PagedMultisigQueryClient, PagedVestingQueryClient, }, cosmwasm_client::types::ExecuteResult, - CosmWasmClient, Fee, + BlockResponse, CosmWasmClient, Fee, TendermintRpcClient, }; use nym_validator_client::nyxd::{ hash::{Hash, SHA256_HASH_SIZE}, @@ -56,6 +56,7 @@ use nym_validator_client::{ use nym_vesting_contract_common::AccountVestingCoins; use serde::Deserialize; use std::sync::Arc; +use tendermint::abci::response::Info; use tokio::sync::{RwLock, RwLockReadGuard}; #[macro_export] @@ -132,6 +133,14 @@ impl Client { self.inner.read().await } + pub(crate) async fn abci_info(&self) -> Result { + Ok(nyxd_query!(self, abci_info().await?)) + } + + pub(crate) async fn block_info(&self, height: u32) -> Result { + Ok(nyxd_query!(self, block(height).await?)) + } + pub(crate) async fn client_address(&self) -> Option { let guard = self.inner.read().await; match &*guard { diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 070d93ead9..7fb3e6a067 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -2250,9 +2250,9 @@ dependencies = [ [[package]] name = "hickory-resolver" -version = "0.24.3" +version = "0.24.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf287bde7b776e85d7188e6e5db7cf410a2f9531fe82817eb87feed034c8d14" +checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e" dependencies = [ "cfg-if", "futures-util", @@ -3267,6 +3267,7 @@ dependencies = [ "cosmwasm-std", "ecdsa", "getset", + "humantime-serde", "nym-compact-ecash", "nym-config", "nym-contracts-common", From 2a6fe6624dc5c1be00fbf0b55b39285b0b069c75 Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Mon, 3 Mar 2025 11:49:09 +0000 Subject: [PATCH 020/133] [DOCs/operators]: Advanced server setup: install KVM, virtualise machines, prep VMs for nym-node (#5493) * initialise KVM docs * initialise steps for KVM installation and setup * document guide to setup KVM network bridge * add new page with KVM installation * add disclaimer * add VM configuration guide * first version finalised, ready for testing and review * finish VM guide * setup guide finished * add last sentence --- .../nyx-outputs/nyx-total-stake.md | 2 +- .../outputs/api-scraping-outputs/time-now.md | 2 +- .../command-outputs/nym-node-run-help.md | 18 +- .../preliminary-steps/vps-setup/advanced.mdx | 746 ++++++++++++++++++ 4 files changed, 759 insertions(+), 9 deletions(-) create mode 100644 documentation/docs/pages/operators/nodes/preliminary-steps/vps-setup/advanced.mdx diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md index 64cb46e4ca..e36dbca0e2 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md @@ -1 +1 @@ -44.332 +44.811 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md index 6b6e11be89..07d784ff82 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md @@ -1 +1 @@ -Monday, February 3rd 2025, 13:47:19 UTC +Wednesday, February 26th 2025, 16:02:47 UTC diff --git a/documentation/docs/components/outputs/command-outputs/nym-node-run-help.md b/documentation/docs/components/outputs/command-outputs/nym-node-run-help.md index 32508afe64..1a70eacf1d 100644 --- a/documentation/docs/components/outputs/command-outputs/nym-node-run-help.md +++ b/documentation/docs/components/outputs/command-outputs/nym-node-run-help.md @@ -16,8 +16,10 @@ Options: If this is a brand new nym-node, specify whether it should only be initialised without actually running the subprocesses [env: NYMNODE_INIT_ONLY=] --local Flag specifying this node will be running in a local setting [env: NYMNODE_LOCAL=] - --mode - Specifies the current mode of this nym-node [env: NYMNODE_MODE=] [possible values: mixnode, entry-gateway, exit-gateway] + --mode [...] + Specifies the current mode(s) of this nym-node [env: NYMNODE_MODE=] [possible values: mixnode, entry-gateway, exit-gateway, exit-providers-only] + --modes + Specifies the current mode(s) of this nym-node as a single flag [env: NYMNODE_MODES=] [possible values: mixnode, entry-gateway, exit-gateway, exit-providers-only] -w, --write-changes If this node has been initialised before, specify whether to write any new changes to the config file [env: NYMNODE_WRITE_CONFIG_CHANGES=] --bonding-information-output @@ -31,7 +33,7 @@ Options: --location Optional **physical** location of this node's server. Either full country name (e.g. 'Poland'), two-letter alpha2 (e.g. 'PL'), three-letter alpha3 (e.g. 'POL') or three-digit numeric-3 (e.g. '616') can be provided [env: NYMNODE_LOCATION=] --http-bind-address - Socket address this node will use for binding its http API. default: `0.0.0.0:8080` [env: NYMNODE_HTTP_BIND_ADDRESS=] + Socket address this node will use for binding its http API. default: `[::]:8080` [env: NYMNODE_HTTP_BIND_ADDRESS=] --landing-page-assets-path Path to assets directory of custom landing page of this node [env: NYMNODE_HTTP_LANDING_ASSETS=] --http-access-token @@ -43,27 +45,29 @@ Options: --expose-crypto-hardware Specify whether detailed system crypto hardware information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_CRYPTO_HARDWARE=] [possible values: true, false] --mixnet-bind-address - Address this node will bind to for listening for mixnet packets default: `0.0.0.0:1789` [env: NYMNODE_MIXNET_BIND_ADDRESS=] + Address this node will bind to for listening for mixnet packets default: `[::]:1789` [env: NYMNODE_MIXNET_BIND_ADDRESS=] --mixnet-announce-port If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind a proxy [env: NYMNODE_MIXNET_ANNOUNCE_PORT=] --nym-api-urls Addresses to nym APIs from which the node gets the view of the network [env: NYMNODE_NYM_APIS=] --nyxd-urls Addresses to nyxd chain endpoint which the node will use for chain interactions [env: NYMNODE_NYXD=] + --enable-console-logging + Specify whether running statistics of this node should be logged to the console [env: NYMNODE_ENABLE_CONSOLE_LOGGING=] [possible values: true, false] --wireguard-enabled Specifies whether the wireguard service is enabled on this node [env: NYMNODE_WG_ENABLED=] [possible values: true, false] --wireguard-bind-address - Socket address this node will use for binding its wireguard interface. default: `0.0.0.0:51822` [env: NYMNODE_WG_BIND_ADDRESS=] + Socket address this node will use for binding its wireguard interface. default: `[::]:51822` [env: NYMNODE_WG_BIND_ADDRESS=] --wireguard-announced-port Port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=] --wireguard-private-network-prefix The prefix denoting the maximum number of the clients that can be connected via Wireguard. The maximum value for IPv4 is 32 and for IPv6 is 128 [env: NYMNODE_WG_PRIVATE_NETWORK_PREFIX=] --verloc-bind-address - Socket address this node will use for binding its verloc API. default: `0.0.0.0:1790` [env: NYMNODE_VERLOC_BIND_ADDRESS=] + Socket address this node will use for binding its verloc API. default: `[::]:1790` [env: NYMNODE_VERLOC_BIND_ADDRESS=] --verloc-announce-port If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind a proxy [env: NYMNODE_VERLOC_ANNOUNCE_PORT=] --entry-bind-address - Socket address this node will use for binding its client websocket API. default: `0.0.0.0:9000` [env: NYMNODE_ENTRY_BIND_ADDRESS=] + Socket address this node will use for binding its client websocket API. default: `[::]:9000` [env: NYMNODE_ENTRY_BIND_ADDRESS=] --announce-ws-port Custom announced port for listening for websocket client traffic. If unspecified, the value from the `bind_address` will be used instead [env: NYMNODE_ENTRY_ANNOUNCE_WS_PORT=] --announce-wss-port diff --git a/documentation/docs/pages/operators/nodes/preliminary-steps/vps-setup/advanced.mdx b/documentation/docs/pages/operators/nodes/preliminary-steps/vps-setup/advanced.mdx new file mode 100644 index 0000000000..f92cee55df --- /dev/null +++ b/documentation/docs/pages/operators/nodes/preliminary-steps/vps-setup/advanced.mdx @@ -0,0 +1,746 @@ + +import { Callout } from 'nextra/components'; +import { Tabs } from 'nextra/components'; +import { VarInfo } from 'components/variable-info.tsx'; +import { Steps } from 'nextra/components'; +import {Accordion, AccordionItem} from "@nextui-org/react"; +import { MyTab } from 'components/generic-tabs.tsx'; +import { AccordionTemplate } from 'components/accordion-template.tsx'; + +# Advanced Server Administration + +This page is for experienced operators and aspiring sys-admins who seek for higher optimisation and better efficiency of their work managing Nym infrastructure. The steps shared on this page cannot be simply copy-pasted, they ask you for more attention and consideration all the way from choosing server and OS to specs per VM allocation. + + + +## Virtualising a Dedicated Server + +Some operators or squads of operators orchestrate multiple Nym nodes. Among other benefits (which are out of scope of this page), these operators can decide to acquire one larger dedicated (or bare-metal) server with enough specs (CPU, RAM, storage, bandwidth and port speed) to meet [minimum requirements](../../../nodes#minimum-requirements) for multiple nodes run in parallel. + +This guide explains how to prepare your server in order to be able to host multiple nodes running on separated VMs. + + +This guide is based on Ubuntu 22.04, in case you prefer another OS, you may have to do a bit of your own research to troubleshoot networking configuration and other parameters. + + +### Installing KVM on a Server with Ubuntu 22.04 + +**KVM** stands for **Kernel-based Virtual Machine**. It is a virtualization technology for Linux that allows a user to run multiple virtual machines (VMs) on a single physical machine. KVM turns the Linux kernel into a hypervisor, enabling it to manage multiple virtualised systems. + +Follow the steps below to install KVM on Ubuntu 22.04 LTS. + +#### Prerequisites + + +Operators aiming to run Nym node as mixnet [Exit Gateway](../../../community-counsel/exit-gateway) or with wireguard enabled should familiarize themselves with the challenges possibly coming along `nym-node` operation, described in our [community counsel](../../../community-counsel) and follow up with [legal suggestions](../../../community-counsel/legal). Particularly important is to [introduce yourself](../../../community-counsel/legal#introduce-nym-node-to-your-provider) and your intentions to run a Nym node to your provider. + +This step is essential part of legal self defense because it may prevent your provider immediately shutting down your entire service (with all the VMs on it) when receiving first abuse report. + +Additionally, before purchasing a large server, **contact the provider and ask if the offered CPU supports Virtualization Technology (VT)**, without this feature you will not be able to proceed. + + +Start with obtaining a server with Ubuntu 22.04 LTS: +- Make sure that your server meets [minimum requirements](../vps-setup#nym-node---dedicated-server) multiplied by number of `nym-node` instance you aim to run on it. +- Most people rent a server from a provider and it comes with a pre-installed OS (in this guide we use Ubuntu 22.04). In case your choice is a bare-metal machine, you probably know what you are doing, there are some useful guides to install a new OS, like [this one on ostechnix.com](https://ostechnix.com/install-ubuntu-server/). + +Make sure thay your system actually supports hardware virtualisation: +- Check out the methods documented in [this guide by ostechnix.com](https://ostechnix.com/how-to-find-if-a-cpu-supports-virtualization-technology-vt/). + +Order enough IPv4 and IPv6 (static and public) addresses to have one of each for each planned VM plus one extra for the main machine. + + +When you have your OS installed, validated CPU virtualisation support and obtained IP addresses, you can start configuring your VMs, following the steps below. + +> Note that the commands below require root permission. You can either go through the setup as `root` or use `sudo` prefix with the commands used in the guide. You can switch to `root` shell by entering one of these commands `sudo su` or `sudo -i`. + + +##### 1. Install KVM + +- Install KVM and required components: +```sh +apt install qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils virtinst +``` +
+ +- `qemu-kvm`: Provides the core **KVM virtualization** support using QEMU. +- `libvirt-daemon-system`: Manages virtual machines via the **libvirt daemon**. +- `libvirt-clients` Provides command-line tools like `virsh` to manage VMs. +- `bridge-utils`: Enables **network bridging**, allowing VMs to communicate over the network. +- `virtinst`: Includes `virt-install` for **creating virtual machines** via CLI. + + +- Start the `libvertd` service: +```sh +systemctl enable libvirtd +systemctl start libvirtd +``` +- Validate by checking status of `libvirt` service: +```sh +systemctl status libvirtd +``` +
+ +The command output should look similar to this one: +``` +root@nym-exit:~# systemctl status libvirtd +● libvirtd.service - Virtualization daemon + Loaded: loaded (/lib/systemd/system/libvirtd.service; enabled; vendor preset: enabled) + Active: active (running) since Thu 2025-02-27 14:25:28 MSK; 2min 1s ago +TriggeredBy: ● libvirtd-ro.socket + ● libvirtd.socket + ● libvirtd-admin.socket + Docs: man:libvirtd(8) + https://libvirt.org + Main PID: 6232 (libvirtd) + Tasks: 21 (limit: 32768) + Memory: 11.8M + CPU: 852ms + CGroup: /system.slice/libvirtd.service + ├─6232 /usr/sbin/libvirtd + ├─6460 /usr/sbin/dnsmasq --conf-file=/var/lib/libvirt/dnsmasq/default.conf --leasefile-ro --dhcp-script=/usr/lib/libvirt/libvirt_leaseshelper + └─6461 /usr/sbin/dnsmasq --conf-file=/var/lib/libvirt/dnsmasq/default.conf --leasefile-ro --dhcp-script=/usr/lib/libvirt/libvirt_leaseshelper + +Feb 27 14:25:28 nym-exit.example.com systemd[1]: Started Virtualization daemon. +Feb 27 14:25:30 nym-exit.example.com dnsmasq[6460]: started, version 2.90 cachesize 150 +Feb 27 14:25:30 nym-exit.example.com dnsmasq[6460]: compile time options: IPv6 GNU-getopt DBus no-UBus i18n IDN2 DHCP DHCPv6 no-Lua TFTP conntrack ipset no-nftset auth cryptohash DNSSEC loop-detect inotify dump> +Feb 27 14:25:30 nym-exit.example.com dnsmasq-dhcp[6460]: DHCP, IP range 192.168.122.2 -- 192.168.122.254, lease time 1h +Feb 27 14:25:30 nym-exit.example.com dnsmasq-dhcp[6460]: DHCP, sockets bound exclusively to interface virbr0 +Feb 27 14:25:30 nym-exit.example.com dnsmasq[6460]: reading /etc/resolv.conf +Feb 27 14:25:30 nym-exit.example.com dnsmasq[6460]: using nameserver 127.0.0.53#53 +Feb 27 14:25:30 nym-exit.example.com dnsmasq[6460]: read /etc/hosts - 8 names +Feb 27 14:25:30 nym-exit.example.com dnsmasq[6460]: read /var/lib/libvirt/dnsmasq/default.addnhosts - 0 names +Feb 27 14:25:30 nym-exit.example.com dnsmasq-dhcp[6460]: read /var/lib/libvirt/dnsmasq/default.hostsfile +``` + + +- In case you don't configure KVM as `root`, add your current user to the `kvm` and `libvirt` groups to enable VM creation and management using the `virsh` command-line tool or the `virt-manager` GUI: +```bash +usermod -aG kvm $USER +usermod -aG libvirt $USER +``` + +##### 2. Setup Bridge Networking with KVM + +A **bridged network** lets VMs share the host’s network interface, allowing direct IPv4/IPv6 access like a physical machine. + +By default, KVM sets up a **private virtual bridge**, enabling VM-to-VM communication within the host. It provides its own subnet, DHCP, and NAT for external access. + +Check the IP of KVM’s default virtual interfaces with: + +```bash +ip a +``` +
+ +The command output should look similar to this one: +``` +root@nym-exit:~# ip a +1: lo: mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 + link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 + inet 127.0.0.1/8 scope host lo + valid_lft forever preferred_lft forever + inet6 ::1/128 scope host + valid_lft forever preferred_lft forever +2: eno1: mtu 1500 qdisc noop state DOWN group default qlen 1000 + link/ether 14:02:ec:35:2e:14 brd ff:ff:ff:ff:ff:ff + altname enp2s0f0 +3: eno49: mtu 1500 qdisc mq state UP group default qlen 1000 + link/ether 38:63:bb:2e:9d:20 brd ff:ff:ff:ff:ff:ff + altname enp4s0f0 + inet 31.222.238.222/24 brd 31.222.238.255 scope global eno49 + valid_lft forever preferred_lft forever + inet6 fe80::3a63:bbff:fe2e:9d20/64 scope link + valid_lft forever preferred_lft forever +4: eno2: mtu 1500 qdisc noop state DOWN group default qlen 1000 + link/ether 14:02:ec:35:2e:15 brd ff:ff:ff:ff:ff:ff + altname enp2s0f1 +5: eno3: mtu 1500 qdisc noop state DOWN group default qlen 1000 + link/ether 14:02:ec:35:2e:16 brd ff:ff:ff:ff:ff:ff + altname enp2s0f2 +6: eno50: mtu 1500 qdisc noop state DOWN group default qlen 1000 + link/ether 38:63:bb:2e:9d:24 brd ff:ff:ff:ff:ff:ff + altname enp4s0f1 +7: eno4: mtu 1500 qdisc noop state DOWN group default qlen 1000 + link/ether 14:02:ec:35:2e:17 brd ff:ff:ff:ff:ff:ff + altname enp2s0f3 +8: virbr0: mtu 1500 qdisc noqueue state DOWN group default qlen 1000 + link/ether 52:54:00:ac:d3:ba brd ff:ff:ff:ff:ff:ff + inet 192.168.122.1/24 brd 192.168.122.255 scope global virbr0 + valid_lft forever preferred_lft forever +``` + + + +By default, KVM uses the `virbr0` network with `.1/24`, assigning guest VMs IPs in the `.0/24` range. The host OS is reachable at `.1`, allowing SSH and file transfers (`scp`) between the host and guests. + +This setup works if you only access VMs from the host. However, remote systems on a different subnet (e.g., `.0/24`) **cannot** reach the VMs. + +To enable external access, we need a *public bridge* that connects VMs to the host’s main network, using its DHCP. This ensures VMs get IPs in the same range as the host. + +Before configuring a public bridge, **disable Netfilter** on bridges for better performance and security, as it is enabled by default. + +- Create a file located at `/etc/sysctl.d/bridge.conf`: +```bash +nano /etc/sysctl.d/bridge.conf + +# in case of using custom editor, replace nano in the syntax +``` + +- Paste inside the following block, save and exit: +```ini +net.bridge.bridge-nf-call-ip6tables=0 +net.bridge.bridge-nf-call-iptables=0 +net.bridge.bridge-nf-call-arptables=0 +``` + +- Create a file `/etc/udev/rules.d/99-bridge.rules`: +```bash +nano /etc/udev/rules.d/99-bridge.rules +``` + +- Paste this line, save and exit: +```bash +ACTION=="add", SUBSYSTEM=="module", KERNEL=="br_netfilter", RUN+="/sbin/sysctl -p /etc/sysctl.d/bridge.conf" +``` + +This disables Netfilter on bridges at startup. Save, exit, and reboot to apply changes. + +- Disable KVM’s default networking. Find the default network interface with: +```bash +ip link +``` + +
+ +The command output should look similar to this one: +``` +root@nym-exit:~# ip link +1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 + link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 +2: eno1: mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 + link/ether 14:02:ec:35:2e:14 brd ff:ff:ff:ff:ff:ff + altname enp2s0f0 +3: eno2: mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 + link/ether 14:02:ec:35:2e:15 brd ff:ff:ff:ff:ff:ff + altname enp2s0f1 +4: eno49: mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000 + link/ether 38:63:bb:2e:9d:20 brd ff:ff:ff:ff:ff:ff + altname enp4s0f0 +5: eno3: mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 + link/ether 14:02:ec:35:2e:16 brd ff:ff:ff:ff:ff:ff + altname enp2s0f2 +6: eno50: mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 + link/ether 38:63:bb:2e:9d:24 brd ff:ff:ff:ff:ff:ff + altname enp4s0f1 +7: eno4: mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 + link/ether 14:02:ec:35:2e:17 brd ff:ff:ff:ff:ff:ff + altname enp2s0f3 +8: virbr0: mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default qlen 1000 + link/ether 52:54:00:ac:d3:ba brd ff:ff:ff:ff:ff:ff +``` + +The `virbr0` interface is KVM’s default network. Note your physical interface’s MAC address (e.g., `eno49`). It's the only interface that is currently `UP` and running (`LOWER_UP` state). Other interfaces are `DOWN` and not in use. + + +- Remove the default KVM network: +```bash +virsh net-destroy default +``` + +- Remove the default network configuration: +```bash +virsh net-undefine default +``` + +- In case last two commands didn't work, try this: +```bash +ip link delete virbr0 type bridge +``` +- Verify that the `virbr0` and `virbr0-nic` interfaces are deleted: +```bash +ip link +``` + +The command output should look similar to this one: +``` +root@nym-exit:~# ip link +1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 + link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 +2: eno1: mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 + link/ether 14:02:ec:35:2e:14 brd ff:ff:ff:ff:ff:ff + altname enp2s0f0 +3: eno2: mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 + link/ether 14:02:ec:35:2e:15 brd ff:ff:ff:ff:ff:ff + altname enp2s0f1 +4: eno49: mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000 + link/ether 38:63:bb:2e:9d:20 brd ff:ff:ff:ff:ff:ff + altname enp4s0f0 +5: eno3: mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 + link/ether 14:02:ec:35:2e:16 brd ff:ff:ff:ff:ff:ff + altname enp2s0f2 +6: eno50: mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 + link/ether 38:63:bb:2e:9d:24 brd ff:ff:ff:ff:ff:ff + altname enp4s0f1 +7: eno4: mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000 + link/ether 14:02:ec:35:2e:17 brd ff:ff:ff:ff:ff:ff + altname enp2s0f3 +``` +KVM network is gone. + + + +##### 3. Setup KVM public bridge for new VMs + +To create a KVM network bridge on Ubuntu, edit a config file located in `/etc/netplan/` either called `00-installer.yaml` or `00-installer-config.yaml` and add the bridge details. + +- Before you edit the file, make a backup to stay on the save side: +```bash +cp /etc/netplan/00-installer-config.yaml /etc/netplan/00-installer-config.yaml.bak +# or +cp /etc/netplan/00-installer.yaml /etc/netplan/00-installer.yaml.bak +``` + +- Open `00-installer-config.yaml` or `00-installer.yaml.`config in a text editor: +```bash +nano /etc/netplan/00-installer.yaml +# or +nano /etc/netplan/00-installer-config.yaml +``` + +- Edit the block below and paste it to the config file, save and exit: +```ini +##################################################### +######## CHANGE ALL VARIABLES IN <> BRACKETS ######## +##################################################### + +# is your own one, you can get with command ip link show +# is your server main IPv4 address +# value can be found by running: ip route | grep default + + +# This is the network config written by 'subiquity' +network: + version: 2 + ethernets: + : + dhcp4: false + dhcp6: false + + # Bridge interface configuration + bridges: + br0: + interfaces: [] + addresses: [/24] + routes: + - to: default + via: + mtu: 1500 + nameservers: + addresses: + - 8.8.8.8 + - 1.1.1.1 + - 77.88.8.8 + parameters: + stp: false # Disable STP unless multiple bridges exist + forward-delay: 15 # Can be shortened, 15 sec is a common default +``` + + +Ensure the indentation matches exactly as shown above. Incorrect spacing will prevent the bridged network interface from activating. + + +- Validate `netplan` configuration without applying to prevent breaking network changes: +```bash +netplan generate + +# Correct configuration output will show nothing +``` + +- Safety test your changes to catch syntax errors before applying: +```bash +netplan try +``` + +- Apply your changes: +```bash +netplan --debug apply +``` + +- In case of proubems try some of these steps: + +- Validate YAML configuration, given that YAML is syntax sensitive: +```bash +apt install yamllint -y + +yamllint /etc/netplan/00-installer.yaml +# or +yamllint /etc/netplan/00-installer-config.yaml + + +``` +- Apply correct permissions: +```bash +chmod 600 /etc/netplan/00-installer.yaml +chown root:root /etc/netplan/00-installer.yaml +``` + +- Manually bring up the bridge: +```bash +ip link add name br0 type bridge +ip link set br0 up +ip a show br0 +``` + +- ensure `systemd-networkd` is enabled: +```bash +systemctl restart systemd-networkd +systemctl status systemd-networkd +# if inactive, enable it: +systemctl enable --now systemd-networkd +``` + + +- If things went wrong, you can always revert from the backed up file: +```bash +cp /etc/netplan/00-installer-config.yaml.bak /etc/netplan/00-installer-config.yaml +# or +cp /etc/netplan/00-installer.yaml.bak /etc/netplan/00-installer.yaml +# and +netplan apply +``` + + +Using different IPs for your physical NIC and KVM bridge will disconnect SSH when applying changes. Reconnect using the bridge's new IP. If both share the same IP, no disruption occurs. + + + +- Verify that the IP address has been assigned to the bridge interface: +```bash +ip a +``` + +The command output should look similar to this one: +``` +root@nym-exit:~# ip a +1: lo: mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 + link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 + inet 127.0.0.1/8 scope host lo + valid_lft forever preferred_lft forever + inet6 ::1/128 scope host + valid_lft forever preferred_lft forever +2: eno1: mtu 1500 qdisc noop state DOWN group default qlen 1000 + link/ether 14:02:ec:35:2e:14 brd ff:ff:ff:ff:ff:ff + altname enp2s0f0 +3: eno2: mtu 1500 qdisc noop state DOWN group default qlen 1000 + link/ether 14:02:ec:35:2e:15 brd ff:ff:ff:ff:ff:ff + altname enp2s0f1 +4: eno3: mtu 1500 qdisc noop state DOWN group default qlen 1000 + link/ether 14:02:ec:35:2e:16 brd ff:ff:ff:ff:ff:ff + altname enp2s0f2 +5: eno49: mtu 1500 qdisc mq master br0 state UP group default qlen 1000 + link/ether 38:63:bb:2e:9d:20 brd ff:ff:ff:ff:ff:ff + altname enp4s0f0 +6: eno4: mtu 1500 qdisc noop state DOWN group default qlen 1000 + link/ether 14:02:ec:35:2e:17 brd ff:ff:ff:ff:ff:ff + altname enp2s0f3 +7: eno50: mtu 1500 qdisc noop state DOWN group default qlen 1000 + link/ether 38:63:bb:2e:9d:24 brd ff:ff:ff:ff:ff:ff + altname enp4s0f1 +8: br0: mtu 1500 qdisc noqueue state UP group default qlen 1000 + link/ether 46:50:aa:c0:49:a5 brd ff:ff:ff:ff:ff:ff + inet 31.222.238.222/24 brd 31.222.238.255 scope global br0 + valid_lft forever preferred_lft forever + inet6 fe80::4450:aaff:fec0:49a5/64 scope link + valid_lft forever preferred_lft forever +``` +The bridged interface `br0` now has the IP ``, and `` shows `master br0`, indicating it is part of the bridge. + + +Alternatively you can use `brctl` command to display the KVM bridge network status: +```bash +brctl show br0 +``` + +##### 4. Add Bridge Network to KVM + +- Configure KVM to use the bridge by creating `host-bridge.xml`, open a text editor and pate the block below: +```bash +nano host-bridge.xml +``` + +```xml + + host-bridge + + + +``` + +- Start the new bridge and set it as the default for VMs: +```bash +virsh net-define host-bridge.xml +virsh net-start host-bridge +virsh net-autostart host-bridge +``` + +- Verify that the KVM bridge is active: +```bash +virsh net-list --all +``` + +```bash +root@nym-exit:~# virsh net-list --all + Name State Autostart Persistent +------------------------------------------------ + host-bridge active yes yes +``` + + +KVM bridge networking is successfully set up and active! + +Your KVM installation is now ready to deploy and manage VMs. + +
+ +### Setting Up Virtual Machines + +After finishing the [installation of KVM](#installing-kvm-on-a-server-with-ubuntu-2204), we can move to the virtualisation configuration. + +> **The steps below will guide you through a setup of one VM, therefore you will have to repeat this process for each VM**. That also means that you have to be mindful of space and memory allocation. + + +##### 1. Install OS for VMs + +This is the OS on which the nodes themselves will run. You can chose any GNU/Linux of your preference. For this guide we are going to be using Ubuntu 24.04 LTS (Noble Numbat) cloud image from [cloud-images.ubuntu.com](https://cloud-images.ubuntu.com/noble/current/). + +- Download Ubuntu Cloud image: +```bash +wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img +``` +- Copy the image to to `/var/lib/libvirt/images/` asigning to it a name your VM +```bash +cp noble-server-cloudimg-amd64.img /var/lib/libvirt/images/.img + +# for example: +# cp noble-server-cloudimg-amd64.img /var/lib/libvirt/images/ubuntu-1.img +``` + +##### 2. Create and resize a virtual machine + +- Get `guestfs-tools` to be able to customize your login credentials: +```bash +apt install guestfs-tools +``` + +- Define login credentials: +```bash +virt-customize -a /var/lib/libvirt/images/.img --root-password password: +# for example +# virt-customize -a /var/lib/libvirt/images/ubuntu-1.img --root-password password:makesuretosaveyourpasswordslocallytoapasswordmanager +``` + +- Use `qemu-img` tool with a command `resize` to create a VM according your needs. You can see `qemu` [documentation page`](https://www.qemu.org/docs/master/tools/qemu-img.html) for more info on how to use it correctly. +```bash +qemu-img resize /var/lib/libvirt/images/.img +G +# for example +# qemu-img resize /var/lib/libvirt/images/ubuntu-1.img +100G +``` + +- Resize it from within it after `virt-install` command: +```bash +virt-install \ +--name \ +--ram= \ +--vcpus= \ +--cpu host \ +--hvm \ +--disk bus=virtio,path=/var/lib/libvirt/images/.img \ +--network bridge=br0 \ +--graphics none \ +--console pty,target_type=serial \ +--osinfo \ +--import +``` + +- In our example we go with 4 GB RAM on the same machine as before: +
+ +```bash +virt-install \ +--name ubuntu-1 \ +--ram=4096 \ +--vcpus=4 \ +--cpu host \ +--hvm \ +--disk bus=virtio,path=/var/lib/libvirt/images/ubuntu-1.img \ +--network bridge=br0 \ +--graphics none \ +--console pty,target_type=serial \ +--osinfo ubuntunoble \ +--import +``` + + +- After loading you should see a login console, you can also initiate it by: +```bash +virsh console +# for example +# virsh console ubuntu-1 +``` + +- Log in to your new VM using your credentials. + +##### 3. Validate your setup + +- Make sure the `root` disk has the expected space by running: +```bash +df -h +``` + +- If not, run: +```bash +growpart /dev/vda 1 +resize2fs /dev/vda1 +``` + +##### 4. Configure networking for the VM + +As this guide is based on a newer Ubuntu, we use `netplan`, this may be different on different OS. + +- Open `/etc/netplan/01-network-config.yaml` in your favourite text editor: +```bash +nano /etc/netplan/01-network-config.yaml +``` + +- Insert this config, using your correct IP configuration, save and exit: +```ini +network: + version: 2 + renderer: networkd + ethernets: + : + dhcp4: false + dhcp6: false # Set to true if you want automatic IPv6 assignment + addresses: + - /24 # Assign IPv4 address to the VM + - /64 # Assign IPv6 address to the VM + routes: + - to: default + via: # IPv4 gateway (host machine) + - to: default + via: # IPv6 gateway (host machine) + nameservers: + addresses: + - 1.1.1.1 # Cloudflare IPv4 DNS + - 8.8.8.8 # Google IPv4 DNS + - 2606:4700:4700::1111 # Cloudflare IPv6 DNS + - 2001:4860:4860::8888 # Google IPv6 DNS +``` +- Fix wide permissions on the config file: +```bash +chmod 600 /etc/netplan/01-network-config.yaml +``` + +- Check if the config has any errors: +```bash +netplan generate +``` + +- Apply the configuration: +```bash +netplan --debug apply +``` + +- Verify by checking if IPv4 and IPv6 are assigned correctly and if they route: +```bash +ip -4 a +ip -6 a +``` +```bash +ip -4 r +ip -6 r +``` +```bash +# to ping through IPv6, use: +ping6 nym.com +``` +- You should be able to ping your new VM from a local machine: +```bash +ping +ping6 +``` + +
+ +Your VM should be working and fully routable. To be able to use it properly, we will create a direct SSH access to the VM. + +#### Configure VM SSH access + + + +##### 1. Log in to your VM, update and upgrade your OS: +- Log in to your server using as `root` or as a non-root user with `sudo` privileges +```bash +apt update; apt upgrade +``` + +##### 2. Generate new host SSH keys + +Since we used a `cloud-init` image without an SSH server, we need to generate SSH host keys for client authentication and server identity verification. All of them will be saved to this location: `/etc/ssh/`. + +- Generate a new RSA host key: +```bash +ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key +``` +- Generate a new DSA host key: +```bash +ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key +``` +- Generate a new ECDSA host key: +```bash +ssh-keygen -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key +``` +- Finally, generate a new ED25519 host key: +```bash +ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key +``` +##### 3. Restart the SSH service on the server +- Run: +```bash +systemctl restart ssh.service +``` + +##### 4. Check if the SSH serice is active +- Run: +```bash +systemctl status ssh.service +``` + +##### 5. Create file `~/.ssh/authorized_keys` and add you public key: +- Create `.ssh` directory: +```bash +mkdir ~/.ssh +``` + +- Open with your favourite text editor: +```bash +nano ~/.ssh/authorized_keys +``` +- Paste your SSH public key, save and exit + +- In case of non-root, setup a correct ownership and permissions: +```bash +chmod 600 ~/.ssh/authorized_keys +chmod 700 ~/.ssh +chown : ~/.ssh +``` + +##### 5. Test by connecting via SSH + +- Now you should be able to connect to the VM directly from your local terminal +```bash +ssh root@ -i ~/.ssh/your_ssh_key +``` + + +Now your VM is almost ready for `nym-node` [setup](../../nym-node/setup). Before you proceed, ssh in and [configure all prerequisities](../vps-setup#vps-configuration) needed for `nym-node` installation and operation. From 9be9c04f52628e13fe85d154171bd9892fd49003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Mon, 3 Mar 2025 14:06:20 +0100 Subject: [PATCH 021/133] Add SURBs soft threshold (#5535) * Add surbs soft threshold * wip * Proactively request more SURBs than needed * fmt * cleanup * wip logging * wip * debugging * wip * Tidy * tidy * Set threshold buffer default for IPR * rustfmt * wasm fixes * debug * Tweak debug message * Set default min buffer to 0 * Tweak backlog message * Restore debug message * tweak * tweak * wasm --- common/client-core/config-types/src/lib.rs | 6 ++ common/client-core/config-types/src/old/v5.rs | 1 + .../real_traffic_stream.rs | 20 +++++-- .../client/replies/reply_controller/mod.rs | 57 ++++++++++++++----- .../src/client/transmission_buffer.rs | 5 +- common/wasm/client-core/src/config/mod.rs | 6 ++ .../wasm/client-core/src/config/override.rs | 8 +++ nym-node/src/config/service_providers.rs | 17 +++++- 8 files changed, 95 insertions(+), 25 deletions(-) diff --git a/common/client-core/config-types/src/lib.rs b/common/client-core/config-types/src/lib.rs index 8347a38c0e..8eb95d62f1 100644 --- a/common/client-core/config-types/src/lib.rs +++ b/common/client-core/config-types/src/lib.rs @@ -45,6 +45,7 @@ const DEFAULT_COVER_TRAFFIC_PRIMARY_SIZE_RATIO: f64 = 0.70; // clients/client-core/src/client/replies/reply_storage/surb_storage.rs const DEFAULT_MINIMUM_REPLY_SURB_STORAGE_THRESHOLD: usize = 10; const DEFAULT_MAXIMUM_REPLY_SURB_STORAGE_THRESHOLD: usize = 200; +const DEFAULT_MINIMUM_REPLY_SURB_THRESHOLD_BUFFER: usize = 0; // define how much to request at once // clients/client-core/src/client/replies/reply_controller.rs @@ -621,6 +622,10 @@ pub struct ReplySurbs { /// Defines the maximum number of reply surbs the client wants to keep in its storage at any times. pub maximum_reply_surb_storage_threshold: usize, + /// Defines the soft threshold ontop of the minimum reply surb storage threshold for when the client + /// should proactively request additional reply surbs. + pub minimum_reply_surb_threshold_buffer: usize, + /// Defines the minimum number of reply surbs the client would request. pub minimum_reply_surb_request_size: u32, @@ -660,6 +665,7 @@ impl Default for ReplySurbs { ReplySurbs { minimum_reply_surb_storage_threshold: DEFAULT_MINIMUM_REPLY_SURB_STORAGE_THRESHOLD, maximum_reply_surb_storage_threshold: DEFAULT_MAXIMUM_REPLY_SURB_STORAGE_THRESHOLD, + minimum_reply_surb_threshold_buffer: DEFAULT_MINIMUM_REPLY_SURB_THRESHOLD_BUFFER, minimum_reply_surb_request_size: DEFAULT_MINIMUM_REPLY_SURB_REQUEST_SIZE, maximum_reply_surb_request_size: DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE, maximum_allowed_reply_surb_request_size: DEFAULT_MAXIMUM_ALLOWED_SURB_REQUEST_SIZE, diff --git a/common/client-core/config-types/src/old/v5.rs b/common/client-core/config-types/src/old/v5.rs index df673a0986..872d107cc5 100644 --- a/common/client-core/config-types/src/old/v5.rs +++ b/common/client-core/config-types/src/old/v5.rs @@ -181,6 +181,7 @@ impl From for Config { maximum_reply_surb_age: value.debug.reply_surbs.maximum_reply_surb_age, maximum_reply_key_age: value.debug.reply_surbs.maximum_reply_key_age, surb_mix_hops: value.debug.reply_surbs.surb_mix_hops, + ..Default::default() }, ..Default::default() }, diff --git a/common/client-core/src/client/real_messages_control/real_traffic_stream.rs b/common/client-core/src/client/real_messages_control/real_traffic_stream.rs index 2789228dcf..d20b8040aa 100644 --- a/common/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/common/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -517,17 +517,25 @@ where use crate::error::ClientCoreStatusMessage; let packets = self.transmission_buffer.total_size(); - let backlog = self.transmission_buffer.total_size_in_bytes() as f64 / 1024.0; - let lanes = self.transmission_buffer.num_lanes(); + let lanes = self.transmission_buffer.lanes(); let mult = self.sending_delay_controller.current_multiplier(); let delay = self.current_average_message_sending_delay().as_millis(); + + let lane_status = lanes + .iter() + .map(|lane_name| { + let lane_length = self.transmission_buffer.lane_length(lane_name).unwrap_or(0); + format!("{lane_name:?}: {lane_length}") + }) + .collect::>() + .join(", "); + let status_str = if self.config.traffic.disable_main_poisson_packet_distribution { - format!("Packet backlog: {backlog:.2} kiB ({packets}), {lanes} lanes, no delay") + format!("Packet backlog: {lane_status}, no delay") } else { - format!( - "Packet backlog: {backlog:.2} kiB ({packets}), {lanes} lanes, avg delay: {delay}ms ({mult})" - ) + format!("Packet backlog: {lane_status}, avg delay: {delay}ms ({mult})") }; + if packets > 1000 { log::warn!("{status_str}"); } else if packets > 0 { diff --git a/common/client-core/src/client/replies/reply_controller/mod.rs b/common/client-core/src/client/replies/reply_controller/mod.rs index 085fbfa6e4..fb8cdc468b 100644 --- a/common/client-core/src/client/replies/reply_controller/mod.rs +++ b/common/client-core/src/client/replies/reply_controller/mod.rs @@ -102,6 +102,7 @@ where fragments: I, lane: TransmissionLane, ) { + trace!("buffering pending replies for {recipient}"); self.pending_replies .entry(*recipient) .or_insert_with(TransmissionBuffer::new) @@ -113,6 +114,7 @@ where recipient: &AnonymousSenderTag, fragments: Vec<(TransmissionLane, Fragment)>, ) { + trace!("re-inserting pending replies for {recipient}"); // the buffer should ALWAYS exist at this point, if it doesn't, it's a bug... self.pending_replies .entry(*recipient) @@ -125,6 +127,7 @@ where recipient: &AnonymousSenderTag, data: Vec>, ) { + trace!("re-inserting pending retransmissions for {recipient}"); // the underlying entry MUST exist as we've just got data from there let map_entry = self .pending_retransmissions @@ -142,7 +145,7 @@ where } fn should_request_more_surbs(&self, target: &AnonymousSenderTag) -> bool { - trace!("checking if we should request more surbs from {:?}", target); + trace!("checking if we should request more surbs from {target}"); let pending_queue_size = self .pending_replies @@ -158,11 +161,6 @@ where let total_queue = pending_queue_size + retransmission_queue; - // simple as that - there's absolutely nothing to retransmit - if total_queue == 0 { - return false; - } - let available_surbs = self .full_reply_storage .surbs_storage_ref() @@ -179,11 +177,27 @@ where .full_reply_storage .surbs_storage_ref() .max_surb_threshold(); + let min_surbs_threshold_buffer = + self.config.reply_surbs.minimum_reply_surb_threshold_buffer; - debug!("total queue size: {total_queue} = pending data {pending_queue_size} + pending retransmission {retransmission_queue}, available surbs: {available_surbs} pending surbs: {pending_surbs} threshold range: {min_surbs_threshold}..{max_surbs_threshold}"); + // After clearing the queue, we want to have at least `min_surbs_threshold` surbs available + // and reserved for requesting additional surbs, and in addition to that we also want to + // have `min_surbs_threshold_buffer` surbs available proactively. + let target_surbs_after_clearing_queue = min_surbs_threshold + min_surbs_threshold_buffer; - (pending_surbs + available_surbs) < max_surbs_threshold - && (pending_surbs + available_surbs) < (total_queue + min_surbs_threshold) + // Check if we have enough surbs to handle the total queue and maintain minimum thresholds + let total_required_surbs = total_queue + target_surbs_after_clearing_queue; + let total_available_surbs = pending_surbs + available_surbs; + + debug!("total queue size: {total_queue} = pending data {pending_queue_size} + pending retransmission {retransmission_queue}, available surbs: {available_surbs} pending surbs: {pending_surbs} threshold range: {min_surbs_threshold}..+{min_surbs_threshold_buffer}..{max_surbs_threshold}"); + + // We should request more surbs if: + // 1. We haven't hit the maximum surb threshold, and + // 2. We don't have enough surbs to handle the queue plus minimum thresholds + let is_below_max_threshold = total_available_surbs < max_surbs_threshold; + let is_below_required_surbs = total_available_surbs < total_required_surbs; + + is_below_max_threshold && is_below_required_surbs } async fn handle_send_reply( @@ -244,6 +258,10 @@ where &recipient_tag, ); warn!("failed to send reply to {recipient_tag}: {err}"); + info!( + "buffering {no_fragments} fragments for {recipient_tag}", + no_fragments = to_send.len() + ); self.insert_pending_replies(&recipient_tag, to_send, lane); } } @@ -251,6 +269,13 @@ where // if there's leftover data we didn't send because we didn't have enough (or any) surbs - buffer it if !fragments.is_empty() { + // Ideally we should have enough surbs above the minimum threshold to handle sending + // new replies without having to first request more surbs. That's why I'd like to log + // these cases as they might indicate a problem with the surb management. + debug!( + "buffering {no_fragments} fragments for {recipient_tag}", + no_fragments = fragments.len() + ); self.insert_pending_replies(&recipient_tag, fragments, lane); } @@ -265,6 +290,7 @@ where target: AnonymousSenderTag, amount: u32, ) -> Result<(), PreparationError> { + debug!("requesting {amount} additional reply surbs for {target}"); let reply_surb = self .full_reply_storage .surbs_storage_ref() @@ -686,7 +712,7 @@ where // it should take into consideration the average latency, sending rate and queue size. // it should request as many surbs as it takes to saturate its sending rate before next batch arrives async fn request_reply_surbs_for_queue_clearing(&mut self, target: AnonymousSenderTag) { - trace!("requesting surbs for queues clearing"); + trace!("requesting surbs for queue clearing"); let pending_queue_size = self .pending_replies @@ -700,17 +726,18 @@ where .map(|pending_queue| pending_queue.len()) .unwrap_or_default(); + let min_surbs_buffer = self.config.reply_surbs.minimum_reply_surb_threshold_buffer as u32; + let total_queue = (pending_queue_size + retransmission_queue) as u32; - if total_queue == 0 { - trace!("the pending queues for {:?} are already empty", target); - return; - } + // To proactively request additional surbs, we aim to have a buffer of extra surbs in our + // storage. + let total_queue_with_buffer = total_queue + min_surbs_buffer; let request_size = min( self.config.reply_surbs.maximum_reply_surb_request_size, max( - total_queue, + total_queue_with_buffer, self.config.reply_surbs.minimum_reply_surb_request_size, ), ); diff --git a/common/client-core/src/client/transmission_buffer.rs b/common/client-core/src/client/transmission_buffer.rs index d6ee86e861..e6eb7d6891 100644 --- a/common/client-core/src/client/transmission_buffer.rs +++ b/common/client-core/src/client/transmission_buffer.rs @@ -58,8 +58,8 @@ impl TransmissionBuffer { } #[cfg(not(target_arch = "wasm32"))] - pub(crate) fn num_lanes(&self) -> usize { - self.buffer.keys().count() + pub(crate) fn lanes(&self) -> Vec { + self.buffer.keys().cloned().collect() } pub(crate) fn lane_length(&self, lane: &TransmissionLane) -> Option { @@ -83,6 +83,7 @@ impl TransmissionBuffer { } #[cfg(not(target_arch = "wasm32"))] + #[allow(unused)] pub(crate) fn total_size_in_bytes(&self) -> usize where T: SizedData, diff --git a/common/wasm/client-core/src/config/mod.rs b/common/wasm/client-core/src/config/mod.rs index ecd7be7927..64b8be5d40 100644 --- a/common/wasm/client-core/src/config/mod.rs +++ b/common/wasm/client-core/src/config/mod.rs @@ -462,6 +462,10 @@ pub struct ReplySurbsWasm { /// Defines the maximum number of reply surbs the client wants to keep in its storage at any times. pub maximum_reply_surb_storage_threshold: usize, + /// Defines the soft threshold ontop of the minimum reply surb storage threshold for when the client + /// should proactively request additional reply surbs. + pub minimum_reply_surb_threshold_buffer: usize, + /// Defines the minimum number of reply surbs the client would request. pub minimum_reply_surb_request_size: u32, @@ -503,6 +507,7 @@ impl From for ConfigReplySurbs { ConfigReplySurbs { minimum_reply_surb_storage_threshold: reply_surbs.minimum_reply_surb_storage_threshold, maximum_reply_surb_storage_threshold: reply_surbs.maximum_reply_surb_storage_threshold, + minimum_reply_surb_threshold_buffer: reply_surbs.minimum_reply_surb_threshold_buffer, minimum_reply_surb_request_size: reply_surbs.minimum_reply_surb_request_size, maximum_reply_surb_request_size: reply_surbs.maximum_reply_surb_request_size, maximum_allowed_reply_surb_request_size: reply_surbs @@ -529,6 +534,7 @@ impl From for ReplySurbsWasm { ReplySurbsWasm { minimum_reply_surb_storage_threshold: reply_surbs.minimum_reply_surb_storage_threshold, maximum_reply_surb_storage_threshold: reply_surbs.maximum_reply_surb_storage_threshold, + minimum_reply_surb_threshold_buffer: reply_surbs.minimum_reply_surb_threshold_buffer, minimum_reply_surb_request_size: reply_surbs.minimum_reply_surb_request_size, maximum_reply_surb_request_size: reply_surbs.maximum_reply_surb_request_size, maximum_allowed_reply_surb_request_size: reply_surbs diff --git a/common/wasm/client-core/src/config/override.rs b/common/wasm/client-core/src/config/override.rs index 0a485e49c7..72da8bd43d 100644 --- a/common/wasm/client-core/src/config/override.rs +++ b/common/wasm/client-core/src/config/override.rs @@ -339,6 +339,11 @@ pub struct ReplySurbsWasmOverride { #[tsify(optional)] pub maximum_reply_surb_storage_threshold: Option, + /// Defines the soft threshold ontop of the minimum reply surb storage threshold for when the client + /// should proactively request additional reply surbs. + #[tsify(optional)] + pub minimum_reply_surb_threshold_buffer: Option, + /// Defines the minimum number of reply surbs the client would request. #[tsify(optional)] pub minimum_reply_surb_request_size: Option, @@ -386,6 +391,9 @@ impl From for ReplySurbsWasm { maximum_reply_surb_storage_threshold: value .maximum_reply_surb_storage_threshold .unwrap_or(def.maximum_reply_surb_storage_threshold), + minimum_reply_surb_threshold_buffer: value + .minimum_reply_surb_threshold_buffer + .unwrap_or(def.minimum_reply_surb_threshold_buffer), minimum_reply_surb_request_size: value .minimum_reply_surb_request_size .unwrap_or(def.minimum_reply_surb_request_size), diff --git a/nym-node/src/config/service_providers.rs b/nym-node/src/config/service_providers.rs index c870d1947a..b7d2f2cb77 100644 --- a/nym-node/src/config/service_providers.rs +++ b/nym-node/src/config/service_providers.rs @@ -3,12 +3,17 @@ use crate::config::authenticator::Authenticator; use crate::config::persistence::ServiceProvidersPaths; -use nym_client_core_config_types::DebugConfig as ClientDebugConfig; +use nym_client_core_config_types::{DebugConfig as ClientDebugConfig, ReplySurbs}; use nym_config::defaults::mainnet; use serde::{Deserialize, Serialize}; use std::path::Path; use url::Url; +// Default reply surb storage threshold. +// It is set higher than the default since the traffic is mostly a stream of data, +// and not a bursty request/response pattern. +pub const DEFAULT_IPR_MINIMUM_REPLY_SURB_THRESHOLD_BUFFER: usize = 50; + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ServiceProvidersConfig { @@ -145,10 +150,18 @@ pub struct IpPacketRouterDebug { impl Default for IpPacketRouterDebug { fn default() -> Self { + let client_debug = ClientDebugConfig { + reply_surbs: ReplySurbs { + minimum_reply_surb_threshold_buffer: + DEFAULT_IPR_MINIMUM_REPLY_SURB_THRESHOLD_BUFFER, + ..Default::default() + }, + ..Default::default() + }; IpPacketRouterDebug { enabled: true, disable_poisson_rate: true, - client_debug: Default::default(), + client_debug, } } } From 7060fa6dad58f17543f5086c73b1854ad1ceae60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 3 Mar 2025 17:24:10 +0000 Subject: [PATCH 022/133] fixed sphinx version metrics registration (#5546) --- contracts/Cargo.lock | 2 +- nym-node/nym-node-metrics/src/mixnet.rs | 3 ++- nym-node/nym-node-metrics/src/prometheus_wrapper.rs | 8 +++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 7a2e25bfb6..ddf71c7999 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 4 +version = 3 [[package]] name = "ahash" diff --git a/nym-node/nym-node-metrics/src/mixnet.rs b/nym-node/nym-node-metrics/src/mixnet.rs index ee99510c38..f4b9da4722 100644 --- a/nym-node/nym-node-metrics/src/mixnet.rs +++ b/nym-node/nym-node-metrics/src/mixnet.rs @@ -63,6 +63,7 @@ impl MixingStats { .or_default() .forward_packets .received += 1; + *self.ingress.received_versions.entry(version).or_default() += 1; } @@ -213,7 +214,7 @@ impl Display for PacketKind { PacketKind::Unknown => "unknown".fmt(f), PacketKind::Outfox => "outfox".fmt(f), PacketKind::Sphinx(sphinx_version) => { - write!(f, "sphinx-{sphinx_version}") + write!(f, "sphinx_{sphinx_version}") } } } diff --git a/nym-node/nym-node-metrics/src/prometheus_wrapper.rs b/nym-node/nym-node-metrics/src/prometheus_wrapper.rs index 8fdfb5b488..e6c18c6f38 100644 --- a/nym-node/nym-node-metrics/src/prometheus_wrapper.rs +++ b/nym-node/nym-node-metrics/src/prometheus_wrapper.rs @@ -291,7 +291,13 @@ impl PrometheusMetric { } fn set(&self, value: i64) { - metrics_registry().set(&self.name(), value); + let reg = metrics_registry(); + if !reg.set(&self.name(), value) { + if let Some(registrable) = self.to_registrable_metric() { + reg.register_metric(registrable); + reg.set(&self.name(), value); + } + } } fn set_float(&self, value: f64) { From 155c4d37ef74ced56d8832fb7750e85cc92b2ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 3 Mar 2025 17:51:30 +0000 Subject: [PATCH 023/133] feature: v2 authentication request (#5537) * introduced v2 authentication request between clients and gateways * client to send v2 auth when possible * added persistence to last used authentication timestamp * added clients identity to signed plaintext --- Cargo.lock | 3 + .../gateway-client/src/client/mod.rs | 92 ++++-- common/gateway-requests/Cargo.toml | 3 + .../src/authentication/encrypted_address.rs | 6 + common/gateway-requests/src/lib.rs | 21 +- common/gateway-requests/src/types/error.rs | 28 ++ .../src/types/text_request/authenticate.rs | 142 +++++++++ .../{text_request.rs => text_request/mod.rs} | 23 +- ...303120000_add_authentication_timestamp.sql | 7 + common/gateway-storage/src/lib.rs | 14 + common/gateway-storage/src/models.rs | 2 +- common/gateway-storage/src/shared_keys.rs | 28 +- gateway/src/config.rs | 3 + .../node/client_handling/active_clients.rs | 33 +- .../client_handling/websocket/common_state.rs | 12 +- .../connection_handler/authenticated.rs | 4 +- .../websocket/connection_handler/fresh.rs | 289 ++++++++++++------ .../websocket/connection_handler/helpers.rs | 37 +++ .../websocket/connection_handler/mod.rs | 10 +- .../src/node/client_handling/websocket/mod.rs | 2 +- gateway/src/node/mod.rs | 7 +- nym-node/src/config/gateway_tasks.rs | 7 +- nym-node/src/config/helpers.rs | 1 + 23 files changed, 623 insertions(+), 151 deletions(-) create mode 100644 common/gateway-requests/src/types/text_request/authenticate.rs rename common/gateway-requests/src/types/{text_request.rs => text_request/mod.rs} (89%) create mode 100644 common/gateway-storage/migrations/20250303120000_add_authentication_timestamp.sql create mode 100644 gateway/src/node/client_handling/websocket/connection_handler/helpers.rs diff --git a/Cargo.lock b/Cargo.lock index 1d72eae859..179dc53b81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5835,13 +5835,16 @@ dependencies = [ "nym-credentials-interface", "nym-crypto", "nym-pemstore", + "nym-serde-helpers", "nym-sphinx", "nym-task", "rand 0.8.5", "serde", "serde_json", "strum 0.26.3", + "subtle 2.6.1", "thiserror 2.0.11", + "time", "tokio", "tracing", "tungstenite 0.20.1", diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index b72c7a033c..88a55dd850 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -20,8 +20,8 @@ use nym_credentials_interface::TicketType; use nym_crypto::asymmetric::identity; use nym_gateway_requests::registration::handshake::client_handshake; use nym_gateway_requests::{ - BinaryRequest, ClientControlRequest, ClientRequest, SensitiveServerResponse, ServerResponse, - SharedGatewayKey, SharedSymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION, + BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersionExt, + SensitiveServerResponse, ServerResponse, SharedGatewayKey, SharedSymmetricKey, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION, }; use nym_sphinx::forwarding::packet::MixPacket; @@ -563,28 +563,10 @@ impl GatewayClient { Ok(zeroizing_updated_key) } - async fn authenticate(&mut self) -> Result<(), GatewayClientError> { - let Some(shared_key) = self.shared_key.as_ref() else { - return Err(GatewayClientError::NoSharedKeyAvailable); - }; - - if !self.connection.is_established() { - return Err(GatewayClientError::ConnectionNotEstablished); - } - debug!("authenticating with gateway"); - - let self_address = self - .local_identity - .as_ref() - .public_key() - .derive_destination_address(); - - let msg = ClientControlRequest::new_authenticate( - self_address, - shared_key, - self.cfg.bandwidth.require_tickets, - )?; - + async fn send_authenticate_request_and_handle_response( + &mut self, + msg: ClientControlRequest, + ) -> Result<(), GatewayClientError> { match self.send_websocket_message(msg).await? { ServerResponse::Authenticate { protocol_version, @@ -608,6 +590,51 @@ impl GatewayClient { } } + async fn authenticate_v1(&mut self) -> Result<(), GatewayClientError> { + debug!("using v1 authentication"); + + let Some(shared_key) = self.shared_key.as_ref() else { + return Err(GatewayClientError::NoSharedKeyAvailable); + }; + + let self_address = self + .local_identity + .public_key() + .derive_destination_address(); + + let msg = ClientControlRequest::new_authenticate( + self_address, + shared_key, + self.cfg.bandwidth.require_tickets, + )?; + self.send_authenticate_request_and_handle_response(msg) + .await + } + + async fn authenticate_v2(&mut self) -> Result<(), GatewayClientError> { + debug!("using v2 authentication"); + let Some(shared_key) = self.shared_key.as_ref() else { + return Err(GatewayClientError::NoSharedKeyAvailable); + }; + + let msg = ClientControlRequest::new_authenticate_v2(shared_key, &self.local_identity)?; + self.send_authenticate_request_and_handle_response(msg) + .await + } + + async fn authenticate(&mut self, use_v2: bool) -> Result<(), GatewayClientError> { + if !self.connection.is_established() { + return Err(GatewayClientError::ConnectionNotEstablished); + } + debug!("authenticating with gateway"); + + if use_v2 { + self.authenticate_v2().await + } else { + self.authenticate_v1().await + } + } + /// Helper method to either call register or authenticate based on self.shared_key value #[instrument(skip_all, fields( @@ -623,19 +650,25 @@ impl GatewayClient { } // 1. check gateway's protocol version - let supports_aes_gcm_siv = match self.get_gateway_protocol().await { - Ok(protocol) => protocol >= AES_GCM_SIV_PROTOCOL_VERSION, + let gw_protocol = match self.get_gateway_protocol().await { + Ok(protocol) => Some(protocol), Err(_) => { // if we failed to send the request, it means the gateway is running the old binary, // so it has reset our connection - we have to reconnect self.establish_connection().await?; - false + None } }; + let supports_aes_gcm_siv = gw_protocol.supports_aes256_gcm_siv(); + let supports_auth_v2 = gw_protocol.supports_authenticate_v2(); + if !supports_aes_gcm_siv { warn!("this gateway is on an old version that doesn't support AES256-GCM-SIV"); } + if !supports_auth_v2 { + warn!("this gateway is on an old version that doesn't support authentication v2") + } if self.authenticated { debug!("Already authenticated"); @@ -650,7 +683,7 @@ impl GatewayClient { } if self.shared_key.is_some() { - self.authenticate().await?; + self.authenticate(supports_auth_v2).await?; if self.authenticated { // if we are authenticated it means we MUST have an associated shared_key @@ -983,7 +1016,8 @@ impl GatewayClient { } // if we're reconnecting, because we lost connection, we need to re-authenticate the connection - self.authenticate().await?; + self.authenticate(self.negotiated_protocol.supports_authenticate_v2()) + .await?; // this call is NON-blocking self.start_listening_for_mixnet_messages()?; diff --git a/common/gateway-requests/Cargo.toml b/common/gateway-requests/Cargo.toml index b461c428c1..3448184eae 100644 --- a/common/gateway-requests/Cargo.toml +++ b/common/gateway-requests/Cargo.toml @@ -20,11 +20,14 @@ serde_json = { workspace = true } strum = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true, features = ["log"] } +time = { workspace = true } +subtle = { workspace = true } zeroize = { workspace = true } nym-crypto = { path = "../crypto", features = ["aead", "hashing"] } nym-pemstore = { path = "../pemstore" } nym-sphinx = { path = "../nymsphinx" } +nym-serde-helpers = { path = "../serde-helpers", features = ["base64"] } nym-task = { path = "../task" } nym-credentials = { path = "../credentials" } diff --git a/common/gateway-requests/src/authentication/encrypted_address.rs b/common/gateway-requests/src/authentication/encrypted_address.rs index 8b81074454..772b8ca91b 100644 --- a/common/gateway-requests/src/authentication/encrypted_address.rs +++ b/common/gateway-requests/src/authentication/encrypted_address.rs @@ -15,6 +15,12 @@ use thiserror::Error; // this is no longer constant size due to the differences in ciphertext between aes128ctr and aes256gcm-siv (inclusion of tag) pub struct EncryptedAddressBytes(Vec); +impl From> for EncryptedAddressBytes { + fn from(encrypted_address: Vec) -> Self { + EncryptedAddressBytes(encrypted_address) + } +} + #[derive(Debug, Error)] pub enum EncryptedAddressConversionError { #[error("Failed to decode the encrypted address - {0}")] diff --git a/common/gateway-requests/src/lib.rs b/common/gateway-requests/src/lib.rs index b7af197cc5..0d300bc4a4 100644 --- a/common/gateway-requests/src/lib.rs +++ b/common/gateway-requests/src/lib.rs @@ -19,7 +19,7 @@ pub use shared_key::{ SharedGatewayKey, SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey, }; -pub const CURRENT_PROTOCOL_VERSION: u8 = AES_GCM_SIV_PROTOCOL_VERSION; +pub const CURRENT_PROTOCOL_VERSION: u8 = AUTHENTICATE_V2_PROTOCOL_VERSION; /// Defines the current version of the communication protocol between gateway and clients. /// It has to be incremented for any breaking change. @@ -27,10 +27,29 @@ pub const CURRENT_PROTOCOL_VERSION: u8 = AES_GCM_SIV_PROTOCOL_VERSION; // 1 - initial release // 2 - changes to client credentials structure // 3 - change to AES-GCM-SIV and non-zero IVs +// 4 - introduction of v2 authentication protocol to prevent reply attacks pub const INITIAL_PROTOCOL_VERSION: u8 = 1; pub const CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION: u8 = 2; pub const AES_GCM_SIV_PROTOCOL_VERSION: u8 = 3; +pub const AUTHENTICATE_V2_PROTOCOL_VERSION: u8 = 4; // TODO: could using `Mac` trait here for OutputSize backfire? // Should hmac itself be exposed, imported and used instead? pub type LegacyGatewayMacSize = ::OutputSize; + +pub trait GatewayProtocolVersionExt { + fn supports_aes256_gcm_siv(&self) -> bool; + fn supports_authenticate_v2(&self) -> bool; +} + +impl GatewayProtocolVersionExt for Option { + fn supports_aes256_gcm_siv(&self) -> bool { + let Some(protocol) = *self else { return false }; + protocol >= AES_GCM_SIV_PROTOCOL_VERSION + } + + fn supports_authenticate_v2(&self) -> bool { + let Some(protocol) = *self else { return false }; + protocol >= AUTHENTICATE_V2_PROTOCOL_VERSION + } +} diff --git a/common/gateway-requests/src/types/error.rs b/common/gateway-requests/src/types/error.rs index 31d94b75b7..0a785e60ec 100644 --- a/common/gateway-requests/src/types/error.rs +++ b/common/gateway-requests/src/types/error.rs @@ -3,6 +3,7 @@ use crate::SharedKeyUsageError; use nym_credentials_interface::CompactEcashError; +use nym_crypto::asymmetric::ed25519::SignatureError; use nym_sphinx::addressing::nodes::NymNodeRoutingAddressError; use nym_sphinx::forwarding::packet::MixPacketFormattingError; use nym_sphinx::params::packet_sizes::PacketSize; @@ -92,7 +93,34 @@ pub enum GatewayRequestsError { #[error("the provided [v1] credential has invalid number of parameters - {0}")] InvalidNumberOfEmbededParameters(u32), + #[error("failed to authenticate the client: {0}")] + Authentication(#[from] AuthenticationFailure), + // variant to catch legacy errors #[error("{0}")] Other(String), } + +#[derive(Debug, Error)] +pub enum AuthenticationFailure { + #[error(transparent)] + KeyUsageFailure(#[from] SharedKeyUsageError), + + #[error("failed to verify provided address ciphertext")] + MalformedCiphertext, + + #[error("failed to verify request signature")] + InvalidSignature(#[from] SignatureError), + + #[error("provided request timestamp is in the future")] + RequestTimestampInFuture, + + #[error("the client is not registered")] + NotRegistered, + + #[error("the provided request is too stale to process")] + StaleRequest, + + #[error("the provided request timestamp is smaller or equal to a one previously used")] + RequestReuse, +} diff --git a/common/gateway-requests/src/types/text_request/authenticate.rs b/common/gateway-requests/src/types/text_request/authenticate.rs new file mode 100644 index 0000000000..6015beb8ad --- /dev/null +++ b/common/gateway-requests/src/types/text_request/authenticate.rs @@ -0,0 +1,142 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{AuthenticationFailure, GatewayRequestsError, SharedGatewayKey}; +use nym_crypto::asymmetric::ed25519; +use serde::{Deserialize, Serialize}; +use std::iter; +use std::time::Duration; +use subtle::ConstantTimeEq; +use time::OffsetDateTime; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct AuthenticateRequest { + #[serde(flatten)] + pub content: AuthenticateRequestContent, + + pub request_signature: ed25519::Signature, +} + +impl AuthenticateRequest { + pub fn new( + protocol_version: u8, + shared_key: &SharedGatewayKey, + identity_keys: &ed25519::KeyPair, + ) -> Result { + let content = AuthenticateRequestContent::new( + protocol_version, + shared_key, + *identity_keys.public_key(), + )?; + let plaintext = content.plaintext(); + let request_signature = identity_keys.private_key().sign(&plaintext); + + Ok(AuthenticateRequest { + content, + request_signature, + }) + } + + pub fn verify_timestamp(&self, max_request_age: Duration) -> Result<(), AuthenticationFailure> { + let now = OffsetDateTime::now_utc(); + if self.content.request_timestamp() + max_request_age < now { + return Err(AuthenticationFailure::StaleRequest); + } + if self.content.request_timestamp() > now { + return Err(AuthenticationFailure::RequestTimestampInFuture); + } + Ok(()) + } + + pub fn ensure_timestamp_not_reused( + &self, + previous: OffsetDateTime, + ) -> Result<(), AuthenticationFailure> { + if self.content.request_timestamp() <= previous { + return Err(AuthenticationFailure::RequestReuse); + } + Ok(()) + } + + pub fn verify_ciphertext( + &self, + shared_key: &SharedGatewayKey, + ) -> Result<(), AuthenticationFailure> { + let expected = shared_key.encrypt( + self.content + .client_identity + .derive_destination_address() + .as_bytes_ref(), + Some(&self.content.nonce), + )?; + + if !bool::from(expected.ct_eq(&self.content.address_ciphertext)) { + return Err(AuthenticationFailure::MalformedCiphertext); + } + Ok(()) + } + + pub fn verify_signature(&self) -> Result<(), AuthenticationFailure> { + let plaintext = self.content.plaintext(); + self.content + .client_identity + .verify(plaintext, &self.request_signature) + .map_err(Into::into) + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct AuthenticateRequestContent { + pub protocol_version: u8, + + // this is identical to the client's address + pub client_identity: ed25519::PublicKey, + + #[serde(with = "nym_serde_helpers::base64")] + pub address_ciphertext: Vec, + + #[serde(with = "nym_serde_helpers::base64")] + pub nonce: Vec, + + pub request_unix_timestamp: u64, +} + +impl AuthenticateRequestContent { + fn new( + protocol_version: u8, + shared_key: &SharedGatewayKey, + client_identity: ed25519::PublicKey, + ) -> Result { + let nonce = shared_key.random_nonce_or_iv(); + let destination_address = client_identity.derive_destination_address(); + + let address_ciphertext = + shared_key.encrypt(destination_address.as_bytes_ref(), Some(&nonce))?; + let now = OffsetDateTime::now_utc(); + Ok(AuthenticateRequestContent { + protocol_version, + client_identity, + address_ciphertext, + nonce, + request_unix_timestamp: now.unix_timestamp() as u64, // SAFETY: we're running this in post 1970... + }) + } +} + +impl AuthenticateRequestContent { + pub fn plaintext(&self) -> Vec { + iter::once(self.protocol_version) + .chain(self.client_identity.to_bytes()) + .chain(self.address_ciphertext.iter().copied()) + .chain(self.nonce.iter().copied()) + .chain(self.request_unix_timestamp.to_be_bytes()) + .collect() + } + + pub fn request_timestamp(&self) -> OffsetDateTime { + OffsetDateTime::from_unix_timestamp(self.request_unix_timestamp as i64) + .unwrap_or(OffsetDateTime::UNIX_EPOCH) + } +} diff --git a/common/gateway-requests/src/types/text_request.rs b/common/gateway-requests/src/types/text_request/mod.rs similarity index 89% rename from common/gateway-requests/src/types/text_request.rs rename to common/gateway-requests/src/types/text_request/mod.rs index 0fb864a533..7182dd834e 100644 --- a/common/gateway-requests/src/types/text_request.rs +++ b/common/gateway-requests/src/types/text_request/mod.rs @@ -2,16 +2,21 @@ // SPDX-License-Identifier: Apache-2.0 use crate::models::CredentialSpendingRequest; +use crate::text_request::authenticate::AuthenticateRequest; use crate::{ GatewayRequestsError, SharedGatewayKey, SymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION, - CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION, + AUTHENTICATE_V2_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, + INITIAL_PROTOCOL_VERSION, }; use nym_credentials_interface::CredentialSpendingData; +use nym_crypto::asymmetric::ed25519; use nym_sphinx::DestinationAddressBytes; use serde::{Deserialize, Serialize}; use std::str::FromStr; use tungstenite::Message; +pub mod authenticate; + // wrapper for all encrypted requests for ease of use #[derive(Serialize, Deserialize, Debug, Clone)] #[non_exhaustive] @@ -68,6 +73,9 @@ pub enum ClientControlRequest { enc_address: String, iv: String, }, + + AuthenticateV2(Box), + #[serde(alias = "handshakePayload")] RegisterHandshakeInitRequest { #[serde(default)] @@ -123,9 +131,22 @@ impl ClientControlRequest { }) } + pub fn new_authenticate_v2( + shared_key: &SharedGatewayKey, + identity_keys: &ed25519::KeyPair, + ) -> Result { + // if we're using v2 authentication, we must announce at least that protocol version + let protocol_version = AUTHENTICATE_V2_PROTOCOL_VERSION; + + Ok(ClientControlRequest::AuthenticateV2(Box::new( + AuthenticateRequest::new(protocol_version, shared_key, identity_keys)?, + ))) + } + pub fn name(&self) -> String { match self { ClientControlRequest::Authenticate { .. } => "Authenticate".to_string(), + ClientControlRequest::AuthenticateV2(..) => "AuthenticateV2".to_string(), ClientControlRequest::RegisterHandshakeInitRequest { .. } => { "RegisterHandshakeInitRequest".to_string() } diff --git a/common/gateway-storage/migrations/20250303120000_add_authentication_timestamp.sql b/common/gateway-storage/migrations/20250303120000_add_authentication_timestamp.sql new file mode 100644 index 0000000000..e211803e9e --- /dev/null +++ b/common/gateway-storage/migrations/20250303120000_add_authentication_timestamp.sql @@ -0,0 +1,7 @@ +/* + * Copyright 2025 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +ALTER TABLE shared_keys + ADD COLUMN last_used_authentication TIMESTAMP WITHOUT TIME ZONE; \ No newline at end of file diff --git a/common/gateway-storage/src/lib.rs b/common/gateway-storage/src/lib.rs index 5f289ec106..2d05d43fe7 100644 --- a/common/gateway-storage/src/lib.rs +++ b/common/gateway-storage/src/lib.rs @@ -200,6 +200,20 @@ impl GatewayStorage { Ok(()) } + pub async fn update_last_used_authentication_timestamp( + &self, + client_id: i64, + last_used_authentication_timestamp: OffsetDateTime, + ) -> Result<(), GatewayStorageError> { + self.shared_key_manager + .update_last_used_authentication_timestamp( + client_id, + last_used_authentication_timestamp, + ) + .await?; + Ok(()) + } + pub async fn get_client(&self, client_id: i64) -> Result, GatewayStorageError> { let client = self.client_manager.get_client(client_id).await?; Ok(client) diff --git a/common/gateway-storage/src/models.rs b/common/gateway-storage/src/models.rs index c623f72602..665b8fee47 100644 --- a/common/gateway-storage/src/models.rs +++ b/common/gateway-storage/src/models.rs @@ -14,13 +14,13 @@ pub struct Client { #[derive(FromRow)] pub struct PersistedSharedKeys { - #[allow(dead_code)] pub client_id: i64, #[allow(dead_code)] pub client_address_bs58: String, pub derived_aes128_ctr_blake3_hmac_keys_bs58: Option, pub derived_aes256_gcm_siv_key: Option>, + pub last_used_authentication: Option, } impl TryFrom for SharedGatewayKey { diff --git a/common/gateway-storage/src/shared_keys.rs b/common/gateway-storage/src/shared_keys.rs index 9d17535fb2..a7ae832cac 100644 --- a/common/gateway-storage/src/shared_keys.rs +++ b/common/gateway-storage/src/shared_keys.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::models::PersistedSharedKeys; +use time::OffsetDateTime; #[derive(Clone)] pub(crate) struct SharedKeysManager { @@ -68,6 +69,22 @@ impl SharedKeysManager { Ok(()) } + pub(crate) async fn update_last_used_authentication_timestamp( + &self, + client_id: i64, + last_used: OffsetDateTime, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE shared_keys SET last_used_authentication = ? WHERE client_id = ?;", + last_used, + client_id + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + /// Tries to retrieve shared keys stored for the particular client. /// /// # Arguments @@ -77,13 +94,10 @@ impl SharedKeysManager { &self, client_address_bs58: &str, ) -> Result, sqlx::Error> { - sqlx::query_as!( - PersistedSharedKeys, - "SELECT * FROM shared_keys WHERE client_address_bs58 = ?", - client_address_bs58 - ) - .fetch_optional(&self.connection_pool) - .await + sqlx::query_as("SELECT * FROM shared_keys WHERE client_address_bs58 = ?") + .bind(client_address_bs58) + .fetch_optional(&self.connection_pool) + .await } /// Removes from the database shared keys derived with the particular client. diff --git a/gateway/src/config.rs b/gateway/src/config.rs index 66d6180f31..9039dd1102 100644 --- a/gateway/src/config.rs +++ b/gateway/src/config.rs @@ -98,6 +98,9 @@ pub struct Debug { pub stale_messages_max_age: Duration, pub zk_nym_tickets: ZkNymTicketHandlerDebug, + + /// Defines the maximum age of a signed authentication request before it's deemed too stale to process. + pub maximum_auth_request_age: Duration, } #[derive(Debug, Clone)] diff --git a/gateway/src/node/client_handling/active_clients.rs b/gateway/src/node/client_handling/active_clients.rs index 4a3e55bef5..eeff95d907 100644 --- a/gateway/src/node/client_handling/active_clients.rs +++ b/gateway/src/node/client_handling/active_clients.rs @@ -6,11 +6,20 @@ use crate::node::client_handling::embedded_clients::LocalEmbeddedClientHandle; use dashmap::DashMap; use nym_sphinx::DestinationAddressBytes; use std::sync::Arc; +use time::OffsetDateTime; use tracing::warn; +#[derive(Clone)] +pub(crate) struct RemoteClientData { + // note, this does **NOT** indicate timestamp of when client connected + // it is (for v2 auth) timestamp the client **signed** when it created the request + pub(crate) session_request_timestamp: OffsetDateTime, + pub(crate) channels: ClientIncomingChannels, +} + enum ActiveClient { /// Handle to a remote client connected via a network socket. - Remote(ClientIncomingChannels), + Remote(RemoteClientData), /// Handle to a locally (inside the same process) running client. Embedded(LocalEmbeddedClientHandle), @@ -19,14 +28,14 @@ enum ActiveClient { impl ActiveClient { fn get_sender_ref(&self) -> &MixMessageSender { match self { - ActiveClient::Remote(remote) => &remote.mix_message_sender, + ActiveClient::Remote(remote) => &remote.channels.mix_message_sender, ActiveClient::Embedded(embedded) => &embedded.mix_message_sender, } } fn get_sender(&self) -> MixMessageSender { match self { - ActiveClient::Remote(remote) => remote.mix_message_sender.clone(), + ActiveClient::Remote(remote) => remote.channels.mix_message_sender.clone(), ActiveClient::Embedded(embedded) => embedded.mix_message_sender.clone(), } } @@ -78,18 +87,18 @@ impl ActiveClientsStore { pub(crate) fn get_remote_client( &self, address: DestinationAddressBytes, - ) -> Option { + ) -> Option { let entry = self.inner.get(&address)?; let handle = entry.value(); - let ActiveClient::Remote(channels) = handle else { + let ActiveClient::Remote(remote) = handle else { warn!("attempted to get a remote handle to a embedded network requester"); return None; }; // if the entry is stale, remove it from the map - if !channels.mix_message_sender.is_closed() { - Some(channels.clone()) + if !remote.channels.mix_message_sender.is_closed() { + Some(remote.clone()) } else { // drop the reference to the map to prevent deadlocks drop(entry); @@ -137,10 +146,14 @@ impl ActiveClientsStore { client: DestinationAddressBytes, handle: MixMessageSender, is_active_request_sender: IsActiveRequestSender, + session_request_timestamp: OffsetDateTime, ) { - let entry = ActiveClient::Remote(ClientIncomingChannels { - mix_message_sender: handle, - is_active_request_sender, + let entry = ActiveClient::Remote(RemoteClientData { + session_request_timestamp, + channels: ClientIncomingChannels { + mix_message_sender: handle, + is_active_request_sender, + }, }); if self.inner.insert(client, entry).is_some() { panic!("inserted a duplicate remote client") diff --git a/gateway/src/node/client_handling/websocket/common_state.rs b/gateway/src/node/client_handling/websocket/common_state.rs index 223c534b26..a571117895 100644 --- a/gateway/src/node/client_handling/websocket/common_state.rs +++ b/gateway/src/node/client_handling/websocket/common_state.rs @@ -9,15 +9,23 @@ use nym_mixnet_client::forwarder::MixForwardingSender; use nym_node_metrics::events::MetricEventsSender; use nym_node_metrics::NymNodeMetrics; use std::sync::Arc; +use std::time::Duration; + +#[derive(Clone)] +pub(crate) struct Config { + pub(crate) enforce_zk_nym: bool, + pub(crate) max_auth_request_age: Duration, + + pub(crate) bandwidth: BandwidthFlushingBehaviourConfig, +} // I can see this being possible expanded with say storage or client store #[derive(Clone)] pub(crate) struct CommonHandlerState { + pub(crate) cfg: Config, pub(crate) ecash_verifier: Arc, pub(crate) storage: GatewayStorage, pub(crate) local_identity: Arc, - pub(crate) only_coconut_credentials: bool, - pub(crate) bandwidth_cfg: BandwidthFlushingBehaviourConfig, pub(crate) metrics: NymNodeMetrics, pub(crate) metrics_sender: MetricEventsSender, pub(crate) outbound_mix_sender: MixForwardingSender, diff --git a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs index 6c39b613ce..16c5a42faa 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -194,8 +194,8 @@ impl AuthenticatedHandler { fresh.shared_state.storage.clone(), ClientBandwidth::new(bandwidth.into()), client.id, - fresh.shared_state.bandwidth_cfg, - fresh.shared_state.only_coconut_credentials, + fresh.shared_state.cfg.bandwidth, + fresh.shared_state.cfg.enforce_zk_nym, ), inner: fresh, client, diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index 818aea8fdb..4c0eb81f35 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -1,11 +1,13 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node::client_handling::active_clients::RemoteClientData; use crate::node::client_handling::websocket::common_state::CommonHandlerState; +use crate::node::client_handling::websocket::connection_handler::helpers::KeyWithAuthTimestamp; use crate::node::client_handling::websocket::connection_handler::INITIAL_MESSAGE_TIMEOUT; use crate::node::client_handling::websocket::{ connection_handler::{AuthenticatedHandler, ClientDetails, InitialAuthResult, SocketStream}, - message_receiver::{IsActive, IsActiveRequestSender}, + message_receiver::IsActive, }; use futures::{ channel::{mpsc, oneshot}, @@ -13,14 +15,16 @@ use futures::{ }; use nym_credentials_interface::AvailableBandwidth; use nym_crypto::aes::cipher::crypto_common::rand_core::RngCore; -use nym_crypto::asymmetric::identity; +use nym_crypto::asymmetric::ed25519; +use nym_gateway_requests::authenticate::AuthenticateRequest; use nym_gateway_requests::authentication::encrypted_address::{ EncryptedAddressBytes, EncryptedAddressConversionError, }; use nym_gateway_requests::{ registration::handshake::{error::HandshakeError, gateway_handshake}, types::{ClientControlRequest, ServerResponse}, - BinaryResponse, SharedGatewayKey, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION, + AuthenticationFailure, BinaryResponse, SharedGatewayKey, CURRENT_PROTOCOL_VERSION, + INITIAL_PROTOCOL_VERSION, }; use nym_gateway_storage::error::GatewayStorageError; use nym_node_metrics::events::MetricsEvent; @@ -30,6 +34,7 @@ use rand::CryptoRng; use std::net::SocketAddr; use std::time::Duration; use thiserror::Error; +use time::OffsetDateTime; use tokio::io::{AsyncRead, AsyncWrite}; use tokio::time::timeout; use tokio_tungstenite::tungstenite::{protocol::Message, Error as WsError}; @@ -37,6 +42,12 @@ use tracing::*; #[derive(Debug, Error)] pub(crate) enum InitialAuthenticationError { + #[error(transparent)] + AuthenticationFailure(#[from] AuthenticationFailure), + + #[error("attempted to overwrite client session with a stale authentication")] + StaleSessionOverwrite, + #[error("Internal gateway storage error")] StorageError(#[from] GatewayStorageError), @@ -290,15 +301,15 @@ impl FreshHandler { // of doing full parse of the init_data elsewhere fn extract_remote_identity_from_register_init( init_data: &[u8], - ) -> Result { - if init_data.len() < identity::PUBLIC_KEY_LENGTH { + ) -> Result { + if init_data.len() < ed25519::PUBLIC_KEY_LENGTH { Err(InitialAuthenticationError::HandshakeError( HandshakeError::MalformedRequest, )) } else { - identity::PublicKey::from_bytes(&init_data[..identity::PUBLIC_KEY_LENGTH]).map_err( - |_| InitialAuthenticationError::HandshakeError(HandshakeError::MalformedRequest), - ) + ed25519::PublicKey::from_bytes(&init_data[..ed25519::PUBLIC_KEY_LENGTH]).map_err(|_| { + InitialAuthenticationError::HandshakeError(HandshakeError::MalformedRequest) + }) } } @@ -351,6 +362,21 @@ impl FreshHandler { Ok(()) } + async fn retrieve_shared_key( + &self, + client: DestinationAddressBytes, + ) -> Result, InitialAuthenticationError> { + let shared_keys = self.shared_state.storage.get_shared_keys(client).await?; + + let Some(stored_shared_keys) = shared_keys else { + return Ok(None); + }; + + let keys = KeyWithAuthTimestamp::try_from_stored(stored_shared_keys, client)?; + + Ok(Some(keys)) + } + /// Checks whether the stored shared keys match the received data, i.e. whether the upon decryption /// the provided encrypted address matches the expected unencrypted address. /// @@ -361,31 +387,18 @@ impl FreshHandler { /// * `client_address`: address of the client. /// * `encrypted_address`: encrypted address of the client, presumably encrypted using the shared keys. /// * `iv`: nonce/iv created for this particular encryption. - async fn verify_stored_shared_key( + async fn auth_v1_verify_stored_shared_key( &self, client_address: DestinationAddressBytes, encrypted_address: EncryptedAddressBytes, nonce: &[u8], - ) -> Result, InitialAuthenticationError> { - let shared_keys = self - .shared_state - .storage - .get_shared_keys(client_address) - .await?; - - let Some(stored_shared_keys) = shared_keys else { + ) -> Result, InitialAuthenticationError> { + let Some(keys) = self.retrieve_shared_key(client_address).await? else { return Ok(None); }; - let keys = SharedGatewayKey::try_from(stored_shared_keys).map_err(|source| { - InitialAuthenticationError::MalformedStoredSharedKey { - client_id: client_address.as_base58_string(), - source, - } - })?; - // LEGACY ISSUE: we're not verifying HMAC key - if encrypted_address.verify(&client_address, &keys, nonce) { + if encrypted_address.verify(&client_address, &keys.key, nonce) { Ok(Some(keys)) } else { Ok(None) @@ -428,49 +441,19 @@ impl FreshHandler { } } - /// Using the received challenge data, i.e. client's address as well the ciphertext of it plus - /// a fresh IV, attempts to authenticate the client by checking whether the ciphertext matches - /// the expected value if encrypted with the shared key. - /// - /// Finally, upon completion, all previously stored messages are pushed back to the client. - /// - /// # Arguments - /// - /// * `client_address`: address of the client wishing to authenticate. - /// * `encrypted_address`: ciphertext of the address of the client wishing to authenticate. - /// * `iv`: fresh nonce/IV received with the request. - async fn authenticate_client( - &mut self, - client_address: DestinationAddressBytes, - encrypted_address: EncryptedAddressBytes, - nonce: &[u8], - ) -> Result, InitialAuthenticationError> - where - S: AsyncRead + AsyncWrite + Unpin, - { - debug!( - "Processing authenticate client request for: {}", - client_address.as_base58_string() - ); - - let shared_keys = self - .verify_stored_shared_key(client_address, encrypted_address, nonce) - .await?; - - if let Some(shared_keys) = shared_keys { - self.push_stored_messages_to_client(client_address, &shared_keys) - .await?; - Ok(Some(shared_keys)) - } else { - Ok(None) - } - } - async fn handle_duplicate_client( &mut self, address: DestinationAddressBytes, - mut is_active_request_tx: IsActiveRequestSender, + remote_client_data: RemoteClientData, + new_session_start: OffsetDateTime, ) -> Result<(), InitialAuthenticationError> { + let mut is_active_request_tx = remote_client_data.channels.is_active_request_sender; + + // new session must **always** be explicitly more recent + if new_session_start <= remote_client_data.session_request_timestamp { + return Err(InitialAuthenticationError::StaleSessionOverwrite); + } + // Ask the other connection to ping if they are still active. // Use a oneshot channel to return the result to us let (ping_result_sender, ping_result_receiver) = oneshot::channel(); @@ -519,6 +502,32 @@ impl FreshHandler { Ok(()) } + #[allow(dead_code)] + async fn get_registered_client_id( + &self, + client_address: DestinationAddressBytes, + ) -> Result { + self.shared_state + .storage + .get_mixnet_client_id(client_address) + .await + .map_err(Into::into) + } + + async fn get_registered_available_bandwidth( + &self, + client_id: i64, + ) -> Result { + let available_bandwidth: AvailableBandwidth = self + .shared_state + .storage + .get_available_bandwidth(client_id) + .await? + .map(From::from) + .unwrap_or_default(); + Ok(available_bandwidth) + } + /// Tries to handle the received authentication request by checking correctness of the received data. /// /// # Arguments @@ -531,7 +540,7 @@ impl FreshHandler { address = %address, ) )] - async fn handle_authenticate( + async fn handle_legacy_authenticate( &mut self, client_protocol_version: Option, address: String, @@ -541,7 +550,7 @@ impl FreshHandler { where S: AsyncRead + AsyncWrite + Unpin, { - debug!("handling client registration"); + debug!("handling client authentication (v1)"); let negotiated_protocol = self.negotiate_client_protocol(client_protocol_version)?; // populate the negotiated protocol for future uses @@ -554,38 +563,38 @@ impl FreshHandler { .into_vec() .map_err(InitialAuthenticationError::MalformedIV)?; - // Check for duplicate clients - if let Some(client_tx) = self - .shared_state - .active_clients_store - .get_remote_client(address) - { - warn!("Detected duplicate connection for client: {address}"); - self.handle_duplicate_client(address, client_tx.is_active_request_sender) - .await?; - } - + // validate the shared key let Some(shared_keys) = self - .authenticate_client(address, encrypted_address, &nonce) + .auth_v1_verify_stored_shared_key(address, encrypted_address, &nonce) .await? else { // it feels weird to be returning an 'Ok' here, but I didn't want to change the existing behaviour return Ok(InitialAuthResult::new_failed(Some(negotiated_protocol))); }; - let client_id = self + // in v1 we don't have explicit data so we have to use current timestamp + // (which does nothing but just allows us to use the same codepath) + let session_request_start = OffsetDateTime::now_utc(); + + // Check for duplicate clients + if let Some(remote_client_data) = self .shared_state - .storage - .get_mixnet_client_id(address) + .active_clients_store + .get_remote_client(address) + { + warn!("Detected duplicate connection for client: {address}"); + self.handle_duplicate_client(address, remote_client_data, session_request_start) + .await?; + } + + let client_id = shared_keys.client_id; + + // if applicable, push stored messages + self.push_stored_messages_to_client(address, &shared_keys.key) .await?; - let available_bandwidth: AvailableBandwidth = self - .shared_state - .storage - .get_available_bandwidth(client_id) - .await? - .map(From::from) - .unwrap_or_default(); + // check the bandwidth + let available_bandwidth = self.get_registered_available_bandwidth(client_id).await?; let bandwidth_remaining = if available_bandwidth.expired() { self.shared_state.storage.reset_bandwidth(client_id).await?; @@ -595,7 +604,98 @@ impl FreshHandler { }; Ok(InitialAuthResult::new( - Some(ClientDetails::new(client_id, address, shared_keys)), + Some(ClientDetails::new( + client_id, + address, + shared_keys.key, + session_request_start, + )), + ServerResponse::Authenticate { + protocol_version: Some(negotiated_protocol), + status: true, + bandwidth_remaining, + }, + )) + } + + async fn handle_authenticate_v2( + &mut self, + request: Box, + ) -> Result + where + S: AsyncRead + AsyncWrite + Unpin, + { + debug!("handling client authentication (v2)"); + + let negotiated_protocol = + self.negotiate_client_protocol(Some(request.content.protocol_version))?; + // populate the negotiated protocol for future uses + self.negotiated_protocol = Some(negotiated_protocol); + + let address = request.content.client_identity.derive_destination_address(); + + // do cheap checks first + // is the provided timestamp relatively recent (and not in the future?) + request.verify_timestamp(self.shared_state.cfg.max_auth_request_age)?; + + // does the message signature verify? + request.verify_signature()?; + + // retrieve the actually stored key and check if the ciphertext matches + let Some(shared_key) = self.retrieve_shared_key(address).await? else { + return Err(AuthenticationFailure::NotRegistered)?; + }; + request.verify_ciphertext(&shared_key.key)?; + + let session_request_start = request.content.request_timestamp(); + + // if the client has already authenticated in the past, make sure this authentication timestamp + // is different and greater than the old one (in case it was replayed) + if let Some(prior_usage) = shared_key.last_used_authentication { + request.ensure_timestamp_not_reused(prior_usage)?; + } + + // check for duplicate clients + if let Some(client_data) = self + .shared_state + .active_clients_store + .get_remote_client(address) + { + warn!("Detected duplicate connection for client: {address}"); + self.handle_duplicate_client(address, client_data, session_request_start) + .await?; + } + + let client_id = shared_key.client_id; + + // update the auth timestamp for future uses + self.shared_state + .storage + .update_last_used_authentication_timestamp(client_id, session_request_start) + .await?; + + // push any old stored messages to the client + // (this will be removed soon) + self.push_stored_messages_to_client(address, &shared_key.key) + .await?; + + // finally check and retrieve client's bandwidth + let available_bandwidth = self.get_registered_available_bandwidth(client_id).await?; + + let bandwidth_remaining = if available_bandwidth.expired() { + self.shared_state.storage.reset_bandwidth(client_id).await?; + 0 + } else { + available_bandwidth.bytes + }; + + Ok(InitialAuthResult::new( + Some(ClientDetails::new( + client_id, + address, + shared_key.key, + session_request_start, + )), ServerResponse::Authenticate { protocol_version: Some(negotiated_protocol), status: true, @@ -688,7 +788,12 @@ impl FreshHandler { debug!(client_id = %client_id, "managed to finalize client registration"); - let client_details = ClientDetails::new(client_id, remote_address, shared_keys); + let client_details = ClientDetails::new( + client_id, + remote_address, + shared_keys, + OffsetDateTime::now_utc(), + ); Ok(InitialAuthResult::new( Some(client_details), @@ -734,9 +839,10 @@ impl FreshHandler { enc_address, iv, } => { - self.handle_authenticate(protocol_version, address, enc_address, iv) + self.handle_legacy_authenticate(protocol_version, address, enc_address, iv) .await } + ClientControlRequest::AuthenticateV2(req) => self.handle_authenticate_v2(req).await, ClientControlRequest::RegisterHandshakeInitRequest { protocol_version, data, @@ -827,6 +933,7 @@ impl FreshHandler { registration_details.address, mix_sender, is_active_request_sender, + registration_details.session_request_timestamp, ); return AuthenticatedHandler::upgrade( diff --git a/gateway/src/node/client_handling/websocket/connection_handler/helpers.rs b/gateway/src/node/client_handling/websocket/connection_handler/helpers.rs new file mode 100644 index 0000000000..5a5d24c5d6 --- /dev/null +++ b/gateway/src/node/client_handling/websocket/connection_handler/helpers.rs @@ -0,0 +1,37 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::client_handling::websocket::connection_handler::fresh::InitialAuthenticationError; +use nym_gateway_requests::SharedGatewayKey; +use nym_gateway_storage::models::PersistedSharedKeys; +use nym_sphinx::DestinationAddressBytes; +use time::OffsetDateTime; + +pub(crate) struct KeyWithAuthTimestamp { + pub(crate) client_id: i64, + pub(crate) key: SharedGatewayKey, + pub(crate) last_used_authentication: Option, +} + +impl KeyWithAuthTimestamp { + pub(crate) fn try_from_stored( + stored_shared_keys: PersistedSharedKeys, + client: DestinationAddressBytes, + ) -> Result { + let last_used_authentication = stored_shared_keys.last_used_authentication; + let client_id = stored_shared_keys.client_id; + + let key = SharedGatewayKey::try_from(stored_shared_keys).map_err(|source| { + InitialAuthenticationError::MalformedStoredSharedKey { + client_id: client.as_base58_string(), + source, + } + })?; + + Ok(KeyWithAuthTimestamp { + client_id, + key, + last_used_authentication, + }) + } +} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs index f9f6fc584d..38026fa8be 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs @@ -8,16 +8,17 @@ use nym_gateway_requests::ServerResponse; use nym_sphinx::DestinationAddressBytes; use rand::{CryptoRng, Rng}; use std::time::Duration; +use time::OffsetDateTime; use tokio::io::{AsyncRead, AsyncWrite}; use tokio_tungstenite::WebSocketStream; use tracing::{debug, instrument, trace, warn}; -use zeroize::{Zeroize, ZeroizeOnDrop}; pub(crate) use self::authenticated::AuthenticatedHandler; pub(crate) use self::fresh::FreshHandler; pub(crate) mod authenticated; mod fresh; +pub(crate) mod helpers; const WEBSOCKET_HANDSHAKE_TIMEOUT: Duration = Duration::from_millis(1_500); const INITIAL_MESSAGE_TIMEOUT: Duration = Duration::from_millis(10_000); @@ -40,12 +41,13 @@ impl SocketStream { } } -#[derive(Zeroize, ZeroizeOnDrop)] pub(crate) struct ClientDetails { - #[zeroize(skip)] pub(crate) address: DestinationAddressBytes, pub(crate) id: i64, pub(crate) shared_keys: SharedGatewayKey, + // note, this does **NOT ALWAYS** indicate timestamp of when client connected + // it is (for v2 auth) timestamp the client **signed** when it created the request + pub(crate) session_request_timestamp: OffsetDateTime, } impl ClientDetails { @@ -53,11 +55,13 @@ impl ClientDetails { id: i64, address: DestinationAddressBytes, shared_keys: SharedGatewayKey, + session_request_timestamp: OffsetDateTime, ) -> Self { ClientDetails { address, id, shared_keys, + session_request_timestamp, } } } diff --git a/gateway/src/node/client_handling/websocket/mod.rs b/gateway/src/node/client_handling/websocket/mod.rs index 12c252feca..2405870a42 100644 --- a/gateway/src/node/client_handling/websocket/mod.rs +++ b/gateway/src/node/client_handling/websocket/mod.rs @@ -8,4 +8,4 @@ pub(crate) mod connection_handler; pub(crate) mod listener; pub(crate) mod message_receiver; -pub(crate) use common_state::CommonHandlerState; +pub(crate) use common_state::{CommonHandlerState, Config}; diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 1dd6f2c217..dd4a866a2c 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -249,11 +249,14 @@ impl GatewayTasksBuilder { active_clients_store: ActiveClientsStore, ) -> Result { let shared_state = websocket::CommonHandlerState { + cfg: websocket::Config { + enforce_zk_nym: self.config.gateway.enforce_zk_nyms, + max_auth_request_age: self.config.debug.maximum_auth_request_age, + bandwidth: (&self.config).into(), + }, ecash_verifier: self.ecash_manager().await?, storage: self.storage.clone(), local_identity: Arc::clone(&self.identity_keypair), - only_coconut_credentials: self.config.gateway.enforce_zk_nyms, - bandwidth_cfg: (&self.config).into(), metrics: self.metrics.clone(), metrics_sender: self.metrics_sender.clone(), outbound_mix_sender: self.mix_packet_sender.clone(), diff --git a/nym-node/src/config/gateway_tasks.rs b/nym-node/src/config/gateway_tasks.rs index 9f814f2bf5..4d83c4632d 100644 --- a/nym-node/src/config/gateway_tasks.rs +++ b/nym-node/src/config/gateway_tasks.rs @@ -47,6 +47,9 @@ pub struct Debug { /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. pub message_retrieval_limit: i64, + /// Defines the maximum age of a signed authentication request before it's deemed too stale to process. + pub maximum_auth_request_age: Duration, + pub stale_messages: StaleMessageDebug, pub client_bandwidth: ClientBandwidthDebug, @@ -55,13 +58,15 @@ pub struct Debug { } impl Debug { - const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; + pub const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; + pub const DEFAULT_MAXIMUM_AUTH_REQUEST_AGE: Duration = Duration::from_secs(30); } impl Default for Debug { fn default() -> Self { Debug { message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + maximum_auth_request_age: Self::DEFAULT_MAXIMUM_AUTH_REQUEST_AGE, stale_messages: Default::default(), client_bandwidth: Default::default(), zk_nym_tickets: Default::default(), diff --git a/nym-node/src/config/helpers.rs b/nym-node/src/config/helpers.rs index 2afd58301c..aa2da4ae3c 100644 --- a/nym-node/src/config/helpers.rs +++ b/nym-node/src/config/helpers.rs @@ -59,6 +59,7 @@ fn ephemeral_gateway_config(config: &Config) -> nym_gateway::config::Config { .zk_nym_tickets .maximum_time_between_redemption, }, + maximum_auth_request_age: config.gateway_tasks.debug.maximum_auth_request_age, }, ) } From 94ff8a79eea7873c37cc1922f5f15d5c2d12419a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 3 Mar 2025 18:03:47 +0000 Subject: [PATCH 024/133] feature: disallow routing mix packets to nodes not present in the topology (#5526) * new NymNodeTopologyProvider to also keep track of ips of all nodes * added nym-api endpoint for nodes existence by ip * change behaviour of updating allowed nodes alongside the topology * clippy * license fix * fix default filtering limit --- .../validator-client/src/client.rs | 10 +- .../validator-client/src/nym_api/mod.rs | 22 +- .../validator-client/src/nym_api/routes.rs | 1 + common/config/src/helpers.rs | 9 - common/nymsphinx/addressing/src/nodes.rs | 6 + common/topology/src/lib.rs | 9 + nym-api/nym-api-requests/src/nym_nodes.rs | 13 + nym-api/src/node_describe_cache/mod.rs | 24 +- .../src/nym_nodes/handlers/unstable/mod.rs | 45 +- nym-node/src/cli/commands/run/mod.rs | 9 +- nym-node/src/config/gateway_tasks.rs | 6 + nym-node/src/config/mod.rs | 12 +- nym-node/src/error.rs | 6 + .../node/mixnet/packet_forwarding/global.rs | 79 ++++ .../mod.rs} | 36 +- nym-node/src/node/mod.rs | 38 +- nym-node/src/node/shared_network.rs | 434 ++++++++++++++++++ nym-node/src/node/shared_topology.rs | 106 ----- 18 files changed, 727 insertions(+), 138 deletions(-) create mode 100644 nym-node/src/node/mixnet/packet_forwarding/global.rs rename nym-node/src/node/mixnet/{packet_forwarding.rs => packet_forwarding/mod.rs} (85%) create mode 100644 nym-node/src/node/shared_network.rs delete mode 100644 nym-node/src/node/shared_topology.rs diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index e5a9a7ea8a..c41ba519d4 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -23,11 +23,12 @@ use nym_api_requests::models::{ NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse, }; use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated}; -use nym_api_requests::nym_nodes::SkimmedNode; +use nym_api_requests::nym_nodes::{NodesByAddressesResponse, SkimmedNode}; use nym_coconut_dkg_common::types::EpochId; use nym_ecash_contract_common::deposit::DepositId; use nym_http_api_client::UserAgent; use nym_network_defaults::NymNetworkDetails; +use std::net::IpAddr; use time::Date; use url::Url; @@ -710,4 +711,11 @@ impl NymApiClient { .issued_ticketbooks_challenge(expiration_date, deposits) .await?) } + + pub async fn nodes_by_addresses( + &self, + addresses: Vec, + ) -> Result { + Ok(self.nym_api.nodes_by_addresses(addresses).await?) + } } diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index e1bac67513..7011a1904a 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -15,7 +15,9 @@ use nym_api_requests::models::{ AnnotationResponse, ApiHealthResponse, LegacyDescribedMixNode, NodePerformanceResponse, NodeRefreshBody, NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse, }; -use nym_api_requests::nym_nodes::PaginatedCachedNodesResponse; +use nym_api_requests::nym_nodes::{ + NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponse, +}; use nym_api_requests::pagination::PaginatedResponse; pub use nym_api_requests::{ ecash::{ @@ -40,6 +42,7 @@ pub use nym_http_api_client::Client; use nym_http_api_client::{ApiClient, NO_PARAMS}; use nym_mixnet_contract_common::mixnode::MixNodeDetails; use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId, NymNodeDetails}; +use std::net::IpAddr; use time::format_description::BorrowedFormatItem; use time::Date; use tracing::instrument; @@ -1015,6 +1018,23 @@ pub trait NymApiClientExt: ApiClient { .await } + async fn nodes_by_addresses( + &self, + addresses: Vec, + ) -> Result { + self.post_json( + &[ + routes::API_VERSION, + "unstable", + routes::NYM_NODES_ROUTES, + routes::nym_nodes::BY_ADDRESSES, + ], + NO_PARAMS, + &NodesByAddressesRequestBody { addresses }, + ) + .await + } + #[instrument(level = "debug", skip(self))] async fn get_network_details(&self) -> Result { self.get_json( diff --git a/common/client-libs/validator-client/src/nym_api/routes.rs b/common/client-libs/validator-client/src/nym_api/routes.rs index 767acc0b8d..1dae2254f4 100644 --- a/common/client-libs/validator-client/src/nym_api/routes.rs +++ b/common/client-libs/validator-client/src/nym_api/routes.rs @@ -43,6 +43,7 @@ pub mod nym_nodes { pub const NYM_NODES_BONDED: &str = "bonded"; pub const NYM_NODES_REWARDED_SET: &str = "rewarded-set"; pub const NYM_NODES_REFRESH_DESCRIBED: &str = "refresh-described"; + pub const BY_ADDRESSES: &str = "by-addresses"; } pub const STATUS_ROUTES: &str = "status"; diff --git a/common/config/src/helpers.rs b/common/config/src/helpers.rs index cfed0dc1c7..d1ea90010b 100644 --- a/common/config/src/helpers.rs +++ b/common/config/src/helpers.rs @@ -25,15 +25,6 @@ pub fn in6addr_any_init() -> IpAddr { IpAddr::V6(Ipv6Addr::UNSPECIFIED) } -/// Helper for providing binding warnings if node tries to bind to any of those -pub const SPECIAL_ADDRESSES: &[IpAddr] = &[ - IpAddr::V4(Ipv4Addr::LOCALHOST), - IpAddr::V4(Ipv4Addr::UNSPECIFIED), - IpAddr::V4(Ipv4Addr::BROADCAST), - IpAddr::V6(Ipv6Addr::LOCALHOST), - IpAddr::V6(Ipv6Addr::UNSPECIFIED), -]; - // TODO: is it really part of 'Config'? pub trait OptionalSet { /// If the value is available (i.e. `Some`), the provided closure is applied. diff --git a/common/nymsphinx/addressing/src/nodes.rs b/common/nymsphinx/addressing/src/nodes.rs index c748a72dcd..4171410b6c 100644 --- a/common/nymsphinx/addressing/src/nodes.rs +++ b/common/nymsphinx/addressing/src/nodes.rs @@ -161,6 +161,12 @@ impl From for SocketAddr { } } +impl AsRef for NymNodeRoutingAddress { + fn as_ref(&self) -> &SocketAddr { + &self.0 + } +} + impl TryInto for NymNodeRoutingAddress { type Error = NymNodeRoutingAddressError; diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 2150d587cb..4354721bc5 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -254,6 +254,15 @@ impl NymTopology { } } + pub fn with_additional_nodes(mut self, nodes: impl Iterator) -> Self + where + N: TryInto, + >::Error: Display, + { + self.add_additional_nodes(nodes); + self + } + pub fn has_node_details(&self, node_id: NodeId) -> bool { self.node_details.contains_key(&node_id) } diff --git a/nym-api/nym-api-requests/src/nym_nodes.rs b/nym-api/nym-api-requests/src/nym_nodes.rs index 96c981c8a1..544e07a861 100644 --- a/nym-api/nym-api-requests/src/nym_nodes.rs +++ b/nym-api/nym-api-requests/src/nym_nodes.rs @@ -10,6 +10,7 @@ use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::reward_params::Performance; use nym_mixnet_contract_common::{Interval, NodeId}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::net::IpAddr; use time::OffsetDateTime; use utoipa::ToSchema; @@ -212,3 +213,15 @@ pub struct FullFatNode { // kinda temporary for now to make as few changes as possible for now pub self_described: Option, } + +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, ToSchema)] +pub struct NodesByAddressesRequestBody { + #[schema(value_type = Vec)] + pub addresses: Vec, +} + +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, ToSchema)] +pub struct NodesByAddressesResponse { + #[schema(value_type = HashMap>)] + pub existence: HashMap>, +} diff --git a/nym-api/src/node_describe_cache/mod.rs b/nym-api/src/node_describe_cache/mod.rs index 9cecb37e1e..8fa5adf452 100644 --- a/nym-api/src/node_describe_cache/mod.rs +++ b/nym-api/src/node_describe_cache/mod.rs @@ -17,6 +17,7 @@ use nym_mixnet_contract_common::{NodeId, NymNodeDetails}; use nym_node_requests::api::client::{NymNodeApiClientError, NymNodeApiClientExt}; use nym_topology::node::{RoutingNode, RoutingNodeError}; use std::collections::HashMap; +use std::net::IpAddr; use std::time::Duration; use thiserror::Error; use tracing::{debug, error, info}; @@ -84,10 +85,14 @@ impl NodeDescriptionTopologyExt for NymNodeDescription { #[derive(Debug, Clone)] pub struct DescribedNodes { nodes: HashMap, + addresses_cache: HashMap, } impl DescribedNodes { pub fn force_update(&mut self, node: NymNodeDescription) { + for ip in &node.description.host_information.ip_address { + self.addresses_cache.insert(*ip, node.node_id); + } self.nodes.insert(node.node_id, node); } @@ -129,6 +134,10 @@ impl DescribedNodes { .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) .filter(|n| n.description.declared_role.can_operate_exit_gateway()) } + + pub fn node_with_address(&self, address: IpAddr) -> Option { + self.addresses_cache.get(&address).copied() + } } pub struct NodeDescriptionProvider { @@ -396,9 +405,20 @@ impl CacheItemProvider for NodeDescriptionProvider { .collect::>() .await; - info!("refreshed self described data for {} nodes", nodes.len()); + let mut addresses_cache = HashMap::new(); + for node in nodes.values() { + for ip in &node.description.host_information.ip_address { + addresses_cache.insert(*ip, node.node_id); + } + } - Ok(DescribedNodes { nodes }) + info!("refreshed self described data for {} nodes", nodes.len()); + info!("with {} unique ip addresses", addresses_cache.len()); + + Ok(DescribedNodes { + nodes, + addresses_cache, + }) } } diff --git a/nym-api/src/nym_nodes/handlers/unstable/mod.rs b/nym-api/src/nym_nodes/handlers/unstable/mod.rs index d1ee06ed1e..f858065d2b 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/mod.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/mod.rs @@ -20,6 +20,7 @@ //! - `/mixnodes/` => only returns mixnode role data //! - `/gateway/` => only returns (entry) gateway role data +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::nym_nodes::handlers::unstable::full_fat::nodes_detailed; use crate::nym_nodes::handlers::unstable::semi_skimmed::nodes_expanded; use crate::nym_nodes::handlers::unstable::skimmed::{ @@ -29,10 +30,14 @@ use crate::nym_nodes::handlers::unstable::skimmed::{ }; use crate::support::http::helpers::PaginationRequest; use crate::support::http::state::AppState; -use axum::routing::get; -use axum::Router; -use nym_api_requests::nym_nodes::NodeRoleQueryParam; +use axum::extract::State; +use axum::routing::{get, post}; +use axum::{Json, Router}; +use nym_api_requests::nym_nodes::{ + NodeRoleQueryParam, NodesByAddressesRequestBody, NodesByAddressesResponse, +}; use serde::Deserialize; +use std::collections::HashMap; use tower_http::compression::CompressionLayer; pub(crate) mod full_fat; @@ -74,6 +79,7 @@ pub(crate) fn nym_node_routes_unstable() -> Router { .nest("/full-fat", Router::new().route("/", get(nodes_detailed))) .route("/gateways/skimmed", get(skimmed::deprecated_gateways_basic)) .route("/mixnodes/skimmed", get(skimmed::deprecated_mixnodes_basic)) + .route("/by-addresses", post(nodes_by_addresses)) .layer(CompressionLayer::new()) } @@ -129,3 +135,36 @@ impl<'a> From<&'a NodesParams> for PaginationRequest { } } } + +#[utoipa::path( + tag = "Unstable Nym Nodes", + post, + request_body = NodesByAddressesRequestBody, + path = "/by-addresses", + context_path = "/v1/unstable/nym-nodes", + responses( + (status = 200, body = NodesByAddressesResponse) + ) +)] +async fn nodes_by_addresses( + state: State, + Json(body): Json, +) -> AxumResult> { + // if the request is too big, simply reject it + if body.addresses.len() > 100 { + return Err(AxumErrorResponse::bad_request( + "requested too many addresses", + )); + } + + // TODO: perhaps introduce different cache because realistically nym-api will receive + // request for the same couple addresses from all nodes in quick succession + let describe_cache = state.describe_nodes_cache_data().await?; + + let mut existence = HashMap::new(); + for address in body.addresses { + existence.insert(address, describe_cache.node_with_address(address)); + } + + Ok(Json(NodesByAddressesResponse { existence })) +} diff --git a/nym-node/src/cli/commands/run/mod.rs b/nym-node/src/cli/commands/run/mod.rs index 9dfd1061f0..8b568150ed 100644 --- a/nym-node/src/cli/commands/run/mod.rs +++ b/nym-node/src/cli/commands/run/mod.rs @@ -4,8 +4,8 @@ use crate::config::upgrade_helpers::try_load_current_config; use crate::error::NymNodeError; use crate::node::bonding_information::BondingInformation; +use crate::node::mixnet::packet_forwarding::global::is_global_ip; use crate::node::NymNode; -use nym_config::helpers::SPECIAL_ADDRESSES; use std::fs; use std::net::IpAddr; use tracing::{debug, info, trace, warn}; @@ -17,7 +17,7 @@ pub(crate) use args::Args; fn check_public_ips(ips: &[IpAddr], local: bool) -> Result<(), NymNodeError> { let mut suspicious_ip = Vec::new(); for ip in ips { - if SPECIAL_ADDRESSES.contains(ip) { + if !is_global_ip(ip) { if !local { return Err(NymNodeError::InvalidPublicIp { address: *ip }); } @@ -92,6 +92,11 @@ pub(crate) async fn execute(mut args: Args) -> Result<(), NymNodeError> { } check_public_ips(&config.host.public_ips, local)?; + let mut config = config; + if local { + config.debug.testnet = true + } + let nym_node = NymNode::new(config) .await? .with_accepted_operator_terms_and_conditions(accepted_operator_terms_and_conditions); diff --git a/nym-node/src/config/gateway_tasks.rs b/nym-node/src/config/gateway_tasks.rs index 4d83c4632d..d35a978333 100644 --- a/nym-node/src/config/gateway_tasks.rs +++ b/nym-node/src/config/gateway_tasks.rs @@ -47,6 +47,10 @@ pub struct Debug { /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. pub message_retrieval_limit: i64, + /// Specifies the minimum performance of mixnodes in the network that are to be used in internal topologies + /// of the services providers + pub minimum_mix_performance: u8, + /// Defines the maximum age of a signed authentication request before it's deemed too stale to process. pub maximum_auth_request_age: Duration, @@ -59,6 +63,7 @@ pub struct Debug { impl Debug { pub const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; + pub const DEFAULT_MINIMUM_MIX_PERFORMANCE: u8 = 50; pub const DEFAULT_MAXIMUM_AUTH_REQUEST_AGE: Duration = Duration::from_secs(30); } @@ -67,6 +72,7 @@ impl Default for Debug { Debug { message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, maximum_auth_request_age: Self::DEFAULT_MAXIMUM_AUTH_REQUEST_AGE, + minimum_mix_performance: Self::DEFAULT_MINIMUM_MIX_PERFORMANCE, stale_messages: Default::default(), client_bandwidth: Default::default(), zk_nym_tickets: Default::default(), diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs index b46d19e370..c71f003fbc 100644 --- a/nym-node/src/config/mod.rs +++ b/nym-node/src/config/mod.rs @@ -780,16 +780,26 @@ pub struct Debug { /// Specifies the time to live of the internal topology provider cache. #[serde(with = "humantime_serde")] pub topology_cache_ttl: Duration, + + /// Specifies the time between attempting to resolve any pending unknown nodes in the routing filter + #[serde(with = "humantime_serde")] + pub routing_nodes_check_interval: Duration, + + /// Specifies whether this node runs in testnet mode thus allowing it to route packets on local interfaces + pub testnet: bool, } impl Debug { - pub const DEFAULT_TOPOLOGY_CACHE_TTL: Duration = Duration::from_secs(5 * 60); + pub const DEFAULT_TOPOLOGY_CACHE_TTL: Duration = Duration::from_secs(10 * 60); + pub const DEFAULT_ROUTING_NODES_CHECK_INTERVAL: Duration = Duration::from_secs(5 * 60); } impl Default for Debug { fn default() -> Self { Debug { topology_cache_ttl: Self::DEFAULT_TOPOLOGY_CACHE_TTL, + routing_nodes_check_interval: Self::DEFAULT_ROUTING_NODES_CHECK_INTERVAL, + testnet: false, } } } diff --git a/nym-node/src/error.rs b/nym-node/src/error.rs index 3c50451e07..5eb5545e21 100644 --- a/nym-node/src/error.rs +++ b/nym-node/src/error.rs @@ -4,6 +4,7 @@ use crate::node::http::error::NymNodeHttpError; use crate::wireguard::error::WireguardError; use nym_ip_packet_router::error::ClientCoreError; +use nym_validator_client::ValidatorClientError; use std::io; use std::net::IpAddr; use std::path::PathBuf; @@ -141,6 +142,11 @@ pub enum NymNodeError { source: ipnetwork::IpNetworkError, }, + #[error( + "failed to retrieve initial network topology - can't start the node without it: {source}" + )] + InitialTopologyQueryFailure { source: ValidatorClientError }, + #[error(transparent)] GatewayFailure(#[from] nym_gateway::GatewayError), diff --git a/nym-node/src/node/mixnet/packet_forwarding/global.rs b/nym-node/src/node/mixnet/packet_forwarding/global.rs new file mode 100644 index 0000000000..6f27444a57 --- /dev/null +++ b/nym-node/src/node/mixnet/packet_forwarding/global.rs @@ -0,0 +1,79 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +// use `ip` feature without nightly +// issue: https://github.com/rust-lang/rust/issues/27709 +pub(crate) const fn is_global_ip(ip: &IpAddr) -> bool { + match ip { + IpAddr::V4(addr) => is_global_ipv4(addr), + IpAddr::V6(addr) => is_global_ipv6(addr), + } +} + +const fn is_shared_ipv4(ip: &Ipv4Addr) -> bool { + ip.octets()[0] == 100 && (ip.octets()[1] & 0b1100_0000 == 0b0100_0000) +} + +const fn is_benchmarking_ipv4(ip: &Ipv4Addr) -> bool { + ip.octets()[0] == 198 && (ip.octets()[1] & 0xfe) == 18 +} + +const fn is_reserved_ipv4(ip: &Ipv4Addr) -> bool { + ip.octets()[0] & 240 == 240 && !ip.is_broadcast() +} + +const fn is_global_ipv4(ip: &Ipv4Addr) -> bool { + !(ip.octets()[0] == 0 // "This network" + || ip.is_private() + || is_shared_ipv4(ip) + || ip.is_loopback() + || ip.is_link_local() + // addresses reserved for future protocols (`192.0.0.0/24`) + // .9 and .10 are documented as globally reachable so they're excluded + || ( + ip.octets()[0] == 192 && ip.octets()[1] == 0 && ip.octets()[2] == 0 + && ip.octets()[3] != 9 && ip.octets()[3] != 10 + ) + || ip.is_documentation() + || is_benchmarking_ipv4(ip) + || is_reserved_ipv4(ip) + || ip.is_broadcast()) +} + +const fn is_documentation_ipv6(ip: &Ipv6Addr) -> bool { + (ip.segments()[0] == 0x2001) && (ip.segments()[1] == 0xdb8) +} + +const fn is_global_ipv6(ip: &Ipv6Addr) -> bool { + !(ip.is_unspecified() + || ip.is_loopback() + // IPv4-mapped Address (`::ffff:0:0/96`) + || matches!(ip.segments(), [0, 0, 0, 0, 0, 0xffff, _, _]) + // IPv4-IPv6 Translat. (`64:ff9b:1::/48`) + || matches!(ip.segments(), [0x64, 0xff9b, 1, _, _, _, _, _]) + // Discard-Only Address Block (`100::/64`) + || matches!(ip.segments(), [0x100, 0, 0, 0, _, _, _, _]) + // IETF Protocol Assignments (`2001::/23`) + || (matches!(ip.segments(), [0x2001, b, _, _, _, _, _, _] if b < 0x200) + && !( + // Port Control Protocol Anycast (`2001:1::1`) + u128::from_be_bytes(ip.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0001 + // Traversal Using Relays around NAT Anycast (`2001:1::2`) + || u128::from_be_bytes(ip.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0002 + // AMT (`2001:3::/32`) + || matches!(ip.segments(), [0x2001, 3, _, _, _, _, _, _]) + // AS112-v6 (`2001:4:112::/48`) + || matches!(ip.segments(), [0x2001, 4, 0x112, _, _, _, _, _]) + // ORCHIDv2 (`2001:20::/28`) + // Drone Remote ID Protocol Entity Tags (DETs) Prefix (`2001:30::/28`)` + || matches!(ip.segments(), [0x2001, b, _, _, _, _, _, _] if b >= 0x20 && b <= 0x3F) + )) + // 6to4 (`2002::/16`) – it's not explicitly documented as globally reachable, + // IANA says N/A. + || matches!(ip.segments(), [0x2002, _, _, _, _, _, _, _]) + || is_documentation_ipv6(ip) + || ip.is_unique_local() + || ip.is_unicast_link_local()) +} diff --git a/nym-node/src/node/mixnet/packet_forwarding.rs b/nym-node/src/node/mixnet/packet_forwarding/mod.rs similarity index 85% rename from nym-node/src/node/mixnet/packet_forwarding.rs rename to nym-node/src/node/mixnet/packet_forwarding/mod.rs index cb85fc5af9..a61f5517ec 100644 --- a/nym-node/src/node/mixnet/packet_forwarding.rs +++ b/nym-node/src/node/mixnet/packet_forwarding/mod.rs @@ -1,6 +1,8 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node::mixnet::packet_forwarding::global::is_global_ip; +use crate::node::shared_network::RoutingFilter; use futures::StreamExt; use nym_mixnet_client::forwarder::{ mix_forwarding_channels, MixForwardingReceiver, MixForwardingSender, PacketToForward, @@ -11,14 +13,20 @@ use nym_nonexhaustive_delayqueue::{Expired, NonExhaustiveDelayQueue}; use nym_sphinx_forwarding::packet::MixPacket; use nym_task::ShutdownToken; use std::io; +use std::net::IpAddr; use tokio::time::Instant; use tracing::{debug, error, trace, warn}; +pub(crate) mod global; + pub struct PacketForwarder { + testnet: bool, + delay_queue: NonExhaustiveDelayQueue, mixnet_client: C, metrics: NymNodeMetrics, + routing_filter: RoutingFilter, packet_sender: MixForwardingSender, packet_receiver: MixForwardingReceiver, @@ -26,13 +34,21 @@ pub struct PacketForwarder { } impl PacketForwarder { - pub fn new(client: C, metrics: NymNodeMetrics, shutdown: ShutdownToken) -> Self { + pub fn new( + client: C, + testnet: bool, + routing_filter: RoutingFilter, + metrics: NymNodeMetrics, + shutdown: ShutdownToken, + ) -> Self { let (packet_sender, packet_receiver) = mix_forwarding_channels(); PacketForwarder { + testnet, delay_queue: NonExhaustiveDelayQueue::new(), mixnet_client: client, metrics, + routing_filter, packet_sender, packet_receiver, shutdown, @@ -43,11 +59,29 @@ impl PacketForwarder { self.packet_sender.clone() } + fn should_route(&mut self, ip_addr: IpAddr) -> bool { + // only allow non-global ips on testnets + if self.testnet && !is_global_ip(&ip_addr) { + return true; + } + + self.routing_filter.attempt_resolve(ip_addr).should_route() + } + fn forward_packet(&mut self, packet: MixPacket) where C: SendWithoutResponse, { let next_hop = packet.next_hop(); + + if !self.should_route(next_hop.as_ref().ip()) { + debug!("dropping packet as the egress address does not belong to any known node"); + self.metrics + .mixnet + .egress_dropped_forward_packet(next_hop.into()); + return; + } + let packet_type = packet.packet_type(); let packet = packet.into_packet(); diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index b803d772d0..3a8d7bf867 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -28,7 +28,9 @@ use crate::node::metrics::handler::pending_egress_packets_updater::PendingEgress use crate::node::mixnet::packet_forwarding::PacketForwarder; use crate::node::mixnet::shared::ProcessingConfig; use crate::node::mixnet::SharedFinalHopData; -use crate::node::shared_topology::NymNodeTopologyProvider; +use crate::node::shared_network::{ + CachedNetwork, CachedTopologyProvider, NetworkRefresher, RoutingFilter, +}; use nym_bin_common::bin_info; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_gateway::node::{ActiveClientsStore, GatewayTasksBuilder}; @@ -67,7 +69,7 @@ pub mod helpers; pub(crate) mod http; pub(crate) mod metrics; pub(crate) mod mixnet; -mod shared_topology; +mod shared_network; pub struct GatewayTasksData { mnemonic: Arc>, @@ -530,16 +532,15 @@ impl NymNode { self.x25519_noise_keys.public_key() } - // the reason it's here as opposed to in the gateway directly, - // is that other nym-node tasks will also eventually need it - // (such as the ones for obtaining noise keys of other nodes) - fn build_topology_provider(&self) -> Result { - Ok(NymNodeTopologyProvider::new( - self.as_gateway_topology_node()?, - self.config.debug.topology_cache_ttl, + async fn build_network_refresher(&self) -> Result { + NetworkRefresher::initialise_new( self.user_agent(), self.config.mixnet.nym_api_urls.clone(), - )) + self.config.debug.topology_cache_ttl, + self.config.debug.routing_nodes_check_interval, + self.shutdown_manager.clone_token("network-refresher"), + ) + .await } fn as_gateway_topology_node(&self) -> Result { @@ -583,13 +584,19 @@ impl NymNode { async fn start_gateway_tasks( &mut self, + cached_network: CachedNetwork, metrics_sender: MetricEventsSender, active_clients_store: ActiveClientsStore, mix_packet_sender: MixForwardingSender, task_client: TaskClient, ) -> Result<(), NymNodeError> { let config = gateway_tasks_config(&self.config); - let topology_provider = Box::new(self.build_topology_provider()?); + + let topology_provider = Box::new(CachedTopologyProvider::new( + self.as_gateway_topology_node()?, + cached_network, + self.config.gateway_tasks.debug.minimum_mix_performance, + )); let mut gateway_tasks_builder = GatewayTasksBuilder::new( config.gateway, @@ -952,6 +959,7 @@ impl NymNode { pub(crate) fn start_mixnet_listener( &self, active_clients_store: &ActiveClientsStore, + routing_filter: RoutingFilter, shutdown: ShutdownToken, ) -> (MixForwardingSender, ActiveConnections) { let processing_config = ProcessingConfig::new(&self.config); @@ -980,6 +988,8 @@ impl NymNode { let mut packet_forwarder = PacketForwarder::new( mixnet_client, + self.config.debug.testnet, + routing_filter, self.metrics.clone(), shutdown.clone_with_suffix("mix-packet-forwarder"), ); @@ -1028,13 +1038,14 @@ impl NymNode { }); self.try_refresh_remote_nym_api_cache().await; - self.start_verloc_measurements(); + let network_refresher = self.build_network_refresher().await?; let active_clients_store = ActiveClientsStore::new(); let (mix_packet_sender, active_egress_mixnet_connections) = self.start_mixnet_listener( &active_clients_store, + network_refresher.routing_filter(), self.shutdown_manager.clone_token("mixnet-traffic"), ); @@ -1045,6 +1056,7 @@ impl NymNode { ); self.start_gateway_tasks( + network_refresher.cached_network(), metrics_sender, active_clients_store, mix_packet_sender, @@ -1052,6 +1064,8 @@ impl NymNode { ) .await?; + network_refresher.start(); + self.shutdown_manager.close(); self.shutdown_manager.wait_for_shutdown_signal().await; diff --git a/nym-node/src/node/shared_network.rs b/nym-node/src/node/shared_network.rs new file mode 100644 index 0000000000..c127aa1586 --- /dev/null +++ b/nym-node/src/node/shared_network.rs @@ -0,0 +1,434 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::NymNodeError; +use arc_swap::ArcSwap; +use async_trait::async_trait; +use nym_gateway::node::UserAgent; +use nym_node_metrics::prometheus_wrapper::{PrometheusMetric, PROMETHEUS_METRICS}; +use nym_task::ShutdownToken; +use nym_topology::node::RoutingNode; +use nym_topology::{EpochRewardedSet, NymTopology, Role, TopologyProvider}; +use nym_validator_client::nym_nodes::{NodesByAddressesResponse, SkimmedNode}; +use nym_validator_client::{NymApiClient, ValidatorClientError}; +use std::collections::HashSet; +use std::net::IpAddr; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::RwLock; +use tokio::time::interval; +use tracing::log::error; +use tracing::{debug, trace, warn}; +use url::Url; + +#[derive(Clone)] +pub(crate) struct RoutingFilter { + resolved: KnownNodes, + + // while this is technically behind a lock, it should not be called too often as once resolved it will + // be present on the arcswap in either allowed or denied section + pending: UnknownNodes, +} + +impl RoutingFilter { + fn new_empty() -> Self { + RoutingFilter { + resolved: Default::default(), + pending: Default::default(), + } + } + + pub(crate) fn attempt_resolve(&self, ip: IpAddr) -> Resolution { + if self.resolved.inner.allowed.load().contains(&ip) { + Resolution::Accept + } else if self.resolved.inner.denied.load().contains(&ip) { + Resolution::Deny + } else { + self.pending.try_insert(ip); + Resolution::Unknown + } + } +} + +#[derive(Clone, Default)] +struct UnknownNodes(Arc>>); + +impl UnknownNodes { + fn try_insert(&self, ip: IpAddr) { + // if we can immediately grab the lock to push it into the pending queue, amazing, let's do it + // otherwise we can do it next time we see this ip + // (if we can't hold the lock, it means it's being updated at this very moment which is actually a good thing) + if let Ok(mut guard) = self.0.try_write() { + guard.insert(ip); + } + } + + async fn clear(&self) { + self.0.write().await.clear(); + } + + async fn nodes(&self) -> HashSet { + self.0.read().await.clone() + } +} + +// for now we don't care about keys, etc. +// we only want to know if given ip belongs to a known node +#[derive(Debug, Default, Clone)] +pub(crate) struct KnownNodes { + inner: Arc, +} + +#[derive(Debug, Default)] +struct KnownNodesInner { + allowed: ArcSwap>, + denied: ArcSwap>, +} + +pub(crate) enum Resolution { + Unknown, + Deny, + Accept, +} + +impl From for Resolution { + fn from(value: bool) -> Self { + if value { + Resolution::Accept + } else { + Resolution::Deny + } + } +} + +impl Resolution { + pub(crate) fn should_route(&self) -> bool { + matches!(self, Resolution::Accept) + } +} + +impl KnownNodes { + fn swap_allowed(&self, new: HashSet) { + self.inner.allowed.store(Arc::new(new)) + } + + fn swap_denied(&self, new: HashSet) { + self.inner.denied.store(Arc::new(new)) + } +} + +struct NodesQuerier { + client: NymApiClient, + nym_api_urls: Vec, + currently_used_api: usize, +} + +impl NodesQuerier { + fn use_next_nym_api(&mut self) { + if self.nym_api_urls.len() == 1 { + warn!("There's only a single nym API available - it won't be possible to use a different one"); + return; + } + + self.currently_used_api = (self.currently_used_api + 1) % self.nym_api_urls.len(); + self.client + .change_nym_api(self.nym_api_urls[self.currently_used_api].clone()) + } + + async fn rewarded_set(&mut self) -> Result { + let res = self + .client + .get_current_rewarded_set() + .await + .inspect_err(|err| error!("failed to get current rewarded set: {err}")); + + if res.is_err() { + self.use_next_nym_api() + } + res + } + + async fn current_nymnodes(&mut self) -> Result, ValidatorClientError> { + let res = self + .client + .get_all_basic_nodes() + .await + .inspect_err(|err| error!("failed to get network nodes: {err}")); + + if res.is_err() { + self.use_next_nym_api() + } + res + } + + async fn query_for_info( + &mut self, + ips: Vec, + ) -> Result { + let res = self + .client + .nodes_by_addresses(ips) + .await + .inspect_err(|err| error!("failed to obtain node information: {err}")); + + if res.is_err() { + self.use_next_nym_api() + } + res + } +} + +#[derive(Clone)] +pub struct CachedTopologyProvider { + gateway_node: Arc, + cached_network: CachedNetwork, + min_mix_performance: u8, +} + +impl CachedTopologyProvider { + pub(crate) fn new( + gateway_node: RoutingNode, + cached_network: CachedNetwork, + min_mix_performance: u8, + ) -> Self { + CachedTopologyProvider { + gateway_node: Arc::new(gateway_node), + cached_network, + min_mix_performance, + } + } +} + +#[async_trait] +impl TopologyProvider for CachedTopologyProvider { + async fn get_new_topology(&mut self) -> Option { + let network_guard = self.cached_network.inner.read().await; + let self_node = self.gateway_node.identity_key; + + let mut topology = NymTopology::new_empty(network_guard.rewarded_set.clone()) + .with_additional_nodes(network_guard.network_nodes.iter().filter(|node| { + if node.supported_roles.mixnode { + node.performance.round_to_integer() >= self.min_mix_performance + } else { + true + } + })); + + if !topology.has_node_details(self.gateway_node.node_id) { + debug!("{self_node} didn't exist in topology. inserting it.",); + topology.insert_node_details(self.gateway_node.as_ref().clone()); + } + topology.force_set_active(self.gateway_node.node_id, Role::EntryGateway); + + Some(topology) + } +} + +#[derive(Clone)] +pub(crate) struct CachedNetwork { + inner: Arc>, +} + +impl CachedNetwork { + fn new_empty() -> Self { + CachedNetwork { + inner: Arc::new(RwLock::new(CachedNetworkInner { + rewarded_set: Default::default(), + network_nodes: vec![], + })), + } + } +} + +struct CachedNetworkInner { + rewarded_set: EpochRewardedSet, + network_nodes: Vec, +} + +pub struct NetworkRefresher { + querier: NodesQuerier, + full_refresh_interval: Duration, + pending_check_interval: Duration, + shutdown_token: ShutdownToken, + + network: CachedNetwork, + routing_filter: RoutingFilter, +} + +impl NetworkRefresher { + pub(crate) async fn initialise_new( + user_agent: UserAgent, + nym_api_urls: Vec, + full_refresh_interval: Duration, + pending_check_interval: Duration, + shutdown_token: ShutdownToken, + ) -> Result { + let mut this = NetworkRefresher { + querier: NodesQuerier { + client: NymApiClient::new_with_user_agent(nym_api_urls[0].clone(), user_agent), + nym_api_urls, + currently_used_api: 0, + }, + full_refresh_interval, + pending_check_interval, + shutdown_token, + network: CachedNetwork::new_empty(), + routing_filter: RoutingFilter::new_empty(), + }; + + this.obtain_initial_network().await?; + Ok(this) + } + + fn allowed_nodes_copy(&self) -> HashSet { + self.routing_filter + .resolved + .inner + .allowed + .load_full() + .as_ref() + .clone() + } + + fn denied_nodes_copy(&self) -> HashSet { + self.routing_filter + .resolved + .inner + .denied + .load_full() + .as_ref() + .clone() + } + + async fn inspect_pending(&mut self) { + let to_resolve = self.routing_filter.pending.nodes().await; + + // no pending requests to resolve + if to_resolve.is_empty() { + return; + } + + let mut allowed = self.allowed_nodes_copy(); + let mut denied = self.denied_nodes_copy(); + + // short circuit: check if the pending nodes are not already resolved + // (it could happen due to lack of full sync between pending lock and arcswap(s)) + if to_resolve + .iter() + .all(|p| allowed.contains(p) || denied.contains(p)) + { + return; + } + + // 1. attempt to use the new nym-api query to get information just by ips + let nodes = to_resolve.into_iter().collect(); + if let Ok(res) = self.querier.query_for_info(nodes).await { + for (ip, maybe_id) in res.existence { + if maybe_id.is_some() { + allowed.insert(ip); + } else { + denied.insert(ip); + } + } + + self.routing_filter.resolved.swap_allowed(allowed); + self.routing_filter.resolved.swap_denied(denied); + self.routing_filter.pending.clear().await; + return; + } + + // 2. we assume nym-api doesn't support that query yet - we have to do the full refresh + self.refresh_network_nodes().await; + } + + async fn refresh_network_nodes_inner(&mut self) -> Result<(), ValidatorClientError> { + let rewarded_set = self.querier.rewarded_set().await?; + let nodes = self.querier.current_nymnodes().await?; + + // collect all known/allowed nodes information + let known_nodes = nodes + .iter() + .flat_map(|n| n.ip_addresses.iter()) + .copied() + .collect::>(); + + let pending = self.routing_filter.pending.nodes().await; + let mut current_denied = self.denied_nodes_copy(); + + for allowed in &known_nodes { + // if some node has become known, it should be removed from the denied set + current_denied.remove(allowed); + } + + // any pending node, if not present in the new set of allowed nodes, should be added in the denied set + for pending_node in pending { + if !known_nodes.contains(&pending_node) { + current_denied.insert(pending_node); + } + } + + self.routing_filter.resolved.swap_allowed(known_nodes); + self.routing_filter.resolved.swap_denied(current_denied); + self.routing_filter.pending.clear().await; + + let mut network_guard = self.network.inner.write().await; + network_guard.network_nodes = nodes; + network_guard.rewarded_set = rewarded_set; + + Ok(()) + } + + async fn refresh_network_nodes(&mut self) { + let timer = + PROMETHEUS_METRICS.start_timer(PrometheusMetric::ProcessTopologyQueryResolutionLatency); + + if self.refresh_network_nodes_inner().await.is_err() { + // don't use the histogram observation as some queries didn't complete + if let Some(obs) = timer { + obs.stop_and_discard(); + } + } + } + + pub(crate) async fn obtain_initial_network(&mut self) -> Result<(), NymNodeError> { + self.refresh_network_nodes_inner() + .await + .map_err(|source| NymNodeError::InitialTopologyQueryFailure { source }) + } + + pub(crate) fn routing_filter(&self) -> RoutingFilter { + self.routing_filter.clone() + } + + pub(crate) fn cached_network(&self) -> CachedNetwork { + self.network.clone() + } + + pub(crate) async fn run(&mut self) { + let mut full_refresh_interval = interval(self.full_refresh_interval); + full_refresh_interval.reset(); + + let mut pending_check_interval = interval(self.pending_check_interval); + pending_check_interval.reset(); + + while !self.shutdown_token.is_cancelled() { + tokio::select! { + biased; + _ = self.shutdown_token.cancelled() => { + trace!("NetworkRefresher: Received shutdown"); + } + _ = pending_check_interval.tick() => { + self.inspect_pending().await; + } + _ = full_refresh_interval.tick() => { + self.refresh_network_nodes().await; + } + } + } + trace!("NetworkRefresher: Exiting"); + } + + pub(crate) fn start(mut self) { + tokio::spawn(async move { self.run().await }); + } +} diff --git a/nym-node/src/node/shared_topology.rs b/nym-node/src/node/shared_topology.rs deleted file mode 100644 index f3ba72fd00..0000000000 --- a/nym-node/src/node/shared_topology.rs +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use async_trait::async_trait; -use nym_gateway::node::{NymApiTopologyProvider, NymApiTopologyProviderConfig, UserAgent}; -use nym_node_metrics::prometheus_wrapper::{PrometheusMetric, PROMETHEUS_METRICS}; -use nym_topology::node::RoutingNode; -use nym_topology::{NymTopology, Role, TopologyProvider}; -use std::sync::Arc; -use std::time::Duration; -use time::OffsetDateTime; -use tokio::sync::Mutex; -use tracing::debug; -use url::Url; -// I wouldn't be surprised if this became the start of the node topology cache - -#[derive(Clone)] -pub struct NymNodeTopologyProvider { - inner: Arc>, -} - -impl NymNodeTopologyProvider { - pub fn new( - gateway_node: RoutingNode, - cache_ttl: Duration, - user_agent: UserAgent, - nym_api_url: Vec, - ) -> NymNodeTopologyProvider { - NymNodeTopologyProvider { - inner: Arc::new(Mutex::new(NymNodeTopologyProviderInner { - inner: NymApiTopologyProvider::new( - NymApiTopologyProviderConfig { - min_mixnode_performance: 50, - min_gateway_performance: 0, - use_extended_topology: false, - ignore_egress_epoch_role: true, - }, - nym_api_url, - Some(user_agent), - ), - cache_ttl, - cached_at: OffsetDateTime::UNIX_EPOCH, - cached: None, - gateway_node, - })), - } - } -} - -struct NymNodeTopologyProviderInner { - inner: NymApiTopologyProvider, - cache_ttl: Duration, - cached_at: OffsetDateTime, - cached: Option, - gateway_node: RoutingNode, -} - -impl NymNodeTopologyProviderInner { - fn cached_topology(&self) -> Option { - if let Some(cached_topology) = &self.cached { - if self.cached_at + self.cache_ttl > OffsetDateTime::now_utc() { - return Some(cached_topology.clone()); - } - } - - None - } - - async fn update_cache(&mut self) -> Option { - let updated_cache = match self.inner.get_new_topology().await { - None => None, - Some(mut base) => { - if !base.has_node_details(self.gateway_node.node_id) { - debug!( - "{} didn't exist in topology. inserting it.", - self.gateway_node.identity_key - ); - base.insert_node_details(self.gateway_node.clone()); - } - base.force_set_active(self.gateway_node.node_id, Role::EntryGateway); - Some(base) - } - }; - - self.cached_at = OffsetDateTime::now_utc(); - self.cached = updated_cache.clone(); - - updated_cache - } -} - -#[async_trait] -impl TopologyProvider for NymNodeTopologyProvider { - async fn get_new_topology(&mut self) -> Option { - let mut guard = self.inner.lock().await; - // check the cache - if let Some(cached) = guard.cached_topology() { - return Some(cached); - } - - // the observation will be included on drop - let _timer = - PROMETHEUS_METRICS.start_timer(PrometheusMetric::ProcessTopologyQueryResolutionLatency); - guard.update_cache().await - } -} From f6c19ec02bda2ae5eda2595a734067d9d95563e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 20:05:21 +0100 Subject: [PATCH 025/133] build(deps): bump the patch-updates group across 1 directory with 14 updates (#5549) --- Cargo.lock | 284 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 20 ++-- 2 files changed, 150 insertions(+), 154 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 179dc53b81..447136c790 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -229,9 +229,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.96" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] name = "arbitrary" @@ -344,9 +344,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.86" +version = "0.1.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" +checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97" dependencies = [ "proc-macro2", "quote", @@ -774,9 +774,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1230237285e3e10cde447185e8975408ae24deaa67205ce684805c25bc0c7937" +checksum = "675f87afced0413c9bb02843499dbbd3882a237645883f71a2b59644a6d2f753" dependencies = [ "arrayref", "arrayvec", @@ -784,7 +784,6 @@ dependencies = [ "cfg-if", "constant_time_eq", "digest 0.10.7", - "memmap2", ] [[package]] @@ -1042,9 +1041,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.39" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" dependencies = [ "android-tzdata", "iana-time-zone", @@ -1052,7 +1051,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -1095,9 +1094,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.30" +version = "4.5.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d" +checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" dependencies = [ "clap_builder", "clap_derive", @@ -1105,9 +1104,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.30" +version = "4.5.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c" +checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" dependencies = [ "anstream", "anstyle", @@ -1117,9 +1116,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.45" +version = "4.5.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e3040c8291884ddf39445dc033c70abc2bc44a42f0a3a00571a0f483a83f0cd" +checksum = "f5c5508ea23c5366f77e53f5a0070e5a84e51687ec3ef9e0464c86dc8d13ce98" dependencies = [ "clap", ] @@ -1190,9 +1189,9 @@ dependencies = [ [[package]] name = "console" -version = "0.15.10" +version = "0.15.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" dependencies = [ "encode_unicode", "libc", @@ -2463,7 +2462,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", ] @@ -2475,7 +2474,7 @@ dependencies = [ "console_error_panic_hook", "js-sys", "serde-wasm-bindgen 0.6.5", - "thiserror 2.0.11", + "thiserror 2.0.12", "wasm-bindgen", "wasm-bindgen-futures", "wasm-storage", @@ -2865,9 +2864,9 @@ dependencies = [ [[package]] name = "getset" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded738faa0e88d3abc9d1a13cb11adc2073c400969eeb8793cf7132589959fc" +checksum = "f3586f256131df87204eb733da72e3d3eb4f343c639f4b7be279ac7c48baeafe" dependencies = [ "proc-macro-error2", "proc-macro2", @@ -4354,15 +4353,6 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "memmap2" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" -dependencies = [ - "libc", -] - [[package]] name = "memoffset" version = "0.9.1" @@ -4451,7 +4441,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde-wasm-bindgen 0.6.5", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tsify", "url", @@ -4867,7 +4857,7 @@ dependencies = [ "sha2 0.9.9", "sqlx", "tempfile", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tokio-stream", @@ -4908,7 +4898,7 @@ dependencies = [ "serde_json", "sha2 0.10.8", "tendermint 0.40.1", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "ts-rs", "utoipa", @@ -4960,7 +4950,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tokio-stream", "tokio-util", @@ -4983,7 +4973,7 @@ dependencies = [ "rand 0.8.5", "serde", "sha2 0.10.8", - "thiserror 2.0.11", + "thiserror 2.0.12", "x25519-dalek", ] @@ -5003,7 +4993,7 @@ dependencies = [ "nym-task", "nym-validator-client", "rand 0.8.5", - "thiserror 2.0.11", + "thiserror 2.0.12", "url", "zeroize", ] @@ -5042,7 +5032,7 @@ dependencies = [ "nym-validator-client", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -5120,7 +5110,7 @@ dependencies = [ "serde_json", "tap", "tempfile", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "toml 0.8.20", @@ -5157,7 +5147,7 @@ dependencies = [ "serde", "serde_json", "tap", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tokio-tungstenite", @@ -5215,7 +5205,7 @@ dependencies = [ "si-scale", "tap", "tempfile", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tokio-stream", @@ -5240,7 +5230,7 @@ dependencies = [ "nym-sphinx-addressing", "nym-sphinx-params", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "url", ] @@ -5255,7 +5245,7 @@ dependencies = [ "nym-gateway-requests", "serde", "sqlx", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "url", @@ -5273,7 +5263,7 @@ dependencies = [ "nym-sphinx", "nym-task", "sqlx", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", ] @@ -5292,7 +5282,7 @@ dependencies = [ "serde", "serde-wasm-bindgen 0.6.5", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "tsify", "wasm-bindgen", "wasm-bindgen-futures", @@ -5330,7 +5320,7 @@ dependencies = [ "serde", "serde_derive", "sha2 0.9.9", - "thiserror 2.0.11", + "thiserror 2.0.12", "zeroize", ] @@ -5377,7 +5367,7 @@ dependencies = [ "serde", "sha2 0.9.9", "subtle 2.6.1", - "thiserror 2.0.11", + "thiserror 2.0.12", "zeroize", ] @@ -5390,7 +5380,7 @@ dependencies = [ "log", "nym-network-defaults", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "toml 0.8.20", "url", ] @@ -5406,7 +5396,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "utoipa", "vergen", ] @@ -5467,7 +5457,7 @@ dependencies = [ "strum 0.26.3", "strum_macros 0.26.4", "tempfile", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tokio-util", @@ -5515,7 +5505,7 @@ dependencies = [ "nym-ecash-time", "serde", "sqlx", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "zeroize", ] @@ -5533,7 +5523,7 @@ dependencies = [ "nym-credentials-interface", "nym-ecash-time", "nym-validator-client", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", ] @@ -5556,7 +5546,7 @@ dependencies = [ "nym-validator-client", "rand 0.8.5", "si-scale", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tracing", @@ -5580,7 +5570,7 @@ dependencies = [ "nym-validator-client", "rand 0.8.5", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "zeroize", ] @@ -5596,7 +5586,7 @@ dependencies = [ "rand 0.8.5", "serde", "strum 0.26.3", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "utoipa", ] @@ -5625,7 +5615,7 @@ dependencies = [ "serde_bytes", "sha2 0.10.8", "subtle-encoding", - "thiserror 2.0.11", + "thiserror 2.0.12", "x25519-dalek", "zeroize", ] @@ -5649,7 +5639,7 @@ dependencies = [ "serde", "serde_derive", "sha2 0.9.9", - "thiserror 2.0.11", + "thiserror 2.0.12", "zeroize", ] @@ -5664,7 +5654,7 @@ dependencies = [ "cw-utils", "cw2", "nym-multisig-contract-common", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -5690,7 +5680,7 @@ dependencies = [ "reqwest 0.12.4", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", "utoipa", ] @@ -5714,7 +5704,7 @@ dependencies = [ "nym-explorer-api-requests", "reqwest 0.12.4", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tracing", "url", @@ -5774,7 +5764,7 @@ dependencies = [ "rand 0.8.5", "sha2 0.10.8", "sqlx", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tokio-stream", @@ -5808,7 +5798,7 @@ dependencies = [ "rand 0.8.5", "serde", "si-scale", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tokio-stream", @@ -5843,7 +5833,7 @@ dependencies = [ "serde_json", "strum 0.26.3", "subtle 2.6.1", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tracing", @@ -5862,7 +5852,7 @@ dependencies = [ "nym-statistics-common", "sqlx", "strum 0.26.3", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tracing", @@ -5878,7 +5868,7 @@ dependencies = [ "nym-gateway-requests", "nym-sphinx", "sqlx", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tracing", @@ -5894,7 +5884,7 @@ dependencies = [ "nym-ffi-shared", "nym-sdk", "nym-sphinx-anonymous-replies", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "uniffi", "uniffi_build", @@ -5923,7 +5913,7 @@ dependencies = [ "reqwest 0.12.4", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tracing", "url", @@ -5956,7 +5946,7 @@ version = "0.1.0" dependencies = [ "nym-credential-storage", "nym-credentials", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tracing", "zeroize", @@ -5982,7 +5972,7 @@ version = "0.1.0" dependencies = [ "log", "rand 0.8.5", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -5996,7 +5986,7 @@ dependencies = [ "nym-sphinx", "rand 0.8.5", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tokio-util", @@ -6036,7 +6026,7 @@ dependencies = [ "serde", "serde_json", "tap", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tokio-tun", @@ -6052,7 +6042,7 @@ dependencies = [ "k256", "ledger-transport", "ledger-transport-hid", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -6097,7 +6087,7 @@ dependencies = [ "serde", "serde-json-wasm", "serde_repr", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "ts-rs", "utoipa", @@ -6123,7 +6113,7 @@ dependencies = [ "nym-task", "rand 0.8.5", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tokio-util", @@ -6142,7 +6132,7 @@ dependencies = [ "cw4", "schemars", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -6235,7 +6225,7 @@ dependencies = [ "sqlx", "tap", "tempfile", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tokio-tungstenite", @@ -6299,7 +6289,7 @@ dependencies = [ "serde_json", "si-scale", "sysinfo", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tokio-util", @@ -6345,7 +6335,7 @@ dependencies = [ "serde", "serde_json", "strum 0.26.3", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "utoipa", @@ -6404,7 +6394,7 @@ dependencies = [ "sqlx", "strum 0.26.3", "strum_macros 0.26.4", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tokio-util", @@ -6447,7 +6437,7 @@ dependencies = [ "rand_chacha 0.3.1", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "wasm-utils", ] @@ -6462,7 +6452,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde-wasm-bindgen 0.6.5", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tsify", "wasm-bindgen", @@ -6503,7 +6493,7 @@ name = "nym-ordered-buffer" version = "0.1.0" dependencies = [ "log", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -6520,7 +6510,7 @@ dependencies = [ "rand 0.8.5", "rayon", "sphinx-packet", - "thiserror 2.0.11", + "thiserror 2.0.12", "x25519-dalek", "zeroize", ] @@ -6577,7 +6567,7 @@ dependencies = [ "reqwest 0.12.4", "serde", "tap", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tokio-stream", "tokio-util", @@ -6620,7 +6610,7 @@ dependencies = [ "nym-sphinx-anonymous-replies", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", ] @@ -6650,7 +6640,7 @@ dependencies = [ "serde", "serde_json", "tap", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "url", @@ -6684,7 +6674,7 @@ dependencies = [ "schemars", "serde", "tap", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "url", ] @@ -6716,7 +6706,7 @@ dependencies = [ "serde", "serde_json", "tap", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -6741,7 +6731,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "rand_distr", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", ] @@ -6759,7 +6749,7 @@ dependencies = [ "nym-topology", "rand 0.8.5", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "zeroize", ] @@ -6771,7 +6761,7 @@ dependencies = [ "nym-sphinx-types", "rand 0.8.5", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -6788,7 +6778,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "wasm-bindgen", ] @@ -6805,7 +6795,7 @@ dependencies = [ "nym-sphinx-types", "rand 0.8.5", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "utoipa", ] @@ -6823,7 +6813,7 @@ dependencies = [ "nym-sphinx-types", "nym-topology", "rand 0.8.5", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -6834,7 +6824,7 @@ dependencies = [ "nym-sphinx-addressing", "nym-sphinx-params", "nym-sphinx-types", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -6849,7 +6839,7 @@ dependencies = [ "nym-sphinx-forwarding", "nym-sphinx-params", "nym-sphinx-types", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tokio-util", ] @@ -6861,7 +6851,7 @@ dependencies = [ "nym-crypto", "nym-sphinx-types", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -6870,7 +6860,7 @@ version = "0.1.0" dependencies = [ "nym-sphinx-addressing", "nym-sphinx-types", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -6879,7 +6869,7 @@ version = "0.2.0" dependencies = [ "nym-outfox", "sphinx-packet", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -6898,7 +6888,7 @@ dependencies = [ "sha2 0.10.8", "si-scale", "sysinfo", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "wasmtimer", @@ -6915,7 +6905,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "zeroize", ] @@ -6926,7 +6916,7 @@ dependencies = [ "cfg-if", "futures", "log", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tokio-util", "tracing", @@ -6968,7 +6958,7 @@ dependencies = [ "reqwest 0.12.4", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "tracing", "tsify", "wasm-bindgen", @@ -6982,7 +6972,7 @@ dependencies = [ "etherparse", "log", "nym-wireguard-types", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tokio-tun", ] @@ -7010,7 +7000,7 @@ dependencies = [ "sha2 0.10.8", "strum 0.26.3", "tempfile", - "thiserror 2.0.11", + "thiserror 2.0.12", "ts-rs", "url", "utoipa", @@ -7057,7 +7047,7 @@ dependencies = [ "serde_json", "sha2 0.9.9", "tendermint-rpc", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tracing", @@ -7101,7 +7091,7 @@ dependencies = [ "serde_with", "sha2 0.10.8", "sqlx", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tracing", @@ -7120,7 +7110,7 @@ dependencies = [ "nym-task", "nym-validator-client", "rand 0.8.5", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tokio-util", @@ -7138,7 +7128,7 @@ dependencies = [ "nym-contracts-common", "nym-mixnet-contract-common", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "ts-rs", ] @@ -7159,7 +7149,7 @@ dependencies = [ "serde", "serde-wasm-bindgen 0.6.5", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tsify", "wasm-bindgen", @@ -7206,7 +7196,7 @@ dependencies = [ "nym-node-metrics", "nym-task", "nym-wireguard-types", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tokio-stream", @@ -7225,7 +7215,7 @@ dependencies = [ "nym-network-defaults", "rand 0.8.5", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "x25519-dalek", ] @@ -7252,7 +7242,7 @@ dependencies = [ "serde_json", "sha2 0.10.8", "tar", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tracing", @@ -7281,7 +7271,7 @@ dependencies = [ "serde", "serde_json", "sqlx", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tokio-util", @@ -7308,7 +7298,7 @@ dependencies = [ "sqlx", "tendermint 0.40.1", "tendermint-rpc", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "tokio-stream", @@ -7629,7 +7619,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" dependencies = [ "memchr", - "thiserror 2.0.11", + "thiserror 2.0.12", "ucd-trie", ] @@ -7717,18 +7707,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", @@ -8114,9 +8104,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.38" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" dependencies = [ "proc-macro2", ] @@ -8419,7 +8409,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "359fc315ed556eb0e42ce74e76f4b1cd807b50fa6307f3de4e51f92dbe86e2d5" dependencies = [ "lazy_static", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -8881,9 +8871,9 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" dependencies = [ "dyn-clone", "indexmap 1.9.3", @@ -8895,9 +8885,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" dependencies = [ "proc-macro2", "quote", @@ -9080,9 +9070,9 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.15" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" +checksum = "364fec0df39c49a083c9a8a18a23a6bcfd9af130fe9fe321d18520a0d113e09e" dependencies = [ "serde", ] @@ -9122,9 +9112,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.139" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -9145,7 +9135,7 @@ dependencies = [ "serde_json", "serde_json_path_core", "serde_json_path_macros", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -9157,7 +9147,7 @@ dependencies = [ "inventory", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -10192,7 +10182,7 @@ dependencies = [ "serde_json", "sqlx", "tempfile", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tokio", "toml 0.8.20", @@ -10221,11 +10211,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.12", ] [[package]] @@ -10241,9 +10231,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", @@ -10815,7 +10805,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e640d9b0964e9d39df633548591090ab92f7a4567bc31d3891af23471a3365c6" dependencies = [ "lazy_static", - "thiserror 2.0.11", + "thiserror 2.0.12", "ts-rs-macros", ] @@ -11476,7 +11466,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde-wasm-bindgen 0.6.5", - "thiserror 2.0.11", + "thiserror 2.0.12", "time", "tsify", "url", @@ -11498,7 +11488,7 @@ dependencies = [ "nym-store-cipher", "serde", "serde-wasm-bindgen 0.6.5", - "thiserror 2.0.11", + "thiserror 2.0.12", "wasm-bindgen", "wasm-utils", ] @@ -11754,6 +11744,12 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "windows-link" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" + [[package]] name = "windows-result" version = "0.1.2" @@ -12228,7 +12224,7 @@ dependencies = [ "flate2", "indexmap 2.7.1", "memchr", - "thiserror 2.0.11", + "thiserror 2.0.12", "zopfli", ] @@ -12250,7 +12246,7 @@ dependencies = [ "rand 0.8.5", "reqwest 0.12.4", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tsify", "uuid", diff --git a/Cargo.toml b/Cargo.toml index 5713f5888d..e2f7140c32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -192,10 +192,10 @@ aes = "0.8.1" aes-gcm = "0.10.1" aes-gcm-siv = "0.11.1" ammonia = "4" -anyhow = "1.0.95" +anyhow = "1.0.97" arc-swap = "1.7.1" argon2 = "0.5.0" -async-trait = "0.1.86" +async-trait = "0.1.87" axum = "0.7.5" axum-client-ip = "0.6.1" axum-extra = "0.9.4" @@ -206,7 +206,7 @@ bincode = "1.3.3" bip39 = { version = "2.0.0", features = ["zeroize"] } bit-vec = "0.7.0" # can we unify those? bitvec = "1.0.0" -blake3 = "1.5.5" +blake3 = "1.6.1" bloomfilter = "1.0.14" bs58 = "0.5.1" bytecodec = "0.4.15" @@ -216,14 +216,14 @@ celes = "2.5.0" cfg-if = "1.0.0" chacha20 = "0.9.0" chacha20poly1305 = "0.10.1" -chrono = "0.4.39" +chrono = "0.4.40" cipher = "0.4.3" -clap = "4.5.30" +clap = "4.5.31" clap_complete = "4.5" clap_complete_fig = "4.5" colored = "2.2" comfy-table = "7.1.4" -console = "0.15.10" +console = "0.15.11" console-subscriber = "0.1.1" console_error_panic_hook = "0.1" const-str = "0.5.6" @@ -252,7 +252,7 @@ futures = "0.3.31" futures-util = "0.3" generic-array = "0.14.7" getrandom = "0.2.10" -getset = "0.1.4" +getset = "0.1.5" handlebars = "3.5.5" headers = "0.4.0" hex = "0.4.3" @@ -310,12 +310,12 @@ rocket_cors = "0.6.0" rocket_okapi = "0.8.0" rs_merkle = "1.4.2" safer-ffi = "0.1.13" -schemars = "0.8.21" +schemars = "0.8.22" semver = "1.0.25" serde = "1.0.217" -serde_bytes = "0.11.15" +serde_bytes = "0.11.16" serde_derive = "1.0" -serde_json = "1.0.138" +serde_json = "1.0.140" serde_json_path = "0.7.2" serde_repr = "0.1" serde_with = "3.9.0" From 9c1723983189ac7ebe220f3405ab50f7f8d2d7f3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 00:35:12 +0100 Subject: [PATCH 026/133] build(deps): bump flate2 from 1.0.35 to 1.1.0 (#5510) Bumps [flate2](https://github.com/rust-lang/flate2-rs) from 1.0.35 to 1.1.0. - [Release notes](https://github.com/rust-lang/flate2-rs/releases) - [Changelog](https://github.com/rust-lang/flate2-rs/blob/main/CHANGELOG.md) - [Commits](https://github.com/rust-lang/flate2-rs/compare/1.0.35...1.1.0) --- updated-dependencies: - dependency-name: flate2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 447136c790..45d07c36d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2575,9 +2575,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.35" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" dependencies = [ "crc32fast", "miniz_oxide", diff --git a/Cargo.toml b/Cargo.toml index e2f7140c32..a0d6f9f56c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -247,7 +247,7 @@ envy = "0.4" etherparse = "0.13.0" eyre = "0.6.9" fastrand = "2.1.1" -flate2 = "1.0.35" +flate2 = "1.1.0" futures = "0.3.31" futures-util = "0.3" generic-array = "0.14.7" From 080ec80722387cb4f98d4eab6722f0298bdbf426 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 00:36:24 +0100 Subject: [PATCH 027/133] build(deps): bump uuid from 1.13.2 to 1.15.1 (#5542) Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.13.2 to 1.15.1. - [Release notes](https://github.com/uuid-rs/uuid/releases) - [Commits](https://github.com/uuid-rs/uuid/compare/v1.13.2...v1.15.1) --- updated-dependencies: - dependency-name: uuid dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 45d07c36d0..2561a27586 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11262,9 +11262,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c1f41ffb7cf259f1ecc2876861a17e7142e63ead296f671f81f6ae85903e0d6" +checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587" dependencies = [ "getrandom 0.3.1", "serde", From 6c781a00647731db97ffdd040f29793a2ef81ca9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 00:37:35 +0100 Subject: [PATCH 028/133] build(deps): bump itertools from 0.13.0 to 0.14.0 (#5509) Bumps [itertools](https://github.com/rust-itertools/itertools) from 0.13.0 to 0.14.0. - [Changelog](https://github.com/rust-itertools/itertools/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-itertools/itertools/compare/v0.13.0...v0.14.0) --- updated-dependencies: - dependency-name: itertools dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 23 +++++++---------------- Cargo.toml | 2 +- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2561a27586..2030cafd17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2440,7 +2440,7 @@ dependencies = [ "dotenvy", "humantime-serde", "isocountry", - "itertools 0.13.0", + "itertools 0.14.0", "log", "maxminddb", "nym-bin-common", @@ -3966,15 +3966,6 @@ 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 = "itertools" version = "0.14.0" @@ -4812,7 +4803,7 @@ dependencies = [ "futures", "getset", "humantime-serde", - "itertools 0.13.0", + "itertools 0.14.0", "k256", "nym-api-requests", "nym-bandwidth-controller", @@ -5312,7 +5303,7 @@ dependencies = [ "ff", "getrandom 0.2.15", "group", - "itertools 0.13.0", + "itertools 0.14.0", "nym-dkg", "nym-pemstore", "rand 0.8.5", @@ -5359,7 +5350,7 @@ dependencies = [ "digest 0.9.0", "ff", "group", - "itertools 0.13.0", + "itertools 0.14.0", "nym-network-defaults", "nym-pemstore", "rand 0.8.5", @@ -6370,7 +6361,7 @@ dependencies = [ "cosmwasm-std", "envy", "futures-util", - "itertools 0.13.0", + "itertools 0.14.0", "moka", "nym-bin-common", "nym-contracts-common", @@ -6986,7 +6977,7 @@ dependencies = [ "cosmwasm-std", "eyre", "hmac", - "itertools 0.13.0", + "itertools 0.14.0", "log", "nym-config", "nym-crypto", @@ -7026,7 +7017,7 @@ dependencies = [ "eyre", "flate2", "futures", - "itertools 0.13.0", + "itertools 0.14.0", "nym-api-requests", "nym-coconut-bandwidth-contract-common", "nym-coconut-dkg-common", diff --git a/Cargo.toml b/Cargo.toml index a0d6f9f56c..d82e9c2f11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -273,7 +273,7 @@ inquire = "0.6.2" ip_network = "0.4.1" ipnetwork = "0.20" isocountry = "0.3.2" -itertools = "0.13.0" +itertools = "0.14.0" k256 = "0.13" lazy_static = "1.5.0" ledger-transport = "0.10.0" From 41b9b0e5bd354c495205dd501c5966a3398cec04 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Tue, 4 Mar 2025 10:40:08 +0100 Subject: [PATCH 029/133] update changelog --- CHANGELOG.md | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 293f70002b..50bad8f2cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,96 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ## [Unreleased] +## [2025.4-dorina] (2025-03-04) + +- fixed sphinx version metrics registration ([#5546]) +- Feature/chain status api ([#5539]) +- Add SURBs soft threshold ([#5535]) +- Simplify IPR v8 ([#5532]) +- Shared instance for DNS AsyncResolver ([#5523]) +- merge #5512 again after reverting due to incorrect rebase ([#5520]) +- cherry-pick 17d3ff2d775f61aee381d90a304ed416c08f33fc onto dorina ([#5519]) +- cherry-pick 6e5d0dac1b75413c5f09122b0d953f8ec6ef48df onto dorina ([#5518]) +- chore: workspace global panic preventing lints ([#5512]) +- bugfix: dont query for ecash apis unless necessary when spending ticketbooks ([#5508]) +- bugfix: bound check when recovering a reply SURB ([#5502]) +- chore: removed all old coconut code ([#5500]) +- IPR request types v8 ([#5498]) +- Support static routes for HTTP requests ([#5487]) +- build(deps): bump the patch-updates group across 1 directory with 3 updates ([#5482]) +- added missing import to doctest ([#5480]) +- adjusted TestSetup::new_complex to ensure bonded node's existence ([#5478]) +- Trigger contracts CI on main workspace Cargo changes ([#5477]) +- build(deps): bump http from 1.1.0 to 1.2.0 ([#5472]) +- build(deps): bump utoipa-swagger-ui from 8.0.3 to 8.1.0 ([#5471]) +- build(deps): bump colored from 2.1.0 to 2.2.0 ([#5470]) +- build(deps): bump celes from 2.4.0 to 2.5.0 ([#5469]) +- build(deps): bump the patch-updates group with 2 updates ([#5467]) +- build(deps): bump elliptic from 6.5.4 to 6.6.1 in /docker/typescript_client/upload_contract ([#5463]) +- Run cargo autoinherit ([#5460]) +- Fix clippy::precedence ([#5457]) +- Provide Interval context with node descriptor endpoints ([#5456]) +- fix: update fx average rate calcs to ignore 0 values ([#5454]) +- Feature/add gbp currency ([#5453]) +- Add helper to extract a list of sqlite files with journal files wal/shm ([#5452]) +- Add a middleware layer to the nym api allowing for data compression ([#5451]) +- Condense core API functionalities and enable gzip decompression for reqwest payloads ([#5450]) +- build(deps): bump uniffi_build from 0.25.3 to 0.29.0 ([#5448]) +- Upgrade tower to 0.5.2 ([#5446]) +- build(deps): bump hickory-proto from 0.24.2 to 0.24.3 ([#5444]) +- Seedable clients ([#5440]) +- build(deps): bump the patch-updates group across 1 directory with 10 updates ([#5439]) +- Remove all recv_with_delay and add shutdown condition to loops in client-core ([#5435]) +- Disable the test for checking the remaining bandwidth in nym-node-status-api ([#5425]) +- Dz nym node stats ([#5418]) +- build(deps): bump hyper from 1.4.1 to 1.6.0 ([#5416]) +- build(deps): bump publicsuffix from 2.2.3 to 2.3.0 ([#5367]) +- Nymnode entrypoint docker ([#5300]) + +[#5546]: https://github.com/nymtech/nym/pull/5546 +[#5539]: https://github.com/nymtech/nym/pull/5539 +[#5535]: https://github.com/nymtech/nym/pull/5535 +[#5532]: https://github.com/nymtech/nym/pull/5532 +[#5523]: https://github.com/nymtech/nym/pull/5523 +[#5520]: https://github.com/nymtech/nym/pull/5520 +[#5519]: https://github.com/nymtech/nym/pull/5519 +[#5518]: https://github.com/nymtech/nym/pull/5518 +[#5512]: https://github.com/nymtech/nym/pull/5512 +[#5508]: https://github.com/nymtech/nym/pull/5508 +[#5502]: https://github.com/nymtech/nym/pull/5502 +[#5500]: https://github.com/nymtech/nym/pull/5500 +[#5498]: https://github.com/nymtech/nym/pull/5498 +[#5487]: https://github.com/nymtech/nym/pull/5487 +[#5482]: https://github.com/nymtech/nym/pull/5482 +[#5480]: https://github.com/nymtech/nym/pull/5480 +[#5478]: https://github.com/nymtech/nym/pull/5478 +[#5477]: https://github.com/nymtech/nym/pull/5477 +[#5472]: https://github.com/nymtech/nym/pull/5472 +[#5471]: https://github.com/nymtech/nym/pull/5471 +[#5470]: https://github.com/nymtech/nym/pull/5470 +[#5469]: https://github.com/nymtech/nym/pull/5469 +[#5467]: https://github.com/nymtech/nym/pull/5467 +[#5463]: https://github.com/nymtech/nym/pull/5463 +[#5460]: https://github.com/nymtech/nym/pull/5460 +[#5457]: https://github.com/nymtech/nym/pull/5457 +[#5456]: https://github.com/nymtech/nym/pull/5456 +[#5454]: https://github.com/nymtech/nym/pull/5454 +[#5453]: https://github.com/nymtech/nym/pull/5453 +[#5452]: https://github.com/nymtech/nym/pull/5452 +[#5451]: https://github.com/nymtech/nym/pull/5451 +[#5450]: https://github.com/nymtech/nym/pull/5450 +[#5448]: https://github.com/nymtech/nym/pull/5448 +[#5446]: https://github.com/nymtech/nym/pull/5446 +[#5444]: https://github.com/nymtech/nym/pull/5444 +[#5440]: https://github.com/nymtech/nym/pull/5440 +[#5439]: https://github.com/nymtech/nym/pull/5439 +[#5435]: https://github.com/nymtech/nym/pull/5435 +[#5425]: https://github.com/nymtech/nym/pull/5425 +[#5418]: https://github.com/nymtech/nym/pull/5418 +[#5416]: https://github.com/nymtech/nym/pull/5416 +[#5367]: https://github.com/nymtech/nym/pull/5367 +[#5300]: https://github.com/nymtech/nym/pull/5300 + ## [2025.3-ruta] (2025-02-10) - Push down forget me to client configs ([#5431]) From 42d559bc69a7dd5cb617a0cabbb2a8d98e43ce01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 4 Mar 2025 10:46:12 +0000 Subject: [PATCH 030/133] fix prometheus metric naming test due to changes to packet version scheme --- nym-node/nym-node-metrics/src/prometheus_wrapper.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nym-node/nym-node-metrics/src/prometheus_wrapper.rs b/nym-node/nym-node-metrics/src/prometheus_wrapper.rs index e6c18c6f38..f634e7c644 100644 --- a/nym-node/nym-node-metrics/src/prometheus_wrapper.rs +++ b/nym-node/nym-node-metrics/src/prometheus_wrapper.rs @@ -419,7 +419,7 @@ mod tests { } .to_string(); assert_eq!( - "nym_node_mixnet_ingress_packet_version_sphinx-42", + "nym_node_mixnet_ingress_packet_version_sphinx_42", parameterised ); } From f623bbd57c6ae4a0db1344603d3a653af3d3192c Mon Sep 17 00:00:00 2001 From: Tommy Verrall Date: Tue, 4 Mar 2025 18:06:01 +0100 Subject: [PATCH 031/133] wireguard exit policy rules --- .../exit-policy-tests.sh | 227 ++++++ .../exit-policy-wireguard.sh | 676 ++++++++++++++++++ .../validate-exit-blocking-test.sh | 40 ++ 3 files changed, 943 insertions(+) create mode 100644 scripts/wireguard-exit-policy/exit-policy-tests.sh create mode 100644 scripts/wireguard-exit-policy/exit-policy-wireguard.sh create mode 100644 scripts/wireguard-exit-policy/validate-exit-blocking-test.sh diff --git a/scripts/wireguard-exit-policy/exit-policy-tests.sh b/scripts/wireguard-exit-policy/exit-policy-tests.sh new file mode 100644 index 0000000000..ac57dc123d --- /dev/null +++ b/scripts/wireguard-exit-policy/exit-policy-tests.sh @@ -0,0 +1,227 @@ +#!/bin/bash +# Nym Exit Policy Verification Unit Tests + +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +NYM_CHAIN="NYM-EXIT" +WG_INTERFACE="nymwg" + +# Function to check port range rules - with sampling for large ranges +check_port_range_rules() { + local port_range="$1" + local protocol="${2:-tcp}" + local chain="${3:-$NYM_CHAIN}" + local start_port=$(echo "$port_range" | cut -d'-' -f1) + local end_port=$(echo "$port_range" | cut -d'-' -f2) + local range_size=$((end_port - start_port + 1)) + local failures=0 + local sample_size=5 + local step=1 + + # For large ranges, test only a sample of ports + if [ $range_size -gt $sample_size ]; then + step=$((range_size / sample_size)) + # Ensure we test the start and end ports + local sample_ports=($start_port $end_port) + + for ((i=1; i/dev/null; then + echo -e "${RED}✗ Rule missing: $chain $protocol port $port${NC}" + ((failures++)) + fi + fi + done + else + for ((port=start_port; port<=end_port; port++)); do + if ! iptables -t filter -C "$chain" -p "$protocol" --dport "$port" -j ACCEPT 2>/dev/null; then + echo -e "${RED}✗ Rule missing: $chain $protocol port $port${NC}" + ((failures++)) + fi + done + fi + + if [ $failures -eq 0 ]; then + echo -e "${GREEN}✓ Rule exists: $chain $protocol port range $port_range${NC}" + return 0 + else + echo -e "${RED}✗ Some rules missing in port range $port_range${NC}" + return 1 + fi +} + +# Test port range rules +test_port_range_rules() { + echo -e "${YELLOW}Testing Port Range Rules...${NC}" + + # Select the essential port ranges for testing + local port_ranges=( + "20-21:tcp:FTP" + "80-81:tcp:HTTP" + "2082-2083:tcp:CPanel" + "5222-5223:tcp:XMPP" + "27000-27050:tcp:Steam (sampling)" + "989-990:tcp:FTP over TLS" + "5000-5005:tcp:RTP/VoIP" + "8087-8088:tcp:Simplify Media" + "8232-8233:tcp:Zcash" + "8332-8333:tcp:Bitcoin" + ) + + local total_failures=0 + + for range in "${port_ranges[@]}"; do + IFS=':' read -r port_range protocol service <<< "$range" + + echo -e "${YELLOW}Testing $service $protocol port range $port_range${NC}" + + check_port_range_rules "$port_range" "$protocol" + failures=$? + + total_failures=$((total_failures + failures)) + done + + return $total_failures +} + +# Test critical services +test_critical_services() { + echo -e "${YELLOW}Testing Critical Service Rules...${NC}" + + local tcp_services=( + 22 # SSH + 53 # DNS + 80 # HTTP + 443 # HTTPS + 853 # DNS over TLS + 1194 # OpenVPN + ) + + local udp_services=( + 53 # DNS + 123 # NTP + 1194 # OpenVPN + ) + + local failures=0 + + # Test TCP services + for port in "${tcp_services[@]}"; do + if ! iptables -t filter -C "$NYM_CHAIN" -p tcp --dport "$port" -j ACCEPT 2>/dev/null; then + echo -e "${RED}✗ Rule missing: NYM-EXIT tcp port $port${NC}" + ((failures++)) + else + echo -e "${GREEN}✓ Rule exists: NYM-EXIT tcp port $port${NC}" + fi + done + + # Test UDP services + for port in "${udp_services[@]}"; do + if ! iptables -t filter -C "$NYM_CHAIN" -p udp --dport "$port" -j ACCEPT 2>/dev/null; then + echo -e "${RED}✗ Rule missing: NYM-EXIT udp port $port${NC}" + ((failures++)) + else + echo -e "${GREEN}✓ Rule exists: NYM-EXIT udp port $port${NC}" + fi + done + + return $failures +} + +# Verify default reject rule exists - with improved detection +test_default_reject_rule() { + echo -e "${YELLOW}Testing Default Reject Rule...${NC}" + + # Try different patterns to detect the reject rule + if iptables -L "$NYM_CHAIN" | grep -q "REJECT.*all.*anywhere.*anywhere.*reject-with"; then + echo -e "${GREEN}✓ Default REJECT rule exists${NC}" + return 0 + elif iptables -L "$NYM_CHAIN" | grep -q "REJECT.*all -- .*everywhere.*everywhere"; then + echo -e "${GREEN}✓ Default REJECT rule exists${NC}" + return 0 + elif iptables -L "$NYM_CHAIN" | grep -q "REJECT.*all.*0.0.0.0/0.*0.0.0.0/0"; then + echo -e "${GREEN}✓ Default REJECT rule exists${NC}" + return 0 + elif iptables -n -L "$NYM_CHAIN" | grep -qE "REJECT.*all.*0.0.0.0/0.*0.0.0.0/0"; then + echo -e "${GREEN}✓ Default REJECT rule exists${NC}" + return 0 + elif iptables -L "$NYM_CHAIN" | tail -1 | grep -q "REJECT"; then + echo -e "${GREEN}✓ Default REJECT rule exists at the end of chain${NC}" + return 0 + else + echo -e "${RED}✗ Default REJECT rule missing${NC}" + # Display the last 3 rules in the chain for debugging + echo -e "${YELLOW}Last 3 rules in the chain:${NC}" + iptables -L "$NYM_CHAIN" | tail -3 + return 1 + fi +} + +run_all_tests() { + local total_failures=0 + local total_tests=0 + local skip_default_reject=false + + # Parse arguments + while [[ $# -gt 0 ]]; do + case "$1" in + --skip-default-reject) + skip_default_reject=true + shift + ;; + *) + echo -e "${RED}Unknown argument: $1${NC}" + exit 1 + ;; + esac + done + + local test_functions=( + "test_port_range_rules" + "test_critical_services" + ) + + if [ "$skip_default_reject" = false ]; then + test_functions+=("test_default_reject_rule") + fi + + echo -e "${YELLOW}Running Nym Exit Policy Verification Tests...${NC}" + + for test_func in "${test_functions[@]}"; do + ((total_tests++)) + $test_func + if [ $? -ne 0 ]; then + ((total_failures++)) + echo -e "${RED}Test $test_func FAILED${NC}" + else + echo -e "${GREEN}Test $test_func PASSED${NC}" + fi + done + + echo -e "\n${YELLOW}Test Summary:${NC}" + echo -e "Total Tests: $total_tests" + echo -e "Failures: $total_failures" + + if [ $total_failures -eq 0 ]; then + echo -e "${GREEN}All Tests Passed Successfully!${NC}" + exit 0 + else + echo -e "${RED}Some Tests Failed. Please review the iptables configuration.${NC}" + exit 1 + fi +} + +if [[ $EUID -ne 0 ]]; then + echo -e "${RED}This script must be run as root${NC}" + exit 1 +fi + +# Run the tests +run_all_tests "$@" \ No newline at end of file diff --git a/scripts/wireguard-exit-policy/exit-policy-wireguard.sh b/scripts/wireguard-exit-policy/exit-policy-wireguard.sh new file mode 100644 index 0000000000..d8a2c200d9 --- /dev/null +++ b/scripts/wireguard-exit-policy/exit-policy-wireguard.sh @@ -0,0 +1,676 @@ +#!/bin/bash +# +# Nym Wireguard Exit Policy Manager +# Version: 1.1.0 +# +# This script manages iptables rules for Nym Wireguard exit policies +# Features: +# - Implements the Nym exit policy from official documentation +# - Makes rules persistent across reboots +# - Provides commands to inspect and manage rules +# - Groups rules logically for easier management +# - Integrates with existing Nym node configuration +# +# Usage: ./nym-exit-policy.sh [command] + +set -e + +NETWORK_DEVICE=$(ip route show default | awk '/default/ {print $5}') +WG_INTERFACE="nymwg" +NYM_CHAIN="NYM-EXIT" +POLICY_FILE="/etc/nym/exit-policy.txt" +EXIT_POLICY_LOCATION="https://nymtech.net/.wellknown/network-requester/exit-policy.txt" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +add_port_rules() { + local chain="$1" + local port="$2" + local protocol="${3:-tcp}" + + # Check if the port contains a range + if [[ "$port" == *"-"* ]]; then + # Port range handling - add as a single rule with a range + local start_port=$(echo "$port" | cut -d'-' -f1) + local end_port=$(echo "$port" | cut -d'-' -f2) + + # Add as a single rule with a port range + if ! $chain -C "$NYM_CHAIN" -p "$protocol" --dport "$start_port:$end_port" -j ACCEPT 2>/dev/null; then + $chain -A "$NYM_CHAIN" -p "$protocol" --dport "$start_port:$end_port" -j ACCEPT + echo -e " ${GREEN}Added: $NYM_CHAIN $protocol port range $start_port:$end_port${NC}" + fi + else + # Single port handling + if ! $chain -C "$NYM_CHAIN" -p "$protocol" --dport "$port" -j ACCEPT 2>/dev/null; then + $chain -A "$NYM_CHAIN" -p "$protocol" --dport "$port" -j ACCEPT + echo -e " ${GREEN}Added: $NYM_CHAIN $protocol port $port${NC}" + fi + fi +} + +install_dependencies() { + if ! dpkg -s iptables-persistent >/dev/null 2>&1; then + echo -e "${YELLOW}Installing iptables-persistent...${NC}" + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y iptables-persistent + echo -e "${GREEN}iptables-persistent installed.${NC}" + else + echo -e "${GREEN}iptables-persistent is already installed.${NC}" + fi + + # Check for other required dependencies + for cmd in iptables ip6tables ip grep sed awk wget curl; do + if ! command -v "$cmd" &>/dev/null; then + echo -e "${YELLOW}Installing $cmd...${NC}" + apt-get install -y "$cmd" + fi + done +} + +configure_ip_forwarding() { + echo -e "${YELLOW}Configuring IP forwarding...${NC}" + + # Remove any existing forwarding settings to avoid duplicates + sed -i "/^net.ipv6.conf.all.forwarding=/d" /etc/sysctl.conf + sed -i "/^net.ipv4.ip_forward=/d" /etc/sysctl.conf + + # Add forwarding settings + echo "net.ipv6.conf.all.forwarding=1" | tee -a /etc/sysctl.conf + echo "net.ipv4.ip_forward=1" | tee -a /etc/sysctl.conf + + # Apply changes + sysctl -p /etc/sysctl.conf + + # Verify settings + ipv4_forwarding=$(cat /proc/sys/net/ipv4/ip_forward) + ipv6_forwarding=$(cat /proc/sys/net/ipv6/conf/all/forwarding) + + if [ "$ipv4_forwarding" == "1" ] && [ "$ipv6_forwarding" == "1" ]; then + echo -e "${GREEN}IP forwarding configured successfully.${NC}" + else + echo -e "${RED}Failed to configure IP forwarding.${NC}" + exit 1 + fi +} + +create_nym_chain() { + echo -e "${YELLOW}Creating Nym exit policy chain...${NC}" + + # Check if the chain already exists + if iptables -L "$NYM_CHAIN" &>/dev/null; then + echo -e "${YELLOW}Chain $NYM_CHAIN already exists. Flushing it...${NC}" + iptables -F "$NYM_CHAIN" + else + echo -e "${YELLOW}Creating chain $NYM_CHAIN...${NC}" + iptables -N "$NYM_CHAIN" + fi + + # Do the same for IPv6 + if ip6tables -L "$NYM_CHAIN" &>/dev/null; then + echo -e "${YELLOW}Chain $NYM_CHAIN already exists in ip6tables. Flushing it...${NC}" + ip6tables -F "$NYM_CHAIN" + else + echo -e "${YELLOW}Creating chain $NYM_CHAIN in ip6tables...${NC}" + ip6tables -N "$NYM_CHAIN" + fi + + # Link it to the FORWARD chain if not already linked + if ! iptables -C FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null; then + echo -e "${YELLOW}Linking $NYM_CHAIN to FORWARD chain...${NC}" + iptables -A FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" + fi + + # Link IPv6 chain + if ! ip6tables -C FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null; then + echo -e "${YELLOW}Linking $NYM_CHAIN to IPv6 FORWARD chain...${NC}" + ip6tables -A FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" + fi +} + +setup_nat_rules() { + echo -e "${YELLOW}Setting up NAT rules...${NC}" + + # Check if NAT rule for IPv4 exists + if ! iptables -t nat -C POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE 2>/dev/null; then + iptables -t nat -A POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE + echo -e "${GREEN}Added IPv4 NAT rule.${NC}" + else + echo -e "${GREEN}IPv4 NAT rule already exists.${NC}" + fi + + # Check if NAT rule for IPv6 exists + if ! ip6tables -t nat -C POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE 2>/dev/null; then + ip6tables -t nat -A POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE + echo -e "${GREEN}Added IPv6 NAT rule.${NC}" + else + echo -e "${GREEN}IPv6 NAT rule already exists.${NC}" + fi + + # Setup forwarding rules for Wireguard interface + if ! iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null; then + iptables -A FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT + echo -e "${GREEN}Added IPv4 forwarding rule (WG → Internet).${NC}" + fi + + if ! iptables -C FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then + iptables -A FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT + echo -e "${GREEN}Added IPv4 forwarding rule (Internet → WG for established connections).${NC}" + fi + + # IPv6 forwarding rules + if ! ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null; then + ip6tables -A FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT + echo -e "${GREEN}Added IPv6 forwarding rule (WG → Internet).${NC}" + fi + + if ! ip6tables -C FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then + ip6tables -A FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT + echo -e "${GREEN}Added IPv6 forwarding rule (Internet → WG for established connections).${NC}" + fi +} + +configure_dns_and_icmp() { + echo -e "${YELLOW}Configuring DNS and ICMP rules...${NC}" + + # ICMP rules for ping + if ! iptables -C INPUT -p icmp --icmp-type echo-request -j ACCEPT 2>/dev/null; then + iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT + echo -e "${GREEN}Added IPv4 ICMP rule (allow ping requests).${NC}" + fi + + if ! iptables -C OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT 2>/dev/null; then + iptables -A OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT + echo -e "${GREEN}Added IPv4 ICMP rule (allow ping replies).${NC}" + fi + + # ICMPv6 rules for ping6 + if ! ip6tables -C INPUT -p ipv6-icmp -j ACCEPT 2>/dev/null; then + ip6tables -A INPUT -p ipv6-icmp -j ACCEPT + echo -e "${GREEN}Added IPv6 ICMP rule (allow ping6).${NC}" + fi + + # DNS rules + if ! iptables -C INPUT -p udp --dport 53 -j ACCEPT 2>/dev/null; then + iptables -A INPUT -p udp --dport 53 -j ACCEPT + echo -e "${GREEN}Added IPv4 DNS rule (UDP).${NC}" + fi + + if ! iptables -C INPUT -p tcp --dport 53 -j ACCEPT 2>/dev/null; then + iptables -A INPUT -p tcp --dport 53 -j ACCEPT + echo -e "${GREEN}Added IPv4 DNS rule (TCP).${NC}" + fi + + # IPv6 DNS rules + if ! ip6tables -C INPUT -p udp --dport 53 -j ACCEPT 2>/dev/null; then + ip6tables -A INPUT -p udp --dport 53 -j ACCEPT + echo -e "${GREEN}Added IPv6 DNS rule (UDP).${NC}" + fi + + if ! ip6tables -C INPUT -p tcp --dport 53 -j ACCEPT 2>/dev/null; then + ip6tables -A INPUT -p tcp --dport 53 -j ACCEPT + echo -e "${GREEN}Added IPv6 DNS rule (TCP).${NC}" + fi +} + +# Apply Spamhaus blocklist from the Nym exit policy +apply_spamhaus_blocklist() { + echo -e "${YELLOW}Applying Spamhaus blocklist...${NC}" + + # Create directory if not exists + mkdir -p "$(dirname "$POLICY_FILE")" + + # Try to download the policy file + echo -e "${YELLOW}Downloading exit policy from $EXIT_POLICY_LOCATION${NC}" + if ! wget -q "$EXIT_POLICY_LOCATION" -O "$POLICY_FILE" 2>/dev/null; then + echo -e "${RED}Failed to download exit policy. Using minimal blocklist.${NC}" + + # Create a minimal policy file with a few entries + cat >"$POLICY_FILE" </dev/null; then + iptables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT + fi + + # Apply IPv6 rules for IPv6 addresses + if [[ "$ip_range" == *":"* ]] && ! ip6tables -C "$NYM_CHAIN" -d "$ip_range" -j REJECT 2>/dev/null; then + ip6tables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT + fi + fi + done + + echo -e "${GREEN}Blocklist applied successfully.${NC}" +} + +add_default_reject_rule() { + echo -e "${YELLOW}Adding default reject rule...${NC}" + + # First remove any existing plain reject rules (without specific destinations) + iptables -D "$NYM_CHAIN" -j REJECT 2>/dev/null || true + iptables -D "$NYM_CHAIN" -j REJECT --reject-with icmp-port-unreachable 2>/dev/null || true + ip6tables -D "$NYM_CHAIN" -j REJECT 2>/dev/null || true + ip6tables -D "$NYM_CHAIN" -j REJECT --reject-with icmp6-port-unreachable 2>/dev/null || true + + # Add the default catch-all reject rule (must be the last rule in the chain) + iptables -A "$NYM_CHAIN" -j REJECT --reject-with icmp-port-unreachable + ip6tables -A "$NYM_CHAIN" -j REJECT --reject-with icmp6-port-unreachable + + echo -e "${GREEN}Default reject rule added successfully.${NC}" +} + +apply_port_allowlist() { + echo -e "${YELLOW}Applying allowed ports...${NC}" + + # Dictionary of services and their ports + declare -A PORT_MAPPINGS=( + ["FTP"]="20-21" + ["SSH"]="22" + ["WHOIS"]="43" + ["DNS"]="53" + ["Finger"]="79" + ["HTTP"]="80-81" + ["Kerberos"]="88" + ["POP3"]="110" + ["NTP"]="123" + ["IMAP"]="143" + ["IMAP3"]="220" + ["LDAP"]="389" + ["HTTPS"]="443" + ["SMBWindowsFileShare"]="445" + ["Kpasswd"]="464" + ["RTSP"]="554" + ["LDAPS"]="636" + ["SILC"]="706" + ["KerberosAdmin"]="749" + ["DNSOverTLS"]="853" + ["Rsync"]="873" + ["VMware"]="902-904" + ["RemoteHTTPS"]="981" + ["FTPOverTLS"]="989-990" + ["NetnewsAdmin"]="991" + ["TelnetOverTLS"]="992" + ["IMAPOverTLS"]="993" + ["POP3OverTLS"]="995" + ["OpenVPN"]="1194" + ["QTServerAdmin"]="1220" + ["PKTKRB"]="1293" + ["MSSQL"]="1433" + ["VLSILicenseManager"]="1500" + ["OracleDB"]="1521" + ["Sametime"]="1533" + ["GroupWise"]="1677" + ["PPTP"]="1723" + ["RTSPAlt"]="1755" + ["MSNP"]="1863" + ["NFS"]="2049" + ["CPanel"]="2082-2083" + ["GNUnet"]="2086-2087" + ["NBX"]="2095-2096" + ["Zephyr"]="2102-2104" + ["XboxLive"]="3074" + ["MySQL"]="3306" + ["SVN"]="3690" + ["RWHOIS"]="4321" + ["Virtuozzo"]="4643" + ["RTPVOIP"]="5000-5005" + ["MMCC"]="5050" + ["ICQ"]="5190" + ["XMPP"]="5222-5223" + ["AndroidMarket"]="5228" + ["PostgreSQL"]="5432" + ["MongoDBDefault"]="27017" + ["Electrum"]="8082" + ["SimplifyMedia"]="8087-8088" + ["Zcash"]="8232-8233" + ["Bitcoin"]="8332-8333" + ["HTTPSALT"]="8443" + ["TeamSpeak"]="8767" + ["MQTTS"]="8883" + ["HTTPProxy"]="8888" + ["TorORPort"]="9001" + ["TorDirPort"]="9030" + ["Tari"]="9053" + ["Gaming"]="9339" + ["Git"]="9418" + ["HTTPSALT2"]="9443" + ["Lightning"]="9735" + ["NDMP"]="10000" + ["OpenPGP"]="11371" + ["GoogleVoice"]="19294" + ["EnsimControlPanel"]="19638" + ["Minecraft"]="25565" + ["Steam"]="27000-27050" + ["ElectrumSSL"]="50002" + ["MOSH"]="60000-61000" + ["Mumble"]="64738" + ) + + # Add TCP and UDP rules for each allowed port + for service in "${!PORT_MAPPINGS[@]}"; do + port="${PORT_MAPPINGS[$service]}" + echo -e "${YELLOW}Adding rules for $service (Port: $port)${NC}" + + # Add both TCP and UDP rules for all services + add_port_rules iptables "$port" "tcp" + add_port_rules ip6tables "$port" "tcp" + add_port_rules iptables "$port" "udp" + add_port_rules ip6tables "$port" "udp" + done + + add_default_reject_rule + + echo -e "${GREEN}Port allowlist applied successfully.${NC}" +} + +safe_iptables_rule_remove() { + local chain="$1" + local table="${2:-filter}" + local interface="$3" + + # Remove rule if it exists + if iptables -t "$table" -C "$chain" -o "$interface" -j "$NYM_CHAIN" 2>/dev/null; then + iptables -t "$table" -D "$chain" -o "$interface" -j "$NYM_CHAIN" + fi +} + +safe_ip6tables_rule_remove() { + local chain="$1" + local table="${2:-filter}" + local interface="$3" + + # Remove rule if it exists + if ip6tables -t "$table" -C "$chain" -o "$interface" -j "$NYM_CHAIN" 2>/dev/null; then + ip6tables -t "$table" -D "$chain" -o "$interface" -j "$NYM_CHAIN" + fi +} + +clear_rules() { + echo -e "${YELLOW}Clearing Nym exit policy rules...${NC}" + + # Flush all rules in the NYM-EXIT chain + iptables -F "$NYM_CHAIN" 2>/dev/null || true + ip6tables -F "$NYM_CHAIN" 2>/dev/null || true + + # Remove the chain from FORWARD if it exists + iptables -D FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null || true + ip6tables -D FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null || true + + # Delete the chains + iptables -X "$NYM_CHAIN" 2>/dev/null || true + ip6tables -X "$NYM_CHAIN" 2>/dev/null || true + + echo -e "${GREEN}Nym exit policy rules cleared successfully.${NC}" +} + +remove_duplicate_rules() { + local interface="$1" + + if [[ -z "$interface" ]]; then + echo -e "${RED}Error: No interface specified. Usage: $0 remove-duplicates ${NC}" >&2 + exit 1 + fi + + echo -e "${YELLOW}Detecting and removing duplicate rules for $interface...${NC}" + + # Verbose duplicate rule detection for IPv4 + echo -e "${YELLOW}Checking IPv4 duplicate rules:${NC}" + iptables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" | sort | uniq -d && { + echo -e "${RED}Duplicate IPv4 rules found! Removing...${NC}" + # Remove duplicates by saving unique rules + iptables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" | sort | uniq | while read -r rule; do + # Carefully remove duplicates + full_rule=$(echo "$rule" | sed 's/^-A/iptables -D/') + eval "$full_rule" 2>/dev/null + done + } || echo -e "${GREEN}No duplicate IPv4 rules found.${NC}" + + # Verbose duplicate rule detection for IPv6 + echo -e "${YELLOW}Checking IPv6 duplicate rules:${NC}" + ip6tables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" | sort | uniq -d && { + echo -e "${RED}Duplicate IPv6 rules found! Removing...${NC}" + # Remove duplicates by saving unique rules + ip6tables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" | sort | uniq | while read -r rule; do + # Carefully remove duplicates + full_rule=$(echo "$rule" | sed 's/^-A/ip6tables -D/') + eval "$full_rule" 2>/dev/null + done + } || echo -e "${GREEN}No duplicate IPv6 rules found.${NC}" + + # Additional verification + echo -e "\n${YELLOW}Rule verification:${NC}" + echo "IPv4 Rules:" + iptables -L FORWARD -v -n | grep "$interface" + echo "IPv6 Rules:" + ip6tables -L FORWARD -v -n | grep "$interface" + + echo -e "${GREEN}Duplicate rule removal process completed.${NC}" +} + +save_rules() { + echo -e "${YELLOW}Saving iptables rules to make them persistent...${NC}" + + if [ -d "/etc/iptables" ]; then + # For Debian/Ubuntu with iptables-persistent + iptables-save >/etc/iptables/rules.v4 + ip6tables-save >/etc/iptables/rules.v6 + echo -e "${GREEN}Rules saved to /etc/iptables/rules.v4 and /etc/iptables/rules.v6${NC}" + else + # Fallback method + iptables-save >/etc/iptables.rules + ip6tables-save >/etc/ip6tables.rules + echo -e "${GREEN}Rules saved to /etc/iptables.rules and /etc/ip6tables.rules${NC}" + + # Add loading script to rc.local if it doesn't exist + if [ ! -f "/etc/network/if-pre-up.d/iptables" ]; then + cat >/etc/network/if-pre-up.d/iptables </dev/null; then + echo -e "${RED}WARNING: Wireguard interface $WG_INTERFACE not found!${NC}" + return 1 + fi + + # Interface details + echo -e "\n${YELLOW}Interface Details:${NC}" + ip link show "$WG_INTERFACE" + + # IP Addresses + echo -e "\n${YELLOW}IP Addresses:${NC}" + ip -4 addr show dev "$WG_INTERFACE" + ip -6 addr show dev "$WG_INTERFACE" + + # Iptables Chain Status + echo -e "\n${YELLOW}Iptables Chains:${NC}" + { + echo "IPv4 Chain:" + iptables -L "$NYM_CHAIN" -n -v + echo -e "\nIPv6 Chain:" + ip6tables -L "$NYM_CHAIN" -n -v + } || echo "One or both chains not found" + + # Forwarding Status + echo -e "\n${YELLOW}IP Forwarding:${NC}" + echo "IPv4: $(cat /proc/sys/net/ipv4/ip_forward)" + echo "IPv6: $(cat /proc/sys/net/ipv6/conf/all/forwarding)" +} + +test_connectivity() { + echo -e "${YELLOW}Testing connectivity through $WG_INTERFACE...${NC}" + + # More comprehensive interface check + interface_info=$(ip link show "$WG_INTERFACE" 2>/dev/null) + + if [ -z "$interface_info" ]; then + echo -e "${RED}Interface $WG_INTERFACE not found!${NC}" + return 1 + fi + + # Check for multiple possible interface states + if ! echo "$interface_info" | grep -qE "state (UP|UNKNOWN|DORMANT)"; then + echo -e "${RED}Interface $WG_INTERFACE is not in an active state!${NC}" + echo "$interface_info" + return 1 + fi + + # Detailed interface information + echo -e "${GREEN}Interface Details:${NC}" + echo "$interface_info" + + # Get IP addresses with more robust method + ipv4_address=$(ip -4 addr show dev "$WG_INTERFACE" | grep -oP '(?<=inet\s)\d+\.\d+\.\d+\.\d+/\d+' | cut -d'/' -f1 | head -n1) + ipv6_address=$(ip -6 addr show dev "$WG_INTERFACE" scope global | grep -oP '(?<=inet6\s)[0-9a-f:]+/\d+' | cut -d'/' -f1 | head -n1) + + echo -e "${GREEN}IPv4 Address:${NC} ${ipv4_address:-Not found}" + echo -e "${GREEN}IPv6 Address:${NC} ${ipv6_address:-Not found}" + + # Connectivity tests + if [[ -n "$ipv4_address" ]]; then + echo -e "${YELLOW}Testing IPv4 connectivity from $ipv4_address...${NC}" + + # Ping test + if timeout 5 ping -c 3 -I "$ipv4_address" 8.8.8.8 >/dev/null 2>&1; then + echo -e "${GREEN}IPv4 connectivity to 8.8.8.8: Success${NC}" + else + echo -e "${RED}IPv4 connectivity to 8.8.8.8: Failed${NC}" + fi + + # DNS resolution test + if timeout 5 ping -c 3 -I "$ipv4_address" google.com >/dev/null 2>&1; then + echo -e "${GREEN}IPv4 DNS resolution: Success${NC}" + else + echo -e "${RED}IPv4 DNS resolution: Failed${NC}" + fi + + # HTTP(S) connectivity test + if command -v curl &>/dev/null; then + if timeout 5 curl -s --interface "$ipv4_address" -o /dev/null -w "%{http_code}" https://www.google.com | grep -q "200"; then + echo -e "${GREEN}IPv4 HTTPS connectivity: Success${NC}" + else + echo -e "${RED}IPv4 HTTPS connectivity: Failed${NC}" + fi + fi + else + echo -e "${RED}No IPv4 address configured on $WG_INTERFACE${NC}" + fi + + # Similar tests for IPv6 if available + if [[ -n "$ipv6_address" ]]; then + echo -e "${YELLOW}Testing IPv6 connectivity from $ipv6_address...${NC}" + + if timeout 5 ping6 -c 3 -I "$ipv6_address" 2001:4860:4860::8888 >/dev/null 2>&1; then + echo -e "${GREEN}IPv6 connectivity to Google DNS: Success${NC}" + else + echo -e "${RED}IPv6 connectivity to Google DNS: Failed${NC}" + fi + + if timeout 5 ping6 -c 3 -I "$ipv6_address" google.com >/dev/null 2>&1; then + echo -e "${GREEN}IPv6 DNS resolution: Success${NC}" + else + echo -e "${RED}IPv6 DNS resolution: Failed${NC}" + fi + + if command -v curl &>/dev/null; then + if timeout 5 curl -s --interface "$ipv6_address" -o /dev/null -w "%{http_code}" https://www.google.com | grep -q "200"; then + echo -e "${GREEN}IPv6 HTTPS connectivity: Success${NC}" + else + echo -e "${RED}IPv6 HTTPS connectivity: Failed${NC}" + fi + fi + else + echo -e "${YELLOW}No IPv6 address configured on $WG_INTERFACE${NC}" + fi + + echo -e "${GREEN}Connectivity tests completed.${NC}" +} + +main() { + # Check for root privileges + if [ "$(id -u)" -ne 0 ]; then + echo -e "${RED}This script must be run as root${NC}" >&2 + exit 1 + fi + + # Parse command-line arguments + case "$1" in + install) + install_dependencies + configure_ip_forwarding + create_nym_chain + setup_nat_rules + configure_dns_and_icmp + apply_spamhaus_blocklist + apply_port_allowlist + save_rules + echo -e "${GREEN}Nym exit policy installed successfully.${NC}" + ;; + status) + show_status + ;; + test) + test_connectivity + ;; + clear) + clear_rules + echo -e "${GREEN}Nym exit policy rules cleared.${NC}" + ;; + remove-duplicates) + remove_duplicate_rules "$2" + ;; + help | --help | -h) + echo "Usage: $0 [command]" + echo "" + echo "Commands:" + echo " install Install and configure Nym exit policy" + echo " status Show current Nym exit policy status" + echo " test Test connectivity through Wireguard interface" + echo " clear Remove all Nym exit policy rules" + echo " remove-duplicates Remove duplicate iptables rules for an interface" + echo " help Show this help message" + ;; + *) + echo -e "${RED}Invalid command. Use '$0 help' for usage information.${NC}" >&2 + exit 1 + ;; + esac +} + +main "$@" diff --git a/scripts/wireguard-exit-policy/validate-exit-blocking-test.sh b/scripts/wireguard-exit-policy/validate-exit-blocking-test.sh new file mode 100644 index 0000000000..e93d28a24f --- /dev/null +++ b/scripts/wireguard-exit-policy/validate-exit-blocking-test.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +validate_exit_policy() { + echo "=== Nym Exit Policy Blocking Validation ===" + + # Check iptables rules + echo "Checking iptables NYM-EXIT chain:" + sudo iptables -L NYM-EXIT -v -n + + # Test IP ranges and individual IPs + test_ips=( + "5.188.10.0/24" # Blocked network range + "31.132.36.50" # Specific blocked IP + "37.9.42.100" # Another blocked IP + ) + + for target in "${test_ips[@]}"; do + echo -e "\n\e[33mTesting blocking for $target\e[0m" + + # Multiple connection test methods + methods=( + "ping -c 4 -W 2" + "curl -m 5 http://$target" + "nc -z -w 5 $target 80" + "telnet $target 80" + ) + + for method in "${methods[@]}"; do + echo -n "Testing with $method: " + if sudo timeout 5 $method >/dev/null 2>&1; then + echo -e "\e[31mFAILED: Connection succeeded (Blocking ineffective)\e[0m" + else + echo -e "\e[32mBLOCKED\e[0m" + fi + done + done +} + +# Run the test +validate_exit_policy \ No newline at end of file From 681c054890fd2a32f9adde680c0e65cd2cc8cf23 Mon Sep 17 00:00:00 2001 From: Tommy Verrall Date: Tue, 4 Mar 2025 18:08:26 +0100 Subject: [PATCH 032/133] rename file --- ...xit-policy-wireguard.sh => wireguard-exit-policy-manager.sh} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename scripts/wireguard-exit-policy/{exit-policy-wireguard.sh => wireguard-exit-policy-manager.sh} (99%) diff --git a/scripts/wireguard-exit-policy/exit-policy-wireguard.sh b/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh similarity index 99% rename from scripts/wireguard-exit-policy/exit-policy-wireguard.sh rename to scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh index d8a2c200d9..7445986242 100644 --- a/scripts/wireguard-exit-policy/exit-policy-wireguard.sh +++ b/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh @@ -1,7 +1,7 @@ #!/bin/bash # # Nym Wireguard Exit Policy Manager -# Version: 1.1.0 +# Version: 1.0.0 # # This script manages iptables rules for Nym Wireguard exit policies # Features: From 09ea406c02e9a3beebc062f525e4ea1b4222dcbb Mon Sep 17 00:00:00 2001 From: mfahampshire Date: Wed, 5 Mar 2025 11:39:55 +0000 Subject: [PATCH 033/133] DOCS v2025.4-dorina release notes (#5552) * WIP changelog * [DOCs/operators]: Adding operators notes to new changelog PR(#5564) --------- Co-authored-by: import this <97586125+serinko@users.noreply.github.com> --- .../docs/pages/operators/changelog.mdx | 315 ++++++++++++++---- 1 file changed, 243 insertions(+), 72 deletions(-) diff --git a/documentation/docs/pages/operators/changelog.mdx b/documentation/docs/pages/operators/changelog.mdx index c4c88b6812..85a4d21875 100644 --- a/documentation/docs/pages/operators/changelog.mdx +++ b/documentation/docs/pages/operators/changelog.mdx @@ -47,6 +47,177 @@ This page displays a full list of all the changes during our release cycle from +## `v2025.4-dorina` +- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.4-dorina) +- [`nym-node`](nodes/nym-node.mdx) version `1.6.0` +```shell +Binary Name: nym-node +Build Timestamp: 2025-03-04T09:03:11.322601809Z +Build Version: 1.6.0 +Commit SHA: 7060fa6dad58f17543f5086c73b1854ad1ceae60 +Commit Date: 2025-03-03T17:24:10.000000000Z +Commit Branch: release/2025.4-dorina +rustc Version: 1.86.0-nightly +rustc Channel: nightly +cargo Profile: release +``` + +### Operators Updates & Tools + +- New advanced [guide to virtualise a dedicated server](nodes/preliminary-steps/vps-setup/advanced), providing steps for experienced operators and aspiring sys-admins who seek for higher optimisation and better efficiency of their work orchestrating multiple nodes. +- New Service Grant program is being implemented by operators setting up new ~150 Exit Gateways following the updated [specs for `nym-node`](nodes#minimum-requirements) + +### Features +- [Feature/chain status api](https://github.com/nymtech/nym/pull/5539): +this PR introduces `/v1/network/chain-status` endpoint on nym api to give basic information about the current block as seen by this API (with some caching) and updates `/health` endpoint to give stall information. + +- [Add SURBs soft threshold](https://github.com/nymtech/nym/pull/5535): the IPR should try to keep a buffer of available SURBs to reduce latency. + +- [Simplify IPR v8](https://github.com/nymtech/nym/pull/5532): purge stuff from IPR v8 to simplify; use protocol field in v8. + +- [Shared instance for DNS AsyncResolver](https://github.com/nymtech/nym/pull/5523): +make the `HickoryDnsResolver` use a shared instance by default to limit fd use. Now the multiple `nym_http_api_client::Client`s that get built should take advantage of a shared connection pool for DNS lookups. If for some reason a client does need an independent AsyncResolver this can still be done with the added `thread_resolver` function. + +- [cherry-pick 17d3ff2d775f61aee381d90a304ed416c08f33fc onto dorina](https://github.com/nymtech/nym/pull/5519) + +- [cherry-pick 6e5d0dac1b75413c5f09122b0d953f8ec6ef48df onto dorina](https://github.com/nymtech/nym/pull/5518) + +- [feat: add config option for maximum number of client connections](https://github.com/nymtech/nym/pull/5513) + +- [IPR request types v8](https://github.com/nymtech/nym/pull/5498): Bump IPR request/response types to v8 + +- [Support static routes for HTTP requests](https://github.com/nymtech/nym/pull/5487) + +- [added missing import to doctest](https://github.com/nymtech/nym/pull/5480) + +- [adjusted TestSetup::new_complex to ensure bonded node's existence](https://github.com/nymtech/nym/pull/5478) + +- [Trigger contracts CI on main workspace Cargo changes](https://github.com/nymtech/nym/pull/5477): since the contracts workspace depends on the common code in the main workspace, and since the contracts are critical to not have regressions in, trigger contracts CI on any changes to the workspace Cargo.toml and lock files. + +- [Run cargo autoinherit](https://github.com/nymtech/nym/pull/5460): run `cargo autoinherit` to move a bunch of dependencies to the workspace level. `Cargo.lock` remains untouched. + +- [Disable debug in wasm and wallet workflows too](https://github.com/nymtech/nym/pull/5459) + +- [Fix clippy::precedence](https://github.com/nymtech/nym/pull/5457): fix clippy warnings for the Rust beta toolchain. + +- [Provide Interval context with node descriptor endpoints](https://github.com/nymtech/nym/pull/5456): add current interval context information to existing enpoints using `build_skimmed_nodes_response` under the hood. This allows clients checking for a refresh to send the `epoch_uid` as a query parameter when fetching updates so the server can tell it that there have been no changes, instead of sending duplicate data over and over. The changes in this PR should be backwards compatible and never interfere with existing clients. The additions to the `NodeParams` are optional, so there is no error if a client does not send them. Old clients will not send `epoch_uid` by accident so they cannot accidentally clear their set of known nodes. In the response the status field is optional so if it is missing (e.g. if a new client talks to an old server) the connection still works. For old clients speaking to new servers, the json parsing will simply ignore extra information not included in the objects json spec. + +}> + + Made a request to the `/api/v1/unstable/nym-nodes/semi-skimmed` endpoint: + - without entering an epoch_id + - with entering the current epoch_id + - with entering an old epoch_id + - with entering a future epoch_id + All results returned the same data, as expected + + + +- [Feature/add gbp currency](https://github.com/nymtech/nym/pull/5453) + +- [Add helper to extract a list of sqlite files with journal files wal/shm](https://github.com/nymtech/nym/pull/5452) + +- [Add a middleware layer to the nym api allowing for data compression](https://github.com/nymtech/nym/pull/5451): after Testing in a minimal example, this does work as expected. Routes still default to plain encoding, however if a client indicates support for a compressed encoding using the `Accept-Encoding` header then the served response will be compressed with the appropriate `Content-Encoding` header. ([Proof-of-Concept](https://gist.github.com/jmwample/c15a983e804fc338fee3d1b037d216b0)) + +}> + Sent requests with and without Accept-Encoding: gzip to observe response behaviour + Verified if responses included content-encoding: gzip when compression was requested + Checked response file output to confirm actual compression occurred + + +- [Condense core API functionalities and enable gzip decompression for reqwest payloads](https://github.com/nymtech/nym/pull/5450): this PR is intended to take out variables from our usage of HTTP requests in the `nym-http-api-client`. To do this the PR: (1) adds the `ApiClientCore` trait with the minimal feature required to send a request, (2) turns all request sending into trait extension automatically implemented for any type that implements `ApiClientCore`. This has the benefit of consistent expected behavior for all clients using `nym-http-api-client`, including features added going forward. FOR EXAMPLE this pr adds a default header `"accept-encoding: gzip;q=1.0, *;q=0.5"` which indicates that compression is preferred whenever available. Other features to keep in mind here are things like configurable retries, domain fallbacks, etc. Note: the `Apiclient` interface could be simplified, but that would require refactoring our downstream usages of the API. For now this isn't necessary as `ApiClient` is implemented automatically so it costs nothing to have it this way. It just allows divergent usage in downstream crates. + +}> + Measured nym-api response times before and after updating the API client using curl + Checked if the client automatically decompressed Gzip responses + + +- [Seedable clients](https://github.com/nymtech/nym/pull/5440): Adds `DerivationMaterial` and accompanying methods to builders. `DerivationMaterial` encapsulates parameters for deterministic key derivation using HKDF (SHA-512). Use the `derive_secret()` method to generate a 32-byte secret. To prepare for a new derivation, call the `next()` method which increments the index. **It is the caller's responsibility to track and persist the derivation index if keys need to be rederived.** + + + + ```rust + let master_key = [0u8; 32]; // your secret master key + let salt = "unique-salt-value".to_string(); + let material = DerivationMaterial::new(master_key, 0, salt.as_bytes()); + + // Derive a secret + let secret = material.derive_secret().expect("Failed to derive secret"); + + // Prepare for the next derivation + let next_material = material.next(); + ``` + + + +- [Remove all recv_with_delay and add shutdown condition to loops in client-core](https://github.com/nymtech/nym/pull/5435): inside client-core we want to prepare the ground for moving a behaviour close to what we have in the vpn client. Remove all the recv_with_delay since we want to just stop. Add shutdown condition to all select loops to guard against the shutdown listener being polled inside the select blocks. Remove unwraps when sending on unbounded channels in case the receiver exits before the sender. Move `TaskClient` to be a member field so make it easier to wrap log errors in a shutdown check. Update all fork names to use underscore consistently, since the task separator is hyphen + +}> + Validated that all binaries including `nym-node`, `nym-client`, `nym-network-requester`, and `nym-socks5-client` are behaving well without indicating the presence of any unexpected errors or crashes + + +- [Disable the test for checking the remaining bandwidth in nym-node-status-api](https://github.com/nymtech/nym/pull/5425): this check fails almost every time on CI, possibly due to rate limiting? It's not good to disable the check, but it's blocking CI as it stands now. Given that we have the check above for locating the ip, we at least have a little coverage. + +- [Dz nym node stats](https://github.com/nymtech/nym/pull/5418): removed obsolete fields from stats (blacklisted mixnodes, blacklisted gateways, bonded mixnodes, bonded gateways). Introduced nym-node scraping, beside just mixnodes. `/mixnodes/stats` now returns data for nym-nodes as well, which results in much more accurate "packets mixed" stats. + +- [Nymnode entrypoint docker](https://github.com/nymtech/nym/pull/5300) + +### Bugfix +- [bugfix: dont query for ecash apis unless necessary when spending ticketbooks](https://github.com/nymtech/nym/pull/5508) + +- [bugfix: bound check when recovering a reply SURB](https://github.com/nymtech/nym/pull/5502) + +- [fix: update fx average rate calcs to ignore 0 values](https://github.com/nymtech/nym/pull/5454) + +### Chore +- [chore: workspace global panic preventing lints](https://github.com/nymtech/nym/pull/5512) + +- [chore: removed all old coconut code](https://github.com/nymtech/nym/pull/5500) + +- [build(deps): bump the patch-updates group across 1 directory with 3 updates](https://github.com/nymtech/nym/pull/5482): updates `clap` from 4.5.28 to 4.5.30, updates `clap` from 4.5.28 to 4.5.30, updates `prost` from 0.13.4 to 0.13.5. + +- [build(deps): bump http from 1.1.0 to 1.2.0](https://github.com/nymtech/nym/pull/5472) + +- [build(deps): bump utoipa-swagger-ui from 8.0.3 to 8.1.0](https://github.com/nymtech/nym/pull/5471) + +- [build(deps): bump colored from 2.1.0 to 2.2.0](https://github.com/nymtech/nym/pull/5470) + +- [build(deps): bump celes from 2.4.0 to 2.5.0](https://github.com/nymtech/nym/pull/5469) + +- [build(deps): bump the patch-updates group with 2 updates](https://github.com/nymtech/nym/pull/5467): updates `clap` from 4.5.28 to 4.5.29, updates `prost` from 0.13.4 to 0.13.5. + +- [build(deps): bump elliptic from 6.5.4 to 6.6.1 in /docker/typescript_client/upload_contract](https://github.com/nymtech/nym/pull/5463): bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.4 to 6.6.1. + +- [build(deps): bump uniffi_build from 0.25.3 to 0.29.0](https://github.com/nymtech/nym/pull/5448) + +- [Upgrade tower to 0.5.2](https://github.com/nymtech/nym/pull/5446) + +- [build(deps): bump hickory-proto from 0.24.2 to 0.24.3 in /nym-wallet](https://github.com/nymtech/nym/pull/5445) + +- [build(deps): bump hickory-proto from 0.24.2 to 0.24.3](https://github.com/nymtech/nym/pull/5444) + +- [build(deps): bump the patch-updates group across 1 directory with 10 updates](https://github.com/nymtech/nym/pull/5439): +Bumps the patch-updates group with 10 updates in the directory: + +| Package | From | To | +| --- | --- | --- | +| [async-trait](https://github.com/dtolnay/async-trait) | `0.1.85` | `0.1.86` | +| [clap](https://github.com/clap-rs/clap) | `4.5.27` | `4.5.28` | +| [comfy-table](https://github.com/nukesor/comfy-table) | `7.1.3` | `7.1.4` | +| [hickory-resolver](https://github.com/hickory-dns/hickory-dns) | `0.24.2` | `0.24.3` | +| [once_cell](https://github.com/matklad/once_cell) | `1.20.2` | `1.20.3` | +| [pin-project](https://github.com/taiki-e/pin-project) | `1.1.8` | `1.1.9` | +| [serde_json_path](https://github.com/hiltontj/serde_json_path) | `0.7.1` | `0.7.2` | +| [toml](https://github.com/toml-rs/toml) | `0.8.19` | `0.8.20` | +| [cosmrs](https://github.com/cosmos/cosmos-rust) | `0.21.0` | `0.21.1` | +| [tokio-postgres](https://github.com/sfackler/rust-postgres) | `0.7.12` | `0.7.13` | + +- [build(deps): bump openssl from 0.10.56 to 0.10.70 in /nym-wallet](https://github.com/nymtech/nym/pull/5422) + +- [build(deps): bump hyper from 1.4.1 to 1.6.0](https://github.com/nymtech/nym/pull/5416) + +- [build(deps): bump publicsuffix from 2.2.3 to 2.3.0](https://github.com/nymtech/nym/pull/5367) + ## `v2025.3-ruta` - [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.3-ruta) @@ -97,7 +268,7 @@ As we announced in [`hu` release notes](#service-grant-program-v2), we are opene ##### Locations -These locations and slots are approximate and constantly change based on new operators submissions. Please keep in mind that we are going to chose people with respect to empty slots in the given location at the time of their submission. In case of competing submissions in the same location we will take the one with better specs vs price ratio. +These locations and slots are approximate and constantly change based on new operators submissions. Please keep in mind that we are going to chose people with respect to empty slots in the given location at the time of their submission. In case of competing submissions in the same location we will take the one with better specs vs price ratio. | LOCATION | SLOTS | | :-- | --: | @@ -151,7 +322,7 @@ These locations and slots are approximate and constantly change based on new ope - [Send shutdown instead of panic when reaching max fail](https://github.com/nymtech/nym/pull/5398): Remove a panic and an unwrap inside `client-core` that is hit occasionally in the vpn client. This is a change that can have wide ranging impact since it changes the task handling and it's inside `client-core`, which is used in many components and services, so preventing regressions is important. -- [Relocate a validator api function](https://github.com/nymtech/nym/pull/5401): Adds a function to the `nym-validator-client` crate that hits the network details endpoint and returns an object parsed from `json`. This was floating loose in the `nym-vpn-client` repo. +- [Relocate a validator api function](https://github.com/nymtech/nym/pull/5401): Adds a function to the `nym-validator-client` crate that hits the network details endpoint and returns an object parsed from `json`. This was floating loose in the `nym-vpn-client` repo. - [Bump the patch-updates group across 1 directory with 9 updates](https://github.com/nymtech/nym/pull/5406) @@ -199,7 +370,7 @@ From `nym-node v1.3.0` operators can technically choose multiple functionalities - Updated maintenance guides to [backup](nodes/maintenance#backup-a-node), [restore](nodes/maintenance#restoring-a-node) and [move](nodes/maintenance#moving-a-node) a node, containing a new and important commands to backup and restore `clients.sqlite` database. -- [New explanation of `nym-node` functionalities](nodes/nym-node/setup#functionality-mode) describing how Gateways get selected in Mixnet mode and Wireguard mode. +- [New explanation of `nym-node` functionalities](nodes/nym-node/setup#functionality-mode) describing how Gateways get selected in Mixnet mode and Wireguard mode. Wireguard nodes route data directly to the open internet. Therefore it exposes IP of operators server (VPS) to abuse complains. Before you decide to run a node with active wireguard routing, please read our [Community Counsel pages](community-counsel/exit-gateway) containing more information and some legal content. @@ -223,7 +394,7 @@ We have been notified that a handful of nodes have been taken down by abuse repo - Join [Community legal counsel](https://nym.com/docs/operators/community-counsel) - our collective knowledge hub. Add your findings by opening a [Pull Request](https://nym.com/docs/operators/add-content) - While we are working on a new list of more friendly providers, consider to move away from these provides as soon as possible: - + - Servinga / VPS2day (AS39378) - Frantech / Ponynet / BuyVM (AS53667) - OVH SAS / OVHcloud (AS16276) @@ -233,7 +404,7 @@ We have been notified that a handful of nodes have been taken down by abuse repo - Psychz Networks (AS40676) - 1337 Services GmbH / RDP.sh (AS210558) -- Backup your nodes to have access to `.nym` directory locally. Follow [node](nodes/maintenance#backup-a-node) and [proxy configuration](nodes/maintenance#backup-proxy-configuration) backup guides to be able to [restore your node](nodes/maintenance#restoring-a-node) later on on another machine, without losing your delegation. +- Backup your nodes to have access to `.nym` directory locally. Follow [node](nodes/maintenance#backup-a-node) and [proxy configuration](nodes/maintenance#backup-proxy-configuration) backup guides to be able to [restore your node](nodes/maintenance#restoring-a-node) later on on another machine, without losing your delegation. - We would like to ask operators who use reverse proxy and a domain (required for Gateways) to start using a common convention starting with `nym-exit` for their nodes URL. The entire address should have this new format: ``` @@ -258,7 +429,7 @@ nym-exit.mysquad.org **The `NYM-EXIT` part in the beginning is what's important.** -- When registering a domain, check [Top Level Domain (TLD)](https://www.techopedia.com/definition/1348/top-level-domain-tld) terms and conditions. For example `.icu` is a no go. Having a wrong TLD may lead to your domain being taken away from you when facing a DMCA report. +- When registering a domain, check [Top Level Domain (TLD)](https://www.techopedia.com/definition/1348/top-level-domain-tld) terms and conditions. For example `.icu` is a no go. Having a wrong TLD may lead to your domain being taken away from you when facing a DMCA report. - Write a message to your provider and introduce your intention to run a Nym Node on their service @@ -322,7 +493,7 @@ Undelegated due to high saturation: #### Service Grant Program v2 -Aside from delegating on top of nodes, Nym runs a Service Grant Program (SGP) to support Exit Gateway operators before they will be rewarded by collecting [zk-nym tickets](../network/cryptography/zk-nym) from users subscription. Operators included in SGP are long term active community members with the highest requirements on the technical setup and upgrading pace. We are about to start a second iteration of SGP very soon (SGPv2). The final slots and locations are yet to be concluded. Priority to participate in SGPv2 will be given to the current operators in SGP. Based on the number of slots, we will then determine how many more operators can sign up. +Aside from delegating on top of nodes, Nym runs a Service Grant Program (SGP) to support Exit Gateway operators before they will be rewarded by collecting [zk-nym tickets](../network/cryptography/zk-nym) from users subscription. Operators included in SGP are long term active community members with the highest requirements on the technical setup and upgrading pace. We are about to start a second iteration of SGP very soon (SGPv2). The final slots and locations are yet to be concluded. Priority to participate in SGPv2 will be given to the current operators in SGP. Based on the number of slots, we will then determine how many more operators can sign up. ##### Rules of SGPv2 @@ -330,7 +501,7 @@ Aside from delegating on top of nodes, Nym runs a Service Grant Program (SGP) to **We will share more info soon in the channels. The rules are not set in stone and could potentially be altered or updated in the future! Do *not* purchase new servers neither migrate your nodes just yet.** -As we finalising last details of *"Project Smoosh"*, where one binary - `nym-node` - can run as an `entry-gateway`, `mixnode` or `exit-gateway` in Mixnet mode as well as `entry-gateway` or `exit-gateway` in Wireguard mode, we plan to step up the game. SGPv2 grants will be higher if operators can meet new requirements. +As we finalising last details of *"Project Smoosh"*, where one binary - `nym-node` - can run as an `entry-gateway`, `mixnode` or `exit-gateway` in Mixnet mode as well as `entry-gateway` or `exit-gateway` in Wireguard mode, we plan to step up the game. SGPv2 grants will be higher if operators can meet new requirements. **Minimum Specs & Requirements** @@ -402,7 +573,7 @@ These are minimum requirements to become a part of SGPv2. We aim to have nodes o - [build(deps): bump criterion from `0.4.0` to `0.5.1`](https://github.com/nymtech/nym/pull/4911): Bumps [criterion](https://github.com/bheisler/criterion.rs) from `0.4.0` to `0.5.1`. -- [NS API: add mixnet scraper](https://github.com/nymtech/nym/pull/5200) +- [NS API: add mixnet scraper](https://github.com/nymtech/nym/pull/5200) - [build(deps): bump http from `1.1.0` to `1.2.0`](https://github.com/nymtech/nym/pull/5228): Bumps [http](https://github.com/hyperium/http) from `1.1.0` to `1.2.0`. @@ -410,41 +581,41 @@ These are minimum requirements to become a part of SGPv2. We aim to have nodes o - [Add windows to CI builds](https://github.com/nymtech/nym/pull/5269) -- [nym topology revamp](https://github.com/nymtech/nym/pull/5271): This PR changes the internals of the `NymTopology` to blur the lines between explicit mixnodes and gateways so that what used to be considered a "mixnode" could be a valid egress point of the network. `NymTopology` is no longer divided into `BTreeMap>` and `Vec`. instead there's information about the current rewarded set (to support future VRF work) and a simple map of `HashMap`. The new features are mostly controlled via 2 new flags/config values: +- [nym topology revamp](https://github.com/nymtech/nym/pull/5271): This PR changes the internals of the `NymTopology` to blur the lines between explicit mixnodes and gateways so that what used to be considered a "mixnode" could be a valid egress point of the network. `NymTopology` is no longer divided into `BTreeMap>` and `Vec`. instead there's information about the current rewarded set (to support future VRF work) and a simple map of `HashMap`. The new features are mostly controlled via 2 new flags/config values: - `use_extended_topology` that tells the client to retrieve **all** network nodes rather than the ones that got assigned "active" mixnode role (or support being a gateway) - `ignore_egress_epoch_role` that tells the client it's fine to construct egress packets to nodes that are **not** assigned entry or exit gateway role - + - [Nyx Chain Watcher](https://github.com/nymtech/nym/pull/5274) - [Include `IPINFO_API_TOKEN` in nightly CI](https://github.com/nymtech/nym/pull/5285) -- [Move tun constants to network defaults](https://github.com/nymtech/nym/pull/5286): +- [Move tun constants to network defaults](https://github.com/nymtech/nym/pull/5286): }> 1. **Regression Testing**: -- Verified no issues arose when running tests for the affected files. -- Tested TUN behaviour with new nym-nodes in the hu branch. - -**Results**: +- Verified no issues arose when running tests for the affected files. +- Tested TUN behaviour with new nym-nodes in the hu branch. -- **No bugs detected**. -- Tunnels are functioning as expected, with traffic routing and IP generation working seamlessly. - +**Results**: + +- **No bugs detected**. +- Tunnels are functioning as expected, with traffic routing and IP generation working seamlessly. + - [Add dependabot assignes for the root cargo ecosystem](https://github.com/nymtech/nym/pull/5297) - [build(deps): bump the patch-updates group across 1 directory with 35 updates](https://github.com/nymtech/nym/pull/5310): Bumps the `patch-updates` group with 33 updates -- [Periodically remove stale gateway messages](https://github.com/nymtech/nym/pull/5312): This PR introduces a simple task that removes gateway messages that haven't been retrieved in (by default) 24h. +- [Periodically remove stale gateway messages](https://github.com/nymtech/nym/pull/5312): This PR introduces a simple task that removes gateway messages that haven't been retrieved in (by default) 24h. }> **Automation Script for Data Cleanup Validation** Test Objective: Validate that the stale message cleanup mechanism in the database correctly removes records older than the configured threshold (24 hours). - + Test Setup: 1. Environment: * SQLite database * Bash script: used to insert data. - + Steps Performed: 1. Ran the insert_data.sh script to populate the database with test data: * Recent records inserted successfully. @@ -452,8 +623,8 @@ Steps Performed: 3. Confirmed that all 20 records (10 recent + 10 stale) were present. 4. Allowed the system to run for 24 hours to trigger the cleanup mechanism. 5. Queried the database again after 24 hours: sqlite3 gateway_storage.db "SELECT * FROM message_store;" -6. - +6. + Expected Result: * All stale records (older than 24 hours) should be removed. * Recent records should remain in the database. @@ -464,18 +635,18 @@ Actual Result: - [Use expect in geodata test to give error message on failure](https://github.com/nymtech/nym/pull/5314): Keep hitting this error on CI, from what I think is network hickup. But it's hard to tell form the log since the error is swallowed. Explicitly unwrap the result so we get a more detailed error output. }> -**Quick Code Review** - -**Summary** +**Quick Code Review** + +**Summary** 1. **CI Workflow**: Adjusted paths to optimise build triggers, avoiding unnecessary CI runs while ensuring coverage for key directories 2. **Geolocation Test**: Improved error handling by replacing assertions with `.expect` for clearer debugging in API regression tests - -**Conclusion** + +**Conclusion** Regression testing confirms everything works as intended. **Approved**. -- [`CancellationToken`-based shutdowns](https://github.com/nymtech/nym/pull/5325): This PR introduces scaffolding for using `CancellationToken` and `TaskTracker` for our graceful shutdowns rather than the existing `TaskClient` and `TaskManager`. +- [`CancellationToken`-based shutdowns](https://github.com/nymtech/nym/pull/5325): This PR introduces scaffolding for using `CancellationToken` and `TaskTracker` for our graceful shutdowns rather than the existing `TaskClient` and `TaskManager`. - [Introduce `/load` endpoint for self-reported quantised Nym Node load](https://github.com/nymtech/nym/pull/5326): This PR introduces a new `/load` endpoint on a `NymNode` to return its current load. It returns the following data: ```rust @@ -505,7 +676,7 @@ The actual values for`NodeLoad` are determined as follows: - Thus we calculate two additional auxiliary `Load` values, for memory usage and swap usage, i.e.: `used_memory / total_memory` and `used_swap / total_swap` respectively. - Then we check whether either of the `MemoryLoad` or `SwapLoad` is bigger than the current base `Load` of the machine we have determined, if so, it's increased by one tier / bucket. For example, say the current machine load is `Load::Low`, but the memory usage is at 90% (`Load::VeryHigh`). that would result in the reported `Load` being bumped up to `Load::Medium` instead. The same logic applies with swap load, **however, only if the total swap > 1GB**. this is to prevent weird edge cases where the machine has hardly any swap. - Finally, the `.total` `Load` uses the same "tier bumping" behaviour using the `.total` and `.network` loads, i.e. `if network > machine`, then `total = machine + 1`. for example if `machine` `Load` is `Load::Low`, but `network` `Load` is `Load::Medium`, then the `total` `Load` is set to `Load::Medium` instead. - + - [Bump the `patch-updates` group with 8 updates](https://github.com/nymtech/nym/pull/5336) @@ -517,29 +688,29 @@ The actual values for`NodeLoad` are determined as follows: - [Bump mikefarah/yq from `4.44.6` to `4.45.1`](https://github.com/nymtech/nym/pull/5342) -- [Update `indexed_db_futures`](https://github.com/nymtech/nym/pull/5347): Updates the `indexed_db_futures` dependency to stop relying on the fork. +- [Update `indexed_db_futures`](https://github.com/nymtech/nym/pull/5347): Updates the `indexed_db_futures` dependency to stop relying on the fork. - [Refresh wasm sdk](https://github.com/nymtech/nym/pull/5353): This PR refreshes the wasm clients to make them usable in the current network - [Client gateway selection](https://github.com/nymtech/nym/pull/5358): Changed how gateway is selected. - For init the target gateway can't support mixing - For egress, unless `ignore_epoch_roles` is specified, the gateway can't be currently assigned to a mixing layer. But it can be standby or inactive - + - [Exposed `NymApiClient` method for obtaining node performance history](https://github.com/nymtech/nym/pull/5360) - [Bind to `[::]` on `nym-node` for both IP versions](https://github.com/nymtech/nym/pull/5361) }> **IPv4 Configuration and Migration Testing** - + Testing Steps: - Initiated and ran a nym-node with version 1.3.1 on an IPv4-only machine, then updated it to the new 'hu', 1.4.0 version. - Initiated and ran a new nym-node with 'hu', 1.4.0 always on a machine with only IPv4. - Initiated and ran a new nym-node with 'hu', 1.4.0 on a machine with both IPv4 and IPv6 for regression testing. - + Results: - No functional issues during version updates. - Logged message on all versions: "no registered client for destination: ff02::2" - + Status: Pass @@ -554,10 +725,10 @@ Status: Pass - Ensured a client is able to select a node which aside from a gateway, can also act as a mixnode - Verified 'ignore_epoch_roles' is the default mode - _note; this is most likely not going to be a permanent solution_ - + **Results:** -- Clients and topology are behaving correctly - +- Clients and topology are behaving correctly + Status: Pass @@ -612,7 +783,7 @@ We are developing a design where operators can enable multiple modes, and let th - Moved top level `authenticator` section to `service_providers` so that it'd live alongside NR and IPR - Added general `debug` section - Added `metrics` section -- All documentation migrated to a new URL [nym.com/docs](https://nym.com/docs) alongside the rebranding of Nym organisation. +- All documentation migrated to a new URL [nym.com/docs](https://nym.com/docs) alongside the rebranding of Nym organisation. - Updated [network architecture diagrams](https://nym.com/docs/network/architecture) - New blow-by-blow mixnet [traffic flow](https://nym.com/docs/network/architecture) section - [Winter Nym Squad League started](https://forum.nym.com/t/nym-squad-league-farewell-fall-welcome-winter/977) @@ -648,11 +819,11 @@ We are developing a design where operators can enable multiple modes, and let th - Everything in `mixnode` directory has been removed because there was nothing really left there. The mixing socket listener was unified in `nym-node` and similarly `verloc` was also moved there - `gateway` directory was similarly reduced in size. Now it also creates appropriate tasks as opposed to the whole gateway process. eventually it might also be further stripped, but today is not that day. - Removed the generic parameter on the `GatewayStorage` to simplify all the generics down the stack. it wasn't used anyway - + CLI: - Added `--modes` argument to specify all node modes with a single command (or `env` variable). for example: `--modes="mixnode,entry"`. Can't be used alongside `--modes` - Extended `--mode` argument to allow specifying it multiple times, for example: `--mode mixnode --mode entry`. can't be used alongside `--mode` - + Config changes: - Replaced `mode` with `modes` to allow setting the node to run with say, `entry` + `mixnode` roles simultaneously - Added `maximum_forward_packet_delay` to `mixnet.debug` section @@ -677,17 +848,17 @@ Config changes: - [Remove unneeded async function annotation](https://github.com/nymtech/nym/pull/5246) - [Add control messages to `GatewayTransciver`](https://github.com/nymtech/nym/pull/5247) }> -**Review and Testing: Forget Me Implementation** +**Review and Testing: Forget Me Implementation** -- Validated the encryption and delivery of `ForgetMe` control messages to the gateway - -**Testing: MixTrafficController Integration** +- Validated the encryption and delivery of `ForgetMe` control messages to the gateway -- Verified that the `MixTrafficController` invokes `ForgetMe` logic correctly during shutdown -- Tested behaviour for gateway transceiver failures while sending control messages - -**Testing: Gateway Storage Updates** -- Confirmed successful deletion of client data (e.g., inbox messages, bandwidth allocations) from persistent storage +**Testing: MixTrafficController Integration** + +- Verified that the `MixTrafficController` invokes `ForgetMe` logic correctly during shutdown +- Tested behaviour for gateway transceiver failures while sending control messages + +**Testing: Gateway Storage Updates** +- Confirmed successful deletion of client data (e.g., inbox messages, bandwidth allocations) from persistent storage - [Add conversion unit tests for auth msg](https://github.com/nymtech/nym/pull/5251) @@ -704,13 +875,13 @@ Config changes: }> 1. **Review File: `common/credential-storage/src/backends/sqlite.rs`** - Verified addition of `close` method for the SQLite backend - + 2. **Review File: `common/credential-storage/src/ephemeral_storage.rs`** - Confirmed addition of `close` method for ephemeral storage with no action required - + 3. **Review File: `common/credential-storage/src/persistent_storage/mod.rs`** - Ensured `close` method integration for persistent storage - + 4. **Review File: `common/credential-storage/src/storage.rs`** - Verified updates to the `Storage` trait to include `close` and `cleanup_expired` methods @@ -719,15 +890,15 @@ Config changes: }> 1. **Review File: `common/network-defaults/src/constants.rs`** - Confirmed updated `mixnet_vpn` constants were added. - + 2. **Review File: `service-providers/ip-packet-router/src/constants.rs`** - Checked replacement of legacy `TUN_*` constants with new `mixnet_vpn` constants. - Validated alignment of routing traffic configurations. - + 3. **Review File: `service-providers/ip-packet-router/src/ip_packet_router.rs`** - Ensured new `nym_network_defaults::constants::mixnet_vpn` constants replaced old references. - Verified `TunDeviceConfig` consistency. - + 4. **Review File: `service-providers/ip-packet-router/src/util/generate_new_ip.rs`** - Confirmed substitution of `TUN_DEVICE_*` constants with `NYM_TUN_DEVICE_*` constants. - Tested functionality for generating random IPs within subnet. @@ -743,54 +914,54 @@ Config changes: - `nym_node_mixnet_ingress_excessive_delay_packets` - `nym_node_mixnet_ingress_forward_hop_packets_dropped` - `nym_node_mixnet_ingress_final_hop_packets_dropped` - + - `nym_node_mixnet_ingress_forward_hop_packets_received_rate` - - `nym_node_mixnet_ingress_final_hop_packets_received_rate` - - `nym_node_mixnet_ingress_malformed_packets_received_rate` - - `nym_node_mixnet_ingress_excessive_delay_packets_rate` + - `nym_node_mixnet_ingress_final_hop_packets_received_rate` + - `nym_node_mixnet_ingress_malformed_packets_received_rate` + - `nym_node_mixnet_ingress_excessive_delay_packets_rate` - `nym_node_mixnet_ingress_forward_hop_packets_dropped_rate` - `nym_node_mixnet_ingress_final_hop_packets_dropped_rate` - + - egress: - `nym_node_mixnet_egress_stored_on_disk_final_hop_packets` - `nym_node_mixnet_egress_forward_hop_packets_sent` - `nym_node_mixnet_egress_ack_packets_sent` - `nym_node_mixnet_egress_forward_hop_packets_dropped` - + - `nym_node_mixnet_egress_forward_hop_packets_sent_rate` - `nym_node_mixnet_egress_ack_packets_sent_rate` - `nym_node_mixnet_egress_forward_hop_packets_dropped_rate` - + - client sessions - `nym_node_entry_client_sessions_unique_users` - `nym_node_entry_client_sessions_sessions_started` - `nym_node_entry_client_sessions_finished_sessions` - `nym_node_entry_client_sessions_durations_{TYP}` (histogram), for example `nym_node_entry_client_sessions_durations_vpn` - + - wireguard: - `nym_node_wireguard_bytes_rx` - `nym_node_wireguard_bytes_tx` - `nym_node_wireguard_bytes_total_peers` - `nym_node_wireguard_bytes_active_peers` - + - `nym_node_wireguard_bytes_rx_rate` - `nym_node_wireguard_bytes_tx_rate` - - + + - network - `nym_node_network_active_ingress_mixnet_connections` - `nym_node_network_active_ingress_web_socket_connections` - `nym_node_network_active_egress_mixnet_connections` - + - process - `nym_node_process_forward_hop_packets_being_delayed` - `nym_node_process_packet_forwarder_queue_size` - `nym_node_process_topology_query_resolution_latency` (histogram) - `nym_node_process_final_hop_packets_pending_delivery` - - `nym_node_process_forward_hop_packets_pending_delivery` + - `nym_node_process_forward_hop_packets_pending_delivery` -- [Amend 250gb limit](https://github.com/nymtech/nym/pull/5313): Change bandwidth cap to 250gb +- [Amend 250gb limit](https://github.com/nymtech/nym/pull/5313): Change bandwidth cap to 250gb - [Warn users if node is run in exit mode only](https://github.com/nymtech/nym/pull/5320): Throws a warning if node is run in "exit" mode only as by default, this will **NOT** enable entry capabilities, i.e. opening the websocket. thus making it ineligible for rewarded set selection (and rewards) - [Reduce log severity for number of packets being delayed](https://github.com/nymtech/nym/pull/5321) - [Apply 1.84 linter suggestions](https://github.com/nymtech/nym/pull/5330) @@ -806,7 +977,7 @@ Config changes: - [Make sure to apply gateway score filtering when choosing initial node](https://github.com/nymtech/nym/pull/5256) - [Fixed client session histogram buckets](https://github.com/nymtech/nym/pull/5316) - [Contract version assignment](https://github.com/nymtech/nym/pull/5318): This PR fixes updates to current nym-node version as well as introduces migration to fix the existing state of the mainnet contract - - [Make sure refresh data key matches bond info](https://github.com/nymtech/nym/pull/5329): This is to forbid operators from reusing the same underlying identity key for multiple nodes by overwriting the describe data. the reported key has to always match what the node has bonded with (and the contract enforces uniqueness) + - [Make sure refresh data key matches bond info](https://github.com/nymtech/nym/pull/5329): This is to forbid operators from reusing the same underlying identity key for multiple nodes by overwriting the describe data. the reported key has to always match what the node has bonded with (and the contract enforces uniqueness) ## Archived Changelog From bb5b2eafcf0eba9c97968521bfa1612f681ffc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 5 Mar 2025 15:02:07 +0100 Subject: [PATCH 034/133] Allow IPR reconnect to session (#5562) --- .../src/clients/connected_clients.rs | 35 ++++++++++--------- .../ip-packet-router/src/messages/response.rs | 3 -- .../src/messages/response/v6.rs | 3 -- .../src/messages/response/v7.rs | 3 -- .../src/messages/response/v8.rs | 3 -- .../ip-packet-router/src/mixnet_listener.rs | 5 +-- 6 files changed, 21 insertions(+), 31 deletions(-) diff --git a/service-providers/ip-packet-router/src/clients/connected_clients.rs b/service-providers/ip-packet-router/src/clients/connected_clients.rs index 1957ddaa15..853c83f884 100644 --- a/service-providers/ip-packet-router/src/clients/connected_clients.rs +++ b/service-providers/ip-packet-router/src/clients/connected_clients.rs @@ -59,23 +59,24 @@ impl ConnectedClients { .any(|client| client.client_id == *client_id) } - //fn lookup_ip_from_client_id(&self, client_id: &ConnectedClientId) -> Option { - // self.clients_ipv4_mapping - // .iter() - // .find_map(|(ipv4, connected_client)| { - // if connected_client.client_id == *client_id { - // Some(IpPair::new(*ipv4, connected_client.ipv6)) - // } else { - // None - // } - // }) - //} - // - //fn lookup_client(&self, client_id: &ConnectedClientId) -> Option<&ConnectedClient> { - // self.clients_ipv4_mapping - // .values() - // .find(|connected_client| connected_client.client_id == *client_id) - //} + pub(crate) fn lookup_ip_from_client_id(&self, client_id: &ConnectedClientId) -> Option { + self.clients_ipv4_mapping + .iter() + .find_map(|(ipv4, connected_client)| { + if connected_client.client_id == *client_id { + Some(IpPair::new(*ipv4, connected_client.ipv6)) + } else { + None + } + }) + } + + #[allow(unused)] + fn lookup_client(&self, client_id: &ConnectedClientId) -> Option<&ConnectedClient> { + self.clients_ipv4_mapping + .values() + .find(|connected_client| connected_client.client_id == *client_id) + } pub(crate) fn connect( &mut self, diff --git a/service-providers/ip-packet-router/src/messages/response.rs b/service-providers/ip-packet-router/src/messages/response.rs index 6e60d41433..d1bf0d08b4 100644 --- a/service-providers/ip-packet-router/src/messages/response.rs +++ b/service-providers/ip-packet-router/src/messages/response.rs @@ -90,9 +90,6 @@ pub(crate) struct DynamicConnectSuccess { #[derive(Clone, Debug, thiserror::Error)] pub(crate) enum DynamicConnectFailureReason { - #[error("client already connected")] - ClientAlreadyConnected, - #[error("no available ip address")] NoAvailableIp, diff --git a/service-providers/ip-packet-router/src/messages/response/v6.rs b/service-providers/ip-packet-router/src/messages/response/v6.rs index 290f78971b..bf7fc9dbdf 100644 --- a/service-providers/ip-packet-router/src/messages/response/v6.rs +++ b/service-providers/ip-packet-router/src/messages/response/v6.rs @@ -119,9 +119,6 @@ impl From for DynamicConnectResponseReplyV6 { impl From for DynamicConnectFailureReasonV6 { fn from(reason: DynamicConnectFailureReason) -> Self { match reason { - DynamicConnectFailureReason::ClientAlreadyConnected => { - DynamicConnectFailureReasonV6::RequestedNymAddressAlreadyInUse - } DynamicConnectFailureReason::NoAvailableIp => { DynamicConnectFailureReasonV6::NoAvailableIp } diff --git a/service-providers/ip-packet-router/src/messages/response/v7.rs b/service-providers/ip-packet-router/src/messages/response/v7.rs index 60a3aa6d8a..f2ad4e849f 100644 --- a/service-providers/ip-packet-router/src/messages/response/v7.rs +++ b/service-providers/ip-packet-router/src/messages/response/v7.rs @@ -119,9 +119,6 @@ impl From for DynamicConnectResponseReplyV7 { impl From for DynamicConnectFailureReasonV7 { fn from(reason: DynamicConnectFailureReason) -> Self { match reason { - DynamicConnectFailureReason::ClientAlreadyConnected => { - DynamicConnectFailureReasonV7::RequestedNymAddressAlreadyInUse - } DynamicConnectFailureReason::NoAvailableIp => { DynamicConnectFailureReasonV7::NoAvailableIp } diff --git a/service-providers/ip-packet-router/src/messages/response/v8.rs b/service-providers/ip-packet-router/src/messages/response/v8.rs index 3f73479a05..99b87075c8 100644 --- a/service-providers/ip-packet-router/src/messages/response/v8.rs +++ b/service-providers/ip-packet-router/src/messages/response/v8.rs @@ -82,9 +82,6 @@ impl From for ConnectResponseReplyV8 { impl From for ConnectFailureReasonV8 { fn from(reason: DynamicConnectFailureReason) -> Self { match reason { - DynamicConnectFailureReason::ClientAlreadyConnected => { - ConnectFailureReasonV8::ClientAlreadyConnected - } DynamicConnectFailureReason::NoAvailableIp => ConnectFailureReasonV8::NoAvailableIp, DynamicConnectFailureReason::Other(err) => ConnectFailureReasonV8::Other(err), } diff --git a/service-providers/ip-packet-router/src/mixnet_listener.rs b/service-providers/ip-packet-router/src/mixnet_listener.rs index 293718831c..31d4b2e5fe 100644 --- a/service-providers/ip-packet-router/src/mixnet_listener.rs +++ b/service-providers/ip-packet-router/src/mixnet_listener.rs @@ -242,13 +242,14 @@ impl MixnetListener { .map(Duration::from_millis) .unwrap_or(nym_ip_packet_requests::codec::BUFFER_TIMEOUT); - if self.connected_clients.is_client_connected(&reply_to) { + if let Some(ips) = self.connected_clients.lookup_ip_from_client_id(&reply_to) { + log::debug!("Reconnecting to the previous session"); return Ok(Some(VersionedResponse { version, reply_to, response: Response::DynamicConnect { request_id, - reply: DynamicConnectFailureReason::ClientAlreadyConnected.into(), + reply: DynamicConnectSuccess { ips }.into(), }, })); } From 7785d085cfe57b4a5473e089ff75af6ba1d54075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 5 Mar 2025 15:17:51 +0100 Subject: [PATCH 035/133] Handle disconnect in IPR (#5547) * Implement disconnect in the IPR * Remove unused async --- .../ip-packet-router/src/clients/client_id.rs | 2 +- .../src/clients/connected_clients.rs | 36 ++++++----- .../ip-packet-router/src/mixnet_listener.rs | 59 +++++++++++++++---- .../ip-packet-router/src/tun_listener.rs | 2 +- 4 files changed, 69 insertions(+), 30 deletions(-) diff --git a/service-providers/ip-packet-router/src/clients/client_id.rs b/service-providers/ip-packet-router/src/clients/client_id.rs index 5add99a724..2842f261d3 100644 --- a/service-providers/ip-packet-router/src/clients/client_id.rs +++ b/service-providers/ip-packet-router/src/clients/client_id.rs @@ -7,7 +7,7 @@ use nym_sdk::mixnet::{AnonymousSenderTag, Recipient}; use crate::error::{IpPacketRouterError, Result}; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub(crate) enum ConnectedClientId { AnonymousSenderTag(AnonymousSenderTag), NymAddress(Box), diff --git a/service-providers/ip-packet-router/src/clients/connected_clients.rs b/service-providers/ip-packet-router/src/clients/connected_clients.rs index 853c83f884..7100048dd8 100644 --- a/service-providers/ip-packet-router/src/clients/connected_clients.rs +++ b/service-providers/ip-packet-router/src/clients/connected_clients.rs @@ -59,6 +59,24 @@ impl ConnectedClients { .any(|client| client.client_id == *client_id) } + pub(crate) fn disconnect_client(&mut self, client_id: &ConnectedClientId) { + if let Some(ips) = self.lookup_ip_from_client_id(client_id) { + log::debug!("Disconnect client that requested to do so: {ips}"); + self.disconnect_client_handle(ips); + } + } + + fn disconnect_client_handle(&mut self, ips: IpPair) { + self.clients_ipv4_mapping.remove(&ips.ipv4); + self.clients_ipv6_mapping.remove(&ips.ipv6); + self.tun_listener_connected_client_tx + .send(ConnectedClientEvent::Disconnect(DisconnectEvent(ips))) + .inspect_err(|err| { + log::error!("Failed to send disconnect event: {err}"); + }) + .ok(); + } + pub(crate) fn lookup_ip_from_client_id(&self, client_id: &ConnectedClientId) -> Option { self.clients_ipv4_mapping .iter() @@ -164,14 +182,7 @@ impl ConnectedClients { ) { for (ips, _) in &stopped_clients { log::info!("Disconnect stopped client: {ips}"); - self.clients_ipv4_mapping.remove(&ips.ipv4); - self.clients_ipv6_mapping.remove(&ips.ipv6); - self.tun_listener_connected_client_tx - .send(ConnectedClientEvent::Disconnect(DisconnectEvent(*ips))) - .inspect_err(|err| { - log::error!("Failed to send disconnect event: {err}"); - }) - .ok(); + self.disconnect_client_handle(*ips); } } @@ -181,14 +192,7 @@ impl ConnectedClients { ) { for (ips, _) in &inactive_clients { log::info!("Disconnect inactive client: {ips}"); - self.clients_ipv4_mapping.remove(&ips.ipv4); - self.clients_ipv6_mapping.remove(&ips.ipv6); - self.tun_listener_connected_client_tx - .send(ConnectedClientEvent::Disconnect(DisconnectEvent(*ips))) - .inspect_err(|err| { - log::error!("Failed to send disconnect event: {err}"); - }) - .ok(); + self.disconnect_client_handle(*ips); } } diff --git a/service-providers/ip-packet-router/src/mixnet_listener.rs b/service-providers/ip-packet-router/src/mixnet_listener.rs index 31d4b2e5fe..b0957236ac 100644 --- a/service-providers/ip-packet-router/src/mixnet_listener.rs +++ b/service-providers/ip-packet-router/src/mixnet_listener.rs @@ -22,9 +22,9 @@ use crate::{ IpPacketRequest, PingRequest, StaticConnectRequest, }, response::{ - DynamicConnectFailureReason, DynamicConnectSuccess, HealthResponse, InfoLevel, - InfoResponse, InfoResponseReply, Response, StaticConnectFailureReason, - StaticConnectResponse, VersionedResponse, + DisconnectFailureReason, DisconnectResponse, DynamicConnectFailureReason, + DynamicConnectSuccess, HealthResponse, InfoLevel, InfoResponse, InfoResponseReply, + Response, StaticConnectFailureReason, StaticConnectResponse, VersionedResponse, }, ClientVersion, }, @@ -225,7 +225,7 @@ impl MixnetListener { })) } - async fn on_dynamic_connect_request( + fn on_dynamic_connect_request( &mut self, connect_request: DynamicConnectRequest, ) -> PacketHandleResult { @@ -293,12 +293,47 @@ impl MixnetListener { })) } - fn on_disconnect_request(&self, _disconnect_request: DisconnectRequest) -> PacketHandleResult { - log::info!("Received disconnect request: not implemented, dropping"); - Ok(None) + fn on_disconnect_request( + &mut self, + disconnect_request: DisconnectRequest, + ) -> PacketHandleResult { + log::info!( + "Received disconnect request from {}", + disconnect_request.sent_by + ); + + let version = disconnect_request.version; + let request_id = disconnect_request.request_id; + let client_id = disconnect_request.sent_by; + + // Check if the client is connected + if !self.connected_clients.is_client_connected(&client_id) { + log::info!("Client {} is not connected, cannot disconnect", client_id); + return Ok(Some(VersionedResponse { + version, + reply_to: client_id, + response: Response::Disconnect { + request_id, + reply: DisconnectResponse::Failure(DisconnectFailureReason::ClientNotConnected), + }, + })); + } + + // Disconnect the client + log::info!("Disconnecting client {}", client_id); + self.connected_clients.disconnect_client(&client_id); + + Ok(Some(VersionedResponse { + version, + reply_to: client_id, + response: Response::Disconnect { + request_id, + reply: DisconnectResponse::Success, + }, + })) } - async fn on_ping_request(&self, ping_request: PingRequest) -> PacketHandleResult { + fn on_ping_request(&self, ping_request: PingRequest) -> PacketHandleResult { Ok(Some(VersionedResponse { version: ping_request.version, reply_to: ping_request.sent_by, @@ -308,7 +343,7 @@ impl MixnetListener { })) } - async fn on_health_request(&self, health_request: HealthRequest) -> PacketHandleResult { + fn on_health_request(&self, health_request: HealthRequest) -> PacketHandleResult { Ok(Some(VersionedResponse { version: health_request.version, reply_to: health_request.sent_by, @@ -325,10 +360,10 @@ impl MixnetListener { async fn on_control_request(&mut self, control_request: ControlRequest) -> PacketHandleResult { match control_request { ControlRequest::StaticConnect(r) => self.on_static_connect_request(r).await, - ControlRequest::DynamicConnect(r) => self.on_dynamic_connect_request(r).await, + ControlRequest::DynamicConnect(r) => self.on_dynamic_connect_request(r), ControlRequest::Disconnect(r) => self.on_disconnect_request(r), - ControlRequest::Ping(r) => self.on_ping_request(r).await, - ControlRequest::Health(r) => self.on_health_request(r).await, + ControlRequest::Ping(r) => self.on_ping_request(r), + ControlRequest::Health(r) => self.on_health_request(r), } } diff --git a/service-providers/ip-packet-router/src/tun_listener.rs b/service-providers/ip-packet-router/src/tun_listener.rs index 0d4a99e3b9..072ade7ab0 100644 --- a/service-providers/ip-packet-router/src/tun_listener.rs +++ b/service-providers/ip-packet-router/src/tun_listener.rs @@ -103,7 +103,7 @@ impl TunListener { .update(ConnectedClientEvent::Disconnect(DisconnectEvent(*ips))); } } else { - log::info!( + log::debug!( "dropping packet from network: no registered client for destination: {dst_addr}" ); } From c284b1e8b1c9b70636c09537750726dbe8c4c5ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 5 Mar 2025 15:41:44 +0100 Subject: [PATCH 036/133] Create authenticator v5 request/response types (#5561) * Create authenticator v5 request/response types * Support v5 in the authenticator * Fix tests * Bump nym-node version --- Cargo.lock | 2 +- common/authenticator-requests/src/lib.rs | 5 +- common/authenticator-requests/src/traits.rs | 112 +++- .../src/v5/conversion.rs | 478 ++++++++++++++++++ common/authenticator-requests/src/v5/mod.rs | 10 + .../src/v5/registration.rs | 287 +++++++++++ .../authenticator-requests/src/v5/request.rs | 132 +++++ .../authenticator-requests/src/v5/response.rs | 132 +++++ common/authenticator-requests/src/v5/topup.rs | 15 + nym-node/Cargo.toml | 4 +- .../authenticator/src/cli/request.rs | 8 +- service-providers/authenticator/src/error.rs | 3 + .../authenticator/src/mixnet_listener.rs | 153 ++++-- 13 files changed, 1277 insertions(+), 64 deletions(-) create mode 100644 common/authenticator-requests/src/v5/conversion.rs create mode 100644 common/authenticator-requests/src/v5/mod.rs create mode 100644 common/authenticator-requests/src/v5/registration.rs create mode 100644 common/authenticator-requests/src/v5/request.rs create mode 100644 common/authenticator-requests/src/v5/response.rs create mode 100644 common/authenticator-requests/src/v5/topup.rs diff --git a/Cargo.lock b/Cargo.lock index 2aaf965a5d..9cce5fff5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6168,7 +6168,7 @@ dependencies = [ [[package]] name = "nym-node" -version = "1.6.0" +version = "1.6.1" dependencies = [ "anyhow", "arc-swap", diff --git a/common/authenticator-requests/src/lib.rs b/common/authenticator-requests/src/lib.rs index ed987a9a50..351a9b443c 100644 --- a/common/authenticator-requests/src/lib.rs +++ b/common/authenticator-requests/src/lib.rs @@ -6,14 +6,15 @@ pub mod v1; pub mod v2; pub mod v3; pub mod v4; +pub mod v5; mod error; mod util; pub use error::Error; -pub use v4 as latest; +pub use v5 as latest; -pub const CURRENT_VERSION: u8 = 4; +pub const CURRENT_VERSION: u8 = 5; fn make_bincode_serializer() -> impl bincode::Options { use bincode::Options; diff --git a/common/authenticator-requests/src/traits.rs b/common/authenticator-requests/src/traits.rs index 7c7064d174..baaaf8b422 100644 --- a/common/authenticator-requests/src/traits.rs +++ b/common/authenticator-requests/src/traits.rs @@ -8,8 +8,8 @@ use nym_sphinx::addressing::clients::Recipient; use nym_wireguard_types::PeerPublicKey; use crate::{ - v1, v2, v3, - v4::{self, registration::IpPair}, + v1, v2, v3, v4, + v5::{self, registration::IpPair}, Error, }; @@ -19,6 +19,7 @@ pub enum AuthenticatorVersion { V2, V3, V4, + V5, UNKNOWN, } @@ -34,6 +35,8 @@ impl From for AuthenticatorVersion { AuthenticatorVersion::V3 } else if value.version == v4::VERSION { AuthenticatorVersion::V4 + } else if value.version == v5::VERSION { + AuthenticatorVersion::V5 } else { AuthenticatorVersion::UNKNOWN } @@ -68,6 +71,12 @@ impl InitMessage for v4::registration::InitMessage { } } +impl InitMessage for v5::registration::InitMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + pub trait FinalMessage { fn pub_key(&self) -> PeerPublicKey; fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error>; @@ -138,6 +147,24 @@ impl FinalMessage for v4::registration::FinalMessage { self.gateway_client.verify(private_key, nonce) } + fn private_ips(&self) -> IpPair { + self.gateway_client.private_ips.into() + } + + fn credential(&self) -> Option { + self.credential.clone() + } +} + +impl FinalMessage for v5::registration::FinalMessage { + fn pub_key(&self) -> PeerPublicKey { + self.gateway_client.pub_key + } + + fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + self.gateway_client.verify(private_key, nonce) + } + fn private_ips(&self) -> IpPair { self.gateway_client.private_ips } @@ -182,29 +209,39 @@ impl TopUpMessage for v4::topup::TopUpMessage { } } +impl TopUpMessage for v5::topup::TopUpMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } + + fn credential(&self) -> CredentialSpendingData { + self.credential.clone() + } +} + pub enum AuthenticatorRequest { Initial { msg: Box, protocol: Protocol, - reply_to: Recipient, + reply_to: Option, request_id: u64, }, Final { msg: Box, protocol: Protocol, - reply_to: Recipient, + reply_to: Option, request_id: u64, }, QueryBandwidth { msg: Box, protocol: Protocol, - reply_to: Recipient, + reply_to: Option, request_id: u64, }, TopUpBandwidth { msg: Box, protocol: Protocol, - reply_to: Recipient, + reply_to: Option, request_id: u64, }, } @@ -218,7 +255,7 @@ impl From for AuthenticatorRequest { version: value.version, service_provider_type: ServiceProviderType::Authenticator, }, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, }, v1::request::AuthenticatorRequestData::Final(gateway_client) => Self::Final { @@ -227,7 +264,7 @@ impl From for AuthenticatorRequest { version: value.version, service_provider_type: ServiceProviderType::Authenticator, }, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, }, v1::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { @@ -237,7 +274,7 @@ impl From for AuthenticatorRequest { version: value.version, service_provider_type: ServiceProviderType::Authenticator, }, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, } } @@ -251,20 +288,20 @@ impl From for AuthenticatorRequest { v2::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { msg: Box::new(init_message), protocol: value.protocol, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, }, v2::request::AuthenticatorRequestData::Final(final_message) => Self::Final { msg: final_message, protocol: value.protocol, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, }, v2::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { Self::QueryBandwidth { msg: Box::new(peer_public_key), protocol: value.protocol, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, } } @@ -278,20 +315,20 @@ impl From for AuthenticatorRequest { v3::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { msg: Box::new(init_message), protocol: value.protocol, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, }, v3::request::AuthenticatorRequestData::Final(final_message) => Self::Final { msg: final_message, protocol: value.protocol, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, }, v3::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { Self::QueryBandwidth { msg: Box::new(peer_public_key), protocol: value.protocol, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, } } @@ -299,7 +336,7 @@ impl From for AuthenticatorRequest { Self::TopUpBandwidth { msg: top_up_message, protocol: value.protocol, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, } } @@ -313,20 +350,20 @@ impl From for AuthenticatorRequest { v4::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { msg: Box::new(init_message), protocol: value.protocol, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, }, v4::request::AuthenticatorRequestData::Final(final_message) => Self::Final { msg: final_message, protocol: value.protocol, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, }, v4::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { Self::QueryBandwidth { msg: Box::new(peer_public_key), protocol: value.protocol, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), request_id: value.request_id, } } @@ -334,7 +371,42 @@ impl From for AuthenticatorRequest { Self::TopUpBandwidth { msg: top_up_message, protocol: value.protocol, - reply_to: value.reply_to, + reply_to: Some(value.reply_to), + request_id: value.request_id, + } + } + } + } +} + +impl From for AuthenticatorRequest { + fn from(value: v5::request::AuthenticatorRequest) -> Self { + match value.data { + v5::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { + msg: Box::new(init_message), + protocol: value.protocol, + reply_to: None, + request_id: value.request_id, + }, + v5::request::AuthenticatorRequestData::Final(final_message) => Self::Final { + msg: final_message, + protocol: value.protocol, + reply_to: None, + request_id: value.request_id, + }, + v5::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { + Self::QueryBandwidth { + msg: Box::new(peer_public_key), + protocol: value.protocol, + reply_to: None, + request_id: value.request_id, + } + } + v5::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => { + Self::TopUpBandwidth { + msg: top_up_message, + protocol: value.protocol, + reply_to: None, request_id: value.request_id, } } diff --git a/common/authenticator-requests/src/v5/conversion.rs b/common/authenticator-requests/src/v5/conversion.rs new file mode 100644 index 0000000000..e3f7da0830 --- /dev/null +++ b/common/authenticator-requests/src/v5/conversion.rs @@ -0,0 +1,478 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; + +use crate::{v4, v5}; + +impl From for v5::request::AuthenticatorRequest { + fn from(authenticator_request: v4::request::AuthenticatorRequest) -> Self { + Self { + protocol: Protocol { + version: 5, + service_provider_type: ServiceProviderType::Authenticator, + }, + data: authenticator_request.data.into(), + request_id: authenticator_request.request_id, + } + } +} + +impl From for v5::request::AuthenticatorRequestData { + fn from(authenticator_request_data: v4::request::AuthenticatorRequestData) -> Self { + match authenticator_request_data { + v4::request::AuthenticatorRequestData::Initial(init_msg) => { + v5::request::AuthenticatorRequestData::Initial(init_msg.into()) + } + v4::request::AuthenticatorRequestData::Final(final_msg) => { + v5::request::AuthenticatorRequestData::Final(Box::new((*final_msg).into())) + } + v4::request::AuthenticatorRequestData::QueryBandwidth(pub_key) => { + v5::request::AuthenticatorRequestData::QueryBandwidth(pub_key) + } + v4::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => { + v5::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message.into()) + } + } + } +} + +impl From for v5::registration::InitMessage { + fn from(init_msg: v4::registration::InitMessage) -> Self { + Self { + pub_key: init_msg.pub_key, + } + } +} + +impl From for v5::registration::FinalMessage { + fn from(final_msg: v4::registration::FinalMessage) -> Self { + Self { + gateway_client: final_msg.gateway_client.into(), + credential: final_msg.credential, + } + } +} + +impl From for v5::registration::GatewayClient { + fn from(gateway_client: v4::registration::GatewayClient) -> Self { + Self { + pub_key: gateway_client.pub_key, + private_ips: gateway_client.private_ips.into(), + mac: gateway_client.mac.into(), + } + } +} + +impl From for v4::registration::GatewayClient { + fn from(gateway_client: v5::registration::GatewayClient) -> Self { + Self { + pub_key: gateway_client.pub_key, + private_ips: gateway_client.private_ips.into(), + mac: gateway_client.mac.into(), + } + } +} + +impl From for v5::registration::ClientMac { + fn from(client_mac: v4::registration::ClientMac) -> Self { + Self::new((*client_mac).clone()) + } +} + +impl From for v4::registration::ClientMac { + fn from(client_mac: v5::registration::ClientMac) -> Self { + Self::new((*client_mac).clone()) + } +} + +impl From> for Box { + fn from(top_up_message: Box) -> Self { + Box::new(v5::topup::TopUpMessage { + pub_key: top_up_message.pub_key, + credential: top_up_message.credential, + }) + } +} + +impl From for v5::response::AuthenticatorResponse { + fn from(value: v4::response::AuthenticatorResponse) -> Self { + Self { + protocol: Protocol { + version: 5, + service_provider_type: value.protocol.service_provider_type, + }, + data: value.data.into(), + } + } +} + +impl From for v5::response::AuthenticatorResponseData { + fn from(authenticator_response_data: v4::response::AuthenticatorResponseData) -> Self { + match authenticator_response_data { + v4::response::AuthenticatorResponseData::PendingRegistration(pending_response) => { + v5::response::AuthenticatorResponseData::PendingRegistration( + pending_response.into(), + ) + } + v4::response::AuthenticatorResponseData::Registered(registered_response) => { + v5::response::AuthenticatorResponseData::Registered(registered_response.into()) + } + v4::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => v5::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response.into(), + ), + v4::response::AuthenticatorResponseData::TopUpBandwidth(top_up_response) => { + v5::response::AuthenticatorResponseData::TopUpBandwidth(top_up_response.into()) + } + } + } +} + +impl From for v5::response::RegisteredResponse { + fn from(value: v4::response::RegisteredResponse) -> Self { + Self { + request_id: value.request_id, + reply: value.reply.into(), + } + } +} + +impl From for v5::response::PendingRegistrationResponse { + fn from(value: v4::response::PendingRegistrationResponse) -> Self { + Self { + request_id: value.request_id, + reply: value.reply.into(), + } + } +} + +impl From for v5::registration::RegistrationData { + fn from(value: v4::registration::RegistrationData) -> Self { + Self { + nonce: value.nonce, + gateway_data: value.gateway_data.into(), + wg_port: value.wg_port, + } + } +} + +impl From for v4::registration::RegistrationData { + fn from(value: v5::registration::RegistrationData) -> Self { + Self { + nonce: value.nonce, + gateway_data: value.gateway_data.into(), + wg_port: value.wg_port, + } + } +} + +impl From for v5::response::RemainingBandwidthResponse { + fn from(value: v4::response::RemainingBandwidthResponse) -> Self { + Self { + request_id: value.request_id, + reply: value.reply.map(Into::into), + } + } +} + +impl From for v5::response::TopUpBandwidthResponse { + fn from(value: v4::response::TopUpBandwidthResponse) -> Self { + Self { + request_id: value.request_id, + reply: value.reply.into(), + } + } +} + +impl From for v5::registration::RegistredData { + fn from(value: v4::registration::RegistredData) -> Self { + Self { + pub_key: value.pub_key, + private_ips: value.private_ips.into(), + wg_port: value.wg_port, + } + } +} + +impl From for v5::registration::RemainingBandwidthData { + fn from(value: v4::registration::RemainingBandwidthData) -> Self { + Self { + available_bandwidth: value.available_bandwidth, + } + } +} + +impl From for v5::registration::IpPair { + fn from(value: v4::registration::IpPair) -> Self { + Self { + ipv4: value.ipv4, + ipv6: value.ipv6, + } + } +} + +impl From for v4::registration::IpPair { + fn from(value: v5::registration::IpPair) -> Self { + Self { + ipv4: value.ipv4, + ipv6: value.ipv6, + } + } +} + +#[cfg(test)] +mod tests { + use std::{ + net::{Ipv4Addr, Ipv6Addr}, + str::FromStr, + }; + + use nym_credentials_interface::CredentialSpendingData; + use nym_crypto::asymmetric::encryption::PrivateKey; + use nym_sphinx::addressing::Recipient; + use nym_wireguard_types::PeerPublicKey; + use x25519_dalek::PublicKey; + + use super::*; + use crate::{ + util::tests::{CREDENTIAL_BYTES, RECIPIENT}, + v4, + }; + + #[test] + fn upgrade_initial_req() { + let pub_key = PeerPublicKey::new(PublicKey::from([0; 32])); + let reply_to = Recipient::try_from_base58_string(RECIPIENT).unwrap(); + + let (msg, _) = v4::request::AuthenticatorRequest::new_initial_request( + v4::registration::InitMessage::new(pub_key), + reply_to, + ); + let upgraded_msg = v5::request::AuthenticatorRequest::from(msg); + + assert_eq!( + upgraded_msg.protocol, + Protocol { + version: 5, + service_provider_type: ServiceProviderType::Authenticator + } + ); + assert_eq!( + upgraded_msg.data, + v5::request::AuthenticatorRequestData::Initial(v5::registration::InitMessage { + pub_key + }) + ); + } + + #[test] + fn upgrade_final_req() { + let mut rng = rand::thread_rng(); + + let local_secret = PrivateKey::new(&mut rng); + let remote_secret = x25519_dalek::StaticSecret::random_from_rng(&mut rng); + let ipv4 = Ipv4Addr::from_str("10.10.10.10").unwrap(); + let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap(); + let ips = v4::registration::IpPair::new(ipv4, ipv6); + let nonce = 42; + let gateway_client = v4::registration::GatewayClient::new( + &local_secret, + (&remote_secret).into(), + ips, + nonce, + ); + let credential = Some(CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap()); + let final_message = v4::registration::FinalMessage { + gateway_client: gateway_client.clone(), + credential: credential.clone(), + }; + let reply_to = Recipient::try_from_base58_string(RECIPIENT).unwrap(); + + let (msg, _) = + v4::request::AuthenticatorRequest::new_final_request(final_message, reply_to); + let upgraded_msg = v5::request::AuthenticatorRequest::from(msg); + + assert_eq!( + upgraded_msg.protocol, + Protocol { + version: 5, + service_provider_type: ServiceProviderType::Authenticator + } + ); + assert_eq!( + upgraded_msg.data, + v5::request::AuthenticatorRequestData::Final(Box::new( + v5::registration::FinalMessage { + gateway_client: v5::registration::GatewayClient::new( + &local_secret, + (&remote_secret).into(), + v5::registration::IpPair::new(ipv4, ipv6), + nonce + ), + credential + } + )) + ); + } + + #[test] + fn upgrade_query_req() { + let pub_key = PeerPublicKey::new(PublicKey::from([0; 32])); + let reply_to = Recipient::try_from_base58_string(RECIPIENT).unwrap(); + + let (msg, _) = v4::request::AuthenticatorRequest::new_query_request(pub_key, reply_to); + let upgraded_msg = v5::request::AuthenticatorRequest::from(msg); + + assert_eq!( + upgraded_msg.protocol, + Protocol { + version: 5, + service_provider_type: ServiceProviderType::Authenticator + } + ); + assert_eq!( + upgraded_msg.data, + v5::request::AuthenticatorRequestData::QueryBandwidth(pub_key) + ); + } + + #[test] + fn upgrade_pending_reg_resp() { + let mut rng = rand::thread_rng(); + + let local_secret = PrivateKey::new(&mut rng); + let remote_secret = x25519_dalek::StaticSecret::random_from_rng(&mut rng); + let ipv4 = Ipv4Addr::from_str("10.10.10.10").unwrap(); + let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap(); + let ips = v4::registration::IpPair::new(ipv4, ipv6); + let nonce = 42; + let wg_port = 51822; + let gateway_data = v4::registration::GatewayClient::new( + &local_secret, + (&remote_secret).into(), + ips, + nonce, + ); + let registration_data = v4::registration::RegistrationData { + nonce, + gateway_data, + wg_port, + }; + let request_id = 123; + let reply_to = Recipient::try_from_base58_string(RECIPIENT).unwrap(); + + let msg = v4::response::AuthenticatorResponse::new_pending_registration_success( + registration_data, + request_id, + reply_to, + ); + let upgraded_msg = v5::response::AuthenticatorResponse::from(msg); + + assert_eq!( + upgraded_msg.protocol, + Protocol { + version: 5, + service_provider_type: ServiceProviderType::Authenticator + } + ); + + assert_eq!( + upgraded_msg.data, + v5::response::AuthenticatorResponseData::PendingRegistration( + v5::response::PendingRegistrationResponse { + request_id, + reply: v5::registration::RegistrationData { + nonce, + gateway_data: v5::registration::GatewayClient::new( + &local_secret, + (&remote_secret).into(), + v5::registration::IpPair::new(ipv4, ipv6), + nonce + ), + wg_port + } + } + ) + ); + } + + #[test] + fn upgrade_registered_resp() { + let pub_key = PeerPublicKey::new(PublicKey::from([0; 32])); + let ipv4 = Ipv4Addr::from_str("10.1.10.10").unwrap(); + let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap(); + let private_ips = v4::registration::IpPair::new(ipv4, ipv6); + let wg_port = 51822; + let registred_data = v4::registration::RegistredData { + pub_key, + private_ips, + wg_port, + }; + let request_id = 123; + let reply_to = Recipient::try_from_base58_string(RECIPIENT).unwrap(); + + let msg = v4::response::AuthenticatorResponse::new_registered( + registred_data, + reply_to, + request_id, + ); + let upgraded_msg = v5::response::AuthenticatorResponse::from(msg); + + assert_eq!( + upgraded_msg.protocol, + Protocol { + version: 5, + service_provider_type: ServiceProviderType::Authenticator + } + ); + assert_eq!( + upgraded_msg.data, + v5::response::AuthenticatorResponseData::Registered(v5::response::RegisteredResponse { + request_id, + reply: v5::registration::RegistredData { + wg_port, + pub_key, + private_ips: v5::registration::IpPair::new(ipv4, ipv6) + } + }) + ); + } + + #[test] + fn upgrade_remaining_bandwidth_resp() { + let available_bandwidth = 42; + let remaining_bandwidth_data = Some(v4::registration::RemainingBandwidthData { + available_bandwidth, + }); + let request_id = 123; + let reply_to = Recipient::try_from_base58_string(RECIPIENT).unwrap(); + + let msg = v4::response::AuthenticatorResponse::new_remaining_bandwidth( + remaining_bandwidth_data, + reply_to, + request_id, + ); + let upgraded_msg = v5::response::AuthenticatorResponse::from(msg); + + assert_eq!( + upgraded_msg.protocol, + Protocol { + version: 5, + service_provider_type: ServiceProviderType::Authenticator + } + ); + assert_eq!( + upgraded_msg.data, + v5::response::AuthenticatorResponseData::RemainingBandwidth( + v5::response::RemainingBandwidthResponse { + request_id, + reply: Some(v5::registration::RemainingBandwidthData { + available_bandwidth, + }) + } + ) + ); + } +} diff --git a/common/authenticator-requests/src/v5/mod.rs b/common/authenticator-requests/src/v5/mod.rs new file mode 100644 index 0000000000..2f0df82353 --- /dev/null +++ b/common/authenticator-requests/src/v5/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod conversion; +pub mod registration; +pub mod request; +pub mod response; +pub mod topup; + +pub const VERSION: u8 = 5; diff --git a/common/authenticator-requests/src/v5/registration.rs b/common/authenticator-requests/src/v5/registration.rs new file mode 100644 index 0000000000..9bd5c49e44 --- /dev/null +++ b/common/authenticator-requests/src/v5/registration.rs @@ -0,0 +1,287 @@ +// -2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::Error; +use base64::{engine::general_purpose, Engine}; +use nym_credentials_interface::CredentialSpendingData; +use nym_network_defaults::constants::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6}; +use nym_wireguard_types::PeerPublicKey; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::time::SystemTime; +use std::{fmt, ops::Deref, str::FromStr}; + +#[cfg(feature = "verify")] +use hmac::{Hmac, Mac}; +#[cfg(feature = "verify")] +use nym_crypto::asymmetric::encryption::PrivateKey; +#[cfg(feature = "verify")] +use sha2::Sha256; + +pub type PendingRegistrations = HashMap; +pub type PrivateIPs = HashMap; + +#[cfg(feature = "verify")] +pub type HmacSha256 = Hmac; + +pub type Nonce = u64; +pub type Taken = Option; + +pub const BANDWIDTH_CAP_PER_DAY: u64 = 250 * 1024 * 1024 * 1024; // 250 GB + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct IpPair { + pub ipv4: Ipv4Addr, + pub ipv6: Ipv6Addr, +} + +impl IpPair { + pub fn new(ipv4: Ipv4Addr, ipv6: Ipv6Addr) -> Self { + IpPair { ipv4, ipv6 } + } +} + +impl From<(Ipv4Addr, Ipv6Addr)> for IpPair { + fn from((ipv4, ipv6): (Ipv4Addr, Ipv6Addr)) -> Self { + IpPair { ipv4, ipv6 } + } +} + +impl fmt::Display for IpPair { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "({}, {})", self.ipv4, self.ipv6) + } +} + +impl From for IpPair { + fn from(value: IpAddr) -> Self { + let (before_last_byte, last_byte) = match value { + std::net::IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]), + std::net::IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]), + }; + let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16; + let ipv4 = Ipv4Addr::new( + WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0], + WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1], + before_last_byte, + last_byte, + ); + let ipv6 = Ipv6Addr::new( + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[0], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[1], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[2], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[3], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[4], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[5], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[6], + last_bytes, + ); + IpPair::new(ipv4, ipv6) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InitMessage { + /// Base64 encoded x25519 public key + pub pub_key: PeerPublicKey, +} + +impl InitMessage { + pub fn new(pub_key: PeerPublicKey) -> Self { + InitMessage { pub_key } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct FinalMessage { + /// Gateway client data + pub gateway_client: GatewayClient, + + /// Ecash credential + pub credential: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct RegistrationData { + pub nonce: u64, + pub gateway_data: GatewayClient, + pub wg_port: u16, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct RegistredData { + pub pub_key: PeerPublicKey, + pub private_ips: IpPair, + pub wg_port: u16, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct RemainingBandwidthData { + pub available_bandwidth: i64, +} + +/// Client that wants to register sends its PublicKey bytes mac digest encrypted with a DH shared secret. +/// Gateway/Nym node can then verify pub_key payload using the same process +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct GatewayClient { + /// Base64 encoded x25519 public key + pub pub_key: PeerPublicKey, + + /// Assigned private IPs (v4 and v6) + pub private_ips: IpPair, + + /// Sha256 hmac on the data (alongside the prior nonce) + pub mac: ClientMac, +} + +impl GatewayClient { + #[cfg(feature = "verify")] + pub fn new( + local_secret: &PrivateKey, + remote_public: x25519_dalek::PublicKey, + private_ips: IpPair, + nonce: u64, + ) -> Self { + // convert from 1.0 x25519-dalek private key into 2.0 x25519-dalek + #[allow(clippy::expect_used)] + let static_secret = x25519_dalek::StaticSecret::from(local_secret.to_bytes()); + let local_public: x25519_dalek::PublicKey = (&static_secret).into(); + + let dh = static_secret.diffie_hellman(&remote_public); + + // TODO: change that to use our nym_crypto::hmac module instead + #[allow(clippy::expect_used)] + let mut mac = HmacSha256::new_from_slice(dh.as_bytes()) + .expect("x25519 shared secret is always 32 bytes long"); + + mac.update(local_public.as_bytes()); + mac.update(private_ips.to_string().as_bytes()); + mac.update(&nonce.to_le_bytes()); + + GatewayClient { + pub_key: PeerPublicKey::new(local_public), + private_ips, + mac: ClientMac(mac.finalize().into_bytes().to_vec()), + } + } + + // Reusable secret should be gateways Wireguard PK + // Client should perform this step when generating its payload, using its own WG PK + #[cfg(feature = "verify")] + pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + // convert from 1.0 x25519-dalek private key into 2.0 x25519-dalek + #[allow(clippy::expect_used)] + let static_secret = x25519_dalek::StaticSecret::from(gateway_key.to_bytes()); + + let dh = static_secret.diffie_hellman(&self.pub_key); + + // TODO: change that to use our nym_crypto::hmac module instead + #[allow(clippy::expect_used)] + let mut mac = HmacSha256::new_from_slice(dh.as_bytes()) + .expect("x25519 shared secret is always 32 bytes long"); + + mac.update(self.pub_key.as_bytes()); + mac.update(self.private_ips.to_string().as_bytes()); + mac.update(&nonce.to_le_bytes()); + + mac.verify_slice(&self.mac) + .map_err(|source| Error::FailedClientMacVerification { + client: self.pub_key.to_string(), + source, + }) + } + + pub fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +// TODO: change the inner type into generic array of size HmacSha256::OutputSize +// TODO2: rely on our internal crypto/hmac +#[derive(Debug, Clone, PartialEq)] +pub struct ClientMac(Vec); + +impl fmt::Display for ClientMac { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", general_purpose::STANDARD.encode(&self.0)) + } +} + +impl ClientMac { + #[allow(dead_code)] + pub fn new(mac: Vec) -> Self { + ClientMac(mac) + } +} + +impl Deref for ClientMac { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromStr for ClientMac { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mac_bytes: Vec = + general_purpose::STANDARD + .decode(s) + .map_err(|source| Error::MalformedClientMac { + mac: s.to_string(), + source, + })?; + + Ok(ClientMac(mac_bytes)) + } +} + +impl Serialize for ClientMac { + fn serialize(&self, serializer: S) -> Result { + let encoded_key = general_purpose::STANDARD.encode(self.0.clone()); + serializer.serialize_str(&encoded_key) + } +} + +impl<'de> Deserialize<'de> for ClientMac { + fn deserialize>(deserializer: D) -> Result { + let encoded_key = String::deserialize(deserializer)?; + ClientMac::from_str(&encoded_key).map_err(serde::de::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nym_crypto::asymmetric::encryption; + + #[test] + fn create_ip_pair() { + let ipv4: IpAddr = Ipv4Addr::from_str("10.1.10.50").unwrap().into(); + let ipv6: IpAddr = Ipv6Addr::from_str("fc01::0a32").unwrap().into(); + + assert_eq!(IpPair::from(ipv4), IpPair::from(ipv6)); + } + + #[test] + #[cfg(feature = "verify")] + fn client_request_roundtrip() { + let mut rng = rand::thread_rng(); + + let gateway_key_pair = encryption::KeyPair::new(&mut rng); + let client_key_pair = encryption::KeyPair::new(&mut rng); + + let nonce = 1234567890; + + let client = GatewayClient::new( + client_key_pair.private_key(), + x25519_dalek::PublicKey::from(gateway_key_pair.public_key().to_bytes()), + IpPair::new("10.0.0.42".parse().unwrap(), "fc00::42".parse().unwrap()), + nonce, + ); + assert!(client.verify(gateway_key_pair.private_key(), nonce).is_ok()) + } +} diff --git a/common/authenticator-requests/src/v5/request.rs b/common/authenticator-requests/src/v5/request.rs new file mode 100644 index 0000000000..4405ee75e0 --- /dev/null +++ b/common/authenticator-requests/src/v5/request.rs @@ -0,0 +1,132 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::{ + registration::{FinalMessage, InitMessage}, + topup::TopUpMessage, +}; +use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; +use nym_wireguard_types::PeerPublicKey; +use serde::{Deserialize, Serialize}; + +use crate::make_bincode_serializer; + +use super::VERSION; + +fn generate_random() -> u64 { + use rand::RngCore; + let mut rng = rand::rngs::OsRng; + rng.next_u64() +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct AuthenticatorRequest { + pub protocol: Protocol, + pub data: AuthenticatorRequestData, + pub request_id: u64, +} + +impl AuthenticatorRequest { + pub fn from_reconstructed_message( + message: &nym_sphinx::receiver::ReconstructedMessage, + ) -> Result { + use bincode::Options; + make_bincode_serializer().deserialize(&message.message) + } + + pub fn new_initial_request(init_message: InitMessage) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorRequestData::Initial(init_message), + request_id, + }, + request_id, + ) + } + + pub fn new_final_request(final_message: FinalMessage) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorRequestData::Final(Box::new(final_message)), + request_id, + }, + request_id, + ) + } + + pub fn new_query_request(peer_public_key: PeerPublicKey) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorRequestData::QueryBandwidth(peer_public_key), + request_id, + }, + request_id, + ) + } + + pub fn new_topup_request(top_up_message: TopUpMessage) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorRequestData::TopUpBandwidth(Box::new(top_up_message)), + request_id, + }, + request_id, + ) + } + + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + make_bincode_serializer().serialize(self) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum AuthenticatorRequestData { + Initial(InitMessage), + Final(Box), + QueryBandwidth(PeerPublicKey), + TopUpBandwidth(Box), +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + #[test] + fn check_first_bytes_protocol() { + let version = 5; + let data = AuthenticatorRequest { + protocol: Protocol { + version, + service_provider_type: ServiceProviderType::Authenticator, + }, + data: AuthenticatorRequestData::Initial(InitMessage::new( + PeerPublicKey::from_str("yvNUDpT5l7W/xDhiu6HkqTHDQwbs/B3J5UrLmORl1EQ=").unwrap(), + )), + request_id: 1, + }; + let bytes = *data.to_bytes().unwrap().first_chunk::<2>().unwrap(); + assert_eq!(bytes, [version, ServiceProviderType::Authenticator as u8]); + } +} diff --git a/common/authenticator-requests/src/v5/response.rs b/common/authenticator-requests/src/v5/response.rs new file mode 100644 index 0000000000..044b803d0d --- /dev/null +++ b/common/authenticator-requests/src/v5/response.rs @@ -0,0 +1,132 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData}; +use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; +use serde::{Deserialize, Serialize}; + +use crate::make_bincode_serializer; + +use super::VERSION; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct AuthenticatorResponse { + pub protocol: Protocol, + pub data: AuthenticatorResponseData, +} + +impl AuthenticatorResponse { + pub fn new_pending_registration_success( + registration_data: RegistrationData, + request_id: u64, + ) -> Self { + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorResponseData::PendingRegistration(PendingRegistrationResponse { + reply: registration_data, + request_id, + }), + } + } + + pub fn new_registered(registred_data: RegistredData, request_id: u64) -> Self { + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorResponseData::Registered(RegisteredResponse { + reply: registred_data, + request_id, + }), + } + } + + pub fn new_remaining_bandwidth( + remaining_bandwidth_data: Option, + request_id: u64, + ) -> Self { + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorResponseData::RemainingBandwidth(RemainingBandwidthResponse { + reply: remaining_bandwidth_data, + request_id, + }), + } + } + + pub fn new_topup_bandwidth( + remaining_bandwidth_data: RemainingBandwidthData, + request_id: u64, + ) -> Self { + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorResponseData::TopUpBandwidth(TopUpBandwidthResponse { + reply: remaining_bandwidth_data, + request_id, + }), + } + } + + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + make_bincode_serializer().serialize(self) + } + + pub fn from_reconstructed_message( + message: &nym_sphinx::receiver::ReconstructedMessage, + ) -> Result { + use bincode::Options; + make_bincode_serializer().deserialize(&message.message) + } + + pub fn id(&self) -> Option { + match &self.data { + AuthenticatorResponseData::PendingRegistration(response) => Some(response.request_id), + AuthenticatorResponseData::Registered(response) => Some(response.request_id), + AuthenticatorResponseData::RemainingBandwidth(response) => Some(response.request_id), + AuthenticatorResponseData::TopUpBandwidth(response) => Some(response.request_id), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum AuthenticatorResponseData { + PendingRegistration(PendingRegistrationResponse), + Registered(RegisteredResponse), + RemainingBandwidth(RemainingBandwidthResponse), + TopUpBandwidth(TopUpBandwidthResponse), +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct PendingRegistrationResponse { + pub request_id: u64, + pub reply: RegistrationData, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct RegisteredResponse { + pub request_id: u64, + pub reply: RegistredData, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct RemainingBandwidthResponse { + pub request_id: u64, + pub reply: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct TopUpBandwidthResponse { + pub request_id: u64, + pub reply: RemainingBandwidthData, +} diff --git a/common/authenticator-requests/src/v5/topup.rs b/common/authenticator-requests/src/v5/topup.rs new file mode 100644 index 0000000000..1163d07f12 --- /dev/null +++ b/common/authenticator-requests/src/v5/topup.rs @@ -0,0 +1,15 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::CredentialSpendingData; +use nym_wireguard_types::PeerPublicKey; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct TopUpMessage { + /// Base64 encoded x25519 public key + pub pub_key: PeerPublicKey, + + /// Ecash credential + pub credential: CredentialSpendingData, +} diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index a794ca4e98..028db3681a 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node" -version = "1.6.0" +version = "1.6.1" authors.workspace = true repository.workspace = true homepage.workspace = true @@ -101,4 +101,4 @@ cargo_metadata = { workspace = true } [lints] -workspace = true \ No newline at end of file +workspace = true diff --git a/service-providers/authenticator/src/cli/request.rs b/service-providers/authenticator/src/cli/request.rs index 0a8dfe3cd5..a3f39ddc91 100644 --- a/service-providers/authenticator/src/cli/request.rs +++ b/service-providers/authenticator/src/cli/request.rs @@ -111,16 +111,16 @@ pub(crate) async fn execute(args: &Request) -> Result<(), AuthenticatorError> { let authenticator_recipient = Recipient::from_str(&args.authenticator_recipient)?; let (request, _) = match request_data { AuthenticatorRequestData::Initial(init_message) => { - AuthenticatorRequest::new_initial_request(init_message, *mixnet_client.nym_address()) + AuthenticatorRequest::new_initial_request(init_message) } AuthenticatorRequestData::Final(final_message) => { - AuthenticatorRequest::new_final_request(*final_message, *mixnet_client.nym_address()) + AuthenticatorRequest::new_final_request(*final_message) } AuthenticatorRequestData::QueryBandwidth(query_message) => { - AuthenticatorRequest::new_query_request(query_message, *mixnet_client.nym_address()) + AuthenticatorRequest::new_query_request(query_message) } AuthenticatorRequestData::TopUpBandwidth(top_up_message) => { - AuthenticatorRequest::new_topup_request(*top_up_message, *mixnet_client.nym_address()) + AuthenticatorRequest::new_topup_request(*top_up_message) } }; mixnet_client diff --git a/service-providers/authenticator/src/error.rs b/service-providers/authenticator/src/error.rs index 0430a0805b..59a1597147 100644 --- a/service-providers/authenticator/src/error.rs +++ b/service-providers/authenticator/src/error.rs @@ -89,6 +89,9 @@ pub enum AuthenticatorError { #[error("unknown version number")] UnknownVersion, + #[error("missing reply_to for old client")] + MissingReplyToForOldClient, + #[error("{0}")] PublicKey(#[from] nym_wireguard_types::Error), diff --git a/service-providers/authenticator/src/mixnet_listener.rs b/service-providers/authenticator/src/mixnet_listener.rs index dcd3c8e31e..2ebb39b2c7 100644 --- a/service-providers/authenticator/src/mixnet_listener.rs +++ b/service-providers/authenticator/src/mixnet_listener.rs @@ -21,7 +21,7 @@ use nym_authenticator_requests::{ AuthenticatorRequest, AuthenticatorVersion, FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, }, - v1, v2, v3, v4, CURRENT_VERSION, + v1, v2, v3, v4, v5, CURRENT_VERSION, }; use nym_credential_verification::{ bandwidth_storage_manager::BandwidthStorageManager, ecash::EcashManager, @@ -42,7 +42,7 @@ use rand::{prelude::IteratorRandom, thread_rng}; use tokio::sync::RwLock; use tokio_stream::wrappers::IntervalStream; -type AuthenticatorHandleResult = Result<(Vec, Recipient)>; +type AuthenticatorHandleResult = Result<(Vec, Option)>; const DEFAULT_REGISTRATION_TIMEOUT_CHECK: Duration = Duration::from_secs(60); // 1 minute pub(crate) struct RegistredAndFree { @@ -155,7 +155,7 @@ impl MixnetListener { init_message: Box, protocol: Protocol, request_id: u64, - reply_to: Recipient, + reply_to: Option, ) -> AuthenticatorHandleResult { let remote_public = init_message.pub_key(); let nonce: u64 = fastrand::u64(..); @@ -178,7 +178,7 @@ impl MixnetListener { wg_port: registration_data.wg_port, }, request_id, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() .map_err(|err| { @@ -198,7 +198,7 @@ impl MixnetListener { wg_port: registration_data.wg_port, }, request_id, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() .map_err(|err| { @@ -218,7 +218,7 @@ impl MixnetListener { wg_port: registration_data.wg_port, }, request_id, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() .map_err(|err| { @@ -228,12 +228,26 @@ impl MixnetListener { AuthenticatorVersion::V4 => { v4::response::AuthenticatorResponse::new_pending_registration_success( v4::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: registration_data.gateway_data.clone().into(), + wg_port: registration_data.wg_port, + }, + request_id, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::V5 => { + v5::response::AuthenticatorResponse::new_pending_registration_success( + v5::registration::RegistrationData { nonce: registration_data.nonce, gateway_data: registration_data.gateway_data.clone(), wg_port: registration_data.wg_port, }, request_id, - reply_to, ) .to_bytes() .map_err(|err| { @@ -272,7 +286,7 @@ impl MixnetListener { private_ip: allowed_ipv4.into(), wg_port: self.config.authenticator.announced_port, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -285,7 +299,7 @@ impl MixnetListener { private_ip: allowed_ipv4.into(), wg_port: self.config.authenticator.announced_port, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -298,7 +312,7 @@ impl MixnetListener { private_ip: allowed_ipv4.into(), wg_port: self.config.authenticator.announced_port, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -311,7 +325,19 @@ impl MixnetListener { private_ips: (allowed_ipv4, allowed_ipv6).into(), wg_port: self.config.authenticator.announced_port, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, + request_id, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })?, + AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_registered( + v5::registration::RegistredData { + pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + private_ips: (allowed_ipv4, allowed_ipv6).into(), + wg_port: self.config.authenticator.announced_port, + }, request_id, ) .to_bytes() @@ -360,7 +386,7 @@ impl MixnetListener { wg_port: registration_data.wg_port, }, request_id, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() .map_err(|err| { @@ -380,7 +406,7 @@ impl MixnetListener { wg_port: registration_data.wg_port, }, request_id, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() .map_err(|err| { @@ -400,7 +426,7 @@ impl MixnetListener { wg_port: registration_data.wg_port, }, request_id, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() .map_err(|err| { @@ -410,12 +436,26 @@ impl MixnetListener { AuthenticatorVersion::V4 => { v4::response::AuthenticatorResponse::new_pending_registration_success( v4::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: registration_data.gateway_data.into(), + wg_port: registration_data.wg_port, + }, + request_id, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::V5 => { + v5::response::AuthenticatorResponse::new_pending_registration_success( + v5::registration::RegistrationData { nonce: registration_data.nonce, gateway_data: registration_data.gateway_data, wg_port: registration_data.wg_port, }, request_id, - reply_to, ) .to_bytes() .map_err(|err| { @@ -433,7 +473,7 @@ impl MixnetListener { final_message: Box, protocol: Protocol, request_id: u64, - reply_to: Recipient, + reply_to: Option, ) -> AuthenticatorHandleResult { let mut registred_and_free = self.registred_and_free.write().await; let registration_data = registred_and_free @@ -500,7 +540,7 @@ impl MixnetListener { private_ip: registration_data.gateway_data.private_ips.ipv4.into(), wg_port: registration_data.wg_port, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -511,7 +551,7 @@ impl MixnetListener { private_ip: registration_data.gateway_data.private_ips.ipv4.into(), wg_port: registration_data.wg_port, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -522,18 +562,28 @@ impl MixnetListener { private_ip: registration_data.gateway_data.private_ips.ipv4.into(), wg_port: registration_data.wg_port, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::new_registered( v4::registration::RegistredData { + pub_key: registration_data.gateway_data.pub_key, + private_ips: registration_data.gateway_data.private_ips.into(), + wg_port: registration_data.wg_port, + }, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, + request_id, + ) + .to_bytes() + .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_registered( + v5::registration::RegistredData { pub_key: registration_data.gateway_data.pub_key, private_ips: registration_data.gateway_data.private_ips, wg_port: registration_data.wg_port, }, - reply_to, request_id, ) .to_bytes() @@ -579,7 +629,7 @@ impl MixnetListener { msg: Box, protocol: Protocol, request_id: u64, - reply_to: Recipient, + reply_to: Option, ) -> AuthenticatorHandleResult { let bandwidth_data = self.peer_manager.query_bandwidth(msg).await?; let bytes = match AuthenticatorVersion::from(protocol) { @@ -589,7 +639,7 @@ impl MixnetListener { available_bandwidth: data.available_bandwidth as u64, suspended: false, }), - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -602,7 +652,7 @@ impl MixnetListener { bandwidth_data.map(|data| v2::registration::RemainingBandwidthData { available_bandwidth: data.available_bandwidth, }), - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -615,7 +665,7 @@ impl MixnetListener { bandwidth_data.map(|data| v3::registration::RemainingBandwidthData { available_bandwidth: data.available_bandwidth, }), - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -628,7 +678,19 @@ impl MixnetListener { bandwidth_data.map(|data| v4::registration::RemainingBandwidthData { available_bandwidth: data.available_bandwidth, }), - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, + request_id, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::V5 => { + v5::response::AuthenticatorResponse::new_remaining_bandwidth( + bandwidth_data.map(|data| v5::registration::RemainingBandwidthData { + available_bandwidth: data.available_bandwidth, + }), request_id, ) .to_bytes() @@ -646,7 +708,7 @@ impl MixnetListener { msg: Box, protocol: Protocol, request_id: u64, - reply_to: Recipient, + reply_to: Option, ) -> AuthenticatorHandleResult { let Some(ecash_verifier) = self.ecash_verifier.clone() else { return Err(AuthenticatorError::UnsupportedOperation); @@ -681,11 +743,19 @@ impl MixnetListener { let available_bandwidth = verifier.verify().await?; let bytes = match AuthenticatorVersion::from(protocol) { + AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_topup_bandwidth( + v5::registration::RemainingBandwidthData { + available_bandwidth, + }, + request_id, + ) + .to_bytes() + .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::new_topup_bandwidth( v4::registration::RemainingBandwidthData { available_bandwidth, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -694,7 +764,7 @@ impl MixnetListener { v3::registration::RemainingBandwidthData { available_bandwidth, }, - reply_to, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() @@ -762,10 +832,10 @@ impl MixnetListener { async fn handle_response( &self, response: Vec, - recipient: Recipient, + recipient: Option, sender_tag: Option, ) -> Result<()> { - let input_message = create_input_message(recipient, sender_tag, response); + let input_message = create_input_message(recipient, sender_tag, response)?; self.mixnet_client .send(input_message) .await @@ -866,17 +936,30 @@ fn deserialize_request(reconstructed: &ReconstructedMessage) -> Result, reply_to_tag: Option, response_packet: Vec, -) -> InputMessage { +) -> Result { let lane = TransmissionLane::General; let packet_type = None; if let Some(reply_to_tag) = reply_to_tag { log::debug!("Creating message using SURB"); - InputMessage::new_reply(reply_to_tag, response_packet, lane, packet_type) - } else { + Ok(InputMessage::new_reply( + reply_to_tag, + response_packet, + lane, + packet_type, + )) + } else if let Some(nym_address) = nym_address { log::debug!("Creating message using nym_address"); - InputMessage::new_regular(nym_address, response_packet, lane, packet_type) + Ok(InputMessage::new_regular( + nym_address, + response_packet, + lane, + packet_type, + )) + } else { + log::error!("No nym-address or sender tag provided"); + Err(AuthenticatorError::MissingReplyToForOldClient) } } From eacaf844301b3a2b8a691d517f2e7190cfd7335b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 5 Mar 2025 16:43:56 +0000 Subject: [PATCH 037/133] add full response body to error message upon decoding failure (#5566) --- common/http-api-client/src/lib.rs | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index 08c704897d..a8099ce111 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -210,6 +210,12 @@ pub enum HttpClientError { #[error("failed to resolve request. status: '{status}', additional error message: {error}")] EndpointFailure { status: StatusCode, error: E }, + #[error("failed to decode response body: {source} from {content}")] + ResponseDecodeFailure { + source: serde_json::Error, + content: String, + }, + #[cfg(target_arch = "wasm32")] #[error("the request has timed out")] RequestTimeout, @@ -866,19 +872,19 @@ where } if res.status().is_success() { - #[cfg(debug_assertions)] - { - let text = res.text().await.inspect_err(|err| { - tracing::error!("Couldn't even get response text: {err}"); - })?; - tracing::trace!("Result:\n{:#?}", text); - - serde_json::from_str(&text) - .map_err(|err| HttpClientError::GenericRequestFailure(err.to_string())) + // internally reqwest is first retrieving bytes and then performing parsing via serde_json + // (and similarly does the same thing for text()) + let full = res.bytes().await?; + match serde_json::from_slice(&full) { + Ok(data) => Ok(data), + Err(err) => { + let text = String::from_utf8_lossy(&full); + Err(HttpClientError::ResponseDecodeFailure { + source: err, + content: text.into_owned(), + }) + } } - - #[cfg(not(debug_assertions))] - Ok(res.json().await?) } else if res.status() == StatusCode::NOT_FOUND { Err(HttpClientError::NotFound) } else { From aa2f3369043925820490d66fde769378a8ff3877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 5 Mar 2025 16:44:35 +0000 Subject: [PATCH 038/133] hotfix: ensure we bail on merkle leaves insertion upon missing data (#5565) * hotfix: ensure we bail on merkle leaves insertion upon missing data * Update Cargo.toml --------- Co-authored-by: benedetta davico <46782255+benedettadavico@users.noreply.github.com> --- nym-api/Cargo.toml | 4 ++-- nym-api/src/ecash/state/local.rs | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index f95baabd00..239b483bf2 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-api" license = "GPL-3.0" -version = "1.1.51" +version = "1.1.52" authors.workspace = true edition = "2021" rust-version.workspace = true @@ -144,4 +144,4 @@ rand_chacha = { workspace = true } sha2 = "0.9" [lints] -workspace = true \ No newline at end of file +workspace = true diff --git a/nym-api/src/ecash/state/local.rs b/nym-api/src/ecash/state/local.rs index 61b5bab2c4..93cc576a85 100644 --- a/nym-api/src/ecash/state/local.rs +++ b/nym-api/src/ecash/state/local.rs @@ -33,14 +33,17 @@ impl DailyMerkleTree { .into_iter() .map(|l| (l.merkle_index, l)) .collect(); + let total_leaves = leaves.len(); let mut sorted_leaves = Vec::new(); for i in 0..leaves.len() { if let Some(next_leaf) = leaves.remove(&i) { sorted_leaves.push(next_leaf); } else { - let lost = leaves.len() - i + 1; - error!("failed to produce consistent merkle tree. there was no leaf with index {i}. at least {lost} leaves got lost") + let lost = total_leaves - i + 1; + error!("failed to produce consistent merkle tree. there was no leaf with index {i}. at least {lost} leaves got lost"); + // we have to drop all data above that height because we can't rebuild the full tree + break; } } From 0d8b3abc6fabfd241c479e7a7bebb3455fd52b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 5 Mar 2025 23:07:32 +0100 Subject: [PATCH 039/133] Deserialize v5 authenticator requests (#5568) --- Cargo.lock | 2 +- .../authenticator/src/mixnet_listener.rs | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 9cce5fff5f..bbaab3e146 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4747,7 +4747,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "nym-api" -version = "1.1.51" +version = "1.1.52" dependencies = [ "anyhow", "async-trait", diff --git a/service-providers/authenticator/src/mixnet_listener.rs b/service-providers/authenticator/src/mixnet_listener.rs index 2ebb39b2c7..d88a1185c5 100644 --- a/service-providers/authenticator/src/mixnet_listener.rs +++ b/service-providers/authenticator/src/mixnet_listener.rs @@ -928,6 +928,17 @@ fn deserialize_request(reconstructed: &ReconstructedMessage) -> Result { + if request_type == ServiceProviderType::Authenticator as u8 { + v5::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) + .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { + source: err, + }) + .map(Into::into) + } else { + Err(AuthenticatorError::InvalidPacketType(request_type)) + } + } [version, _] => { log::info!("Received packet with invalid version: v{version}"); Err(AuthenticatorError::InvalidPacketVersion(version)) From 8ddef08c72b0047dc8a001e2072cfbec88d0ac3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Thu, 6 Mar 2025 10:09:15 +0100 Subject: [PATCH 040/133] Tweak surb management to be more conservative (#5570) To reduce the risk of the IPR DoS the client: - Lower the timeout until the IPR will disconnect a client - Reduce fewer surbs at a time. Large surb requests increases the latency until all fragments in the response have been delivered. The efficiency gains of having large surb requests dimishes quickly for large sizes as well --- common/client-core/config-types/src/lib.rs | 2 +- .../src/client/real_messages_control/message_handler.rs | 6 ++++-- .../client-core/src/client/replies/reply_controller/mod.rs | 2 +- service-providers/ip-packet-router/src/constants.rs | 4 ++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/common/client-core/config-types/src/lib.rs b/common/client-core/config-types/src/lib.rs index 8eb95d62f1..82e93f9b09 100644 --- a/common/client-core/config-types/src/lib.rs +++ b/common/client-core/config-types/src/lib.rs @@ -50,7 +50,7 @@ const DEFAULT_MINIMUM_REPLY_SURB_THRESHOLD_BUFFER: usize = 0; // define how much to request at once // clients/client-core/src/client/replies/reply_controller.rs const DEFAULT_MINIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 10; -const DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 100; +const DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 30; const DEFAULT_MAXIMUM_ALLOWED_SURB_REQUEST_SIZE: u32 = 500; diff --git a/common/client-core/src/client/real_messages_control/message_handler.rs b/common/client-core/src/client/real_messages_control/message_handler.rs index b8417cead9..e0d0884e96 100644 --- a/common/client-core/src/client/real_messages_control/message_handler.rs +++ b/common/client-core/src/client/real_messages_control/message_handler.rs @@ -33,10 +33,12 @@ pub enum PreparationError { #[error(transparent)] NymTopologyError(#[from] NymTopologyError), - #[error("The received message cannot be sent using a single reply surb. It ended up getting split into {fragments} fragments.")] + #[error("message too long for a single SURB, splitting into {fragments} fragments.")] MessageTooLongForSingleSurb { fragments: usize }, - #[error("Not enough reply SURBs to send the message. We have {available} available and require at least {required}.")] + #[error( + "not enough reply SURBs to send the message, available: {available} required: {required}." + )] NotEnoughSurbs { available: usize, required: usize }, } diff --git a/common/client-core/src/client/replies/reply_controller/mod.rs b/common/client-core/src/client/replies/reply_controller/mod.rs index fb8cdc468b..7b59359407 100644 --- a/common/client-core/src/client/replies/reply_controller/mod.rs +++ b/common/client-core/src/client/replies/reply_controller/mod.rs @@ -746,7 +746,7 @@ where .request_additional_reply_surbs(target, request_size) .await { - warn!("failed to request additional surbs... - {err}") + info!("{err}") } } diff --git a/service-providers/ip-packet-router/src/constants.rs b/service-providers/ip-packet-router/src/constants.rs index 128afa271b..a707199977 100644 --- a/service-providers/ip-packet-router/src/constants.rs +++ b/service-providers/ip-packet-router/src/constants.rs @@ -7,8 +7,8 @@ use std::time::Duration; pub(crate) const DISCONNECT_TIMER_INTERVAL: Duration = Duration::from_secs(10); // We consider a client inactive if it hasn't sent any mixnet packets in this duration -pub(crate) const CLIENT_MIXNET_INACTIVITY_TIMEOUT: Duration = Duration::from_secs(5 * 60); +pub(crate) const CLIENT_MIXNET_INACTIVITY_TIMEOUT: Duration = Duration::from_secs(60); // We consider a client handler inactive if it hasn't received any packets from the tun device in // this duration -pub(crate) const CLIENT_HANDLER_ACTIVITY_TIMEOUT: Duration = Duration::from_secs(10 * 60); +pub(crate) const CLIENT_HANDLER_ACTIVITY_TIMEOUT: Duration = Duration::from_secs(5 * 60); From 698883c03fa559247b73185c6b56ff46ce5a0be6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 6 Mar 2025 09:18:39 +0000 Subject: [PATCH 041/133] feature: v2 authentication request (#5537) (#5563) * introduced v2 authentication request between clients and gateways * client to send v2 auth when possible * added persistence to last used authentication timestamp * added clients identity to signed plaintext --- Cargo.lock | 3 + .../gateway-client/src/client/mod.rs | 92 ++++-- common/gateway-requests/Cargo.toml | 3 + .../src/authentication/encrypted_address.rs | 6 + common/gateway-requests/src/lib.rs | 21 +- common/gateway-requests/src/types/error.rs | 28 ++ .../src/types/text_request/authenticate.rs | 142 +++++++++ .../{text_request.rs => text_request/mod.rs} | 23 +- ...303120000_add_authentication_timestamp.sql | 7 + common/gateway-storage/src/lib.rs | 14 + common/gateway-storage/src/models.rs | 2 +- common/gateway-storage/src/shared_keys.rs | 28 +- gateway/src/config.rs | 3 + .../node/client_handling/active_clients.rs | 33 +- .../client_handling/websocket/common_state.rs | 12 +- .../connection_handler/authenticated.rs | 4 +- .../websocket/connection_handler/fresh.rs | 289 ++++++++++++------ .../websocket/connection_handler/helpers.rs | 37 +++ .../websocket/connection_handler/mod.rs | 10 +- .../src/node/client_handling/websocket/mod.rs | 2 +- gateway/src/node/mod.rs | 7 +- nym-node/src/config/gateway_tasks.rs | 9 +- nym-node/src/config/helpers.rs | 1 + 23 files changed, 624 insertions(+), 152 deletions(-) create mode 100644 common/gateway-requests/src/types/text_request/authenticate.rs rename common/gateway-requests/src/types/{text_request.rs => text_request/mod.rs} (89%) create mode 100644 common/gateway-storage/migrations/20250303120000_add_authentication_timestamp.sql create mode 100644 gateway/src/node/client_handling/websocket/connection_handler/helpers.rs diff --git a/Cargo.lock b/Cargo.lock index bbaab3e146..5f75819cd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5760,13 +5760,16 @@ dependencies = [ "nym-credentials-interface", "nym-crypto", "nym-pemstore", + "nym-serde-helpers", "nym-sphinx", "nym-task", "rand 0.8.5", "serde", "serde_json", "strum 0.26.3", + "subtle 2.6.1", "thiserror 2.0.11", + "time", "tokio", "tracing", "tungstenite 0.20.1", diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index 84cb571c72..6bff2c1805 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -20,8 +20,8 @@ use nym_credentials_interface::TicketType; use nym_crypto::asymmetric::identity; use nym_gateway_requests::registration::handshake::client_handshake; use nym_gateway_requests::{ - BinaryRequest, ClientControlRequest, ClientRequest, SensitiveServerResponse, ServerResponse, - SharedGatewayKey, SharedSymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION, + BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersionExt, + SensitiveServerResponse, ServerResponse, SharedGatewayKey, SharedSymmetricKey, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION, }; use nym_sphinx::forwarding::packet::MixPacket; @@ -563,28 +563,10 @@ impl GatewayClient { Ok(zeroizing_updated_key) } - async fn authenticate(&mut self) -> Result<(), GatewayClientError> { - let Some(shared_key) = self.shared_key.as_ref() else { - return Err(GatewayClientError::NoSharedKeyAvailable); - }; - - if !self.connection.is_established() { - return Err(GatewayClientError::ConnectionNotEstablished); - } - debug!("authenticating with gateway"); - - let self_address = self - .local_identity - .as_ref() - .public_key() - .derive_destination_address(); - - let msg = ClientControlRequest::new_authenticate( - self_address, - shared_key, - self.cfg.bandwidth.require_tickets, - )?; - + async fn send_authenticate_request_and_handle_response( + &mut self, + msg: ClientControlRequest, + ) -> Result<(), GatewayClientError> { match self.send_websocket_message(msg).await? { ServerResponse::Authenticate { protocol_version, @@ -608,6 +590,51 @@ impl GatewayClient { } } + async fn authenticate_v1(&mut self) -> Result<(), GatewayClientError> { + debug!("using v1 authentication"); + + let Some(shared_key) = self.shared_key.as_ref() else { + return Err(GatewayClientError::NoSharedKeyAvailable); + }; + + let self_address = self + .local_identity + .public_key() + .derive_destination_address(); + + let msg = ClientControlRequest::new_authenticate( + self_address, + shared_key, + self.cfg.bandwidth.require_tickets, + )?; + self.send_authenticate_request_and_handle_response(msg) + .await + } + + async fn authenticate_v2(&mut self) -> Result<(), GatewayClientError> { + debug!("using v2 authentication"); + let Some(shared_key) = self.shared_key.as_ref() else { + return Err(GatewayClientError::NoSharedKeyAvailable); + }; + + let msg = ClientControlRequest::new_authenticate_v2(shared_key, &self.local_identity)?; + self.send_authenticate_request_and_handle_response(msg) + .await + } + + async fn authenticate(&mut self, use_v2: bool) -> Result<(), GatewayClientError> { + if !self.connection.is_established() { + return Err(GatewayClientError::ConnectionNotEstablished); + } + debug!("authenticating with gateway"); + + if use_v2 { + self.authenticate_v2().await + } else { + self.authenticate_v1().await + } + } + /// Helper method to either call register or authenticate based on self.shared_key value #[instrument(skip_all, fields( @@ -623,19 +650,25 @@ impl GatewayClient { } // 1. check gateway's protocol version - let supports_aes_gcm_siv = match self.get_gateway_protocol().await { - Ok(protocol) => protocol >= AES_GCM_SIV_PROTOCOL_VERSION, + let gw_protocol = match self.get_gateway_protocol().await { + Ok(protocol) => Some(protocol), Err(_) => { // if we failed to send the request, it means the gateway is running the old binary, // so it has reset our connection - we have to reconnect self.establish_connection().await?; - false + None } }; + let supports_aes_gcm_siv = gw_protocol.supports_aes256_gcm_siv(); + let supports_auth_v2 = gw_protocol.supports_authenticate_v2(); + if !supports_aes_gcm_siv { warn!("this gateway is on an old version that doesn't support AES256-GCM-SIV"); } + if !supports_auth_v2 { + warn!("this gateway is on an old version that doesn't support authentication v2") + } if self.authenticated { debug!("Already authenticated"); @@ -650,7 +683,7 @@ impl GatewayClient { } if self.shared_key.is_some() { - self.authenticate().await?; + self.authenticate(supports_auth_v2).await?; if self.authenticated { // if we are authenticated it means we MUST have an associated shared_key @@ -983,7 +1016,8 @@ impl GatewayClient { } // if we're reconnecting, because we lost connection, we need to re-authenticate the connection - self.authenticate().await?; + self.authenticate(self.negotiated_protocol.supports_authenticate_v2()) + .await?; // this call is NON-blocking self.start_listening_for_mixnet_messages()?; diff --git a/common/gateway-requests/Cargo.toml b/common/gateway-requests/Cargo.toml index b461c428c1..3448184eae 100644 --- a/common/gateway-requests/Cargo.toml +++ b/common/gateway-requests/Cargo.toml @@ -20,11 +20,14 @@ serde_json = { workspace = true } strum = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true, features = ["log"] } +time = { workspace = true } +subtle = { workspace = true } zeroize = { workspace = true } nym-crypto = { path = "../crypto", features = ["aead", "hashing"] } nym-pemstore = { path = "../pemstore" } nym-sphinx = { path = "../nymsphinx" } +nym-serde-helpers = { path = "../serde-helpers", features = ["base64"] } nym-task = { path = "../task" } nym-credentials = { path = "../credentials" } diff --git a/common/gateway-requests/src/authentication/encrypted_address.rs b/common/gateway-requests/src/authentication/encrypted_address.rs index 8b81074454..772b8ca91b 100644 --- a/common/gateway-requests/src/authentication/encrypted_address.rs +++ b/common/gateway-requests/src/authentication/encrypted_address.rs @@ -15,6 +15,12 @@ use thiserror::Error; // this is no longer constant size due to the differences in ciphertext between aes128ctr and aes256gcm-siv (inclusion of tag) pub struct EncryptedAddressBytes(Vec); +impl From> for EncryptedAddressBytes { + fn from(encrypted_address: Vec) -> Self { + EncryptedAddressBytes(encrypted_address) + } +} + #[derive(Debug, Error)] pub enum EncryptedAddressConversionError { #[error("Failed to decode the encrypted address - {0}")] diff --git a/common/gateway-requests/src/lib.rs b/common/gateway-requests/src/lib.rs index b7af197cc5..0d300bc4a4 100644 --- a/common/gateway-requests/src/lib.rs +++ b/common/gateway-requests/src/lib.rs @@ -19,7 +19,7 @@ pub use shared_key::{ SharedGatewayKey, SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey, }; -pub const CURRENT_PROTOCOL_VERSION: u8 = AES_GCM_SIV_PROTOCOL_VERSION; +pub const CURRENT_PROTOCOL_VERSION: u8 = AUTHENTICATE_V2_PROTOCOL_VERSION; /// Defines the current version of the communication protocol between gateway and clients. /// It has to be incremented for any breaking change. @@ -27,10 +27,29 @@ pub const CURRENT_PROTOCOL_VERSION: u8 = AES_GCM_SIV_PROTOCOL_VERSION; // 1 - initial release // 2 - changes to client credentials structure // 3 - change to AES-GCM-SIV and non-zero IVs +// 4 - introduction of v2 authentication protocol to prevent reply attacks pub const INITIAL_PROTOCOL_VERSION: u8 = 1; pub const CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION: u8 = 2; pub const AES_GCM_SIV_PROTOCOL_VERSION: u8 = 3; +pub const AUTHENTICATE_V2_PROTOCOL_VERSION: u8 = 4; // TODO: could using `Mac` trait here for OutputSize backfire? // Should hmac itself be exposed, imported and used instead? pub type LegacyGatewayMacSize = ::OutputSize; + +pub trait GatewayProtocolVersionExt { + fn supports_aes256_gcm_siv(&self) -> bool; + fn supports_authenticate_v2(&self) -> bool; +} + +impl GatewayProtocolVersionExt for Option { + fn supports_aes256_gcm_siv(&self) -> bool { + let Some(protocol) = *self else { return false }; + protocol >= AES_GCM_SIV_PROTOCOL_VERSION + } + + fn supports_authenticate_v2(&self) -> bool { + let Some(protocol) = *self else { return false }; + protocol >= AUTHENTICATE_V2_PROTOCOL_VERSION + } +} diff --git a/common/gateway-requests/src/types/error.rs b/common/gateway-requests/src/types/error.rs index 31d94b75b7..0a785e60ec 100644 --- a/common/gateway-requests/src/types/error.rs +++ b/common/gateway-requests/src/types/error.rs @@ -3,6 +3,7 @@ use crate::SharedKeyUsageError; use nym_credentials_interface::CompactEcashError; +use nym_crypto::asymmetric::ed25519::SignatureError; use nym_sphinx::addressing::nodes::NymNodeRoutingAddressError; use nym_sphinx::forwarding::packet::MixPacketFormattingError; use nym_sphinx::params::packet_sizes::PacketSize; @@ -92,7 +93,34 @@ pub enum GatewayRequestsError { #[error("the provided [v1] credential has invalid number of parameters - {0}")] InvalidNumberOfEmbededParameters(u32), + #[error("failed to authenticate the client: {0}")] + Authentication(#[from] AuthenticationFailure), + // variant to catch legacy errors #[error("{0}")] Other(String), } + +#[derive(Debug, Error)] +pub enum AuthenticationFailure { + #[error(transparent)] + KeyUsageFailure(#[from] SharedKeyUsageError), + + #[error("failed to verify provided address ciphertext")] + MalformedCiphertext, + + #[error("failed to verify request signature")] + InvalidSignature(#[from] SignatureError), + + #[error("provided request timestamp is in the future")] + RequestTimestampInFuture, + + #[error("the client is not registered")] + NotRegistered, + + #[error("the provided request is too stale to process")] + StaleRequest, + + #[error("the provided request timestamp is smaller or equal to a one previously used")] + RequestReuse, +} diff --git a/common/gateway-requests/src/types/text_request/authenticate.rs b/common/gateway-requests/src/types/text_request/authenticate.rs new file mode 100644 index 0000000000..6015beb8ad --- /dev/null +++ b/common/gateway-requests/src/types/text_request/authenticate.rs @@ -0,0 +1,142 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::{AuthenticationFailure, GatewayRequestsError, SharedGatewayKey}; +use nym_crypto::asymmetric::ed25519; +use serde::{Deserialize, Serialize}; +use std::iter; +use std::time::Duration; +use subtle::ConstantTimeEq; +use time::OffsetDateTime; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct AuthenticateRequest { + #[serde(flatten)] + pub content: AuthenticateRequestContent, + + pub request_signature: ed25519::Signature, +} + +impl AuthenticateRequest { + pub fn new( + protocol_version: u8, + shared_key: &SharedGatewayKey, + identity_keys: &ed25519::KeyPair, + ) -> Result { + let content = AuthenticateRequestContent::new( + protocol_version, + shared_key, + *identity_keys.public_key(), + )?; + let plaintext = content.plaintext(); + let request_signature = identity_keys.private_key().sign(&plaintext); + + Ok(AuthenticateRequest { + content, + request_signature, + }) + } + + pub fn verify_timestamp(&self, max_request_age: Duration) -> Result<(), AuthenticationFailure> { + let now = OffsetDateTime::now_utc(); + if self.content.request_timestamp() + max_request_age < now { + return Err(AuthenticationFailure::StaleRequest); + } + if self.content.request_timestamp() > now { + return Err(AuthenticationFailure::RequestTimestampInFuture); + } + Ok(()) + } + + pub fn ensure_timestamp_not_reused( + &self, + previous: OffsetDateTime, + ) -> Result<(), AuthenticationFailure> { + if self.content.request_timestamp() <= previous { + return Err(AuthenticationFailure::RequestReuse); + } + Ok(()) + } + + pub fn verify_ciphertext( + &self, + shared_key: &SharedGatewayKey, + ) -> Result<(), AuthenticationFailure> { + let expected = shared_key.encrypt( + self.content + .client_identity + .derive_destination_address() + .as_bytes_ref(), + Some(&self.content.nonce), + )?; + + if !bool::from(expected.ct_eq(&self.content.address_ciphertext)) { + return Err(AuthenticationFailure::MalformedCiphertext); + } + Ok(()) + } + + pub fn verify_signature(&self) -> Result<(), AuthenticationFailure> { + let plaintext = self.content.plaintext(); + self.content + .client_identity + .verify(plaintext, &self.request_signature) + .map_err(Into::into) + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct AuthenticateRequestContent { + pub protocol_version: u8, + + // this is identical to the client's address + pub client_identity: ed25519::PublicKey, + + #[serde(with = "nym_serde_helpers::base64")] + pub address_ciphertext: Vec, + + #[serde(with = "nym_serde_helpers::base64")] + pub nonce: Vec, + + pub request_unix_timestamp: u64, +} + +impl AuthenticateRequestContent { + fn new( + protocol_version: u8, + shared_key: &SharedGatewayKey, + client_identity: ed25519::PublicKey, + ) -> Result { + let nonce = shared_key.random_nonce_or_iv(); + let destination_address = client_identity.derive_destination_address(); + + let address_ciphertext = + shared_key.encrypt(destination_address.as_bytes_ref(), Some(&nonce))?; + let now = OffsetDateTime::now_utc(); + Ok(AuthenticateRequestContent { + protocol_version, + client_identity, + address_ciphertext, + nonce, + request_unix_timestamp: now.unix_timestamp() as u64, // SAFETY: we're running this in post 1970... + }) + } +} + +impl AuthenticateRequestContent { + pub fn plaintext(&self) -> Vec { + iter::once(self.protocol_version) + .chain(self.client_identity.to_bytes()) + .chain(self.address_ciphertext.iter().copied()) + .chain(self.nonce.iter().copied()) + .chain(self.request_unix_timestamp.to_be_bytes()) + .collect() + } + + pub fn request_timestamp(&self) -> OffsetDateTime { + OffsetDateTime::from_unix_timestamp(self.request_unix_timestamp as i64) + .unwrap_or(OffsetDateTime::UNIX_EPOCH) + } +} diff --git a/common/gateway-requests/src/types/text_request.rs b/common/gateway-requests/src/types/text_request/mod.rs similarity index 89% rename from common/gateway-requests/src/types/text_request.rs rename to common/gateway-requests/src/types/text_request/mod.rs index 0fb864a533..7182dd834e 100644 --- a/common/gateway-requests/src/types/text_request.rs +++ b/common/gateway-requests/src/types/text_request/mod.rs @@ -2,16 +2,21 @@ // SPDX-License-Identifier: Apache-2.0 use crate::models::CredentialSpendingRequest; +use crate::text_request::authenticate::AuthenticateRequest; use crate::{ GatewayRequestsError, SharedGatewayKey, SymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION, - CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION, + AUTHENTICATE_V2_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, + INITIAL_PROTOCOL_VERSION, }; use nym_credentials_interface::CredentialSpendingData; +use nym_crypto::asymmetric::ed25519; use nym_sphinx::DestinationAddressBytes; use serde::{Deserialize, Serialize}; use std::str::FromStr; use tungstenite::Message; +pub mod authenticate; + // wrapper for all encrypted requests for ease of use #[derive(Serialize, Deserialize, Debug, Clone)] #[non_exhaustive] @@ -68,6 +73,9 @@ pub enum ClientControlRequest { enc_address: String, iv: String, }, + + AuthenticateV2(Box), + #[serde(alias = "handshakePayload")] RegisterHandshakeInitRequest { #[serde(default)] @@ -123,9 +131,22 @@ impl ClientControlRequest { }) } + pub fn new_authenticate_v2( + shared_key: &SharedGatewayKey, + identity_keys: &ed25519::KeyPair, + ) -> Result { + // if we're using v2 authentication, we must announce at least that protocol version + let protocol_version = AUTHENTICATE_V2_PROTOCOL_VERSION; + + Ok(ClientControlRequest::AuthenticateV2(Box::new( + AuthenticateRequest::new(protocol_version, shared_key, identity_keys)?, + ))) + } + pub fn name(&self) -> String { match self { ClientControlRequest::Authenticate { .. } => "Authenticate".to_string(), + ClientControlRequest::AuthenticateV2(..) => "AuthenticateV2".to_string(), ClientControlRequest::RegisterHandshakeInitRequest { .. } => { "RegisterHandshakeInitRequest".to_string() } diff --git a/common/gateway-storage/migrations/20250303120000_add_authentication_timestamp.sql b/common/gateway-storage/migrations/20250303120000_add_authentication_timestamp.sql new file mode 100644 index 0000000000..e211803e9e --- /dev/null +++ b/common/gateway-storage/migrations/20250303120000_add_authentication_timestamp.sql @@ -0,0 +1,7 @@ +/* + * Copyright 2025 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +ALTER TABLE shared_keys + ADD COLUMN last_used_authentication TIMESTAMP WITHOUT TIME ZONE; \ No newline at end of file diff --git a/common/gateway-storage/src/lib.rs b/common/gateway-storage/src/lib.rs index 5f289ec106..2d05d43fe7 100644 --- a/common/gateway-storage/src/lib.rs +++ b/common/gateway-storage/src/lib.rs @@ -200,6 +200,20 @@ impl GatewayStorage { Ok(()) } + pub async fn update_last_used_authentication_timestamp( + &self, + client_id: i64, + last_used_authentication_timestamp: OffsetDateTime, + ) -> Result<(), GatewayStorageError> { + self.shared_key_manager + .update_last_used_authentication_timestamp( + client_id, + last_used_authentication_timestamp, + ) + .await?; + Ok(()) + } + pub async fn get_client(&self, client_id: i64) -> Result, GatewayStorageError> { let client = self.client_manager.get_client(client_id).await?; Ok(client) diff --git a/common/gateway-storage/src/models.rs b/common/gateway-storage/src/models.rs index c623f72602..665b8fee47 100644 --- a/common/gateway-storage/src/models.rs +++ b/common/gateway-storage/src/models.rs @@ -14,13 +14,13 @@ pub struct Client { #[derive(FromRow)] pub struct PersistedSharedKeys { - #[allow(dead_code)] pub client_id: i64, #[allow(dead_code)] pub client_address_bs58: String, pub derived_aes128_ctr_blake3_hmac_keys_bs58: Option, pub derived_aes256_gcm_siv_key: Option>, + pub last_used_authentication: Option, } impl TryFrom for SharedGatewayKey { diff --git a/common/gateway-storage/src/shared_keys.rs b/common/gateway-storage/src/shared_keys.rs index 9d17535fb2..a7ae832cac 100644 --- a/common/gateway-storage/src/shared_keys.rs +++ b/common/gateway-storage/src/shared_keys.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::models::PersistedSharedKeys; +use time::OffsetDateTime; #[derive(Clone)] pub(crate) struct SharedKeysManager { @@ -68,6 +69,22 @@ impl SharedKeysManager { Ok(()) } + pub(crate) async fn update_last_used_authentication_timestamp( + &self, + client_id: i64, + last_used: OffsetDateTime, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE shared_keys SET last_used_authentication = ? WHERE client_id = ?;", + last_used, + client_id + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + /// Tries to retrieve shared keys stored for the particular client. /// /// # Arguments @@ -77,13 +94,10 @@ impl SharedKeysManager { &self, client_address_bs58: &str, ) -> Result, sqlx::Error> { - sqlx::query_as!( - PersistedSharedKeys, - "SELECT * FROM shared_keys WHERE client_address_bs58 = ?", - client_address_bs58 - ) - .fetch_optional(&self.connection_pool) - .await + sqlx::query_as("SELECT * FROM shared_keys WHERE client_address_bs58 = ?") + .bind(client_address_bs58) + .fetch_optional(&self.connection_pool) + .await } /// Removes from the database shared keys derived with the particular client. diff --git a/gateway/src/config.rs b/gateway/src/config.rs index 02d58ad143..1f6db8f0b8 100644 --- a/gateway/src/config.rs +++ b/gateway/src/config.rs @@ -101,6 +101,9 @@ pub struct Debug { pub maximum_open_connections: usize, pub zk_nym_tickets: ZkNymTicketHandlerDebug, + + /// Defines the maximum age of a signed authentication request before it's deemed too stale to process. + pub maximum_auth_request_age: Duration, } #[derive(Debug, Clone)] diff --git a/gateway/src/node/client_handling/active_clients.rs b/gateway/src/node/client_handling/active_clients.rs index 4a3e55bef5..eeff95d907 100644 --- a/gateway/src/node/client_handling/active_clients.rs +++ b/gateway/src/node/client_handling/active_clients.rs @@ -6,11 +6,20 @@ use crate::node::client_handling::embedded_clients::LocalEmbeddedClientHandle; use dashmap::DashMap; use nym_sphinx::DestinationAddressBytes; use std::sync::Arc; +use time::OffsetDateTime; use tracing::warn; +#[derive(Clone)] +pub(crate) struct RemoteClientData { + // note, this does **NOT** indicate timestamp of when client connected + // it is (for v2 auth) timestamp the client **signed** when it created the request + pub(crate) session_request_timestamp: OffsetDateTime, + pub(crate) channels: ClientIncomingChannels, +} + enum ActiveClient { /// Handle to a remote client connected via a network socket. - Remote(ClientIncomingChannels), + Remote(RemoteClientData), /// Handle to a locally (inside the same process) running client. Embedded(LocalEmbeddedClientHandle), @@ -19,14 +28,14 @@ enum ActiveClient { impl ActiveClient { fn get_sender_ref(&self) -> &MixMessageSender { match self { - ActiveClient::Remote(remote) => &remote.mix_message_sender, + ActiveClient::Remote(remote) => &remote.channels.mix_message_sender, ActiveClient::Embedded(embedded) => &embedded.mix_message_sender, } } fn get_sender(&self) -> MixMessageSender { match self { - ActiveClient::Remote(remote) => remote.mix_message_sender.clone(), + ActiveClient::Remote(remote) => remote.channels.mix_message_sender.clone(), ActiveClient::Embedded(embedded) => embedded.mix_message_sender.clone(), } } @@ -78,18 +87,18 @@ impl ActiveClientsStore { pub(crate) fn get_remote_client( &self, address: DestinationAddressBytes, - ) -> Option { + ) -> Option { let entry = self.inner.get(&address)?; let handle = entry.value(); - let ActiveClient::Remote(channels) = handle else { + let ActiveClient::Remote(remote) = handle else { warn!("attempted to get a remote handle to a embedded network requester"); return None; }; // if the entry is stale, remove it from the map - if !channels.mix_message_sender.is_closed() { - Some(channels.clone()) + if !remote.channels.mix_message_sender.is_closed() { + Some(remote.clone()) } else { // drop the reference to the map to prevent deadlocks drop(entry); @@ -137,10 +146,14 @@ impl ActiveClientsStore { client: DestinationAddressBytes, handle: MixMessageSender, is_active_request_sender: IsActiveRequestSender, + session_request_timestamp: OffsetDateTime, ) { - let entry = ActiveClient::Remote(ClientIncomingChannels { - mix_message_sender: handle, - is_active_request_sender, + let entry = ActiveClient::Remote(RemoteClientData { + session_request_timestamp, + channels: ClientIncomingChannels { + mix_message_sender: handle, + is_active_request_sender, + }, }); if self.inner.insert(client, entry).is_some() { panic!("inserted a duplicate remote client") diff --git a/gateway/src/node/client_handling/websocket/common_state.rs b/gateway/src/node/client_handling/websocket/common_state.rs index 223c534b26..a571117895 100644 --- a/gateway/src/node/client_handling/websocket/common_state.rs +++ b/gateway/src/node/client_handling/websocket/common_state.rs @@ -9,15 +9,23 @@ use nym_mixnet_client::forwarder::MixForwardingSender; use nym_node_metrics::events::MetricEventsSender; use nym_node_metrics::NymNodeMetrics; use std::sync::Arc; +use std::time::Duration; + +#[derive(Clone)] +pub(crate) struct Config { + pub(crate) enforce_zk_nym: bool, + pub(crate) max_auth_request_age: Duration, + + pub(crate) bandwidth: BandwidthFlushingBehaviourConfig, +} // I can see this being possible expanded with say storage or client store #[derive(Clone)] pub(crate) struct CommonHandlerState { + pub(crate) cfg: Config, pub(crate) ecash_verifier: Arc, pub(crate) storage: GatewayStorage, pub(crate) local_identity: Arc, - pub(crate) only_coconut_credentials: bool, - pub(crate) bandwidth_cfg: BandwidthFlushingBehaviourConfig, pub(crate) metrics: NymNodeMetrics, pub(crate) metrics_sender: MetricEventsSender, pub(crate) outbound_mix_sender: MixForwardingSender, diff --git a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs index 6c39b613ce..16c5a42faa 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -194,8 +194,8 @@ impl AuthenticatedHandler { fresh.shared_state.storage.clone(), ClientBandwidth::new(bandwidth.into()), client.id, - fresh.shared_state.bandwidth_cfg, - fresh.shared_state.only_coconut_credentials, + fresh.shared_state.cfg.bandwidth, + fresh.shared_state.cfg.enforce_zk_nym, ), inner: fresh, client, diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index 818aea8fdb..4c0eb81f35 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -1,11 +1,13 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node::client_handling::active_clients::RemoteClientData; use crate::node::client_handling::websocket::common_state::CommonHandlerState; +use crate::node::client_handling::websocket::connection_handler::helpers::KeyWithAuthTimestamp; use crate::node::client_handling::websocket::connection_handler::INITIAL_MESSAGE_TIMEOUT; use crate::node::client_handling::websocket::{ connection_handler::{AuthenticatedHandler, ClientDetails, InitialAuthResult, SocketStream}, - message_receiver::{IsActive, IsActiveRequestSender}, + message_receiver::IsActive, }; use futures::{ channel::{mpsc, oneshot}, @@ -13,14 +15,16 @@ use futures::{ }; use nym_credentials_interface::AvailableBandwidth; use nym_crypto::aes::cipher::crypto_common::rand_core::RngCore; -use nym_crypto::asymmetric::identity; +use nym_crypto::asymmetric::ed25519; +use nym_gateway_requests::authenticate::AuthenticateRequest; use nym_gateway_requests::authentication::encrypted_address::{ EncryptedAddressBytes, EncryptedAddressConversionError, }; use nym_gateway_requests::{ registration::handshake::{error::HandshakeError, gateway_handshake}, types::{ClientControlRequest, ServerResponse}, - BinaryResponse, SharedGatewayKey, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION, + AuthenticationFailure, BinaryResponse, SharedGatewayKey, CURRENT_PROTOCOL_VERSION, + INITIAL_PROTOCOL_VERSION, }; use nym_gateway_storage::error::GatewayStorageError; use nym_node_metrics::events::MetricsEvent; @@ -30,6 +34,7 @@ use rand::CryptoRng; use std::net::SocketAddr; use std::time::Duration; use thiserror::Error; +use time::OffsetDateTime; use tokio::io::{AsyncRead, AsyncWrite}; use tokio::time::timeout; use tokio_tungstenite::tungstenite::{protocol::Message, Error as WsError}; @@ -37,6 +42,12 @@ use tracing::*; #[derive(Debug, Error)] pub(crate) enum InitialAuthenticationError { + #[error(transparent)] + AuthenticationFailure(#[from] AuthenticationFailure), + + #[error("attempted to overwrite client session with a stale authentication")] + StaleSessionOverwrite, + #[error("Internal gateway storage error")] StorageError(#[from] GatewayStorageError), @@ -290,15 +301,15 @@ impl FreshHandler { // of doing full parse of the init_data elsewhere fn extract_remote_identity_from_register_init( init_data: &[u8], - ) -> Result { - if init_data.len() < identity::PUBLIC_KEY_LENGTH { + ) -> Result { + if init_data.len() < ed25519::PUBLIC_KEY_LENGTH { Err(InitialAuthenticationError::HandshakeError( HandshakeError::MalformedRequest, )) } else { - identity::PublicKey::from_bytes(&init_data[..identity::PUBLIC_KEY_LENGTH]).map_err( - |_| InitialAuthenticationError::HandshakeError(HandshakeError::MalformedRequest), - ) + ed25519::PublicKey::from_bytes(&init_data[..ed25519::PUBLIC_KEY_LENGTH]).map_err(|_| { + InitialAuthenticationError::HandshakeError(HandshakeError::MalformedRequest) + }) } } @@ -351,6 +362,21 @@ impl FreshHandler { Ok(()) } + async fn retrieve_shared_key( + &self, + client: DestinationAddressBytes, + ) -> Result, InitialAuthenticationError> { + let shared_keys = self.shared_state.storage.get_shared_keys(client).await?; + + let Some(stored_shared_keys) = shared_keys else { + return Ok(None); + }; + + let keys = KeyWithAuthTimestamp::try_from_stored(stored_shared_keys, client)?; + + Ok(Some(keys)) + } + /// Checks whether the stored shared keys match the received data, i.e. whether the upon decryption /// the provided encrypted address matches the expected unencrypted address. /// @@ -361,31 +387,18 @@ impl FreshHandler { /// * `client_address`: address of the client. /// * `encrypted_address`: encrypted address of the client, presumably encrypted using the shared keys. /// * `iv`: nonce/iv created for this particular encryption. - async fn verify_stored_shared_key( + async fn auth_v1_verify_stored_shared_key( &self, client_address: DestinationAddressBytes, encrypted_address: EncryptedAddressBytes, nonce: &[u8], - ) -> Result, InitialAuthenticationError> { - let shared_keys = self - .shared_state - .storage - .get_shared_keys(client_address) - .await?; - - let Some(stored_shared_keys) = shared_keys else { + ) -> Result, InitialAuthenticationError> { + let Some(keys) = self.retrieve_shared_key(client_address).await? else { return Ok(None); }; - let keys = SharedGatewayKey::try_from(stored_shared_keys).map_err(|source| { - InitialAuthenticationError::MalformedStoredSharedKey { - client_id: client_address.as_base58_string(), - source, - } - })?; - // LEGACY ISSUE: we're not verifying HMAC key - if encrypted_address.verify(&client_address, &keys, nonce) { + if encrypted_address.verify(&client_address, &keys.key, nonce) { Ok(Some(keys)) } else { Ok(None) @@ -428,49 +441,19 @@ impl FreshHandler { } } - /// Using the received challenge data, i.e. client's address as well the ciphertext of it plus - /// a fresh IV, attempts to authenticate the client by checking whether the ciphertext matches - /// the expected value if encrypted with the shared key. - /// - /// Finally, upon completion, all previously stored messages are pushed back to the client. - /// - /// # Arguments - /// - /// * `client_address`: address of the client wishing to authenticate. - /// * `encrypted_address`: ciphertext of the address of the client wishing to authenticate. - /// * `iv`: fresh nonce/IV received with the request. - async fn authenticate_client( - &mut self, - client_address: DestinationAddressBytes, - encrypted_address: EncryptedAddressBytes, - nonce: &[u8], - ) -> Result, InitialAuthenticationError> - where - S: AsyncRead + AsyncWrite + Unpin, - { - debug!( - "Processing authenticate client request for: {}", - client_address.as_base58_string() - ); - - let shared_keys = self - .verify_stored_shared_key(client_address, encrypted_address, nonce) - .await?; - - if let Some(shared_keys) = shared_keys { - self.push_stored_messages_to_client(client_address, &shared_keys) - .await?; - Ok(Some(shared_keys)) - } else { - Ok(None) - } - } - async fn handle_duplicate_client( &mut self, address: DestinationAddressBytes, - mut is_active_request_tx: IsActiveRequestSender, + remote_client_data: RemoteClientData, + new_session_start: OffsetDateTime, ) -> Result<(), InitialAuthenticationError> { + let mut is_active_request_tx = remote_client_data.channels.is_active_request_sender; + + // new session must **always** be explicitly more recent + if new_session_start <= remote_client_data.session_request_timestamp { + return Err(InitialAuthenticationError::StaleSessionOverwrite); + } + // Ask the other connection to ping if they are still active. // Use a oneshot channel to return the result to us let (ping_result_sender, ping_result_receiver) = oneshot::channel(); @@ -519,6 +502,32 @@ impl FreshHandler { Ok(()) } + #[allow(dead_code)] + async fn get_registered_client_id( + &self, + client_address: DestinationAddressBytes, + ) -> Result { + self.shared_state + .storage + .get_mixnet_client_id(client_address) + .await + .map_err(Into::into) + } + + async fn get_registered_available_bandwidth( + &self, + client_id: i64, + ) -> Result { + let available_bandwidth: AvailableBandwidth = self + .shared_state + .storage + .get_available_bandwidth(client_id) + .await? + .map(From::from) + .unwrap_or_default(); + Ok(available_bandwidth) + } + /// Tries to handle the received authentication request by checking correctness of the received data. /// /// # Arguments @@ -531,7 +540,7 @@ impl FreshHandler { address = %address, ) )] - async fn handle_authenticate( + async fn handle_legacy_authenticate( &mut self, client_protocol_version: Option, address: String, @@ -541,7 +550,7 @@ impl FreshHandler { where S: AsyncRead + AsyncWrite + Unpin, { - debug!("handling client registration"); + debug!("handling client authentication (v1)"); let negotiated_protocol = self.negotiate_client_protocol(client_protocol_version)?; // populate the negotiated protocol for future uses @@ -554,38 +563,38 @@ impl FreshHandler { .into_vec() .map_err(InitialAuthenticationError::MalformedIV)?; - // Check for duplicate clients - if let Some(client_tx) = self - .shared_state - .active_clients_store - .get_remote_client(address) - { - warn!("Detected duplicate connection for client: {address}"); - self.handle_duplicate_client(address, client_tx.is_active_request_sender) - .await?; - } - + // validate the shared key let Some(shared_keys) = self - .authenticate_client(address, encrypted_address, &nonce) + .auth_v1_verify_stored_shared_key(address, encrypted_address, &nonce) .await? else { // it feels weird to be returning an 'Ok' here, but I didn't want to change the existing behaviour return Ok(InitialAuthResult::new_failed(Some(negotiated_protocol))); }; - let client_id = self + // in v1 we don't have explicit data so we have to use current timestamp + // (which does nothing but just allows us to use the same codepath) + let session_request_start = OffsetDateTime::now_utc(); + + // Check for duplicate clients + if let Some(remote_client_data) = self .shared_state - .storage - .get_mixnet_client_id(address) + .active_clients_store + .get_remote_client(address) + { + warn!("Detected duplicate connection for client: {address}"); + self.handle_duplicate_client(address, remote_client_data, session_request_start) + .await?; + } + + let client_id = shared_keys.client_id; + + // if applicable, push stored messages + self.push_stored_messages_to_client(address, &shared_keys.key) .await?; - let available_bandwidth: AvailableBandwidth = self - .shared_state - .storage - .get_available_bandwidth(client_id) - .await? - .map(From::from) - .unwrap_or_default(); + // check the bandwidth + let available_bandwidth = self.get_registered_available_bandwidth(client_id).await?; let bandwidth_remaining = if available_bandwidth.expired() { self.shared_state.storage.reset_bandwidth(client_id).await?; @@ -595,7 +604,98 @@ impl FreshHandler { }; Ok(InitialAuthResult::new( - Some(ClientDetails::new(client_id, address, shared_keys)), + Some(ClientDetails::new( + client_id, + address, + shared_keys.key, + session_request_start, + )), + ServerResponse::Authenticate { + protocol_version: Some(negotiated_protocol), + status: true, + bandwidth_remaining, + }, + )) + } + + async fn handle_authenticate_v2( + &mut self, + request: Box, + ) -> Result + where + S: AsyncRead + AsyncWrite + Unpin, + { + debug!("handling client authentication (v2)"); + + let negotiated_protocol = + self.negotiate_client_protocol(Some(request.content.protocol_version))?; + // populate the negotiated protocol for future uses + self.negotiated_protocol = Some(negotiated_protocol); + + let address = request.content.client_identity.derive_destination_address(); + + // do cheap checks first + // is the provided timestamp relatively recent (and not in the future?) + request.verify_timestamp(self.shared_state.cfg.max_auth_request_age)?; + + // does the message signature verify? + request.verify_signature()?; + + // retrieve the actually stored key and check if the ciphertext matches + let Some(shared_key) = self.retrieve_shared_key(address).await? else { + return Err(AuthenticationFailure::NotRegistered)?; + }; + request.verify_ciphertext(&shared_key.key)?; + + let session_request_start = request.content.request_timestamp(); + + // if the client has already authenticated in the past, make sure this authentication timestamp + // is different and greater than the old one (in case it was replayed) + if let Some(prior_usage) = shared_key.last_used_authentication { + request.ensure_timestamp_not_reused(prior_usage)?; + } + + // check for duplicate clients + if let Some(client_data) = self + .shared_state + .active_clients_store + .get_remote_client(address) + { + warn!("Detected duplicate connection for client: {address}"); + self.handle_duplicate_client(address, client_data, session_request_start) + .await?; + } + + let client_id = shared_key.client_id; + + // update the auth timestamp for future uses + self.shared_state + .storage + .update_last_used_authentication_timestamp(client_id, session_request_start) + .await?; + + // push any old stored messages to the client + // (this will be removed soon) + self.push_stored_messages_to_client(address, &shared_key.key) + .await?; + + // finally check and retrieve client's bandwidth + let available_bandwidth = self.get_registered_available_bandwidth(client_id).await?; + + let bandwidth_remaining = if available_bandwidth.expired() { + self.shared_state.storage.reset_bandwidth(client_id).await?; + 0 + } else { + available_bandwidth.bytes + }; + + Ok(InitialAuthResult::new( + Some(ClientDetails::new( + client_id, + address, + shared_key.key, + session_request_start, + )), ServerResponse::Authenticate { protocol_version: Some(negotiated_protocol), status: true, @@ -688,7 +788,12 @@ impl FreshHandler { debug!(client_id = %client_id, "managed to finalize client registration"); - let client_details = ClientDetails::new(client_id, remote_address, shared_keys); + let client_details = ClientDetails::new( + client_id, + remote_address, + shared_keys, + OffsetDateTime::now_utc(), + ); Ok(InitialAuthResult::new( Some(client_details), @@ -734,9 +839,10 @@ impl FreshHandler { enc_address, iv, } => { - self.handle_authenticate(protocol_version, address, enc_address, iv) + self.handle_legacy_authenticate(protocol_version, address, enc_address, iv) .await } + ClientControlRequest::AuthenticateV2(req) => self.handle_authenticate_v2(req).await, ClientControlRequest::RegisterHandshakeInitRequest { protocol_version, data, @@ -827,6 +933,7 @@ impl FreshHandler { registration_details.address, mix_sender, is_active_request_sender, + registration_details.session_request_timestamp, ); return AuthenticatedHandler::upgrade( diff --git a/gateway/src/node/client_handling/websocket/connection_handler/helpers.rs b/gateway/src/node/client_handling/websocket/connection_handler/helpers.rs new file mode 100644 index 0000000000..5a5d24c5d6 --- /dev/null +++ b/gateway/src/node/client_handling/websocket/connection_handler/helpers.rs @@ -0,0 +1,37 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::client_handling::websocket::connection_handler::fresh::InitialAuthenticationError; +use nym_gateway_requests::SharedGatewayKey; +use nym_gateway_storage::models::PersistedSharedKeys; +use nym_sphinx::DestinationAddressBytes; +use time::OffsetDateTime; + +pub(crate) struct KeyWithAuthTimestamp { + pub(crate) client_id: i64, + pub(crate) key: SharedGatewayKey, + pub(crate) last_used_authentication: Option, +} + +impl KeyWithAuthTimestamp { + pub(crate) fn try_from_stored( + stored_shared_keys: PersistedSharedKeys, + client: DestinationAddressBytes, + ) -> Result { + let last_used_authentication = stored_shared_keys.last_used_authentication; + let client_id = stored_shared_keys.client_id; + + let key = SharedGatewayKey::try_from(stored_shared_keys).map_err(|source| { + InitialAuthenticationError::MalformedStoredSharedKey { + client_id: client.as_base58_string(), + source, + } + })?; + + Ok(KeyWithAuthTimestamp { + client_id, + key, + last_used_authentication, + }) + } +} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs index f9f6fc584d..38026fa8be 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs @@ -8,16 +8,17 @@ use nym_gateway_requests::ServerResponse; use nym_sphinx::DestinationAddressBytes; use rand::{CryptoRng, Rng}; use std::time::Duration; +use time::OffsetDateTime; use tokio::io::{AsyncRead, AsyncWrite}; use tokio_tungstenite::WebSocketStream; use tracing::{debug, instrument, trace, warn}; -use zeroize::{Zeroize, ZeroizeOnDrop}; pub(crate) use self::authenticated::AuthenticatedHandler; pub(crate) use self::fresh::FreshHandler; pub(crate) mod authenticated; mod fresh; +pub(crate) mod helpers; const WEBSOCKET_HANDSHAKE_TIMEOUT: Duration = Duration::from_millis(1_500); const INITIAL_MESSAGE_TIMEOUT: Duration = Duration::from_millis(10_000); @@ -40,12 +41,13 @@ impl SocketStream { } } -#[derive(Zeroize, ZeroizeOnDrop)] pub(crate) struct ClientDetails { - #[zeroize(skip)] pub(crate) address: DestinationAddressBytes, pub(crate) id: i64, pub(crate) shared_keys: SharedGatewayKey, + // note, this does **NOT ALWAYS** indicate timestamp of when client connected + // it is (for v2 auth) timestamp the client **signed** when it created the request + pub(crate) session_request_timestamp: OffsetDateTime, } impl ClientDetails { @@ -53,11 +55,13 @@ impl ClientDetails { id: i64, address: DestinationAddressBytes, shared_keys: SharedGatewayKey, + session_request_timestamp: OffsetDateTime, ) -> Self { ClientDetails { address, id, shared_keys, + session_request_timestamp, } } } diff --git a/gateway/src/node/client_handling/websocket/mod.rs b/gateway/src/node/client_handling/websocket/mod.rs index 12c252feca..2405870a42 100644 --- a/gateway/src/node/client_handling/websocket/mod.rs +++ b/gateway/src/node/client_handling/websocket/mod.rs @@ -8,4 +8,4 @@ pub(crate) mod connection_handler; pub(crate) mod listener; pub(crate) mod message_receiver; -pub(crate) use common_state::CommonHandlerState; +pub(crate) use common_state::{CommonHandlerState, Config}; diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 9323bb572b..a582a24735 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -249,11 +249,14 @@ impl GatewayTasksBuilder { active_clients_store: ActiveClientsStore, ) -> Result { let shared_state = websocket::CommonHandlerState { + cfg: websocket::Config { + enforce_zk_nym: self.config.gateway.enforce_zk_nyms, + max_auth_request_age: self.config.debug.maximum_auth_request_age, + bandwidth: (&self.config).into(), + }, ecash_verifier: self.ecash_manager().await?, storage: self.storage.clone(), local_identity: Arc::clone(&self.identity_keypair), - only_coconut_credentials: self.config.gateway.enforce_zk_nyms, - bandwidth_cfg: (&self.config).into(), metrics: self.metrics.clone(), metrics_sender: self.metrics_sender.clone(), outbound_mix_sender: self.mix_packet_sender.clone(), diff --git a/nym-node/src/config/gateway_tasks.rs b/nym-node/src/config/gateway_tasks.rs index 1a4c7454ac..97a086177e 100644 --- a/nym-node/src/config/gateway_tasks.rs +++ b/nym-node/src/config/gateway_tasks.rs @@ -50,6 +50,9 @@ pub struct Debug { /// The maximum number of client connections the gateway will keep open at once. pub maximum_open_connections: usize, + /// Defines the maximum age of a signed authentication request before it's deemed too stale to process. + pub maximum_auth_request_age: Duration, + pub stale_messages: StaleMessageDebug, pub client_bandwidth: ClientBandwidthDebug, @@ -58,8 +61,9 @@ pub struct Debug { } impl Debug { - const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; - const DEFAULT_MAXIMUM_OPEN_CONNECTIONS: usize = 8192; + pub const DEFAULT_MAXIMUM_OPEN_CONNECTIONS: usize = 8192; + pub const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; + pub const DEFAULT_MAXIMUM_AUTH_REQUEST_AGE: Duration = Duration::from_secs(30); } impl Default for Debug { @@ -67,6 +71,7 @@ impl Default for Debug { Debug { message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, maximum_open_connections: Self::DEFAULT_MAXIMUM_OPEN_CONNECTIONS, + maximum_auth_request_age: Self::DEFAULT_MAXIMUM_AUTH_REQUEST_AGE, stale_messages: Default::default(), client_bandwidth: Default::default(), zk_nym_tickets: Default::default(), diff --git a/nym-node/src/config/helpers.rs b/nym-node/src/config/helpers.rs index f3380ef223..a56e0442e0 100644 --- a/nym-node/src/config/helpers.rs +++ b/nym-node/src/config/helpers.rs @@ -60,6 +60,7 @@ fn ephemeral_gateway_config(config: &Config) -> nym_gateway::config::Config { .zk_nym_tickets .maximum_time_between_redemption, }, + maximum_auth_request_age: config.gateway_tasks.debug.maximum_auth_request_age, }, ) } From 7335a3dad42be09107c267a365247e5df49aa70a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 6 Mar 2025 11:08:52 +0000 Subject: [PATCH 042/133] fix: gateway protocol negotation for v3/v4 --- .../client_handling/websocket/connection_handler/fresh.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index 4c0eb81f35..bd1f9fd601 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -426,6 +426,11 @@ impl FreshHandler { return Ok(2); } + // a v4 gateway will understand v3 requests (aes256gcm-siv) + if client_protocol_version == 3 { + return Ok(3); + } + // we can't handle clients with higher protocol than ours // (perhaps we could try to negotiate downgrade on our end? sounds like a nice future improvement) if client_protocol_version <= CURRENT_PROTOCOL_VERSION { From 35bf49c48cbf9d4b08e059dea569ff44f2a1923d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 6 Mar 2025 11:24:03 +0000 Subject: [PATCH 043/133] chore: additional logs when attempting to load ecash keys (#5567) --- common/pemstore/Cargo.toml | 1 + common/pemstore/src/lib.rs | 5 +++++ nym-api/src/ecash/dkg/controller/keys.rs | 17 ++++++++++------- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/common/pemstore/Cargo.toml b/common/pemstore/Cargo.toml index f3bc33a6ef..f8d13391dd 100644 --- a/common/pemstore/Cargo.toml +++ b/common/pemstore/Cargo.toml @@ -9,3 +9,4 @@ repository = { workspace = true } [dependencies] pem = { workspace = true } +tracing = { workspace = true } \ No newline at end of file diff --git a/common/pemstore/src/lib.rs b/common/pemstore/src/lib.rs index bd36158229..9f6d3e283b 100644 --- a/common/pemstore/src/lib.rs +++ b/common/pemstore/src/lib.rs @@ -6,6 +6,7 @@ use pem::Pem; use std::fs::File; use std::io::{self, Read, Write}; use std::path::{Path, PathBuf}; +use tracing::debug; pub mod traits; @@ -46,6 +47,10 @@ where T: PemStorableKey, P: AsRef, { + debug!( + "attempting to load key with the following pem type: {}", + T::pem_type() + ); let key_pem = read_pem_file(path)?; if T::pem_type() != key_pem.tag { diff --git a/nym-api/src/ecash/dkg/controller/keys.rs b/nym-api/src/ecash/dkg/controller/keys.rs index bfbaa8c28d..f0038e4869 100644 --- a/nym-api/src/ecash/dkg/controller/keys.rs +++ b/nym-api/src/ecash/dkg/controller/keys.rs @@ -10,7 +10,7 @@ use nym_dkg::bte::keys::KeyPair as DkgKeyPair; use rand::{CryptoRng, RngCore}; use std::path::Path; use thiserror::__private::AsDisplay; -use tracing::warn; +use tracing::{debug, warn}; pub(crate) fn init_bte_keypair( rng: &mut R, @@ -39,17 +39,20 @@ pub(crate) fn load_bte_keypair(config: &config::EcashSigner) -> anyhow::Result anyhow::Result> { + let storage_path = &config.storage_paths.ecash_key_path; + debug!( + "attempting to ecash keypair from {}", + storage_path.display() + ); if !config.storage_paths.ecash_key_path.exists() { + debug!("the provided filepath doesn't exist - the key won't be loaded"); return Ok(None); } - if let Ok(ecash_key) = + let ecash_key = nym_pemstore::load_key::(&config.storage_paths.ecash_key_path) - { - return Ok(Some(ecash_key)); - } - - bail!("ecash key load failure") + .context("failed to load ecash key")?; + Ok(Some(ecash_key)) } // the keys can be considered valid if they were generated for the current dkg epoch From be92ccf0da84d3797feac00b9586ae8e21243046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 6 Mar 2025 11:24:16 +0000 Subject: [PATCH 044/133] bugfix: make sure to correctly decode response content when putting it into error message (#5571) --- Cargo.lock | 9 ++++++--- Cargo.toml | 1 + common/http-api-client/Cargo.toml | 8 +++++++- common/http-api-client/src/lib.rs | 30 ++++++++++++++++++++++++++---- nym-wallet/Cargo.lock | 11 +++++++---- 5 files changed, 47 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5f75819cd5..277779ad57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2291,9 +2291,9 @@ checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] @@ -5767,7 +5767,7 @@ dependencies = [ "serde", "serde_json", "strum 0.26.3", - "subtle 2.6.1", + "subtle 2.5.0", "thiserror 2.0.11", "time", "tokio", @@ -5841,8 +5841,11 @@ name = "nym-http-api-client" version = "0.1.0" dependencies = [ "async-trait", + "bytes", + "encoding_rs", "hickory-resolver", "http 1.2.0", + "mime", "nym-bin-common", "once_cell", "reqwest 0.12.4", diff --git a/Cargo.toml b/Cargo.toml index 14e86aadfc..5452485c28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -241,6 +241,7 @@ doc-comment = "0.3" dotenvy = "0.15.6" ecdsa = "0.16" ed25519-dalek = "2.1" +encoding_rs = "0.8.35" env_logger = "0.11.6" envy = "0.4" etherparse = "0.13.0" diff --git a/common/http-api-client/Cargo.toml b/common/http-api-client/Cargo.toml index 33ff7bc6f8..79a291e827 100644 --- a/common/http-api-client/Cargo.toml +++ b/common/http-api-client/Cargo.toml @@ -21,6 +21,12 @@ serde_json = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } +# used for decoding text responses (they were already implicitly included) +bytes = { workspace = true } +encoding_rs = { workspace = true } +mime = { workspace = true } + + nym-bin-common = { path = "../bin-common" } [target."cfg(not(target_arch = \"wasm32\"))".dependencies] @@ -32,4 +38,4 @@ workspace = true features = ["tokio"] [dev-dependencies] -tokio = { workspace = true, features=["rt", "macros"] } +tokio = { workspace = true, features = ["rt", "macros"] } diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index a8099ce111..4b2938daa4 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -147,13 +147,13 @@ use thiserror::Error; use tracing::{instrument, warn}; use url::Url; +use http::HeaderMap; +pub use reqwest::IntoUrl; #[cfg(not(target_arch = "wasm32"))] use std::net::SocketAddr; #[cfg(not(target_arch = "wasm32"))] use std::sync::Arc; -pub use reqwest::IntoUrl; - mod user_agent; pub use user_agent::UserAgent; @@ -855,6 +855,26 @@ fn sanitize_url, V: AsRef>( url } +fn decode_as_text(bytes: &bytes::Bytes, headers: HeaderMap) -> String { + use encoding_rs::{Encoding, UTF_8}; + use mime::Mime; + + let content_type = headers + .get(http::header::CONTENT_TYPE) + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.parse::().ok()); + + let encoding_name = content_type + .as_ref() + .and_then(|mime| mime.get_param("charset").map(|charset| charset.as_str())) + .unwrap_or("utf-8"); + + let encoding = Encoding::for_label(encoding_name.as_bytes()).unwrap_or(UTF_8); + + let (text, _, _) = encoding.decode(bytes); + text.into_owned() +} + /// Attempt to parse a json object from an HTTP response #[instrument(level = "debug", skip_all)] pub async fn parse_response(res: Response, allow_empty: bool) -> Result> @@ -870,6 +890,8 @@ where return Err(HttpClientError::EmptyResponse { status }); } } + let headers = res.headers().clone(); + tracing::trace!("headers: {:?}", headers); if res.status().is_success() { // internally reqwest is first retrieving bytes and then performing parsing via serde_json @@ -878,10 +900,10 @@ where match serde_json::from_slice(&full) { Ok(data) => Ok(data), Err(err) => { - let text = String::from_utf8_lossy(&full); + let content = decode_as_text(&full, headers); Err(HttpClientError::ResponseDecodeFailure { source: err, - content: text.into_owned(), + content, }) } } diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 7fb3e6a067..969932de63 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -465,9 +465,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" dependencies = [ "serde", ] @@ -1487,9 +1487,9 @@ checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" [[package]] name = "encoding_rs" -version = "0.8.32" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] @@ -3454,8 +3454,11 @@ name = "nym-http-api-client" version = "0.1.0" dependencies = [ "async-trait", + "bytes", + "encoding_rs", "hickory-resolver", "http 1.1.0", + "mime", "nym-bin-common", "once_cell", "reqwest 0.12.4", From c33e4c083612ae216407932bde5333799250a0d9 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Thu, 6 Mar 2025 15:03:43 +0100 Subject: [PATCH 045/133] bumping versions dorina patched --- Cargo.lock | 13 +++++++------ clients/native/Cargo.toml | 2 +- clients/socks5/Cargo.toml | 2 +- explorer-api/Cargo.toml | 2 +- service-providers/network-requester/Cargo.toml | 2 +- tools/nym-cli/Cargo.toml | 2 +- tools/nymvisor/Cargo.toml | 2 +- 7 files changed, 13 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 277779ad57..b018fc913f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2420,7 +2420,7 @@ dependencies = [ [[package]] name = "explorer-api" -version = "1.1.47" +version = "1.1.48" dependencies = [ "chrono", "clap", @@ -4996,7 +4996,7 @@ dependencies = [ [[package]] name = "nym-cli" -version = "1.1.49" +version = "1.1.50" dependencies = [ "anyhow", "base64 0.22.1", @@ -5079,7 +5079,7 @@ dependencies = [ [[package]] name = "nym-client" -version = "1.1.49" +version = "1.1.50" dependencies = [ "bs58", "clap", @@ -6123,7 +6123,7 @@ dependencies = [ [[package]] name = "nym-network-requester" -version = "1.1.50" +version = "1.1.51" dependencies = [ "addr", "anyhow", @@ -6455,6 +6455,7 @@ name = "nym-pemstore" version = "0.3.0" dependencies = [ "pem", + "tracing", ] [[package]] @@ -6551,7 +6552,7 @@ dependencies = [ [[package]] name = "nym-socks5-client" -version = "1.1.49" +version = "1.1.50" dependencies = [ "bs58", "clap", @@ -7156,7 +7157,7 @@ dependencies = [ [[package]] name = "nymvisor" -version = "0.1.14" +version = "0.1.15" dependencies = [ "anyhow", "bytes", diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index a3dfdc0661..9663b41d78 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-client" -version = "1.1.49" +version = "1.1.50" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] description = "Implementation of the Nym Client" edition = "2021" diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index 934c0b6db9..9c4b1522ca 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-socks5-client" -version = "1.1.49" +version = "1.1.50" authors = ["Dave Hrycyszyn "] description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address" edition = "2021" diff --git a/explorer-api/Cargo.toml b/explorer-api/Cargo.toml index 585dfd47bf..6bff50d87a 100644 --- a/explorer-api/Cargo.toml +++ b/explorer-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "explorer-api" -version = "1.1.47" +version = "1.1.48" edition = "2021" license.workspace = true diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index bbc7080819..207f3960c2 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-network-requester" license = "GPL-3.0" -version = "1.1.50" +version = "1.1.51" authors.workspace = true edition.workspace = true rust-version = "1.70" diff --git a/tools/nym-cli/Cargo.toml b/tools/nym-cli/Cargo.toml index 9163feeb0b..a9b24b4ced 100644 --- a/tools/nym-cli/Cargo.toml +++ b/tools/nym-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-cli" -version = "1.1.49" +version = "1.1.50" authors.workspace = true edition = "2021" license.workspace = true diff --git a/tools/nymvisor/Cargo.toml b/tools/nymvisor/Cargo.toml index 57b4de306d..15de5e1610 100644 --- a/tools/nymvisor/Cargo.toml +++ b/tools/nymvisor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nymvisor" -version = "0.1.14" +version = "0.1.15" authors.workspace = true repository.workspace = true homepage.workspace = true From f6b30d0db681b9583e87b2547f3da60797d8ccfb Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Thu, 6 Mar 2025 15:06:24 +0100 Subject: [PATCH 046/133] update changelog for patched-dorina --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50bad8f2cf..785601d969 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ## [Unreleased] +## [2025.4-dorina-patched] (2025-03-06) + +- bugfix: make sure to correctly decode response content when putting it into error message ([#5571]) +- Tweak surb management to be more conservative ([#5570]) +- Deserialize v5 authenticator requests ([#5568]) +- chore: additional logs when attempting to load ecash keys ([#5567]) +- add full response body to error message upon decoding failure ([#5566]) +- hotfix: ensure we bail on merkle leaves insertion upon missing data ([#5565]) +- feature: v2 authentication request (#5537) ([#5563]) +- Create authenticator v5 request/response types ([#5561]) + +[#5571]: https://github.com/nymtech/nym/pull/5571 +[#5570]: https://github.com/nymtech/nym/pull/5570 +[#5568]: https://github.com/nymtech/nym/pull/5568 +[#5567]: https://github.com/nymtech/nym/pull/5567 +[#5566]: https://github.com/nymtech/nym/pull/5566 +[#5565]: https://github.com/nymtech/nym/pull/5565 +[#5563]: https://github.com/nymtech/nym/pull/5563 +[#5561]: https://github.com/nymtech/nym/pull/5561 + ## [2025.4-dorina] (2025-03-04) - fixed sphinx version metrics registration ([#5546]) From b42e5b063e0d5a00bb16fb05ddec8e294707c402 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Thu, 6 Mar 2025 15:45:02 +0100 Subject: [PATCH 047/133] bump api version --- Cargo.lock | 2 +- nym-api/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b018fc913f..ef32d6684a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4747,7 +4747,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "nym-api" -version = "1.1.52" +version = "1.1.53" dependencies = [ "anyhow", "async-trait", diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 239b483bf2..aa6b966b7f 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-api" license = "GPL-3.0" -version = "1.1.52" +version = "1.1.53" authors.workspace = true edition = "2021" rust-version.workspace = true From 3cb3ebd79b245ca0820c98cbc9d3899cf73fa188 Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Thu, 6 Mar 2025 14:56:40 +0000 Subject: [PATCH 048/133] [DOCs/operators]: Release ntoes for patched version (#5573) --- .../docs/pages/operators/changelog.mdx | 39 +++++++++++++++++++ .../pages/operators/nodes/maintenance.mdx | 13 ++++++- .../pages/operators/nodes/nym-node/setup.mdx | 10 ++--- 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/documentation/docs/pages/operators/changelog.mdx b/documentation/docs/pages/operators/changelog.mdx index 85a4d21875..cec2fbd20f 100644 --- a/documentation/docs/pages/operators/changelog.mdx +++ b/documentation/docs/pages/operators/changelog.mdx @@ -47,6 +47,45 @@ This page displays a full list of all the changes during our release cycle from +## `v2025.4-dorina-patched` + +Patched version of `dorina` with a few fixes and tweaks to the release. We would like to ask `nym-node` operators to upgrade to this version as quickly as possible to implement the fixes across the network and improve general quality before NymVPN launch. + +- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.4-dorina-patched) +- [`nym-node`](nodes/nym-node.mdx) version `1.6.1` + +```shell +nym-node +Binary Name: nym-node +Build Timestamp: 2025-03-06T14:28:21.408539599Z +Build Version: 1.6.1 +Commit SHA: 1fb2ebad7ad1bad455d5b896ea14204211727417 +Commit Date: 2025-03-06T15:26:18.000000000+01:00 +Commit Branch: HEAD +rustc Version: 1.85.0 +rustc Channel: stable +cargo Profile: release +``` + +- Bugfix: make sure to correctly decode response content when putting it into error message ([#5571]) +- Tweak surb management to be more conservative ([#5570]) +- Deserialize v5 authenticator requests ([#5568]) +- Chore: additional logs when attempting to load ecash keys ([#5567]) +- Add full response body to error message upon decoding failure ([#5566]) +- Hotfix: ensure we bail on merkle leaves insertion upon missing data ([#5565]) +- Feature: v2 authentication request (#5537) ([#5563]) +- Create authenticator v5 request/response types ([#5561]) + +[#5571]: https://github.com/nymtech/nym/pull/5571 +[#5570]: https://github.com/nymtech/nym/pull/5570 +[#5568]: https://github.com/nymtech/nym/pull/5568 +[#5567]: https://github.com/nymtech/nym/pull/5567 +[#5566]: https://github.com/nymtech/nym/pull/5566 +[#5565]: https://github.com/nymtech/nym/pull/5565 +[#5563]: https://github.com/nymtech/nym/pull/5563 +[#5561]: https://github.com/nymtech/nym/pull/5561 + + ## `v2025.4-dorina` - [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.4-dorina) - [`nym-node`](nodes/nym-node.mdx) version `1.6.0` diff --git a/documentation/docs/pages/operators/nodes/maintenance.mdx b/documentation/docs/pages/operators/nodes/maintenance.mdx index 5819fde852..a825b3ab32 100644 --- a/documentation/docs/pages/operators/nodes/maintenance.mdx +++ b/documentation/docs/pages/operators/nodes/maintenance.mdx @@ -128,10 +128,15 @@ mkdir -pv ###### 2. Backup `clients.sqlite` database + - Install `sqlite3` ```sh apt install sqlite3 ``` +- **Stop your node** +```sh +service nym-node stop +``` - Open sqlite CLI shell inside `clients.sqlite` database ```sh sqlite3 ~/.nym/nym-nodes/default-nym-node/data/clients.sqlite @@ -151,10 +156,14 @@ sqlite3 ~/.nym/nym-nodes/default-nym-node/data/clients.sqlite scp -r @:~/.nym/nym-nodes/ ``` -###### 4. Verify the success of the backup +###### 4. Verify the success of the backup & start your node The `scp` command should print logs, an operator can see directly whether it was successful or if it encountered any error. However, double check that all your needed configuration is in the backup target directory. +- Start your node +```sh +service nym-node start && journalctl -u nym-node -f +``` Now you have everything needed to restore your `nym-node` on another server. If you are in a need of doing so, follow the steps in [*Restoring a node*](#restoring-a-node) chapter below. @@ -360,7 +369,7 @@ mkdir ~/.nym/nym-nodes ``` ###### 2. Backup `clients.sqlite` database -- Stop your node +- **Stop your node** ```sh service nym-node stop ``` diff --git a/documentation/docs/pages/operators/nodes/nym-node/setup.mdx b/documentation/docs/pages/operators/nodes/nym-node/setup.mdx index 8290f14411..33e3bcc9da 100644 --- a/documentation/docs/pages/operators/nodes/nym-node/setup.mdx +++ b/documentation/docs/pages/operators/nodes/nym-node/setup.mdx @@ -20,12 +20,12 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](. ```sh nym-node Binary Name: nym-node -Build Timestamp: 2025-02-13T11:49:34.670488195Z -Build Version: 1.5.0 -Commit SHA: a3e19b4563843055b305ea9a397eb1ad84b5c378 -Commit Date: 2025-02-10T18:14:47.000000000+01:00 +Build Timestamp: 2025-03-06T14:28:21.408539599Z +Build Version: 1.6.1 +Commit SHA: 1fb2ebad7ad1bad455d5b896ea14204211727417 +Commit Date: 2025-03-06T15:26:18.000000000+01:00 Commit Branch: HEAD -rustc Version: 1.84.1 +rustc Version: 1.85.0 rustc Channel: stable cargo Profile: release ``` From d45d1eb3135f76ebe3f7d6f6d0ecd241a8a84a3f Mon Sep 17 00:00:00 2001 From: mfahampshire Date: Thu, 6 Mar 2025 17:37:19 +0100 Subject: [PATCH 049/133] change hardcoded file to tempdir --- Cargo.lock | 1 + sdk/rust/nym-sdk/Cargo.toml | 1 + sdk/rust/nym-sdk/examples/surb_reply.rs | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 7fef1012b5..fb3513974c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6536,6 +6536,7 @@ dependencies = [ "reqwest 0.12.4", "serde", "tap", + "tempfile", "thiserror 2.0.12", "tokio", "tokio-stream", diff --git a/sdk/rust/nym-sdk/Cargo.toml b/sdk/rust/nym-sdk/Cargo.toml index f30e244e87..c95118cf3c 100644 --- a/sdk/rust/nym-sdk/Cargo.toml +++ b/sdk/rust/nym-sdk/Cargo.toml @@ -58,6 +58,7 @@ tap = { workspace = true } thiserror = { workspace = true } url = { workspace = true } toml = { workspace = true } +tempfile = { workspace = true } # tcpproxy dependencies clap = { workspace = true, features = ["derive"] } diff --git a/sdk/rust/nym-sdk/examples/surb_reply.rs b/sdk/rust/nym-sdk/examples/surb_reply.rs index 926cac0314..649c8aaf7e 100644 --- a/sdk/rust/nym-sdk/examples/surb_reply.rs +++ b/sdk/rust/nym-sdk/examples/surb_reply.rs @@ -3,13 +3,14 @@ use nym_sdk::mixnet::{ StoragePaths, }; use std::path::PathBuf; +use tempfile::TempDir; #[tokio::main] async fn main() { nym_bin_common::logging::setup_logging(); // Specify some config options - let config_dir = PathBuf::from("/tmp/surb-example"); + let config_dir: PathBuf = TempDir::new().unwrap().path().to_path_buf(); let storage_paths = StoragePaths::new_from_dir(&config_dir).unwrap(); // Create the client with a storage backend, and enable it by giving it some paths. If keys From 430c33eb04656a931a38f0e44eea19ae39aaf3f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Thu, 6 Mar 2025 15:26:18 +0100 Subject: [PATCH 050/133] Set DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE to 50 --- common/client-core/config-types/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/client-core/config-types/src/lib.rs b/common/client-core/config-types/src/lib.rs index 82e93f9b09..5930245e51 100644 --- a/common/client-core/config-types/src/lib.rs +++ b/common/client-core/config-types/src/lib.rs @@ -50,7 +50,7 @@ const DEFAULT_MINIMUM_REPLY_SURB_THRESHOLD_BUFFER: usize = 0; // define how much to request at once // clients/client-core/src/client/replies/reply_controller.rs const DEFAULT_MINIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 10; -const DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 30; +const DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 50; const DEFAULT_MAXIMUM_ALLOWED_SURB_REQUEST_SIZE: u32 = 500; From 6bd31b9521de7baf04dfebb9e427eee61df46bae Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Thu, 6 Mar 2025 18:08:58 +0100 Subject: [PATCH 051/133] bump nym-node version --- Cargo.lock | 2 +- nym-node/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef32d6684a..fe70c9ce8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6174,7 +6174,7 @@ dependencies = [ [[package]] name = "nym-node" -version = "1.6.1" +version = "1.6.2" dependencies = [ "anyhow", "arc-swap", diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 028db3681a..0cf816ba5b 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node" -version = "1.6.1" +version = "1.6.2" authors.workspace = true repository.workspace = true homepage.workspace = true From 3880971e5742832afd3d897829a34892257674e7 Mon Sep 17 00:00:00 2001 From: Yana Date: Thu, 6 Mar 2025 21:34:22 +0200 Subject: [PATCH 052/133] delete double memo field in send modal --- nym-wallet/src/components/Send/SendInputModal.tsx | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/nym-wallet/src/components/Send/SendInputModal.tsx b/nym-wallet/src/components/Send/SendInputModal.tsx index c5bd398e85..7ca8bc3623 100644 --- a/nym-wallet/src/components/Send/SendInputModal.tsx +++ b/nym-wallet/src/components/Send/SendInputModal.tsx @@ -157,20 +157,6 @@ export const SendInputModal = ({ initialValue={userFees?.amount} fullWidth /> - onMemoChange(e.target.value)} - value={memo} - error={!memoIsValid} - helperText={ - !memoIsValid - ? ' The text is invalid, only alphanumeric characters and white spaces are allowed' - : undefined - } - InputLabelProps={{ shrink: true }} - fullWidth - /> )} From 01c052e9a455d1396104b158ef869242d160fc5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 6 Mar 2025 20:13:16 +0000 Subject: [PATCH 053/133] use legacy crypto for constructing SURB headers (#5579) --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- common/nymsphinx/anonymous-replies/src/reply_surb.rs | 5 +++-- nym-wallet/Cargo.lock | 1 + 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe70c9ce8c..1d41821313 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9414,9 +9414,9 @@ dependencies = [ [[package]] name = "sphinx-packet" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535f2c430778bf59c22249fcc1ed6d384129eb2f0f694706015d636c688f9ac6" +checksum = "c23047e0cf36ff6904603f499fd13153425cdf5ba47bfbaedbc999da0bd92f4e" dependencies = [ "aes", "arrayref", diff --git a/Cargo.toml b/Cargo.toml index 5452485c28..80d483f003 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -322,7 +322,7 @@ serde_with = "3.9.0" serde_yaml = "0.9.25" sha2 = "0.10.8" si-scale = "0.2.3" -sphinx-packet = "0.3.1" +sphinx-packet = "=0.3.2" sqlx = "0.7.4" strum = "0.26" strum_macros = "0.26" diff --git a/common/nymsphinx/anonymous-replies/src/reply_surb.rs b/common/nymsphinx/anonymous-replies/src/reply_surb.rs index 90bd892d78..a6729e7e5d 100644 --- a/common/nymsphinx/anonymous-replies/src/reply_surb.rs +++ b/common/nymsphinx/anonymous-replies/src/reply_surb.rs @@ -7,7 +7,7 @@ use nym_sphinx_addressing::clients::Recipient; use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, MAX_NODE_ADDRESS_UNPADDED_LEN}; use nym_sphinx_params::packet_sizes::PacketSize; use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm}; -use nym_sphinx_types::{NymPacket, SURBMaterial, SphinxError, SURB}; +use nym_sphinx_types::{NymPacket, SURBMaterial, SphinxError, SURB, UPDATED_LEGACY_VERSION}; use nym_topology::{NymRouteProvider, NymTopologyError}; use rand::{CryptoRng, RngCore}; use serde::de::{Error as SerdeError, Visitor}; @@ -101,7 +101,8 @@ impl ReplySurb { let delays = nym_sphinx_routing::generate_hop_delays(average_delay, route.len()); let destination = recipient.as_sphinx_destination(); - let surb_material = SURBMaterial::new(route, delays, destination); + let surb_material = + SURBMaterial::new(route, delays, destination).with_version(UPDATED_LEGACY_VERSION); // this can't fail as we know we have a valid route to gateway and have correct number of delays Ok(ReplySurb { diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 969932de63..e92a03c300 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -3548,6 +3548,7 @@ name = "nym-pemstore" version = "0.3.0" dependencies = [ "pem", + "tracing", ] [[package]] From 247ebb7c4339de0a298a7fcb2574122a8306c3b8 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Thu, 6 Mar 2025 21:26:16 +0100 Subject: [PATCH 054/133] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 785601d969..6c4e17091f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ## [2025.4-dorina-patched] (2025-03-06) +- use legacy crypto for constructing SURB headers ([#5579]) - bugfix: make sure to correctly decode response content when putting it into error message ([#5571]) - Tweak surb management to be more conservative ([#5570]) - Deserialize v5 authenticator requests ([#5568]) @@ -15,6 +16,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - feature: v2 authentication request (#5537) ([#5563]) - Create authenticator v5 request/response types ([#5561]) +[#5579]: https://github.com/nymtech/nym/pull/5579 [#5571]: https://github.com/nymtech/nym/pull/5571 [#5570]: https://github.com/nymtech/nym/pull/5570 [#5568]: https://github.com/nymtech/nym/pull/5568 From 81bddb5f6d552719d2f06f53c61fab1b65b0002b Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Fri, 7 Mar 2025 09:46:08 +0000 Subject: [PATCH 055/133] [DOCs/operators]: Second patch version changelog (#5580) --- documentation/docs/pages/operators/changelog.mdx | 10 ++++++---- .../docs/pages/operators/nodes/nym-node/setup.mdx | 8 ++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/documentation/docs/pages/operators/changelog.mdx b/documentation/docs/pages/operators/changelog.mdx index cec2fbd20f..4be180e967 100644 --- a/documentation/docs/pages/operators/changelog.mdx +++ b/documentation/docs/pages/operators/changelog.mdx @@ -57,16 +57,17 @@ Patched version of `dorina` with a few fixes and tweaks to the release. We would ```shell nym-node Binary Name: nym-node -Build Timestamp: 2025-03-06T14:28:21.408539599Z -Build Version: 1.6.1 -Commit SHA: 1fb2ebad7ad1bad455d5b896ea14204211727417 -Commit Date: 2025-03-06T15:26:18.000000000+01:00 +Build Timestamp: 2025-03-06T20:32:36.922212778Z +Build Version: 1.6.2 +Commit SHA: 247ebb7c4339de0a298a7fcb2574122a8306c3b8 +Commit Date: 2025-03-06T21:26:16.000000000+01:00 Commit Branch: HEAD rustc Version: 1.85.0 rustc Channel: stable cargo Profile: release ``` +- Use legacy crypto for constructing SURB headers ([#5579]) - Bugfix: make sure to correctly decode response content when putting it into error message ([#5571]) - Tweak surb management to be more conservative ([#5570]) - Deserialize v5 authenticator requests ([#5568]) @@ -76,6 +77,7 @@ cargo Profile: release - Feature: v2 authentication request (#5537) ([#5563]) - Create authenticator v5 request/response types ([#5561]) +[#5579]: https://github.com/nymtech/nym/pull/5579 [#5571]: https://github.com/nymtech/nym/pull/5571 [#5570]: https://github.com/nymtech/nym/pull/5570 [#5568]: https://github.com/nymtech/nym/pull/5568 diff --git a/documentation/docs/pages/operators/nodes/nym-node/setup.mdx b/documentation/docs/pages/operators/nodes/nym-node/setup.mdx index 33e3bcc9da..6d4a09b4b6 100644 --- a/documentation/docs/pages/operators/nodes/nym-node/setup.mdx +++ b/documentation/docs/pages/operators/nodes/nym-node/setup.mdx @@ -20,10 +20,10 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](. ```sh nym-node Binary Name: nym-node -Build Timestamp: 2025-03-06T14:28:21.408539599Z -Build Version: 1.6.1 -Commit SHA: 1fb2ebad7ad1bad455d5b896ea14204211727417 -Commit Date: 2025-03-06T15:26:18.000000000+01:00 +Build Timestamp: 2025-03-06T20:32:36.922212778Z +Build Version: 1.6.2 +Commit SHA: 247ebb7c4339de0a298a7fcb2574122a8306c3b8 +Commit Date: 2025-03-06T21:26:16.000000000+01:00 Commit Branch: HEAD rustc Version: 1.85.0 rustc Channel: stable From 143ede268d43eff2765e71c9a2a4941bb630831f Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Fri, 7 Mar 2025 09:56:45 +0000 Subject: [PATCH 056/133] [DOCs/operators]: Fix typo (#5581) --- documentation/docs/pages/operators/changelog.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/pages/operators/changelog.mdx b/documentation/docs/pages/operators/changelog.mdx index 4be180e967..61304c6904 100644 --- a/documentation/docs/pages/operators/changelog.mdx +++ b/documentation/docs/pages/operators/changelog.mdx @@ -52,7 +52,7 @@ This page displays a full list of all the changes during our release cycle from Patched version of `dorina` with a few fixes and tweaks to the release. We would like to ask `nym-node` operators to upgrade to this version as quickly as possible to implement the fixes across the network and improve general quality before NymVPN launch. - [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.4-dorina-patched) -- [`nym-node`](nodes/nym-node.mdx) version `1.6.1` +- [`nym-node`](nodes/nym-node.mdx) version `1.6.2` ```shell nym-node From 9c4243914e68b58b7288c75494e9a6774e8b3601 Mon Sep 17 00:00:00 2001 From: mfahampshire Date: Fri, 7 Mar 2025 09:57:52 +0000 Subject: [PATCH 057/133] Max/ns api docs (#5544) * first pass * cleanup * added qu * add readme * more verbose err * reword explainer @ top * rename private-key.public to public-key * move instructions to own file + add _meta.json files * first pass probe * remove unnecessary doubled notice to developers * added extra debug log to version() * include PR suggestions * remove commented out function --- documentation/docs/.gitignore | 4 +- .../docs/pages/apis/cosmos-sdk-nyx/_meta.json | 5 + .../docs/pages/apis/explorer-api/_meta.json | 5 + documentation/docs/pages/apis/ns-api.mdx | 7 +- .../docs/pages/apis/ns-api/_meta.json | 5 + .../docs/pages/apis/ns-api/mainnet.mdx | 2 + .../pages/apis/ns-api/ns-api-run-deploy.mdx | 336 ++++++++++++++++++ .../docs/pages/apis/nym-api/_meta.json | 3 + nym-node-status-api/README.md | 6 + .../src/cli/generate_keypair.rs | 3 +- .../nym-node-status-agent/src/probe.rs | 59 ++- .../nym-node-status-api/src/monitor/mod.rs | 11 +- 12 files changed, 427 insertions(+), 19 deletions(-) create mode 100644 documentation/docs/pages/apis/cosmos-sdk-nyx/_meta.json create mode 100644 documentation/docs/pages/apis/explorer-api/_meta.json create mode 100644 documentation/docs/pages/apis/ns-api/_meta.json create mode 100644 documentation/docs/pages/apis/ns-api/ns-api-run-deploy.mdx create mode 100644 documentation/docs/pages/apis/nym-api/_meta.json create mode 100644 nym-node-status-api/README.md diff --git a/documentation/docs/.gitignore b/documentation/docs/.gitignore index 515d7fce8d..60d8350455 100644 --- a/documentation/docs/.gitignore +++ b/documentation/docs/.gitignore @@ -7,4 +7,6 @@ package-lock.json # local env files .env*.local -.env \ No newline at end of file +.env + +scratch.md diff --git a/documentation/docs/pages/apis/cosmos-sdk-nyx/_meta.json b/documentation/docs/pages/apis/cosmos-sdk-nyx/_meta.json new file mode 100644 index 0000000000..8e5aad88d7 --- /dev/null +++ b/documentation/docs/pages/apis/cosmos-sdk-nyx/_meta.json @@ -0,0 +1,5 @@ + +{ + "mainnet":"Mainnet Endpoints", + "sandbox":"Sandbox Endpoints" +} diff --git a/documentation/docs/pages/apis/explorer-api/_meta.json b/documentation/docs/pages/apis/explorer-api/_meta.json new file mode 100644 index 0000000000..8e5aad88d7 --- /dev/null +++ b/documentation/docs/pages/apis/explorer-api/_meta.json @@ -0,0 +1,5 @@ + +{ + "mainnet":"Mainnet Endpoints", + "sandbox":"Sandbox Endpoints" +} diff --git a/documentation/docs/pages/apis/ns-api.mdx b/documentation/docs/pages/apis/ns-api.mdx index 85d2ea4566..9e14e1509c 100644 --- a/documentation/docs/pages/apis/ns-api.mdx +++ b/documentation/docs/pages/apis/ns-api.mdx @@ -1,11 +1,8 @@ import { Callout } from 'nextra/components' # Node Status API - -The Node Status API contains information about the network, its topology, and the routing scores of all nodes within it. It offers broadly similar information to the experimental [Habourmaster frontend](https://harbourmaster.nymtech.net/) but is stable where the Harbourmaster is subject to sudden changes as we modify and experiment with how we scrape and present data about the Mixnet infrastructure. +The Node Status API serves information about individual `nym-nodes` in the Mixnet, such as which role they are operating in, statistics about them, services such as Network Requesters, as well as summaries of the state of the Mixnet. -People building applications or dashboards which requires information about nodes, their uptime, and their delegations should use this instead of Habourmaster. +We recommend that developers building applications such as explorers or analytics interfaces about the Mixnet run their own instance of the API, in order to promote a robust network of downstream services, and spread the load of API calls amongst as many endpoints as possible. - -The code for this service can be found [in our monorepo](https://github.com/nymtech/nym/tree/develop/nym-node-status-api). In the future we will encourage developers to run their own instance of this API in order to distribute endpoints and query load. diff --git a/documentation/docs/pages/apis/ns-api/_meta.json b/documentation/docs/pages/apis/ns-api/_meta.json new file mode 100644 index 0000000000..7d443d9f6d --- /dev/null +++ b/documentation/docs/pages/apis/ns-api/_meta.json @@ -0,0 +1,5 @@ +{ + "ns-api-run-deploy":"Run Instance", + "mainnet":"Mainnet Endpoints", + "sandbox":"Sandbox Endpoints" +} diff --git a/documentation/docs/pages/apis/ns-api/mainnet.mdx b/documentation/docs/pages/apis/ns-api/mainnet.mdx index a517ba0edd..00a6523ae7 100644 --- a/documentation/docs/pages/apis/ns-api/mainnet.mdx +++ b/documentation/docs/pages/apis/ns-api/mainnet.mdx @@ -1,9 +1,11 @@ import { RedocStandalone } from 'redoc'; +import { Callout } from 'nextra/components' The information below is generated with [Redoc](https://redocly.com/docs/redoc) consuming the OpenAPI spec found at [https://mainnet-node-status-api.nymtech.cc/api-docs/openapi.json](https://mainnet-node-status-api.nymtech.cc/api-docs/openapi.json) which is also used to generate the Swagger docs deployed at [https://mainnet-node-status-api.nymtech.cc/swagger/](https://mainnet-node-status-api.nymtech.cc/swagger/).

+ +We invite developers to roll their own UI for their Node Status API instance. + + +## Docker Images +We will ship Docker images for both the `agent` and `api` in the future. There are Docker images for both in root of each corresponding crate ([`agent`](https://github.com/nymtech/nym/blob/09ea406c02e9a3beebc062f525e4ea1b4222dcbb/nym-node-status-api/nym-node-status-agent/Dockerfile), [`api`](https://github.com/nymtech/nym/blob/develop/nym-node-status-api/nym-node-status-api/Dockerfile)) which are used internally, which could be a starting point for developers to dockerize their instance for the moment. + +## Build +### Prerequisites +- Rust +- SQLite +- Get an `ipinfo` key following instructions [here](https://github.com/ipinfo/rust?tab=readme-ov-file#getting-started). + +### Compilation +```shell +cargo build --release --package nym-node-status-api --package nym-node-status-agent --package nym-node-status-client +``` + +## Run +Since the Node Status API depends on both flags and environmental variables, it might be easier to run the binary via a script like the one below - this this script essentially just `source`-s the defined `.env` file after exporting certain binary-specific variables, and then runs the binary. You can find the `.env` files [here](https://github.com/nymtech/nym/tree/master/envs). + + + All CLI flags are configurable as environmental variables and vice versa, so take the following scripts / setups as guides that you can change however best suits your setup. You can see all definitions [here](https://github.com/nymtech/nym/blob/develop/nym-node-status-api/nym-node-status-api/src/cli/mod.rs#L14). + + +```bash +#!/bin/bash + +set -e + +export ENVIRONMENT=${ENVIRONMENT:-"mainnet"} # see nym/envs/ for all possible environments +export NYM_API_CLIENT_TIMEOUT=60 +export NODE_STATUS_API_TESTRUN_REFRESH_INTERVAL=120 +export IPINFO_API_TOKEN= + +monorepo_root= +set -a +source "${monorepo_root}/envs/${ENVIRONMENT}.env" +echo ${monorepo_root}/envs/${ENVIRONMENT}.env +set +a +export RUST_LOG=${RUST_LOG:-debug} # debug is useful to check everything is working initially, but quite verbose + +echo "Verifying environment variables were properly sourced:" +echo "RUST_LOG=${RUST_LOG}" +echo "BECH32_PREFIX=${BECH32_PREFIX}" +echo "NETWORK_NAME=${NETWORK_NAME}" + +nym-node-status-api -- --ipinfo-api-token $IPINFO_API_TOKEN +``` + +### Functionality Without Gateway Probe +Data will be restricted to information that doesn't involve Probe results; `routing` and `config` scores will be `0` and `last_probe` results `null`, as you can see in this snipped output of the `gateways` endpoint: + + +```shell +{ + "gateway_identity_key": "23A7CSaBSA2L67PWuFTPXUnYrCdyVcB7ATYsjUsfdftb", + "bonded": true, + "performance": 99, + "self_described": { + "authenticator": { + "address": "6Gdtw13Fa46AvkqkHELZZCMKWASDodoJeK9APRNpjjdj.7ji8DDkpjA2AdgwK7wbZm8yi4xZGogGJeypBQt4hAw3P@23A7CSaBSA2L67PWuFTPXUnYrCdyVcB7ATYsjUsfdftb" + }, + "auxiliary_details": { + "accepted_operator_terms_and_conditions": true, + "announce_ports": { + "mix_port": null, + "verloc_port": null + }, + "location": null + }, + "build_information": { + "binary_name": "nym-node", + "build_timestamp": "2025-02-13T11:49:34.670488195Z", + "build_version": "1.5.0", + "cargo_profile": "release", + "cargo_triple": "x86_64-unknown-linux-gnu", + "commit_branch": "HEAD", + "commit_sha": "a3e19b4563843055b305ea9a397eb1ad84b5c378", + "commit_timestamp": "2025-02-10T18:14:47.000000000+01:00", + "rustc_channel": "stable", + "rustc_version": "1.84.1" + }, + "declared_role": { + "entry": true, + "exit_ipr": true, + "exit_nr": true, + "mixnode": false + }, + "host_information": { + "hostname": "bwng1.bwnym.xyz", + "ip_address": [ + "95.164.2.86" + ], + "keys": { + "ed25519": "23A7CSaBSA2L67PWuFTPXUnYrCdyVcB7ATYsjUsfdftb", + "x25519": "H6pFjqtdSVxkxEQ3wFnuSoobDAUqHx1bYMVJzPZdRByn", + "x25519_noise": null + } + }, + "ip_packet_router": { + "address": "7ms2D2uYiTuhX6MKeVL5rz5usgehEoxAAovwYm9nJyBF.3siMjk3wTU7ykaXLNi9c7LpX8yonYKPCA4BQoMwhsfTV@23A7CSaBSA2L67PWuFTPXUnYrCdyVcB7ATYsjUsfdftb" + }, + "last_polled": "2025-03-03 09:48:03.635274187 +00:00:00", + "mixnet_websockets": { + "ws_port": 9000, + "wss_port": 9001 + }, + "network_requester": { + "address": "HuNL1pFprNSKW6jdqppibXP5KNKCNJxDh7ivpYcoULN9.C62NahRTUf6kqpNtDVHXoVriQr6yyaU5LtxdgpbsGrtA@23A7CSaBSA2L67PWuFTPXUnYrCdyVcB7ATYsjUsfdftb", + "uses_exit_policy": true + }, + "wireguard": { + "port": 51822, + "public_key": "6o8x9GitFjcrkjrJnivWaQCPnxXykQPYLneNr2FEB8Vq" + } + }, + "explorer_pretty_bond": { + "identity_key": "23A7CSaBSA2L67PWuFTPXUnYrCdyVcB7ATYsjUsfdftb", + "location": { + "latitude": 52.5083, + "longitude": 5.475, + "two_letter_iso_country_code": "NL" + }, + "owner": "n1cp5gq0apat6c7qmenqp5zjprn2vwvc7jl29j8r", + "pledge_amount": { + "amount": "100000000", + "denom": "unym" + } + }, + "description": { + "moniker": "bwn_g1", + "website": "https://bwnym.xyz", + "security_contact": "bwnym@proton.me", + "details": "This gateway is part of the NYM project, which is dedicated to create outstanding privacy software that is legally compliant without sacrificing integrity or having any backdoors." + }, + "last_probe_result": null, + "last_probe_log": null, + "last_testrun_utc": null, + "last_updated_utc": "2025-03-03T10:45:48+00:00", + "routing_score": 0.0, + "config_score": 0 + }, +``` + + +If you have already run the API before, make sure to add the following to your script `--database-url "sqlite://node-status-api.sqlite?mode=rwc` so it will continue using the same DB. + +### Functionality with Gateway Probe +If you want to enable Gateway node probes and have the NS API store that data, you need to periodically run the `nym-node-status-agent`, authenticated with your `nym-node-status-api` instance. Authentication is to make sure that only the `-agent` you are operating (or others you trust) are submitting data to your API instance. + +#### Compile Gateway Probe +The `nym-node-status-agent` is a thin wrapper around the Gateway Probe binary which currently is in the NymVPN repo. `git checkout` to the most recent [release](https://github.com/nymtech/nym-vpn-client/releases), and compile the probe by following the [readme instructions](https://github.com/nymtech/nym-vpn-client/tree/develop/nym-vpn-core/crates/nym-gateway-probe). You will point the `-agent` at this binary when doing Probe testruns. + +#### Generate Keypair +```shell +nym-node-status-agent generate-keypair --path / +# e.g. +# nym-node-status-agent generate-keypair --path ~/.ssh/ns-agent-key +``` + +You will then want to export the generated `public-key` so its accessible to the `nym-node-status-api` however you are setting your environmental variables, as `NODE_STATUS_API_AGENT_KEY_LIST`: + +```bash +export NODE_STATUS_API_AGENT_KEY_LIST= # e.g. "H4z8kx5Kkf5JNQHfxaE1MwRndjDCD1C7HsVhHTFfBZ4J" +``` + +In this situation, you are probably only using one key. However, it is possible to set multiple keys as a comma seperated list, in case you wish to whitelist multiple `-agent`s to be able to submit to a single `-api` instance. + +#### Run the Node Status Agent +```shell +nym-node-status-agent run-probe --server-address http://127.0.0.1 --server-port 8000 --ns-api-auth-key "" --probe-path nym-gateway-probe +``` + +You will see a lot of output like so: + + +```shell +listen_port=48586 +public_key=3f95caf771b8a63b9bdae6c186f7aba2eae93483f1c82c0243b5e00c85b4ec26 +preshared_key=0000000000000000000000000000000000000000000000000000000000000000 +protocol_version=1 +endpoint=185.186.78.251:51822 +last_handshake_time_sec=0 +last_handshake_time_nsec=0 +tx_bytes=0 +rx_bytes=0 +persistent_keepalive_interval=0 +allowed_ip=0.0.0.0/0 +2025/03/03 18:58:28 Pinging nymtech.net seq=0 +2025/03/03 18:58:29 Ping latency: 44.503483ms +2025/03/03 18:58:29 Pinging nymtech.net seq=1 +2025/03/03 18:58:29 Ping latency: 42.852414ms +2025/03/03 18:58:29 Pinging nymtech.net seq=2 +2025/03/03 18:58:29 Ping latency: 43.627256ms +2025/03/03 18:58:29 Pinging nymtech.net seq=3 +2025/03/03 18:58:29 Ping latency: 43.638839ms +2025/03/03 18:58:29 Pinging nymtech.net seq=4 +2025/03/03 18:58:29 Ping latency: 43.345357ms +2025/03/03 18:58:29 Pinging 1.1.1.1 seq=0 +2025/03/03 18:58:29 Ping latency: 46.327233ms +2025/03/03 18:58:34 Pinging 1.1.1.1 seq=1 +2025/03/03 18:58:34 Ping latency: 46.273726ms +2025/03/03 18:58:39 Pinging 1.1.1.1 seq=2 +2025/03/03 18:58:39 Ping latency: 46.542774ms +2025/03/03 18:58:44 Pinging 1.1.1.1 seq=3 +2025/03/03 18:58:44 Ping latency: 45.663545ms +2025/03/03 18:58:49 Pinging 1.1.1.1 seq=4 +2025/03/03 18:58:49 Ping latency: 43.803063ms +2025/03/03 18:58:56 Downloaded file content length: 1.00 MB +2025/03/03 18:58:56 Download duration: 1.308072386s +2025/03/03 18:58:56 private_key=1083749e43f4f8fb008f3f7deef9107ef86a68969670ddbb9f07bf85f94fb564 +listen_port=39129 +public_key=3f95caf771b8a63b9bdae6c186f7aba2eae93483f1c82c0243b5e00c85b4ec26 +preshared_key=0000000000000000000000000000000000000000000000000000000000000000 +protocol_version=1 +endpoint=185.186.78.251:51822 +last_handshake_time_sec=0 +last_handshake_time_nsec=0 +tx_bytes=0 +rx_bytes=0 +persistent_keepalive_interval=0 +allowed_ip=::/0 +2025/03/03 18:58:56 Pinging ipv6.google.com seq=0 +2025/03/03 18:58:56 Ping latency: 42.839528ms +2025/03/03 18:58:56 Pinging ipv6.google.com seq=1 +2025/03/03 18:58:56 Ping latency: 54.844651ms +2025/03/03 18:58:56 Pinging ipv6.google.com seq=2 +2025/03/03 18:58:56 Ping latency: 51.23104ms +2025/03/03 18:58:56 Pinging ipv6.google.com seq=3 +2025/03/03 18:58:56 Ping latency: 43.320409ms +2025/03/03 18:58:56 Pinging ipv6.google.com seq=4 +2025/03/03 18:58:56 Ping latency: 63.517358ms +2025/03/03 18:58:56 Pinging 2001:4860:4860::8888 seq=0 +2025/03/03 18:58:56 Ping latency: 54.682534ms +2025/03/03 18:59:01 Pinging 2001:4860:4860::8888 seq=1 +2025/03/03 18:59:01 Ping latency: 55.56235ms +2025/03/03 18:59:06 Pinging 2001:4860:4860::8888 seq=2 +2025/03/03 18:59:06 Ping latency: 55.970418ms +2025/03/03 18:59:11 Pinging 2001:4860:4860::8888 seq=3 +2025/03/03 18:59:14 Failed to send ping: i/o timeout +2025/03/03 18:59:19 Pinging 2001:4860:4860::8888 seq=4 +2025/03/03 18:59:22 Failed to send ping: i/o timeout +2025/03/03 18:59:27 Pinging 2606:4700:4700::1111 seq=0 +2025/03/03 18:59:27 Ping latency: 45.072616ms +2025/03/03 18:59:32 Pinging 2606:4700:4700::1111 seq=1 +2025/03/03 18:59:32 Ping latency: 44.357306ms +2025/03/03 18:59:37 Pinging 2606:4700:4700::1111 seq=2 +2025/03/03 18:59:37 Ping latency: 44.013562ms +2025/03/03 18:59:42 Pinging 2606:4700:4700::1111 seq=3 +2025/03/03 18:59:42 Ping latency: 46.94342ms +2025/03/03 18:59:47 Pinging 2606:4700:4700::1111 seq=4 +2025/03/03 18:59:48 Ping latency: 43.372288ms +2025/03/03 18:59:53 Pinging 2620:fe::fe seq=0 +2025/03/03 18:59:53 Ping latency: 42.164952ms +2025/03/03 18:59:58 Pinging 2620:fe::fe seq=1 +2025/03/03 18:59:58 Ping latency: 42.295812ms +2025/03/03 19:00:03 Pinging 2620:fe::fe seq=2 +2025/03/03 19:00:03 Ping latency: 43.117534ms +2025/03/03 19:00:08 Pinging 2620:fe::fe seq=3 +2025/03/03 19:00:08 Ping latency: 44.26068ms +2025/03/03 19:00:13 Pinging 2620:fe::fe seq=4 +2025/03/03 19:00:13 Ping latency: 45.29956ms +2025/03/03 19:00:21 Downloaded file content length: 10.00 MB +2025/03/03 19:00:21 Download duration: 3.39529252s +``` + + +Whilst you can run the `-agent` directly, it might be easier to run multiple instances in parallel with a script like so: + +```bash +#!/bin/bash + +set -eu +export ENVIRONMENT=${ENVIRONMENT:-"mainnet"} + +probe_git_ref="nym-vpn-core-v1.4.0" # check for the most recent release +monorepo_root= + +set -a +source "${monorepo_root}/envs/${ENVIRONMENT}.env" +set +a + +export RUST_LOG="info" +export NODE_STATUS_AGENT_SERVER_ADDRESS="http://127.0.0.1" +export NODE_STATUS_AGENT_SERVER_PORT="8000" +export NODE_STATUS_AGENT_AUTH_KEY= +export NODE_STATUS_AGENT_PROBE_EXTRA_ARGS="netstack-download-timeout-sec=30,netstack-num-ping=2,netstack-send-timeout-sec=1,netstack-recv-timeout-sec=1" + +workers=${1:-1} +echo "Running $workers workers in parallel" + +function swarm() { + local workers=$1 + + for ((i = 1; i <= workers; i++)); do + ${monorepo_root}/target/release/nym-node-status-agent run-probe --probe-path ~//nym-vpn-client/nym-vpn-core/target/debug/nym-gateway-probe & + done + + wait + + echo "All agents completed" +} + +swarm $workers +``` + +And run specifying the number of workers with `./.sh `. + + +When running the probe, use logging level `RUST_LOG=info`. The Node Status API relies on that granularity for parsing probe results, and the logs grow incessantly if a lower level (e.g. `DEBUG`) is used. + + +### Ports +By default the API listens on `8000`, so you will need to configure this post to be reachable on your remote server. You can modify this with the `--http_port` flag. diff --git a/documentation/docs/pages/apis/nym-api/_meta.json b/documentation/docs/pages/apis/nym-api/_meta.json new file mode 100644 index 0000000000..3949bf02de --- /dev/null +++ b/documentation/docs/pages/apis/nym-api/_meta.json @@ -0,0 +1,3 @@ +{ + "mainnet":"Mainnet Endpoints" +} diff --git a/nym-node-status-api/README.md b/nym-node-status-api/README.md new file mode 100644 index 0000000000..03da6ca3cb --- /dev/null +++ b/nym-node-status-api/README.md @@ -0,0 +1,6 @@ +# Nym Node Status API +The Node Status API serves information about individual `nym-nodes` in the Mixnet, such as which role they are operating in, statistics about them, services such as Network Requesters, as well as summaries of the state of the Mixnet. + +We recommend that developers building applications such as explorers or analytics interfaces about the Mixnet run their own instance of the API, in order to promote a robust network of downstream services, and spread the load of API calls amongst as many endpoints as possible. + +You can find build and operation instructions in the [docs](https://nym.com/docs/apis/ns-api). diff --git a/nym-node-status-api/nym-node-status-agent/src/cli/generate_keypair.rs b/nym-node-status-api/nym-node-status-agent/src/cli/generate_keypair.rs index e8360af47f..f769d3acb0 100644 --- a/nym-node-status-api/nym-node-status-agent/src/cli/generate_keypair.rs +++ b/nym-node-status-api/nym-node-status-agent/src/cli/generate_keypair.rs @@ -10,7 +10,8 @@ pub(crate) fn generate_key_pair(path: impl AsRef) -> anyhow::Result<()> { let mut private_key_file = File::create(priv_key_path)?; private_key_file.write_all(keypair.private_key().to_base58_string().as_bytes())?; - let pub_key_path = priv_key_path.with_extension("public"); + let pub_key_path = priv_key_path.with_file_name("public-key"); + let mut public_key_file = File::create(&pub_key_path)?; public_key_file.write_all(keypair.public_key().to_base58_string().as_bytes())?; diff --git a/nym-node-status-api/nym-node-status-agent/src/probe.rs b/nym-node-status-api/nym-node-status-agent/src/probe.rs index 596dfaa34c..67e7e508c6 100644 --- a/nym-node-status-api/nym-node-status-agent/src/probe.rs +++ b/nym-node-status-api/nym-node-status-agent/src/probe.rs @@ -1,4 +1,4 @@ -use tracing::error; +use tracing::{debug, error, info}; pub(crate) struct GwProbe { path: String, @@ -10,21 +10,62 @@ impl GwProbe { } pub(crate) async fn version(&self) -> String { + debug!("Attempting to execute binary at: {}", &self.path); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + match tokio::fs::metadata(&self.path).await { + Ok(metadata) => { + let perms = metadata.permissions(); + let mode = perms.mode(); + if mode & 0o111 == 0 { + error!( + "Binary is not executable: {} (mode: {:o})", + &self.path, mode + ); + return "Binary is not executable".to_string(); + } + debug!("Binary exists with permissions: {:o}", mode); + } + Err(e) => { + error!("Failed to stat binary at {}: {}", &self.path, e); + return format!("Failed to access binary: {}", e); + } + } + } + let mut command = tokio::process::Command::new(&self.path); command.stdout(std::process::Stdio::piped()); command.arg("--version"); + info!("Executing command: {:?} --version", &self.path); + match command.spawn() { - Ok(child) => { - if let Ok(output) = child.wait_with_output().await { - return String::from_utf8(output.stdout) - .unwrap_or("Unable to get log from test run".to_string()); + Ok(child) => match child.wait_with_output().await { + Ok(output) => { + if output.status.success() { + String::from_utf8(output.stdout) + .unwrap_or_else(|_| "Unable to parse version output".to_string()) + } else { + let stderr = String::from_utf8(output.stderr) + .unwrap_or_else(|_| "Unable to parse error output".to_string()); + error!( + "Command failed with exit code {}: {}", + output.status.code().unwrap_or(-1), + stderr + ); + format!("Command failed: {}", stderr) + } } - "Unable to get probe version".to_string() - } + Err(e) => { + error!("Failed to get command output: {}", e); + format!("Failed to get command output: {}", e) + } + }, Err(e) => { - error!("Failed to get probe version: {}", e); - "Failed to get probe version".to_string() + error!("Failed to spawn process: {}", e); + format!("Failed to spawn process: {}", e) } } } diff --git a/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs b/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs index 564d1fbf95..05b7dc843d 100644 --- a/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs @@ -296,9 +296,14 @@ impl Monitor { Some(location) => return location, None => { for ip in node.description.host_information.ip_address.iter() { - if let Ok(location) = self.ipinfo.locate_ip(ip.to_string()).await { - self.geocache.insert(node_id, location.clone()).await; - return location; + match self.ipinfo.locate_ip(ip.to_string()).await { + Ok(location) => { + self.geocache.insert(node_id, location.clone()).await; + return location; + } + Err(err) => { + tracing::warn!("Couldn't locate IP {} due to: {}", ip, err) + } } } // if no data could be retrieved From 8a2d98e3cea0fdfc5326c469b99f0bf9e04324a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Mar 2025 17:16:05 +0000 Subject: [PATCH 058/133] build(deps): bump ring from 0.17.9 to 0.17.13 Bumps [ring](https://github.com/briansmith/ring) from 0.17.9 to 0.17.13. - [Changelog](https://github.com/briansmith/ring/blob/main/RELEASES.md) - [Commits](https://github.com/briansmith/ring/commits) --- updated-dependencies: - dependency-name: ring dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7fef1012b5..5b005e0bb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8405,9 +8405,9 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.9" +version = "0.17.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e75ec5e92c4d8aede845126adc388046234541629e76029599ed35a003c7ed24" +checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee" dependencies = [ "cc", "cfg-if", From a69aa23609e502dd8d003739e83e4d8019669816 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Mar 2025 11:35:14 +0100 Subject: [PATCH 059/133] build(deps): bump the patch-updates group with 8 updates (#5585) Bumps the patch-updates group with 8 updates: | Package | From | To | | --- | --- | --- | | [bytes](https://github.com/tokio-rs/bytes) | `1.10.0` | `1.10.1` | | [semver](https://github.com/dtolnay/semver) | `1.0.25` | `1.0.26` | | [serde](https://github.com/serde-rs/serde) | `1.0.218` | `1.0.219` | | [serde_bytes](https://github.com/serde-rs/bytes) | `0.11.16` | `0.11.17` | | [serde_derive](https://github.com/serde-rs/serde) | `1.0.218` | `1.0.219` | | [serde_repr](https://github.com/dtolnay/serde-repr) | `0.1.19` | `0.1.20` | | [time](https://github.com/time-rs/time) | `0.3.37` | `0.3.39` | | [ff](https://github.com/zkcrypto/ff) | `0.13.0` | `0.13.1` | Updates `bytes` from 1.10.0 to 1.10.1 - [Release notes](https://github.com/tokio-rs/bytes/releases) - [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md) - [Commits](https://github.com/tokio-rs/bytes/compare/v1.10.0...v1.10.1) Updates `semver` from 1.0.25 to 1.0.26 - [Release notes](https://github.com/dtolnay/semver/releases) - [Commits](https://github.com/dtolnay/semver/compare/1.0.25...1.0.26) Updates `serde` from 1.0.218 to 1.0.219 - [Release notes](https://github.com/serde-rs/serde/releases) - [Commits](https://github.com/serde-rs/serde/compare/v1.0.218...v1.0.219) Updates `serde_bytes` from 0.11.16 to 0.11.17 - [Release notes](https://github.com/serde-rs/bytes/releases) - [Commits](https://github.com/serde-rs/bytes/compare/0.11.16...0.11.17) Updates `serde_derive` from 1.0.218 to 1.0.219 - [Release notes](https://github.com/serde-rs/serde/releases) - [Commits](https://github.com/serde-rs/serde/compare/v1.0.218...v1.0.219) Updates `serde_repr` from 0.1.19 to 0.1.20 - [Release notes](https://github.com/dtolnay/serde-repr/releases) - [Commits](https://github.com/dtolnay/serde-repr/compare/0.1.19...0.1.20) Updates `time` from 0.3.37 to 0.3.39 - [Release notes](https://github.com/time-rs/time/releases) - [Changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md) - [Commits](https://github.com/time-rs/time/compare/v0.3.37...v0.3.39) Updates `ff` from 0.13.0 to 0.13.1 - [Changelog](https://github.com/zkcrypto/ff/blob/main/CHANGELOG.md) - [Commits](https://github.com/zkcrypto/ff/commits) --- updated-dependencies: - dependency-name: bytes dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: semver dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: serde dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: serde_bytes dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: serde_derive dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: serde_repr dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: time dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates - dependency-name: ff dependency-type: direct:production update-type: version-update:semver-patch dependency-group: patch-updates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 58 +++++++++++++++++++++++++++--------------------------- Cargo.toml | 12 +++++------ 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7fef1012b5..b26e947352 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -902,9 +902,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" dependencies = [ "serde", ] @@ -941,7 +941,7 @@ checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" dependencies = [ "camino", "cargo-platform", - "semver 1.0.25", + "semver 1.0.26", "serde", "serde_json", "thiserror 1.0.69", @@ -955,7 +955,7 @@ checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" dependencies = [ "camino", "cargo-platform", - "semver 1.0.25", + "semver 1.0.26", "serde", "serde_json", "thiserror 1.0.69", @@ -1799,7 +1799,7 @@ dependencies = [ "cosmwasm-std", "cw2", "schemars", - "semver 1.0.25", + "semver 1.0.26", "serde", "thiserror 1.0.69", ] @@ -1814,7 +1814,7 @@ dependencies = [ "cosmwasm-std", "cw-storage-plus", "schemars", - "semver 1.0.25", + "semver 1.0.26", "serde", "thiserror 1.0.69", ] @@ -2527,9 +2527,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "ff" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ "rand_core 0.6.4", "subtle 2.6.1", @@ -4841,7 +4841,7 @@ dependencies = [ "rand_chacha 0.3.1", "reqwest 0.12.4", "schemars", - "semver 1.0.25", + "semver 1.0.26", "serde", "serde_json", "sha2 0.9.9", @@ -6052,7 +6052,7 @@ dependencies = [ "nym-contracts-common", "rand_chacha 0.3.1", "schemars", - "semver 1.0.25", + "semver 1.0.26", "serde", "serde-json-wasm", "serde_repr", @@ -6253,7 +6253,7 @@ dependencies = [ "nym-wireguard", "nym-wireguard-types", "rand 0.8.5", - "semver 1.0.25", + "semver 1.0.26", "serde", "serde_json", "si-scale", @@ -8698,7 +8698,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.25", + "semver 1.0.26", ] [[package]] @@ -8986,9 +8986,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" dependencies = [ "serde", ] @@ -9001,9 +9001,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] @@ -9041,18 +9041,18 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.16" +version = "0.11.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "364fec0df39c49a083c9a8a18a23a6bcfd9af130fe9fe321d18520a0d113e09e" +checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -9155,9 +9155,9 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", @@ -10085,7 +10085,7 @@ dependencies = [ "pin-project", "rand 0.8.5", "reqwest 0.11.27", - "semver 1.0.25", + "semver 1.0.26", "serde", "serde_bytes", "serde_json", @@ -10245,9 +10245,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.37" +version = "0.3.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" dependencies = [ "deranged", "itoa", @@ -10263,15 +10263,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" dependencies = [ "num-conv", "time-core", diff --git a/Cargo.toml b/Cargo.toml index 1cae64acf1..342a3557cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -209,7 +209,7 @@ blake3 = "1.6.1" bloomfilter = "1.0.14" bs58 = "0.5.1" bytecodec = "0.4.15" -bytes = "1.7.2" +bytes = "1.10.1" cargo_metadata = "0.18.1" celes = "2.5.0" cfg-if = "1.0.0" @@ -310,9 +310,9 @@ rocket_okapi = "0.8.0" rs_merkle = "1.4.2" safer-ffi = "0.1.13" schemars = "0.8.22" -semver = "1.0.25" -serde = "1.0.217" -serde_bytes = "0.11.16" +semver = "1.0.26" +serde = "1.0.219" +serde_bytes = "0.11.17" serde_derive = "1.0" serde_json = "1.0.140" serde_json_path = "0.7.2" @@ -332,7 +332,7 @@ tap = "1.0.1" tar = "0.4.44" tempfile = "3.15" thiserror = "2.0" -time = "0.3.37" +time = "0.3.39" tokio = "1.43" tokio-postgres = "0.7" tokio-stream = "0.1.17" @@ -371,7 +371,7 @@ prometheus = { version = "0.13.0" } # plus to make our live easier we need serde support from https://github.com/zkcrypto/bls12_381/pull/125 bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", default-features = false, branch = "temp/experimental-serdect" } group = { version = "0.13.0", default-features = false } -ff = { version = "0.13.0", default-features = false } +ff = { version = "0.13.1", default-features = false } subtle = "2.5.0" # cosmwasm-related From 2126736afff44a6567b662e269362c021af154c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Mar 2025 10:37:03 +0000 Subject: [PATCH 060/133] build(deps): bump tempfile from 3.17.1 to 3.18.0 Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.17.1 to 3.18.0. - [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md) - [Commits](https://github.com/Stebalien/tempfile/compare/v3.17.1...v3.18.0) --- updated-dependencies: - dependency-name: tempfile dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 33 ++++++++++++++++++++++++++------- Cargo.toml | 2 +- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b26e947352..07da8133ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1587,7 +1587,7 @@ dependencies = [ "bitflags 2.8.0", "crossterm_winapi", "parking_lot", - "rustix", + "rustix 0.38.44", "winapi", ] @@ -4144,6 +4144,12 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" + [[package]] name = "lioness" version = "0.1.2" @@ -8710,7 +8716,20 @@ dependencies = [ "bitflags 2.8.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dade4812df5c384711475be5fcd8c162555352945401aed22a35bffeab61f657" +dependencies = [ + "bitflags 2.8.0", + "errno", + "libc", + "linux-raw-sys 0.9.2", "windows-sys 0.59.0", ] @@ -9949,15 +9968,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.17.1" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" +checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567" dependencies = [ "cfg-if", "fastrand 2.3.0", "getrandom 0.3.1", "once_cell", - "rustix", + "rustix 1.0.1", "windows-sys 0.59.0", ] @@ -12041,8 +12060,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909" dependencies = [ "libc", - "linux-raw-sys", - "rustix", + "linux-raw-sys 0.4.15", + "rustix 0.38.44", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 342a3557cd..2f6585f79f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -330,7 +330,7 @@ syn = "1" sysinfo = "0.33.0" tap = "1.0.1" tar = "0.4.44" -tempfile = "3.15" +tempfile = "3.18" thiserror = "2.0" time = "0.3.39" tokio = "1.43" From 1a334b575dd2b372329ce5c2c2fe560152643746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 10 Mar 2025 13:34:08 +0000 Subject: [PATCH 061/133] feat: make sure any terminated task kills the watcher and write run info to db (#5517) * feat: make sure any terminated task kills the watcher and write run info to db * updated chain watcher version --- Cargo.lock | 2 +- ...6bee247bc021b9534f1d4b0fcad32e2e56218.json | 12 + nyx-chain-watcher/Cargo.toml | 2 +- .../005_add_listener_failure_table.sql | 11 + nyx-chain-watcher/src/cli/commands/run/mod.rs | 216 +++++++++++++++--- nyx-chain-watcher/src/db/mod.rs | 2 +- nyx-chain-watcher/src/http/server.rs | 56 +---- nyx-chain-watcher/src/price_scraper/mod.rs | 3 +- 8 files changed, 209 insertions(+), 95 deletions(-) create mode 100644 nyx-chain-watcher/.sqlx/query-1aa7733ad4bbf3e6b8db909b8646bee247bc021b9534f1d4b0fcad32e2e56218.json create mode 100644 nyx-chain-watcher/migrations/005_add_listener_failure_table.sql diff --git a/Cargo.lock b/Cargo.lock index b26e947352..e708637feb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7222,7 +7222,7 @@ dependencies = [ [[package]] name = "nyx-chain-watcher" -version = "0.1.11" +version = "0.1.12" dependencies = [ "anyhow", "async-trait", diff --git a/nyx-chain-watcher/.sqlx/query-1aa7733ad4bbf3e6b8db909b8646bee247bc021b9534f1d4b0fcad32e2e56218.json b/nyx-chain-watcher/.sqlx/query-1aa7733ad4bbf3e6b8db909b8646bee247bc021b9534f1d4b0fcad32e2e56218.json new file mode 100644 index 0000000000..d1fd65f596 --- /dev/null +++ b/nyx-chain-watcher/.sqlx/query-1aa7733ad4bbf3e6b8db909b8646bee247bc021b9534f1d4b0fcad32e2e56218.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO watcher_execution(start, end, error_message)\n VALUES (?, ?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "1aa7733ad4bbf3e6b8db909b8646bee247bc021b9534f1d4b0fcad32e2e56218" +} diff --git a/nyx-chain-watcher/Cargo.toml b/nyx-chain-watcher/Cargo.toml index 285ca3c319..791a867635 100644 --- a/nyx-chain-watcher/Cargo.toml +++ b/nyx-chain-watcher/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nyx-chain-watcher" -version = "0.1.11" +version = "0.1.12" authors.workspace = true repository.workspace = true homepage.workspace = true diff --git a/nyx-chain-watcher/migrations/005_add_listener_failure_table.sql b/nyx-chain-watcher/migrations/005_add_listener_failure_table.sql new file mode 100644 index 0000000000..6a5cdac6de --- /dev/null +++ b/nyx-chain-watcher/migrations/005_add_listener_failure_table.sql @@ -0,0 +1,11 @@ +/* + * Copyright 2025 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +CREATE TABLE watcher_execution +( + start TIMESTAMP WITHOUT TIME ZONE NOT NULL, + end TIMESTAMP WITHOUT TIME ZONE NOT NULL, + error_message TEXT +) \ No newline at end of file diff --git a/nyx-chain-watcher/src/cli/commands/run/mod.rs b/nyx-chain-watcher/src/cli/commands/run/mod.rs index ef2a553995..65f5de15d5 100644 --- a/nyx-chain-watcher/src/cli/commands/run/mod.rs +++ b/nyx-chain-watcher/src/cli/commands/run/mod.rs @@ -2,19 +2,112 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::error::NyxChainWatcherError; -use tokio::join; -use tracing::{error, info, trace}; +use anyhow::Context; +use std::time::Duration; +use time::OffsetDateTime; +use tokio::task::{JoinHandle, JoinSet}; +use tokio_util::sync::CancellationToken; +use tracing::{error, info}; mod args; mod config; use crate::chain_scraper::run_chain_scraper; +use crate::db::DbPool; use crate::{db, http, payment_listener, price_scraper}; pub(crate) use args::Args; use nym_task::signal::wait_for_signal; +async fn try_insert_watcher_execution_information( + db_pool: DbPool, + start: OffsetDateTime, + end: OffsetDateTime, + error_message: Option, +) { + let _ = sqlx::query!( + r#" + INSERT INTO watcher_execution(start, end, error_message) + VALUES (?, ?, ?) + "#, + start, + end, + error_message + ) + .execute(&db_pool) + .await + .inspect_err(|err| error!("failed to insert run information: {err}")); +} + +async fn wait_for_shutdown( + db_pool: DbPool, + start: OffsetDateTime, + main_cancellation_token: CancellationToken, + scraper_cancellation_token: CancellationToken, + mut tasks: JoinSet>>, +) { + async fn finalize_shutdown( + db_pool: DbPool, + start: OffsetDateTime, + main_cancellation_token: CancellationToken, + scraper_cancellation_token: CancellationToken, + mut tasks: JoinSet>>, + error_message: Option, + ) { + // cancel all tasks + main_cancellation_token.cancel(); + scraper_cancellation_token.cancel(); + + // stupid nasty and hacky workaround to make sure all relevant tasks have finished before hard aborting them + // nasty stupid and hacky workaround + tokio::time::sleep(Duration::from_secs(1)).await; + tasks.abort_all(); + + // insert execution result into the db + try_insert_watcher_execution_information( + db_pool, + start, + OffsetDateTime::now_utc(), + error_message, + ) + .await + } + + tokio::select! { + // graceful shutdown + _ = wait_for_signal() => { + info!("received shutdown signal"); + finalize_shutdown(db_pool, start, main_cancellation_token, scraper_cancellation_token, tasks, None).await; + } + _ = scraper_cancellation_token.cancelled() => { + info!("the scraper has issued cancellation"); + finalize_shutdown(db_pool, start, main_cancellation_token, scraper_cancellation_token, tasks, Some("unexpected scraper task cancellation".into())).await; + } + _ = main_cancellation_token.cancelled() => { + info!("one of the tasks has cancelled the token"); + finalize_shutdown(db_pool, start, main_cancellation_token, scraper_cancellation_token, tasks, Some("unexpected main task cancellation".into())).await; + } + task_result = tasks.join_next() => { + // the first unwrap is fine => join set was not empty + let error_message = match task_result.unwrap() { + Err(_join_err) => Some("unexpected join error".to_string()), + Ok(Some(Ok(_))) => None, + Ok(Some(Err(err))) => Some(err.to_string()), + Ok(None) => { + Some("unexpected task cancellation".to_string()) + } + }; + + error!("unexpected task termination: {error_message:?}"); + finalize_shutdown(db_pool, start, main_cancellation_token, scraper_cancellation_token, tasks, error_message).await; + } + + } +} + pub(crate) async fn execute(args: Args, http_port: u16) -> Result<(), NyxChainWatcherError> { - trace!("passed arguments: {args:#?}"); + let start = OffsetDateTime::now_utc(); + + info!("passed arguments: {args:#?}"); let config = config::get_run_config(args)?; @@ -29,9 +122,7 @@ pub(crate) async fn execute(args: Args, http_port: u16) -> Result<(), NyxChainWa ); info!( "Chain History Database path is {:?}", - std::path::Path::new(&config.chain_scraper_database_path()) - .canonicalize() - .unwrap_or_default() + std::path::Path::new(&config.chain_scraper_database_path()).canonicalize() ); // Ensure parent directory exists @@ -41,50 +132,99 @@ pub(crate) async fn execute(args: Args, http_port: u16) -> Result<(), NyxChainWa let connection_url = format!("sqlite://{}?mode=rwc", db_path); let storage = db::Storage::init(connection_url).await?; - let watcher_pool = storage.pool_owned().await; + let watcher_pool = storage.pool_owned(); - // Spawn the chain scraper and get its storage + let mut tasks = JoinSet::new(); + let cancellation_token = CancellationToken::new(); - // Spawn the payment listener task - let payment_listener_handle = tokio::spawn({ - let price_scraper_pool = storage.pool_owned().await; - let scraper_pool = storage.pool_owned().await; - run_chain_scraper(&config, scraper_pool).await?; - let payment_watcher_config = config.payment_watcher_config.unwrap_or_default(); + let price_scraper_pool = storage.pool_owned(); + let scraper_pool = storage.pool_owned(); + let shutdown_pool = storage.pool_owned(); + // spawn all the tasks + + // 1. chain scraper (note: this doesn't really spawn the full scraper on this task, but we don't want to be blocking waiting for its startup) + let scraper_token_handle: JoinHandle> = tokio::spawn({ + let config = config.clone(); async move { - if let Err(e) = - payment_listener::run_payment_listener(payment_watcher_config, price_scraper_pool) - .await - { - error!("Payment listener error: {}", e); - } - Ok::<_, anyhow::Error>(()) + // this only blocks until startup sync is done; it then runs on its own set of tasks + let scraper = run_chain_scraper(&config, scraper_pool).await?; + Ok(scraper.cancel_token()) } }); - // Clone pool for each task that needs it - //let background_pool = db_pool.clone(); + // 2. payment listener + let token = cancellation_token.clone(); + { + tasks.spawn(async move { + token + .run_until_cancelled(async move { + let payment_watcher_config = config.payment_watcher_config.unwrap_or_default(); + payment_listener::run_payment_listener( + payment_watcher_config, + price_scraper_pool, + ) + .await + .inspect_err(|err| error!("Payment listener error: {err}")) + }) + .await + }); + } - let price_scraper_handle = tokio::spawn(async move { - price_scraper::run_price_scraper(&watcher_pool).await; - }); + // 3. price scraper (note, this task never terminates on its own) + { + let token = cancellation_token.clone(); + tasks.spawn(async move { + token + .run_until_cancelled(async move { + price_scraper::run_price_scraper(&watcher_pool).await; + Ok(()) + }) + .await + }); + } - let shutdown_handles = http::server::start_http_api(storage.pool_owned().await, http_port) - .await - .expect("Failed to start server"); + // 4. http api + let http_server = http::server::build_http_api(storage.pool_owned(), http_port).await?; + { + let token = cancellation_token.clone(); + tasks.spawn(async move { + info!("Starting HTTP server on port {http_port}",); + async move { + Some( + http_server + .run(token.cancelled_owned()) + .await + .context("http server failure"), + ) + } + .await + }); + } - info!("Started HTTP server on port {}", http_port); + // 1. wait for either shutdown or scraper having finished startup + tokio::select! { + _ = wait_for_signal() => { + info!("received shutdown signal while waiting for scraper to finish its startup"); + return Ok(()) + } + scraper_token = scraper_token_handle => { + let scraper_token = match scraper_token { + Ok(Ok(token)) => token, + Ok(Err(startup_err)) => { + error!("failed to startup the chain scraper: {startup_err}"); + return Err(startup_err.into()); + } + Err(runtime_err) => { + error!("failed to finish the scraper startup task: {runtime_err}"); + return Ok(()) - // Wait for the short-lived tasks to complete - let _ = join!(price_scraper_handle, payment_listener_handle); + } + }; - // Wait for a signal to terminate the long-running task - wait_for_signal().await; - - if let Err(err) = shutdown_handles.shutdown().await { - error!("{err}"); - }; + wait_for_shutdown(shutdown_pool, start, cancellation_token, scraper_token, tasks).await + } + } Ok(()) } diff --git a/nyx-chain-watcher/src/db/mod.rs b/nyx-chain-watcher/src/db/mod.rs index 74339ca575..00368afb6e 100644 --- a/nyx-chain-watcher/src/db/mod.rs +++ b/nyx-chain-watcher/src/db/mod.rs @@ -34,7 +34,7 @@ impl Storage { } /// Cloning pool is cheap, it's the same underlying set of connections - pub async fn pool_owned(&self) -> DbPool { + pub fn pool_owned(&self) -> DbPool { self.pool.clone() } } diff --git a/nyx-chain-watcher/src/http/server.rs b/nyx-chain-watcher/src/http/server.rs index 375774bd18..a328d86b5d 100644 --- a/nyx-chain-watcher/src/http/server.rs +++ b/nyx-chain-watcher/src/http/server.rs @@ -1,19 +1,14 @@ use axum::Router; use core::net::SocketAddr; -use tokio::{net::TcpListener, task::JoinHandle}; -use tokio_util::sync::{CancellationToken, WaitForCancellationFutureOwned}; +use tokio::net::TcpListener; +use tokio_util::sync::WaitForCancellationFutureOwned; use crate::{ db::DbPool, http::{api::RouterBuilder, state::AppState}, }; -/// Return handles that allow for graceful shutdown of server + awaiting its -/// background tokio task -pub(crate) async fn start_http_api( - db_pool: DbPool, - http_port: u16, -) -> anyhow::Result { +pub(crate) async fn build_http_api(db_pool: DbPool, http_port: u16) -> anyhow::Result { let router_builder = RouterBuilder::with_default_routes(); let state = AppState::new(db_pool); @@ -21,50 +16,7 @@ pub(crate) async fn start_http_api( let bind_addr = format!("0.0.0.0:{}", http_port); let server = router.build_server(bind_addr).await?; - - Ok(start_server(server)) -} - -fn start_server(server: HttpServer) -> ShutdownHandles { - // one copy is stored to trigger a graceful shutdown later - let shutdown_button = CancellationToken::new(); - // other copy is given to server to listen for a shutdown - let shutdown_receiver = shutdown_button.clone(); - let shutdown_receiver = shutdown_receiver.cancelled_owned(); - - let server_handle = tokio::spawn(async move { server.run(shutdown_receiver).await }); - - ShutdownHandles { - server_handle, - shutdown_button, - } -} - -pub(crate) struct ShutdownHandles { - server_handle: JoinHandle>, - shutdown_button: CancellationToken, -} - -impl ShutdownHandles { - /// Send graceful shutdown signal to server and wait for server task to complete - pub(crate) async fn shutdown(self) -> anyhow::Result<()> { - self.shutdown_button.cancel(); - - match self.server_handle.await { - Ok(Ok(_)) => { - tracing::info!("HTTP server shut down without errors"); - } - Ok(Err(err)) => { - tracing::error!("HTTP server terminated with: {err}"); - anyhow::bail!(err) - } - Err(err) => { - tracing::error!("Server task panicked: {err}"); - } - }; - - Ok(()) - } + Ok(server) } pub(crate) struct HttpServer { diff --git a/nyx-chain-watcher/src/price_scraper/mod.rs b/nyx-chain-watcher/src/price_scraper/mod.rs index b0ab2676b6..10df1e17e7 100644 --- a/nyx-chain-watcher/src/price_scraper/mod.rs +++ b/nyx-chain-watcher/src/price_scraper/mod.rs @@ -3,7 +3,6 @@ use crate::db::{ queries::price::insert_nym_prices, }; use core::str; -use tokio::task::JoinHandle; use tokio::time::Duration; use crate::db::DbPool; @@ -13,7 +12,7 @@ const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(60 * 2); const COINGECKO_API_URL: &str = "https://api.coingecko.com/api/v3/simple/price?ids=nym&vs_currencies=chf,usd,eur,gbp,btc"; -pub(crate) async fn run_price_scraper(db_pool: &DbPool) -> JoinHandle<()> { +pub(crate) async fn run_price_scraper(db_pool: &DbPool) { loop { tracing::info!("Running in a loop 🏃"); if let Err(e) = get_coingecko_prices(db_pool).await { From d7ef68d8d1320dd594e3f3dfc71430afe49f0432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 10 Mar 2025 11:33:14 +0000 Subject: [PATCH 062/133] remove fallback to env values for watched addresses --- nyx-chain-watcher/src/chain_scraper/mod.rs | 2 +- nyx-chain-watcher/src/cli/commands/run/mod.rs | 5 +-- nyx-chain-watcher/src/config/mod.rs | 5 +-- .../src/config/payments_watcher.rs | 13 ++++++-- nyx-chain-watcher/src/http/api/watcher.rs | 32 ++----------------- nyx-chain-watcher/src/http/server.rs | 16 ++++++++-- nyx-chain-watcher/src/http/state.rs | 8 +++-- 7 files changed, 41 insertions(+), 40 deletions(-) diff --git a/nyx-chain-watcher/src/chain_scraper/mod.rs b/nyx-chain-watcher/src/chain_scraper/mod.rs index 93f768a236..fd4f0c6854 100644 --- a/nyx-chain-watcher/src/chain_scraper/mod.rs +++ b/nyx-chain-watcher/src/chain_scraper/mod.rs @@ -60,7 +60,7 @@ pub(crate) async fn run_chain_scraper( }) .with_tx_module(EventScraperModule::new( db_pool, - config.payment_watcher_config.clone().unwrap_or_default(), + config.payment_watcher_config.clone(), )); let instance = scraper.build_and_start().await?; diff --git a/nyx-chain-watcher/src/cli/commands/run/mod.rs b/nyx-chain-watcher/src/cli/commands/run/mod.rs index 65f5de15d5..2e16a1788d 100644 --- a/nyx-chain-watcher/src/cli/commands/run/mod.rs +++ b/nyx-chain-watcher/src/cli/commands/run/mod.rs @@ -155,11 +155,11 @@ pub(crate) async fn execute(args: Args, http_port: u16) -> Result<(), NyxChainWa // 2. payment listener let token = cancellation_token.clone(); + let payment_watcher_config = config.payment_watcher_config.clone(); { tasks.spawn(async move { token .run_until_cancelled(async move { - let payment_watcher_config = config.payment_watcher_config.unwrap_or_default(); payment_listener::run_payment_listener( payment_watcher_config, price_scraper_pool, @@ -185,7 +185,8 @@ pub(crate) async fn execute(args: Args, http_port: u16) -> Result<(), NyxChainWa } // 4. http api - let http_server = http::server::build_http_api(storage.pool_owned(), http_port).await?; + let http_server = + http::server::build_http_api(storage.pool_owned(), &config, http_port).await?; { let token = cancellation_token.clone(); tasks.spawn(async move { diff --git a/nyx-chain-watcher/src/config/mod.rs b/nyx-chain-watcher/src/config/mod.rs index 73419a2e9e..3535acce18 100644 --- a/nyx-chain-watcher/src/config/mod.rs +++ b/nyx-chain-watcher/src/config/mod.rs @@ -92,7 +92,7 @@ impl ConfigBuilder { Config { logging: self.logging.unwrap_or_default(), save_path: Some(self.config_path), - payment_watcher_config: self.payment_watcher_config, + payment_watcher_config: self.payment_watcher_config.unwrap_or_default(), data_dir: self.data_dir, db_path: self.db_path, chain_scraper_db_path: self.chain_scraper_db_path, @@ -116,7 +116,8 @@ pub struct Config { #[serde(skip)] chain_scraper_db_path: Option, - pub payment_watcher_config: Option, + #[serde(default)] + pub payment_watcher_config: PaymentWatcherConfig, #[serde(default)] pub logging: LoggingSettings, diff --git a/nyx-chain-watcher/src/config/payments_watcher.rs b/nyx-chain-watcher/src/config/payments_watcher.rs index a335830a85..78956d5762 100644 --- a/nyx-chain-watcher/src/config/payments_watcher.rs +++ b/nyx-chain-watcher/src/config/payments_watcher.rs @@ -1,12 +1,21 @@ use nym_validator_client::nyxd::AccountId; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] +#[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct PaymentWatcherConfig { pub watchers: Vec, } +impl PaymentWatcherConfig { + pub fn watched_transfer_accounts(&self) -> Vec<&AccountId> { + self.watchers + .iter() + .filter_map(|e| e.watch_for_transfer_recipient_accounts.as_ref()) + .flat_map(|a| a) + .collect() + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PaymentWatcherEntry { pub id: String, diff --git a/nyx-chain-watcher/src/http/api/watcher.rs b/nyx-chain-watcher/src/http/api/watcher.rs index 68e6f4f91c..121c86b91c 100644 --- a/nyx-chain-watcher/src/http/api/watcher.rs +++ b/nyx-chain-watcher/src/http/api/watcher.rs @@ -1,9 +1,7 @@ -use crate::config::Config; -use crate::env; use crate::http::error::HttpResult; use crate::http::state::AppState; +use axum::extract::State; use axum::{Json, Router}; -use std::env::var; pub(crate) fn routes() -> Router { Router::new().route("/addresses", axum::routing::get(get_addresses)) @@ -19,30 +17,6 @@ pub(crate) fn routes() -> Router { )] /// Fetch the addresses being watched by the chain watcher -async fn get_addresses() -> HttpResult>> { - let addresses = match Config::read_from_toml_file_in_default_location() { - Ok(config) => config - .payment_watcher_config - .as_ref() - .and_then(|config| { - config.watchers.iter().find_map(|watcher| { - watcher - .watch_for_transfer_recipient_accounts - .as_ref() - .map(|accounts| { - accounts - .iter() - .map(|account| account.to_string()) - .collect::>() - }) - }) - }) - .unwrap_or_default(), - // If the config file doesn't exist, fall back to env variable - Err(_) => var(env::vars::NYX_CHAIN_WATCHER_WATCH_ACCOUNTS) - .map(|accounts| accounts.split(',').map(String::from).collect()) - .unwrap_or_default(), - }; - - Ok(Json(addresses)) +async fn get_addresses(State(state): State) -> HttpResult>> { + Ok(Json(state.watched_addresses.clone())) } diff --git a/nyx-chain-watcher/src/http/server.rs b/nyx-chain-watcher/src/http/server.rs index a328d86b5d..20aa7042c4 100644 --- a/nyx-chain-watcher/src/http/server.rs +++ b/nyx-chain-watcher/src/http/server.rs @@ -3,15 +3,27 @@ use core::net::SocketAddr; use tokio::net::TcpListener; use tokio_util::sync::WaitForCancellationFutureOwned; +use crate::config::Config; use crate::{ db::DbPool, http::{api::RouterBuilder, state::AppState}, }; -pub(crate) async fn build_http_api(db_pool: DbPool, http_port: u16) -> anyhow::Result { +pub(crate) async fn build_http_api( + db_pool: DbPool, + config: &Config, + http_port: u16, +) -> anyhow::Result { let router_builder = RouterBuilder::with_default_routes(); - let state = AppState::new(db_pool); + let watched_accounts = config + .payment_watcher_config + .watched_transfer_accounts() + .iter() + .map(|a| a.to_string()) + .collect(); + + let state = AppState::new(db_pool, watched_accounts); let router = router_builder.with_state(state); let bind_addr = format!("0.0.0.0:{}", http_port); diff --git a/nyx-chain-watcher/src/http/state.rs b/nyx-chain-watcher/src/http/state.rs index 3f72135cf1..99fc155b74 100644 --- a/nyx-chain-watcher/src/http/state.rs +++ b/nyx-chain-watcher/src/http/state.rs @@ -3,11 +3,15 @@ use crate::db::DbPool; #[derive(Debug, Clone)] pub(crate) struct AppState { db_pool: DbPool, + pub(crate) watched_addresses: Vec, } impl AppState { - pub(crate) fn new(db_pool: DbPool) -> Self { - Self { db_pool } + pub(crate) fn new(db_pool: DbPool, watched_addresses: Vec) -> Self { + Self { + db_pool, + watched_addresses, + } } pub(crate) fn db_pool(&self) -> &DbPool { From f429092e21fb082d076ce4f8596fcc1ba7aacded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 10 Mar 2025 17:27:20 +0000 Subject: [PATCH 063/133] added basic payment listener information to status api --- Cargo.lock | 1 - ...b3b637383b370c24566497bcc5ea625e82db.json} | 4 +- nyx-chain-watcher/Cargo.toml | 1 - nyx-chain-watcher/src/chain_scraper/mod.rs | 26 +- nyx-chain-watcher/src/cli/commands/init.rs | 16 +- .../src/cli/commands/run/args.rs | 4 +- .../src/cli/commands/run/config.rs | 18 +- nyx-chain-watcher/src/cli/commands/run/mod.rs | 29 ++- nyx-chain-watcher/src/config/mod.rs | 8 +- .../src/config/payments_watcher.rs | 25 +- nyx-chain-watcher/src/db/models.rs | 26 ++ nyx-chain-watcher/src/helpers.rs | 55 +++++ nyx-chain-watcher/src/http/api/mod.rs | 10 +- nyx-chain-watcher/src/http/api/status.rs | 98 ++++++++ nyx-chain-watcher/src/http/api/watcher.rs | 2 +- nyx-chain-watcher/src/http/mod.rs | 1 + nyx-chain-watcher/src/http/models.rs | 92 +++++++ nyx-chain-watcher/src/http/server.rs | 20 +- nyx-chain-watcher/src/http/state.rs | 169 ++++++++++++- nyx-chain-watcher/src/main.rs | 1 + nyx-chain-watcher/src/models.rs | 2 +- nyx-chain-watcher/src/payment_listener/mod.rs | 225 +++++++++++------- .../src/payment_listener/watcher.rs | 73 ++++++ 23 files changed, 737 insertions(+), 169 deletions(-) rename nyx-chain-watcher/.sqlx/{query-7b9abf4ff422b8d7a942955dc4fba380e7d5f0127f4745705b8ac9af6c170d19.json => query-2e0fd886f2c68b105f5e609032bfb3b637383b370c24566497bcc5ea625e82db.json} (80%) create mode 100644 nyx-chain-watcher/src/helpers.rs create mode 100644 nyx-chain-watcher/src/http/api/status.rs create mode 100644 nyx-chain-watcher/src/http/models.rs create mode 100644 nyx-chain-watcher/src/payment_listener/watcher.rs diff --git a/Cargo.lock b/Cargo.lock index e708637feb..51ac8f5c4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7237,7 +7237,6 @@ dependencies = [ "nym-validator-client", "nyxd-scraper", "reqwest 0.12.4", - "rocket", "schemars", "serde", "serde_json", diff --git a/nyx-chain-watcher/.sqlx/query-7b9abf4ff422b8d7a942955dc4fba380e7d5f0127f4745705b8ac9af6c170d19.json b/nyx-chain-watcher/.sqlx/query-2e0fd886f2c68b105f5e609032bfb3b637383b370c24566497bcc5ea625e82db.json similarity index 80% rename from nyx-chain-watcher/.sqlx/query-7b9abf4ff422b8d7a942955dc4fba380e7d5f0127f4745705b8ac9af6c170d19.json rename to nyx-chain-watcher/.sqlx/query-2e0fd886f2c68b105f5e609032bfb3b637383b370c24566497bcc5ea625e82db.json index c873acca91..327bcd47b6 100644 --- a/nyx-chain-watcher/.sqlx/query-7b9abf4ff422b8d7a942955dc4fba380e7d5f0127f4745705b8ac9af6c170d19.json +++ b/nyx-chain-watcher/.sqlx/query-2e0fd886f2c68b105f5e609032bfb3b637383b370c24566497bcc5ea625e82db.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT * FROM transactions\n WHERE height > ? \n ORDER BY height ASC, message_index ASC\n ", + "query": "\n SELECT * FROM transactions\n WHERE height > ?\n ORDER BY height ASC, message_index ASC\n ", "describe": { "columns": [ { @@ -64,5 +64,5 @@ true ] }, - "hash": "7b9abf4ff422b8d7a942955dc4fba380e7d5f0127f4745705b8ac9af6c170d19" + "hash": "2e0fd886f2c68b105f5e609032bfb3b637383b370c24566497bcc5ea625e82db" } diff --git a/nyx-chain-watcher/Cargo.toml b/nyx-chain-watcher/Cargo.toml index 791a867635..bbe9ecc98d 100644 --- a/nyx-chain-watcher/Cargo.toml +++ b/nyx-chain-watcher/Cargo.toml @@ -29,7 +29,6 @@ nym-node-requests = { path = "../nym-node/nym-node-requests", features = [ nym-validator-client = { path = "../common/client-libs/validator-client" } nyxd-scraper = { path = "../common/nyxd-scraper" } reqwest = { workspace = true, features = ["rustls-tls"] } -rocket = { workspace = true } schemars = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/nyx-chain-watcher/src/chain_scraper/mod.rs b/nyx-chain-watcher/src/chain_scraper/mod.rs index fd4f0c6854..eba1865c6e 100644 --- a/nyx-chain-watcher/src/chain_scraper/mod.rs +++ b/nyx-chain-watcher/src/chain_scraper/mod.rs @@ -1,4 +1,4 @@ -use crate::config::PaymentWatcherConfig; +use crate::config::PaymentWatchersConfig; use crate::env::vars::{ NYXD_SCRAPER_START_HEIGHT, NYXD_SCRAPER_UNSAFE_NUKE_DB, NYXD_SCRAPER_USE_BEST_EFFORT_START_HEIGHT, @@ -10,7 +10,7 @@ use nyxd_scraper::{ }; use sqlx::SqlitePool; use std::fs; -use tracing::{info, warn}; +use tracing::{error, info, warn}; pub(crate) async fn run_chain_scraper( config: &crate::config::Config, @@ -73,11 +73,11 @@ pub(crate) async fn run_chain_scraper( pub struct EventScraperModule { db_pool: SqlitePool, - payment_config: PaymentWatcherConfig, + payment_config: PaymentWatchersConfig, } impl EventScraperModule { - pub fn new(db_pool: SqlitePool, payment_config: PaymentWatcherConfig) -> Self { + pub fn new(db_pool: SqlitePool, payment_config: PaymentWatchersConfig) -> Self { Self { db_pool, payment_config, @@ -132,6 +132,12 @@ impl TxModule for EventScraperModule { return Ok(()); } + if tx.tx.body.messages.len() > 1 { + error!( + "this transaction has more than 1 message in it - payment information will be lost" + ); + } + // Process each event for event in events { // Only process transfer events @@ -157,17 +163,7 @@ impl TxModule for EventScraperModule { // If we have all required fields, check if recipient is watched and store if let (Some(recipient), Some(sender), Some(amount)) = (recipient, sender, amount) { // Check if any watcher is watching this recipient - let is_watched = self.payment_config.watchers.iter().any(|watcher| { - if let Some(watched_accounts) = - &watcher.watch_for_transfer_recipient_accounts - { - watched_accounts - .iter() - .any(|account| account.to_string() == recipient) - } else { - false - } - }); + let is_watched = self.payment_config.is_being_watched(&recipient); if is_watched { if let Err(e) = self diff --git a/nyx-chain-watcher/src/cli/commands/init.rs b/nyx-chain-watcher/src/cli/commands/init.rs index a545552556..50be4f056b 100644 --- a/nyx-chain-watcher/src/cli/commands/init.rs +++ b/nyx-chain-watcher/src/cli/commands/init.rs @@ -3,8 +3,8 @@ use crate::cli::DEFAULT_NYX_CHAIN_WATCHER_ID; use crate::config::payments_watcher::HttpAuthenticationOptions::AuthorizationBearerToken; -use crate::config::payments_watcher::PaymentWatcherEntry; -use crate::config::{default_config_filepath, Config, ConfigBuilder, PaymentWatcherConfig}; +use crate::config::payments_watcher::PaymentWatcherConfig; +use crate::config::{default_config_filepath, Config, ConfigBuilder, PaymentWatchersConfig}; use crate::error::NyxChainWatcherError; use nym_config::save_unformatted_config_to_file; use nym_validator_client::nyxd::AccountId; @@ -18,22 +18,22 @@ pub(crate) async fn execute(_args: Args) -> Result<(), NyxChainWatcherError> { let data_dir = Config::default_data_directory(&config_path)?; let builder = ConfigBuilder::new(config_path.clone(), data_dir).with_payment_watcher_config( - PaymentWatcherConfig { - watchers: vec![PaymentWatcherEntry { + PaymentWatchersConfig { + watchers: vec![PaymentWatcherConfig { id: DEFAULT_NYX_CHAIN_WATCHER_ID.to_string(), webhook_url: "https://webhook.site".to_string(), - watch_for_transfer_recipient_accounts: Some(vec![AccountId::from_str( + watch_for_transfer_recipient_accounts: vec![AccountId::from_str( "n17g9a2pwwkg8m60wf59pq6mv0c2wusg9ukparkz", ) - .unwrap()]), + .unwrap()], authentication: Some(AuthorizationBearerToken { token: "1234".to_string(), }), description: None, - watch_for_chain_message_types: Some(vec![ + watch_for_chain_message_types: vec![ "/cosmos.bank.v1beta1.MsgSend".to_string(), "/ibc.applications.transfer.v1.MsgTransfer".to_string(), - ]), + ], }], }, ); diff --git a/nyx-chain-watcher/src/cli/commands/run/args.rs b/nyx-chain-watcher/src/cli/commands/run/args.rs index cc20ae2bac..acc1a04573 100644 --- a/nyx-chain-watcher/src/cli/commands/run/args.rs +++ b/nyx-chain-watcher/src/cli/commands/run/args.rs @@ -20,7 +20,7 @@ pub(crate) struct Args { value_delimiter = ',', env = NYX_CHAIN_WATCHER_WATCH_ACCOUNTS )] - pub watch_for_transfer_recipient_accounts: Option>, + pub watch_for_transfer_recipient_accounts: Vec, /// (Override) Watch for chain messages of these types #[clap( @@ -28,7 +28,7 @@ pub(crate) struct Args { value_delimiter = ',', env = NYX_CHAIN_WATCHER_WATCH_CHAIN_MESSAGE_TYPES )] - pub watch_for_chain_message_types: Option>, + pub watch_for_chain_message_types: Vec, /// (Override) The webhook to call when we find something #[clap( diff --git a/nyx-chain-watcher/src/cli/commands/run/config.rs b/nyx-chain-watcher/src/cli/commands/run/config.rs index 02e5a2fadc..73e923a888 100644 --- a/nyx-chain-watcher/src/cli/commands/run/config.rs +++ b/nyx-chain-watcher/src/cli/commands/run/config.rs @@ -1,7 +1,7 @@ use crate::cli::commands::run::args::Args; use crate::cli::DEFAULT_NYX_CHAIN_WATCHER_ID; -use crate::config::payments_watcher::{HttpAuthenticationOptions, PaymentWatcherEntry}; -use crate::config::{default_config_filepath, Config, ConfigBuilder, PaymentWatcherConfig}; +use crate::config::payments_watcher::{HttpAuthenticationOptions, PaymentWatcherConfig}; +use crate::config::{default_config_filepath, Config, ConfigBuilder, PaymentWatchersConfig}; use crate::error::NyxChainWatcherError; use tracing::{info, warn}; @@ -18,8 +18,8 @@ pub(crate) fn get_run_config(args: Args) -> Result } = args; // if there are no args set, then try load the config - if args.watch_for_transfer_recipient_accounts.is_none() - && args.watch_for_transfer_recipient_accounts.is_none() + if args.watch_for_transfer_recipient_accounts.is_empty() + && args.watch_for_transfer_recipient_accounts.is_empty() && args.chain_watcher_db_path.is_none() { info!("Loading default config file..."); @@ -27,12 +27,12 @@ pub(crate) fn get_run_config(args: Args) -> Result } // set default messages - if watch_for_chain_message_types.is_none() { - watch_for_chain_message_types = Some(vec!["/cosmos.bank.v1beta1.MsgSend".to_string()]); + if watch_for_chain_message_types.is_empty() { + watch_for_chain_message_types = vec!["/cosmos.bank.v1beta1.MsgSend".to_string()]; } // warn if no accounts set - if watch_for_transfer_recipient_accounts.is_none() { + if watch_for_transfer_recipient_accounts.is_empty() { warn!( "You did not specify any accounts to watch in {}. Only chain data will be stored.", crate::env::vars::NYX_CHAIN_WATCHER_WATCH_ACCOUNTS @@ -58,8 +58,8 @@ pub(crate) fn get_run_config(args: Args) -> Result let authentication = webhook_auth.map(|token| HttpAuthenticationOptions::AuthorizationBearerToken { token }); - let watcher_config = PaymentWatcherConfig { - watchers: vec![PaymentWatcherEntry { + let watcher_config = PaymentWatchersConfig { + watchers: vec![PaymentWatcherConfig { id: DEFAULT_NYX_CHAIN_WATCHER_ID.to_string(), description: None, watch_for_transfer_recipient_accounts: watch_for_transfer_recipient_accounts diff --git a/nyx-chain-watcher/src/cli/commands/run/mod.rs b/nyx-chain-watcher/src/cli/commands/run/mod.rs index 2e16a1788d..247b324bf9 100644 --- a/nyx-chain-watcher/src/cli/commands/run/mod.rs +++ b/nyx-chain-watcher/src/cli/commands/run/mod.rs @@ -14,7 +14,9 @@ mod config; use crate::chain_scraper::run_chain_scraper; use crate::db::DbPool; -use crate::{db, http, payment_listener, price_scraper}; +use crate::http::state::PaymentListenerState; +use crate::payment_listener::PaymentListener; +use crate::{db, http, price_scraper}; pub(crate) use args::Args; use nym_task::signal::wait_for_signal; @@ -141,6 +143,9 @@ pub(crate) async fn execute(args: Args, http_port: u16) -> Result<(), NyxChainWa let scraper_pool = storage.pool_owned(); let shutdown_pool = storage.pool_owned(); + // construct shared state + let payment_listener_shared_state = PaymentListenerState::new(); + // spawn all the tasks // 1. chain scraper (note: this doesn't really spawn the full scraper on this task, but we don't want to be blocking waiting for its startup) @@ -156,16 +161,17 @@ pub(crate) async fn execute(args: Args, http_port: u16) -> Result<(), NyxChainWa // 2. payment listener let token = cancellation_token.clone(); let payment_watcher_config = config.payment_watcher_config.clone(); + let payment_listener = PaymentListener::new( + price_scraper_pool, + payment_watcher_config, + payment_listener_shared_state.clone(), + )?; { tasks.spawn(async move { token .run_until_cancelled(async move { - payment_listener::run_payment_listener( - payment_watcher_config, - price_scraper_pool, - ) - .await - .inspect_err(|err| error!("Payment listener error: {err}")) + payment_listener.run().await; + Ok(()) }) .await }); @@ -185,8 +191,13 @@ pub(crate) async fn execute(args: Args, http_port: u16) -> Result<(), NyxChainWa } // 4. http api - let http_server = - http::server::build_http_api(storage.pool_owned(), &config, http_port).await?; + let http_server = http::server::build_http_api( + storage.pool_owned(), + &config, + http_port, + payment_listener_shared_state, + ) + .await?; { let token = cancellation_token.clone(); tasks.spawn(async move { diff --git a/nyx-chain-watcher/src/config/mod.rs b/nyx-chain-watcher/src/config/mod.rs index 3535acce18..ce4e9448a5 100644 --- a/nyx-chain-watcher/src/config/mod.rs +++ b/nyx-chain-watcher/src/config/mod.rs @@ -14,7 +14,7 @@ use tracing::{debug, error}; pub(crate) mod payments_watcher; mod template; -pub use crate::config::payments_watcher::PaymentWatcherConfig; +pub use crate::config::payments_watcher::PaymentWatchersConfig; use crate::error::NyxChainWatcherError; const DEFAULT_NYM_CHAIN_WATCHER_DIR: &str = "nym-chain-watcher"; @@ -46,7 +46,7 @@ pub struct ConfigBuilder { pub chain_scraper_db_path: Option, - pub payment_watcher_config: Option, + pub payment_watcher_config: Option, pub logging: Option, } @@ -76,7 +76,7 @@ impl ConfigBuilder { #[allow(dead_code)] pub fn with_payment_watcher_config( mut self, - payment_watcher_config: impl Into, + payment_watcher_config: impl Into, ) -> Self { self.payment_watcher_config = Some(payment_watcher_config.into()); self @@ -117,7 +117,7 @@ pub struct Config { chain_scraper_db_path: Option, #[serde(default)] - pub payment_watcher_config: PaymentWatcherConfig, + pub payment_watcher_config: PaymentWatchersConfig, #[serde(default)] pub logging: LoggingSettings, diff --git a/nyx-chain-watcher/src/config/payments_watcher.rs b/nyx-chain-watcher/src/config/payments_watcher.rs index 78956d5762..8092b773c7 100644 --- a/nyx-chain-watcher/src/config/payments_watcher.rs +++ b/nyx-chain-watcher/src/config/payments_watcher.rs @@ -2,27 +2,28 @@ use nym_validator_client::nyxd::AccountId; use serde::{Deserialize, Serialize}; #[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct PaymentWatcherConfig { - pub watchers: Vec, +pub struct PaymentWatchersConfig { + pub watchers: Vec, } -impl PaymentWatcherConfig { - pub fn watched_transfer_accounts(&self) -> Vec<&AccountId> { - self.watchers - .iter() - .filter_map(|e| e.watch_for_transfer_recipient_accounts.as_ref()) - .flat_map(|a| a) - .collect() +impl PaymentWatchersConfig { + pub fn is_being_watched(&self, account: &str) -> bool { + self.watchers.iter().any(|watcher| { + watcher + .watch_for_transfer_recipient_accounts + .iter() + .any(|acc| acc.as_ref() == account) + }) } } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PaymentWatcherEntry { +pub struct PaymentWatcherConfig { pub id: String, pub description: Option, pub webhook_url: String, - pub watch_for_transfer_recipient_accounts: Option>, - pub watch_for_chain_message_types: Option>, + pub watch_for_transfer_recipient_accounts: Vec, + pub watch_for_chain_message_types: Vec, pub authentication: Option, } diff --git a/nyx-chain-watcher/src/db/models.rs b/nyx-chain-watcher/src/db/models.rs index 8cddac7f30..b27fcfe34f 100644 --- a/nyx-chain-watcher/src/db/models.rs +++ b/nyx-chain-watcher/src/db/models.rs @@ -1,4 +1,8 @@ +use anyhow::Context; +use nym_validator_client::nyxd::Coin; use serde::{Deserialize, Serialize}; +use sqlx::FromRow; +use time::OffsetDateTime; use utoipa::ToSchema; #[derive(Clone, Deserialize, Debug, ToSchema)] @@ -41,3 +45,25 @@ pub(crate) struct PaymentRecord { pub(crate) timestamp: i64, pub(crate) height: i64, } + +#[derive(Serialize, Deserialize, Debug, FromRow)] +pub(crate) struct Transaction { + pub(crate) id: i64, + pub(crate) tx_hash: String, + pub(crate) height: i64, + pub(crate) message_index: i64, + pub(crate) sender: String, + pub(crate) recipient: String, + pub(crate) amount: String, + pub(crate) memo: Option, + pub(crate) created_at: Option, +} + +impl Transaction { + pub(crate) fn funds(&self) -> anyhow::Result { + self.amount + .as_str() + .parse() + .context("failed to parse transaction amount") + } +} diff --git a/nyx-chain-watcher/src/helpers.rs b/nyx-chain-watcher/src/helpers.rs new file mode 100644 index 0000000000..88f250cf68 --- /dev/null +++ b/nyx-chain-watcher/src/helpers.rs @@ -0,0 +1,55 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use serde::{Deserialize, Serialize}; +use std::collections::vec_deque::{IntoIter, Iter}; +use std::collections::VecDeque; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct RingBuffer { + #[serde(flatten)] + inner: VecDeque, +} + +impl RingBuffer { + pub fn new(capacity: usize) -> Self { + Self { + inner: VecDeque::with_capacity(capacity), + } + } + + pub fn push(&mut self, item: T) { + if self.inner.len() == self.inner.capacity() { + self.inner.pop_front(); + self.inner.push_back(item); + debug_assert!(self.inner.len() == self.inner.capacity()); + } else { + self.inner.push_back(item); + } + } + + pub fn iter(&self) -> Iter<'_, T> { + self.inner.iter() + } +} + +impl From> for VecDeque { + fn from(value: RingBuffer) -> Self { + value.inner + } +} + +impl From> for Vec { + fn from(value: RingBuffer) -> Self { + value.inner.into() + } +} + +impl IntoIterator for RingBuffer { + type Item = T; + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.inner.into_iter() + } +} diff --git a/nyx-chain-watcher/src/http/api/mod.rs b/nyx-chain-watcher/src/http/api/mod.rs index 9e405a439d..6374f4ca9f 100644 --- a/nyx-chain-watcher/src/http/api/mod.rs +++ b/nyx-chain-watcher/src/http/api/mod.rs @@ -8,6 +8,7 @@ use utoipa_swagger_ui::SwaggerUi; use crate::http::{api_docs, server::HttpServer, state::AppState}; pub(crate) mod price; +pub(crate) mod status; pub(crate) mod watcher; pub(crate) struct RouterBuilder { @@ -25,8 +26,13 @@ impl RouterBuilder { "/", axum::routing::get(|| async { Redirect::permanent("/swagger") }), ) - .nest("/v1", Router::new().nest("/price", price::routes())) - .nest("/v1", Router::new().nest("/watcher", watcher::routes())); + .nest( + "/v1", + Router::new() + .nest("/status", status::routes()) + .nest("/price", price::routes()) + .nest("/watcher", watcher::routes()), + ); Self { unfinished_router: router, diff --git a/nyx-chain-watcher/src/http/api/status.rs b/nyx-chain-watcher/src/http/api/status.rs new file mode 100644 index 0000000000..846cefa6fc --- /dev/null +++ b/nyx-chain-watcher/src/http/api/status.rs @@ -0,0 +1,98 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::http::models::status::{ + ActivePaymentWatchersResponse, PaymentListenerFailureDetails, PaymentListenerStatusResponse, + ProcessedPayment, WatcherFailureDetails, WatcherState, +}; +use crate::http::state::{AppState, PaymentListenerState}; +use axum::extract::State; +use axum::routing::get; +use axum::{Json, Router}; +use std::ops::Deref; + +pub(crate) fn routes() -> Router { + Router::new() + .route("/active-payment-watchers", get(active_payment_watchers)) + .route("/payment-listener", get(payment_listener_status)) +} + +#[utoipa::path( + tag = "Status", + get, + path = "/active-payment-watchers", + context_path = "/v1/status", + responses( + (status = 200, body = ActivePaymentWatchersResponse) + ) +)] +pub(crate) async fn active_payment_watchers( + State(state): State, +) -> Json { + Json(ActivePaymentWatchersResponse { + watchers: state.registered_payment_watchers.deref().clone(), + }) +} + +#[utoipa::path( + tag = "Status", + get, + path = "/payment-listener", + context_path = "/v1/status", + responses( + (status = 200, body = PaymentListenerStatusResponse) + ) +)] +pub(crate) async fn payment_listener_status( + State(state): State, +) -> Json { + let guard = state.inner.read().await; + + // sorry for the nasty conversion code here, run out of time : ( + Json(PaymentListenerStatusResponse { + last_checked: guard.last_checked, + processed_payments_since_startup: guard.processed_payments_since_startup, + watcher_errors_since_startup: guard.watcher_errors_since_startup, + payment_listener_errors_since_startup: guard.payment_listener_errors_since_startup, + last_processed_payment: guard + .last_processed_payment + .as_ref() + .map(|p| ProcessedPayment { + processed_at: p.processed_at, + tx_hash: p.tx_hash.clone(), + message_index: p.message_index, + height: p.height, + sender: p.sender.clone(), + receiver: p.receiver.clone(), + funds: p.funds.clone(), + memo: p.memo.clone(), + }), + latest_failures: guard + .latest_failures + .iter() + .map(|f| PaymentListenerFailureDetails { + timestamp: f.timestamp, + error: f.error.clone(), + }) + .collect(), + watchers: guard + .watchers + .iter() + .map(|(w, state)| { + ( + w.clone(), + WatcherState { + latest_failures: state + .latest_failures + .iter() + .map(|f| WatcherFailureDetails { + timestamp: f.timestamp, + error: f.error.clone(), + }) + .collect(), + }, + ) + }) + .collect(), + }) +} diff --git a/nyx-chain-watcher/src/http/api/watcher.rs b/nyx-chain-watcher/src/http/api/watcher.rs index 121c86b91c..585bec1278 100644 --- a/nyx-chain-watcher/src/http/api/watcher.rs +++ b/nyx-chain-watcher/src/http/api/watcher.rs @@ -18,5 +18,5 @@ pub(crate) fn routes() -> Router { /// Fetch the addresses being watched by the chain watcher async fn get_addresses(State(state): State) -> HttpResult>> { - Ok(Json(state.watched_addresses.clone())) + Ok(Json(state.watched_accounts())) } diff --git a/nyx-chain-watcher/src/http/mod.rs b/nyx-chain-watcher/src/http/mod.rs index 1506514f0f..c57d17c47b 100644 --- a/nyx-chain-watcher/src/http/mod.rs +++ b/nyx-chain-watcher/src/http/mod.rs @@ -1,5 +1,6 @@ pub(crate) mod api; pub(crate) mod api_docs; pub(crate) mod error; +pub(crate) mod models; pub(crate) mod server; pub(crate) mod state; diff --git a/nyx-chain-watcher/src/http/models.rs b/nyx-chain-watcher/src/http/models.rs new file mode 100644 index 0000000000..8e7776cf68 --- /dev/null +++ b/nyx-chain-watcher/src/http/models.rs @@ -0,0 +1,92 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +// if we ever create some sort of chain watcher client, those would need to be extracted + +pub mod status { + use crate::config::payments_watcher::PaymentWatcherConfig; + use crate::models::openapi_schema; + use nym_validator_client::nyxd::Coin; + use serde::{Deserialize, Serialize}; + use std::collections::HashMap; + use time::OffsetDateTime; + use utoipa::ToSchema; + + #[derive(Debug, Serialize, Deserialize, ToSchema)] + pub struct ActivePaymentWatchersResponse { + pub watchers: Vec, + } + + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] + pub struct PaymentWatcher { + pub id: String, + pub description: String, + pub webhook_url: String, + pub watched_accounts: Vec, + pub watched_message_types: Vec, + } + + impl From<&PaymentWatcherConfig> for PaymentWatcher { + fn from(value: &PaymentWatcherConfig) -> Self { + PaymentWatcher { + id: value.id.clone(), + description: value.description.clone().unwrap_or_default(), + webhook_url: value.webhook_url.clone(), + watched_accounts: value + .watch_for_transfer_recipient_accounts + .iter() + .map(|a| a.to_string()) + .collect(), + watched_message_types: value.watch_for_chain_message_types.clone(), + } + } + } + + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] + pub struct PaymentListenerStatusResponse { + #[serde(with = "time::serde::rfc3339")] + pub last_checked: OffsetDateTime, + + pub processed_payments_since_startup: u64, + pub watcher_errors_since_startup: u64, + pub payment_listener_errors_since_startup: u64, + + pub last_processed_payment: Option, + + pub latest_failures: Vec, + pub watchers: HashMap, + } + + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] + pub(crate) struct ProcessedPayment { + pub processed_at: OffsetDateTime, + + pub tx_hash: String, + pub message_index: u64, + pub height: u64, + pub sender: String, + pub receiver: String, + + #[schema(value_type = openapi_schema::Coin)] + pub funds: Coin, + + pub memo: String, + } + + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] + pub(crate) struct PaymentListenerFailureDetails { + pub(crate) timestamp: OffsetDateTime, + pub(crate) error: String, + } + + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] + pub(crate) struct WatcherState { + pub(crate) latest_failures: Vec, + } + + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] + pub(crate) struct WatcherFailureDetails { + pub(crate) timestamp: OffsetDateTime, + pub(crate) error: String, + } +} diff --git a/nyx-chain-watcher/src/http/server.rs b/nyx-chain-watcher/src/http/server.rs index 20aa7042c4..a9b095b6ff 100644 --- a/nyx-chain-watcher/src/http/server.rs +++ b/nyx-chain-watcher/src/http/server.rs @@ -4,6 +4,7 @@ use tokio::net::TcpListener; use tokio_util::sync::WaitForCancellationFutureOwned; use crate::config::Config; +use crate::http::state::PaymentListenerState; use crate::{ db::DbPool, http::{api::RouterBuilder, state::AppState}, @@ -13,17 +14,20 @@ pub(crate) async fn build_http_api( db_pool: DbPool, config: &Config, http_port: u16, + payment_listener_state: PaymentListenerState, ) -> anyhow::Result { let router_builder = RouterBuilder::with_default_routes(); - let watched_accounts = config - .payment_watcher_config - .watched_transfer_accounts() - .iter() - .map(|a| a.to_string()) - .collect(); - - let state = AppState::new(db_pool, watched_accounts); + let state = AppState::new( + db_pool, + config + .payment_watcher_config + .watchers + .iter() + .map(Into::into) + .collect(), + payment_listener_state, + ); let router = router_builder.with_state(state); let bind_addr = format!("0.0.0.0:{}", http_port); diff --git a/nyx-chain-watcher/src/http/state.rs b/nyx-chain-watcher/src/http/state.rs index 99fc155b74..ff230cf242 100644 --- a/nyx-chain-watcher/src/http/state.rs +++ b/nyx-chain-watcher/src/http/state.rs @@ -1,20 +1,183 @@ use crate::db::DbPool; +use crate::helpers::RingBuffer; +use crate::http::models::status::PaymentWatcher; +use crate::models::WebhookPayload; +use axum::extract::FromRef; +use nym_validator_client::nyxd::Coin; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; +use time::OffsetDateTime; +use tokio::sync::RwLock; #[derive(Debug, Clone)] pub(crate) struct AppState { db_pool: DbPool, - pub(crate) watched_addresses: Vec, + pub(crate) registered_payment_watchers: Arc>, + pub(crate) payment_listener_state: PaymentListenerState, } impl AppState { - pub(crate) fn new(db_pool: DbPool, watched_addresses: Vec) -> Self { + pub(crate) fn new( + db_pool: DbPool, + registered_payment_watchers: Vec, + payment_listener_state: PaymentListenerState, + ) -> Self { Self { db_pool, - watched_addresses, + registered_payment_watchers: Arc::new(registered_payment_watchers), + payment_listener_state, } } pub(crate) fn db_pool(&self) -> &DbPool { &self.db_pool } + + pub(crate) fn watched_accounts(&self) -> Vec { + self.registered_payment_watchers + .iter() + .flat_map(|w| w.watched_accounts.iter()) + .cloned() + .collect() + } +} + +#[derive(Debug, Clone)] +pub(crate) struct PaymentListenerState { + pub(crate) inner: Arc>, +} + +impl PaymentListenerState { + // TODO: make those configurable + const MAX_WATCHER_FAILURES: usize = 20; + const MAX_PAYMENT_LISTENER_FAILURES: usize = 50; + + pub(crate) fn new() -> Self { + PaymentListenerState { + inner: Arc::new(RwLock::new(PaymentListenerStateInner { + last_checked: OffsetDateTime::UNIX_EPOCH, + processed_payments_since_startup: 0, + watcher_errors_since_startup: 0, + payment_listener_errors_since_startup: 0, + last_processed_payment: None, + latest_failures: RingBuffer::new(Self::MAX_PAYMENT_LISTENER_FAILURES), + watchers: Default::default(), + })), + } + } + + pub(crate) async fn insert_listener_failure(&self, failure: PaymentListenerFailureDetails) { + let mut guard = self.inner.write().await; + + guard.payment_listener_errors_since_startup += 1; + guard.latest_failures.push(failure); + } + + pub(crate) async fn insert_watcher_failure(&self, id: &str, failure: WatcherFailureDetails) { + self.inner + .write() + .await + .watchers + .entry(id.to_string()) + .or_insert(WatcherState { + latest_failures: RingBuffer::new(Self::MAX_WATCHER_FAILURES), + }) + .latest_failures + .push(failure); + } + + pub(crate) async fn processed_payment_transaction(&self, payment: ProcessedPayment) { + let mut guard = self.inner.write().await; + + guard.processed_payments_since_startup += 1; + guard.last_processed_payment = Some(payment) + } + + pub(crate) async fn update_last_checked(&self) { + self.inner.write().await.last_checked = OffsetDateTime::now_utc(); + } +} + +impl FromRef for PaymentListenerState { + fn from_ref(input: &AppState) -> Self { + input.payment_listener_state.clone() + } +} + +#[derive(Debug)] +pub(crate) struct PaymentListenerStateInner { + pub(crate) last_checked: OffsetDateTime, + + pub(crate) processed_payments_since_startup: u64, + pub(crate) watcher_errors_since_startup: u64, + pub(crate) payment_listener_errors_since_startup: u64, + + pub(crate) last_processed_payment: Option, + + pub(crate) latest_failures: RingBuffer, + pub(crate) watchers: HashMap, +} + +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct ProcessedPayment { + pub processed_at: OffsetDateTime, + + pub tx_hash: String, + pub message_index: u64, + pub height: u64, + pub sender: String, + pub receiver: String, + pub funds: Coin, + pub memo: String, +} + +impl From for ProcessedPayment { + fn from(payload: WebhookPayload) -> Self { + ProcessedPayment { + processed_at: OffsetDateTime::now_utc(), + tx_hash: payload.transaction_hash, + message_index: payload.message_index, + height: payload.height as u64, + sender: payload.sender_address, + receiver: payload.receiver_address, + funds: payload.funds.into(), + memo: payload.memo.unwrap_or_default(), + } + } +} + +#[derive(Debug, Clone)] +pub(crate) struct PaymentListenerFailureDetails { + pub(crate) timestamp: OffsetDateTime, + pub(crate) error: String, +} + +impl PaymentListenerFailureDetails { + pub(crate) fn new(error: String) -> Self { + PaymentListenerFailureDetails { + timestamp: OffsetDateTime::now_utc(), + error, + } + } +} + +#[derive(Debug)] +pub(crate) struct WatcherState { + pub(crate) latest_failures: RingBuffer, +} + +#[derive(Debug)] +pub(crate) struct WatcherFailureDetails { + pub(crate) timestamp: OffsetDateTime, + pub(crate) error: String, +} + +impl WatcherFailureDetails { + pub(crate) fn new(error: String) -> Self { + WatcherFailureDetails { + timestamp: OffsetDateTime::now_utc(), + error, + } + } } diff --git a/nyx-chain-watcher/src/main.rs b/nyx-chain-watcher/src/main.rs index e6c3f8ea26..96b531ddba 100644 --- a/nyx-chain-watcher/src/main.rs +++ b/nyx-chain-watcher/src/main.rs @@ -10,6 +10,7 @@ mod config; mod db; mod env; mod error; +pub(crate) mod helpers; mod http; mod logging; pub mod models; diff --git a/nyx-chain-watcher/src/models.rs b/nyx-chain-watcher/src/models.rs index ce1c0886f4..1fc0ca2636 100644 --- a/nyx-chain-watcher/src/models.rs +++ b/nyx-chain-watcher/src/models.rs @@ -1,6 +1,6 @@ use nym_validator_client::nyxd::CosmWasmCoin; -use rocket::serde::{Deserialize, Serialize}; use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; use utoipa::ToSchema; #[derive(Serialize, Deserialize, Clone, JsonSchema, ToSchema)] diff --git a/nyx-chain-watcher/src/payment_listener/mod.rs b/nyx-chain-watcher/src/payment_listener/mod.rs index 826f23dfd8..cd7a100cb7 100644 --- a/nyx-chain-watcher/src/payment_listener/mod.rs +++ b/nyx-chain-watcher/src/payment_listener/mod.rs @@ -1,108 +1,151 @@ -use crate::config::payments_watcher::HttpAuthenticationOptions; -use crate::config::PaymentWatcherConfig; +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::PaymentWatchersConfig; +use crate::db::models::Transaction; use crate::db::queries; +use crate::http::state::{ + PaymentListenerFailureDetails, PaymentListenerState, ProcessedPayment, WatcherFailureDetails, +}; use crate::models::WebhookPayload; -use nym_validator_client::nyxd::Coin; -use reqwest::Client; +use crate::payment_listener::watcher::PaymentWatcher; +use anyhow::Context; use sqlx::SqlitePool; -use std::str::FromStr; use tokio::time::{self, Duration}; -use tracing::{error, info}; +use tracing::{debug, error, info}; -pub(crate) async fn run_payment_listener( - payment_watcher_config: PaymentWatcherConfig, - watcher_pool: SqlitePool, -) -> anyhow::Result<()> { - let client = Client::new(); +pub(crate) mod watcher; - loop { - // 1. get the last height this watcher ran at - let last_checked_height = queries::payments::get_last_checked_height(&watcher_pool).await?; - info!("Last checked height: {}", last_checked_height); +pub(crate) struct PaymentListener { + db_pool: SqlitePool, + payment_watchers: Vec, + shared_state: PaymentListenerState, +} - // 2. iterate through watchers - for watcher in &payment_watcher_config.watchers { - if watcher.watch_for_transfer_recipient_accounts.is_some() { - // 3. Query new transactions for this watcher's recipient accounts - let transactions = sqlx::query!( - r#" - SELECT * FROM transactions - WHERE height > ? - ORDER BY height ASC, message_index ASC - "#, - last_checked_height - ) - .fetch_all(&watcher_pool) - .await?; +impl PaymentListener { + pub(crate) fn new( + db_pool: SqlitePool, + config: PaymentWatchersConfig, + shared_state: PaymentListenerState, + ) -> anyhow::Result { + Ok(PaymentListener { + db_pool, + payment_watchers: config + .watchers + .iter() + .map(|watcher_cfg| PaymentWatcher::new(watcher_cfg.clone())) + .collect::>>()?, + shared_state, + }) + } - if !transactions.is_empty() { - info!( - "[watcher = {}] Processing {} transactions", - watcher.id, - transactions.len() - ); - } + async fn retrieve_unprocessed_transactions(&self) -> anyhow::Result> { + let last_checked_height = queries::payments::get_last_checked_height(&self.db_pool).await?; + let txs = sqlx::query_as!( + Transaction, + r#" + SELECT * FROM transactions + WHERE height > ? + ORDER BY height ASC, message_index ASC + "#, + last_checked_height + ) + .fetch_all(&self.db_pool) + .await?; - for tx in transactions { - let funds = Coin::from_str(&tx.amount)?; - let amount: f64 = funds.amount as f64 / 1e6f64; // convert to major value, there will be precision loss + Ok(txs) + } - // Store transaction hash for later use - let tx_hash = tx.tx_hash.clone(); - let message_index = tx.message_index; + async fn process_transaction(&self, tx: Transaction) -> anyhow::Result<()> { + // 3.1 process any payments + let funds = tx.funds()?; + let amount: f64 = funds.amount as f64 / 1e6f64; // convert to major value, there will be precision loss - queries::payments::insert_payment( - &watcher_pool, - tx.tx_hash, - tx.sender.clone(), - tx.recipient.clone(), - amount, - tx.height, - tx.memo.clone(), + // TODO: FIXME: it may happen that we insert a payment but fail to invoke all webhooks + + queries::payments::insert_payment( + &self.db_pool, + tx.tx_hash.clone(), + tx.sender.clone(), + tx.recipient.clone(), + amount, + tx.height, + tx.memo.clone(), + ) + .await?; + + // 3.1. invoke all relevant webhooks for all registered watchers + let webhook_data = WebhookPayload { + transaction_hash: tx.tx_hash, + message_index: tx.message_index as u64, + sender_address: tx.sender, + receiver_address: tx.recipient, + funds: funds.into(), + height: tx.height as u128, + memo: tx.memo, + }; + + for watcher in &self.payment_watchers { + if let Err(err) = watcher.invoke_webhook(&webhook_data).await { + error!("watcher {} failure: {err:#}", watcher.id()); + self.shared_state + .insert_watcher_failure( + watcher.id(), + WatcherFailureDetails::new(err.to_string()), ) - .await?; - - let webhook_data = WebhookPayload { - transaction_hash: tx_hash.clone(), - message_index: message_index as u64, - sender_address: tx.sender, - receiver_address: tx.recipient, - funds: funds.into(), - height: tx.height as u128, - memo: tx.memo, - }; - - let mut request_builder = client.post(&watcher.webhook_url).json(&webhook_data); - - if let Some(auth) = &watcher.authentication { - match auth { - HttpAuthenticationOptions::AuthorizationBearerToken { token } => { - request_builder = request_builder.bearer_auth(token); - } - } - } - - match request_builder.send().await { - Ok(res) => info!( - "[watcher = {}] ✅ Webhook {} {} - tx {}, index {}", - watcher.id, - res.status(), - res.url(), - tx_hash, - message_index, - ), - Err(e) => error!( - "[watcher = {}] ❌ Webhook {:?} {:?} error = {}", - watcher.id, - e.status(), - e.url(), - e, - ), - } - } + .await } } - time::sleep(Duration::from_secs(10)).await; + self.shared_state + .processed_payment_transaction(ProcessedPayment::from(webhook_data)) + .await; + + Ok(()) + } + + async fn check_for_unprocessed_payments(&self) -> anyhow::Result<()> { + // 1. retrieve any unprocessed transactions + let unprocessed_transactions = self + .retrieve_unprocessed_transactions() + .await + .context("failed to retrieve unprocessed transactions")?; + + if unprocessed_transactions.is_empty() { + debug!("no payment transactions to process."); + return Ok(()); + } else { + info!( + "processing {} payment transactions", + unprocessed_transactions.len() + ); + } + + // 2. attempt to process them + for tx in unprocessed_transactions { + let hash = tx.tx_hash.clone(); + let height = tx.height; + self.process_transaction(tx).await.with_context(|| { + format!("failed to process transaction {hash} at height {height}") + })?; + } + + Ok(()) + } + + pub(crate) async fn run(&self) { + loop { + time::sleep(Duration::from_secs(10)).await; + + if let Err(err) = self.check_for_unprocessed_payments().await { + error!("failed to fully process payments: {err:#}"); + self.shared_state + .insert_listener_failure(PaymentListenerFailureDetails::new(err.to_string())) + .await; + continue; + } + + self.shared_state.update_last_checked().await; + } } } diff --git a/nyx-chain-watcher/src/payment_listener/watcher.rs b/nyx-chain-watcher/src/payment_listener/watcher.rs new file mode 100644 index 0000000000..5feddfb39b --- /dev/null +++ b/nyx-chain-watcher/src/payment_listener/watcher.rs @@ -0,0 +1,73 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::payments_watcher::{HttpAuthenticationOptions, PaymentWatcherConfig}; +use crate::models::WebhookPayload; +use anyhow::Context; +use reqwest::{Client, Url}; +use tracing::{error, info}; + +pub(crate) struct PaymentWatcher { + webhook_url: Url, + config: PaymentWatcherConfig, +} + +impl PaymentWatcher { + pub(crate) fn new(config: PaymentWatcherConfig) -> anyhow::Result { + Ok(PaymentWatcher { + webhook_url: config + .webhook_url + .as_str() + .parse() + .context("couldn't create payment watcher: provided webhook URL is malformed")?, + config, + }) + } + + pub(super) fn id(&self) -> &str { + &self.config.id + } + + pub(crate) async fn invoke_webhook(&self, payload: &WebhookPayload) -> anyhow::Result<()> { + let client = Client::builder() + .user_agent(format!( + "nyx-chain-watcher/{}/watcher-{}", + env!("CARGO_PKG_VERSION"), + self.config.id + )) + .build() + .context("failed to build reqwest client")?; + + let mut request_builder = client.post(self.webhook_url.clone()).json(payload); + + if let Some(auth) = &self.config.authentication { + match auth { + HttpAuthenticationOptions::AuthorizationBearerToken { token } => { + request_builder = request_builder.bearer_auth(token); + } + } + } + + match request_builder.send().await { + Ok(res) => info!( + "[watcher = {}] ✅ Webhook {} {} - tx {}, index {}", + self.config.id, + res.status(), + res.url(), + payload.transaction_hash, + payload.message_index, + ), + Err(err) => { + error!( + "[watcher = {}] ❌ Webhook {:?} {:?} error = {err}", + self.config.id, + err.status(), + err.url(), + ); + return Err(err.into()); + } + } + + Ok(()) + } +} From c2c3df98cb5beacb30af54e5b2d96449d3a8f60c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 10 Mar 2025 17:28:24 +0000 Subject: [PATCH 064/133] updated payment watcher version --- nyx-chain-watcher/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nyx-chain-watcher/Cargo.toml b/nyx-chain-watcher/Cargo.toml index bbe9ecc98d..f17233f96b 100644 --- a/nyx-chain-watcher/Cargo.toml +++ b/nyx-chain-watcher/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nyx-chain-watcher" -version = "0.1.12" +version = "0.1.13" authors.workspace = true repository.workspace = true homepage.workspace = true From 2d00fcd9346e5964984c44a2ba7ae0345d37b511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Tue, 11 Mar 2025 08:35:40 +0100 Subject: [PATCH 065/133] Allow resetting all SURB sender tags (#5600) * Allow resetting all SURB sender tags * wasm fixes * More wasm fixes --- common/client-core/config-types/src/lib.rs | 4 ++++ .../src/client/base_client/non_wasm_helpers.rs | 2 +- .../surb-storage/src/backend/fs_backend/mod.rs | 10 ++++++++-- common/wasm/client-core/src/config/mod.rs | 5 +++++ common/wasm/client-core/src/config/override.rs | 4 ++++ 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/common/client-core/config-types/src/lib.rs b/common/client-core/config-types/src/lib.rs index 8eb95d62f1..3af0746a8b 100644 --- a/common/client-core/config-types/src/lib.rs +++ b/common/client-core/config-types/src/lib.rs @@ -658,6 +658,9 @@ pub struct ReplySurbs { /// Specifies the number of mixnet hops the packet should go through. If not specified, then /// the default value is used. pub surb_mix_hops: Option, + + /// Specifies if we should reset all the sender tags on startup + pub fresh_sender_tags: bool, } impl Default for ReplySurbs { @@ -675,6 +678,7 @@ impl Default for ReplySurbs { maximum_reply_surb_age: DEFAULT_MAXIMUM_REPLY_SURB_AGE, maximum_reply_key_age: DEFAULT_MAXIMUM_REPLY_KEY_AGE, surb_mix_hops: None, + fresh_sender_tags: false, } } } diff --git a/common/client-core/src/client/base_client/non_wasm_helpers.rs b/common/client-core/src/client/base_client/non_wasm_helpers.rs index ce3f05b0f2..e7ef621c9d 100644 --- a/common/client-core/src/client/base_client/non_wasm_helpers.rs +++ b/common/client-core/src/client/base_client/non_wasm_helpers.rs @@ -88,7 +88,7 @@ pub async fn setup_fs_reply_surb_backend>( let db_path = db_path.as_ref(); if db_path.exists() { info!("loading existing surb database"); - match fs_backend::Backend::try_load(db_path).await { + match fs_backend::Backend::try_load(db_path, surb_config.fresh_sender_tags).await { Ok(backend) => Ok(backend), Err(err) => { error!("failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future"); diff --git a/common/client-core/surb-storage/src/backend/fs_backend/mod.rs b/common/client-core/surb-storage/src/backend/fs_backend/mod.rs index 4914f6621f..4467e17538 100644 --- a/common/client-core/surb-storage/src/backend/fs_backend/mod.rs +++ b/common/client-core/surb-storage/src/backend/fs_backend/mod.rs @@ -10,7 +10,7 @@ use crate::{ CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys, UsedSenderTags, }; use async_trait::async_trait; -use log::{error, info, warn}; +use log::{debug, error, info, warn}; use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; use std::fs; use std::path::{Path, PathBuf}; @@ -52,7 +52,10 @@ impl Backend { Ok(backend) } - pub async fn try_load>(database_path: P) -> Result { + pub async fn try_load>( + database_path: P, + fresh_sender_tags: bool, + ) -> Result { let owned_path: PathBuf = database_path.as_ref().into(); if owned_path.file_name().is_none() { return Err(StorageError::DatabasePathWithoutFilename { @@ -118,6 +121,9 @@ impl Backend { if days > 2 { info!("it's been over {days} days and {hours} hours since we last used our data store. our used sender tags are already outdated - we're going to purge them now."); manager.delete_all_tags().await?; + } else if fresh_sender_tags { + debug!("starting with fresh sender tags"); + manager.delete_all_tags().await?; } Ok(Backend { diff --git a/common/wasm/client-core/src/config/mod.rs b/common/wasm/client-core/src/config/mod.rs index 64b8be5d40..b038e20737 100644 --- a/common/wasm/client-core/src/config/mod.rs +++ b/common/wasm/client-core/src/config/mod.rs @@ -494,6 +494,9 @@ pub struct ReplySurbsWasm { /// Defines how many mix nodes the reply surb should go through. /// If not set, the default value is going to be used. pub surb_mix_hops: Option, + + /// Specifies if we should reset all the sender tags on startup + pub fresh_sender_tags: bool, } impl Default for ReplySurbsWasm { @@ -525,6 +528,7 @@ impl From for ConfigReplySurbs { reply_surbs.maximum_reply_key_age_ms as u64, ), surb_mix_hops: reply_surbs.surb_mix_hops, + fresh_sender_tags: reply_surbs.fresh_sender_tags, } } } @@ -548,6 +552,7 @@ impl From for ReplySurbsWasm { maximum_reply_surb_age_ms: reply_surbs.maximum_reply_surb_age.as_millis() as u32, maximum_reply_key_age_ms: reply_surbs.maximum_reply_key_age.as_millis() as u32, surb_mix_hops: reply_surbs.surb_mix_hops, + fresh_sender_tags: reply_surbs.fresh_sender_tags, } } } diff --git a/common/wasm/client-core/src/config/override.rs b/common/wasm/client-core/src/config/override.rs index 72da8bd43d..bc9275660a 100644 --- a/common/wasm/client-core/src/config/override.rs +++ b/common/wasm/client-core/src/config/override.rs @@ -378,6 +378,9 @@ pub struct ReplySurbsWasmOverride { #[tsify(optional)] pub surb_mix_hops: Option, + + /// Specifies if we should reset all the sender tags on startup + pub fresh_sender_tags: bool, } impl From for ReplySurbsWasm { @@ -416,6 +419,7 @@ impl From for ReplySurbsWasm { .maximum_reply_key_age_ms .unwrap_or(def.maximum_reply_key_age_ms), surb_mix_hops: value.surb_mix_hops, + fresh_sender_tags: value.fresh_sender_tags, } } } From 933769401c32cb0da4410fe2ce20d276c56fd76c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Mar 2025 08:07:30 +0000 Subject: [PATCH 066/133] build(deps): bump tokio from 1.43.0 to 1.44.0 Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.43.0 to 1.44.0. - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.43.0...tokio-1.44.0) --- updated-dependencies: - dependency-name: tokio dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e0cc22c617..4e91754b7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10334,9 +10334,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.43.0" +version = "1.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a" dependencies = [ "backtrace", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 2f6585f79f..e934f39bab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -333,7 +333,7 @@ tar = "0.4.44" tempfile = "3.18" thiserror = "2.0" time = "0.3.39" -tokio = "1.43" +tokio = "1.44" tokio-postgres = "0.7" tokio-stream = "0.1.17" tokio-test = "0.4.4" From 9e84b1f0c1e66c12d55d23e53fe56c015c465e06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 11 Mar 2025 09:33:44 +0000 Subject: [PATCH 067/133] ci clippy --- Cargo.lock | 2 +- contracts/Cargo.lock | 24 +++++++------- nym-wallet/Cargo.lock | 32 +++++++++---------- ...4d4ea4e55c2a9c4d4f7e1c7e57bcb848ee08.json} | 6 ++-- nyx-chain-watcher/src/payment_listener/mod.rs | 3 +- 5 files changed, 34 insertions(+), 33 deletions(-) rename nyx-chain-watcher/.sqlx/{query-2e0fd886f2c68b105f5e609032bfb3b637383b370c24566497bcc5ea625e82db.json => query-f69907735e9b1e1572c4bf6fe8d44d4ea4e55c2a9c4d4f7e1c7e57bcb848ee08.json} (71%) diff --git a/Cargo.lock b/Cargo.lock index 51ac8f5c4f..c68034a9ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7222,7 +7222,7 @@ dependencies = [ [[package]] name = "nyx-chain-watcher" -version = "0.1.12" +version = "0.1.13" dependencies = [ "anyhow", "async-trait", diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 84d01817de..af830109c4 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -1522,18 +1522,18 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" dependencies = [ "serde", ] [[package]] name = "serde" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] @@ -1558,9 +1558,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -1777,9 +1777,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.37" +version = "0.3.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" dependencies = [ "deranged", "itoa", @@ -1794,15 +1794,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" dependencies = [ "num-conv", "time-core", diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 35379a35ef..0e40335742 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -1583,9 +1583,9 @@ dependencies = [ [[package]] name = "ff" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ "rand_core 0.6.4", "subtle", @@ -5059,18 +5059,18 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" dependencies = [ "serde", ] [[package]] name = "serde" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] @@ -5086,18 +5086,18 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.16" +version = "0.11.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "364fec0df39c49a083c9a8a18a23a6bcfd9af130fe9fe321d18520a0d113e09e" +checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -6052,9 +6052,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.37" +version = "0.3.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" dependencies = [ "deranged", "itoa 1.0.9", @@ -6069,15 +6069,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" dependencies = [ "num-conv", "time-core", diff --git a/nyx-chain-watcher/.sqlx/query-2e0fd886f2c68b105f5e609032bfb3b637383b370c24566497bcc5ea625e82db.json b/nyx-chain-watcher/.sqlx/query-f69907735e9b1e1572c4bf6fe8d44d4ea4e55c2a9c4d4f7e1c7e57bcb848ee08.json similarity index 71% rename from nyx-chain-watcher/.sqlx/query-2e0fd886f2c68b105f5e609032bfb3b637383b370c24566497bcc5ea625e82db.json rename to nyx-chain-watcher/.sqlx/query-f69907735e9b1e1572c4bf6fe8d44d4ea4e55c2a9c4d4f7e1c7e57bcb848ee08.json index 327bcd47b6..bd0a1777e5 100644 --- a/nyx-chain-watcher/.sqlx/query-2e0fd886f2c68b105f5e609032bfb3b637383b370c24566497bcc5ea625e82db.json +++ b/nyx-chain-watcher/.sqlx/query-f69907735e9b1e1572c4bf6fe8d44d4ea4e55c2a9c4d4f7e1c7e57bcb848ee08.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT * FROM transactions\n WHERE height > ?\n ORDER BY height ASC, message_index ASC\n ", + "query": "\n SELECT id, tx_hash, height, message_index, sender, recipient, amount, memo, created_at as \"created_at: ::time::OffsetDateTime\"\n FROM transactions\n WHERE height > ?\n ORDER BY height ASC, message_index ASC\n ", "describe": { "columns": [ { @@ -44,7 +44,7 @@ "type_info": "Text" }, { - "name": "created_at", + "name": "created_at: ::time::OffsetDateTime", "ordinal": 8, "type_info": "Datetime" } @@ -64,5 +64,5 @@ true ] }, - "hash": "2e0fd886f2c68b105f5e609032bfb3b637383b370c24566497bcc5ea625e82db" + "hash": "f69907735e9b1e1572c4bf6fe8d44d4ea4e55c2a9c4d4f7e1c7e57bcb848ee08" } diff --git a/nyx-chain-watcher/src/payment_listener/mod.rs b/nyx-chain-watcher/src/payment_listener/mod.rs index cd7a100cb7..f941e6c012 100644 --- a/nyx-chain-watcher/src/payment_listener/mod.rs +++ b/nyx-chain-watcher/src/payment_listener/mod.rs @@ -44,7 +44,8 @@ impl PaymentListener { let txs = sqlx::query_as!( Transaction, r#" - SELECT * FROM transactions + SELECT id, tx_hash, height, message_index, sender, recipient, amount, memo, created_at as "created_at: ::time::OffsetDateTime" + FROM transactions WHERE height > ? ORDER BY height ASC, message_index ASC "#, From f724478763fdc885255fb076021e545a3b764470 Mon Sep 17 00:00:00 2001 From: import this <97586125+serinko@users.noreply.github.com> Date: Tue, 11 Mar 2025 11:18:18 +0000 Subject: [PATCH 068/133] [DOCs/operators]: Add steps to synchronize server time, using NTP (#5603) --- .../operators/snippets/ntp-time-sync.mdx | 10 ++++++++++ .../nyx-outputs/circulating-supply.md | 2 +- .../nyx-outputs/nyx-percent-stake.md | 2 +- .../nyx-outputs/nyx-total-stake.md | 2 +- .../nyx-outputs/stake-saturation.md | 2 +- .../nyx-outputs/staking-target.md | 2 +- .../nyx-outputs/token-table.md | 6 +++--- .../outputs/api-scraping-outputs/time-now.md | 2 +- .../nodes/preliminary-steps/vps-setup.mdx | 15 ++++++++++----- 9 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 documentation/docs/components/operators/snippets/ntp-time-sync.mdx diff --git a/documentation/docs/components/operators/snippets/ntp-time-sync.mdx b/documentation/docs/components/operators/snippets/ntp-time-sync.mdx new file mode 100644 index 0000000000..945a39470a --- /dev/null +++ b/documentation/docs/components/operators/snippets/ntp-time-sync.mdx @@ -0,0 +1,10 @@ +- Run synchronization against [pool.ntp.org](https://www.ntppool.org/en/): +```bash +ntpdate -q pool.ntp.org +``` +- Enable `ntp` service, start and review the status: +```bash +systemctl enable --now ntp +service ntp start +service ntp status +``` diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md index 16f5edc4de..f71e380d34 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md @@ -1 +1 @@ -807_251_217 +808_623_916 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md index 39bbb86bce..a0b4b18b43 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md @@ -1 +1 @@ -0.64% +0.68% diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md index e36dbca0e2..05292828a2 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md @@ -1 +1 @@ -44.811 +42.157 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md index 11c0eeb3d3..6a6f41769e 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md @@ -1 +1 @@ -1_025_628 +1_028_488 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md index 8bf62f72c2..dd8fe066d0 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md @@ -1 +1 @@ -403_625_608 +404_311_958 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md index dabaa45e3a..d4a66cde69 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md @@ -1,7 +1,7 @@ | **Item** | **Description** | **Amount in NYM** | |:-------------------|:------------------------------------------------------|--------------------:| | Total Supply | Maximum amount of NYM token in existence | 1_000_000_000 | -| Mixmining Reserve | Tokens releasing for operators rewards | 192_748_782 | +| Mixmining Reserve | Tokens releasing for operators rewards | 191_376_083 | | Vesting Tokens | Tokens locked outside of cicrulation for future claim | 0 | -| Circulating Supply | Amount of unlocked tokens | 807_251_217 | -| Stake Saturation | Optimal size of node self-bond + delegation | 1_025_628 | +| Circulating Supply | Amount of unlocked tokens | 808_623_916 | +| Stake Saturation | Optimal size of node self-bond + delegation | 1_028_488 | diff --git a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md index 07d784ff82..dddd026436 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md @@ -1 +1 @@ -Wednesday, February 26th 2025, 16:02:47 UTC +Tuesday, March 11th 2025, 11:04:18 UTC diff --git a/documentation/docs/pages/operators/nodes/preliminary-steps/vps-setup.mdx b/documentation/docs/pages/operators/nodes/preliminary-steps/vps-setup.mdx index 3488295629..5cba5bdbed 100644 --- a/documentation/docs/pages/operators/nodes/preliminary-steps/vps-setup.mdx +++ b/documentation/docs/pages/operators/nodes/preliminary-steps/vps-setup.mdx @@ -3,8 +3,9 @@ import { VarInfo } from 'components/variable-info.tsx'; import { Steps } from 'nextra/components';import { Tabs } from 'nextra/components'; import { MyTab } from 'components/generic-tabs.tsx'; import PortsNymNode from 'components/operators/snippets/ports-nym-node.mdx'; -import PortsValidator from 'components/operators/snippets/ports-validator.mdx' -import NymNodeSpecs from 'components/operators/snippets/nym-node-specs.mdx' +import PortsValidator from 'components/operators/snippets/ports-validator.mdx'; +import NymNodeSpecs from 'components/operators/snippets/nym-node-specs.mdx'; +import NTPSync from 'components/operators/snippets/ntp-time-sync.mdx' # VPS Setup & Configuration @@ -42,14 +43,18 @@ apt update -y && apt --fix-broken install ``` - Install dependencies ```sh -apt -y install ca-certificates jq curl wget ufw jq tmux pkg-config build-essential libssl-dev git +apt -y install ca-certificates jq curl wget ufw jq tmux pkg-config build-essential libssl-dev git ntp ntpdate ``` - Double check ufw is installed correctly ```sh apt install ufw --fix-missing ``` -###### 2. Configure your firewall using Uncomplicated Firewall (UFW) +###### 2. Synchronize time of your server + + + +###### 3. Configure your firewall using Uncomplicated Firewall (UFW) For a `nym-node` or Nyx validator to recieve traffic, you need to open ports on the server. The following commands will allow you to set up a firewall using `ufw`. @@ -70,7 +75,7 @@ ufw enable ufw status ``` -###### 3. Open all needed ports to have your firewall for `nym-node` working correctly +###### 4. Open all needed ports to have your firewall for `nym-node` working correctly
Date: Tue, 11 Mar 2025 16:02:00 +0100 Subject: [PATCH 069/133] Merge pull request #5541 from nymtech/dependabot/cargo/rs_merkle-1.5.0 build(deps): bump rs_merkle from 1.4.2 to 1.5.0 --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 568f9e55b0..295bf51f44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8605,9 +8605,9 @@ dependencies = [ [[package]] name = "rs_merkle" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b241d2e59b74ef9e98d94c78c47623d04c8392abaf82014dfd372a16041128f" +checksum = "bb09b49230ba22e8c676e7b75dfe2887dea8121f18b530ae0ba519ce442d2b21" dependencies = [ "sha2 0.10.8", ] diff --git a/Cargo.toml b/Cargo.toml index ba1acb4468..b6ee317bfe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -308,7 +308,7 @@ reqwest = { version = "0.12.4", default-features = false } rocket = "0.5.0" rocket_cors = "0.6.0" rocket_okapi = "0.8.0" -rs_merkle = "1.4.2" +rs_merkle = "1.5.0" safer-ffi = "0.1.13" schemars = "0.8.22" semver = "1.0.26" From 6194ac07b83f83bf2bc090168ac3fccc7e554716 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 10:57:02 +0000 Subject: [PATCH 070/133] build(deps): bump ring from 0.17.3 to 0.17.13 in /nym-wallet (#5582) Bumps [ring](https://github.com/briansmith/ring) from 0.17.3 to 0.17.13. - [Changelog](https://github.com/briansmith/ring/blob/main/RELEASES.md) - [Commits](https://github.com/briansmith/ring/commits) --- updated-dependencies: - dependency-name: ring dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- nym-wallet/Cargo.lock | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 1ad18262e2..6708de9c7f 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -540,11 +540,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.82" +version = "1.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" dependencies = [ - "libc", + "shlex", ] [[package]] @@ -4760,16 +4760,16 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.3" +version = "0.17.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babe80d5c16becf6594aa32ad2be8fe08498e7ae60b77de8df700e67f191d7e" +checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee" dependencies = [ "cc", + "cfg-if", "getrandom 0.2.10", "libc", - "spin", "untrusted", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -5270,6 +5270,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -5364,12 +5370,6 @@ dependencies = [ "system-deps 5.0.0", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "spki" version = "0.7.2" @@ -6114,9 +6114,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.43.0" +version = "1.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a" dependencies = [ "backtrace", "bytes", From 4588a3036e30e35d9402ba01e0cb1a91fed2993e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 10:58:41 +0000 Subject: [PATCH 071/133] build(deps): bump webpack-dev-middleware in /wasm/zknym-lib/internal-dev (#5589) Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 5.3.3 to 5.3.4. - [Release notes](https://github.com/webpack/webpack-dev-middleware/releases) - [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md) - [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.3...v5.3.4) --- updated-dependencies: - dependency-name: webpack-dev-middleware dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- wasm/zknym-lib/internal-dev/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wasm/zknym-lib/internal-dev/yarn.lock b/wasm/zknym-lib/internal-dev/yarn.lock index 73b5db4071..1ad31e3d74 100644 --- a/wasm/zknym-lib/internal-dev/yarn.lock +++ b/wasm/zknym-lib/internal-dev/yarn.lock @@ -2057,9 +2057,9 @@ webpack-cli@^4.9.2: webpack-merge "^5.7.3" webpack-dev-middleware@^5.3.1: - version "5.3.3" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz#efae67c2793908e7311f1d9b06f2a08dcc97e51f" - integrity sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA== + version "5.3.4" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517" + integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== dependencies: colorette "^2.0.10" memfs "^3.4.3" From 3ee73d541e8690ae9505aef4567896b71e5dc182 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 10:58:56 +0000 Subject: [PATCH 072/133] build(deps): bump braces in /wasm/zknym-lib/internal-dev (#5590) Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3. - [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3) --- updated-dependencies: - dependency-name: braces dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- wasm/zknym-lib/internal-dev/yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/wasm/zknym-lib/internal-dev/yarn.lock b/wasm/zknym-lib/internal-dev/yarn.lock index 1ad31e3d74..a6b1fe07cb 100644 --- a/wasm/zknym-lib/internal-dev/yarn.lock +++ b/wasm/zknym-lib/internal-dev/yarn.lock @@ -497,11 +497,11 @@ brace-expansion@^1.1.7: concat-map "0.0.1" braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" browserslist@^4.14.5: version "4.21.5" @@ -903,10 +903,10 @@ faye-websocket@^0.11.3: dependencies: websocket-driver ">=0.5.1" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" From 5aba886f14a2ce1a1e82a4c8853b932c1ad9025e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 10:59:20 +0000 Subject: [PATCH 073/133] build(deps): bump cookie and express in /wasm/mix-fetch/internal-dev (#5591) Bumps [cookie](https://github.com/jshttp/cookie) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together. Updates `cookie` from 0.6.0 to 0.7.1 - [Release notes](https://github.com/jshttp/cookie/releases) - [Commits](https://github.com/jshttp/cookie/compare/v0.6.0...v0.7.1) Updates `express` from 4.19.2 to 4.21.2 - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/4.21.2/History.md) - [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.2) --- updated-dependencies: - dependency-name: cookie dependency-type: indirect - dependency-name: express dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- wasm/mix-fetch/internal-dev/package-lock.json | 361 ++++++++++++---- wasm/mix-fetch/internal-dev/yarn.lock | 402 +++++++++++------- 2 files changed, 532 insertions(+), 231 deletions(-) diff --git a/wasm/mix-fetch/internal-dev/package-lock.json b/wasm/mix-fetch/internal-dev/package-lock.json index cdf4937a9e..15448adb31 100644 --- a/wasm/mix-fetch/internal-dev/package-lock.json +++ b/wasm/mix-fetch/internal-dev/package-lock.json @@ -23,11 +23,13 @@ "webpack-dev-server": "^4.7.4" } }, - "../go-mix-conn/build": {}, + "../go-mix-conn/build": { + "name": "go-mix-conn", + "version": "0.0.1" + }, "../pkg": { "name": "@nymproject/mix-fetch-wasm", - "version": "1.2.2", - "license": "Apache-2.0" + "version": "1.2.2" }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", @@ -699,9 +701,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dev": true, "license": "MIT", "dependencies": { @@ -713,7 +715,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -816,15 +818,32 @@ "node": ">= 0.8" } }, - "node_modules/call-bind": { + "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1011,9 +1030,9 @@ } }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, "license": "MIT", "engines": { @@ -1168,6 +1187,21 @@ "node": ">=6" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1183,9 +1217,9 @@ "license": "ISC" }, "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==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "license": "MIT", "engines": { @@ -1219,6 +1253,26 @@ "node": ">=4" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", @@ -1226,6 +1280,19 @@ "dev": true, "license": "MIT" }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -1342,38 +1409,38 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -1382,6 +1449,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/array-flatten": { @@ -1482,14 +1553,14 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -1570,27 +1641,54 @@ "license": "ISC" }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, - "license": "MIT" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -1669,6 +1767,19 @@ "resolved": "../go-mix-conn/build", "link": true }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1707,9 +1818,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==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", "engines": { @@ -1719,6 +1830,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hello-wasm-pack": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/hello-wasm-pack/-/hello-wasm-pack-0.1.0.tgz", @@ -2164,6 +2288,16 @@ "node": ">=8" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -2188,11 +2322,14 @@ } }, "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==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -2380,11 +2517,14 @@ } }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2564,9 +2704,9 @@ "license": "MIT" }, "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==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "dev": true, "license": "MIT" }, @@ -2655,13 +2795,13 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -2966,9 +3106,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, "license": "MIT", "dependencies": { @@ -2990,6 +3130,16 @@ "node": ">= 0.8.0" } }, + "node_modules/send/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/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3077,16 +3227,16 @@ } }, "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==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" @@ -3146,15 +3296,76 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" diff --git a/wasm/mix-fetch/internal-dev/yarn.lock b/wasm/mix-fetch/internal-dev/yarn.lock index bc06468f0a..53ffd85bc7 100644 --- a/wasm/mix-fetch/internal-dev/yarn.lock +++ b/wasm/mix-fetch/internal-dev/yarn.lock @@ -34,7 +34,7 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@1.4.14": +"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": version "1.4.14" resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== @@ -60,7 +60,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -75,7 +75,6 @@ "@nymproject/mix-fetch-wasm@file:../pkg": version "1.2.2" - resolved "file:../pkg" "@types/body-parser@*": version "1.19.2" @@ -374,7 +373,7 @@ acorn-import-assertions@^1.7.6: resolved "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz" integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== -acorn@^8, acorn@^8.5.0, acorn@^8.7.1: +acorn@^8.5.0, acorn@^8.7.1: version "8.8.2" resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== @@ -398,7 +397,7 @@ ajv-keywords@^5.0.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.12.5, ajv@^6.9.1: +ajv@^6.12.5: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -408,7 +407,7 @@ ajv@^6.12.5, ajv@^6.9.1: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.8.0, ajv@^8.8.2: +ajv@^8.0.0, ajv@^8.8.0: version "8.12.0" resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== @@ -431,16 +430,16 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" -array-flatten@^2.1.2: - version "2.1.2" - resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz" - integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== - array-flatten@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== +array-flatten@^2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz" + integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" @@ -456,10 +455,10 @@ binary-extensions@^2.0.0: resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -body-parser@1.20.2: - version "1.20.2" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz" - integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== +body-parser@1.20.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== dependencies: bytes "3.1.2" content-type "~1.0.5" @@ -469,7 +468,7 @@ body-parser@1.20.2: http-errors "2.0.0" iconv-lite "0.4.24" on-finished "2.4.1" - qs "6.11.0" + qs "6.13.0" raw-body "2.5.2" type-is "~1.6.18" unpipe "1.0.0" @@ -499,7 +498,7 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.1.1" -browserslist@^4.14.5, "browserslist@>= 4.21.0": +browserslist@^4.14.5: version "4.21.5" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz" integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w== @@ -524,13 +523,21 @@ bytes@3.1.2: resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== -call-bind@^1.0.0: +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" caniuse-lite@^1.0.30001449: version "1.0.30001474" @@ -628,10 +635,10 @@ cookie-signature@1.0.6: resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== -cookie@0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz" - integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== +cookie@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" + integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== copy-webpack-plugin@^11.0.0: version "11.0.0" @@ -659,13 +666,6 @@ cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -debug@^4.1.0: - version "4.3.4" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - debug@2.6.9: version "2.6.9" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" @@ -673,6 +673,13 @@ debug@2.6.9: dependencies: ms "2.0.0" +debug@^4.1.0: + version "4.3.4" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + default-gateway@^6.0.3: version "6.0.3" resolved "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz" @@ -685,16 +692,16 @@ define-lazy-prop@^2.0.0: resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" - integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== - depd@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + destroy@1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" @@ -724,6 +731,15 @@ dns-packet@^5.2.2: dependencies: "@leichtgewicht/ip-codec" "^2.0.1" +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" @@ -739,6 +755,11 @@ encodeurl@~1.0.2: resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + enhanced-resolve@^5.10.0: version "5.12.0" resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz" @@ -752,11 +773,28 @@ envinfo@^7.7.3: resolved "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz" integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + es-module-lexer@^0.9.0: version "0.9.3" resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz" integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" @@ -823,36 +861,36 @@ execa@^5.0.0: strip-final-newline "^2.0.0" express@^4.17.3: - version "4.19.2" - resolved "https://registry.npmjs.org/express/-/express-4.19.2.tgz" - integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== + version "4.21.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" + integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.2" + body-parser "1.20.3" content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.6.0" + cookie "0.7.1" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" etag "~1.8.1" - finalhandler "1.2.0" + finalhandler "1.3.1" fresh "0.5.2" http-errors "2.0.0" - merge-descriptors "1.0.1" + merge-descriptors "1.0.3" methods "~1.1.2" on-finished "2.4.1" parseurl "~1.3.3" - path-to-regexp "0.1.7" + path-to-regexp "0.1.12" proxy-addr "~2.0.7" - qs "6.11.0" + qs "6.13.0" range-parser "~1.2.1" safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" + send "0.19.0" + serve-static "1.16.2" setprototypeof "1.2.0" statuses "2.0.1" type-is "~1.6.18" @@ -906,13 +944,13 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" -finalhandler@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz" - integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== dependencies: debug "2.6.9" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" on-finished "2.4.1" parseurl "~1.3.3" @@ -952,26 +990,51 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -get-intrinsic@^1.0.2: - version "1.2.0" - resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz" - integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.3" + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" get-stream@^6.0.0: version "6.0.1" resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== -glob-parent@^5.1.2: +glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -985,13 +1048,6 @@ glob-parent@^6.0.1: dependencies: is-glob "^4.0.3" -glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - glob-to-regexp@^0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz" @@ -1021,7 +1077,12 @@ globby@^13.1.1: slash "^4.0.0" "go-mix-conn@file:../go-mix-conn/build": - resolved "file:../go-mix-conn/build" + version "0.0.1" + +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.11" @@ -1038,10 +1099,10 @@ has-flag@^4.0.0: resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== has@^1.0.3: version "1.0.3" @@ -1050,6 +1111,13 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + hello-wasm-pack@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/hello-wasm-pack/-/hello-wasm-pack-0.1.0.tgz" @@ -1075,16 +1143,6 @@ http-deceiver@^1.2.7: resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz" integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== -http-errors@~1.6.2: - version "1.6.3" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" - integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - http-errors@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" @@ -1096,6 +1154,16 @@ http-errors@2.0.0: statuses "2.0.1" toidentifier "1.0.1" +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + http-parser-js@>=0.5.1: version "0.5.8" resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz" @@ -1154,7 +1222,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3, inherits@2, inherits@2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1169,16 +1237,16 @@ interpret@^2.2.0: resolved "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz" integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== -ipaddr.js@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz" - integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== - ipaddr.js@1.9.1: 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== +ipaddr.js@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz" + integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" @@ -1303,6 +1371,11 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + media-typer@0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" @@ -1315,10 +1388,10 @@ memfs@^3.4.3: dependencies: fs-monkey "^1.0.3" -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" - integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== merge-stream@^2.0.0: version "2.0.0" @@ -1343,7 +1416,7 @@ micromatch@^4.0.2, micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" -"mime-db@>= 1.43.0 < 2", mime-db@1.52.0: +mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": version "1.52.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== @@ -1432,10 +1505,10 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -object-inspect@^1.9.0: - version "1.12.3" - resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz" - integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== obuf@^1.0.0, obuf@^1.1.2: version "1.1.2" @@ -1529,10 +1602,10 @@ path-parse@^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@0.1.7: - version "0.1.7" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== +path-to-regexp@0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" + integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== path-type@^4.0.0: version "4.0.0" @@ -1574,12 +1647,12 @@ punycode@^2.1.0: resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz" integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== -qs@6.11.0: - version "6.11.0" - resolved "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz" - integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== dependencies: - side-channel "^1.0.4" + side-channel "^1.0.6" queue-microtask@^1.2.2: version "1.2.3" @@ -1699,36 +1772,22 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@^5.1.0, safe-buffer@>=5.1.0, safe-buffer@~5.2.0, safe-buffer@5.2.1: +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - "safer-buffer@>= 2.1.2 < 3": version "2.1.2" resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -schema-utils@^3.1.0: - version "3.1.1" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz" - integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== - dependencies: - "@types/json-schema" "^7.0.8" - ajv "^6.12.5" - ajv-keywords "^3.5.2" - -schema-utils@^3.1.1: +schema-utils@^3.1.0, schema-utils@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz" integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== @@ -1759,10 +1818,10 @@ selfsigned@^2.1.1: dependencies: node-forge "^1" -send@0.18.0: - version "0.18.0" - resolved "https://registry.npmjs.org/send/-/send-0.18.0.tgz" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== dependencies: debug "2.6.9" depd "2.0.0" @@ -1798,15 +1857,15 @@ serve-index@^1.9.1: mime-types "~2.1.17" parseurl "~1.3.2" -serve-static@1.15.0: - version "1.15.0" - resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== dependencies: - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" parseurl "~1.3.3" - send "0.18.0" + send "0.19.0" setprototypeof@1.1.0: version "1.1.0" @@ -1842,14 +1901,45 @@ shell-quote@^1.7.3: resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.0.tgz" integrity sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ== -side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.0.6: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" signal-exit@^3.0.3: version "3.0.7" @@ -1906,16 +1996,16 @@ spdy@^4.0.2: select-hose "^2.0.0" spdy-transport "^3.0.0" -"statuses@>= 1.4.0 < 2": - version "1.5.0" - resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" - integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== - statuses@2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" @@ -1998,7 +2088,7 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -unpipe@~1.0.0, unpipe@1.0.0: +unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== @@ -2053,7 +2143,7 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" -webpack-cli@^4.9.2, webpack-cli@4.x.x: +webpack-cli@^4.9.2: version "4.10.0" resolved "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz" integrity sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w== @@ -2131,7 +2221,7 @@ webpack-sources@^3.2.3: resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -"webpack@^4.0.0 || ^5.0.0", "webpack@^4.37.0 || ^5.0.0", webpack@^5.1.0, webpack@^5.70.0, "webpack@4.x.x || 5.x.x": +webpack@^5.70.0: version "5.77.0" resolved "https://registry.npmjs.org/webpack/-/webpack-5.77.0.tgz" integrity sha512-sbGNjBr5Ya5ss91yzjeJTLKyfiwo5C628AFjEa6WSXcZa4E+F57om3Cc8xLb1Jh0b243AWuSYRf3dn7HVeFQ9Q== @@ -2161,7 +2251,7 @@ webpack-sources@^3.2.3: watchpack "^2.4.0" webpack-sources "^3.2.3" -websocket-driver@^0.7.4, websocket-driver@>=0.5.1: +websocket-driver@>=0.5.1, websocket-driver@^0.7.4: version "0.7.4" resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz" integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== From 6565655861fd64354be0260dbb99f87310e4414a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 10:59:36 +0000 Subject: [PATCH 074/133] build(deps): bump cookie and express in /wasm/client/internal-dev (#5592) Bumps [cookie](https://github.com/jshttp/cookie) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together. Updates `cookie` from 0.6.0 to 0.7.1 - [Release notes](https://github.com/jshttp/cookie/releases) - [Commits](https://github.com/jshttp/cookie/compare/v0.6.0...v0.7.1) Updates `express` from 4.19.2 to 4.21.2 - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/4.21.2/History.md) - [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.2) --- updated-dependencies: - dependency-name: cookie dependency-type: indirect - dependency-name: express dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- wasm/client/internal-dev/package-lock.json | 356 ++++++++++++++---- wasm/client/internal-dev/yarn.lock | 400 +++++++++++++-------- 2 files changed, 527 insertions(+), 229 deletions(-) diff --git a/wasm/client/internal-dev/package-lock.json b/wasm/client/internal-dev/package-lock.json index ac154b841a..447a40446e 100644 --- a/wasm/client/internal-dev/package-lock.json +++ b/wasm/client/internal-dev/package-lock.json @@ -24,8 +24,7 @@ }, "../pkg": { "name": "@nymproject/nym-client-wasm", - "version": "1.4.0-rc.0", - "license": "Apache-2.0" + "version": "1.4.0-rc.0" }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", @@ -698,9 +697,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dev": true, "license": "MIT", "dependencies": { @@ -712,7 +711,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -815,15 +814,32 @@ "node": ">= 0.8" } }, - "node_modules/call-bind": { + "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1010,9 +1026,9 @@ } }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, "license": "MIT", "engines": { @@ -1167,6 +1183,21 @@ "node": ">=6" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1182,9 +1213,9 @@ "license": "ISC" }, "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==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "license": "MIT", "engines": { @@ -1218,6 +1249,26 @@ "node": ">=4" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", @@ -1225,6 +1276,19 @@ "dev": true, "license": "MIT" }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -1341,38 +1405,38 @@ } }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -1381,6 +1445,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/array-flatten": { @@ -1481,14 +1549,14 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -1584,27 +1652,54 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, - "license": "MIT" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -1680,6 +1775,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1718,9 +1826,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==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", "engines": { @@ -1730,6 +1838,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hello-wasm-pack": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/hello-wasm-pack/-/hello-wasm-pack-0.1.0.tgz", @@ -2176,6 +2297,16 @@ "node": ">=8" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -2200,11 +2331,14 @@ } }, "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==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -2392,11 +2526,14 @@ } }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2576,9 +2713,9 @@ "license": "MIT" }, "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==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "dev": true, "license": "MIT" }, @@ -2667,13 +2804,13 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -2979,9 +3116,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, "license": "MIT", "dependencies": { @@ -3003,6 +3140,16 @@ "node": ">= 0.8.0" } }, + "node_modules/send/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/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3090,16 +3237,16 @@ } }, "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==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dev": true, "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" @@ -3159,15 +3306,76 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" diff --git a/wasm/client/internal-dev/yarn.lock b/wasm/client/internal-dev/yarn.lock index f65c73527d..80b223e299 100644 --- a/wasm/client/internal-dev/yarn.lock +++ b/wasm/client/internal-dev/yarn.lock @@ -34,7 +34,7 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@1.4.14": +"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": version "1.4.14" resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== @@ -60,7 +60,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -75,7 +75,6 @@ "@nymproject/nym-client-wasm@file:../pkg": version "1.4.0-rc.0" - resolved "file:../pkg" "@types/body-parser@*": version "1.19.2" @@ -374,7 +373,7 @@ acorn-import-assertions@^1.7.6: resolved "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz" integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== -acorn@^8, acorn@^8.5.0, acorn@^8.7.1: +acorn@^8.5.0, acorn@^8.7.1: version "8.8.2" resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== @@ -398,7 +397,7 @@ ajv-keywords@^5.0.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.12.5, ajv@^6.9.1: +ajv@^6.12.5: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -408,7 +407,7 @@ ajv@^6.12.5, ajv@^6.9.1: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.8.0, ajv@^8.8.2: +ajv@^8.0.0, ajv@^8.8.0: version "8.12.0" resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== @@ -431,16 +430,16 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" -array-flatten@^2.1.2: - version "2.1.2" - resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz" - integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== - array-flatten@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== +array-flatten@^2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz" + integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" @@ -456,10 +455,10 @@ binary-extensions@^2.0.0: resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -body-parser@1.20.2: - version "1.20.2" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz" - integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== +body-parser@1.20.3: + version "1.20.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" + integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== dependencies: bytes "3.1.2" content-type "~1.0.5" @@ -469,7 +468,7 @@ body-parser@1.20.2: http-errors "2.0.0" iconv-lite "0.4.24" on-finished "2.4.1" - qs "6.11.0" + qs "6.13.0" raw-body "2.5.2" type-is "~1.6.18" unpipe "1.0.0" @@ -499,7 +498,7 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -browserslist@^4.14.5, "browserslist@>= 4.21.0": +browserslist@^4.14.5: version "4.21.5" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz" integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w== @@ -524,13 +523,21 @@ bytes@3.1.2: resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== -call-bind@^1.0.0: +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" caniuse-lite@^1.0.30001449: version "1.0.30001474" @@ -628,10 +635,10 @@ cookie-signature@1.0.6: resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== -cookie@0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz" - integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== +cookie@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9" + integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== copy-webpack-plugin@^11.0.0: version "11.0.0" @@ -659,13 +666,6 @@ cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -debug@^4.1.0: - version "4.3.4" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - debug@2.6.9: version "2.6.9" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" @@ -673,6 +673,13 @@ debug@2.6.9: dependencies: ms "2.0.0" +debug@^4.1.0: + version "4.3.4" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + default-gateway@^6.0.3: version "6.0.3" resolved "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz" @@ -685,16 +692,16 @@ define-lazy-prop@^2.0.0: resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" - integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== - depd@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + destroy@1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" @@ -724,6 +731,15 @@ dns-packet@^5.2.2: dependencies: "@leichtgewicht/ip-codec" "^2.0.1" +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" @@ -739,6 +755,11 @@ encodeurl@~1.0.2: resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + enhanced-resolve@^5.10.0: version "5.12.0" resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz" @@ -752,11 +773,28 @@ envinfo@^7.7.3: resolved "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz" integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + es-module-lexer@^0.9.0: version "0.9.3" resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz" integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" @@ -823,36 +861,36 @@ execa@^5.0.0: strip-final-newline "^2.0.0" express@^4.17.3: - version "4.19.2" - resolved "https://registry.npmjs.org/express/-/express-4.19.2.tgz" - integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== + version "4.21.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" + integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.2" + body-parser "1.20.3" content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.6.0" + cookie "0.7.1" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" etag "~1.8.1" - finalhandler "1.2.0" + finalhandler "1.3.1" fresh "0.5.2" http-errors "2.0.0" - merge-descriptors "1.0.1" + merge-descriptors "1.0.3" methods "~1.1.2" on-finished "2.4.1" parseurl "~1.3.3" - path-to-regexp "0.1.7" + path-to-regexp "0.1.12" proxy-addr "~2.0.7" - qs "6.11.0" + qs "6.13.0" range-parser "~1.2.1" safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" + send "0.19.0" + serve-static "1.16.2" setprototypeof "1.2.0" statuses "2.0.1" type-is "~1.6.18" @@ -906,13 +944,13 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -finalhandler@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz" - integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== +finalhandler@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019" + integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ== dependencies: debug "2.6.9" - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" on-finished "2.4.1" parseurl "~1.3.3" @@ -952,26 +990,51 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -get-intrinsic@^1.0.2: - version "1.2.0" - resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz" - integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.3" + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" get-stream@^6.0.0: version "6.0.1" resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== -glob-parent@^5.1.2: +glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -985,13 +1048,6 @@ glob-parent@^6.0.1: dependencies: is-glob "^4.0.3" -glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - glob-to-regexp@^0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz" @@ -1020,6 +1076,11 @@ globby@^13.1.1: merge2 "^1.4.1" slash "^4.0.0" +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" @@ -1035,10 +1096,10 @@ has-flag@^4.0.0: resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== has@^1.0.3: version "1.0.3" @@ -1047,6 +1108,13 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + hello-wasm-pack@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/hello-wasm-pack/-/hello-wasm-pack-0.1.0.tgz" @@ -1072,16 +1140,6 @@ http-deceiver@^1.2.7: resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz" integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== -http-errors@~1.6.2: - version "1.6.3" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" - integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - http-errors@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" @@ -1093,6 +1151,16 @@ http-errors@2.0.0: statuses "2.0.1" toidentifier "1.0.1" +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + http-parser-js@>=0.5.1: version "0.5.8" resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz" @@ -1151,7 +1219,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3, inherits@2, inherits@2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1166,16 +1234,16 @@ interpret@^2.2.0: resolved "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz" integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== -ipaddr.js@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz" - integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== - ipaddr.js@1.9.1: 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== +ipaddr.js@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz" + integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" @@ -1300,6 +1368,11 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + media-typer@0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" @@ -1312,10 +1385,10 @@ memfs@^3.4.3: dependencies: fs-monkey "^1.0.3" -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" - integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== merge-stream@^2.0.0: version "2.0.0" @@ -1340,7 +1413,7 @@ micromatch@^4.0.2, micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" -"mime-db@>= 1.43.0 < 2", mime-db@1.52.0: +mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": version "1.52.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== @@ -1429,10 +1502,10 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -object-inspect@^1.9.0: - version "1.12.3" - resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz" - integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== obuf@^1.0.0, obuf@^1.1.2: version "1.1.2" @@ -1526,10 +1599,10 @@ path-parse@^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@0.1.7: - version "0.1.7" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== +path-to-regexp@0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7" + integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ== path-type@^4.0.0: version "4.0.0" @@ -1571,12 +1644,12 @@ punycode@^2.1.0: resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz" integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== -qs@6.11.0: - version "6.11.0" - resolved "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz" - integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== +qs@6.13.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906" + integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg== dependencies: - side-channel "^1.0.4" + side-channel "^1.0.6" queue-microtask@^1.2.2: version "1.2.3" @@ -1696,36 +1769,22 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@^5.1.0, safe-buffer@>=5.1.0, safe-buffer@~5.2.0, safe-buffer@5.2.1: +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - "safer-buffer@>= 2.1.2 < 3": version "2.1.2" resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -schema-utils@^3.1.0: - version "3.1.1" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz" - integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== - dependencies: - "@types/json-schema" "^7.0.8" - ajv "^6.12.5" - ajv-keywords "^3.5.2" - -schema-utils@^3.1.1: +schema-utils@^3.1.0, schema-utils@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz" integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== @@ -1756,10 +1815,10 @@ selfsigned@^2.1.1: dependencies: node-forge "^1" -send@0.18.0: - version "0.18.0" - resolved "https://registry.npmjs.org/send/-/send-0.18.0.tgz" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== +send@0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" + integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw== dependencies: debug "2.6.9" depd "2.0.0" @@ -1795,15 +1854,15 @@ serve-index@^1.9.1: mime-types "~2.1.17" parseurl "~1.3.2" -serve-static@1.15.0: - version "1.15.0" - resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== +serve-static@1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296" + integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw== dependencies: - encodeurl "~1.0.2" + encodeurl "~2.0.0" escape-html "~1.0.3" parseurl "~1.3.3" - send "0.18.0" + send "0.19.0" setprototypeof@1.1.0: version "1.1.0" @@ -1839,14 +1898,45 @@ shell-quote@^1.7.3: resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.0.tgz" integrity sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ== -side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.0.6: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" signal-exit@^3.0.3: version "3.0.7" @@ -1903,16 +1993,16 @@ spdy@^4.0.2: select-hose "^2.0.0" spdy-transport "^3.0.0" -"statuses@>= 1.4.0 < 2": - version "1.5.0" - resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" - integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== - statuses@2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" @@ -1995,7 +2085,7 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -unpipe@~1.0.0, unpipe@1.0.0: +unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== @@ -2050,7 +2140,7 @@ wbuf@^1.1.0, wbuf@^1.7.3: dependencies: minimalistic-assert "^1.0.0" -webpack-cli@^4.9.2, webpack-cli@4.x.x: +webpack-cli@^4.9.2: version "4.10.0" resolved "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz" integrity sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w== @@ -2128,7 +2218,7 @@ webpack-sources@^3.2.3: resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -"webpack@^4.0.0 || ^5.0.0", "webpack@^4.37.0 || ^5.0.0", webpack@^5.1.0, webpack@^5.70.0, "webpack@4.x.x || 5.x.x": +webpack@^5.70.0: version "5.77.0" resolved "https://registry.npmjs.org/webpack/-/webpack-5.77.0.tgz" integrity sha512-sbGNjBr5Ya5ss91yzjeJTLKyfiwo5C628AFjEa6WSXcZa4E+F57om3Cc8xLb1Jh0b243AWuSYRf3dn7HVeFQ9Q== @@ -2158,7 +2248,7 @@ webpack-sources@^3.2.3: watchpack "^2.4.0" webpack-sources "^3.2.3" -websocket-driver@^0.7.4, websocket-driver@>=0.5.1: +websocket-driver@>=0.5.1, websocket-driver@^0.7.4: version "0.7.4" resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz" integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== From ec5d342e3a6544548dd9abb635af9be6ca8c1bcf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 11:00:21 +0000 Subject: [PATCH 075/133] build(deps): bump serve-static and express (#5594) Bumps [serve-static](https://github.com/expressjs/serve-static) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together. Updates `serve-static` from 1.15.0 to 1.16.2 - [Release notes](https://github.com/expressjs/serve-static/releases) - [Changelog](https://github.com/expressjs/serve-static/blob/v1.16.2/HISTORY.md) - [Commits](https://github.com/expressjs/serve-static/compare/v1.15.0...v1.16.2) Updates `express` from 4.19.2 to 4.21.2 - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/4.21.2/History.md) - [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.2) --- updated-dependencies: - dependency-name: serve-static dependency-type: indirect - dependency-name: express dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From b80a4c8614ec7e73e2a8ce2e024b00d571481293 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 11:00:38 +0000 Subject: [PATCH 076/133] build(deps): bump body-parser and express (#5596) Bumps [body-parser](https://github.com/expressjs/body-parser) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together. Updates `body-parser` from 1.20.2 to 1.20.3 - [Release notes](https://github.com/expressjs/body-parser/releases) - [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md) - [Commits](https://github.com/expressjs/body-parser/compare/1.20.2...1.20.3) Updates `express` from 4.19.2 to 4.21.2 - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/4.21.2/History.md) - [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.2) --- updated-dependencies: - dependency-name: body-parser dependency-type: indirect - dependency-name: express dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From eca88b0fa44927abea3f5262b93fe4129979d594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 12 Mar 2025 11:02:03 +0000 Subject: [PATCH 077/133] introduce internal tool for checking signer status (#5598) * introduce internal tool for checking signer status * fixed nym-api types due to moving values around * added abci version --- Cargo.lock | 19 ++ Cargo.toml | 4 +- .../validator-client/src/nym_api/mod.rs | 27 +- .../validator-client/src/nym_api/routes.rs | 3 + nym-api/nym-api-requests/Cargo.toml | 1 + nym-api/nym-api-requests/src/models.rs | 300 +++++++++++++++++- nym-api/src/network/handlers.rs | 3 +- nym-api/src/network/models.rs | 291 ----------------- nym-api/src/support/http/state.rs | 16 +- nym-wallet/Cargo.lock | 1 + .../validator-status-check/Cargo.toml | 30 ++ .../src/commands/build_info.rs | 15 + .../src/commands/check_network.rs | 73 +++++ .../src/commands/check_signer.rs | 21 ++ .../src/commands/mod.rs | 51 +++ .../validator-status-check/src/helpers.rs | 39 +++ .../validator-status-check/src/main.rs | 24 ++ .../validator-status-check/src/models.rs | 222 +++++++++++++ 18 files changed, 833 insertions(+), 307 deletions(-) create mode 100644 tools/internal/validator-status-check/Cargo.toml create mode 100644 tools/internal/validator-status-check/src/commands/build_info.rs create mode 100644 tools/internal/validator-status-check/src/commands/check_network.rs create mode 100644 tools/internal/validator-status-check/src/commands/check_signer.rs create mode 100644 tools/internal/validator-status-check/src/commands/mod.rs create mode 100644 tools/internal/validator-status-check/src/helpers.rs create mode 100644 tools/internal/validator-status-check/src/main.rs create mode 100644 tools/internal/validator-status-check/src/models.rs diff --git a/Cargo.lock b/Cargo.lock index 5b49a28518..8d0e1dba7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4896,6 +4896,7 @@ dependencies = [ "serde_json", "sha2 0.10.8", "tendermint 0.40.1", + "tendermint-rpc", "thiserror 2.0.12", "time", "ts-rs", @@ -11264,6 +11265,24 @@ dependencies = [ "serde", ] +[[package]] +name = "validator-status-check" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "comfy-table", + "nym-bin-common", + "nym-network-defaults", + "nym-validator-client", + "serde", + "serde_json", + "strum 0.26.3", + "time", + "tokio", + "tracing", +] + [[package]] name = "valuable" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index b6ee317bfe..491f5d89cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -137,7 +137,7 @@ members = [ "tools/internal/testnet-manager", "tools/internal/testnet-manager", "tools/internal/testnet-manager/dkg-bypass-contract", - "tools/internal/testnet-manager/dkg-bypass-contract", + "tools/internal/testnet-manager/dkg-bypass-contract", "tools/internal/validator-status-check", "tools/nym-cli", "tools/nym-id-cli", "tools/nym-nr-query", @@ -447,4 +447,4 @@ dbg_macro = "deny" exit = "deny" panic = "deny" unimplemented = "deny" -unreachable = "deny" \ No newline at end of file +unreachable = "deny" diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index 7011a1904a..a9b1bc54d5 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -12,8 +12,9 @@ use nym_api_requests::ecash::models::{ }; use nym_api_requests::ecash::VerificationKeyResponse; use nym_api_requests::models::{ - AnnotationResponse, ApiHealthResponse, LegacyDescribedMixNode, NodePerformanceResponse, - NodeRefreshBody, NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse, + AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainStatusResponse, + LegacyDescribedMixNode, NodePerformanceResponse, NodeRefreshBody, NymNodeDescription, + PerformanceHistoryResponse, RewardedSetResponse, }; use nym_api_requests::nym_nodes::{ NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponse, @@ -69,6 +70,19 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] + async fn build_information(&self) -> Result { + self.get_json( + &[ + routes::API_VERSION, + routes::API_STATUS_ROUTES, + routes::BUILD_INFORMATION, + ], + NO_PARAMS, + ) + .await + } + #[deprecated] #[instrument(level = "debug", skip(self))] async fn get_mixnodes(&self) -> Result, NymAPIError> { @@ -1043,6 +1057,15 @@ pub trait NymApiClientExt: ApiClient { ) .await } + + #[instrument(level = "debug", skip(self))] + async fn get_chain_status(&self) -> Result { + self.get_json( + &[routes::API_VERSION, routes::NETWORK, routes::CHAIN_STATUS], + NO_PARAMS, + ) + .await + } } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] diff --git a/common/client-libs/validator-client/src/nym_api/routes.rs b/common/client-libs/validator-client/src/nym_api/routes.rs index 1dae2254f4..8787264c4a 100644 --- a/common/client-libs/validator-client/src/nym_api/routes.rs +++ b/common/client-libs/validator-client/src/nym_api/routes.rs @@ -49,6 +49,8 @@ pub mod nym_nodes { pub const STATUS_ROUTES: &str = "status"; pub const API_STATUS_ROUTES: &str = "api-status"; pub const HEALTH: &str = "health"; +pub const BUILD_INFORMATION: &str = "build-information"; + pub const MIXNODE: &str = "mixnode"; pub const GATEWAY: &str = "gateway"; pub const NYM_NODES: &str = "nym-nodes"; @@ -70,4 +72,5 @@ pub const SUBMIT_NODE: &str = "submit-node-monitoring-results"; pub const SERVICE_PROVIDERS: &str = "services"; pub const DETAILS: &str = "details"; +pub const CHAIN_STATUS: &str = "chain-status"; pub const NETWORK: &str = "network"; diff --git a/nym-api/nym-api-requests/Cargo.toml b/nym-api/nym-api-requests/Cargo.toml index 9ca0fe6ea4..5c9ee287b6 100644 --- a/nym-api/nym-api-requests/Cargo.toml +++ b/nym-api/nym-api-requests/Cargo.toml @@ -17,6 +17,7 @@ humantime-serde = { workspace = true } serde_json = { workspace = true } sha2.workspace = true tendermint = { workspace = true } +tendermint-rpc = { workspace = true } thiserror.workspace = true time = { workspace = true, features = ["serde", "parsing", "formatting"] } ts-rs = { workspace = true, optional = true } diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index 2558613c8c..a39b6f25c7 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -27,9 +27,7 @@ use nym_network_defaults::{DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_ use nym_node_requests::api::v1::authenticator::models::Authenticator; use nym_node_requests::api::v1::gateway::models::Wireguard; use nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter; -use nym_node_requests::api::v1::node::models::{ - AuxiliaryDetails, BinaryBuildInformationOwned, NodeRoles, -}; +use nym_node_requests::api::v1::node::models::{AuxiliaryDetails, NodeRoles}; use schemars::gen::SchemaGenerator; use schemars::schema::{InstanceType, Schema, SchemaObject}; use schemars::JsonSchema; @@ -43,6 +41,8 @@ use thiserror::Error; use time::{Date, OffsetDateTime}; use utoipa::{IntoParams, ToResponse, ToSchema}; +pub use nym_node_requests::api::v1::node::models::BinaryBuildInformationOwned; + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] pub struct RequestError { message: String, @@ -1215,6 +1215,7 @@ impl From for WireguardDetails { #[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct ApiHealthResponse { pub status: ApiStatus, + #[serde(default)] pub chain_status: ChainStatus, pub uptime: u64, } @@ -1225,10 +1226,11 @@ pub enum ApiStatus { Up, } -#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Default, schemars::JsonSchema, ToSchema)] #[serde(rename_all = "snake_case")] pub enum ChainStatus { Synced, + #[default] Unknown, Stalled { #[serde( @@ -1428,7 +1430,297 @@ impl From for RewardedSetResponse } } +#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct ChainStatusResponse { + pub connected_nyxd: String, + pub status: DetailedChainStatus, +} + +#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct DetailedChainStatus { + pub abci: crate::models::tendermint_types::AbciInfo, + pub latest_block: BlockInfo, +} + +#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct BlockInfo { + pub block_id: BlockId, + pub block: FullBlockInfo, + // if necessary we might put block data here later too +} + +impl From for BlockInfo { + fn from(value: tendermint_rpc::endpoint::block::Response) -> Self { + BlockInfo { + block_id: value.block_id.into(), + block: FullBlockInfo { + header: value.block.header.into(), + }, + } + } +} + +#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct FullBlockInfo { + pub header: BlockHeader, +} + +// copy tendermint types definitions whilst deriving schema types on them and dropping unwanted fields +pub mod tendermint_types { + use schemars::JsonSchema; + use serde::{Deserialize, Serialize}; + use tendermint::abci::response::Info; + use tendermint::block::header::Version; + use tendermint::{block, Hash}; + use utoipa::ToSchema; + + #[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] + pub struct AbciInfo { + /// Some arbitrary information. + pub data: String, + + /// The application software semantic version. + pub version: String, + + /// The application protocol version. + pub app_version: u64, + + /// The latest block for which the app has called [`Commit`]. + pub last_block_height: u64, + + /// The latest result of [`Commit`]. + pub last_block_app_hash: String, + } + + impl From for AbciInfo { + fn from(value: Info) -> Self { + AbciInfo { + data: value.data, + version: value.version, + app_version: value.app_version, + last_block_height: value.last_block_height.value(), + last_block_app_hash: value.last_block_app_hash.to_string(), + } + } + } + + /// `Version` contains the protocol version for the blockchain and the + /// application. + /// + /// + #[derive( + Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, ToSchema, + )] + pub struct HeaderVersion { + /// Block version + pub block: u64, + + /// App version + pub app: u64, + } + + impl From for HeaderVersion { + fn from(value: Version) -> Self { + HeaderVersion { + block: value.block, + app: value.app, + } + } + } + + /// Block identifiers which contain two distinct Merkle roots of the block, + /// as well as the number of parts in the block. + /// + /// + /// + /// Default implementation is an empty Id as defined by the Go implementation in + /// . + /// + /// If the Hash is empty in BlockId, the BlockId should be empty (encoded to None). + /// This is implemented outside of this struct. Use the Default trait to check for an empty BlockId. + /// See: + #[derive( + Serialize, + Deserialize, + Copy, + Clone, + Debug, + Default, + Hash, + Eq, + PartialEq, + PartialOrd, + Ord, + JsonSchema, + ToSchema, + )] + pub struct BlockId { + /// The block's main hash is the Merkle root of all the fields in the + /// block header. + #[schemars(with = "String")] + #[schema(value_type = String)] + pub hash: Hash, + + /// Parts header (if available) is used for secure gossipping of the block + /// during consensus. It is the Merkle root of the complete serialized block + /// cut into parts. + /// + /// PartSet is used to split a byteslice of data into parts (pieces) for + /// transmission. By splitting data into smaller parts and computing a + /// Merkle root hash on the list, you can verify that a part is + /// legitimately part of the complete data, and the part can be forwarded + /// to other peers before all the parts are known. In short, it's a fast + /// way to propagate a large file over a gossip network. + /// + /// + /// + /// PartSetHeader in protobuf is defined as never nil using the gogoproto + /// annotations. This does not translate to Rust, but we can indicate this + /// in the domain type. + pub part_set_header: PartSetHeader, + } + + impl From for BlockId { + fn from(value: block::Id) -> Self { + BlockId { + hash: value.hash, + part_set_header: value.part_set_header.into(), + } + } + } + + /// Block parts header + #[derive( + Clone, + Copy, + Debug, + Default, + Hash, + Eq, + PartialEq, + PartialOrd, + Ord, + Deserialize, + Serialize, + JsonSchema, + ToSchema, + )] + #[non_exhaustive] + pub struct PartSetHeader { + /// Number of parts in this block + pub total: u32, + + /// Hash of the parts set header, + #[schemars(with = "String")] + #[schema(value_type = String)] + pub hash: Hash, + } + + impl From for PartSetHeader { + fn from(value: block::parts::Header) -> Self { + PartSetHeader { + total: value.total, + hash: value.hash, + } + } + } + + /// Block `Header` values contain metadata about the block and about the + /// consensus, as well as commitments to the data in the current block, the + /// previous block, and the results returned by the application. + /// + /// + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] + pub struct BlockHeader { + /// Header version + pub version: HeaderVersion, + + /// Chain ID + pub chain_id: String, + + /// Current block height + pub height: u64, + + /// Current timestamp + #[schemars(with = "String")] + #[schema(value_type = String)] + pub time: tendermint::Time, + + /// Previous block info + pub last_block_id: Option, + + /// Commit from validators from the last block + #[schemars(with = "Option")] + #[schema(value_type = Option)] + pub last_commit_hash: Option, + + /// Merkle root of transaction hashes + #[schemars(with = "Option")] + #[schema(value_type = Option)] + pub data_hash: Option, + + /// Validators for the current block + #[schemars(with = "String")] + #[schema(value_type = String)] + pub validators_hash: Hash, + + /// Validators for the next block + #[schemars(with = "String")] + #[schema(value_type = String)] + pub next_validators_hash: Hash, + + /// Consensus params for the current block + #[schemars(with = "String")] + #[schema(value_type = String)] + pub consensus_hash: Hash, + + /// State after txs from the previous block + #[schemars(with = "String")] + #[schema(value_type = String)] + pub app_hash: Hash, + + /// Root hash of all results from the txs from the previous block + #[schemars(with = "Option")] + #[schema(value_type = Option)] + pub last_results_hash: Option, + + /// Hash of evidence included in the block + #[schemars(with = "Option")] + #[schema(value_type = Option)] + pub evidence_hash: Option, + + /// Original proposer of the block + #[serde(with = "nym_serde_helpers::hex")] + #[schemars(with = "String")] + #[schema(value_type = String)] + pub proposer_address: Vec, + } + + impl From for BlockHeader { + fn from(value: block::Header) -> Self { + BlockHeader { + version: value.version.into(), + chain_id: value.chain_id.to_string(), + height: value.height.value(), + time: value.time, + last_block_id: value.last_block_id.map(Into::into), + last_commit_hash: value.last_commit_hash, + data_hash: value.data_hash, + validators_hash: value.validators_hash, + next_validators_hash: value.next_validators_hash, + consensus_hash: value.consensus_hash, + app_hash: Hash::try_from(value.app_hash.as_bytes().to_vec()).unwrap_or_default(), + last_results_hash: value.last_results_hash, + evidence_hash: value.evidence_hash, + proposer_address: value.proposer_address.as_bytes().to_vec(), + } + } + } +} + +use crate::models::tendermint_types::{BlockHeader, BlockId}; pub use config_score::*; + pub mod config_score { use nym_contracts_common::NaiveFloat; use serde::{Deserialize, Serialize}; diff --git a/nym-api/src/network/handlers.rs b/nym-api/src/network/handlers.rs index a308a9fa82..380e78e736 100644 --- a/nym-api/src/network/handlers.rs +++ b/nym-api/src/network/handlers.rs @@ -1,11 +1,12 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::network::models::{ChainStatusResponse, ContractInformation, NetworkDetails}; +use crate::network::models::{ContractInformation, NetworkDetails}; use crate::node_status_api::models::AxumResult; use crate::support::http::state::AppState; use axum::extract::State; use axum::{extract, Json, Router}; +use nym_api_requests::models::ChainStatusResponse; use nym_contracts_common::ContractBuildInformation; use std::collections::HashMap; use tower_http::compression::CompressionLayer; diff --git a/nym-api/src/network/models.rs b/nym-api/src/network/models.rs index 066b473921..1819ce024d 100644 --- a/nym-api/src/network/models.rs +++ b/nym-api/src/network/models.rs @@ -1,9 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::network::models::tendermint_types::{AbciInfo, BlockHeader, BlockId}; use nym_config::defaults::NymNetworkDetails; -use nym_validator_client::nyxd::BlockResponse; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; @@ -29,292 +27,3 @@ pub struct ContractInformation { pub(crate) address: Option, pub(crate) details: Option, } - -#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct ChainStatusResponse { - pub connected_nyxd: String, - pub status: ChainStatus, -} - -#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct ChainStatus { - pub abci: AbciInfo, - pub latest_block: BlockInfo, -} - -#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct BlockInfo { - pub block_id: BlockId, - pub block: FullBlockInfo, - // if necessary we might put block data here later too -} - -impl From for BlockInfo { - fn from(value: BlockResponse) -> Self { - BlockInfo { - block_id: value.block_id.into(), - block: FullBlockInfo { - header: value.block.header.into(), - }, - } - } -} - -#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct FullBlockInfo { - pub header: BlockHeader, -} - -// copy tendermint types definitions whilst deriving schema types on them and dropping unwanted fields -pub mod tendermint_types { - use nym_validator_client::nyxd::Hash; - use schemars::JsonSchema; - use serde::{Deserialize, Serialize}; - use tendermint::abci::response::Info; - use tendermint::block; - use tendermint::block::header::Version; - use utoipa::ToSchema; - - #[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] - pub struct AbciInfo { - /// Some arbitrary information. - pub data: String, - - /// The application software semantic version. - pub version: String, - - /// The application protocol version. - pub app_version: u64, - - /// The latest block for which the app has called [`Commit`]. - pub last_block_height: u64, - - /// The latest result of [`Commit`]. - pub last_block_app_hash: String, - } - - impl From for AbciInfo { - fn from(value: Info) -> Self { - AbciInfo { - data: value.data, - version: value.version, - app_version: value.app_version, - last_block_height: value.last_block_height.value(), - last_block_app_hash: value.last_block_app_hash.to_string(), - } - } - } - - /// `Version` contains the protocol version for the blockchain and the - /// application. - /// - /// - #[derive( - Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, ToSchema, - )] - pub struct HeaderVersion { - /// Block version - pub block: u64, - - /// App version - pub app: u64, - } - - impl From for HeaderVersion { - fn from(value: Version) -> Self { - HeaderVersion { - block: value.block, - app: value.app, - } - } - } - - /// Block identifiers which contain two distinct Merkle roots of the block, - /// as well as the number of parts in the block. - /// - /// - /// - /// Default implementation is an empty Id as defined by the Go implementation in - /// . - /// - /// If the Hash is empty in BlockId, the BlockId should be empty (encoded to None). - /// This is implemented outside of this struct. Use the Default trait to check for an empty BlockId. - /// See: - #[derive( - Serialize, - Deserialize, - Copy, - Clone, - Debug, - Default, - Hash, - Eq, - PartialEq, - PartialOrd, - Ord, - JsonSchema, - ToSchema, - )] - pub struct BlockId { - /// The block's main hash is the Merkle root of all the fields in the - /// block header. - #[schemars(with = "String")] - #[schema(value_type = String)] - pub hash: Hash, - - /// Parts header (if available) is used for secure gossipping of the block - /// during consensus. It is the Merkle root of the complete serialized block - /// cut into parts. - /// - /// PartSet is used to split a byteslice of data into parts (pieces) for - /// transmission. By splitting data into smaller parts and computing a - /// Merkle root hash on the list, you can verify that a part is - /// legitimately part of the complete data, and the part can be forwarded - /// to other peers before all the parts are known. In short, it's a fast - /// way to propagate a large file over a gossip network. - /// - /// - /// - /// PartSetHeader in protobuf is defined as never nil using the gogoproto - /// annotations. This does not translate to Rust, but we can indicate this - /// in the domain type. - pub part_set_header: PartSetHeader, - } - - impl From for BlockId { - fn from(value: block::Id) -> Self { - BlockId { - hash: value.hash, - part_set_header: value.part_set_header.into(), - } - } - } - - /// Block parts header - #[derive( - Clone, - Copy, - Debug, - Default, - Hash, - Eq, - PartialEq, - PartialOrd, - Ord, - Deserialize, - Serialize, - JsonSchema, - ToSchema, - )] - #[non_exhaustive] - pub struct PartSetHeader { - /// Number of parts in this block - pub total: u32, - - /// Hash of the parts set header, - #[schemars(with = "String")] - #[schema(value_type = String)] - pub hash: Hash, - } - - impl From for PartSetHeader { - fn from(value: block::parts::Header) -> Self { - PartSetHeader { - total: value.total, - hash: value.hash, - } - } - } - - /// Block `Header` values contain metadata about the block and about the - /// consensus, as well as commitments to the data in the current block, the - /// previous block, and the results returned by the application. - /// - /// - #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] - pub struct BlockHeader { - /// Header version - pub version: HeaderVersion, - - /// Chain ID - pub chain_id: String, - - /// Current block height - pub height: u64, - - /// Current timestamp - #[schemars(with = "String")] - #[schema(value_type = String)] - pub time: tendermint::Time, - - /// Previous block info - pub last_block_id: Option, - - /// Commit from validators from the last block - #[schemars(with = "Option")] - #[schema(value_type = Option)] - pub last_commit_hash: Option, - - /// Merkle root of transaction hashes - #[schemars(with = "Option")] - #[schema(value_type = Option)] - pub data_hash: Option, - - /// Validators for the current block - #[schemars(with = "String")] - #[schema(value_type = String)] - pub validators_hash: Hash, - - /// Validators for the next block - #[schemars(with = "String")] - #[schema(value_type = String)] - pub next_validators_hash: Hash, - - /// Consensus params for the current block - #[schemars(with = "String")] - #[schema(value_type = String)] - pub consensus_hash: Hash, - - /// State after txs from the previous block - #[schemars(with = "String")] - #[schema(value_type = String)] - pub app_hash: Hash, - - /// Root hash of all results from the txs from the previous block - #[schemars(with = "Option")] - #[schema(value_type = Option)] - pub last_results_hash: Option, - - /// Hash of evidence included in the block - #[schemars(with = "Option")] - #[schema(value_type = Option)] - pub evidence_hash: Option, - - /// Original proposer of the block - #[serde(with = "nym_serde_helpers::hex")] - #[schemars(with = "String")] - #[schema(value_type = String)] - pub proposer_address: Vec, - } - - impl From for BlockHeader { - fn from(value: block::Header) -> Self { - BlockHeader { - version: value.version.into(), - chain_id: value.chain_id.to_string(), - height: value.height.value(), - time: value.time, - last_block_id: value.last_block_id.map(Into::into), - last_commit_hash: value.last_commit_hash, - data_hash: value.data_hash, - validators_hash: value.validators_hash, - next_validators_hash: value.next_validators_hash, - consensus_hash: value.consensus_hash, - app_hash: Hash::try_from(value.app_hash.as_bytes().to_vec()).unwrap_or_default(), - last_results_hash: value.last_results_hash, - evidence_hash: value.evidence_hash, - proposer_address: value.proposer_address.as_bytes().to_vec(), - } - } - } -} diff --git a/nym-api/src/support/http/state.rs b/nym-api/src/support/http/state.rs index 6bda58cb6c..224db98e53 100644 --- a/nym-api/src/support/http/state.rs +++ b/nym-api/src/support/http/state.rs @@ -3,7 +3,7 @@ use crate::circulating_supply_api::cache::CirculatingSupplyCache; use crate::ecash::state::EcashState; -use crate::network::models::{ChainStatus, NetworkDetails}; +use crate::network::models::NetworkDetails; use crate::node_describe_cache::DescribedNodes; use crate::node_status_api::handlers::unstable; use crate::node_status_api::models::AxumErrorResponse; @@ -15,7 +15,9 @@ use crate::support::caching::Cache; use crate::support::nyxd::Client; use crate::support::storage; use axum::extract::FromRef; -use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation}; +use nym_api_requests::models::{ + DetailedChainStatus, GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation, +}; use nym_mixnet_contract_common::NodeId; use nym_task::TaskManager; use nym_topology::CachedEpochRewardedSet; @@ -151,7 +153,7 @@ impl ChainStatusCache { struct ChainStatusCacheInner { last_refreshed_at: OffsetDateTime, - cache_value: ChainStatus, + cache_value: DetailedChainStatus, } impl ChainStatusCacheInner { @@ -167,7 +169,7 @@ impl ChainStatusCache { pub(crate) async fn get_or_refresh( &self, client: &Client, - ) -> Result { + ) -> Result { if let Some(cached) = self.check_cache().await { return Ok(cached); } @@ -175,7 +177,7 @@ impl ChainStatusCache { self.refresh(client).await } - async fn check_cache(&self) -> Option { + async fn check_cache(&self) -> Option { let guard = self.inner.read().await; let inner = guard.as_ref()?; if inner.is_valid(self.cache_ttl) { @@ -184,7 +186,7 @@ impl ChainStatusCache { None } - async fn refresh(&self, client: &Client) -> Result { + async fn refresh(&self, client: &Client) -> Result { // 1. attempt to get write lock permit let mut guard = self.inner.write().await; @@ -201,7 +203,7 @@ impl ChainStatusCache { .block_info(abci.last_block_height.value() as u32) .await?; - let status = ChainStatus { + let status = DetailedChainStatus { abci: abci.into(), latest_block: block.into(), }; diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 6708de9c7f..5a632330f9 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -3293,6 +3293,7 @@ dependencies = [ "serde_json", "sha2 0.10.8", "tendermint 0.40.1", + "tendermint-rpc", "thiserror 2.0.11", "time", "utoipa", diff --git a/tools/internal/validator-status-check/Cargo.toml b/tools/internal/validator-status-check/Cargo.toml new file mode 100644 index 0000000000..99eb182621 --- /dev/null +++ b/tools/internal/validator-status-check/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "validator-status-check" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +anyhow = { workspace = true } +clap = { workspace = true, features = ["cargo", "derive"] } +comfy-table = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +strum = { workspace = true, features = ["derive"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tracing = { workspace = true } +time = { workspace = true } + + +nym-validator-client = { path = "../../../common/client-libs/validator-client" } +nym-bin-common = { path = "../../../common/bin-common", features = ["output_format", "basic_tracing"] } +nym-network-defaults = { path = "../../../common/network-defaults" } + +[lints] +workspace = true diff --git a/tools/internal/validator-status-check/src/commands/build_info.rs b/tools/internal/validator-status-check/src/commands/build_info.rs new file mode 100644 index 0000000000..7b682aac12 --- /dev/null +++ b/tools/internal/validator-status-check/src/commands/build_info.rs @@ -0,0 +1,15 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_bin_common::bin_info_owned; +use nym_bin_common::output_format::OutputFormat; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) fn execute(args: Args) { + println!("{}", args.output.format(&bin_info_owned!())) +} diff --git a/tools/internal/validator-status-check/src/commands/check_network.rs b/tools/internal/validator-status-check/src/commands/check_network.rs new file mode 100644 index 0000000000..e388e90c74 --- /dev/null +++ b/tools/internal/validator-status-check/src/commands/check_network.rs @@ -0,0 +1,73 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::helpers::{get_known_dealers, get_signer_status}; +use crate::models::SignerStatus; +use comfy_table::Table; +use nym_bin_common::output_format::OutputFormat; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +#[derive(Serialize, Deserialize)] +struct NetworkStatus { + available_api_nodes: f64, + available_rpc_nodes: f64, + detailed: Vec, +} + +impl From> for NetworkStatus { + fn from(value: Vec) -> Self { + let nodes = value.len() as f64; + let api = value.iter().filter(|s| s.api_up()).count() as f64; + let rpc = value.iter().filter(|s| s.rpc_up()).count() as f64; + + NetworkStatus { + available_api_nodes: api / nodes, + available_rpc_nodes: rpc / nodes, + detailed: value, + } + } +} + +impl Display for NetworkStatus { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!( + f, + "available signers: {:.2}%, available rpc nodes: {:.2}%", + self.available_api_nodes, self.available_rpc_nodes + )?; + + let mut table = Table::new(); + table.set_header(vec![ + "signer", + "api version", + "rpc status", + "rpc endpoint", + "abci version", + ]); + for signer in &self.detailed { + table.add_row(signer.to_table_row()); + } + write!(f, "{table}") + } +} + +pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { + let dealers = get_known_dealers().await?; + + let mut signers = Vec::new(); + for dealer in dealers { + signers.push(get_signer_status(&dealer.announce_address).await) + } + + let out = args.output.format(&NetworkStatus::from(signers)); + println!("{out}"); + + Ok(()) +} diff --git a/tools/internal/validator-status-check/src/commands/check_signer.rs b/tools/internal/validator-status-check/src/commands/check_signer.rs new file mode 100644 index 0000000000..ee50e3ab04 --- /dev/null +++ b/tools/internal/validator-status-check/src/commands/check_signer.rs @@ -0,0 +1,21 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::helpers::get_signer_status; +use nym_bin_common::output_format::OutputFormat; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, + + /// api address of the specified signer + #[clap(long)] + signer: String, +} + +pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { + let out = args.output.format(&get_signer_status(&args.signer).await); + println!("{out}"); + Ok(()) +} diff --git a/tools/internal/validator-status-check/src/commands/mod.rs b/tools/internal/validator-status-check/src/commands/mod.rs new file mode 100644 index 0000000000..abf0f5c8b1 --- /dev/null +++ b/tools/internal/validator-status-check/src/commands/mod.rs @@ -0,0 +1,51 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use clap::{Parser, Subcommand}; +use nym_bin_common::bin_info; +use std::path::PathBuf; +use std::sync::OnceLock; + +mod build_info; +mod check_network; +mod check_signer; + +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Parser, Debug)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub struct Cli { + /// Path pointing to an env file that configures the CLI. + #[clap(short, long)] + pub(crate) config_env_file: Option, + + #[clap(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +pub(crate) enum Commands { + /// Check status of an individual signer + CheckSigner(check_signer::Args), + + /// Check status of all signers + CheckNetwork(check_network::Args), + + /// Show build information of this binary + BuildInfo(build_info::Args), +} + +impl Cli { + pub async fn execute(self) -> anyhow::Result<()> { + match self.command { + Commands::CheckSigner(args) => check_signer::execute(args).await?, + Commands::CheckNetwork(args) => check_network::execute(args).await?, + Commands::BuildInfo(args) => build_info::execute(args), + } + + Ok(()) + } +} diff --git a/tools/internal/validator-status-check/src/helpers.rs b/tools/internal/validator-status-check/src/helpers.rs new file mode 100644 index 0000000000..feb4dd488e --- /dev/null +++ b/tools/internal/validator-status-check/src/helpers.rs @@ -0,0 +1,39 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::models::SignerStatus; +use anyhow::bail; +use nym_network_defaults::NymNetworkDetails; +use nym_validator_client::nyxd::contract_traits::dkg_query_client::DealerDetails; +use nym_validator_client::nyxd::contract_traits::PagedDkgQueryClient; +use nym_validator_client::nyxd::Config; +use nym_validator_client::QueryHttpRpcNyxdClient; +use tracing::info; + +async fn get_query_client() -> anyhow::Result { + let network = NymNetworkDetails::new_from_env(); + + let Some(endpoint_info) = network.endpoints.first() else { + bail!("no known rpc endpoints available") + }; + + let config = Config::try_from_nym_network_details(&network)?; + Ok(QueryHttpRpcNyxdClient::connect( + config, + endpoint_info.nyxd_url.as_str(), + )?) +} + +pub(crate) async fn get_known_dealers() -> anyhow::Result> { + let client = get_query_client().await?; + Ok(client.get_all_current_dealers().await?) +} + +pub(crate) async fn get_signer_status(raw_api_endpoint: &str) -> SignerStatus { + info!("attempting to get signer status of {raw_api_endpoint}..."); + let mut status = SignerStatus::new(raw_api_endpoint.to_string()); + + status.try_update_api_version().await; + status.try_update_rpc_status().await; + status +} diff --git a/tools/internal/validator-status-check/src/main.rs b/tools/internal/validator-status-check/src/main.rs new file mode 100644 index 0000000000..668aa5dd2b --- /dev/null +++ b/tools/internal/validator-status-check/src/main.rs @@ -0,0 +1,24 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::commands::Cli; +use clap::Parser; +use nym_bin_common::logging::setup_tracing_logger; +use nym_network_defaults::setup_env; +use tracing::trace; + +mod commands; +mod helpers; +mod models; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + trace!("args: {cli:#?}"); + + setup_env(cli.config_env_file.as_ref()); + setup_tracing_logger(); + + cli.execute().await?; + Ok(()) +} diff --git a/tools/internal/validator-status-check/src/models.rs b/tools/internal/validator-status-check/src/models.rs new file mode 100644 index 0000000000..12b8337f5d --- /dev/null +++ b/tools/internal/validator-status-check/src/models.rs @@ -0,0 +1,222 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_validator_client::client::NymApiClientExt; +use nym_validator_client::NymApiClient; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use strum::{Display, EnumProperty}; +use time::{Duration, OffsetDateTime}; +use tracing::error; + +#[derive(Serialize, Deserialize)] +pub(crate) struct SignerStatus { + api_endpoint: String, + api_version: ApiVersion, + rpc_status: RpcStatus, + used_rpc_endpoint: RpcEndpoint, + abci_version: AbciVersion, +} + +impl Display for SignerStatus { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + writeln!(f, "api_endpoint: {}", self.api_endpoint)?; + writeln!(f, "api_version: {}", self.api_version)?; + writeln!(f, "rpc_status: {}", self.rpc_status)?; + writeln!(f, "used_rpc_endpoint: {}", self.used_rpc_endpoint)?; + writeln!(f, "abci_version: {}", self.abci_version)?; + Ok(()) + } +} + +impl SignerStatus { + pub(crate) fn new(api_endpoint: String) -> Self { + SignerStatus { + api_endpoint, + api_version: Default::default(), + rpc_status: Default::default(), + used_rpc_endpoint: Default::default(), + abci_version: Default::default(), + } + } + + pub(crate) fn api_up(&self) -> bool { + matches!(self.api_version, ApiVersion::Available { .. }) + } + + pub(crate) fn rpc_up(&self) -> bool { + matches!(self.rpc_status, RpcStatus::Up) + } + + fn build_api_client(&self) -> Option { + let api_endpoint = match self.api_endpoint.as_str().parse() { + Ok(endpoint) => endpoint, + Err(err) => { + error!("{} is not a valid api endpoint: {err}", self.api_endpoint); + return None; + } + }; + + Some(NymApiClient::new(api_endpoint)) + } + + pub(crate) async fn try_update_api_version(&mut self) { + let Some(client) = self.build_api_client() else { + return; + }; + match client.nym_api.build_information().await { + Ok(build_info) => { + self.api_version = ApiVersion::Available { + version: build_info.build_version, + }; + } + Err(err) => { + error!( + "failed to retrieve build information of {}: {err}", + self.api_endpoint + ) + } + } + } + + pub(crate) async fn try_update_rpc_status(&mut self) { + let Some(client) = self.build_api_client() else { + return; + }; + + match client.nym_api.get_chain_status().await { + Ok(chain_status) => { + self.used_rpc_endpoint = RpcEndpoint(chain_status.connected_nyxd); + let last_block = + OffsetDateTime::from(chain_status.status.latest_block.block.header.time); + let now = OffsetDateTime::now_utc(); + let diff = now - last_block; + if diff < Duration::minutes(2) { + self.rpc_status = RpcStatus::Up + } else { + self.rpc_status = RpcStatus::Down + } + self.abci_version = AbciVersion::Available { + version: chain_status.status.abci.version, + } + } + Err(err) => { + error!( + "failed to retrieve chain status from {}: {err}", + self.api_endpoint + ); + } + } + } + + pub(crate) fn to_table_row(&self) -> Vec { + vec![ + self.api_endpoint.to_string(), + self.api_version.as_cell(), + self.rpc_status.as_cell(), + self.used_rpc_endpoint.as_cell(), + self.abci_version.as_cell(), + ] + } +} + +#[derive(Serialize, Deserialize, Default)] +struct RpcEndpoint(String); + +impl Display for RpcEndpoint { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + self.0.fmt(f) + } +} + +impl RpcEndpoint { + fn as_cell(&self) -> String { + if self.0.contains("localhost") || self.0.contains("127.0.0.1") { + format!("✅ {}", self.0) + } else if self.0.contains("nymtech") { + format!("❗ {}", self.0) + } else if self.0.is_empty() { + "⚠️ unknown".to_string() + } else { + format!("⚠️ {}", self.0) + } + } +} + +#[derive( + Clone, Default, PartialOrd, PartialEq, Ord, Eq, Display, EnumProperty, Serialize, Deserialize, +)] +#[strum(serialize_all = "snake_case")] +enum AbciVersion { + #[strum(props(emoji = "✅"))] + #[strum(to_string = "{version}")] + Available { version: String }, + + #[strum(props(emoji = "❗"))] + #[default] + Unavailable, +} + +impl AbciVersion { + // SAFETY: every variant has a `emoji` prop defined + #[allow(clippy::unwrap_used)] + fn as_cell(&self) -> String { + format!("{} {}", self.get_str("emoji").unwrap(), self) + } +} + +#[derive( + Clone, Default, PartialOrd, PartialEq, Ord, Eq, Display, EnumProperty, Serialize, Deserialize, +)] +#[strum(serialize_all = "snake_case")] +enum ApiVersion { + #[strum(props(emoji = "✅"))] + #[strum(to_string = "{version}")] + Available { version: String }, + + #[strum(props(emoji = "❗"))] + #[default] + Unavailable, +} + +impl ApiVersion { + // SAFETY: every variant has a `emoji` prop defined + #[allow(clippy::unwrap_used)] + fn as_cell(&self) -> String { + format!("{} {}", self.get_str("emoji").unwrap(), self) + } +} + +#[derive( + Copy, + Clone, + Default, + PartialOrd, + PartialEq, + Ord, + Eq, + Display, + EnumProperty, + Serialize, + Deserialize, +)] +#[strum(serialize_all = "snake_case")] +enum RpcStatus { + #[strum(props(emoji = "❗"))] + Down, + + #[strum(props(emoji = "✅"))] + Up, + + #[strum(props(emoji = "⚠️"))] + #[default] + Unknown, +} + +impl RpcStatus { + // SAFETY: every variant has a `emoji` prop defined + #[allow(clippy::unwrap_used)] + fn as_cell(&self) -> String { + format!("{} {}", self.get_str("emoji").unwrap(), self) + } +} From 7d59a2477a93466279fd1a6371aca5652d5d165f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 12 Mar 2025 11:02:19 +0000 Subject: [PATCH 078/133] chore: change auth v2 timestamp skew and allow values from the future (#5604) * chore: change auth v2 timestamp skew and allow values from the future * made the if statement more readable --- common/gateway-requests/src/types/error.rs | 13 ++++--- .../src/types/text_request/authenticate.rs | 19 +++++++--- contracts/Cargo.lock | 38 +++++++++++++++++++ gateway/src/config.rs | 4 +- .../client_handling/websocket/common_state.rs | 2 +- .../websocket/connection_handler/fresh.rs | 2 +- gateway/src/node/mod.rs | 2 +- nym-node/src/config/gateway_tasks.rs | 9 +++-- nym-node/src/config/helpers.rs | 2 +- 9 files changed, 70 insertions(+), 21 deletions(-) diff --git a/common/gateway-requests/src/types/error.rs b/common/gateway-requests/src/types/error.rs index 0a785e60ec..edd8e41b22 100644 --- a/common/gateway-requests/src/types/error.rs +++ b/common/gateway-requests/src/types/error.rs @@ -10,6 +10,7 @@ use nym_sphinx::params::packet_sizes::PacketSize; use serde::{Deserialize, Serialize}; use std::string::FromUtf8Error; use thiserror::Error; +use time::OffsetDateTime; // specific errors (that should not be nested!!) for clients to match on #[derive(Debug, Copy, Clone, Error, Serialize, Deserialize)] @@ -112,15 +113,15 @@ pub enum AuthenticationFailure { #[error("failed to verify request signature")] InvalidSignature(#[from] SignatureError), - #[error("provided request timestamp is in the future")] - RequestTimestampInFuture, - #[error("the client is not registered")] NotRegistered, - #[error("the provided request is too stale to process")] - StaleRequest, + #[error("the provided request timestamp is excessively skewed. got {received} whilst the server time is {server}")] + ExcessiveTimestampSkew { + received: OffsetDateTime, + server: OffsetDateTime, + }, - #[error("the provided request timestamp is smaller or equal to a one previously used")] + #[error("the provided request timestamp is smaller or equal to one previously used")] RequestReuse, } diff --git a/common/gateway-requests/src/types/text_request/authenticate.rs b/common/gateway-requests/src/types/text_request/authenticate.rs index 6015beb8ad..6c4c884957 100644 --- a/common/gateway-requests/src/types/text_request/authenticate.rs +++ b/common/gateway-requests/src/types/text_request/authenticate.rs @@ -38,13 +38,22 @@ impl AuthenticateRequest { }) } - pub fn verify_timestamp(&self, max_request_age: Duration) -> Result<(), AuthenticationFailure> { + pub fn verify_timestamp( + &self, + max_request_timestamp_skew: Duration, + ) -> Result<(), AuthenticationFailure> { let now = OffsetDateTime::now_utc(); - if self.content.request_timestamp() + max_request_age < now { - return Err(AuthenticationFailure::StaleRequest); + if self.content.request_timestamp() < now - max_request_timestamp_skew { + return Err(AuthenticationFailure::ExcessiveTimestampSkew { + received: self.content.request_timestamp(), + server: now, + }); } - if self.content.request_timestamp() > now { - return Err(AuthenticationFailure::RequestTimestampInFuture); + if self.content.request_timestamp() - max_request_timestamp_skew > now { + return Err(AuthenticationFailure::ExcessiveTimestampSkew { + received: self.content.request_timestamp(), + server: now, + }); } Ok(()) } diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index af830109c4..3f161750a1 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -1185,6 +1185,7 @@ name = "nym-pemstore" version = "0.3.0" dependencies = [ "pem", + "tracing", ] [[package]] @@ -1251,6 +1252,12 @@ dependencies = [ "regex", ] +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + [[package]] name = "pkcs8" version = "0.9.0" @@ -1840,6 +1847,37 @@ dependencies = [ "winnow", ] +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + [[package]] name = "typenum" version = "1.18.0" diff --git a/gateway/src/config.rs b/gateway/src/config.rs index 1f6db8f0b8..363ddbb88e 100644 --- a/gateway/src/config.rs +++ b/gateway/src/config.rs @@ -102,8 +102,8 @@ pub struct Debug { pub zk_nym_tickets: ZkNymTicketHandlerDebug, - /// Defines the maximum age of a signed authentication request before it's deemed too stale to process. - pub maximum_auth_request_age: Duration, + /// Defines the timestamp skew of a signed authentication request before it's deemed too excessive to process. + pub max_request_timestamp_skew: Duration, } #[derive(Debug, Clone)] diff --git a/gateway/src/node/client_handling/websocket/common_state.rs b/gateway/src/node/client_handling/websocket/common_state.rs index a571117895..8347cb007e 100644 --- a/gateway/src/node/client_handling/websocket/common_state.rs +++ b/gateway/src/node/client_handling/websocket/common_state.rs @@ -14,7 +14,7 @@ use std::time::Duration; #[derive(Clone)] pub(crate) struct Config { pub(crate) enforce_zk_nym: bool, - pub(crate) max_auth_request_age: Duration, + pub(crate) max_request_timestamp_skew: Duration, pub(crate) bandwidth: BandwidthFlushingBehaviourConfig, } diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index bd1f9fd601..f7f210791e 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -641,7 +641,7 @@ impl FreshHandler { // do cheap checks first // is the provided timestamp relatively recent (and not in the future?) - request.verify_timestamp(self.shared_state.cfg.max_auth_request_age)?; + request.verify_timestamp(self.shared_state.cfg.max_request_timestamp_skew)?; // does the message signature verify? request.verify_signature()?; diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index a582a24735..c13df9f241 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -251,7 +251,7 @@ impl GatewayTasksBuilder { let shared_state = websocket::CommonHandlerState { cfg: websocket::Config { enforce_zk_nym: self.config.gateway.enforce_zk_nyms, - max_auth_request_age: self.config.debug.maximum_auth_request_age, + max_request_timestamp_skew: self.config.debug.max_request_timestamp_skew, bandwidth: (&self.config).into(), }, ecash_verifier: self.ecash_manager().await?, diff --git a/nym-node/src/config/gateway_tasks.rs b/nym-node/src/config/gateway_tasks.rs index c5a4a542ed..0fcf54d9dd 100644 --- a/nym-node/src/config/gateway_tasks.rs +++ b/nym-node/src/config/gateway_tasks.rs @@ -54,8 +54,9 @@ pub struct Debug { /// of the services providers pub minimum_mix_performance: u8, - /// Defines the maximum age of a signed authentication request before it's deemed too stale to process. - pub maximum_auth_request_age: Duration, + /// Defines the timestamp skew of a signed authentication request before it's deemed too excessive to process. + #[serde(alias = "maximum_auth_request_age")] + pub max_request_timestamp_skew: Duration, pub stale_messages: StaleMessageDebug, @@ -67,7 +68,7 @@ pub struct Debug { impl Debug { pub const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; pub const DEFAULT_MINIMUM_MIX_PERFORMANCE: u8 = 50; - pub const DEFAULT_MAXIMUM_AUTH_REQUEST_AGE: Duration = Duration::from_secs(30); + pub const DEFAULT_MAXIMUM_AUTH_REQUEST_TIMESTAMP_SKEW: Duration = Duration::from_secs(120); pub const DEFAULT_MAXIMUM_OPEN_CONNECTIONS: usize = 8192; } @@ -76,7 +77,7 @@ impl Default for Debug { Debug { message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, maximum_open_connections: Self::DEFAULT_MAXIMUM_OPEN_CONNECTIONS, - maximum_auth_request_age: Self::DEFAULT_MAXIMUM_AUTH_REQUEST_AGE, + max_request_timestamp_skew: Self::DEFAULT_MAXIMUM_AUTH_REQUEST_TIMESTAMP_SKEW, minimum_mix_performance: Self::DEFAULT_MINIMUM_MIX_PERFORMANCE, stale_messages: Default::default(), client_bandwidth: Default::default(), diff --git a/nym-node/src/config/helpers.rs b/nym-node/src/config/helpers.rs index a56e0442e0..0dae4e17db 100644 --- a/nym-node/src/config/helpers.rs +++ b/nym-node/src/config/helpers.rs @@ -60,7 +60,7 @@ fn ephemeral_gateway_config(config: &Config) -> nym_gateway::config::Config { .zk_nym_tickets .maximum_time_between_redemption, }, - maximum_auth_request_age: config.gateway_tasks.debug.maximum_auth_request_age, + max_request_timestamp_skew: config.gateway_tasks.debug.max_request_timestamp_skew, }, ) } From f8d863249ef991f4ad09df2eaceff8436daab52e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 12 Mar 2025 11:02:34 +0000 Subject: [PATCH 079/133] Merge pull request #5605 from nymtech/chore/update-bls12_381-fork Chore/update bls12 381 fork --- Cargo.lock | 12 +-- Cargo.toml | 2 +- common/dkg/Cargo.toml | 2 +- common/dkg/src/utils.rs | 98 ++++++++++++++++++- common/nym_offline_compact_ecash/Cargo.toml | 4 +- common/nym_offline_compact_ecash/src/utils.rs | 79 +++++++++++++-- nym-api/Cargo.toml | 2 +- nym-api/src/ecash/tests/mod.rs | 2 +- nym-wallet/Cargo.lock | 14 +-- 9 files changed, 188 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8d0e1dba7c..66f91ff74c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -816,9 +816,9 @@ dependencies = [ [[package]] name = "bls12_381" version = "0.8.0" -source = "git+https://github.com/jstuczyn/bls12_381?branch=temp/experimental-serdect#22cd0a16b674af1629110a2dc8b6cf6c73ea4cd9" +source = "git+https://github.com/jstuczyn/bls12_381?branch=temp/experimental-serdect-updated#9bf520059cb28323fc51469cae86868ef4fa6fbd" dependencies = [ - "digest 0.9.0", + "digest 0.10.7", "ff", "group", "pairing", @@ -4850,7 +4850,7 @@ dependencies = [ "semver 1.0.26", "serde", "serde_json", - "sha2 0.9.9", + "sha2 0.10.8", "sqlx", "tempfile", "tendermint 0.40.1", @@ -5331,7 +5331,7 @@ dependencies = [ "bs58", "cfg-if", "criterion", - "digest 0.9.0", + "digest 0.10.7", "ff", "group", "itertools 0.14.0", @@ -5340,7 +5340,7 @@ dependencies = [ "rand 0.8.5", "rayon", "serde", - "sha2 0.9.9", + "sha2 0.10.8", "subtle 2.6.1", "thiserror 2.0.12", "zeroize", @@ -5613,7 +5613,7 @@ dependencies = [ "rand_core 0.6.4", "serde", "serde_derive", - "sha2 0.9.9", + "sha2 0.10.8", "thiserror 2.0.12", "zeroize", ] diff --git a/Cargo.toml b/Cargo.toml index 491f5d89cd..8ecb2f35ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -370,7 +370,7 @@ prometheus = { version = "0.13.0" } # unfortunately until https://github.com/zkcrypto/bls12_381/issues/10 is resolved, we have to rely on the fork # as we need to be able to serialize Gt so that we could create the lookup table for baby-step-giant-step algorithm # plus to make our live easier we need serde support from https://github.com/zkcrypto/bls12_381/pull/125 -bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", default-features = false, branch = "temp/experimental-serdect" } +bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", default-features = false, branch = "temp/experimental-serdect-updated" } group = { version = "0.13.0", default-features = false } ff = { version = "0.13.1", default-features = false } subtle = "2.5.0" diff --git a/common/dkg/Cargo.toml b/common/dkg/Cargo.toml index 799e38a702..58e899d7b3 100644 --- a/common/dkg/Cargo.toml +++ b/common/dkg/Cargo.toml @@ -21,7 +21,7 @@ lazy_static = { workspace = true } rand = { workspace = true } rand_chacha = { workspace = true } rand_core = { workspace = true } -sha2 = "0.9" +sha2 = { workspace = true } serde = { workspace = true } serde_derive = { workspace = true } thiserror = { workspace = true } diff --git a/common/dkg/src/utils.rs b/common/dkg/src/utils.rs index 28fb99ad5d..e654a83f8c 100644 --- a/common/dkg/src/utils.rs +++ b/common/dkg/src/utils.rs @@ -54,12 +54,12 @@ pub(crate) fn hash_to_scalar>(msg: M, domain: &[u8]) -> Scalar { pub(crate) fn hash_to_scalars>(msg: M, domain: &[u8], n: usize) -> Vec { let mut output = vec![Scalar::zero(); n]; - Scalar::hash_to_field::>(msg.as_ref(), domain, &mut output); + Scalar::hash_to_field::, _>([msg], domain, &mut output); output } pub(crate) fn hash_g2>(msg: M, domain: &[u8]) -> G2Projective { - >>::hash_to_curve(msg, domain) + >>::hash_to_curve([msg], domain) } pub(crate) fn combine_scalar_chunks(chunks: &[Scalar]) -> Scalar { @@ -112,3 +112,97 @@ pub(crate) fn deserialize_g2(b: &[u8]) -> Option { G2Projective::from_bytes(&encoding).into() } } + +#[cfg(test)] +mod tests { + use super::*; + use bls12_381::G2Affine; + + #[test] + fn test_hash_to_scalar() { + let msg1 = "foo"; + let expected1 = Scalar::from_bytes(&[ + 253, 57, 224, 227, 175, 195, 226, 82, 46, 175, 33, 126, 171, 239, 255, 92, 108, 168, 6, + 79, 90, 11, 235, 236, 221, 10, 85, 133, 42, 81, 95, 30, + ]) + .unwrap(); + + let msg2 = "bar"; + let expected2 = Scalar::from_bytes(&[ + 48, 83, 69, 52, 42, 18, 135, 244, 211, 190, 160, 196, 118, 154, 24, 126, 0, 125, 72, + 201, 170, 225, 123, 201, 52, 120, 171, 132, 235, 182, 20, 26, + ]) + .unwrap(); + + let msg3 = [ + 33, 135, 76, 234, 71, 35, 247, 216, 39, 242, 42, 88, 152, 29, 74, 135, 9, 29, 216, 123, + 250, 87, 108, 29, 245, 126, 109, 102, 84, 71, 158, 224, 145, 243, 49, 121, 244, 27, + 115, 121, 25, 66, 216, 67, 97, 101, 140, 160, 77, 239, 114, 215, 152, 48, 15, 231, 101, + 60, 42, 92, 128, 131, 161, 43, + ]; + let expected3 = Scalar::from_bytes(&[ + 128, 189, 8, 43, 186, 55, 52, 61, 171, 196, 159, 177, 162, 100, 27, 143, 85, 83, 218, + 171, 91, 220, 155, 25, 7, 38, 2, 36, 4, 93, 136, 4, + ]) + .unwrap(); + + assert_eq!( + hash_to_scalar(msg1, b"NYMECASH-V01-CS02-with-expander-SHA256"), + expected1 + ); + assert_eq!( + hash_to_scalar(msg2, b"NYMECASH-V01-CS02-with-expander-SHA256"), + expected2 + ); + assert_eq!( + hash_to_scalar(msg3, b"NYMECASH-V01-CS02-with-expander-SHA256"), + expected3 + ); + } + + #[test] + fn test_hash_g2() { + let msg1 = "foo"; + let expected1 = G2Affine::from_compressed(&[ + 175, 187, 62, 7, 29, 17, 42, 93, 28, 93, 234, 253, 101, 166, 158, 187, 153, 82, 93, 18, + 11, 233, 36, 107, 51, 117, 30, 127, 32, 254, 210, 77, 133, 12, 253, 255, 84, 128, 36, + 214, 234, 103, 50, 21, 26, 78, 112, 49, 20, 69, 19, 109, 7, 78, 33, 227, 196, 180, 168, + 219, 73, 251, 192, 221, 41, 138, 160, 131, 191, 186, 156, 117, 179, 179, 191, 235, 171, + 26, 219, 148, 170, 179, 11, 38, 137, 14, 95, 115, 171, 186, 163, 82, 158, 6, 239, 88, + ]) + .unwrap() + .into(); + + let msg2 = "bar"; + let expected2 = G2Affine::from_compressed(&[ + 183, 25, 90, 187, 34, 184, 30, 182, 215, 242, 158, 83, 116, 34, 210, 96, 188, 79, 83, + 255, 100, 122, 90, 188, 196, 93, 164, 253, 20, 106, 205, 33, 48, 140, 60, 149, 66, 246, + 121, 244, 146, 66, 170, 60, 113, 95, 102, 237, 25, 231, 8, 42, 121, 124, 180, 140, 34, + 104, 173, 251, 89, 189, 28, 196, 49, 66, 101, 38, 68, 44, 40, 235, 21, 35, 204, 123, + 218, 238, 216, 92, 134, 217, 212, 246, 176, 77, 187, 0, 245, 134, 132, 73, 31, 44, 137, + 197, + ]) + .unwrap() + .into(); + let msg3 = [ + 33, 135, 76, 234, 71, 35, 247, 216, 39, 242, 42, 88, 152, 29, 74, 135, 9, 29, 216, 123, + 250, 87, 108, 29, 245, 126, 109, 102, 84, 71, 158, 224, 145, 243, 49, 121, 244, 27, + 115, 121, 25, 66, 216, 67, 97, 101, 140, 160, 77, 239, 114, 215, 152, 48, 15, 231, 101, + 60, 42, 92, 128, 131, 161, 43, + ]; + let expected3 = G2Affine::from_compressed(&[ + 151, 185, 8, 123, 223, 150, 192, 192, 115, 10, 3, 129, 49, 179, 31, 108, 0, 17, 46, + 231, 184, 164, 247, 228, 22, 142, 87, 70, 120, 111, 154, 15, 245, 110, 32, 84, 53, 117, + 239, 93, 89, 119, 32, 17, 39, 250, 198, 137, 6, 95, 137, 202, 54, 244, 238, 190, 11, + 217, 237, 95, 72, 59, 140, 56, 3, 42, 61, 195, 192, 101, 46, 204, 207, 75, 70, 176, + 207, 48, 24, 195, 248, 234, 178, 168, 54, 109, 19, 189, 51, 52, 120, 69, 248, 226, 102, + 91, + ]) + .unwrap() + .into(); + + assert_eq!(hash_g2(msg1, b"DUMMY_TEST_DOMAIN"), expected1); + assert_eq!(hash_g2(msg2, b"DUMMY_TEST_DOMAIN"), expected2); + assert_eq!(hash_g2(msg3, b"DUMMY_TEST_DOMAIN"), expected3); + } +} diff --git a/common/nym_offline_compact_ecash/Cargo.toml b/common/nym_offline_compact_ecash/Cargo.toml index 9b29d818fe..718787b457 100644 --- a/common/nym_offline_compact_ecash/Cargo.toml +++ b/common/nym_offline_compact_ecash/Cargo.toml @@ -15,10 +15,10 @@ bls12_381 = { workspace = true, features = ["alloc", "pairings", "experimental", bincode.workspace = true cfg-if.workspace = true itertools = { workspace = true } -digest = "0.9" +digest = { workspace = true } rand = { workspace = true } thiserror = { workspace = true } -sha2 = "0.9" +sha2 = { workspace = true } bs58 = { workspace = true } serde = { workspace = true, features = ["derive"] } rayon = { workspace = true, optional = true } diff --git a/common/nym_offline_compact_ecash/src/utils.rs b/common/nym_offline_compact_ecash/src/utils.rs index 57a648fff2..a04f377ddf 100644 --- a/common/nym_offline_compact_ecash/src/utils.rs +++ b/common/nym_offline_compact_ecash/src/utils.rs @@ -113,17 +113,13 @@ const G1_HASH_DOMAIN: &[u8] = b"NYMECASH-V01-CS02-with-BLS12381G1_XMD:SHA-256_SS const SCALAR_HASH_DOMAIN: &[u8] = b"NYMECASH-V01-CS02-with-expander-SHA256"; pub fn hash_g1>(msg: M) -> G1Projective { - >>::hash_to_curve(msg, G1_HASH_DOMAIN) + >>::hash_to_curve([msg], G1_HASH_DOMAIN) } pub fn hash_to_scalar>(msg: M) -> Scalar { let mut output = vec![Scalar::zero()]; - Scalar::hash_to_field::>( - msg.as_ref(), - SCALAR_HASH_DOMAIN, - &mut output, - ); + Scalar::hash_to_field::, _>([msg], SCALAR_HASH_DOMAIN, &mut output); output[0] } @@ -401,4 +397,75 @@ mod tests { assert_eq!(hash_to_scalar(msg2), hash_to_scalar(msg2)); assert_ne!(hash_to_scalar(msg1), hash_to_scalar(msg2)); } + + #[test] + fn test_hash_to_scalar() { + let msg1 = "foo"; + let expected1 = Scalar::from_bytes(&[ + 253, 57, 224, 227, 175, 195, 226, 82, 46, 175, 33, 126, 171, 239, 255, 92, 108, 168, 6, + 79, 90, 11, 235, 236, 221, 10, 85, 133, 42, 81, 95, 30, + ]) + .unwrap(); + + let msg2 = "bar"; + let expected2 = Scalar::from_bytes(&[ + 48, 83, 69, 52, 42, 18, 135, 244, 211, 190, 160, 196, 118, 154, 24, 126, 0, 125, 72, + 201, 170, 225, 123, 201, 52, 120, 171, 132, 235, 182, 20, 26, + ]) + .unwrap(); + + let msg3 = [ + 33, 135, 76, 234, 71, 35, 247, 216, 39, 242, 42, 88, 152, 29, 74, 135, 9, 29, 216, 123, + 250, 87, 108, 29, 245, 126, 109, 102, 84, 71, 158, 224, 145, 243, 49, 121, 244, 27, + 115, 121, 25, 66, 216, 67, 97, 101, 140, 160, 77, 239, 114, 215, 152, 48, 15, 231, 101, + 60, 42, 92, 128, 131, 161, 43, + ]; + let expected3 = Scalar::from_bytes(&[ + 128, 189, 8, 43, 186, 55, 52, 61, 171, 196, 159, 177, 162, 100, 27, 143, 85, 83, 218, + 171, 91, 220, 155, 25, 7, 38, 2, 36, 4, 93, 136, 4, + ]) + .unwrap(); + + assert_eq!(hash_to_scalar(msg1), expected1); + assert_eq!(hash_to_scalar(msg2), expected2); + assert_eq!(hash_to_scalar(msg3), expected3); + } + + #[test] + fn test_hash_to_g1() { + let msg1 = "foo"; + let expected1 = G1Affine::from_compressed(&[ + 161, 109, 186, 0, 192, 221, 83, 87, 71, 31, 120, 201, 185, 35, 62, 239, 46, 120, 117, + 150, 191, 227, 128, 161, 78, 201, 207, 167, 86, 181, 229, 115, 2, 6, 178, 16, 251, 118, + 219, 115, 184, 96, 2, 10, 31, 63, 150, 70, + ]) + .unwrap() + .into(); + + let msg2 = "bar"; + let expected2 = G1Affine::from_compressed(&[ + 135, 102, 204, 42, 221, 49, 209, 192, 250, 87, 59, 255, 197, 93, 37, 113, 38, 2, 154, + 233, 68, 234, 206, 182, 121, 212, 166, 210, 74, 155, 190, 33, 203, 237, 176, 60, 249, + 241, 53, 170, 18, 168, 49, 35, 1, 151, 205, 174, + ]) + .unwrap() + .into(); + let msg3 = [ + 33, 135, 76, 234, 71, 35, 247, 216, 39, 242, 42, 88, 152, 29, 74, 135, 9, 29, 216, 123, + 250, 87, 108, 29, 245, 126, 109, 102, 84, 71, 158, 224, 145, 243, 49, 121, 244, 27, + 115, 121, 25, 66, 216, 67, 97, 101, 140, 160, 77, 239, 114, 215, 152, 48, 15, 231, 101, + 60, 42, 92, 128, 131, 161, 43, + ]; + let expected3 = G1Affine::from_compressed(&[ + 184, 200, 211, 115, 47, 45, 39, 185, 105, 9, 222, 247, 132, 241, 121, 130, 238, 224, + 155, 109, 105, 201, 137, 154, 132, 149, 214, 233, 136, 69, 77, 132, 174, 30, 46, 123, + 20, 92, 219, 18, 45, 29, 208, 127, 158, 145, 130, 41, + ]) + .unwrap() + .into(); + + assert_eq!(hash_g1(msg1), expected1); + assert_eq!(hash_g1(msg2), expected2); + assert_eq!(hash_g1(msg3), expected3); + } } diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index aa6b966b7f..5c4d336f86 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -141,7 +141,7 @@ tempfile = { workspace = true } cw3 = { workspace = true } cw-utils = { workspace = true } rand_chacha = { workspace = true } -sha2 = "0.9" +sha2 = { workspace = true } [lints] workspace = true diff --git a/nym-api/src/ecash/tests/mod.rs b/nym-api/src/ecash/tests/mod.rs index e1f0c38083..3afd91f7e7 100644 --- a/nym-api/src/ecash/tests/mod.rs +++ b/nym-api/src/ecash/tests/mod.rs @@ -109,7 +109,7 @@ impl InternalCounters { // just hash the current counter self.tx_hash_counter += 1; - Hash::Sha256(sha2::Sha256::digest(&self.tx_hash_counter.to_be_bytes()).into()) + Hash::Sha256(sha2::Sha256::digest(self.tx_hash_counter.to_be_bytes()).into()) } #[allow(dead_code)] diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 5a632330f9..f4dc00b70a 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -385,15 +385,15 @@ dependencies = [ [[package]] name = "bls12_381" version = "0.8.0" -source = "git+https://github.com/jstuczyn/bls12_381?branch=temp/experimental-serdect#22cd0a16b674af1629110a2dc8b6cf6c73ea4cd9" +source = "git+https://github.com/jstuczyn/bls12_381?branch=temp/experimental-serdect-updated#9bf520059cb28323fc51469cae86868ef4fa6fbd" dependencies = [ - "digest 0.9.0", + "digest 0.10.7", "ff", "group", "pairing", "rand_core 0.6.4", "serde", - "serdect 0.3.0-pre.0", + "serdect 0.3.0", "subtle", "zeroize", ] @@ -3342,7 +3342,7 @@ dependencies = [ "bls12_381", "bs58", "cfg-if", - "digest 0.9.0", + "digest 0.10.7", "ff", "group", "itertools 0.14.0", @@ -3350,7 +3350,7 @@ dependencies = [ "nym-pemstore", "rand 0.8.5", "serde", - "sha2 0.9.9", + "sha2 0.10.8", "subtle", "thiserror 2.0.11", "zeroize", @@ -5198,9 +5198,9 @@ dependencies = [ [[package]] name = "serdect" -version = "0.3.0-pre.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791ef964bfaba6be28a5c3f0c56836e17cb711ac009ca1074b9c735a3ebf240a" +checksum = "f42f67da2385b51a5f9652db9c93d78aeaf7610bf5ec366080b6de810604af53" dependencies = [ "base16ct", "serde", From 66792f57ed20d693d521ad2433b12babcb8f71e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 11:02:53 +0000 Subject: [PATCH 080/133] build(deps): bump @babel/helpers from 7.24.4 to 7.26.10 (#5606) Bumps [@babel/helpers](https://github.com/babel/babel/tree/HEAD/packages/babel-helpers) from 7.24.4 to 7.26.10. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-helpers) --- updated-dependencies: - dependency-name: "@babel/helpers" dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 54 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7f26c95e1c..fd515a7129 100644 --- a/yarn.lock +++ b/yarn.lock @@ -35,6 +35,15 @@ "@babel/highlight" "^7.24.2" picocolors "^1.0.0" +"@babel/code-frame@^7.26.2": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.5", "@babel/compat-data@^7.24.4": version "7.24.4" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.4.tgz#6f102372e9094f25d908ca0d34fc74c74606059a" @@ -273,11 +282,21 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz#f99c36d3593db9540705d0739a1f10b5e20c696e" integrity sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ== +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + "@babel/helper-validator-identifier@^7.22.20": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + "@babel/helper-validator-option@^7.23.5": version "7.23.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" @@ -293,13 +312,12 @@ "@babel/types" "^7.22.19" "@babel/helpers@^7.12.5", "@babel/helpers@^7.24.4": - version "7.24.4" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.4.tgz#dc00907fd0d95da74563c142ef4cd21f2cb856b6" - integrity sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw== + version "7.26.10" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.10.tgz#6baea3cd62ec2d0c1068778d63cb1314f6637384" + integrity sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g== dependencies: - "@babel/template" "^7.24.0" - "@babel/traverse" "^7.24.1" - "@babel/types" "^7.24.0" + "@babel/template" "^7.26.9" + "@babel/types" "^7.26.10" "@babel/highlight@^7.10.4", "@babel/highlight@^7.24.2": version "7.24.2" @@ -316,6 +334,13 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.4.tgz#234487a110d89ad5a3ed4a8a566c36b9453e8c88" integrity sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg== +"@babel/parser@^7.26.9": + version "7.26.10" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.10.tgz#e9bdb82f14b97df6569b0b038edd436839c57749" + integrity sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA== + dependencies: + "@babel/types" "^7.26.10" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.24.4": version "7.24.4" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.4.tgz#6125f0158543fb4edf1c22f322f3db67f21cb3e1" @@ -1242,6 +1267,15 @@ "@babel/parser" "^7.24.0" "@babel/types" "^7.24.0" +"@babel/template@^7.26.9": + version "7.26.9" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.26.9.tgz#4577ad3ddf43d194528cff4e1fa6b232fa609bb2" + integrity sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA== + dependencies: + "@babel/code-frame" "^7.26.2" + "@babel/parser" "^7.26.9" + "@babel/types" "^7.26.9" + "@babel/traverse@^7.1.6", "@babel/traverse@^7.12.11", "@babel/traverse@^7.12.9", "@babel/traverse@^7.13.0", "@babel/traverse@^7.24.1", "@babel/traverse@^7.7.2": version "7.24.1" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.1.tgz#d65c36ac9dd17282175d1e4a3c49d5b7988f530c" @@ -1267,6 +1301,14 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" +"@babel/types@^7.26.10", "@babel/types@^7.26.9": + version "7.26.10" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.10.tgz#396382f6335bd4feb65741eacfc808218f859259" + integrity sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@base2/pretty-print-object@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz#371ba8be66d556812dc7fb169ebc3c08378f69d4" From e9a7b48da0c4174b8f80b82c8451799f1be881d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 12 Mar 2025 12:57:17 +0100 Subject: [PATCH 081/133] Export lane queue lengths in sdk (#5609) --- common/task/src/connections.rs | 4 ++++ sdk/rust/nym-sdk/src/mixnet.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/common/task/src/connections.rs b/common/task/src/connections.rs index af13111526..6739ecf9f5 100644 --- a/common/task/src/connections.rs +++ b/common/task/src/connections.rs @@ -103,4 +103,8 @@ impl LaneQueueLengthsInner { { self.map.entry(*lane).and_modify(f); } + + pub fn total(&self) -> usize { + self.map.values().sum() + } } diff --git a/sdk/rust/nym-sdk/src/mixnet.rs b/sdk/rust/nym-sdk/src/mixnet.rs index 6fc8cae2dc..b540aa752d 100644 --- a/sdk/rust/nym-sdk/src/mixnet.rs +++ b/sdk/rust/nym-sdk/src/mixnet.rs @@ -82,7 +82,7 @@ pub use nym_sphinx::{ pub use nym_statistics_common::clients::{ connection::ConnectionStatsEvent, ClientStatsEvents, ClientStatsSender, }; -pub use nym_task::connections::TransmissionLane; +pub use nym_task::connections::{LaneQueueLengths, TransmissionLane}; pub use nym_topology::{provider_trait::TopologyProvider, NymTopology}; pub use paths::StoragePaths; pub use socks5_client::Socks5MixnetClient; From a8403b585b8f6a678d1f869c650a00c7f6ed7d77 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 11:58:12 +0000 Subject: [PATCH 082/133] build(deps-dev): bump webpack in /wasm/mix-fetch/internal-dev (#5597) Bumps [webpack](https://github.com/webpack/webpack) from 5.77.0 to 5.98.0. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.77.0...v5.98.0) --- updated-dependencies: - dependency-name: webpack dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- wasm/mix-fetch/internal-dev/package-lock.json | 508 +++++++----------- wasm/mix-fetch/internal-dev/package.json | 2 +- wasm/mix-fetch/internal-dev/yarn.lock | 500 +++++++++-------- 3 files changed, 443 insertions(+), 567 deletions(-) diff --git a/wasm/mix-fetch/internal-dev/package-lock.json b/wasm/mix-fetch/internal-dev/package-lock.json index 15448adb31..a338e041af 100644 --- a/wasm/mix-fetch/internal-dev/package-lock.json +++ b/wasm/mix-fetch/internal-dev/package-lock.json @@ -18,7 +18,7 @@ "devDependencies": { "copy-webpack-plugin": "^11.0.0", "hello-wasm-pack": "^0.1.0", - "webpack": "^5.70.0", + "webpack": "^5.98.0", "webpack-cli": "^4.9.2", "webpack-dev-server": "^4.7.4" } @@ -42,24 +42,24 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "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/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "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": { @@ -67,9 +67,9 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "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": { @@ -77,32 +77,32 @@ } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "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/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "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.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@leichtgewicht/ip-codec": { @@ -197,9 +197,9 @@ } }, "node_modules/@types/eslint": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.37.0.tgz", - "integrity": "sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", "dependencies": { @@ -208,9 +208,9 @@ } }, "node_modules/@types/eslint-scope": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", - "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "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": { @@ -219,9 +219,9 @@ } }, "node_modules/@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true, "license": "MIT" }, @@ -344,73 +344,73 @@ } }, "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==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "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==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true, "license": "MIT" }, "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==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true, "license": "MIT" }, "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==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true, "license": "MIT" }, "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==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "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==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true, "license": "MIT" }, "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==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "license": "MIT", "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.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "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==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, "license": "MIT", "dependencies": { @@ -418,9 +418,9 @@ } }, "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==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -428,79 +428,79 @@ } }, "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==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true, "license": "MIT" }, "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==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, "license": "MIT", "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.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "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==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, "license": "MIT", "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.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "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==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, "license": "MIT", "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.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "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==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, "license": "MIT", "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.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "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==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -572,9 +572,9 @@ } }, "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", "bin": { @@ -584,16 +584,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/ajv": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", @@ -773,9 +763,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", - "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "dev": true, "funding": [ { @@ -785,14 +775,18 @@ { "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.30001449", - "electron-to-chromium": "^1.4.284", - "node-releases": "^2.0.8", - "update-browserslist-db": "^1.0.10" + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -850,9 +844,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001474", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001474.tgz", - "integrity": "sha512-iaIZ8gVrWfemh5DG3T9/YqarVZoYf0r188IjaGwx68j4Pf0SGY6CQkmJUIE+NZHkkecQGohzXmBGEwWDr9aM3Q==", + "version": "1.0.30001703", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001703.tgz", + "integrity": "sha512-kRlAGTRWgPsOj7oARC9m1okJEXdL/8fekFVcxA8Hl7GH4r/sN4OJn/i6Flde373T50KS7Y37oFbMwlE8+F42kQ==", "dev": true, "funding": [ { @@ -1210,9 +1204,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.4.352", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.352.tgz", - "integrity": "sha512-ikFUEyu5/q+wJpMOxWxTaEVk2M1qKqTGKKyfJmod1CPZxKfYnxVS41/GCBQg21ItBpZybyN8sNpRqCUGm+Zc4Q==", + "version": "1.5.114", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.114.tgz", + "integrity": "sha512-DFptFef3iktoKlFQK/afbo274/XNWD00Am0xa7M8FZUepHlHT8PEuiNBoRfFHbH1okqN58AlhbJ4QTkcnXorjA==", "dev": true, "license": "ISC" }, @@ -1227,9 +1221,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", - "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", "dev": true, "license": "MIT", "dependencies": { @@ -1274,9 +1268,9 @@ } }, "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==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", "dev": true, "license": "MIT" }, @@ -1294,9 +1288,9 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "engines": { @@ -1499,13 +1493,6 @@ "node": ">= 6" } }, - "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, - "license": "MIT" - }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -2487,9 +2474,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", - "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true, "license": "MIT" }, @@ -2721,9 +2708,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, @@ -3066,19 +3053,19 @@ "license": "MIT" }, "node_modules/schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "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" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", @@ -3148,9 +3135,9 @@ "license": "MIT" }, "node_modules/serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "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": { @@ -3576,14 +3563,14 @@ } }, "node_modules/terser": { - "version": "5.16.8", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.8.tgz", - "integrity": "sha512-QI5g1E/ef7d+PsDifb+a6nnVgC4F22Bg6T0xrBrz6iloVB4PUkkunp6V8nzoOOZJIzjWVdAGqCdlKlhLq/TbIA==", + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", + "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -3595,17 +3582,17 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz", - "integrity": "sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==", + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", + "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.16.5" + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" }, "engines": { "node": ">= 10.13.0" @@ -3629,59 +3616,6 @@ } } }, - "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", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "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/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" - }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "license": "MIT", - "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/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -3737,9 +3671,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { @@ -3749,15 +3683,19 @@ { "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.2.0", + "picocolors": "^1.1.1" }, "bin": { - "browserslist-lint": "cli.js" + "update-browserslist-db": "cli.js" }, "peerDependencies": { "browserslist": ">= 4.21.0" @@ -3811,9 +3749,9 @@ } }, "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dev": true, "license": "MIT", "dependencies": { @@ -3835,35 +3773,34 @@ } }, "node_modules/webpack": { - "version": "5.77.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.77.0.tgz", - "integrity": "sha512-sbGNjBr5Ya5ss91yzjeJTLKyfiwo5C628AFjEa6WSXcZa4E+F57om3Cc8xLb1Jh0b243AWuSYRf3dn7HVeFQ9Q==", + "version": "5.98.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", + "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", "dev": true, "license": "MIT", "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", + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.10.0", - "es-module-lexer": "^0.9.0", + "enhanced-resolve": "^5.17.1", + "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": "^4.3.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", - "watchpack": "^2.4.0", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { @@ -4048,59 +3985,6 @@ "node": ">=10.13.0" } }, - "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", - "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/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", - "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" - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "license": "MIT", - "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/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", diff --git a/wasm/mix-fetch/internal-dev/package.json b/wasm/mix-fetch/internal-dev/package.json index 439def5de3..1e813770d9 100644 --- a/wasm/mix-fetch/internal-dev/package.json +++ b/wasm/mix-fetch/internal-dev/package.json @@ -30,7 +30,7 @@ "devDependencies": { "copy-webpack-plugin": "^11.0.0", "hello-wasm-pack": "^0.1.0", - "webpack": "^5.70.0", + "webpack": "^5.98.0", "webpack-cli": "^4.9.2", "webpack-dev-server": "^4.7.4" }, diff --git a/wasm/mix-fetch/internal-dev/yarn.lock b/wasm/mix-fetch/internal-dev/yarn.lock index 53ffd85bc7..96b9349b62 100644 --- a/wasm/mix-fetch/internal-dev/yarn.lock +++ b/wasm/mix-fetch/internal-dev/yarn.lock @@ -7,45 +7,50 @@ resolved "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== -"@jridgewell/gen-mapping@^0.3.0": - version "0.3.2" - resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz" - integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.8" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142" + integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA== 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" -"@jridgewell/resolve-uri@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz" - integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -"@jridgewell/set-array@^1.0.1": - version "1.1.2" - resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz" - integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== -"@jridgewell/source-map@^0.3.2": - version "0.3.2" - resolved "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz" - integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== +"@jridgewell/source-map@^0.3.3": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== 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" -"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": +"@jridgewell/sourcemap-codec@^1.4.10": version "1.4.14" resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz" integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== -"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.17" - resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz" - integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== +"@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== dependencies: - "@jridgewell/resolve-uri" "3.1.0" - "@jridgewell/sourcemap-codec" "1.4.14" + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" "@leichtgewicht/ip-codec@^2.0.1": version "2.0.4" @@ -106,10 +111,10 @@ dependencies: "@types/node" "*" -"@types/eslint-scope@^3.7.3": - version "3.7.4" - resolved "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz" - integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== +"@types/eslint-scope@^3.7.7": + version "3.7.7" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" + integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== dependencies: "@types/eslint" "*" "@types/estree" "*" @@ -122,10 +127,10 @@ "@types/estree" "*" "@types/json-schema" "*" -"@types/estree@*", "@types/estree@^0.0.51": - version "0.0.51" - resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz" - integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== +"@types/estree@*", "@types/estree@^1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": version "4.17.33" @@ -153,7 +158,7 @@ dependencies: "@types/node" "*" -"@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== @@ -212,125 +217,125 @@ dependencies: "@types/node" "*" -"@webassemblyjs/ast@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz" - integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== +"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6" + integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ== dependencies: - "@webassemblyjs/helper-numbers" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/helper-numbers" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" -"@webassemblyjs/floating-point-hex-parser@1.11.1": - 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== +"@webassemblyjs/floating-point-hex-parser@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz#fcca1eeddb1cc4e7b6eed4fc7956d6813b21b9fb" + integrity sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA== -"@webassemblyjs/helper-api-error@1.11.1": - 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== +"@webassemblyjs/helper-api-error@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz#e0a16152248bc38daee76dd7e21f15c5ef3ab1e7" + integrity sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ== -"@webassemblyjs/helper-buffer@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz" - integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== +"@webassemblyjs/helper-buffer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz#822a9bc603166531f7d5df84e67b5bf99b72b96b" + integrity sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA== -"@webassemblyjs/helper-numbers@1.11.1": - 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== +"@webassemblyjs/helper-numbers@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz#dbd932548e7119f4b8a7877fd5a8d20e63490b2d" + integrity sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA== dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" + "@webassemblyjs/floating-point-hex-parser" "1.13.2" + "@webassemblyjs/helper-api-error" "1.13.2" "@xtuc/long" "4.2.2" -"@webassemblyjs/helper-wasm-bytecode@1.11.1": - 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== +"@webassemblyjs/helper-wasm-bytecode@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz#e556108758f448aae84c850e593ce18a0eb31e0b" + integrity sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA== -"@webassemblyjs/helper-wasm-section@1.11.1": - 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== +"@webassemblyjs/helper-wasm-section@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz#9629dda9c4430eab54b591053d6dc6f3ba050348" + integrity sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw== 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.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/wasm-gen" "1.14.1" -"@webassemblyjs/ieee754@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz" - integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== +"@webassemblyjs/ieee754@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz#1c5eaace1d606ada2c7fd7045ea9356c59ee0dba" + integrity sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw== dependencies: "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/leb128@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz" - integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== +"@webassemblyjs/leb128@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.13.2.tgz#57c5c3deb0105d02ce25fa3fd74f4ebc9fd0bbb0" + integrity sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw== dependencies: "@xtuc/long" "4.2.2" -"@webassemblyjs/utf8@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz" - integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== +"@webassemblyjs/utf8@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz#917a20e93f71ad5602966c2d685ae0c6c21f60f1" + integrity sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ== -"@webassemblyjs/wasm-edit@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz" - integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== +"@webassemblyjs/wasm-edit@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz#ac6689f502219b59198ddec42dcd496b1004d597" + integrity sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ== 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.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/helper-wasm-section" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-opt" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" + "@webassemblyjs/wast-printer" "1.14.1" -"@webassemblyjs/wasm-gen@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz" - integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== +"@webassemblyjs/wasm-gen@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz#991e7f0c090cb0bb62bbac882076e3d219da9570" + integrity sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg== 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.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" -"@webassemblyjs/wasm-opt@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz" - integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== +"@webassemblyjs/wasm-opt@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz#e6f71ed7ccae46781c206017d3c14c50efa8106b" + integrity sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw== 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.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" -"@webassemblyjs/wasm-parser@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz" - integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== +"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz#b3e13f1893605ca78b52c68e54cf6a865f90b9fb" + integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ== 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.14.1" + "@webassemblyjs/helper-api-error" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" -"@webassemblyjs/wast-printer@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz" - integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== +"@webassemblyjs/wast-printer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz#3bb3e9638a8ae5fdaf9610e7a06b4d9f9aa6fe07" + integrity sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw== dependencies: - "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/ast" "1.14.1" "@xtuc/long" "4.2.2" "@webpack-cli/configtest@^1.2.0": @@ -368,15 +373,10 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: mime-types "~2.1.34" negotiator "0.6.3" -acorn-import-assertions@^1.7.6: - version "1.8.0" - resolved "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz" - integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== - -acorn@^8.5.0, acorn@^8.7.1: - version "8.8.2" - resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz" - integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== +acorn@^8.14.0, acorn@^8.8.2: + version "8.14.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" + integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== ajv-formats@^2.1.1: version "2.1.1" @@ -385,29 +385,14 @@ ajv-formats@^2.1.1: dependencies: ajv "^8.0.0" -ajv-keywords@^3.5.2: - version "3.5.2" - resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz" - integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== - -ajv-keywords@^5.0.0: +ajv-keywords@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz" integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== dependencies: fast-deep-equal "^3.1.3" -ajv@^6.12.5: - version "6.12.6" - resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - 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" - -ajv@^8.0.0, ajv@^8.8.0: +ajv@^8.0.0: version "8.12.0" resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== @@ -417,6 +402,16 @@ ajv@^8.0.0, ajv@^8.8.0: require-from-string "^2.0.2" uri-js "^4.2.2" +ajv@^8.9.0: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + ansi-html-community@^0.0.8: version "0.0.8" resolved "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz" @@ -498,15 +493,15 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.1.1" -browserslist@^4.14.5: - version "4.21.5" - resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz" - integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w== +browserslist@^4.24.0: + version "4.24.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b" + integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A== dependencies: - caniuse-lite "^1.0.30001449" - electron-to-chromium "^1.4.284" - node-releases "^2.0.8" - update-browserslist-db "^1.0.10" + caniuse-lite "^1.0.30001688" + electron-to-chromium "^1.5.73" + node-releases "^2.0.19" + update-browserslist-db "^1.1.1" buffer-from@^1.0.0: version "1.1.2" @@ -539,10 +534,10 @@ call-bound@^1.0.2: call-bind-apply-helpers "^1.0.2" get-intrinsic "^1.3.0" -caniuse-lite@^1.0.30001449: - version "1.0.30001474" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001474.tgz" - integrity sha512-iaIZ8gVrWfemh5DG3T9/YqarVZoYf0r188IjaGwx68j4Pf0SGY6CQkmJUIE+NZHkkecQGohzXmBGEwWDr9aM3Q== +caniuse-lite@^1.0.30001688: + version "1.0.30001703" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001703.tgz#977cb4920598c158f491ecf4f4f2cfed9e354718" + integrity sha512-kRlAGTRWgPsOj7oARC9m1okJEXdL/8fekFVcxA8Hl7GH4r/sN4OJn/i6Flde373T50KS7Y37oFbMwlE8+F42kQ== chokidar@^3.5.3: version "3.5.3" @@ -745,10 +740,10 @@ ee-first@1.1.1: resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== -electron-to-chromium@^1.4.284: - version "1.4.352" - resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.352.tgz" - integrity sha512-ikFUEyu5/q+wJpMOxWxTaEVk2M1qKqTGKKyfJmod1CPZxKfYnxVS41/GCBQg21ItBpZybyN8sNpRqCUGm+Zc4Q== +electron-to-chromium@^1.5.73: + version "1.5.114" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.114.tgz#f2bb4fda80a7db4ea273565e75b0ebbe19af0ac3" + integrity sha512-DFptFef3iktoKlFQK/afbo274/XNWD00Am0xa7M8FZUepHlHT8PEuiNBoRfFHbH1okqN58AlhbJ4QTkcnXorjA== encodeurl@~1.0.2: version "1.0.2" @@ -760,10 +755,10 @@ encodeurl@~2.0.0: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== -enhanced-resolve@^5.10.0: - version "5.12.0" - resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz" - integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ== +enhanced-resolve@^5.17.1: + version "5.18.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz#728ab082f8b7b6836de51f1637aab5d3b9568faf" + integrity sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" @@ -783,10 +778,10 @@ es-errors@^1.3.0: resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== -es-module-lexer@^0.9.0: - version "0.9.3" - resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz" - integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== +es-module-lexer@^1.2.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.6.0.tgz#da49f587fd9e68ee2404fe4e256c0c7d3a81be21" + integrity sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ== es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: version "1.1.1" @@ -795,10 +790,10 @@ es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: dependencies: es-errors "^1.3.0" -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== escape-html@~1.0.3: version "1.0.3" @@ -913,10 +908,10 @@ fast-glob@^3.2.11, fast-glob@^3.3.0: merge2 "^1.3.0" micromatch "^4.0.4" -fast-json-stable-stringify@^2.0.0: - 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== +fast-uri@^3.0.1: + version "3.0.6" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" + integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== fastest-levenshtein@^1.0.12: version "1.0.16" @@ -1084,7 +1079,7 @@ gopd@^1.2.0: resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== -graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: +graceful-fs@^4.1.2, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6: 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== @@ -1336,11 +1331,6 @@ json-parse-even-better-errors@^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== -json-schema-traverse@^0.4.1: - 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== - json-schema-traverse@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" @@ -1488,10 +1478,10 @@ node-forge@^1: resolved "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz" integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== -node-releases@^2.0.8: - version "2.0.10" - resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz" - integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w== +node-releases@^2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" @@ -1617,6 +1607,11 @@ picocolors@^1.0.0: resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" @@ -1787,24 +1782,15 @@ safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -schema-utils@^3.1.0, schema-utils@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz" - integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== - dependencies: - "@types/json-schema" "^7.0.8" - ajv "^6.12.5" - ajv-keywords "^3.5.2" - -schema-utils@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz" - integrity sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg== +schema-utils@^4.0.0, schema-utils@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.0.tgz#3b669f04f71ff2dfb5aba7ce2d5a9d79b35622c0" + integrity sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g== 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" select-hose@^2.0.0: version "2.0.0" @@ -1837,13 +1823,20 @@ send@0.19.0: range-parser "~1.2.1" statuses "2.0.1" -serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: +serialize-javascript@^6.0.0: version "6.0.1" resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz" integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== dependencies: randombytes "^2.1.0" +serialize-javascript@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + serve-index@^1.9.1: version "1.9.1" resolved "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz" @@ -2042,24 +2035,24 @@ tapable@^2.1.1, tapable@^2.2.0: resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== -terser-webpack-plugin@^5.1.3: - version "5.3.7" - resolved "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz" - integrity sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw== +terser-webpack-plugin@^5.3.11: + version "5.3.14" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz#9031d48e57ab27567f02ace85c7d690db66c3e06" + integrity sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw== dependencies: - "@jridgewell/trace-mapping" "^0.3.17" + "@jridgewell/trace-mapping" "^0.3.25" jest-worker "^27.4.5" - schema-utils "^3.1.1" - serialize-javascript "^6.0.1" - terser "^5.16.5" + schema-utils "^4.3.0" + serialize-javascript "^6.0.2" + terser "^5.31.1" -terser@^5.16.5: - version "5.16.8" - resolved "https://registry.npmjs.org/terser/-/terser-5.16.8.tgz" - integrity sha512-QI5g1E/ef7d+PsDifb+a6nnVgC4F22Bg6T0xrBrz6iloVB4PUkkunp6V8nzoOOZJIzjWVdAGqCdlKlhLq/TbIA== +terser@^5.31.1: + version "5.39.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.39.0.tgz#0e82033ed57b3ddf1f96708d123cca717d86ca3a" + integrity sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw== dependencies: - "@jridgewell/source-map" "^0.3.2" - acorn "^8.5.0" + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" commander "^2.20.0" source-map-support "~0.5.20" @@ -2093,13 +2086,13 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== -update-browserslist-db@^1.0.10: - version "1.0.10" - resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz" - integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== +update-browserslist-db@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" + integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" + escalade "^3.2.0" + picocolors "^1.1.1" uri-js@^4.2.2: version "4.4.1" @@ -2128,10 +2121,10 @@ vary@~1.1.2: resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== -watchpack@^2.4.0: - version "2.4.0" - resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz" - integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== +watchpack@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" + integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== dependencies: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" @@ -2221,34 +2214,33 @@ webpack-sources@^3.2.3: resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@^5.70.0: - version "5.77.0" - resolved "https://registry.npmjs.org/webpack/-/webpack-5.77.0.tgz" - integrity sha512-sbGNjBr5Ya5ss91yzjeJTLKyfiwo5C628AFjEa6WSXcZa4E+F57om3Cc8xLb1Jh0b243AWuSYRf3dn7HVeFQ9Q== +webpack@^5.98.0: + version "5.98.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.98.0.tgz#44ae19a8f2ba97537978246072fb89d10d1fbd17" + integrity sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA== 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" + "@types/eslint-scope" "^3.7.7" + "@types/estree" "^1.0.6" + "@webassemblyjs/ast" "^1.14.1" + "@webassemblyjs/wasm-edit" "^1.14.1" + "@webassemblyjs/wasm-parser" "^1.14.1" + acorn "^8.14.0" + browserslist "^4.24.0" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.10.0" - es-module-lexer "^0.9.0" + enhanced-resolve "^5.17.1" + 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 "^4.3.0" tapable "^2.1.1" - terser-webpack-plugin "^5.1.3" - watchpack "^2.4.0" + terser-webpack-plugin "^5.3.11" + watchpack "^2.4.1" webpack-sources "^3.2.3" websocket-driver@>=0.5.1, websocket-driver@^0.7.4: From a9cf016af27ca45660224d86900a128703bb9080 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 11:58:19 +0000 Subject: [PATCH 083/133] build(deps-dev): bump ws in /wasm/mix-fetch/internal-dev (#5593) Bumps [ws](https://github.com/websockets/ws) from 8.13.0 to 8.18.1. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/8.13.0...8.18.1) --- updated-dependencies: - dependency-name: ws dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- wasm/mix-fetch/internal-dev/package-lock.json | 6 +++--- wasm/mix-fetch/internal-dev/yarn.lock | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/wasm/mix-fetch/internal-dev/package-lock.json b/wasm/mix-fetch/internal-dev/package-lock.json index a338e041af..596dca6c6f 100644 --- a/wasm/mix-fetch/internal-dev/package-lock.json +++ b/wasm/mix-fetch/internal-dev/package-lock.json @@ -4041,9 +4041,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", "dev": true, "license": "MIT", "engines": { diff --git a/wasm/mix-fetch/internal-dev/yarn.lock b/wasm/mix-fetch/internal-dev/yarn.lock index 96b9349b62..c9e1578198 100644 --- a/wasm/mix-fetch/internal-dev/yarn.lock +++ b/wasm/mix-fetch/internal-dev/yarn.lock @@ -2275,6 +2275,6 @@ wrappy@1: integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== ws@^8.13.0: - version "8.13.0" - resolved "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz" - integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== + version "8.18.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb" + integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w== From f62d8813e0667a8faaf7d5b773bf18e088c66015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 12 Mar 2025 12:01:58 +0000 Subject: [PATCH 084/133] chore: start sending v2 sphinx packets (#5554) * chore: start sending v2 sphinx packets * updated surb construction to use current format --- common/nymsphinx/anonymous-replies/src/reply_surb.rs | 5 ++--- common/nymsphinx/types/src/lib.rs | 5 ----- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/common/nymsphinx/anonymous-replies/src/reply_surb.rs b/common/nymsphinx/anonymous-replies/src/reply_surb.rs index a6729e7e5d..90bd892d78 100644 --- a/common/nymsphinx/anonymous-replies/src/reply_surb.rs +++ b/common/nymsphinx/anonymous-replies/src/reply_surb.rs @@ -7,7 +7,7 @@ use nym_sphinx_addressing::clients::Recipient; use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, MAX_NODE_ADDRESS_UNPADDED_LEN}; use nym_sphinx_params::packet_sizes::PacketSize; use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm}; -use nym_sphinx_types::{NymPacket, SURBMaterial, SphinxError, SURB, UPDATED_LEGACY_VERSION}; +use nym_sphinx_types::{NymPacket, SURBMaterial, SphinxError, SURB}; use nym_topology::{NymRouteProvider, NymTopologyError}; use rand::{CryptoRng, RngCore}; use serde::de::{Error as SerdeError, Visitor}; @@ -101,8 +101,7 @@ impl ReplySurb { let delays = nym_sphinx_routing::generate_hop_delays(average_delay, route.len()); let destination = recipient.as_sphinx_destination(); - let surb_material = - SURBMaterial::new(route, delays, destination).with_version(UPDATED_LEGACY_VERSION); + let surb_material = SURBMaterial::new(route, delays, destination); // this can't fail as we know we have a valid route to gateway and have correct number of delays Ok(ReplySurb { diff --git a/common/nymsphinx/types/src/lib.rs b/common/nymsphinx/types/src/lib.rs index c9441dbc4d..517e45e783 100644 --- a/common/nymsphinx/types/src/lib.rs +++ b/common/nymsphinx/types/src/lib.rs @@ -30,7 +30,6 @@ pub use sphinx_packet::{ route::{Destination, DestinationAddressBytes, Node, NodeAddressBytes, SURBIdentifier}, surb::{SURBMaterial, SURB}, version::Version, - version::UPDATED_LEGACY_VERSION, Error as SphinxError, ProcessedPacket, ProcessedPacketData, }; @@ -91,12 +90,8 @@ impl NymPacket { destination: &Destination, delays: &[Delay], ) -> Result { - // FIXME: - // for now explicitly use the legacy version until sufficient number of nodes - // understand both variants Ok(NymPacket::Sphinx( SphinxPacketBuilder::new() - .with_version(UPDATED_LEGACY_VERSION) .with_payload_size(size) .build_packet(message, route, destination, delays)?, )) From ce124a29a7a3a7539202f239cf74e01a862b3476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 12 Mar 2025 12:12:28 +0000 Subject: [PATCH 085/133] Chore/more payment watcher debug endpoints (#5608) * add new endpoints for health and build information * fixed timestamp serialisation in api responses * status routes for price scraper * state for processing bank msg * clippy --- Cargo.lock | 4 +- .../validator-client/src/nyxd/mod.rs | 1 + .../nyxd-scraper/src/block_processor/mod.rs | 8 +- common/nyxd-scraper/src/error.rs | 9 + common/nyxd-scraper/src/modules/msg_module.rs | 2 + nym-wallet/Cargo.lock | 4 +- nyx-chain-watcher/Cargo.toml | 6 +- nyx-chain-watcher/src/chain_scraper/mod.rs | 155 +++++++----- nyx-chain-watcher/src/cli/commands/run/mod.rs | 15 +- nyx-chain-watcher/src/db/models.rs | 4 +- nyx-chain-watcher/src/http/api/status.rs | 136 ++++++++++- nyx-chain-watcher/src/http/models.rs | 70 ++++++ nyx-chain-watcher/src/http/server.rs | 6 +- nyx-chain-watcher/src/http/state.rs | 223 +++++++++++++++++- nyx-chain-watcher/src/price_scraper/mod.rs | 90 ++++--- 15 files changed, 607 insertions(+), 126 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 66f91ff74c..e939caf769 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7234,7 +7234,7 @@ dependencies = [ [[package]] name = "nyx-chain-watcher" -version = "0.1.13" +version = "0.1.14" dependencies = [ "anyhow", "async-trait", @@ -7244,14 +7244,12 @@ dependencies = [ "nym-bin-common", "nym-config", "nym-network-defaults", - "nym-node-requests", "nym-task", "nym-validator-client", "nyxd-scraper", "reqwest 0.12.4", "schemars", "serde", - "serde_json", "sqlx", "thiserror 2.0.12", "time", diff --git a/common/client-libs/validator-client/src/nyxd/mod.rs b/common/client-libs/validator-client/src/nyxd/mod.rs index 458ba45c27..03746e305e 100644 --- a/common/client-libs/validator-client/src/nyxd/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/mod.rs @@ -62,6 +62,7 @@ pub use cw3; pub use cw4; pub use cw_controllers; pub use fee::{gas_price::GasPrice, GasAdjustable, GasAdjustment}; +pub use prost::Name; pub use tendermint_rpc::endpoint::block::Response as BlockResponse; pub use tendermint_rpc::{ endpoint::{tx::Response as TxResponse, validators::Response as ValidatorResponse}, diff --git a/common/nyxd-scraper/src/block_processor/mod.rs b/common/nyxd-scraper/src/block_processor/mod.rs index e75d62b7df..d4007de96b 100644 --- a/common/nyxd-scraper/src/block_processor/mod.rs +++ b/common/nyxd-scraper/src/block_processor/mod.rs @@ -182,9 +182,11 @@ impl BlockProcessor { // the ones concerned with individual messages for (index, msg) in block_tx.tx.body.messages.iter().enumerate() { for msg_module in &mut self.msg_modules { - msg_module - .handle_msg(index, msg, &block_tx, &mut tx) - .await? + if msg.type_url == msg_module.type_url() { + msg_module + .handle_msg(index, msg, &block_tx, &mut tx) + .await? + } } } } diff --git a/common/nyxd-scraper/src/error.rs b/common/nyxd-scraper/src/error.rs index 6e413983bd..3337ee969c 100644 --- a/common/nyxd-scraper/src/error.rs +++ b/common/nyxd-scraper/src/error.rs @@ -83,6 +83,15 @@ pub enum ScraperError { source: cosmrs::ErrorReport, }, + #[error("could not parse msg in tx {hash} at index {index} into {type_url}: {source}")] + MsgParseFailure { + hash: Hash, + index: usize, + type_url: String, + #[source] + source: cosmrs::ErrorReport, + }, + #[error("received an invalid chain subscription event of kind {kind} while we were waiting for new block data (query: '{query}')")] InvalidSubscriptionEvent { query: String, kind: String }, diff --git a/common/nyxd-scraper/src/modules/msg_module.rs b/common/nyxd-scraper/src/modules/msg_module.rs index df64761f7d..1d195bee14 100644 --- a/common/nyxd-scraper/src/modules/msg_module.rs +++ b/common/nyxd-scraper/src/modules/msg_module.rs @@ -9,6 +9,8 @@ use cosmrs::Any; #[async_trait] pub trait MsgModule { + fn type_url(&self) -> String; + async fn handle_msg( &mut self, index: usize, diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index f4dc00b70a..556fbb5ddf 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -4784,9 +4784,9 @@ dependencies = [ [[package]] name = "rs_merkle" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b241d2e59b74ef9e98d94c78c47623d04c8392abaf82014dfd372a16041128f" +checksum = "bb09b49230ba22e8c676e7b75dfe2887dea8121f18b530ae0ba519ce442d2b21" dependencies = [ "sha2 0.10.8", ] diff --git a/nyx-chain-watcher/Cargo.toml b/nyx-chain-watcher/Cargo.toml index f17233f96b..bbc2ed1939 100644 --- a/nyx-chain-watcher/Cargo.toml +++ b/nyx-chain-watcher/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nyx-chain-watcher" -version = "0.1.13" +version = "0.1.14" authors.workspace = true repository.workspace = true homepage.workspace = true @@ -23,15 +23,11 @@ nym-config = { path = "../common/config" } nym-bin-common = { path = "../common/bin-common", features = ["output_format"] } nym-network-defaults = { path = "../common/network-defaults" } nym-task = { path = "../common/task" } -nym-node-requests = { path = "../nym-node/nym-node-requests", features = [ - "openapi", -] } nym-validator-client = { path = "../common/client-libs/validator-client" } nyxd-scraper = { path = "../common/nyxd-scraper" } reqwest = { workspace = true, features = ["rustls-tls"] } schemars = { workspace = true } serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "time"] } thiserror = { workspace = true } time = { workspace = true } diff --git a/nyx-chain-watcher/src/chain_scraper/mod.rs b/nyx-chain-watcher/src/chain_scraper/mod.rs index eba1865c6e..21e716d580 100644 --- a/nyx-chain-watcher/src/chain_scraper/mod.rs +++ b/nyx-chain-watcher/src/chain_scraper/mod.rs @@ -3,18 +3,21 @@ use crate::env::vars::{ NYXD_SCRAPER_START_HEIGHT, NYXD_SCRAPER_UNSAFE_NUKE_DB, NYXD_SCRAPER_USE_BEST_EFFORT_START_HEIGHT, }; +use crate::http::state::BankScraperModuleState; use async_trait::async_trait; +use nym_validator_client::nyxd::{Any, Coin, CosmosCoin, Hash, Msg, MsgSend, Name}; use nyxd_scraper::{ - error::ScraperError, storage::StorageTransaction, NyxdScraper, ParsedTransactionResponse, - PruningOptions, TxModule, + error::ScraperError, storage::StorageTransaction, MsgModule, NyxdScraper, + ParsedTransactionResponse, PruningOptions, }; use sqlx::SqlitePool; use std::fs; -use tracing::{error, info, warn}; +use tracing::{info, warn}; pub(crate) async fn run_chain_scraper( config: &crate::config::Config, db_pool: SqlitePool, + shared_state: BankScraperModuleState, ) -> anyhow::Result { let websocket_url = std::env::var("NYXD_WS").expect("NYXD_WS not defined"); @@ -58,9 +61,10 @@ pub(crate) async fn run_chain_scraper( use_best_effort_start_height, }, }) - .with_tx_module(EventScraperModule::new( + .with_msg_module(BankScraperModule::new( db_pool, config.payment_watcher_config.clone(), + shared_state, )); let instance = scraper.build_and_start().await?; @@ -71,16 +75,22 @@ pub(crate) async fn run_chain_scraper( Ok(instance) } -pub struct EventScraperModule { +pub struct BankScraperModule { db_pool: SqlitePool, payment_config: PaymentWatchersConfig, + shared_state: BankScraperModuleState, } -impl EventScraperModule { - pub fn new(db_pool: SqlitePool, payment_config: PaymentWatchersConfig) -> Self { +impl BankScraperModule { + pub fn new( + db_pool: SqlitePool, + payment_config: PaymentWatchersConfig, + shared_state: BankScraperModuleState, + ) -> Self { Self { db_pool, payment_config, + shared_state, } } @@ -108,23 +118,47 @@ impl EventScraperModule { amount, memo ) - .execute(&self.db_pool) - .await?; + .execute(&self.db_pool) + .await?; Ok(()) } -} + fn get_unym_coin(&self, coins: &[CosmosCoin]) -> Option { + coins + .iter() + .find(|coin| coin.denom.as_ref() == "unym") + .map(|c| c.clone().into()) + } + + // TODO: ideally this should be done by the scraper itself + fn recover_bank_msg( + &self, + tx_hash: Hash, + index: usize, + msg: &Any, + ) -> Result { + MsgSend::from_any(msg).map_err(|source| ScraperError::MsgParseFailure { + hash: tx_hash, + index, + type_url: self.type_url(), + source, + }) + } +} #[async_trait] -impl TxModule for EventScraperModule { - async fn handle_tx( +impl MsgModule for BankScraperModule { + fn type_url(&self) -> String { + ::Proto::type_url() + } + + async fn handle_msg( &mut self, + index: usize, + msg: &Any, tx: &ParsedTransactionResponse, - _: &mut StorageTransaction, + _storage_tx: &mut StorageTransaction, ) -> Result<(), ScraperError> { - let events = &tx.tx_result.events; - let height = tx.height.value() as i64; - let tx_hash = tx.hash.to_string(); let memo = tx.tx.body.memo.clone(); // Don't process failed transactions @@ -132,56 +166,53 @@ impl TxModule for EventScraperModule { return Ok(()); } - if tx.tx.body.messages.len() > 1 { - error!( - "this transaction has more than 1 message in it - payment information will be lost" - ); - } + let msg = self.recover_bank_msg(tx.hash, index, msg)?; - // Process each event - for event in events { - // Only process transfer events - if event.kind == "transfer" { - let mut recipient = None; - let mut sender = None; - let mut amount = None; - // TODO: get message index from event - let message_index = 0; + // Check if any watcher is watching this recipient + let is_watched = self + .payment_config + .is_being_watched(msg.to_address.as_ref()); - // Extract transfer event attributes - for attr in &event.attributes { - if let (Ok(key), Ok(value)) = (attr.key_str(), attr.value_str()) { - match key { - "recipient" => recipient = Some(value.to_string()), - "sender" => sender = Some(value.to_string()), - "amount" => amount = Some(value.to_string()), - _ => continue, - } - } - } + self.shared_state + .new_bank_msg(tx, index, &msg, is_watched) + .await; - // If we have all required fields, check if recipient is watched and store - if let (Some(recipient), Some(sender), Some(amount)) = (recipient, sender, amount) { - // Check if any watcher is watching this recipient - let is_watched = self.payment_config.is_being_watched(&recipient); + if is_watched { + let Some(unym_coin) = self.get_unym_coin(&msg.amount) else { + let warn = format!( + "{} sent {:?} instead of unym!", + msg.from_address, msg.amount + ); + warn!("{warn}"); + self.shared_state + .new_rejection(tx.hash.to_string(), tx.height.value(), index as u32, warn) + .await; - if is_watched { - if let Err(e) = self - .store_transfer_event( - &tx_hash, - height, - message_index, - sender, - recipient, - amount, - Some(memo.clone()), - ) - .await - { - warn!("Failed to store transfer event: {}", e); - } - } - } + // we don't want to fail the whole processing - this is not a failure in that sense! + return Ok(()); + }; + + if let Err(err) = self + .store_transfer_event( + &tx.hash.to_string(), + tx.height.value() as i64, + index as i64, + msg.from_address.to_string(), + msg.to_address.to_string(), + unym_coin.to_string(), + Some(memo.clone()), + ) + .await + { + warn!("Failed to store transfer event: {err}"); + self.shared_state + .new_rejection( + tx.hash.to_string(), + tx.height.value(), + index as u32, + format!("storage failure: {err}"), + ) + .await; } } diff --git a/nyx-chain-watcher/src/cli/commands/run/mod.rs b/nyx-chain-watcher/src/cli/commands/run/mod.rs index 247b324bf9..84d9f10f3b 100644 --- a/nyx-chain-watcher/src/cli/commands/run/mod.rs +++ b/nyx-chain-watcher/src/cli/commands/run/mod.rs @@ -14,9 +14,10 @@ mod config; use crate::chain_scraper::run_chain_scraper; use crate::db::DbPool; -use crate::http::state::PaymentListenerState; +use crate::http::state::{BankScraperModuleState, PaymentListenerState, PriceScraperState}; use crate::payment_listener::PaymentListener; -use crate::{db, http, price_scraper}; +use crate::price_scraper::PriceScraper; +use crate::{db, http}; pub(crate) use args::Args; use nym_task::signal::wait_for_signal; @@ -145,15 +146,18 @@ pub(crate) async fn execute(args: Args, http_port: u16) -> Result<(), NyxChainWa // construct shared state let payment_listener_shared_state = PaymentListenerState::new(); + let price_scraper_shared_state = PriceScraperState::new(); + let bank_scraper_module_shared_state = BankScraperModuleState::new(); // spawn all the tasks // 1. chain scraper (note: this doesn't really spawn the full scraper on this task, but we don't want to be blocking waiting for its startup) let scraper_token_handle: JoinHandle> = tokio::spawn({ let config = config.clone(); + let shared_state = bank_scraper_module_shared_state.clone(); async move { // this only blocks until startup sync is done; it then runs on its own set of tasks - let scraper = run_chain_scraper(&config, scraper_pool).await?; + let scraper = run_chain_scraper(&config, scraper_pool, shared_state).await?; Ok(scraper.cancel_token()) } }); @@ -178,12 +182,13 @@ pub(crate) async fn execute(args: Args, http_port: u16) -> Result<(), NyxChainWa } // 3. price scraper (note, this task never terminates on its own) + let price_scraper = PriceScraper::new(price_scraper_shared_state.clone(), watcher_pool); { let token = cancellation_token.clone(); tasks.spawn(async move { token .run_until_cancelled(async move { - price_scraper::run_price_scraper(&watcher_pool).await; + price_scraper.run().await; Ok(()) }) .await @@ -196,6 +201,8 @@ pub(crate) async fn execute(args: Args, http_port: u16) -> Result<(), NyxChainWa &config, http_port, payment_listener_shared_state, + price_scraper_shared_state, + bank_scraper_module_shared_state, ) .await?; { diff --git a/nyx-chain-watcher/src/db/models.rs b/nyx-chain-watcher/src/db/models.rs index b27fcfe34f..fa49317cda 100644 --- a/nyx-chain-watcher/src/db/models.rs +++ b/nyx-chain-watcher/src/db/models.rs @@ -5,7 +5,7 @@ use sqlx::FromRow; use time::OffsetDateTime; use utoipa::ToSchema; -#[derive(Clone, Deserialize, Debug, ToSchema)] +#[derive(Clone, Serialize, Deserialize, Debug, ToSchema)] pub(crate) struct CurrencyPrices { pub(crate) chf: f32, pub(crate) usd: f32, @@ -15,7 +15,7 @@ pub(crate) struct CurrencyPrices { } // Struct to hold Coingecko response -#[derive(Clone, Deserialize, Debug, ToSchema)] +#[derive(Clone, Serialize, Deserialize, Debug, ToSchema)] pub(crate) struct CoingeckoPriceResponse { pub(crate) nym: CurrencyPrices, } diff --git a/nyx-chain-watcher/src/http/api/status.rs b/nyx-chain-watcher/src/http/api/status.rs index 846cefa6fc..df6ec70a91 100644 --- a/nyx-chain-watcher/src/http/api/status.rs +++ b/nyx-chain-watcher/src/http/api/status.rs @@ -2,19 +2,60 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::http::models::status::{ - ActivePaymentWatchersResponse, PaymentListenerFailureDetails, PaymentListenerStatusResponse, - ProcessedPayment, WatcherFailureDetails, WatcherState, + ActivePaymentWatchersResponse, ApiStatus, BankModuleStatusResponse, BankMsgDetails, + BankMsgRejection, HealthResponse, PaymentListenerFailureDetails, PaymentListenerStatusResponse, + PriceScraperLastError, PriceScraperLastSuccess, PriceScraperStatusResponse, ProcessedPayment, + WatcherFailureDetails, WatcherState, +}; +use crate::http::state::{ + AppState, BankScraperModuleState, PaymentListenerState, PriceScraperState, StatusState, }; -use crate::http::state::{AppState, PaymentListenerState}; use axum::extract::State; use axum::routing::get; use axum::{Json, Router}; +use nym_bin_common::build_information::BinaryBuildInformationOwned; use std::ops::Deref; pub(crate) fn routes() -> Router { Router::new() + .route("/health", get(health)) + .route("/build-information", get(build_information)) .route("/active-payment-watchers", get(active_payment_watchers)) .route("/payment-listener", get(payment_listener_status)) + .route("/price-scraper", get(price_scraper_status)) + .route("/bank-module-scraper", get(bank_module_status)) +} + +#[utoipa::path( + tag = "Status", + get, + path = "/build-information", + context_path = "/v1/status", + responses( + (status = 200, body = BinaryBuildInformationOwned) + ) +)] +async fn build_information(State(state): State) -> Json { + Json(state.build_information.to_owned()) +} + +#[utoipa::path( + tag = "Status", + get, + path = "/health", + context_path = "/v1/status", + responses( + (status = 200, body = HealthResponse) + ) +)] +async fn health(State(state): State) -> Json { + let uptime = state.startup_time.elapsed(); + + let health = HealthResponse { + status: ApiStatus::Up, + uptime: uptime.as_secs(), + }; + Json(health) } #[utoipa::path( @@ -96,3 +137,92 @@ pub(crate) async fn payment_listener_status( .collect(), }) } + +#[utoipa::path( + tag = "Status", + get, + path = "/price-scraper", + context_path = "/v1/status", + responses( + (status = 200, body = PriceScraperStatusResponse) + ) +)] +pub(crate) async fn price_scraper_status( + State(state): State, +) -> Json { + let guard = state.inner.read().await; + Json(PriceScraperStatusResponse { + last_success: guard + .last_success + .as_ref() + .map(|s| PriceScraperLastSuccess { + timestamp: s.timestamp, + response: s.response.clone(), + }), + last_failure: guard.last_failure.as_ref().map(|f| PriceScraperLastError { + timestamp: f.timestamp, + message: f.message.clone(), + }), + }) +} + +#[utoipa::path( + tag = "Status", + get, + path = "/bank-module-scraper", + context_path = "/v1/status", + responses( + (status = 200, body = BankModuleStatusResponse) + ) +)] +pub(crate) async fn bank_module_status( + State(state): State, +) -> Json { + let guard = state.inner.read().await; + Json(BankModuleStatusResponse { + processed_bank_msgs_since_startup: guard.processed_bank_msgs_since_startup, + processed_bank_msgs_to_watched_addresses_since_startup: guard + .processed_bank_msgs_to_watched_addresses_since_startup, + rejected_bank_msgs_to_watched_addresses_since_startup: guard + .rejected_bank_msgs_to_watched_addresses_since_startup, + last_seen_bank_msgs: guard + .last_seen_bank_msgs + .iter() + .map(|msg| BankMsgDetails { + processed_at: msg.processed_at, + tx_hash: msg.tx_hash.clone(), + height: msg.height, + index: msg.index, + from: msg.from.clone(), + to: msg.to.clone(), + amount: msg.amount.clone(), + memo: msg.memo.clone(), + }) + .collect(), + last_seen_watched_bank_msgs: guard + .last_seen_watched_bank_msgs + .iter() + .map(|msg| BankMsgDetails { + processed_at: msg.processed_at, + tx_hash: msg.tx_hash.clone(), + height: msg.height, + index: msg.index, + from: msg.from.clone(), + to: msg.to.clone(), + amount: msg.amount.clone(), + memo: msg.memo.clone(), + }) + .collect(), + last_rejected_watched_bank_msgs: guard + .last_rejected_watched_bank_msgs + .iter() + .map(|r| BankMsgRejection { + rejected_at: r.rejected_at, + tx_hash: r.tx_hash.clone(), + height: r.height, + index: r.index, + error: r.error.clone(), + }) + .collect(), + }) +} diff --git a/nyx-chain-watcher/src/http/models.rs b/nyx-chain-watcher/src/http/models.rs index 8e7776cf68..c433ea9702 100644 --- a/nyx-chain-watcher/src/http/models.rs +++ b/nyx-chain-watcher/src/http/models.rs @@ -5,6 +5,7 @@ pub mod status { use crate::config::payments_watcher::PaymentWatcherConfig; + use crate::db::models::CoingeckoPriceResponse; use crate::models::openapi_schema; use nym_validator_client::nyxd::Coin; use serde::{Deserialize, Serialize}; @@ -12,6 +13,18 @@ pub mod status { use time::OffsetDateTime; use utoipa::ToSchema; + #[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] + #[serde(rename_all = "lowercase")] + pub enum ApiStatus { + Up, + } + + #[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] + pub struct HealthResponse { + pub status: ApiStatus, + pub uptime: u64, + } + #[derive(Debug, Serialize, Deserialize, ToSchema)] pub struct ActivePaymentWatchersResponse { pub watchers: Vec, @@ -59,6 +72,7 @@ pub mod status { #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub(crate) struct ProcessedPayment { + #[serde(with = "time::serde::rfc3339")] pub processed_at: OffsetDateTime, pub tx_hash: String, @@ -75,6 +89,7 @@ pub mod status { #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub(crate) struct PaymentListenerFailureDetails { + #[serde(with = "time::serde::rfc3339")] pub(crate) timestamp: OffsetDateTime, pub(crate) error: String, } @@ -86,7 +101,62 @@ pub mod status { #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub(crate) struct WatcherFailureDetails { + #[serde(with = "time::serde::rfc3339")] pub(crate) timestamp: OffsetDateTime, pub(crate) error: String, } + + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] + pub(crate) struct PriceScraperStatusResponse { + pub(crate) last_success: Option, + pub(crate) last_failure: Option, + } + + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] + pub(crate) struct PriceScraperLastSuccess { + #[serde(with = "time::serde::rfc3339")] + pub(crate) timestamp: OffsetDateTime, + pub(crate) response: CoingeckoPriceResponse, + } + + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] + pub(crate) struct PriceScraperLastError { + #[serde(with = "time::serde::rfc3339")] + pub(crate) timestamp: OffsetDateTime, + pub(crate) message: String, + } + + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] + pub(crate) struct BankModuleStatusResponse { + pub(crate) processed_bank_msgs_since_startup: usize, + pub(crate) processed_bank_msgs_to_watched_addresses_since_startup: usize, + pub(crate) rejected_bank_msgs_to_watched_addresses_since_startup: usize, + + pub(crate) last_seen_bank_msgs: Vec, + pub(crate) last_seen_watched_bank_msgs: Vec, + pub(crate) last_rejected_watched_bank_msgs: Vec, + } + + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] + pub(crate) struct BankMsgDetails { + #[serde(with = "time::serde::rfc3339")] + pub(crate) processed_at: OffsetDateTime, + pub(crate) tx_hash: String, + pub(crate) height: u64, + pub(crate) index: u32, + pub(crate) from: String, + pub(crate) to: String, + pub(crate) amount: Vec, + pub(crate) memo: String, + } + + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] + pub(crate) struct BankMsgRejection { + #[serde(with = "time::serde::rfc3339")] + pub(crate) rejected_at: OffsetDateTime, + pub(crate) tx_hash: String, + pub(crate) height: u64, + pub(crate) index: u32, + pub(crate) error: String, + } } diff --git a/nyx-chain-watcher/src/http/server.rs b/nyx-chain-watcher/src/http/server.rs index a9b095b6ff..96e7d47f95 100644 --- a/nyx-chain-watcher/src/http/server.rs +++ b/nyx-chain-watcher/src/http/server.rs @@ -4,7 +4,7 @@ use tokio::net::TcpListener; use tokio_util::sync::WaitForCancellationFutureOwned; use crate::config::Config; -use crate::http::state::PaymentListenerState; +use crate::http::state::{BankScraperModuleState, PaymentListenerState, PriceScraperState}; use crate::{ db::DbPool, http::{api::RouterBuilder, state::AppState}, @@ -15,6 +15,8 @@ pub(crate) async fn build_http_api( config: &Config, http_port: u16, payment_listener_state: PaymentListenerState, + price_scraper_state: PriceScraperState, + bank_scraper_module_shared_state: BankScraperModuleState, ) -> anyhow::Result { let router_builder = RouterBuilder::with_default_routes(); @@ -27,6 +29,8 @@ pub(crate) async fn build_http_api( .map(Into::into) .collect(), payment_listener_state, + price_scraper_state, + bank_scraper_module_shared_state, ); let router = router_builder.with_state(state); diff --git a/nyx-chain-watcher/src/http/state.rs b/nyx-chain-watcher/src/http/state.rs index ff230cf242..3f0d813f44 100644 --- a/nyx-chain-watcher/src/http/state.rs +++ b/nyx-chain-watcher/src/http/state.rs @@ -1,20 +1,29 @@ +use crate::db::models::CoingeckoPriceResponse; use crate::db::DbPool; use crate::helpers::RingBuffer; use crate::http::models::status::PaymentWatcher; use crate::models::WebhookPayload; use axum::extract::FromRef; -use nym_validator_client::nyxd::Coin; +use nym_bin_common::bin_info; +use nym_bin_common::build_information::BinaryBuildInformation; +use nym_validator_client::nyxd::{Coin, MsgSend}; +use nyxd_scraper::ParsedTransactionResponse; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::ops::Deref; use std::sync::Arc; use time::OffsetDateTime; use tokio::sync::RwLock; +use tokio::time::Instant; #[derive(Debug, Clone)] pub(crate) struct AppState { db_pool: DbPool, pub(crate) registered_payment_watchers: Arc>, pub(crate) payment_listener_state: PaymentListenerState, + pub(crate) status_state: StatusState, + pub(crate) price_scraper_state: PriceScraperState, + pub(crate) bank_scraper_module_state: BankScraperModuleState, } impl AppState { @@ -22,11 +31,16 @@ impl AppState { db_pool: DbPool, registered_payment_watchers: Vec, payment_listener_state: PaymentListenerState, + price_scraper_state: PriceScraperState, + bank_scraper_module_state: BankScraperModuleState, ) -> Self { Self { db_pool, registered_payment_watchers: Arc::new(registered_payment_watchers), payment_listener_state, + status_state: Default::default(), + price_scraper_state, + bank_scraper_module_state, } } @@ -43,6 +57,79 @@ impl AppState { } } +#[derive(Clone, Debug)] +pub(crate) struct StatusState { + inner: Arc, +} + +impl Default for StatusState { + fn default() -> Self { + StatusState { + inner: Arc::new(StatusStateInner { + startup_time: Instant::now(), + build_information: bin_info!(), + }), + } + } +} + +impl Deref for StatusState { + type Target = StatusStateInner; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +#[derive(Debug)] +pub(crate) struct StatusStateInner { + pub(crate) startup_time: Instant, + pub(crate) build_information: BinaryBuildInformation, +} + +#[derive(Debug, Clone)] +pub(crate) struct PriceScraperState { + pub(crate) inner: Arc>, +} + +impl PriceScraperState { + pub(crate) fn new() -> Self { + PriceScraperState { + inner: Arc::new(Default::default()), + } + } + + pub(crate) async fn new_failure>(&self, error: S) { + self.inner.write().await.last_failure = Some(PriceScraperLastError { + timestamp: OffsetDateTime::now_utc(), + message: error.into(), + }) + } + pub(crate) async fn new_success(&self, response: CoingeckoPriceResponse) { + self.inner.write().await.last_success = Some(PriceScraperLastSuccess { + timestamp: OffsetDateTime::now_utc(), + response, + }) + } +} + +#[derive(Debug, Default)] +pub(crate) struct PriceScraperStateInner { + pub(crate) last_success: Option, + pub(crate) last_failure: Option, +} + +#[derive(Debug)] +pub(crate) struct PriceScraperLastSuccess { + pub(crate) timestamp: OffsetDateTime, + pub(crate) response: CoingeckoPriceResponse, +} + +#[derive(Debug)] +pub(crate) struct PriceScraperLastError { + pub(crate) timestamp: OffsetDateTime, + pub(crate) message: String, +} + #[derive(Debug, Clone)] pub(crate) struct PaymentListenerState { pub(crate) inner: Arc>, @@ -99,12 +186,6 @@ impl PaymentListenerState { } } -impl FromRef for PaymentListenerState { - fn from_ref(input: &AppState) -> Self { - input.payment_listener_state.clone() - } -} - #[derive(Debug)] pub(crate) struct PaymentListenerStateInner { pub(crate) last_checked: OffsetDateTime, @@ -181,3 +262,131 @@ impl WatcherFailureDetails { } } } + +#[derive(Debug, Clone)] +pub(crate) struct BankScraperModuleState { + pub(crate) inner: Arc>, +} + +impl BankScraperModuleState { + // TODO: make those configurable + const MAX_LAST_BANK_MSGS: usize = 20; + const MAX_LAST_WATCHED_BANK_MSGS: usize = 10; + const MAX_LAST_REJECTED_BANK_MSGS: usize = 25; + + pub(crate) fn new() -> Self { + BankScraperModuleState { + inner: Arc::new(RwLock::new(BankScraperModuleStateInner { + processed_bank_msgs_since_startup: 0, + processed_bank_msgs_to_watched_addresses_since_startup: 0, + rejected_bank_msgs_to_watched_addresses_since_startup: 0, + last_seen_bank_msgs: RingBuffer::new(Self::MAX_LAST_BANK_MSGS), + last_seen_watched_bank_msgs: RingBuffer::new(Self::MAX_LAST_WATCHED_BANK_MSGS), + last_rejected_watched_bank_msgs: RingBuffer::new(Self::MAX_LAST_REJECTED_BANK_MSGS), + })), + } + } + + pub(crate) async fn new_bank_msg( + &self, + tx: &ParsedTransactionResponse, + index: usize, + msg: &MsgSend, + is_watched: bool, + ) { + let mut guard = self.inner.write().await; + guard.processed_bank_msgs_since_startup += 1; + + let details = BankMsgDetails { + processed_at: OffsetDateTime::now_utc(), + tx_hash: tx.hash.to_string(), + height: tx.height.value(), + index: index as u32, + from: msg.from_address.to_string(), + to: msg.to_address.to_string(), + amount: msg.amount.iter().map(|c| c.to_string()).collect(), + memo: tx.tx.body.memo.clone(), + }; + guard.last_seen_bank_msgs.push(details.clone()); + + if is_watched { + guard.processed_bank_msgs_to_watched_addresses_since_startup += 1; + guard.last_seen_watched_bank_msgs.push(details.clone()); + } + } + + pub(crate) async fn new_rejection>( + &self, + tx_hash: String, + height: u64, + index: u32, + error: S, + ) { + self.inner + .write() + .await + .last_rejected_watched_bank_msgs + .push(BankMsgRejection { + rejected_at: OffsetDateTime::now_utc(), + tx_hash, + height, + index, + error: error.into(), + }) + } +} + +#[derive(Debug)] +pub(crate) struct BankScraperModuleStateInner { + pub(crate) processed_bank_msgs_since_startup: usize, + pub(crate) processed_bank_msgs_to_watched_addresses_since_startup: usize, + pub(crate) rejected_bank_msgs_to_watched_addresses_since_startup: usize, + + pub(crate) last_seen_bank_msgs: RingBuffer, + pub(crate) last_seen_watched_bank_msgs: RingBuffer, + pub(crate) last_rejected_watched_bank_msgs: RingBuffer, +} + +#[derive(Debug, Clone)] +pub(crate) struct BankMsgDetails { + pub(crate) processed_at: OffsetDateTime, + pub(crate) tx_hash: String, + pub(crate) height: u64, + pub(crate) index: u32, + pub(crate) from: String, + pub(crate) to: String, + pub(crate) amount: Vec, + pub(crate) memo: String, +} + +#[derive(Debug)] +pub(crate) struct BankMsgRejection { + pub(crate) rejected_at: OffsetDateTime, + pub(crate) tx_hash: String, + pub(crate) height: u64, + pub(crate) index: u32, + pub(crate) error: String, +} + +impl FromRef for PaymentListenerState { + fn from_ref(input: &AppState) -> Self { + input.payment_listener_state.clone() + } +} +impl FromRef for StatusState { + fn from_ref(input: &AppState) -> Self { + input.status_state.clone() + } +} + +impl FromRef for PriceScraperState { + fn from_ref(input: &AppState) -> Self { + input.price_scraper_state.clone() + } +} + +impl FromRef for BankScraperModuleState { + fn from_ref(input: &AppState) -> Self { + input.bank_scraper_module_state.clone() + } +} diff --git a/nyx-chain-watcher/src/price_scraper/mod.rs b/nyx-chain-watcher/src/price_scraper/mod.rs index 10df1e17e7..6442118579 100644 --- a/nyx-chain-watcher/src/price_scraper/mod.rs +++ b/nyx-chain-watcher/src/price_scraper/mod.rs @@ -6,49 +6,71 @@ use core::str; use tokio::time::Duration; use crate::db::DbPool; +use crate::http::state::PriceScraperState; const REFRESH_DELAY: Duration = Duration::from_secs(300); const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(60 * 2); const COINGECKO_API_URL: &str = "https://api.coingecko.com/api/v3/simple/price?ids=nym&vs_currencies=chf,usd,eur,gbp,btc"; -pub(crate) async fn run_price_scraper(db_pool: &DbPool) { - loop { - tracing::info!("Running in a loop 🏃"); - if let Err(e) = get_coingecko_prices(db_pool).await { - tracing::error!("❌ Failed to get CoinGecko prices: {e}"); - tracing::info!("Retrying in {}s...", FAILURE_RETRY_DELAY.as_secs()); - tokio::time::sleep(FAILURE_RETRY_DELAY).await; - } else { - tracing::info!("✅ Successfully fetched CoinGecko prices"); - tokio::time::sleep(REFRESH_DELAY).await; - } - } +pub(crate) struct PriceScraper { + shared_state: PriceScraperState, + db_pool: DbPool, } -async fn get_coingecko_prices(pool: &DbPool) -> anyhow::Result<()> { - tracing::info!("💰 Fetching CoinGecko prices from {}", COINGECKO_API_URL); - - let response = reqwest::get(COINGECKO_API_URL) - .await? - .json::() - .await; - - tracing::info!("Got response {:?}", response); - match response { - Ok(resp) => { - let price_record = PriceRecord { - timestamp: time::OffsetDateTime::now_utc().unix_timestamp(), - nym: resp.nym, - }; - - insert_nym_prices(pool, price_record).await?; - } - Err(e) => { - //tracing::info!("💰 CoinGecko price response: {:?}", response); - tracing::error!("Error sending request: {}", e); +impl PriceScraper { + pub(crate) fn new(shared_state: PriceScraperState, db_pool: DbPool) -> Self { + PriceScraper { + shared_state, + db_pool, } } - Ok(()) + async fn get_coingecko_prices(&self) -> anyhow::Result { + tracing::info!("💰 Fetching CoinGecko prices from {COINGECKO_API_URL}"); + + let response = reqwest::get(COINGECKO_API_URL) + .await? + .json::() + .await; + + tracing::info!("Got response {:?}", response); + match response { + Ok(resp) => { + let price_record = PriceRecord { + timestamp: time::OffsetDateTime::now_utc().unix_timestamp(), + nym: resp.nym.clone(), + }; + + insert_nym_prices(&self.db_pool, price_record).await?; + Ok(resp) + } + Err(err) => { + //tracing::info!("💰 CoinGecko price response: {:?}", response); + tracing::error!("Error sending request: {err}"); + Err(err.into()) + } + } + } + + pub(crate) async fn run(&self) { + loop { + tracing::info!("Running in a loop 🏃"); + match self.get_coingecko_prices().await { + Ok(coingecko_price_response) => { + self.shared_state + .new_success(coingecko_price_response) + .await; + tracing::info!("✅ Successfully fetched CoinGecko prices"); + tokio::time::sleep(REFRESH_DELAY).await; + } + Err(err) => { + tracing::error!("❌ Failed to get CoinGecko prices: {err}"); + tracing::info!("Retrying in {}s...", FAILURE_RETRY_DELAY.as_secs()); + self.shared_state.new_failure(err.to_string()).await; + tokio::time::sleep(FAILURE_RETRY_DELAY).await; + } + } + } + } } From 6a3558129949b36ea4da554db110df4ddf9a4e56 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 13:40:54 +0000 Subject: [PATCH 086/133] build(deps-dev): bump webpack-dev-middleware (#5610) Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 5.3.3 to 5.3.4. - [Release notes](https://github.com/webpack/webpack-dev-middleware/releases) - [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md) - [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.3...v5.3.4) --- updated-dependencies: - dependency-name: webpack-dev-middleware dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- wasm/client/internal-dev/package-lock.json | 6 +++--- wasm/client/internal-dev/yarn.lock | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/wasm/client/internal-dev/package-lock.json b/wasm/client/internal-dev/package-lock.json index 447a40446e..24a84309c8 100644 --- a/wasm/client/internal-dev/package-lock.json +++ b/wasm/client/internal-dev/package-lock.json @@ -3951,9 +3951,9 @@ } }, "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==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", "dev": true, "license": "MIT", "dependencies": { diff --git a/wasm/client/internal-dev/yarn.lock b/wasm/client/internal-dev/yarn.lock index 80b223e299..84f64331bc 100644 --- a/wasm/client/internal-dev/yarn.lock +++ b/wasm/client/internal-dev/yarn.lock @@ -2159,9 +2159,9 @@ webpack-cli@^4.9.2: webpack-merge "^5.7.3" webpack-dev-middleware@^5.3.1: - 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== + version "5.3.4" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517" + integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== dependencies: colorette "^2.0.10" memfs "^3.4.3" From 8b03e66ba7d0939c71a82a945e395e502814b7a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 13:41:18 +0000 Subject: [PATCH 087/133] build(deps): bump braces in /sdk/typescript/packages/nodejs-client (#5611) Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3. - [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3) --- updated-dependencies: - dependency-name: braces dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- sdk/typescript/packages/nodejs-client/yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sdk/typescript/packages/nodejs-client/yarn.lock b/sdk/typescript/packages/nodejs-client/yarn.lock index 2736c80b20..e2fdd3fe38 100644 --- a/sdk/typescript/packages/nodejs-client/yarn.lock +++ b/sdk/typescript/packages/nodejs-client/yarn.lock @@ -506,11 +506,11 @@ brace-expansion@^2.0.1: balanced-match "^1.0.0" braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== dependencies: - fill-range "^7.0.1" + fill-range "^7.1.1" builtin-modules@^3.3.0: version "3.3.0" @@ -1215,10 +1215,10 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" From 79ce611d2133452ede6e61cece3c92ff11ceb39d Mon Sep 17 00:00:00 2001 From: Jack Wampler Date: Wed, 12 Mar 2025 10:14:04 -0600 Subject: [PATCH 088/133] Server Side internal DoT/DoH opt out (#5577) --- Cargo.lock | 1 + .../validator-client/src/coconut/mod.rs | 6 ++ common/http-api-client/src/dns.rs | 23 +++++++ common/http-api-client/src/lib.rs | 62 +++++++++++++------ nym-api/src/node_describe_cache/mod.rs | 1 + .../nym-node-status-api/Cargo.toml | 1 + .../nym-node-status-api/src/monitor/mod.rs | 8 ++- .../src/node_scraper/mod.rs | 8 ++- nym-node/Cargo.toml | 2 +- nym-node/src/error.rs | 7 +++ nym-node/src/node/http/error.rs | 7 +++ nym-node/src/node/mod.rs | 20 +++++- nym-node/src/node/shared_network.rs | 11 +++- 13 files changed, 128 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e939caf769..4d30ae54a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6355,6 +6355,7 @@ dependencies = [ "nym-contracts-common", "nym-crypto", "nym-explorer-client", + "nym-http-api-client", "nym-network-defaults", "nym-node-metrics", "nym-node-requests", diff --git a/common/client-libs/validator-client/src/coconut/mod.rs b/common/client-libs/validator-client/src/coconut/mod.rs index 95270a25ed..67974c0170 100644 --- a/common/client-libs/validator-client/src/coconut/mod.rs +++ b/common/client-libs/validator-client/src/coconut/mod.rs @@ -83,6 +83,12 @@ impl TryFrom for EcashApiClient { let url_address = Url::parse(&share.announce_address)?; + // The NymApiClient constructed here uses the default (hickory DoT/DoH) resolver because + // this EcashApiClient is used by both client and non-client applications. + // + // In non-client applications this resolver can cause warning logs about H2 connection + // failure. This indicates that the long lived https connection was closed by the remote + // peer and the resolver will have to reconnect. It should not impact actual functionality Ok(EcashApiClient { api_client: NymApiClient::new(url_address), verification_key: VerificationKeyAuth::try_from_bs58(&share.share)?, diff --git a/common/http-api-client/src/dns.rs b/common/http-api-client/src/dns.rs index 7e0519de98..4d4aae82d6 100644 --- a/common/http-api-client/src/dns.rs +++ b/common/http-api-client/src/dns.rs @@ -1,3 +1,6 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + //! DNS resolver configuration for internal lookups. //! //! The resolver itself is the set combination of the google, cloudflare, and quad9 endpoints @@ -9,6 +12,19 @@ //! //! Requires the `dns-over-https-rustls`, `webpki-roots` feature for the //! `hickory-resolver` crate +//! +//! +//! Note: The hickory DoH resolver can cause warning logs about H2 connection failure. This +//! indicates that the long lived https connection was closed by the remote peer and the resolver +//! will have to reconnect. It should not impact actual functionality. +//! +//! code ref: https://github.com/hickory-dns/hickory-dns/blob/06a8b1ce9bd9322d8e6accf857d30257e1274427/crates/proto/src/h2/h2_client_stream.rs#L534 +//! +//! example log: +//! +//! ```txt +//! WARN /home/ubuntu/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/hickory-proto-0.24.3/src/h2/h2_client_stream.rs:493: h2 connection failed: unexpected end of file +//! ``` #![deny(missing_docs)] use crate::ClientBuilder; @@ -33,6 +49,13 @@ impl ClientBuilder { /// Override the DNS resolver implementation used by the underlying http client. pub fn dns_resolver(mut self, resolver: Arc) -> Self { self.reqwest_client_builder = self.reqwest_client_builder.dns_resolver(resolver); + self.use_secure_dns = false; + self + } + + /// Override the DNS resolver implementation used by the underlying http client. + pub fn no_hickory_dns(mut self) -> Self { + self.use_secure_dns = false; self } } diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index 4b2938daa4..5a360f7be1 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -228,6 +228,8 @@ pub struct ClientBuilder { timeout: Option, custom_user_agent: bool, reqwest_client_builder: reqwest::ClientBuilder, + #[allow(dead_code)] // not dead code, just unused in wasm + use_secure_dns: bool, } impl ClientBuilder { @@ -239,37 +241,46 @@ impl ClientBuilder { U: IntoUrl, E: Display, { - // a naive check: if the provided URL does not start with http(s), add that scheme let str_url = url.as_str(); + // a naive check: if the provided URL does not start with http(s), add that scheme if !str_url.starts_with("http") { let alt = format!("http://{str_url}"); warn!("the provided url ('{str_url}') does not contain scheme information. Changing it to '{alt}' ..."); // TODO: or should we maybe default to https? Self::new(alt) } else { - #[cfg(target_arch = "wasm32")] - let reqwest_client_builder = reqwest::ClientBuilder::new(); + Ok(Self::new_with_url(url.into_url()?)) + } + } - #[cfg(not(target_arch = "wasm32"))] - let reqwest_client_builder = { - let r = reqwest::ClientBuilder::new() - .dns_resolver(Arc::new(HickoryDnsResolver::default())); + /// Constructs a new http `ClientBuilder` from a valid url. + pub fn new_with_url(url: Url) -> Self { + if !url.scheme().starts_with("http") { + warn!("the provided url ('{url}') does not use HTTP / HTTPS scheme"); + } - // Note this is extra as the `gzip` feature for `reqwest` crate should be enabled which - // `"Enable[s] auto gzip decompression by checking the Content-Encoding response header."` - // - // I am going to leave it here anyways so that gzip decompression is attempted even if - // that feature is removed. - r.gzip(true) - }; + #[cfg(target_arch = "wasm32")] + let reqwest_client_builder = reqwest::ClientBuilder::new(); - Ok(ClientBuilder { - url: url.into_url()?, - timeout: None, - custom_user_agent: false, - reqwest_client_builder, - }) + #[cfg(not(target_arch = "wasm32"))] + let reqwest_client_builder = { + let r = reqwest::ClientBuilder::new(); + + // Note this is extra as the `gzip` feature for `reqwest` crate should be enabled which + // `"Enable[s] auto gzip decompression by checking the Content-Encoding response header."` + // + // I am going to leave it here anyways so that gzip decompression is attempted even if + // that feature is removed. + r.gzip(true) + }; + + ClientBuilder { + url, + timeout: None, + custom_user_agent: false, + reqwest_client_builder, + use_secure_dns: true, } } @@ -325,10 +336,18 @@ impl ClientBuilder { let mut builder = self .reqwest_client_builder .timeout(self.timeout.unwrap_or(DEFAULT_TIMEOUT)); + + // if no custom user agent was set, use a default if !self.custom_user_agent { builder = builder.user_agent(format!("nym-http-api-client/{}", env!("CARGO_PKG_VERSION"))) } + + // unless explicitly disabled use the DoT/DoH enabled resolver + if self.use_secure_dns { + builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::default())); + } + builder.build()? }; @@ -355,6 +374,9 @@ pub struct Client { impl Client { /// Create a new http `Client` // no timeout until https://github.com/seanmonstar/reqwest/issues/1135 is fixed + // + // In order to prevent interference in API requests at the DNS phase we default to a resolver + // that uses DoT and DoH. pub fn new(base_url: Url, timeout: Option) -> Self { Self::new_url::<_, String>(base_url, timeout).expect( "we provided valid url and we were unwrapping previous construction errors anyway", diff --git a/nym-api/src/node_describe_cache/mod.rs b/nym-api/src/node_describe_cache/mod.rs index 8fa5adf452..cdb9ab67b8 100644 --- a/nym-api/src/node_describe_cache/mod.rs +++ b/nym-api/src/node_describe_cache/mod.rs @@ -191,6 +191,7 @@ async fn try_get_client( // if provided host was malformed, no point in continuing let client = match nym_node_requests::api::Client::builder(address).and_then(|b| { b.with_timeout(Duration::from_secs(5)) + .no_hickory_dns() .with_user_agent("nym-api-describe-cache") .build() }) { diff --git a/nym-node-status-api/nym-node-status-api/Cargo.toml b/nym-node-status-api/nym-node-status-api/Cargo.toml index 0642149101..f78218e246 100644 --- a/nym-node-status-api/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/nym-node-status-api/Cargo.toml @@ -29,6 +29,7 @@ nym-bin-common = { path = "../../common/bin-common", features = ["models"] } nym-node-status-client = { path = "../nym-node-status-client" } nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "serde"] } nym-explorer-client = { path = "../../explorer-api/explorer-client" } +nym-http-api-client = { path = "../../common/http-api-client" } nym-network-defaults = { path = "../../common/network-defaults" } nym-serde-helpers = { path = "../../common/serde-helpers"} nym-statistics-common = { path = "../../common/statistics" } diff --git a/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs b/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs index 05b7dc843d..81711d7640 100644 --- a/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs @@ -97,8 +97,12 @@ impl Monitor { .clone() .expect("rust sdk mainnet default missing api_url"); - let api_client = - NymApiClient::new_with_timeout(default_api_url, self.nym_api_client_timeout); + let nym_api = nym_http_api_client::ClientBuilder::new_with_url(default_api_url) + .no_hickory_dns() + .with_timeout(self.nym_api_client_timeout) + .build::<&str>()?; + + let api_client = NymApiClient { nym_api }; let described_nodes = api_client .get_all_described_nodes() diff --git a/nym-node-status-api/nym-node-status-api/src/node_scraper/mod.rs b/nym-node-status-api/nym-node-status-api/src/node_scraper/mod.rs index 04dd897098..f294b47b0f 100644 --- a/nym-node-status-api/nym-node-status-api/src/node_scraper/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/node_scraper/mod.rs @@ -57,7 +57,12 @@ async fn run( .clone() .expect("rust sdk mainnet default missing api_url"); - let api_client = NymApiClient::new_with_timeout(default_api_url, nym_api_client_timeout); + let nym_api = nym_http_api_client::ClientBuilder::new_with_url(default_api_url) + .no_hickory_dns() + .with_timeout(nym_api_client_timeout) + .build::<&str>()?; + + let api_client = NymApiClient { nym_api }; //SW TBC what nodes exactly need to be scraped, the skimmed node endpoint seems to return more nodes let bonded_nodes = api_client.get_all_bonded_nym_nodes().await?; @@ -170,6 +175,7 @@ impl MetricsScrapingData { let client = match nym_node_requests::api::Client::builder(address).and_then(|b| { b.with_timeout(Duration::from_secs(5)) .with_user_agent("node-status-api-metrics-scraper") + .no_hickory_dns() .build() }) { Ok(client) => client, diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 0cf816ba5b..79685d1ae9 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -71,7 +71,7 @@ nym-verloc = { path = "../common/verloc" } nym-metrics = { path = "../common/nym-metrics" } nym-gateway-stats-storage = { path = "../common/gateway-stats-storage" } nym-topology = { path = "../common/topology" } - +nym-http-api-client = { path = "../common/http-api-client" } # http server # useful for `#[axum_macros::debug_handler]` diff --git a/nym-node/src/error.rs b/nym-node/src/error.rs index 5eb5545e21..d7cec1edbd 100644 --- a/nym-node/src/error.rs +++ b/nym-node/src/error.rs @@ -3,6 +3,7 @@ use crate::node::http::error::NymNodeHttpError; use crate::wireguard::error::WireguardError; +use nym_http_api_client::HttpClientError; use nym_ip_packet_router::error::ClientCoreError; use nym_validator_client::ValidatorClientError; use std::io; @@ -209,3 +210,9 @@ pub enum ServiceProvidersError { #[error(transparent)] ExternalClientCore(#[from] ClientCoreError), } + +impl From for NymNodeError { + fn from(value: HttpClientError) -> Self { + Self::HttpFailure(NymNodeHttpError::ClientError { source: value }) + } +} diff --git a/nym-node/src/node/http/error.rs b/nym-node/src/node/http/error.rs index 251705f520..f63ed26f49 100644 --- a/nym-node/src/node/http/error.rs +++ b/nym-node/src/node/http/error.rs @@ -1,6 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use nym_http_api_client::HttpClientError; use std::io; use std::net::SocketAddr; use thiserror::Error; @@ -24,4 +25,10 @@ pub enum NymNodeHttpError { #[from] source: nym_crypto::asymmetric::encryption::KeyRecoveryError, }, + + #[error("error building or using HTTP client: {source}")] + ClientError { + #[from] + source: HttpClientError, + }, } diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index 3a8d7bf867..f68a2d5553 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -814,9 +814,23 @@ impl NymNode { return; } - for nym_api in &self.config.mixnet.nym_api_urls { - info!("trying {nym_api}..."); - let client = NymApiClient::new_with_user_agent(nym_api.clone(), self.user_agent()); + for nym_api_url in &self.config.mixnet.nym_api_urls { + info!("trying {nym_api_url}..."); + + let nym_api = + match nym_http_api_client::ClientBuilder::new_with_url(nym_api_url.clone()) + .no_hickory_dns() + .with_user_agent(self.user_agent()) + .build::<&str>() + { + Ok(b) => b, + Err(e) => { + warn!("failed to build http client for \"{nym_api_url}\": {e}",); + continue; + } + }; + + let client = NymApiClient { nym_api }; // make new request every time in case previous one takes longer and invalidates the signature let request = NodeRefreshBody::new(self.ed25519_identity_keys.private_key()); diff --git a/nym-node/src/node/shared_network.rs b/nym-node/src/node/shared_network.rs index c127aa1586..aa2e378541 100644 --- a/nym-node/src/node/shared_network.rs +++ b/nym-node/src/node/shared_network.rs @@ -9,6 +9,7 @@ use nym_node_metrics::prometheus_wrapper::{PrometheusMetric, PROMETHEUS_METRICS} use nym_task::ShutdownToken; use nym_topology::node::RoutingNode; use nym_topology::{EpochRewardedSet, NymTopology, Role, TopologyProvider}; +use nym_validator_client::nym_api::NymApiClientExt; use nym_validator_client::nym_nodes::{NodesByAddressesResponse, SkimmedNode}; use nym_validator_client::{NymApiClient, ValidatorClientError}; use std::collections::HashSet; @@ -167,6 +168,7 @@ impl NodesQuerier { ) -> Result { let res = self .client + .nym_api .nodes_by_addresses(ips) .await .inspect_err(|err| error!("failed to obtain node information: {err}")); @@ -174,7 +176,7 @@ impl NodesQuerier { if res.is_err() { self.use_next_nym_api() } - res + Ok(res?) } } @@ -263,9 +265,14 @@ impl NetworkRefresher { pending_check_interval: Duration, shutdown_token: ShutdownToken, ) -> Result { + let nym_api = nym_http_api_client::Client::builder(nym_api_urls[0].clone())? + .no_hickory_dns() + .with_user_agent(user_agent) + .build()?; + let mut this = NetworkRefresher { querier: NodesQuerier { - client: NymApiClient::new_with_user_agent(nym_api_urls[0].clone(), user_agent), + client: NymApiClient { nym_api }, nym_api_urls, currently_used_api: 0, }, From dc88650d6d8aced3e51bb58742f5ecccbeeccd3e Mon Sep 17 00:00:00 2001 From: Fouad Date: Thu, 13 Mar 2025 11:31:59 +0000 Subject: [PATCH 089/133] Explorer V2 (#5548) * remove pnpm lock file (should only be using yarn) * Add lefthook configuration for pre-commit checks * Add explorer-v2 to package.json dependencies * add explorer v2 * update explorer v2 package name * + basepath + redirect to basepath + blog icons refactor + icons refactor * Add Getting Started instructions to README * fix noise graph bug and line graph UI * Delete unused translations, clean up console logs * / test image url * update yarn.lock --------- Co-authored-by: RadekSabacky Co-authored-by: windy-ux <75579979+windy-ux@users.noreply.github.com> Co-authored-by: Yana Co-authored-by: Mark Sinclair --- README.md | 10 + explorer-v2/.eslintrc.json | 3 + explorer-v2/.gitignore | 40 + explorer-v2/README.md | 36 + explorer-v2/biome.json | 33 + explorer-v2/next.config.ts | 23 + explorer-v2/package.json | 64 + explorer-v2/public/file.svg | 1 + explorer-v2/public/globe.svg | 1 + explorer-v2/public/icons/arrow-up-right.svg | 61 + explorer-v2/public/icons/arrow.svg | 7 + explorer-v2/public/icons/buy-card.svg | 31 + explorer-v2/public/icons/chevron.svg | 13 + explorer-v2/public/icons/chevronMenu.svg | 14 + explorer-v2/public/icons/copy-file.svg | 35 + explorer-v2/public/icons/cross.svg | 135 + explorer-v2/public/icons/discord.svg | 3 + explorer-v2/public/icons/document.svg | 10 + explorer-v2/public/icons/download.svg | 4 + explorer-v2/public/icons/elips.svg | 22 + explorer-v2/public/icons/explorer-card.svg | 3 + explorer-v2/public/icons/gateway.svg | 79 + explorer-v2/public/icons/github.svg | 3 + explorer-v2/public/icons/nym-logo.svg | 14 + explorer-v2/public/icons/operate-card.svg | 3 + explorer-v2/public/icons/stake-card.svg | 3 + explorer-v2/public/icons/telegram.svg | 3 + explorer-v2/public/icons/token.svg | 23 + explorer-v2/public/icons/twitter.svg | 4 + explorer-v2/public/icons/youtube.svg | 5 + explorer-v2/public/icons/youtubeInverted.svg | 4 + explorer-v2/public/images/Network.webp | Bin 0 -> 98936 bytes explorer-v2/public/images/buy-article.webp | Bin 0 -> 555820 bytes explorer-v2/public/images/placeholder.webp | Bin 0 -> 47094 bytes explorer-v2/public/images/stake-article.webp | Bin 0 -> 396370 bytes explorer-v2/public/next.svg | 1 + .../public/profileImagePlaceholder.png | Bin 0 -> 3832 bytes explorer-v2/public/vercel.svg | 1 + explorer-v2/public/window.svg | 1 + .../account/[address]/not-found/page.tsx | 62 + .../app/(pages)/account/[address]/page.tsx | 67 + .../src/app/(pages)/nym-node/[id]/page.tsx | 128 + .../app/(pages)/onboarding/[slug]/page.tsx | 155 + .../src/app/(pages)/onboarding/page.tsx | 15 + explorer-v2/src/app/(pages)/stake/page.tsx | 24 + explorer-v2/src/app/(pages)/table/page.tsx | 30 + explorer-v2/src/app/api/index.tsx | 271 + explorer-v2/src/app/api/strapi.d.ts | 23417 ++++++++++++++++ explorer-v2/src/app/api/types.ts | 482 + explorer-v2/src/app/api/urls.ts | 28 + explorer-v2/src/app/constants.ts | 1 + explorer-v2/src/app/error.tsx | 27 + explorer-v2/src/app/favicon.png | Bin 0 -> 1144 bytes .../src/app/features/footer/api/getFooter.ts | 34 + .../footer/components/footer-links.tsx | 72 + .../app/features/footer/config/constants.ts | 1 + explorer-v2/src/app/globals.css | 68 + explorer-v2/src/app/i18n/index.ts | 41 + .../src/app/i18n/locales/en/footer.json | 92 + .../app/i18n/locales/en/social-channels.json | 12 + explorer-v2/src/app/i18n/settings.ts | 16 + explorer-v2/src/app/i18n/types/freepass.ts | 46 + explorer-v2/src/app/i18n/types/index.ts | 13 + explorer-v2/src/app/icon.png | Bin 0 -> 1144 bytes explorer-v2/src/app/layout.tsx | 30 + explorer-v2/src/app/lib/strapi.d.ts | 23417 ++++++++++++++++ explorer-v2/src/app/lib/strapiClient.ts | 38 + explorer-v2/src/app/page.tsx | 67 + .../AccountBalancesCard.tsx | 219 + .../AccountBalancesTable.tsx | 321 + .../accountPageComponents/AccountInfoCard.tsx | 84 + .../src/components/blogs/BlogArticleCards.tsx | 86 + .../src/components/blogs/TableOfContents.tsx | 37 + explorer-v2/src/components/blogs/types.ts | 29 + .../components/breadcrumbs/Breadcrumbs.tsx | 70 + explorer-v2/src/components/button/index.tsx | 124 + explorer-v2/src/components/button/types.ts | 31 + .../src/components/buttonOrLink/index.tsx | 63 + .../src/components/cards/ExplorerCard.tsx | 42 + .../src/components/cards/ExplorerHeroCard.tsx | 71 + .../src/components/clientOnly/ClientOnly.tsx | 18 + explorer-v2/src/components/comments/index.tsx | 82 + .../contentLayout/ContentLayout.tsx | 25 + .../copyToClipboard/CopyToClipboard.tsx | 47 + .../components/countryFlag/CountryFlag.tsx | 22 + .../src/components/epochtime/EpochTime.tsx | 76 + .../src/components/favorite/Favorite.tsx | 56 + explorer-v2/src/components/footer/MuiLink.tsx | 28 + .../components/footer/NewsLetterSignUp.tsx | 63 + .../src/components/footer/SocialChannels.tsx | 40 + .../src/components/footer/SocialIcon.tsx | 21 + explorer-v2/src/components/footer/Wrapper.tsx | 21 + .../src/components/footer/footer-links.tsx | 81 + explorer-v2/src/components/footer/index.tsx | 155 + .../src/components/header/DesktopHeader.tsx | 57 + .../src/components/header/HeaderItem.tsx | 31 + .../src/components/header/MobileHeader.tsx | 155 + explorer-v2/src/components/header/index.tsx | 12 + .../src/components/header/menuItems.ts | 25 + .../src/components/header/navigationIcons.ts | 6 + .../components/headings/SectionHeading.tsx | 7 + .../src/components/icons/ArrowUpRight.tsx | 14 + explorer-v2/src/components/icons/Chevron.tsx | 15 + explorer-v2/src/components/icons/CopyFile.tsx | 15 + explorer-v2/src/components/icons/Cross.tsx | 8 + explorer-v2/src/components/icons/Elips.tsx | 6 + explorer-v2/src/components/icons/Gateway.tsx | 10 + explorer-v2/src/components/icons/NymLogo.tsx | 9 + .../src/components/icons/NymTokenSVG.tsx | 40 + explorer-v2/src/components/icons/Token.tsx | 6 + explorer-v2/src/components/input/Input.tsx | 38 + .../CurrentEpochCard.tsx | 108 + .../NetworkStakeCard.tsx | 89 + .../landingPageComponents/NoiseCard.tsx | 132 + .../StakersNumberCard.tsx | 50 + .../landingPageComponents/TokenomicsCard.tsx | 127 + .../src/components/lineChart/index.tsx | 119 + explorer-v2/src/components/list/ListItem.tsx | 50 + explorer-v2/src/components/loading/index.tsx | 37 + .../src/components/modal/InfoModal.tsx | 53 + .../src/components/modal/SimpleModal.tsx | 53 + explorer-v2/src/components/modal/index.tsx | 156 + explorer-v2/src/components/muiLink/index.tsx | 28 + .../src/components/nodeTable/NodeTable.tsx | 396 + .../nodeTable/NodeTableWithAction.tsx | 117 + .../nymNodePageComponents/BasicInfoCard.tsx | 109 + .../nymNodePageComponents/ChatCard.tsx | 10 + .../DelegationsTable.tsx | 144 + .../nymNodePageComponents/NodeDataCard.tsx | 88 + .../NodeDelegationsCard.tsx | 51 + .../NodeParametersCard.tsx | 125 + .../nymNodePageComponents/NodeProfileCard.tsx | 205 + .../nymNodePageComponents/NodeRoleCard.tsx | 288 + .../src/components/price/TitlePrice.tsx | 31 + .../components/price/UpDownPriceIndicator.tsx | 33 + .../progressBars/EpochProgressBar.tsx | 33 + .../progressBars/MultiSegmentProgressBar.tsx | 35 + .../components/progressBars/ProgressBar.tsx | 19 + .../progressBars/StaticProgressBar.tsx | 29 + explorer-v2/src/components/qrCode/QrCode.tsx | 22 + .../redeemRewards/RedeemRewardsModal.tsx | 45 + .../search/NodeAndAddressSearch.tsx | 120 + .../components/staking/OriginalStakeCard.tsx | 58 + .../src/components/staking/OverviewCards.tsx | 46 + .../src/components/staking/StakeActions.tsx | 88 + .../src/components/staking/StakeModal.tsx | 124 + .../src/components/staking/StakeTable.tsx | 646 + .../staking/StakeTableWithAction.tsx | 115 + .../src/components/staking/SubHeaderRow.tsx | 18 + .../staking/SubHeaderRowActions.tsx | 173 + .../components/staking/TotalRewardsCard.tsx | 58 + .../src/components/staking/TotalStakeCard.tsx | 58 + explorer-v2/src/components/staking/schemas.ts | 47 + .../src/components/starRating/StarRating.tsx | 31 + .../components/toggleButton/ToggleButton.tsx | 60 + .../src/components/wallet/ConnectWallet.tsx | 81 + .../src/components/wallet/WalletAddress.tsx | 23 + .../src/components/wallet/WalletBalance.tsx | 39 + explorer-v2/src/components/wrapper/index.tsx | 20 + explorer-v2/src/config/index.ts | 6 + .../src/data/Welcome-to-explorer-2.json | 107 + explorer-v2/src/data/buy-nym.json | 158 + explorer-v2/src/data/operate-a-node.json | 158 + explorer-v2/src/data/staking-guide.json | 121 + .../LabGrotesque/LabGrotesque-Black.woff | Bin 0 -> 62644 bytes .../LabGrotesque/LabGrotesque-Black.woff2 | Bin 0 -> 51180 bytes .../LabGrotesque-BlackItalic.woff | Bin 0 -> 66220 bytes .../LabGrotesque-BlackItalic.woff2 | Bin 0 -> 54072 bytes .../fonts/LabGrotesque/LabGrotesque-Bold.woff | Bin 0 -> 62960 bytes .../LabGrotesque/LabGrotesque-Bold.woff2 | Bin 0 -> 51496 bytes .../LabGrotesque/LabGrotesque-BoldItalic.woff | Bin 0 -> 66916 bytes .../LabGrotesque-BoldItalic.woff2 | Bin 0 -> 54812 bytes .../LabGrotesque/LabGrotesque-Italic.woff | Bin 0 -> 63352 bytes .../LabGrotesque/LabGrotesque-Italic.woff2 | Bin 0 -> 51800 bytes .../LabGrotesque/LabGrotesque-Light.woff | Bin 0 -> 56152 bytes .../LabGrotesque/LabGrotesque-Light.woff2 | Bin 0 -> 45936 bytes .../LabGrotesque-LightItalic.woff | Bin 0 -> 59828 bytes .../LabGrotesque-LightItalic.woff2 | Bin 0 -> 48960 bytes .../LabGrotesque/LabGrotesque-Medium.woff | Bin 0 -> 61200 bytes .../LabGrotesque/LabGrotesque-Medium.woff2 | Bin 0 -> 50020 bytes .../LabGrotesque-MediumItalic.woff | Bin 0 -> 65124 bytes .../LabGrotesque-MediumItalic.woff2 | Bin 0 -> 53376 bytes .../LabGrotesque/LabGrotesque-Regular.woff | Bin 0 -> 59984 bytes .../LabGrotesque/LabGrotesque-Regular.woff2 | Bin 0 -> 49072 bytes .../fonts/LabGrotesque/LabGrotesque-Thin.woff | Bin 0 -> 57404 bytes .../LabGrotesque/LabGrotesque-Thin.woff2 | Bin 0 -> 47076 bytes .../LabGrotesque/LabGrotesque-ThinItalic.woff | Bin 0 -> 60956 bytes .../LabGrotesque-ThinItalic.woff2 | Bin 0 -> 49868 bytes .../fonts/LabGrotesque/_LicenseAgreement.txt | 2 + .../LabGrotesqueMono-Bold.woff | Bin 0 -> 45496 bytes .../LabGrotesqueMono-Bold.woff2 | Bin 0 -> 36800 bytes .../LabGrotesqueMono-Regular.woff | Bin 0 -> 42516 bytes .../LabGrotesqueMono-Regular.woff2 | Bin 0 -> 34440 bytes .../LabGrotesqueMono/_LicenseAgreement.txt | 2 + explorer-v2/src/fonts/index.ts | 85 + explorer-v2/src/hooks/useGetPendingEvents.tsx | 53 + explorer-v2/src/hooks/useGetWalletBalance.tsx | 42 + explorer-v2/src/hooks/useNymClient.ts | 47 + .../src/providers/CosmosKitProvider.tsx | 79 + explorer-v2/src/providers/EpochProvider.tsx | 109 + explorer-v2/src/providers/QueryProvider.tsx | 24 + explorer-v2/src/providers/ThemeProvider.tsx | 18 + explorer-v2/src/providers/index.tsx | 18 + explorer-v2/src/theme/colours.ts | 31 + .../src/theme/markdown-styles.module.css | 18 + explorer-v2/src/theme/mui-theme.d.ts | 71 + explorer-v2/src/theme/theme.tsx | 302 + explorer-v2/src/theme/typography.ts | 133 + explorer-v2/src/typings/FC.d.ts | 1 + .../src/typings/react-world-flags.d.ts | 1 + explorer-v2/src/utils/countryName.ts | 9 + explorer-v2/src/utils/currency.ts | 55 + explorer-v2/src/utils/formatBigNumbers.ts | 15 + explorer-v2/src/utils/getIconByName.tsx | 69 + explorer-v2/tsconfig.json | 29 + lefthook.yml | 49 + package.json | 3 +- pnpm-lock.yaml | 5334 ---- yarn.lock | 4676 ++- 219 files changed, 62763 insertions(+), 5362 deletions(-) create mode 100644 explorer-v2/.eslintrc.json create mode 100644 explorer-v2/.gitignore create mode 100644 explorer-v2/README.md create mode 100644 explorer-v2/biome.json create mode 100644 explorer-v2/next.config.ts create mode 100644 explorer-v2/package.json create mode 100644 explorer-v2/public/file.svg create mode 100644 explorer-v2/public/globe.svg create mode 100644 explorer-v2/public/icons/arrow-up-right.svg create mode 100644 explorer-v2/public/icons/arrow.svg create mode 100644 explorer-v2/public/icons/buy-card.svg create mode 100644 explorer-v2/public/icons/chevron.svg create mode 100644 explorer-v2/public/icons/chevronMenu.svg create mode 100644 explorer-v2/public/icons/copy-file.svg create mode 100644 explorer-v2/public/icons/cross.svg create mode 100644 explorer-v2/public/icons/discord.svg create mode 100644 explorer-v2/public/icons/document.svg create mode 100644 explorer-v2/public/icons/download.svg create mode 100644 explorer-v2/public/icons/elips.svg create mode 100644 explorer-v2/public/icons/explorer-card.svg create mode 100644 explorer-v2/public/icons/gateway.svg create mode 100644 explorer-v2/public/icons/github.svg create mode 100644 explorer-v2/public/icons/nym-logo.svg create mode 100644 explorer-v2/public/icons/operate-card.svg create mode 100644 explorer-v2/public/icons/stake-card.svg create mode 100644 explorer-v2/public/icons/telegram.svg create mode 100644 explorer-v2/public/icons/token.svg create mode 100644 explorer-v2/public/icons/twitter.svg create mode 100644 explorer-v2/public/icons/youtube.svg create mode 100644 explorer-v2/public/icons/youtubeInverted.svg create mode 100644 explorer-v2/public/images/Network.webp create mode 100644 explorer-v2/public/images/buy-article.webp create mode 100644 explorer-v2/public/images/placeholder.webp create mode 100644 explorer-v2/public/images/stake-article.webp create mode 100644 explorer-v2/public/next.svg create mode 100644 explorer-v2/public/profileImagePlaceholder.png create mode 100644 explorer-v2/public/vercel.svg create mode 100644 explorer-v2/public/window.svg create mode 100644 explorer-v2/src/app/(pages)/account/[address]/not-found/page.tsx create mode 100644 explorer-v2/src/app/(pages)/account/[address]/page.tsx create mode 100644 explorer-v2/src/app/(pages)/nym-node/[id]/page.tsx create mode 100644 explorer-v2/src/app/(pages)/onboarding/[slug]/page.tsx create mode 100644 explorer-v2/src/app/(pages)/onboarding/page.tsx create mode 100644 explorer-v2/src/app/(pages)/stake/page.tsx create mode 100644 explorer-v2/src/app/(pages)/table/page.tsx create mode 100644 explorer-v2/src/app/api/index.tsx create mode 100644 explorer-v2/src/app/api/strapi.d.ts create mode 100644 explorer-v2/src/app/api/types.ts create mode 100644 explorer-v2/src/app/api/urls.ts create mode 100644 explorer-v2/src/app/constants.ts create mode 100644 explorer-v2/src/app/error.tsx create mode 100644 explorer-v2/src/app/favicon.png create mode 100644 explorer-v2/src/app/features/footer/api/getFooter.ts create mode 100644 explorer-v2/src/app/features/footer/components/footer-links.tsx create mode 100644 explorer-v2/src/app/features/footer/config/constants.ts create mode 100644 explorer-v2/src/app/globals.css create mode 100644 explorer-v2/src/app/i18n/index.ts create mode 100644 explorer-v2/src/app/i18n/locales/en/footer.json create mode 100644 explorer-v2/src/app/i18n/locales/en/social-channels.json create mode 100644 explorer-v2/src/app/i18n/settings.ts create mode 100644 explorer-v2/src/app/i18n/types/freepass.ts create mode 100644 explorer-v2/src/app/i18n/types/index.ts create mode 100644 explorer-v2/src/app/icon.png create mode 100644 explorer-v2/src/app/layout.tsx create mode 100644 explorer-v2/src/app/lib/strapi.d.ts create mode 100644 explorer-v2/src/app/lib/strapiClient.ts create mode 100644 explorer-v2/src/app/page.tsx create mode 100644 explorer-v2/src/components/accountPageComponents/AccountBalancesCard.tsx create mode 100644 explorer-v2/src/components/accountPageComponents/AccountBalancesTable.tsx create mode 100644 explorer-v2/src/components/accountPageComponents/AccountInfoCard.tsx create mode 100644 explorer-v2/src/components/blogs/BlogArticleCards.tsx create mode 100644 explorer-v2/src/components/blogs/TableOfContents.tsx create mode 100644 explorer-v2/src/components/blogs/types.ts create mode 100644 explorer-v2/src/components/breadcrumbs/Breadcrumbs.tsx create mode 100644 explorer-v2/src/components/button/index.tsx create mode 100644 explorer-v2/src/components/button/types.ts create mode 100644 explorer-v2/src/components/buttonOrLink/index.tsx create mode 100644 explorer-v2/src/components/cards/ExplorerCard.tsx create mode 100644 explorer-v2/src/components/cards/ExplorerHeroCard.tsx create mode 100644 explorer-v2/src/components/clientOnly/ClientOnly.tsx create mode 100644 explorer-v2/src/components/comments/index.tsx create mode 100644 explorer-v2/src/components/contentLayout/ContentLayout.tsx create mode 100644 explorer-v2/src/components/copyToClipboard/CopyToClipboard.tsx create mode 100644 explorer-v2/src/components/countryFlag/CountryFlag.tsx create mode 100644 explorer-v2/src/components/epochtime/EpochTime.tsx create mode 100644 explorer-v2/src/components/favorite/Favorite.tsx create mode 100644 explorer-v2/src/components/footer/MuiLink.tsx create mode 100644 explorer-v2/src/components/footer/NewsLetterSignUp.tsx create mode 100644 explorer-v2/src/components/footer/SocialChannels.tsx create mode 100644 explorer-v2/src/components/footer/SocialIcon.tsx create mode 100644 explorer-v2/src/components/footer/Wrapper.tsx create mode 100644 explorer-v2/src/components/footer/footer-links.tsx create mode 100644 explorer-v2/src/components/footer/index.tsx create mode 100644 explorer-v2/src/components/header/DesktopHeader.tsx create mode 100644 explorer-v2/src/components/header/HeaderItem.tsx create mode 100644 explorer-v2/src/components/header/MobileHeader.tsx create mode 100644 explorer-v2/src/components/header/index.tsx create mode 100644 explorer-v2/src/components/header/menuItems.ts create mode 100644 explorer-v2/src/components/header/navigationIcons.ts create mode 100644 explorer-v2/src/components/headings/SectionHeading.tsx create mode 100644 explorer-v2/src/components/icons/ArrowUpRight.tsx create mode 100644 explorer-v2/src/components/icons/Chevron.tsx create mode 100644 explorer-v2/src/components/icons/CopyFile.tsx create mode 100644 explorer-v2/src/components/icons/Cross.tsx create mode 100644 explorer-v2/src/components/icons/Elips.tsx create mode 100644 explorer-v2/src/components/icons/Gateway.tsx create mode 100644 explorer-v2/src/components/icons/NymLogo.tsx create mode 100644 explorer-v2/src/components/icons/NymTokenSVG.tsx create mode 100644 explorer-v2/src/components/icons/Token.tsx create mode 100644 explorer-v2/src/components/input/Input.tsx create mode 100644 explorer-v2/src/components/landingPageComponents/CurrentEpochCard.tsx create mode 100644 explorer-v2/src/components/landingPageComponents/NetworkStakeCard.tsx create mode 100644 explorer-v2/src/components/landingPageComponents/NoiseCard.tsx create mode 100644 explorer-v2/src/components/landingPageComponents/StakersNumberCard.tsx create mode 100644 explorer-v2/src/components/landingPageComponents/TokenomicsCard.tsx create mode 100644 explorer-v2/src/components/lineChart/index.tsx create mode 100644 explorer-v2/src/components/list/ListItem.tsx create mode 100644 explorer-v2/src/components/loading/index.tsx create mode 100644 explorer-v2/src/components/modal/InfoModal.tsx create mode 100644 explorer-v2/src/components/modal/SimpleModal.tsx create mode 100644 explorer-v2/src/components/modal/index.tsx create mode 100644 explorer-v2/src/components/muiLink/index.tsx create mode 100644 explorer-v2/src/components/nodeTable/NodeTable.tsx create mode 100644 explorer-v2/src/components/nodeTable/NodeTableWithAction.tsx create mode 100644 explorer-v2/src/components/nymNodePageComponents/BasicInfoCard.tsx create mode 100644 explorer-v2/src/components/nymNodePageComponents/ChatCard.tsx create mode 100644 explorer-v2/src/components/nymNodePageComponents/DelegationsTable.tsx create mode 100644 explorer-v2/src/components/nymNodePageComponents/NodeDataCard.tsx create mode 100644 explorer-v2/src/components/nymNodePageComponents/NodeDelegationsCard.tsx create mode 100644 explorer-v2/src/components/nymNodePageComponents/NodeParametersCard.tsx create mode 100644 explorer-v2/src/components/nymNodePageComponents/NodeProfileCard.tsx create mode 100644 explorer-v2/src/components/nymNodePageComponents/NodeRoleCard.tsx create mode 100644 explorer-v2/src/components/price/TitlePrice.tsx create mode 100644 explorer-v2/src/components/price/UpDownPriceIndicator.tsx create mode 100644 explorer-v2/src/components/progressBars/EpochProgressBar.tsx create mode 100644 explorer-v2/src/components/progressBars/MultiSegmentProgressBar.tsx create mode 100644 explorer-v2/src/components/progressBars/ProgressBar.tsx create mode 100644 explorer-v2/src/components/progressBars/StaticProgressBar.tsx create mode 100644 explorer-v2/src/components/qrCode/QrCode.tsx create mode 100644 explorer-v2/src/components/redeemRewards/RedeemRewardsModal.tsx create mode 100644 explorer-v2/src/components/search/NodeAndAddressSearch.tsx create mode 100644 explorer-v2/src/components/staking/OriginalStakeCard.tsx create mode 100644 explorer-v2/src/components/staking/OverviewCards.tsx create mode 100644 explorer-v2/src/components/staking/StakeActions.tsx create mode 100644 explorer-v2/src/components/staking/StakeModal.tsx create mode 100644 explorer-v2/src/components/staking/StakeTable.tsx create mode 100644 explorer-v2/src/components/staking/StakeTableWithAction.tsx create mode 100644 explorer-v2/src/components/staking/SubHeaderRow.tsx create mode 100644 explorer-v2/src/components/staking/SubHeaderRowActions.tsx create mode 100644 explorer-v2/src/components/staking/TotalRewardsCard.tsx create mode 100644 explorer-v2/src/components/staking/TotalStakeCard.tsx create mode 100644 explorer-v2/src/components/staking/schemas.ts create mode 100644 explorer-v2/src/components/starRating/StarRating.tsx create mode 100644 explorer-v2/src/components/toggleButton/ToggleButton.tsx create mode 100644 explorer-v2/src/components/wallet/ConnectWallet.tsx create mode 100644 explorer-v2/src/components/wallet/WalletAddress.tsx create mode 100644 explorer-v2/src/components/wallet/WalletBalance.tsx create mode 100644 explorer-v2/src/components/wrapper/index.tsx create mode 100644 explorer-v2/src/config/index.ts create mode 100644 explorer-v2/src/data/Welcome-to-explorer-2.json create mode 100644 explorer-v2/src/data/buy-nym.json create mode 100644 explorer-v2/src/data/operate-a-node.json create mode 100644 explorer-v2/src/data/staking-guide.json create mode 100644 explorer-v2/src/fonts/LabGrotesque/LabGrotesque-Black.woff create mode 100644 explorer-v2/src/fonts/LabGrotesque/LabGrotesque-Black.woff2 create mode 100644 explorer-v2/src/fonts/LabGrotesque/LabGrotesque-BlackItalic.woff create mode 100644 explorer-v2/src/fonts/LabGrotesque/LabGrotesque-BlackItalic.woff2 create mode 100644 explorer-v2/src/fonts/LabGrotesque/LabGrotesque-Bold.woff create mode 100644 explorer-v2/src/fonts/LabGrotesque/LabGrotesque-Bold.woff2 create mode 100644 explorer-v2/src/fonts/LabGrotesque/LabGrotesque-BoldItalic.woff create mode 100644 explorer-v2/src/fonts/LabGrotesque/LabGrotesque-BoldItalic.woff2 create mode 100644 explorer-v2/src/fonts/LabGrotesque/LabGrotesque-Italic.woff create mode 100644 explorer-v2/src/fonts/LabGrotesque/LabGrotesque-Italic.woff2 create mode 100644 explorer-v2/src/fonts/LabGrotesque/LabGrotesque-Light.woff create mode 100644 explorer-v2/src/fonts/LabGrotesque/LabGrotesque-Light.woff2 create mode 100644 explorer-v2/src/fonts/LabGrotesque/LabGrotesque-LightItalic.woff create mode 100644 explorer-v2/src/fonts/LabGrotesque/LabGrotesque-LightItalic.woff2 create mode 100644 explorer-v2/src/fonts/LabGrotesque/LabGrotesque-Medium.woff create mode 100644 explorer-v2/src/fonts/LabGrotesque/LabGrotesque-Medium.woff2 create mode 100644 explorer-v2/src/fonts/LabGrotesque/LabGrotesque-MediumItalic.woff create mode 100644 explorer-v2/src/fonts/LabGrotesque/LabGrotesque-MediumItalic.woff2 create mode 100644 explorer-v2/src/fonts/LabGrotesque/LabGrotesque-Regular.woff create mode 100644 explorer-v2/src/fonts/LabGrotesque/LabGrotesque-Regular.woff2 create mode 100644 explorer-v2/src/fonts/LabGrotesque/LabGrotesque-Thin.woff create mode 100644 explorer-v2/src/fonts/LabGrotesque/LabGrotesque-Thin.woff2 create mode 100644 explorer-v2/src/fonts/LabGrotesque/LabGrotesque-ThinItalic.woff create mode 100644 explorer-v2/src/fonts/LabGrotesque/LabGrotesque-ThinItalic.woff2 create mode 100644 explorer-v2/src/fonts/LabGrotesque/_LicenseAgreement.txt create mode 100644 explorer-v2/src/fonts/LabGrotesqueMono/LabGrotesqueMono-Bold.woff create mode 100644 explorer-v2/src/fonts/LabGrotesqueMono/LabGrotesqueMono-Bold.woff2 create mode 100644 explorer-v2/src/fonts/LabGrotesqueMono/LabGrotesqueMono-Regular.woff create mode 100644 explorer-v2/src/fonts/LabGrotesqueMono/LabGrotesqueMono-Regular.woff2 create mode 100644 explorer-v2/src/fonts/LabGrotesqueMono/_LicenseAgreement.txt create mode 100644 explorer-v2/src/fonts/index.ts create mode 100644 explorer-v2/src/hooks/useGetPendingEvents.tsx create mode 100644 explorer-v2/src/hooks/useGetWalletBalance.tsx create mode 100644 explorer-v2/src/hooks/useNymClient.ts create mode 100644 explorer-v2/src/providers/CosmosKitProvider.tsx create mode 100644 explorer-v2/src/providers/EpochProvider.tsx create mode 100644 explorer-v2/src/providers/QueryProvider.tsx create mode 100644 explorer-v2/src/providers/ThemeProvider.tsx create mode 100644 explorer-v2/src/providers/index.tsx create mode 100644 explorer-v2/src/theme/colours.ts create mode 100644 explorer-v2/src/theme/markdown-styles.module.css create mode 100644 explorer-v2/src/theme/mui-theme.d.ts create mode 100644 explorer-v2/src/theme/theme.tsx create mode 100644 explorer-v2/src/theme/typography.ts create mode 100644 explorer-v2/src/typings/FC.d.ts create mode 100644 explorer-v2/src/typings/react-world-flags.d.ts create mode 100644 explorer-v2/src/utils/countryName.ts create mode 100644 explorer-v2/src/utils/currency.ts create mode 100644 explorer-v2/src/utils/formatBigNumbers.ts create mode 100644 explorer-v2/src/utils/getIconByName.tsx create mode 100644 explorer-v2/tsconfig.json create mode 100644 lefthook.yml delete mode 100644 pnpm-lock.yaml diff --git a/README.md b/README.md index 54e8ff0734..3d2b53feea 100644 --- a/README.md +++ b/README.md @@ -67,3 +67,13 @@ As a general approach, licensing is as follows this pattern: - documentation is Apache 2.0 or CC0-1.0 Nym Node Operators and Validators Terms and Conditions can be found [here](https://nym.com/operators-validators-terms). + +## Getting Started + +```bash +yarn install +``` + +```bash +yarn build +``` diff --git a/explorer-v2/.eslintrc.json b/explorer-v2/.eslintrc.json new file mode 100644 index 0000000000..3722418549 --- /dev/null +++ b/explorer-v2/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["next/core-web-vitals", "next/typescript"] +} diff --git a/explorer-v2/.gitignore b/explorer-v2/.gitignore new file mode 100644 index 0000000000..d32cc78b89 --- /dev/null +++ b/explorer-v2/.gitignore @@ -0,0 +1,40 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/explorer-v2/README.md b/explorer-v2/README.md new file mode 100644 index 0000000000..e215bc4ccf --- /dev/null +++ b/explorer-v2/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/explorer-v2/biome.json b/explorer-v2/biome.json new file mode 100644 index 0000000000..509af0d9bd --- /dev/null +++ b/explorer-v2/biome.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "ignore": ["node_modules", ".next", "public"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noExplicitAny": "warn" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + } +} diff --git a/explorer-v2/next.config.ts b/explorer-v2/next.config.ts new file mode 100644 index 0000000000..9c0e03fea6 --- /dev/null +++ b/explorer-v2/next.config.ts @@ -0,0 +1,23 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + reactStrictMode: true, + + basePath: "/explorer", + assetPrefix: "/explorer", + trailingSlash: false, + + async redirects() { + return [ + // Change the basePath to /explorer + { + source: "/", + destination: "/explorer", + basePath: false, + permanent: true, + }, + ] + } +}; + +export default nextConfig; diff --git a/explorer-v2/package.json b/explorer-v2/package.json new file mode 100644 index 0000000000..9ec1f64cd8 --- /dev/null +++ b/explorer-v2/package.json @@ -0,0 +1,64 @@ +{ + "name": "@nymproject/explorer-v2", + "version": "0.1.0", + "scripts": { + "dev": "next dev", + "build": "next build", + "build:prod": "yarn --cwd .. build && next build", + "start": "next start", + "lint": "biome check --fix" + }, + "engines": { + "node": ">=20.0.0" + }, + "dependencies": { + "@chain-registry/types": "^0.50.36", + "@cosmos-kit/keplr-extension": "^2.14.0", + "@cosmos-kit/react": "^2.20.1", + "@emotion/cache": "^11.13.5", + "@emotion/react": "^11.13.5", + "@emotion/styled": "^11.13.5", + "@interchain-ui/react": "^1.26.1", + "@mui/icons-material": "^5.16.11", + "@mui/material": "^6.1.10", + "@mui/material-nextjs": "^6.1.9", + "@mui/x-date-pickers": "^7.23.2", + "@nivo/line": "^0.88.0", + "@nymproject/contract-clients": "^1.4.1", + "@nymproject/react": "1.0.0", + "@tanstack/react-query": "^5.64.2", + "@tanstack/react-query-devtools": "^5.64.2", + "@tanstack/react-query-next-experimental": "^5.66.0", + "@tanstack/react-table": "^8.20.6", + "@types/qs": "^6.9.18", + "@uidotdev/usehooks": "^2.4.1", + "chain-registry": "^1.69.64", + "cldr-compact-number": "^0.4.0", + "date-fns": "^4.1.0", + "i18next": "^24.2.2", + "i18next-resources-to-backend": "^1.2.1", + "isomorphic-dompurify": "^2.21.0", + "material-react-table": "^3.0.3", + "next": "^15.2.0", + "openapi-fetch": "^0.13.4", + "qrcode.react": "^4.1.0", + "qs": "^6.14.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-i18next": "^15.4.0", + "react-markdown": "^9.0.3", + "react-random-avatars": "^1.3.1", + "react-world-flags": "^1.6.0", + "zod": "^3.24.1" + }, + "devDependencies": { + "@biomejs/biome": "1.9.4", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "eslint": "^8", + "eslint-config-next": "15.0.3", + "lefthook": "^1.8.5", + "typescript": "^5" + } +} diff --git a/explorer-v2/public/file.svg b/explorer-v2/public/file.svg new file mode 100644 index 0000000000..004145cddf --- /dev/null +++ b/explorer-v2/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/explorer-v2/public/globe.svg b/explorer-v2/public/globe.svg new file mode 100644 index 0000000000..567f17b0d7 --- /dev/null +++ b/explorer-v2/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/explorer-v2/public/icons/arrow-up-right.svg b/explorer-v2/public/icons/arrow-up-right.svg new file mode 100644 index 0000000000..85c8811910 --- /dev/null +++ b/explorer-v2/public/icons/arrow-up-right.svg @@ -0,0 +1,61 @@ + + + + + + + diff --git a/explorer-v2/public/icons/arrow.svg b/explorer-v2/public/icons/arrow.svg new file mode 100644 index 0000000000..3e5d0f6f14 --- /dev/null +++ b/explorer-v2/public/icons/arrow.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/explorer-v2/public/icons/buy-card.svg b/explorer-v2/public/icons/buy-card.svg new file mode 100644 index 0000000000..b51ac05a31 --- /dev/null +++ b/explorer-v2/public/icons/buy-card.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/explorer-v2/public/icons/chevron.svg b/explorer-v2/public/icons/chevron.svg new file mode 100644 index 0000000000..d6426509f1 --- /dev/null +++ b/explorer-v2/public/icons/chevron.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/explorer-v2/public/icons/chevronMenu.svg b/explorer-v2/public/icons/chevronMenu.svg new file mode 100644 index 0000000000..640cce1853 --- /dev/null +++ b/explorer-v2/public/icons/chevronMenu.svg @@ -0,0 +1,14 @@ + + + diff --git a/explorer-v2/public/icons/copy-file.svg b/explorer-v2/public/icons/copy-file.svg new file mode 100644 index 0000000000..ff12690bfb --- /dev/null +++ b/explorer-v2/public/icons/copy-file.svg @@ -0,0 +1,35 @@ + + + + + diff --git a/explorer-v2/public/icons/cross.svg b/explorer-v2/public/icons/cross.svg new file mode 100644 index 0000000000..72732bb0b5 --- /dev/null +++ b/explorer-v2/public/icons/cross.svg @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + diff --git a/explorer-v2/public/icons/discord.svg b/explorer-v2/public/icons/discord.svg new file mode 100644 index 0000000000..ed48b19966 --- /dev/null +++ b/explorer-v2/public/icons/discord.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer-v2/public/icons/document.svg b/explorer-v2/public/icons/document.svg new file mode 100644 index 0000000000..ba6c3209a4 --- /dev/null +++ b/explorer-v2/public/icons/document.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/explorer-v2/public/icons/download.svg b/explorer-v2/public/icons/download.svg new file mode 100644 index 0000000000..88b6b35201 --- /dev/null +++ b/explorer-v2/public/icons/download.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/explorer-v2/public/icons/elips.svg b/explorer-v2/public/icons/elips.svg new file mode 100644 index 0000000000..7f95c1055a --- /dev/null +++ b/explorer-v2/public/icons/elips.svg @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/explorer-v2/public/icons/explorer-card.svg b/explorer-v2/public/icons/explorer-card.svg new file mode 100644 index 0000000000..d146f4c382 --- /dev/null +++ b/explorer-v2/public/icons/explorer-card.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer-v2/public/icons/gateway.svg b/explorer-v2/public/icons/gateway.svg new file mode 100644 index 0000000000..50673da861 --- /dev/null +++ b/explorer-v2/public/icons/gateway.svg @@ -0,0 +1,79 @@ + + + + + + + + + diff --git a/explorer-v2/public/icons/github.svg b/explorer-v2/public/icons/github.svg new file mode 100644 index 0000000000..583e772c18 --- /dev/null +++ b/explorer-v2/public/icons/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer-v2/public/icons/nym-logo.svg b/explorer-v2/public/icons/nym-logo.svg new file mode 100644 index 0000000000..d6c718150a --- /dev/null +++ b/explorer-v2/public/icons/nym-logo.svg @@ -0,0 +1,14 @@ + + + + + diff --git a/explorer-v2/public/icons/operate-card.svg b/explorer-v2/public/icons/operate-card.svg new file mode 100644 index 0000000000..ccce4549ae --- /dev/null +++ b/explorer-v2/public/icons/operate-card.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer-v2/public/icons/stake-card.svg b/explorer-v2/public/icons/stake-card.svg new file mode 100644 index 0000000000..2b7c24e2a4 --- /dev/null +++ b/explorer-v2/public/icons/stake-card.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer-v2/public/icons/telegram.svg b/explorer-v2/public/icons/telegram.svg new file mode 100644 index 0000000000..ab1bd1e5fd --- /dev/null +++ b/explorer-v2/public/icons/telegram.svg @@ -0,0 +1,3 @@ + + + diff --git a/explorer-v2/public/icons/token.svg b/explorer-v2/public/icons/token.svg new file mode 100644 index 0000000000..6b56daa618 --- /dev/null +++ b/explorer-v2/public/icons/token.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + diff --git a/explorer-v2/public/icons/twitter.svg b/explorer-v2/public/icons/twitter.svg new file mode 100644 index 0000000000..2fa76f9dd9 --- /dev/null +++ b/explorer-v2/public/icons/twitter.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer-v2/public/icons/youtube.svg b/explorer-v2/public/icons/youtube.svg new file mode 100644 index 0000000000..3899653f45 --- /dev/null +++ b/explorer-v2/public/icons/youtube.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/explorer-v2/public/icons/youtubeInverted.svg b/explorer-v2/public/icons/youtubeInverted.svg new file mode 100644 index 0000000000..d89e357463 --- /dev/null +++ b/explorer-v2/public/icons/youtubeInverted.svg @@ -0,0 +1,4 @@ + + + + diff --git a/explorer-v2/public/images/Network.webp b/explorer-v2/public/images/Network.webp new file mode 100644 index 0000000000000000000000000000000000000000..f8eec4111186aacbac3c53c243eed393f816536a GIT binary patch literal 98936 zcmeFYWmFwcvp2eN_u#I7Na1w&MOK`U!!QBZ2cXxNU1b5kYlmB_n zIqSU7`{|x_Kiy%?o|@_Ep6=@Es$W$Ps`4^2_I@w`&9{;tG(HGup#lH^9_Z;q04PKU z$V*9mAcBS70$_;@?d`4Mp_Gm7XGc|8Nh&RE9V(<<01Wi+?_p@{WH0gIgW}80zpnq3 z{Ok96_8;CUwwJb!pK`wm1j=^V)c$(!wn)VBFRuT~71hMl$rxJp2t7HC?HxY@05Ggj z`oq`H_Aj&tlz!z1?G%)bexc3(q4Qp7qkrhezdY)yQc#{@C{1N-VQ2!SkD)ZH(SLhx z{@-XD>n|_weK}r6if(EPQiEP!K~FM(G{6$z1h5BK0}KJK04l%-0Qdie|3#=|04abS zl-m{H2(4!humF69){%fdd1=8EO3MRm0mcA!04tP!1K@<7Z(igKtqVP_XZ}NetpD}Q ze+>W-ZG%7{&HwdGp$!12;s*dwoBrz=qW}Pag9ZTfH`yCH8vbn#4*CmgW(EM97XSbl z+5iCFIJAzglNtA$zvVFKk^lh0Dg<)D001Dx0RT_Y5XeIg1oD&%0KhE+0PVI=jZyrg z3CHdFK&@mmH%KrK_mX0YdtqAa*;%=47y4(Dbq}LF~Eq43%u4AD$Rx+x; zE|Br6+cOfR-dcjh%>eqw)Zu{{QBC*O&rv-oHfx%T;OgT7+qvgEl^uk)Z8Ts+rZgr5 zGT!ap+*m2C@Ys6wP%}eoSiPbZvYfs&gg74K$plAE#qT9X>;pN;B1? z;vZydUY5}PQ}>k`|M?qj}tG`)E{HCGh%ho1f*rrxHBekkQb z_(wDC2!bIav%iP-uzg{6_wewr>TcWfmFLNg7l)}OP(L-NlsH5-N3MYyfmWQu33bLI zUy^AR?t}EgCqT|6!jfenz9=l$-MeIEZ3y;H^Y8gKyg!QYm#htrnKtMBY%CE?Ghjl# z=}uusN&X45DWB&SFvG}E(k9+9TWl7sGWileQ>BNIYltigf4*28Qmfo8<|J4}Ti?9$ zN)yXxN&v^Sj!BM-I}eOV95d?JBa~Q3!)%1&nOMAh&EuaQaxNXugjQ~{lwWowbpv-j zGA4HRc5r;*0*!d7-X@Kjd&$e)hK;eo=2Ox2V%w^-(lX+29yz1fJUYweeEGGTPqFQj z-dMFZb1`)ClV;mvgnx4KVqozIe}v#tA<*Q=K6}q3vYN|@`dts;asTUZ;?{wROk>sQ zTL0+%r<077UoFOAZwgLJDf+{=#7KO#&Z~pIv&OgzcTZ+WH2vy{)oxus5$Puh5FM2vqRX{mlgQ%2~3AXU^K!ZM3<6y^jw@&U(m1~7?VQ?629NRxVvEbS~~ z(oNvsr<$m%KlZ(ss;y|adL0=678q<`!0H#Hvef)CEtrsT0jjrw3h9%mqvI#{XpgZQ z$k<(~G;UUCUCKve47^X=F!Hg{T8C{&i}gqKbv7kePix&YLjjTajGUq}uaQ+!i*4Nb z4s5YmvT1}An5uu8*Kp~(JeGBFNT*ve`C zoN~$OuNaR(JR*F4n}z`5a9Oj>8SX9YL>WUeij=Sj2?3Fr+KFkMv|z4nX(?opQ9~re zUPB^bfF+#*@~c`K+CX4LObb7oME!`HcdgXb8llP37u}_L5Ls0#34si&l%v5bgd7dO z$o2Q~cTAR-ZDddJS+VzOlTYyh&q=vi=W3Hv-uu8y%6qJ*;AbgFHe~$&zy1GA8mE;1 zD?(of{_oG8FXVqkE8rj#|1b1uxZ}bdsd?u{fvN>@V-@YrrSo)61;rb#QkMYPo3BQ9 z0I2xy6*g-@c=kwn!4U+OgQOJ49dYe{|96Uw#I8J~+(ZT=0r97H&;NHEc&`$VZ2#>$ z;IVRWS4#R!8w3Vbn_W3?;keNL&&hPUM0M{!=K$uQ=8kG4q;R6FFU!vG|DSwa!6~ez zI&c#HC4R(Y1(46cIKmU_e>16IC;WKR?+Ve?s2UaJZP+o>zkQx_=7K^|5}7}0dA)&& zc%E92lj&gK0Mq&Wv#wi*B>!VkpHe;|L3*~ZcnIFQQV^{WoW6cj{F9usue*cIU;J?t zA&wMW{Pv#3s&yfdlj5nsi#555yd+f-c0+3=4mbE%`g@ zArzd&DdFcOGz8Z@mBcg`);|n6_rrR)Da(s7Xnl7rh>bc^$Hm`hBlvz^(+fxhv>FxX z8R-zjOaA9j6*pXRAY?FALS{dp)4_;M`eZ&hV!Fh}yx?&2r68TB)bD4RU-j{M0atU5 z7Db4z>-`6MWbfMQ-|;tktFsyB)nCaxDqEB(1j?F!5X0RaDZ7$;2a)7cdj$)v3^B_O zdu_9q_y~Ng(3D7l6{70&{vS(Sc28u|`brJX`yvRJ`+3C35 z9iH!gtO`!^@9^Ou@ZN>RVY}sC;))8$<7n-y=x$zZWJ%A*U;du@qaN~jK z?zPoZ06{2|gF-$gjz!?f@OOa3rr=(gCs3i|GvmD0804fQP?Zmikuz#C-n$AbRv<(n^5w$iIe2wltsBD+bAmuP zR>?=)5jK;28XZ&3h=SL>HF|!d>LJxv9S^TVDA`~pYgX9J zzl^c}5rQkx0UbFz*SOtIdH258-v`QymqPV!ES>eeb+Yltmlc2ndk3BVR*4*A_*_SX z2!)%$D62Qd@BFrra&Rrq>zM|Xa^T>a<-AwurB8!xZuAzSBf z5*tM+(QwKb&+&J>_twTYRYQp=GBgSheK%{BNwYC{ChNwrMcFT8`b*3@;@tXL;R}c^ zv40*0&PppHud~!^Hy_o6NwoE@YJoAM*O&(_fwans3OOP{sL!}Khy*#uMq#yCkyk<4T4pUN{ z$YVqs-(s-KZvVFLQ951Q2sqjZqkjCV-W~evguQ;y-j@Qsf(82>V4sCyjfkIfA;UD; z6TRmV19lZd?6C5za7JwU6jsKrdInw#&k3owtrjkxnjy;xZ|iEk z<=gkSI8g%zhN7(CD_f37D{p}b3%rwAbm-I0kg+FIfpoSTq^mpblof?v7?tn2`s@={ z^{*?Cd%eS`xXQ=XIouVWTXcu^d@aff=>pZ&0Pjvf7dtwn?UOC6-rv9WxO%R50!nBA zP8v$Qgcl#lyVRGmL+})HeJxpz5h@G4z7chSKliv0w9{Df$Y{c_Zlgz_)hCd0^;)_l zfd7ORp24kF25Fal zeKw-t9iqU37^GC0?u$WPy!8C$yXFq!1}G-dP*_sq&(ZyB&QwK!yMIV<({JR=CSZx; zj2Rbs_m+&7hG_H^Z0qYFZG>+UmcMsS9)DDW6Bju{1T|4)+?fM$CNc%Ps9#`Y&3pMKoC20>%m~tVPoKgSfR=Vn& zMry@(v<{$=g@;sZsa|nSC#}g~-ea{d*=8)%$p0Wr`i@RtNa*~QsTKJ%`0>;>p*363 z61dt9-YkdJwhntA8|srINq-4HTqE-*EYEgr9kRJq9ed$#{^#i-`=L;1xSTJK4c z(&_~5l9^Q2oT{Z#7tbeoN^mbs4T!KiBaQo`%G#KMv1~kI}lj{ zdCkr7l$C!_=0+W$*OH zgI$$ha4v+S5jzxsBXpHg>@ z#sFX`Zh_#!sCdHY^M$QZEvxVOy7e!s6O)j1mYeIPna~Uxf9UC#?kP@LXZhLl{EpUi zCqISmisL7JgVqNs94=Rw@L&~CQ4tJfwsx3cxYg?tG0P^oADA?#0;!Br^1vWLjwRtt z115fJ|7Kqm;`b)XEN7g>fKs2a{Y#B%sX`#mBc^7hIcI)s6j)O!0>~BcyO@#X)}PGy z2{Wr5MwIN_l$T$TQFCNwFwMY7#Hr%ySU2kXYFGDC6&{dp)9QIiB!U7j4G3JH8Ixgq zliAgfE^Q#tZ~(k>v?WZyLFNXmPghuukvMh`e8~E@hYwGnw@!;5@xz5~*tj^a zi>ldX%a6kuN##>vnNV4g8L+t5I3PF#Gn_`VWuw&Mj)u_QGHo+AN%N7ZK4` zcg35^rcXk><&kr+nRNn1NwwV*z!_(PsdGO{)a2AWMm&+iqqdYep|w5Z1Fi4@sIv!; zy`ia|@Mie3eDK#BWi7z2zEB?zfJpAMUF<-2YOB5sM@@C{^5s@OQ0=?Rdu9C~h!owy zKE1*X$8q)fRk@*BpWa(hn?pO?E-*nVN9YJ=2S~;8)r5)hd~tQKH15#FsP{;-bYDO0 zhK=}57!^vFbgChzqmg4rRB}Z*&jINr;TOYX+Fdg5_sU#+Lms%=?m?FNn7It+!Uvjsb2q8Bqkm#%3C7r3oGi^Si9n zsR)ydFe4BB@|)MEf+6~90~mN;6ZR*hRh};O3gC%)%ZsyG)0i{3YZClz3qPyrbukZbDK7g}qv`E6`<^IG{t&S`1=uz^($DL8bwS~y)jE&iWHq9b{)1n` zfMIpq+cGf1#%b-Q`vcQ(qhMNag;#ME?n}NqG(iN{xZ(5?_+Xy0uxNYnt=2Aut^Z~t&QpO%X zg;q2dotq?nMGqBqM@=3p*n% zimOAqyK3_TT@&G-68j!3QpJSdy>rjUO7%{!b8njUT}e(=sFokM$N{5onJ+N%P4+r+ z0khuvC5mv4%@MK1+LwSegep~hdJVjNf2em<1~pApQnpF4w0mVFbLp12;M+2*X)hh; z4!$M&NPn8c%e%da$T9`xpPZ1*a zn$yJVW0q3KLH0i73UB0lhETH)#i&6+f4*sxf7|@V(Q;I?6W6!&qyB(eq%ENX7DCRd zoi@BU5V7ih*Enr=?=s=?OBQmv<{Qcn^d_oUAmgA{B&EGQ`QCjs-T`in=Tmxpx;BAc z6ZzxPQp5*>sVzc@ovfHtoqvb^Ec~&G9Np!;+Rd5jb*-=;q?dD*OK`DnH4*78F5>)7 zwprj#F1t#j$ppKrPU4Q`hma7|m0AVgQV9cZ80tZF4vCm)C6F$3)ljuysNc1 zL{)@bVoXtgB&$!`(&oE8p`&|mKb>qqJH)~w8S%9Xpd2O>;m+INqZgPUSaOKjpwCM` zBiytgCZW+L&0B`m$SQLB@S30`5$4$3W+x*f2hF({%RAU^UHj)kbDg1GEFdEh-StFS z%;V`&P+Ny4T}-t&qr-cOsyt&rFKb}nUH6m;JcK;$^nNrcf&Sj|Cklkzxe;nHloAIO zFJKDU((l*D&q|!{G-Ok@6*D-x?+Z8qg00+DGMF^|oaz&*|z{exF_W z{+cQAClnCiBvQxwzFHi6^e6G5r8$8vU%_Z$<^H6)?9}f1g5b|u+ySn?rB_gCD3Od) zuD&||41;w79T+AtlOx5D^Wy4lox;Zhg7XSw8Yx@Mn#ZB5W%UES80soMkJfrcX*rKT zJz?%_@^T#0p6_>93+?&1FiaS)99oKpEq7+I%1tIQ=RTm>d`n7JB(2zTtr@Ei4WDUN zAt>49Rd;_=AB8aKt-3z_3h{zoY|7mDK(@nMmN8VeC*Mej6LcOl@c z`_AHs?*!O~@>sYvsF37#0^E9&#Nb4p(fC_zCqMuCnVZ>ZGsEKpgDHX+9l+j7=8C3p)t_Ud%R-I8rDp-Q8}c^|kJIuBt*+9}(mdz@P!8c27! zj*zxNPF@-GQa`ILaYs=5QDEvVD6eiHhdupUHu6>_vOU>=LBdodL6D2Ut($ol8$@c9 z9B0N2)b^cNFhr133>G(wlnt5fL-RC&yt_L(%dir!PW_}%ozj5KqQsJtZ3X%iyeEo_ z&{ea>>f+H^T)$sM9xT+hl5ACXuG80|v2OkfeLFwWK?BKdHqCLoPDR@+^iR5+sgI-Uu<$p7G!5qXudux|EmL>{pP{&o`-)M7495UR) zGW;=V;c0I}Q5&=Ky{kpV_p?1BB(f~BO+O<{QfW2A@>XG>`Io2{-O1rnY zk#1-^2TeZu*m8!^d-ZGyAEr>0KwSk!1NJ;46QoR~=*G4pW065p5eDA{GVATuH(~ z!9}CYsiNT;U}7`JfYV^qtq0C7p#yc2y#W)+%1x)!MOo!)WEZmq6|^g8-Sd&l-@Ick z@nz^KGC2kd8tQ;pj?-|Q)DK?Gx|H%P*z2|Dl4SOAmbL_s=vBU*NPTlNKCS*hUl?rG z^@!=8Iyk}9BNm*SN%xh8HNjTD%ncF%sw-OzQ%{R28|bQZ6;VJ2GV3Eo%r%=>)Yv(G z)aR3tx;>%l7NV)H@zPq%Ko63-wR0S`cE00^{bS-J5CF+fOm{TlYyRC5uLxzea~p`I zqYYmPjhB$^@>X*;JECNXoy7T_)87f1H{4&6ku!6DqSEdE>Tc#(M#jU2%YsXH#-YMw z$hE1$ORPqh!=)Z3%_$FCZ!z6g8ZFrD*hz#Hp&*6Cm(e*&B*+y7ZVzMUXZ_ zZDVGbvAi}F3ysyb>pQ+p$|EBt-ZTx8z{kO3sf^X4n5gM{8#uG9E{`5_#u+55AR%7=mwnIs%tGp$mPRs{LT9gOSHttC)jm5*CiOoM zu?T?I7gWJDnpdZ5+v%DBBQ_R$u3?Hm2*ds{fXhv{F%*2w_{cB6M<8xiLwV)u!?iYG zjSTrBM8x|VtUjSLp>8P;2cpv`$HgF@{j#L4LaqWrfs^SBz;r^v^QxTfb@ZdFoHt@O zq@gZ7uZNLW+50V0f1{hOohE9PTol@YVR;KP7+{7s zo2~NWvyrRMO2|_rq*Nsf?&n#L{z?RLSuB@>eL`svJJ}*_&{lod@0GFfz+d>HX{bNN z*zKxDZiL|?d`1d(^rSq(cjG6&f^m*k!PTAS@83zEzKaq6(VTl({rhGzo~dLGdovd9Q(UM zatE)_1ww2iz!u`z0jBtZ{Xt7beS*EC-+o*Uwdq}6r$r+;uN2v6){$S9w@g)?&)xb> z@(3B6$p9Q&Xs?Uyp9qe`@_@U}_EyQeTG1R1rEP!t^$ziAQD1O)#nTN z&U+OsD}8z5GJz!$pQ8$rpOucRG9}AX(>4j{>O^;icGz8|L|cN+wYrO2Cx?q>$s67m znpk-*D^;}3QG6}N-AVid*M(xFJ1E)JLCYn+p0H9tbVGN=6Resl%5Ra++yt;+32i2M-7lBxmU7)Pq*lEd9>hntRepYjoRJ25 zNH9a6$}qoffhb`N~J-D31ovy)CS|r5$K_~Sr2|4pDE${Hw!(z~GHmVZIqzwD@Ayj}knjsdc zI017w#9Wa_MZF2eEXq|*Lp=uNOk`|xgcdIrY2P))hlhmYm*92O!-CHtz9`G7rJZJn z?$$$N?R)_fOI?@st;E0=^O^EH(Gwx}J|n{)?Y&s<3od#-8*LeHB06zlJ)*4{sbSn9 z-h9<7PsLr8!rSUOa7BkZZneeyY5NU-psZYPO6rwUnxUr?_$ij>%@qHEhoxS}UhFk> zdr-Ty_(4BDS?w2JWhO%?_Vjhl#YIRlRgXEuCpZGn2vwT8)gjMErrD*CdLzWG}^!F!ZwSt zrSW{vy1FbJg;lB@0J+C2Qw)fsICUEe9I)dEHNgY=bcW^N^1@V&UjRr9F~{Hv2?RJ# zTl8U!=`T%Uq~~b4)CcKBa^LJ3Bd@38#C~k*mDMB?E*y%gYOUd&Aad>-t^bV8v{rQ& zj-@#CsTQTAPc36!3cn(CsvqVa2P{@~wm__(^~E4@2j`*`TaAC@!*7DFA4A3UU^L6j zXalaTZ(y~%GfrpD`YEw2E$)h|__P?*=$L(hazw6>+K!(!ym-PP@dtcq!sj31SX05j zL?f651>yysUtQF@yxYPNZzLj`v=jlOMW&4xw?xiqZ%Hr4@+%yU&MMmC-|{_vRczOz z4U#Azl~&jtovq~wdG#zzvc7yxvZshnL+@bzX^~VK6+(_?|C{71G5Idt=GyEfHeyVs zALJXX0GA^P=9JIzA%meST3&w7>|eP98r>q@BcgabX2sQKrcB>T5tBn(PkX)dH728I2mN!28@d-q&avtW3CQ|zPK43 zd$8VstSn2q?*Lk~T-~kp7F*Z5uE!BH#T}c-mOmdC{M0iLXQCG*@^081Eq$q_G)g}E z3Yys<$oLwHaVTkPBb&^fx0HT$zC^CHq8aWRvBVjl8u;?ZBDB(GDVmn8BuE08~~=;Ey6*SlL-MTJ34i~XJ)fDB(2 z_x&2(t({O*2m2@&-PWa&AB5qYXuGu<#8q&TA_A4vDpw8V{UF9_Uiv`p=~{Ot%bb8Z zm;d$)XZ~-ZMXV7Y+`kfCfbyksrI?>HK5E*XYty8ZC9&*d@VJyP;XbkENKnjFpP^42 zSEA%gMl-@{t%Hn|jI(=0UTOJdJ?9oPfYVYejXSJ&cOkY}__!OqUd#utt4@5(bjeQL z8~`mIH9yFm!0Wm%=uDUgiN|Q8SMxU4r1o{^`tPu- zIj@`j?1P04ZnV*nQ(In`#!>4#_S|bJxDI)wMWK*d$BE#X>Kogh@lLZmak1%LJIU-I zk%GZksXPF!k zrY5Mv$Jt%&RI0ajgQLG_)xc&!SZMxo8*KwZgtj_cT?2!Z#V@+ca2U~&>FL}{bg1Fm z#FV0Z zgv6s9UraxyZ}K6g*xh_#`@Vja_bi6;TVeSGIw9e9IDFTvi#}nP(qmg4%|4ateQv0v+Kdms6A+x21yA}_y$+oi zRkX^^Z;GhKW}~s(>Zl$X6uukXWrHi}TuxKIdL&k($LD%His)tFpV_Y$EDxq{hre2g z_%>Qj4gMhYy6-p`ae{!<>V!%a9mC1Pre(?=gt8_bU?H;)-07V6vT*J9&amn9R&%6y zBZOcU;9LA6DHQ(vYbi81J){_D65rtuaUG=&39>NMTBKPSn*nQGOn!~>LAeCJMk@Of zJ5c?3kv05=vya~`HMVPIuNSci&l8gIS3+{QI7A^FG@ zR)%XS-Cr~&-M2%AT6J>MZshlvG)iqwRg(;9a{8^XZ74lgXza-vGaK*VCy>nWCT(M> zDZXAN6T+7k!`srRn?I24CcKKo>=Tr9;ce<23|l{=Wa)yJGSx(%`Ftvk)azv-RLK`o z+nOL`gDrL_ueKUX?6nG%fE2NDUQ12+Hc8~2uJsQ+6k>!tjY~>SFrTiWLn41#=kj4+ zOR4!aLs3=cXW%102(i=4LgAD@foU)1@3+R$|w!ePxA<in4>Wnx{QxQB8p9bOa}Y-voouY5llaOH*SBR zCj5|EAIEd(caiYq8-fWZ=pFU47-aI{R)d=PZYAWCjYU@?!kA-k%C-b|-lu}aWT=57 z`BPB?4Us21-f45yQtE)^{wkV%WgzIQgfAs@os78* zrcx~tNvqyS?O|GH+Dv-q4?S{V5z>B>3xFcH`>~FYo0w1zif8-*xD4~O%dAO5v6J~H z2iP%b{L`J{2xi@Ox)}{7UKlKScI;3LG(G^!>wfhrV3D#cU-FQ$=_d&yG_&(^pQHLh z8CrFaqf>%`GoL0uj&+@~n$4~DyAU(a*N~zaxQ6X9@h+?60UQ$srS35__UKC`*t*g6 zD((_rzJx$d$@h+)Z+Ro<@P_MecmVtryCtt`XObIJ^g7Z65ibJXTs&IgeGy9P(Gm1R zApQ9vX6!a90sl!9%V3zST&gN@>j{tND?HAUUW9-M?kAt&36Z5*n!*C}B1?w>3Q(6M z%7z{}X^A@0c&YruI zh`vrJ5IJ@AkuwJh{vjRG?|_R=-V03z9iUxIFDhOI9#%{Ao+bY#$3^@`*#F);VHyLk zhXBYuqJOF5+f`S%P^gfkMJuR+BvL(l6CVd3u_VZJ{f?5%Yya0s2(xCrG_nrj1>whG zf@o;6^ri@wADeiyAhz6q2D!Kt`O(j(ix7h|)o|EoHukXusVc&JHWO+jtyeS@8$t(t ziAN)a2KN^%iPYp#GziY&`n5$dUib8>Jx-d<;FqmnMG_&yz5~eOO}nbu`^c%W5Q{xZ zjZ5BKS=ruRW{Kt0J!I=f(8b9f0CV}jdN21<=`>UR>wA32Qrf}T_MkewD` zFHxHdQFIhsvwl#B!{G>VhHl%iv8`aOn7u;FA%|{fz8hjOtQoQRThV32<;I2%T4?e4 zrLdq@uKTl3V2F>oehV`q)A{R!r7Iz-DwuN*rz(dgF`Y>tz#1+3L)y{yB zVApspWbT_|dR{?8j#hv;jkda!(7GlT8Kzm`D?MSFQk^hZFh1mfr~Pf?x|s6bqSbV{zR_&BfQDIZd5%KXXK2xqgv7(k%&>g7O=Nq z=FIbjaE=0Xh5no@ToQMrNs8$!>M#$%{)@(tm1FA_hdVRJ8{>X9h8cU&vW*C*2B%_I z)M1Ox9T=J)kcgp{2p>8u8M1sTRRm=Sal(QOr$PoSH$&A4AZF5=Aue77EnCS5^1`dA z(KIQ7U}i1p=p%OkoPl{PU2?N_i~}g+a)zRdCp5y_Cp@PAdQU;`vv<={=aP&^hu8jR zlDY=zYeKy6q{eI%_Lk`CZ1BO;)tSvreuZPlq9VTysFY?8rs(%i_b!%t_y?|xJbGHxJ;SznBxpO7qJEsg)RM0X{W*uKpw8`rNuxl* z!TDh6I>FcA_G6)V2cb;|)k`nM6~?Rl8+9^^br7v&^y_U(kk~XPhVkzBD*KvX?VOwA zN85VOz<8H$HlDLIsv_cf-wGS2IG+k&SZQ(kQfnBkKM`)iNh?57QXM0WDj?jos6BtB zAZJM&A)wG)k2vgSAqx>yulg)#p5~7eti6wk&Gcl>tZ)MErfzA!u#S7?#`qyfp zoAqJ_h#^gp5|`*QWV--8?BDyqfw-~Xe9tyFyQ?ni?3cHQtt(uJYc1Qxc5>f1)`gEf zW{`dVcySn<*J^*Tt;WCIFifB@ft{ytYE>w<`RwiM5wtvh&?f(Qb<)!rG{x)dH$k(+s%&4{~Md4a?aOi=bFB# zzMB)Qmhuo%v|K9@)G-a;%w`8}PRu3m*O3H$2G+W=q8k6|GrBT=P|86S=}v>eb<7{KD?-Oj}{NBG0i2EgCq6td}2y^ArcWq>;ht*x6&#mA;5@RZx>QRDjVEdRFB zN>Hg;SNtqgVRd1U2J5vHsz|;a?$aLCfSUQZFp)#D@6d}-1pyA%eT&4G z>4D?U>z6Mxl|(McB#~iV;6GM2N_?P z9K2wgsuM+ul{F!M-ZF4)T3%yCmpW-FF#|VrT~IK%cYzPhW|E5!^Cx$^>>1~(z70iEn{b&4YblI$<-j)w-*C?wa#DAf)t2+nV_yFOe z>UL6TjSkIV!_j4cU_V1Ok{`X-WqoA%F$g2mPpFvTDG%q_b8x#l68KFNV0 z3M#gs<;7z!i;11|R{6`U0je@{>RKhsy#8U8q>b}3wYLs8Gp`x*FIe*{b!-pSyOn9J?TV`VI4m2fShun)xl?)N8H zpXc@CU##@7f8b@|RGs{S98oA)MzrVS{sB?x>uXQrh^9O{hX+7BQFdM+Yc5Ir!rwZB z-^CEjSO-u4s-e8P10s=D{K1Ru=D|OGC7?2()Q;SFT58(K=)qvlVo6hy&q|VcZE}{x z6VtxR(}G2W)u}{34v)7^kP(7M8^{@J6|M@aSe;_ro~J;hr5rJezDuJl5^NeU#>A;s zY&LFJ30XI&0i>c%F`huDoUlr&!y?Gi9qPmO8C8ED+2W=eQ%6O`Vn+ckp5wYHaM8#u z?dcBn$yeTK>`mfNsi&pYz)lqxvuSAy_Pfvb^Xh>bqL|YRE9=L#L6DE6@Q5{yoew&Y z-!ew4jQ&uUk4A}S*;3b;OIkHZF`&yhC+p)1L9xaPjsD6vswypz4$F;6^;5M5ijaO$ z0nxHW#hqcOBq4&)XX+aLW0DqF(W5U&Kq8_po9D^CmKFeTM1Fx9Sy1)P#aLf+!!iK=huXZ6bMYjH+NyVeDy16kg=y{?+8Z6hzXx zH1mPyj3Vq+2uopCUAM73h(7pQtCDYtN~hi5bvdOu=%Bw3#2wBL$14Bu>81sjk44KR z%MqP7i@rTx3swO=9&xT=WQq4 zM#Y4WxsH|}LPo1R_HPD|I4*LWqcotmuIStnyZsY7@w|)7!%f1v8~|&B{-`gQNz*lzUaF&ula?#R zN-Gl$!3Kh~Ww5rnjkp1iJXZ;0el;gv= z`ZR8v0|i!>S)+SGu&OQ4GBXGhV#+i3Ku*I$b{X~6jzZ=$H+fFG579nH>ct?#(`(SO z1zMA3K<2UtQdE~taTfwBlL=mYGjl2Am=^+Qn`iE3TWL@dd`V|VQz4gXifefy*E&@q&!pF_u{yTX;fJ0omy~vS zNrB_#u+1B9ze5ziGF~$+3~1V$mt)?zGwhTXdJ~tSi2XL-#Za=+Fgre`bDJE7o|8RX^|D*MEW>Pq3=p+#VEldkOg&}T3o=3~U=h0~`H5=I1 z75M6Sa>SgOP&+DHSx8z3vYE1<8SZlFUcpjo8RqD?J z5DNH2?*74tA*(gmi!N`J*XZuY0)iv{1UoYf!_YkCid|-L1=yt1{QAwZ8UP&=WO1m$ z8{_GE2(Gv+)4|edEjk&>5;#VoYGaTE(9r|1+BZx<96)+ z4-5<|r4==P`#D3h+|?LdG8$NgZQJ1W7!Freu}$fMZfp)9vE@&2VhIc@Q>KGj4z0>C zj1csFhr4!H#Ask9(K`J0%^Z|^)h5YoBNZ1uCt6HUPP3&Qn34Ue7ni)Qt;}SG{llwe(k%>6N(!&CvYl2uw92Hkd#0`=4BDwnQ0V# z030z9WsE(?Se(rse~Cj5-iuxHV+cHsQv^bD;`i_Es=sAjLJ)0^R0&c<6d=R{^TVFg zP!q>)8vCM;D5&w!);$OJVZ{TU@lZ~g(T?+NIXI3=1WXt!GGhuAE9d%r2h2Dk4fU(O z;6eZC0^O?Xqroe@(sPG)H(JjYx$}CZ_bWOCLAmbs;WB!ulTd5vLbRxa>4i{>O>6{p zPLqL80i)tPZN~bVO{H{ix_8GT1u&ANqm9i%#uhiU(FBGoIg)9q1!N*DxNTI@Zj>5M z$Ts*bmJWIEe!aHlcCCebK>4+0|CApKP=*x2T)s-K5@CX<2e9lE(;kVF@>(YI3 zatvs%G6To|AP#O%ND-WLhue@LHHK|kx zq(bpm9}Lz@WS73FvOoxa$kxMz3i<%p;d)iDyUlA6X6*}N&{9%O7y|fZwJPI=GxN8X z-nsK7wd~+4vX_U=q;@3OM}MZWVnb`={&37OBj56|kLS>Truvmo86$l&W$I}itU+_3 z1X@u#@2~2%QNB8@5s(d6qw7c)r{%D@N973Tm$<@@Rg0%7-{9?mS#@1eaYm#1N~C5h z_#ji+Twd;yxo$&dtbD&f3WfffG-qnjr5?>tB25z*6iU4$ zN}1ME&0=FBAFQqa!{kQlf$aG&Qo?x!1)iE4 zQtlZOe*<6&##yLgH+Oy!0v=M>7vN_qpT8-rJYQ94l~?wNG3VBc{#{NTEm_L|CjrZK z@|2EZ1#Cmk^&dZ6W=hSv`BjuAzm%Xbr0}-4TPyfX)RqUG!Jyo|8=8_iyVo3GYO4Vi z;L|&Gh{R0jV;SV@T}Kt^rTG&$YII?ZQB6vgG=&7~(q{?wW2K_M$;^FiTvy|m3~TCT zA{>KdlC@u2+e5K_Vx^|9UoVr8Z2qnnT+6T2x^|AWErzYy`vi*WqKOy#+*W z2tCVwZk<|vi9Y-LYXpbwY`xs6KT`nwp)wI2O(aa{S{}dE+PicHl3}n;zM%Xau=}S9 z5GcvlRnMk zPU|^eZ7Ctm!YUUS`EV#uXN`m`@%5H`n)1HWMrI57#>kK&V%6Oz@}=#p5I9?dEyPJ= z3f%;oBeW(Dx?&A*CTu@QgMNVN91UG#hXc#dt%wsJq?ZEH#+MDYNVnxY;eBmna@J^Q z9`11trG-DmbVUM+NP{<^!Okgg7HMsEjT3r*g6icXw*mabd5EnJ@>6&0NIxSV(X0a3 z4#EQuN$KP++ld_1l=$&yEKzWGp<~_NEMA6nf=ur{Q3lF{Z{o38xx5a3{5oU^?FcRk zvoQQsHBg&3xKXD9wPo^4QwaCS>at5wJybBdAP)n?} zOW7Mx?{%MzR+(Tl9-l5$B53hoz3vh&q0J?{BMB4ypnJn**Rel+?C9A}4a$;#Dr6Y0N+Dw#3ECg(X(-nCvNf`J zEj9E5EnPe*9Shr1d8DNGmp^Ub$Ubn`nt!Pk_x19MWN)W;u%^Ni7CD&*G`@f2hgnyL z2DG{UH*ha|*pJ#aqclQ|nh2N>@>{`aXv)?F8Jhfs3XD1McQ7Uv_1}R<7d(;vKO|jc zKvhi>2Bnb>>28pcPT|tsDPFpfMnbx~8-Yt+xSpDJ3G5O z^E~G?I($U|VvB*|7X(v^8p&E$;ZV~ocwAlxHw96J&mI1o?sW5k0ISz3kppO3Q};(U z(OrDn5mqy*jH7@ch0Q zu2Zg9?)4j0e~z`46EZEN0#3r?kfwlF4-1bSZl`-+U&NJocoElK%*QLcX^-tA$onN- z5>Dj_$cW4g+y`#|p1rPqoSf=Ad6{<1_}OO4g-L^0&Oq5O@cd84T< z`usaRzl$Oosxgh1fgupl} zRL*Gj=uDhGat%UG_y|~$L9C$o>*p(Sqe9fS#$1~+dvQ0o;VZbrlKBh(Dqpj~gseRO zuTMX%_ZlSoUj>&~!EfFEqCJJUn79F*b8g?dsM|Fb{q{MhVlI_Opj<-$_|eZ8IZln= zwg@s&KHEeoq{2h%o6K#ebX(JsA;mk-Vy-!|j783IKCv@(OoI1x#GDqq49&E9=MC8H ze@`Ds@V1jzS&$6jJ0DY2Q&-wtT`3f@ zoGD^ZJqTcA!u-?(c)tbz287eV2)9t#C<@|bcv#>HN$s^|`i2hJ);-Csd$WG+;77`C z{~wYIOy8H+>m~uIMUntBLtDr=1IMeeG^I+Q=-hRZ z0=P^CB4JEyo4Xe!p-EO{WaZW6yqwuk&4%Dw9+KH7fgSq+4Wsn|C`Wm zbsyc$$*8St?Ddt#NXz`k=7i(dKiTQ}7Kv=WU^GWun-1NFu1}3)y&Daa7f!WoaeRQ6 z#N^!z^MveBqV2+&vG_@4iyCwFwF2~wqc#Uz%{#77f=p3w-q4(+899BkzQf)AL0iFN zfW;ZNL^jW+z4>4_b|57%ERH{7%Oa$>!Gf{ol}@@kPlo-PyxIgfv-rmf0?^jE($6U| z6xdPEe_sDE!)@$qI)+QsQ8W8xbq3zel%g+QOGI8>CQHs2t#&|;Xp90xt`HiPD6{dN zeq!etqO(r7GrDJ(Q#0%J$JjPA4bfq?^)>e8=kFRQZvUu>g?(V1ZWD|8%o|P5u@{Rw z*OOVLH7-913BNe9Z+L0!xy9gFl{mGQfwoV01L(%LI`SY}OFA~veYfyll735yfEG`w z2^4^n9#FPYC?2L&4a6Sfv`-nB!!MWflD-sIF>c-!zfUt=dByIIF)2HhgFx=H2%HeD zF6h^1+>#6Q87a(~K*`7b+9Z*jb25R8XYy}!&uU6IpIaVrzfyDxkUVg=V0(%XYVtRL z48$uhuL9IoQ#pc&Cc`j4$7WAcq8LbU1 zV$c8&bI*ZmpJtm`f_Gcfc&WKtB<$a0K6@MoSGW>tV0+4mK1TH=yDwUINGDO3V>AJgT4saWQo8VCS(#rykI%aoFI}giFlYc1 zoEl%N!Q3bYxC;VmCFqNlkYC6kPW9;j5NWCj;j9C?uQBJWRl>8w#n+$na3$nYDkpKZ z;SPGXl@ZtW;#ub@zW{o6 zm%&s!&3vJk&Qw=2;}22>f--#`qJ%+ffP7wQ?$&XkSQ6kBx&GNIP5jhH!C^RMxeTmF zXho&fW3zc3EDNSi?Wh>b-#ll$ma?D~yZAjLwT4CBZ%O)UKEp3@0zKdFl5kp82Ynuk zQWu_AToY@8MRL)aJGBn*Fo%8dLNy;;EI*IuF6&Az?&IWt{l#aL*&EMulvR>o?Gr&^knII)(CSNRG|CR9_sfw`pyT?eGu}*vkHF#~fW)xW zi~{7P39rBWJ$iQhl9RkLDAysg=lCazpdv2C zs>U`~fxs4WM^@)~cCcYP;nw`cJHeu<c-pu&+Yd-)>U&*+9nOxKl&(a9ogW&hE$Kmtc z0#3)t2-Sd18^C>q|5nu6tx|qqChm5O`vcH_6#ON<6u~ZTAxmleb?PF$=ZonDU&<8? z#H{IxIvj_ucSINg>O|}@Cuxny_>mLTwutq^2d8Zip?9^+F#=J5julgIf2T>!i7N~4 z3d^@^;LhXjA^T^tJDaG~+<@PH=Vke9 z>RY10UrIdUr-Q)-u>#GqK)wxo^fc$aP>8ntkSHQ8G!d1uqQ3n4FZN{n@Nfi1$zDSv6)Q;E>%Uuw z)`XpA!)+3+|Ay#l5wub;o57i#%7l=rha(~*9K2EaO2kAjx)_t0m3H_6=k!h^{cg|57&rp;OFy-2>t(OBV*5w<3S+pia zqA&!|VMpbF9gP5N2l>yVJPuswq?Y7{&B1c4FORplv)8W8E72V?OWrMa7W8G9>BlS} zrZ(P?@@tO&y~_urH;?}#uYr|`dc}d!p5t_K#u;%LHgVQeUFWcd5S=ddqYtmK3bQZP z_x!?VWW?({#}UelH*x2lhP+pSNgS4c|IJ!SWjT~Z`%LwGJGzQHb;Q#nyq~INkMT)6 z_GT01zT#D(4he}Zcqt)A$O-JS)Fd6oL5ardaSf$331~gt*A}kesr+eg!h;$m)}Bqd zEPR%Br*OsLmDN%*7P_yD);fX^PLQ@S&ouFw&8P~*` z-_88|9KmhPEH-b1o7wyyQGu=L0=YN2`3|9rWOYrm8F>|(PG*1qFd5v|RGCK5J*-XY-a>j<1rGCwwAMu#a05vK@P(EKB{}HEn9y zz#u)!0}CrJEcKO+9LF(~j?FwzG$4LrWzpfY(`Zn7PjAlgM^s`(2pAVDluJ`wcB
=ShtX|{ zyf;&p{H7BsOSyTLET>z)dM(Ed{j>%t?*s3}&?C`ws0TVsMc27X8C}ISScQ*zEMl3= z3HkW!I{T*@#V=DH&!EAOYXW8&f}``eR^gFnGHLW@ZA=wDJItDIkKyaf6oeqbKd8&| zQOpDP9cI6*J2TyS{F_P+4ar7%_+d|wY+kofKWkPb{EExC*1S{NChiAaR=LH>NaM^B z_@?m7@<~&^I)lF%MH@7u?YeA5j zlx9LIOI>#%ur$0&kY0tOjJ5!d+xLw#d6 z27s-v#;Z4R+^HPq`?JcWQm8#1j>C0mZ}b==1&#rudBF&$V(Vq|I`5S+IxywEv-~Zy zQ}+qix!zWZ9qfs-*@gD%n_|U&L{&Y>~=&l(rjtc&Trzd===wT(*Wxrs!Q2We#ymvxY^Hs}GG- znX66Pj)ArJp2f2;X$DrC1F47E`iVdFDGD3<-Y143dGa>>fX=_Ou5kTTEKd>DU89`z zE@crKS;`B5GLCR;E6;E64)w|m4G~XoED5W1YGjhNMZ^;a!Wr-KWXs1W?SrnJNAXjVhn5M;xnt>rvF%RDRiw6!V7!|yrU~NHN z3Hoi61_^Lx?{EDit5gjd-8TT&U?+fIFAtyyk_?Y$bU*z---4l%^vkEoR>U5T)R;T4wXu z({LTSUA@-1Mjdii_tS&yka?{ob!MGaZAoR3oaCd{5ZAN%eETKD%!aOMEZ?vkO<@I}i0N~$2nb`2V!0sU5Tj#Q&y7@yRUC1|iv#V8&DMhvAH2j*b zd{K#~KHacP|H}*7n=mTWSnL~3$4mv1jVVrMS3LD^jmqo*koGNy zInf=c-0=ofQ9ulv85-Ox@+}@yFw(97!fpbIr3o$#86*=ms&S$`M#JXCzM+2<#!p@V z{k>8-k|RL2D3xxSN5gno7f|~Zz!!OVOOJ~`-3WyEQTcULXnlUCwiUc6Fi8sdFdQ(S zY)^H=HsIea8W*B3iXy`)NJs*5BM$2E%BnDhgVeTqOd7)S1b ztz%4T-B;YUbgt8_ToH{%aYr!A=C271E6r$UJ2mdPXy@Fq%iejV+5J5KIh09)Gqzad zBZtL&i?OVeE?_1tY;D!1;rjHlO~uAkCU~r2RYlv`r!4F0RnH#(M{2O83wMJ7`I~4X z`&#W3a2eo+0b{T%3fHoOMRA1HW!i#GYBhA1|HDlGL_N3yTFy{2Lln0D)2gxH{1T^n zP0LZFeFH+rv#T^hJ|WXNjC#TTlD@qPU49PQ@SD#l8))|bKDh}rv-Z+V?N3bR*~iHd zI#RoN37@6B@`yAAauLyRbsMn9EtqM^KP_Uv4C?yH%RS@*D zKw0r|VUo0iucutjbTca!vi|e0gS+Vq_5naHYTjP7Y$ThX4AFUYiQsUg^sKk0v6Rf7 z#UR0TH=cC`6xu~U+epbrs|p_Sv$m-P=3Ei`F#pG8^|zk@a(w2d&;PfEku|Z1m4`OT z{^GJ7_;9NTzhXKo*cdvkYcIPn!Jg5r6^K*@jXb&7@!Oua(Ba^t+z_FfT zN>4aB-_3e`dkeg1t0GH*{Jg914MH%vh*hOrQumobf_bX*^7L#e?ZuijisvW)buVI_ zC`0RP86<2VN2lUz(-K9u8nukUQ{lENB4E(8;GBcl?BRTQ_z$@C-ga(5a9w+xsN{SLNtms`LJ z);pEjIvoayI-Q$??4A3eXlHlp`{LI?q}dsXaOD9$JrXBS=w-SI5FSAz_Tr6#OM_^c z2m_IFFPqtJFf=sbB%CjtwmG@6hLf@9CCXnMTpJ2RHg&9c>OKGs(`diA+}~86sr6ux zHDXn^F%)eUb=XVTUpiP34QC1K)yYD8bE$y1 zG06;0g9zRI#W<9MmS-WbSwgFNm3sx|Q%LYS`5&s%HHOX5b(`JfaE5XWcEaQ2xmLvE zdS2z?0(ZPxvE>!OgD7qjZZSDTD^&gBT|fulaVf<&M)=P5jl{Ke0?uDCzuyOAT1%Q7 z39?WP3~B(G{%BF6n9xou69LSv4_c8;k;cpe^^q#5&jf(TBRtbbBT$pUAXkxMgdQO* zyx4zU{6>&;5O(b24DY8RN^~K~)U2N_4Dr4G5E==T zLBD;tv&dL?YIk)+$cy9V(4a%>sG`tzNVI(SfRL+S8Pek-p*huFgcGBY{DrbmZCr)`SjcY}> zo%OuDWGP+hjy4H5hXK)@U@e${kPIwVE?i_T?snK&Ou6BQBAT{HubuWVBT;loIXz23 z68Jb74ZF|qCSV%|m{Vuqlvzs2I!dAD#Xw*M$Zx&a>Z@+*P&we@=~Gw9UWWD|W?_lp zp)_6-&14bl+lS-2Snu;?-HfC*U`rMEBGW|a1th+1@f*WwM}E{>n&qcj?(skUUVQa(E*tXq zu0)V>e+q@t*T3TLUM@Xl1qqe2IyD>s9hu>o7T9!^*G97K_{CSaLH^Imzpw(U6Jytk zT1#8|Zce z0tRLWwfDuRGDOj2+27*ek2=ue$H^#W>NA1s=JvDrw-G>1kC zbZOb8F$z#Aru;uTe}s5kgR+ZzM+uHGPFO4Ll%$1#Z0n3hjxKcR3WGW|OxjA}hT3}t zI{4rK{#`7$Q@t~Q!Fp*PNo`S_&q;J-yP;K5UoK6OsrV2SvcN#D2TcapFwEWV^*odX zEfFac*e8`mjJs3F#uSk5L+XPL`uB6^8(WFV1yi-yd94;jV`s_P;>ztybR6qYMn`OB zN5)>s=yCmhu1g$U6EkaY**ets4PXXYav`Coc8$K$;U+g;_flTIDD~>ebW*~%HKEpo zTgKvzx1i4JNz^->AIzk@ijs}N&&%P~)aRY+(PFC^Z!N&qVCD^I@k>hv;kMLUKt{>n zx-^f%v6%FN{KNP7eT*5X#0&SN#`!y;>1|q{&Z86k#a@TjQUjOM(>=CQq0E0grxFvx zh{(suSC}($4UFCyDApvr$L`Io*n!*S!1wx4aC%}y{O;>s^_hGwZ}5xeO;LP!6mC|& zCoCY*tjo6`tz&Mi3E;CG2(d1`h=nfzNw&)Ah35GMMTLOkl+oT65Np;YShT;C;>| znBgA|;71Y=90_Gw#(K9g32nB3-rcD!$eeJ$Z2o-IMiDuWll2&*?(HE=B=lY+ycaLt zpb6zj5(sn>+R34;en^3Rh58f{W&(^tu+JI0)QF#TVi@h<+D}+E3H=J6E#$@1@FM?{ z{DZQD^=@3vvo)LP>MlXLFzmalZ?}JJPsb!7F@Oyv4wL4`{>7`nXEU3N1bA9yi64`x zjMj@-?*_0p+nSgz?$l&r?aXD$x!#`MeN_~N896X5Kil0}$ z5B);%M~O>zPV)8|q$_rbQ&>k?Zao9l#-I^}abMe-JZC~_4N+G7_PxW`{#1pk#wKBq zG&QiCWpi}M(1-^i1}HOr`wAnSt+Z%dM}X0ghyeP%&-*cRiNzUukE|dt}uvUgoYf>n{b;V zBU$Fyq)fg;*|pVcp_P#5tWZa4d_!S7ruMh`OA;z2$8G={7;qE6yL3!GqKFltoLaO{ zx$`KLC@heRt(^<4^w09VQvQe>*4J>4lo~|f^j%x!E?&g0*Yewt;Eagf(w!d{6M};{ z&vrf9G2#hpq{QvQFXSWKmWVvi9{SjS14 zx?bEdh8h!sg*Z<(8lQtMyLJhZC%W%0mS zRN$_C@DLq7IoH9t}MC8SW2hB8?YG4pW z4Bvi8`Iy(ar1k9l3_q%BRH}e3@vm^4g}~n^CPbmD1SuhrlKn27U==!okpsW|SAD0h z{pHTN9cm;^0F?Tji)1FVr}gJ<*cU#j%7}6IU=x*%FYLd^Wes(GKnDQ0OJv86)#0r* zSi)u&XM9J!Y9)l@M!j^AuJQGDa!v8|A@-V+S#_&&zAs{ddd16`wq_26K;h7DKc{|( z!{~tc^H|~H;l&Q(6m<6`!<t*B<`;OL7x<3pv@G%7L$K}v66=38nnv8 zlQtc^tl}%OzLt_(fblYMg)_6dV@qLDr~|kl9Ok3Yy`y>0KIpND#e1g&9Sw%S$3(gyLH4z=7r%`ALR)_{JJ4 zKHIe;`)rG;p-UJSZBhtOxN=IzEWiYc}sTv8^M#I)EF`I#5tkS8|e7InE9{F7a###b7m?!y3VJscui zbw90UPbj1GPSY_ePp9T|+yz-~Eas5$EV%=mRV9Hcs}yob$4K>Uw`udPmty%KE7 z6wqqWfJsNvWdE`>Xv|0yZ4!OtF!lRan`-oUY}Z1pfp3#bbEmBaOvk9fxi(mm>+u{y zff&avZWq> zc$_E;&ioV_a}>>m&goY5a3~ttQ*^i@Tk`wE=@6Q<3X8s|eDyz8FLzapSUs873d52* zoNgzq2(dyEFM$m`qJEe5TE7s;PrS!B*OnVl>Caq0&%kt>W`dYZ^cd2NU5X3UUkVA^ zYV3Rc12^L=d}NHa)ZEV7+)hdsq10)!BeQYK6X)2qaA_#sWS_Va^#u3qtJs3y&AXKy z3l4jKVD^>srCsRU3V6J_r-3=q``a@FSYVo}N1Sap`HkhC7(rBK zbmWGA#e0%VD-OZOrkZ@P$pkuUaXVp9YyyH|`{LxrF{x%BZ0gjgqMYs#A`x$e7my!I z#XReJ-!Q#+(i5{0!qLp8 zv?*y2h;Nz1;Sjl=S;)A&<*sWy3E#t+UTAEpVX9{fjvyre zH^z;UqEOAFqKg&Me?ffl9*~c=ftd5u=v=o_qa!x za-1!>0g%$FqC(Z4<&c^hyie51u?8>&?pOos7l=NyY{OhL-}e$M56Er6!+nlZ;gb!h z9Qf$Wz<$H@>(UKKiV4$*lu)z=@+8##FDt-^(9nway{Mr8i<4U48xy0U?$=CglJI_i zIpAY|E@sWltIlQdxn02;=fSrq$dN7!wwv8YN=*8&?6=NQ*1J14Qrxv>~bl zs}n*+_3wTxiO_#&u@UVfW8J>FU%mTpl^8m%oKZ86PjfMIOOO;-rjK+U5{?Tg`%iIP zr&QAxa@XIzPBn0SM+Ss!?l;NLg|ip_bZpK7W5~nRin?msU8}=7upC>~MMA2<1V|Y8 z`Er#xC0I|?b20j`k?u{8m$PtMgP4xmj4S~)rKXRs${Rm&fK#MDHmx@*YgMfvi9y!g^+Q)LwI+@-qm&nhx-9&PaM3#3(lI7rV(nUWy%Vff-o@O=j0^D@b)i|7!H z7$Xg##Dl6rp{aX|DK%A<2ZhxMw8NcZ4Pg@-X7<7nZhEFu10tU9KE83KEa{&vs?+#& zSgcQcbc^6%X!tWU1e}M;dpHYNic}u024!7)N&LNJC+*{XOrv_=3wm|}_m@)p#1qJ% zg1aaNRTq8^+Q$!^;Tr;GG&?;|vcwMG9GG@5>q1G~?~xMg{S2zP$1w8p9V#%S!$R0u zG~YYSA_266h-8AlFBc(a>wBoc4y9m+7|3B36L=ApjA>P}_D3$PQOBLhyT3K-D_V>C z_QZ}lp-R+fEt3OVL#cV`2#6i$=h6FBnQ`%C*)p;{4P09RL+yEZW!5L2zOQ7Hug%yQ zEeD?F9kG_b(mer!`O5_!05Ktl+1J^f@~-%}-SGnti@Fm4)Mqu`R~k$iKY8Y*;E|L+ z$r-V8mFZEKb_Yb4m;T)1Z+b@?hcz;QsQtn!eCgL^9JrEYiG?`)1TKlfU)6dlc;FUJ zQpxRHp{_uK^E%}bzRh9D7Q-#z>H#NcS!z$q3SSGESc8a~ggTMD*MlBbukT9D8P(As z?+s5_arkT}QnSeW{=oc>OZ|R)uR_VE`E2$q?@cNO6D+s!6(MWiHe@7^w@QlY>g(ySq)ADe16 zc@LL}q!g9}&5DuzM#Y82iNx3nM|2|6>TJQk?Ph0yI&l}}pQ_D&De>>o%N3#hL!(!r zAaM?IoPD{_E(v5WamUi3H-uYg!J{W~YoD+N#Cm@KR<1K!8cMk-f~k08L?{ho6bBPF z-ysgMl)?Z!hbq`0U*tWyoIXl2#1=edGusMm*vI%4nkrujAST2wNV-OE^hdwWZy&oRBteqUn`?-pE<+iO%q=kUG5Hs--lTmu!D^qdV7V2vx@_fAwc2z<;8V6J=uT z)%VbyWJOmnj!*Gv`lT{WkDL}Cz75$_Y)gXYx%DZlWcBPf_W(I&&fb(*St5`Dh@CEn zEDIKu!s`BooJ!+W44Fl6Qt0F4Z0J`Y3ad}(SbWN_y?OZ-XJkI;7ts7-C|$BR(7bE$ zY29n!c2)Mn-m^fUfYginbu=X0gG={NO!!05dkiwsJ*CA`f92z&=fz`iTf`MU12?~R z!DMIer=Oh~w?RQkDpv$xZu14v^6r6TtpO|VcWBz==S_;qA&--np&?1auHdO@4RFl{ zb-U>_|3SW(n3=RaYJj)f-WCU4(rRmTK>agZ;<;64ir>jX=hfA>T?GdEAc2BxN$S9z z@l{=#s2#XO?oZ#bQbobA`4<1P4ex}4M=zXyoHa7EVOmG_$Z8zvv`%@pbb~OI@{LvxjkPnN_9IAUzS$e;m&S*D5>phAPJ zezi~qYW1DbRbA64Q;9}l!kza5{d)>%vThB`bP*pjVG3&M#g4eLI~?R+TW2UA$clQ& zDuk`F%w^E7K`?w=Q1zIvv}QU(G>728@;26zHva@4GtF~xuGGoIf^bDh&>Pv9Ch8Hu zDW|=At4kP~00xwAT3l@_!l*gCqu#~%y{-&TG8{=E6mytLb+2>Fdbr3jX##J0@nLu~ z;k}Y#S*^BvzQvAtXP^SaGIv8*nw9DDeFtjP=3LB9S*fl5!qZ$i zy}~z?tOdf$80Y{OrXkFAYzd$fTaD6LJcZ7L3+0FU3 zL>q_5*WhVHBjVDx%`mXG09CZA8J!!SvXUG&cr|-Xl5zYPG6?huN16{UPkQYNjzk;= zP^VbL#Rj$oU-YjlKilk)zUc;%`d=VXJKd|g=9;&^y0_~MO#!w|@Rlbr``gb)A9ZSf zuduTmUV|#$o@b$T?PsBt8hwM;Y7Ohr4ut)lY_3$#_+m{3h7#U!+@#)O1H^&*O|`~K z3nn1Q>3 zBA}QlPhz3@kRkxYTZC=c%6^kNX>Njj&|}rHLuMyC4Ir^JehS2dLvge10X~j|RYwPz zozkysP^rdGp8p;nzoEGj;c`4o@M>;ivVuY0vp2f$U8Z9UM7O@XN!{+-?4O~U=2r3f zi(#QPw`h^!Fs`r!d|CzlU7ik+miw376&G-%3R%OezsPH)LY@x$r9$P9F`AX-w0a<< z`WCG!jwMYHXlTD5AV#Rgw-kKq3RAO7S{Bo^w=*8R1~Gnwe&&$?{Y7vC#+bWEEVWGE zQczqPRy}3T>n^i6KN12+_I;AhHcCgrBgaAG3cEsYg9k+Y=6$f-XlMx(gMwrvw1icK ztkYqA+MWB!3H*%{si9#MgUX|3%;rQSWmRh2xe0nBpWkawom?Yh}(+fLlA zEwoo*ypah}c8Ot*XgcDx6v?4XEIuOz4%EF0px0<^)I(m1eGlCutlDpUOKO&_6)Z;z zKu_P!>Ge&M_R*EcmN4?Z%NA+723Z+G8||f!sCWkX$_8u7X_5$TUDQ~sg5HMh<~NgI zX|a)t1Em-Zp?S+K1xD=41OFg)sVu(9k`>LidrxmAE3mUB+*pNQ+XLb@t3gc+rns>H z`|3a;mNMW+%h&Uk%3o@(|(!c2?wC6d|^rAxS;v z4$NOEE9(Xz;w){?bTSn-NTD+T{B4%>glAhuP)##)0Xye>&d|L66-T1MRP#+afC2`-hMSpZjqjcT<^k#<3h7i5KB z;QDwYcxJy>S^trxo{g%CO-z9;aIpfVel=jPsuWG2tR8p0&GctUzy`Cx_6Z|`MJa?EAG(_H|TlPEczfjg{+&V$pn=G=eS+xXHW9n zCl4%_W4uO(gwQx6*#?3WZu;F$ha_aCB&5VJ9p>^(m^4b2#$1nnlf_UZkRae9Z1Yqo zv(+03+a~%!i$74=eIygfl1708K#1qfwZKc61^i7hy$^T$Mx$>LOG4(tV!G(s!kD~~ z5-CKhLU>O^sZ-Q#?AnIAj#b4)*Q3bUfFwexNPzSWqH2kN%wKHjajI!EEVZrj;I7vR zM82V@x%RNh+p){-TY;6Lb+znU} zTOT)EK1Iz=aajV>r{I{{GH9=F)=DMSNFn70!0oa_rl{Q zVHz)C_(P8{NcX7UzBN#0eRKF&NI3ngvM@X2F1 zrlLCnji{SMb$QB$2e^_I^}ymLLa7?5xMS;oF`W^3Hp}qM;YKM1q^34?X9C5)uiOi1 zj^3w#%k}4_Dj_B9&K2?NEPdKU_v5K<{xZJ*aMxiYzjM%lPeJJk<2G`?SdugJ`6``& z#%uXw#Q&`7+qrLo!qcJSN!rBwcdEkrU2rpHKS{i6UL`{{j#kMH7i2Q3{Z(4+rdC?2 zV>ykU;Rp=f&luGzcDxsxutnVD&kqD^V$U>1Qa^It;yo$8#bP6^@O(rcJ-KeLNmYv$ z`|x>rQ}b3l+`#%t+w*xF9RBv9rv`ccTFe)AWn+H5#Qkme>EGY=qc1hpNLS)de>I-- z(sM(V%l%kf7FakWjMFSWhSBE3YL!hKE6J;$d}CR;xDOm^emzZZ8NF z15GN)AJxPmsh?3D{yLxKo6*}Yu<$__u|&5dQYWlr>;Y^G+>=ystn92glPDJZDu(I( zJR!LjFzb$OxcYx=3@p5VMykJaQ?wSs3c;FE@2}SWiZbtKZTdIBe#Ui(U33Sb45^KN zwg=srUS}`X)2IT)#8liZg#3`yg3>qiBMGX;xT|8u^?HPz`Xbsw6B^FlGM2KiCwJd* zcYy+ypnAD;mzsE*SY*Y&A7lte28fYgfyg1SjHx}0)$5XZITU!u-$62A412UgbZB!; zXn2}Ulx@eql8J%lmA!wWx@$K*xCM>kcNh4Z$}ohs;Xv&O4k#S8a4f^`lTHe2Pywx$ zJmh?K{dCzV0?+2nT!NrC*K#VLTmh}YqT13CS?xbFa%vz}awn90yWzzhixp5=0OBuW zu=V7=8ncoWpH+2oD`#x;wAD?6BYY%iaA_nv|FTg2UxoLJUfi0;g*7pXv~9B)+U)NH z#y^m^D;ThwQ_>s7h;RJcryT~Ie+@$4Aj2TZ`Vida~( z(>mv=0U}1I4~5NlD={|K{{0H)2sMisA~5SD>Q(6vt8L6!qq-9V>5lMh?SA2Amz%tj zx&C4thZsEIP*)oweenCc><{ELql)h)`M&g4(kno=l>4!eTz_Q9Xx@bTu0rrY5cA64uP?edbu`4 z$WwTpIj+sbJ!{@62!`N5`cHL1Ks-P?5)^`zq@)rEzmxKytJ=Bsn zxK#qXt@d%Pf4jGWhVM`MlE)_+F+fcfr8Gl)(1OR!;d$i8^$E&>e3ufZ8{Bb$wY?Yy z&0oT`-vjDD!OqOL9~~TVxyIF27vYz6QIJ&m)|75LTBtAsK_QIZW9}l9a)lNnU8eD_Qe<{9xADPyDZQY zoc49ACMUN^kbj&LJo8Vp6ede92Qw02py?M-gXkl6h^ne8s~v2yh9 z0l%xB(h9dAl&V)7PJtc44 zZS@TKw0x)?$K8AX{aj3Dk;c91=U3bsx`Wf6y^Q(> zS?B2T!%|@X>HR{07$LZVwPt)~UU@ThBm7VNqu?D8>K9B*6)*O(FIX~o^}i?v*vrd5 z1^`Wm_4egdP!WkNUD8G01hm&6gshizehWT1;44L33to~)dY^#Y1ud@*JTf4_kVdvW zVFleS`D0Av08K~#jeX}r@iivI5V=89G_mhAP%E3>e#~jC)EgzgTKk_G9jtepW$?Vg z8zz|Ldr2VJ?;9~$1&Wz@gTJ?8zRYGK7QmfnCx8T}h^H4q)5i&5H!^67`PVdu ze;<3mk<&!lHzsY z0M#1H+`FY$4r!dk@L3bh+!uo-b^}c2VcQ7^Sye2##i)kTdKEmHQ=Pr1vFm#RR+PR2 zm?rUwlX0G^EoeJ-?&`fRZaSklRt+J+@6|32I!k#^4Y<(DdQ=a%+$uK3*mtdyI^sJ` z!)p>XTINxuF zV#PSD?>)!-%u?Z^jQdI18b`W97o^;}OUuojp6aI^IMQQ%pmNpuG>mlRG&@-gK~zRk zoF?9Zzb`es^9=8Ane^}2riCkkuRDBkh7-NoRFvu5Ud3tR{US1w64P;^Q_UFUJDdL! zR|JU6ffIsLEf8Oe z?mhe#Y1pP36RuRTFMa;M!Q$#!mXL%YIcvF&x>YP?M>sF1$pm_+nY9fu2It4^5Gx{@XfYv-0kAvi5*|5l86sNKnMN)siOKAaDhDmJ zj6+UW!sp{iOsYXF_7S>P626%{bqCtr24OvATWgo|T;=S2Q9@-PRtG)&AUEhc!FlIm zIf{9Q^IXj=81+mU%AuxMA6*33>|4n6%K0Enjvbv8$`0T)3^xyPKMx2~2D)@I? z)UCm&pAUuqR0DeB1m~CZYFKR-OX4eGw>($I1);F+|eCVg1WALZL7H>(M_brz{%0 zvrmY&qbR1XYm;@j)M%mk9BLeC>K0fnvVboxfjn)f)eZ=ynbXKXZZAY=VG!p3>O3Nr zM?102NX1DyQ`NCZ0i|7Z6OpKu5(V164^Au7G2dR?*Ri!YnwsBGQyaAOw_{Q$&r83l_+}=4 zNsyIlu;kr%#gCtYb8z%JKI!Ps?vKwR+KpoD^iv8|)}qTsbShFGib-Ad6e=af7Cyp8 zoXQ2H8a1Mg;`fBX@Hsu;yI*FSK?E*kv1Z~ta|%^*qG&f5h&J82Mt3Nw)}ds!8;e-J zQHRwu(ueBb9hv0%I<{`Y%gkAK9H(^QzkF1vZ;!&SlJ$x1uS2@ANrtOPs!nJ?EcP9^ zsccUIWS0|-y2e0skjrz!HzpYq#eHfsB>X8F@)We&e3GK8w3Y)}zj^bh5ug62NUjceN4?Aq_pvK|SKA<3fcXg{s+~=| zYzbTVWo*i&d$kup`H9@Uf^2bqK1PKJvZ~&KGre$NCdr?zMVOyJU;T3nf0%PIK+KF= zbHlHBNZ=a}FkiWP+uol=GwxWLvO)BKNsr!@KSZ*gqKc^6`Y^SIrToL{9U{aQw7zZk zia%p%`yQDogdR_7H9llMefJ%TAx;`ewPi{HQoF+HphN$?Es`+6IRD5uWrtG?^g;M~ z)Z!5)zx&Ikv#;lr>Fw~0CT-`z6>BMfEM@*reT|%bsl}j*hM8j2UF4t|cHI64j3{pf zm+PH(jB1F^E_jHfGqqAN0QItO7qF~`#NVmLj7is(uGy`>e(@ns?3d8PjPlNJ0igWs za}Ic1(F;98=&2QkU7WHDYd}iS|57AEv9lEJ_XPwfIHRJ+Xh|VN?zu0(>4Rz!t?`Uc ze}5a3h(m;f(S$BIV_O{}#BPnM5RuhoyIK)&=4FcTexFNXLpMru#2cqL`beX=Ml8ef z!$6A}dn(aQ);UlO4q8N`R`Zi4k2m$18GAF)&F5|4Uy_CBenJm!She@}kEFg*!^73> zL6By+v_CMckkuEaiIh62`4eA)bz?J8M!N9$aqHOg*8UTMSA(WY$oo7i45(-F5Ii^m8!;MV`SQ4nO7_ zNOf?x@RS9>qmmwdO)q1{`FUI{3b!(9Qov-VHh7yhuQkxp)JDt(5io9*?QdQK-_c`Y zcA0P?15epuZ*lWNVPHB+9J4N@)YErc~%u`SjL|iX@E|VxVoOLSRhBsqjio0 zULK=@?AC(S5dvMdSfRfx8jjM4-LcbO-)V$eS~^HhMo<&t+pxIGoe{`!04JBWp^}`= zxRG$WVJ$8_HqM}B4vogql9n6z1B-(IRw{g#az$L~5-N@7d_LUoaclCz8iTFEC-_!lWtD(_T29blVLvHY_2oN!S+NAH1EKl3R5h z@A->cOqC;$aK^jLmw`Rb(h)BEr^GtxWBikEg&gq=_gy?Hpnc+!`Q9*hqsiuUG_Q+d z(bP5Yl0dbZ(Z5N+w#Kelr?$gcZH4`W>?&K5&oiYKUxVh07*#>29^&a|kFLqO)Ea+} z4Y&jJ_$79uJs^zw8qdVmlNq8fvMy;nDWC-g8ug8uM-qx!04o)HwA(1hxhTrG6?ty< zcGs7Fff^TXS1<^i|DiZeISL+&cRGGI<;iECazyA?g6rPP!dt4~zbSliAreaEuY4!BQKkw9>dqcntR4*e8|d$$?P7NMG}U7K4hoSsK%kqU$?g^z<1 z_8Ut~GZ%oh8QB_YJX%ScTAt#VebL85G5Uk4CREevkusv7b~8ip+{E{GlOIY9+6a;n2G$+xY?1Fcp|a z8f~vrltQ|!Cxppv<@euF$^KWln!H*-h9I)lNOsxHon~m0^d8#M>7sG0_KmZ8t|CIB z_fr}=MPqlKxj0Y36XP-_0a*!}uaE`(`Pa_56PB7yl6&YMApfOWRr--aUXZ)KurpPy ztq0jq&=F*lkr#`Av;@udgw7_uRd9pBM-aho0;>mdBUyeZ9pXj6J{nrx0>acrzy8{x z?IZtom{fPv-9-B`Zvb0{kd*-;zG|&f%>mxAWxjdct*rA^Jty5%dk(_odKdU3)O_n4 z3ONoLW{#vjD7RQioWtko0Bi1lVKWOZJg}(n`QbQe{UeK}B6axLwLhaY?e$zh%}Ym` zY$8@QZuF<3khP`s&k_9|Q_ddX#H??1(s)oy;jDHo4>P(x{4*VLi*qd**thV z?lUSvY}vZbB^;D0NF2D>H~!aGZSkBhX?1nP6#v_gwSYY${7IT-!&x=0`KJC+a- zaK)7f5MB~le2}J7TnMsFQ^GfO+O)!sL<7>28V(8&(6QrwOJtep{Cv;9fy$??r%tbp z>pDMjKr2;g(3sJaZD?>>E9cs(BLH40NBB89q~!uMU<}lOAF0qIi*#_+wvFoJa$&#f z!XRU}$)?U!HOb-sgj$WDswu56t>xrMvRhxZz}3wP+rx`is|$|aHTJ|5{7m>(Vy+>s z65>;HcjAQlR5az3J6)_6+TLLG+*Xo?a8*-SJD*mJbC$yHo167JAA6{9{>?Tk@EpL5 zojTNZl2ZpjQ2p=sYtyM<27A2ttcaH0ZXu#JWA;sv!20u?N=uw zGC+=Dn~d-*Qk`i(a6Z$?JgTy+wfvjPT8V0;>e_9ekV8wkOS5k+yGni4#JXpD<;rZM zIE#LTj5wtg;nQx&UrGS*!51bZ484e|GkbAB1a6yFph&s=2!?UoU4=K!qP-m}?ahIp zqts1C4;HB_de+y^1xAnY&jVUNZJF&t9((eQb??xS-4IE+8xz_y(P#eQaZD8$&Y zQp*L#RlLbK#FD;vPml@i<$Ga5zjLDM z)tiieSkfQyXjO(11z(t`Hm;Q0TZw=z)#b!h2yu`XKAy4u%31M7FHP4wk=_pYyjgcB zGin_z&_1ZfP*3sTzA$m;Tq{Sl5&(y1w-Z+hME6k)RdKKRw&2!woL_KCR|arjzG$CF zMx!u2{exHRZe4*C@MUwf1%?m^mPw9gCiz1C@uzE3-KihZYy*DGTw1P_5hO_Ps_owl z5SfpNHc<8xhS~hXi=s6hFrhx1R%($O-;sN`tYzsmzL0UhA~dp|zPjVlUizOvnrWHM zU|De~S3U%82d#H$26<|xKRIvzVBAzXi_g$(e~-E=@mov$bvSo_rr1{eKH2)=D#=Xb z-xT&+(dJ|yDZp$J2@FrW@_N96ipqaSmfm513fxQ?ix}E-`>hkd?;}TnvXX`Kf=P|7fT0i)*wTli_V=5;Xg6&oR zbIrl!DsTcyJ#R7+T{?I~7PG!opdWQjvC+Kw_;3RUq@qi1Ld~2mooL87tK!=+YM142 zL&%S7fntzNqti_uX|V6ye|{i9TfuW}SW(`YcxJ^I89rTz%2aXxp~P>|0+FD?L+gmZ zoswZlZ}QXcEtsM2nUBhF^bGx0uzKojKMnMBB-dZ|>Q1{#N)HgpRtb|T$mPPNzxw1l z#2jL6D>W!*{N)C0CG6QF?LilW0D8rq$1| z{+f50HqA{MuM|^>KA#ck-w1!zgkKNA3KLe)6nt@38?1FRbSydt-owz-^v-1Y{66F) z*)1)+r?@L(imy+~SdIE5h5$?KwHp-BG*6*)F_6lwAC6l-lW+j|M`ZI-M^1q5cX)#! zOAV$%N8%X!ncUex@`xi)AsBBNG}Kd(k^1r!o&K7Jki_r+RT(D}QyC9BY6u|2>EuaV z@@wWQyTxL8VM6}SKm)d77w!)_T0p?h-B6eaqIpzpNG9RRY+A$Go+M3Pu)@prYUaAb znh)-R&jG&jwD>|^=GtNz__{MvS%A!+lPS6^SblX-J$kv}#xgjU?SF>u4A)HrwlD28 zexXh)HNo$W*9|=k6=sz0=V%c@sJ$GX^f{dCz5Fb4{yq2Y7gOt&rq%i4$|zWLsxZ7@sdn9DFG>X7E5%kq zw3wpxs&y1>f+=e$5ak?KT@F7uEyJdI##*p_*%b7z!+Nl&Nf^Tx_#AUFc#!EImtge_ zc*QY=t-;l$PovET`^7{^`fWAL$Gk|G4?IXq}aJkNwdmLm#C5wlwzQKL^eu{MZn@1~HN$Ypxp72&OyRp(Gi^4jy zAv6t`TST2E)u5n*IlkG7aT?6T0Nj4di($=gUk5{P3j+E^R2CKyJ~GMUN?))v$4vkO zgR9Yw68r_^1(si&YL$PSqs0|)H-<|uPswV4XMg}<#xyVTY2%4|UgktyW9#-<+~XpS z#O?kx!Q%H+9EC_uZwDPQ+77R{^HHV)la^{DPw*)`yk0N?_@BLUc;yOT@BpagX3(P) z#2M8DG>Q0c?PMi16I_+=tzTV|;Tn}WdAhXXBIZxb}yP$(cvEKt)~0d2ITExF{KO4aaIYl|z&>lwWBOKrGU3ooy)xwPx+ zezLtz5{r)dHfge0$S7PZEMd_EGWcNEGV1ETGz4y-9-QeW<@Xz|?7)aeS?vY|tl<9l zuTK-bQ;4$d#(+%XE_riE3NQfFX1yG~eio@W7H|*rG571sTwRF4UOinKtVt0n^3svw z*XUM$Hz2r|d2?6XhgV@pXXKGMZ)BjHFFeo=5i{7|zm>0_L#q2apy}zr^`xwlQT`j| z83Ll9oxN~rgfw0*f!aohIU9W){Y|QhTU*7%nqZ<8GvAlZ?__q5^Y;JR7_I- z=%Y)wwyl)_1i9cUM{JPat0f0F@#}Yjd=6%-dUqN_-5VJS>9Jb67{l7-;gC$*I#mH{d`6zG(fB7xka5VXG8 zZWBv6!rh?W(C&?*XbYMKtAjhJgNZ9HSPEkGKL=y{L9`QBvLFGd(Uh@nX)!IxCI~X{l1Q}9 z1?bbLPti_PfgK%noRgJcy3KD1tE(|mVqJ^>{U#dkjZd>&Tn`L@rCN*-Q@`D3ct3=-(lIs}1p~0gA~)ok?=b%u zOgx>SJ<3YsKj5LsH8UKCR>q~#_`LQ{MZy|vE4EDks)6nLiH51Pd=~&@hengF;-)Rh zPTzivSRpKaTO!J&VVEh8v)FFFEtS~_)Y(Ms?ZSSk=(&1#%~jOVnjH1tbE2jW;Iu>VF|2_{>a7Yb9_wG!t>zKxhPTK)l;2vqi}P)A>|?(0 zz$$6++neV4n>o40tYbCvGMl3ACsqE#df@?G@}C6)3AoZj>{0q!S*8Z9a`w&WMwp?6 z3P-KY3qWWij>?4>MQw)VhbEKa{;0#rfeKMfC!*#Zrhi5zE`?yYSfeL}*-a!J-8Ib; zgO_7_125a@pJ6H%@6SY1^!xoi{?r6npcGzws#Xxn)t^<)XwjZFKmk-wT&J3+Kk703wAy9owV7KS%AHqS+d}scdK%h z5lt%j@vqD(0-tAE$6A%2)=|Ar%msZpqey(jfT;{Ow4JK!JuM4O@-G!!9jfBDTP*o- zjtmnUP3aRFg13#vkR#BC3{fJMRf5Hj!33PQqo;T&*`$18D(;EGh+& zW1(xO%3;8WA8o|z!T-PIsxc7*;A%M#zcz2mLnMKT{XxT4nzF8?US>!bu_odj_L&}t zV?eF}eavcnnFFl+F~jaaPIHLSXDu59h1zas_SjlH6rwr$eE#ZK70tn?r%#8W*7{;% zmAO*%Q%*RaH_;DiVU#S)GQMWiT4%ba#C41nT}CQl*>M{LV*t#;?N5fms(wvQT$y>h zBdCckA=)fOmO~}gx%>Ehv19udGD>A^Qx82R>bHsg9kBq>HG;%`jZ4yh?K3 z336vYoS?)}eem-5P<{ezRy3#Hd+6Wu(_iXHHp24EYPMRm7{NcehZB8&)`Br>{h&Lh=M%*7cBf0#DHUHebRyQ%N4E#HcC3-q#pXUEt-suZl6Fr> zlu91!?p>Oi0VQozkH+h_w`B=3!t#}Na?|!9ktF7u#TQHFmcSDH)^lFSW5rlSgrSiO%b3kIQ+!7 zs-_JU;=`$>I|zWPz9d@8)E|Qnzj=$qPbZ%dI+&8E5M0)j9S9K!H5>>Iznc#hiu#hg z&j_H;%hIv4;5Lk)%NQ^7$zsLqYRiGnXLoAA)&qg`v>4c$pz2=Zc;#U&4y+l${W<}h_ey~9@Cq-cyvmw8LtZ+TH{`pQ)LH%0*;4#BuB zv`5tR5}r_lWES~y^G@G#BsUYUTq9BOYb%do!Rb@ zCW6o1!1mNpR(f6j!c1SUZ*SUpntvO7gvTnQhTj*3MtOo4=ABzCjNvcB_FCcLQtPYX zm0I78sM#UCyyJZ|_g!>t>xxr#95R>#@I1(hiu#sHUprJD6{RFQJPF`2UYtjA&{t<~ zV7>SZr#;CTG}x1sy`xH=2?!*!}Zl7fP4-&r|O zC7peG6dU+GX$Mf_{^)zeqx!CtA-bpjkF;9y2xb&QbIkp3N{$7PvM<2t^sd!dRg49v z_)RNK2+15o!`4jYXV8Q?SePMT0H_**vao?U6{+}YVl79VbQOy5xCmlF9X}lMA`MCE z0U4r!LR>=m3?8>mfhzk{QGN9HZ1jb^z!M72({VtqbD2z=1dAVrHUm!(%mtY_Sn$>< zC|yOqONy9-Hjg84)(F;NFefERx~Q{&_g{9R`nCzLUyqKqGU>gAf`y!z18^pdHTT`Q z44!{y)F^R3A=Y9{NA-H2|0px>f~QnuTs#{E(GAO1NfnH+aIj5pYhR#u;g~o-M$zHL)L2>K;G<4=@U3=PA>ph0_s~=*PS+vh7D&!4*8koo}|fOLefW zYn+f)8yy*-+3aY-u_6$~mLtTvMjT26B5-T4lkJ)Bj$_Cp1mv)7d=rh7_2Nrm8=)*N zG-38LyA!U{Y0?jsJ#YW_XUYTAd3kV!3zzU1xgQ*y!Ro02!qJXRHgu!y| zw=4GN$joE}n2+X@&0=E)@a6qb{B%qJulgv%kqqB+_)>xRj0?LaEG)%}MnY=*@tNZxZgp!xgT3 z+EW)4cq&TN0 z3LLSqrVHFUNGJ-534?`PnZDz)>x)ktCTapgXLQGfcbP_w6t6pa({5Md!}I5MCkW zTAH1Aj+lEm4+)>YwBq5{UZQS6eD}`Bex|ZIkPA5t)$Gr8A^Ms$Q~K(RJ#V~o|M#T~ z@~h4bq~L6jb;_15AwX8NM)gt9+Lo%=EP74><@!v9{4}Q>W*g7_mr|hDkx(73$#`9* zJ$89@@+n{z%mDd6zvn;nPHUw#N8?G`#x7zq4S%baGY;p_4y5m(+Tb8Cpo$gqNRqJ;K7E>*9_=RoJt_x=(ctVQa9y!@8=*ew9rI2h|^m0pXI|0{?N2Rs8C zm$2CTYd#e@8Lhb2-Mhg*K~cbwS<3*qy!9@AS2XznWN~@P1}KhbJ$srNiEVlgjzYWs zk6k&3+NfaRuO_eNm#i<}`%$fDk20;%YTIWAHE3-9Ya2&OxZS_Vc}Z`G>+WwSNgpeO zgpRgrLpOoILdOy!eYki5&8RZ*g001b+cd}`04x5d zz0ofS-EAAuINRw*_Vqx|7n^D|;HH`Uh2@DGDXL_r5&m|V*7a${Q?E_49jP1D?2V2p z!ti%wcZSyG6U*m=+N75|q;I~3Ga zPJCr=r)x9w+l6{*YEPWydA*{&Knm4t2_Q{M0(qfqQ+_=m4xv`%0N303ChdQsuR*?v zIFSkExh(}oocpsKJ0Qj~qjVk>o0V1^UNJe#7udxx{r=pfW?K%!^SPXz8{a|%s zHo#@Ojq^-|;t`n{K4|}U`kn?E_#)guR|5Rz_1K52N541^2D@?SFxTRn6wI!CZgz1v z5@2Sm2E<2Xp>0`@wcE$M2I6tSmtHhEYR)YAwkxIm6n6*N0tmI=BW0pxs5dPs$-sSH zCO=1H2DpCJ;SMSdixc~JM|8nI|Dmb+$d&5(I?edT}rGV(-CNU->kt5xV=5}`&oZ8 z8KX$0tqvTCp>gIdZ)x|9IUe}7XQ+kv^*86VKKev-s0UDrGav-ulpA0ws4u292TXG4s`fKB>e-ueS{*6ozld=z z0yS_K`t9HwlvQ7c{83x?$e3Z;mm8WlnJcac9Ek!|aqfu=j65-Z%`-b#G1}os98MZL z{R%a>EjRDOS=O+$(5?<$7s-r8xx%BH*6LJ8{c}^&kFCkz;c@uVT}lY<^(x{5xhcz} zW@PYep%4k5gT1vB6YUs3NhRmJZ(hm*;-RNmmC>ptr+M3;y&KSmydOi;0e)0EBHe$; zIvq<)njuwm@C|KbUBXf@T~v0PJW&xbGTLu#$kJfoElwjUU)4$BH$v%1dt-W^yRcCCiW0L?xD?hxeTC5X&vT8hO%_}bNXoesGAF;(#X zrw&LsLATZ`s|Dp>c=8hVK@LD!4nwJ@NwaV5|7``(i&m#y4~vriOj@rf;z}*R?JD^n zxqE4ZfuMbmOl7ls9tB^KA9+%(>r%|5rL@2W&^@k0#m8irRXgi+mD{{A zgnTTf`qa5za>iRAuc=elVrkRU+ThbP+InO4dRXBQqmr6|a5W9{R}^}yYDbY)NqYqS zjCr{qm{wM+|6y#-*3R_WBC%6A?5%ptU5&-_LR*@}VP&0@$3W9;G?Dl4mi|O{6k5&_ zoBzj8ax%`#TPZQ)olA*vA+9<*a(>7e{}-NnsqTldH9deGbc*!P?z?O76V2lE z5}$)#&lmJD8HJ}fsDn~Z!%?)-Vm6I|zo0(Sm1G3^I}d+r z4O|{)#R3MYo*Pux#Q6c&i3Jps0EeUo6a(JX+iBGn`Wv2tf#78`Ty4jfq3(|RYgMek zJ%*-=ZJb8>2gkRdt{dkN2L%*!tZ7hF6f8K^dSEylIqb$_udM`bt9 zr%|_rB2J*`gbc%S8IP-~u4D^l@gJyGr@4~;#i@q|hTzLTHEyi9`-cguLsQYW>hto;hyF{3*S8@%p0M{Y8>jK% zIv9K}tFymsvYis5)=VDot*mlb^=fZzhKSPUdw(92;%$Koqii~U!(QY@#XPvl$JT$U z=e+)gMk!tB_$wlHy0;hZtF5VVGL0?`*uO%V|Q$LM=f?g$Wr{0BuYt!{IvFFw8U4oH2N)V|jJ^YKVo$+fmTig{X< z(<~{?Ty&K2gB$8~0cgqb;oO$q;+|yKB5$BiwVdYyh|uLYzMQPcZ|c3O7tP~a?wsqZ z9T+G-GMS$LQ`m@8Rdv(Jye*q$Ned`I7zALuN^saCRl zOy!xE1n0R(K01!;2dtxsv^K1{=LAsL*(>!o#AyZQO0svw#c!qR6sF)BJ5`=Pm-SvF zsoN`!t4eJH{`WFp&muGyzt@n+*_t+4t+!*+5|oT0yhv%O7-6>@D+)D;fCPs*ob$ajWUn$PO1xi^lUtG^!SFo?XuQ;riQeZw|m%-Pu5r zK=nogl4V-T@;$qzVVZoyvnejiszFKDvLk-DEz~^@9biOVEB2>=q5C+}MRa^bfD~&^ zUys>x7^7gp^Xs^Sa=Z5U)qh4q%2^)zAFEo}YE3+>!!+pZdWeQ^qr46M z0i-J=Q)rri#7T;z_#JHErg(VwGkFl<&%uPOV8z4-kpF*I4W{YknH(>1uNq52XD2kc z4c%H@oqWgVM0@}8(lTizJ~Wb%nHKWy_AO{c zW;(1)kDdh`jdJLngB^4=rcAn;8FdmLj(zVQ?y1;-Y>l~Gqu?RBR3@224^2dm$!7US z>zMQ?x}$}e9On)tO^CC+<<(rTMS0Q^O!IQqMnHA9P z=hc3RGPlA@fpVY4@ppLG08F`~+nIJkfJ`xNH*}}9bcRW}k42mD3KZdGJ+Y7RuBs2T z7`QH26(%R!yuLD;t_Hc)S%?kE-PcQ9>I(eLGDy!=ji zP6qdTp&y+>%5P{pv4P<3K3u?er32XqxNfsd^^Wduu9wKu#MJ+O#e|4ZlkTW>0eL_z z?B$}3s-f9x7T_w*?0hTvDjKW$ztq+(hdwPY4$|MeKFm|k0o-x8V2{Lgu7il=PD)nm zQp@b}EK~5wwJg~x?Oe?`th0uZD(KSwj{R)=YXjjQaPEn;^Rh8oqr8HNcljKC9zL-_ z#;#QMHJJe+bA98D89uNw^n78wzZD^6- zcD?)!TU0$xUusJ{Qb=2jfNr2oJFbvt&H<)r#<0$)a(luh{{i7q#%>z?O3tY$?5K@d zNHS95X?wC79I8sLC#X@(VuaMl*87Ta2`p;6!9Miuol$t9z zzxO_EWeD(XT;w;TY0zQcbb2JpC?-^yEAJ?pqRMf@eN~>x*oxS%`Z6U9ExFKqSMr|2is}Uah}_`| zAzb>j5Rdy0JPo5w%abmUrscz+bj$y)s!)giZYZi+f>DXBhieiu zRV=LGT#sUYZ)qU-$3jt2@%IL>5`HER4uAB9_j&JAr{^wS34{{+P7@aEOAUhTUdbN_ zk;;V_`5vLa4Rvqw8D@wfIRE=X*0UWlHNurNl2kp)<1y}NP!{oZ9418T_*a+5s+aGM z7|Sdbk}<OapbQAVWU)CTVa2qew_n%t$^{)@c2w``szSPqYbAZPfx&EGDOK+%wA-Q%b1F*cJ zS`N&0Avky#7jTXwH!%dv&Q#wAov1KT##2ow7^}y; za#N}35Bx4K4U|qAZ*36i6BBIjud}A``LAIw?IP#>$R&>jG_lnJ%glxPph{a)yS`E+ z*H;6$83kkAN}}~6fnqU(AIw-;PXpC~h^#A5^={BsjaA?=Fe2_>Qf73)-=eTaZ+Ld% zDPcnK;*74|xoM=dbwlnK@1idCzSZ3gNX^L)q@6!b58r+Zo4nwsyw2a;;_c{TFLm>z zN!2RGjY>H!>0pJrP}~ksusD7`Vmy{Jukpld{`*NOI4im>V+muCA$3W4HAN#^!9q8- ztj@*ZF8Z+|OE~BCf%D`vdl9X|9Al5Iwd}+zjrOOwL4M$mXwopCuMB*V4Ge{RCm?9q znS1#>gGL}EX%EAg-BP&7#E*3dTdwx6?L*t_YEN8pxs-0g8vO++e`8|TSvQs6h0Uu-~x?zdp4*GZm-N?M!iV9@j zCh#Vp`+vliaK1bD8;+IVv?1)u7<c zU)shPQEm8lUo7Dt4B*^L>P4j9VsVS0SF4n{3|ZSD{v% zrD(k*;MX9SOU;$(xMuw*^hfl-C$Oo%W2V|65<}D)M{g#~c4fx9hhuoXDlhl#gY?@> z4&!1J{eR53TeJzUc?^cG{3=@ zw`i#sfJ8|*iAbll>%>9BB0@X2-! z2cjD}y+qTisiK6-QZZ0uPyNGqfU83}wm1BDeDGTv!ao@{ML_%%aM5dv;-R33U(|Lu zxjF<_i3T~?Eki~#^5yi!;L#+BFqPtsc#HwY9BS|EDlAXlszsfaI4;l@HWPz*ceYy%H z1>3?DuO7zRQ;E>wr41oG(V$RG^&6Fo<^ybvb3YoU~X~(!PDSOiH(3 zZ>NDlYOAs{AICaxMy9Ar+{(|9-|$FCNG74}#Q?IEm~)UFtME2~t`EIpky9q>`a^+& zfBHW08zR}=$Ee=e*EGWBs9nwE9ClX<3XjpaAhrRU5<)hqH&k>QNE|?F2e&%UX&oWpzXV}Io1&;V-D(3>l_<8^RhQy1Pf-(o3 zvK9+$d8%b|pF*&q6N;o0G7v}z_|oYPFKH`oJ_Ru|#ms!)ML=&k(dB|)m!(+;ZUqT&GJXXJ%UBb)(MX^p zzvu>J5!E5%@Gt!8nyI*{uz~I!;CG94$h8X%4!-M)Q^iQ(l`3^3{uGbdUs-3;^lzYvs_#{ejsh%j@&_0rk?uDqn2pn;$`GMC5O z&{{b ztaeP4t}5Dp81h2=l>afjB0KkrNj^=ARR6O({7SR0-#rnR;s}@j_{VI5F;!l}g&>R2 zvsFjyr+I(n&FfUPLbYqnm5kK37LLF9VX4X`Pa+TFIrwl*SgTIQvlA^&w!y0=1kXTQ zzU)sfzr}zu!7Sx@v#^sTz#eUJo2z%0A`S=##*SZG3NXCqeQK%#?%mJItoOFq#cv29 z+kDz0&aG27Jn^D{ISc610xSqnMz3-`RkXD)bN9Eg6uI{qv)?h*Xrn?8N8I zqHGqw=6WHyg{vZkGBK~eK1uRxKjV+x-RTn;{?m-XzP`)V6DgF9c`f;r$fs>2xM3YA zG@>AWRryEJ@?$6?4P4C7?0u51!c}0wRUmYh%IK6fxpaZR=H>N7d%C6S=gB<#lP0%! z@`i5jRp>IL$wu+p*e85}EzoXfRq;q+`|m{%v(7uMC*OV!;G<~de=?eu2zLX;dcm_} zvvRp*9gW<9GS{`5hHXE;RN<1T5F9P61)Aj~L+dt3C_xKgz}PJ>lw|oBHl9?U}~PyDU&-Ge<8F^8gGBg5Bq5 z>sug_jspK76tG{YzAGVX1?qy7>f)A%HB$*iuN4IOiEwRaeW(aT)FJJrS^`qJhDA^{ zML9RIjA@J~zI^Sw`*r%es8%BtMl9wk+=6CQHfHv|L0jR9Dqt{}gQtg_RkD_Yd#2Yx zoezJ+_OoJzT`7Mw-wAbFLx_Uq$B1Q!PD`0s#8-m3R)v9DpJC_2^2zg};f%Y>hbQU2 zBcc~tUnd3)^V;IJFusJo&MJ34B1vgVhLBV2IDpRTktOWpnt5TR7w$$fg6@oXa;Qe= z(XhX@<mqZK&AhgTcQv^U7NhHCRzkwNi5??X;L+NPAL6R zI)MN)|ELG?9s!xjg5t4Pm22=3w1JIPN75n=DX10JW1jqafz$uwV*TL$<+xk-4*i zK2R>c@l#!a2oC9S>%#`Nd_Y6fcqJ$CR8e!xw{yOYMD9(pcwg=b+R&}n$*Q9iw$wAm zpD2IKZQ2n}#wrvg1tR;?5OyQLorB@41}P?i&PxuYJX?2R-Vd_07Dk02!2=)C8#gD}1KLSZ0%3ka}>_q4-6<7;B z(n9?9>3=Ame6DHe6Iwqq4(7a|)SDZmQ;jtIn*g=6+Oh>D7=Uw9N%=?}W539vo}h2C)CvYnlE-uyXwSj*4_u23 zKhPs%sFDgx;@7R$es#pR6w2~=!|e|rp}{;GS{nQdlU!n6 z8UHdj*Ho4bh&`gotb3TJi16Zw(~J8O7qw{)u2(=IPKIAy*husvhaep&%J1DhoItx+ zH(-xZ^#lODMrvG313|VcYm6Lrrt)GmNRLua*e_!UywC%HU@2kwmpSW(?w8`Am{+>6 z{33v?lx|dGu7oTzRBMj_9?)N;wxpo#RZbH})%&n}f`%+=skan~RW8$?|-8*&ZeX4H-?F0tL1cK@HVjjY8tTng` z`s|M!oV5Bj0oP|PKV{&2?8TJm;t*Vi_up$aHCSfpVR{3N_7T&j%lzcA+4kKs<#W{F zF~NH(GM#x!$p6;D!ssKPRh_>hL*2x2Ys?ujTN0;AVF_G7nXq8eIJDb(Kap6(t|0-M z(%n0O6Bv(YD`p1Zjv#$Dajb9{TBptwXdcqi);l|!-vp>?=%T=xc^$`2b6D1f}3aOw_7;{h=)RZ>>ZueyzST7H^foa?jO7jD|6=)fam;YD~R%h#7`AI95NxWyTf`uzv+v&eB({inaDO{F1TDq{;%qdW;CJ zV9DR+twj?cmJCk-FhvH|r>UCV=d$7)`nz1ItvGe2+x^Dys$A}S5W2j@^smOxVtA~u zBCWxxQA=8ZAK4zGbv7O5e-Q>t%&-EHQz+5fJ!+|LDnF3@A>^n5`O3w~{LPzV&_mX= zF2V+ElHtVEt?doJ9y>mxdbWq$XLxWZ&A+F(r7}>$Ht4KzIX28i79X1LT{yU9?Hc2w zdBg(Cps<~uem!&IWq3~+Hd8tcI!`sVu&4xv7M z+9F3LA!l9Fo93EtF2+87IZHfeA4&5^K*S(~w-lTy?_#g+NA~(U`b?*X^ufy7JqJ_$ zn(!*f6&29{P;xlP_Qsr4-f{?t(P!>McxqOI{TwaLwHb@TcgMh1(=G_GZ1@SNfX(Z$ zpOK~cHe;Ll@5(?m-M2stbydY%g*7n38%@f0@8TZiK$c0;{-d@A)|Ejsntw?#G!Y{=47 zMa@H`5Yn2zuyypGr;$B-;tkALz8a*mAf11 z(ULeZ6|*?td#v_03)Mz%10q2K{_?w4wV7abAn|4YOp9yW@&dN##D3swT} zbeyJtm(H7HZfhEQr!EJw=mHYSzn`4`n(!~!=$H9#5KF)yVy^*{oR&a$!EwHF;EdIY zrkRbk9^RkGU2!D$SkcHDu+xY2FWJM@fed}Tn|+{r^nqX$sn))`Zteh(;uN*ga!XZ1 z-n)`GIjYD~5aGK}nTjt131PZ5lLPK3nQwhFA5rYqJ;=G+yW4&!-i^r5tYogqKLTVf z^$Ry%oPIH-MND7Bcr+Bcqz`)jWD{w;!lGA zhoTzc+^kt)e}QM={=tN5($kys}w5#*i9?&=T z`=67klc?c^5prFQ%>MpAj;=84mBaq=YY|hdrji^ul(Dt0VUN zPydWzSZo%$1%3$jCSU5=>qxMvO40)otmy8ID_!jpK4Aq`7)tT>|AO)>(0pqes< zcehKxY>3*U`Qr-gn9fYsf%Gcq^3^>(Jo0c`W88I(_b+)WP3A%kIjC7#x1)$$j zl>#%TBpk6$S~RJG$ta>FcBFHjw8akpR`(1q~}{0WgHGN0d)vrPaJ(fge~#@t14Oph9Dq;y@Zz z{5+6v=2~a+=4ST;ts!K9Kn|e@&OsUmxJXxuv}0N2u1Fw})qacODh>exHhm*t*}K#o z2{;JCZ&6=`e-7lWfHW-T?0OVEJQTp1*n@;@3>-@rf!sgLAU~Ex9$q<_ zrOwhe!atLx7oyJn_{7;_l*rZf)s&6qJu285q5C?E1}Oqo*2sM6nTM0y>=M3Q!*qQ` zmU|j5*^bJk)ihqLmEJC~EnLG<`xZPZ2Cw=en@`qvRCOdh#Z(H;e^lILG^o+MYak_Q z3b=Y63RmDYJYwY1=%)<$4+lgP^9?Gcde`~vtZNuEqPVB9c>@s#u`kVm8bsMA!xtPN zT(gcN0P&?(;7V`40#yQZmAa(cFWXC4U@n*56%_vKi|XxEabcvEX3+^f8NcdhnQNp1 z$o{a(ElGUU_0AXphMWP?8D_pcH2T3n_ie(z%VPocU7l6n_rGiU;nMtBI(7il2u|z? zhgs21k-ka}?oS!IAMX#4qX1RekO(hBC>%J$nVCW}-ykT3ucy91h5y?e_&`W62yE5= z&f=N&^xq(jO;pTRy_x)fq!C`nOFDz9fG0hj`7=WJ)-I~ft)};~cinpcV8=R6YH(lG zrEXPYp-BC7MU`Hi{Ssmck%@M&nz3n)b zR)83MK+LHV;1+mwjVB3g;@=DztM+St$A|!?D&bK(2@i@Z5(R=AUEV&zSqsU zPUWU8RMMU`)urlUL?2pc+>u%Z*j3@9u=)we(^IyoX8q?;2Chg;>wh`&K16NhVL= z#&K>KqV94W^HqR>u z!lR`|Qv9E8$t0fp>NdNl)>0qq0ysq+XQ|OeN-C0WG8Mc}@quV_BAJin zNyl6K+=C3XQnZ4qlSWJ%@(C+!Trg6}geqIH3jheP4E-bGn1A*o!OoWOzme5era9{x zNE}=tTqPIoc55H={d={3v-|ZGw6I+FqTVtuT|2_RQDUb>_4Ix7?>1hs!yhpO<8JZ; z@(g%)*alTSUe0_;&u)2Zcbu0J+a6P}&&f1@j9n5b-^kkgpBp5Fa1trGbNsQmNmCm6 zZ?29ZSYh$Ps~Nfg-w%_djvnE(p=D{56_Ea4+fLHO+=ZSrOs6$mm1{+_+L@_o1#4vt zx31AOJSc|i2Pb7rRw+raf1@%crl)@=?1ch^E*~w0s1PMW4cT)4xt=IhR7O6A((kxt z0V+e#}z;T>!u^0MJmKgba@#Gb7) zG0BtGKm%fs8Nf@syChaO%@s66nawu&U7qs+8vy6K3!3G)nMyT9up1275Bzh=AR`N=sI1-Xi<#K`%7oze#Ld z634l05!t}^3j{~24Ld9S6y}3b0sSwrD$%svSCIclZpQF3MLdJIOX$!4225V=sM4P{ z5)r1)wi$yLEaH>`p}K2hP{6i_CMkBXH1dFdh1|Jnz+3q$tH`Q}^NX{UGHjjEt(c3d`GO-y^w=GPbGO~y>(NF*PJ{Ku zz|<<kejdQHK@ZhT2E+Njm&?0OZ{#i%#490(^NZE7)9K; z$=BRfqb3lvOv-Tg@Fv3{b=Ggldo$B<|HeMZt$W&c#GlBWvba*y*|eV$X!7Y#IjAXW z+)9+WO)M)m4AWd2F_NsZv2|E2I1NA7JfILqOhw=kuIDu+gLn=8Asv3La=ZxXGBDOkYj0WIrkcReoW=1`x1SH=s@LNYp(({qnuCdXGg8#P+ zPASE5$M|PH3Bx)@U=Rm0c(aD)dGY}yUeICR!v87UsY}E7RYBs|t}W1rf&l~2r<4$R z{u?mC6H$CLgxKKO?KG8AHh|!8vB77o7GXG(+@XsdSKNRB-PouBj{d4Kk zCJ2d0R~t%IVS<6JUHvn)-=5;*yvh-jmWzM*_%1lI*=+lbdGfKhL3nh|8M{@ zVcMg=X^|24;a?dhI`QxPL{IdKX@C|%{n2~&4kchgyyO=&S+l%+qVo8U$ zB`%co$)+2j-#QDkZl)7irgEU%F?d^tdytr4(ODHqM=}r69$=8CQS>cI4;{a>{mnG{IzTJ#7-6@@-MIi$pWbav4ZP;#D;o>=AB|SgxQ9i3m;AC`W zT4Ue&p45#YkUQhv$B4AtSX@nzj?(%*d&rrsW2k9MgG+G#X9b$FkI5AZYbhIMkMLm6 z;nM5&>RjyjAa61}yPmv5`GZpW+R})|N8tCh;YSztD%B4Np;lXHhXM7J?0TvVI~(>F z`?W#oPgdPAv-aAd+=AnUcB-hUmg>VI{@KyQBxVd`r;(%+>H92dGQ`OQqA2QeKleB*F*dzz$}p`ru)7^m^!FW5v9{u6lgU&r&5s(%IH~85S*?~1B1aIR9hI3#0^Nc^A9OP+3dgjrK2silfsdK}N#Tj@FJ9J;@^9e8* z;)B1dSJJ_PNKtU;Jl!=E?i{kd$*-K0WoGiQ+S%w%=seQnXNnT25Zr29(e+q?dF%(~ zq<9yd+1B+evf0HzfT%;;FFiGRJ+j`(^H?VBg>dZI;-N<&_fkS3J&addE@)+kQ#gy< zWKj>Iy;p6L4cZoVWBxY8^{uqRO^r!3-C#B8GMnwnubXFQkY1RqPsu1dVKepj5l$!Q zWyh8)k?}3D$$qcwkEyRo2~-bc5Vqwz`f;l|dW1L~tNq8y!HqEWLhjL_AVzsE6sC?x zUpk!-B?=3KjZ$fP(Z`_X=hkDZ@8{ag3MT$8gZBtx3jBh46OuAhWK9JTyco^1@K|;R z6NNm}KY4pjViTUS_8I?x(`gQ$mj!_r?Jeas6<5+$B5it}lSdT}jf;@)$kFvY6-IFR zD3pb<#rLPFM4`r22FiTjan93t23#}5X}@>qJXWD~K&L5I{tja$qK~Y>>5XKSB|-0a zg}^1VFr8ed2OZ${(XDp%_)Ub+N>HA#Qyv$a+$+0M*1VqJLYVqqAI%-Ad@_TGK2f%| zKqcwc7#gA_XPgO!6K3!4%x)Ht_7V)2zhPkQeAiyj7n9Xj=@RJNQv8`F{X4YMcbFYk zB_&w6to-mp>{kI1d}Nit7NsoJxK4oL$rDb*$wMwgazo^JV$rjR*FL>cEr_y(f{QC6 zT)}w6j{Id6q=*mgI-)A;2#wyt+1g}RGsMp%5v5n5{3dETBxZbvEwm6D!6pmT5`a`n z*#~;xKx%eykC)Y<`PbXD*`~WTT6&YP0rn&Sj$;U>88YwESf<<>GUvttu5+{wOBcG> zG%B8LH%eR9u`yPZ4_r2YJ_MM&kUymaaDHC%epz{|`aggXJI|@R_y$N41Dpx~yNoXR znwt;VE}$gbOi{>X0zsdqsTFloRH0^6#OYX< ziB5xHk^e&wL;i@h7*}GHB3;gU0Mxl%C(Kfof1#L2IUlG%zF*FmFGbHZBRNJxp@^;6 zv4vq2BW+10E_}_#BU*_XHoL007(nMZ$OyJc-#!`)-qwXcrhEF$FA$=zf)TGbOLsJp%)xsE%G}Y= ze;u`!vnjphSL49Z73Ya%K)X4bmGz9cC*5+p`yJ#BYLbE#v($H zq26>!A>wTMmry;ZRiyZMFlhwwI3?o9b*Vs{-^gd{_%uVbxX_$&uEjUp0;2^X1s;Gk zCTRF%EWpZPBJh6@MxDe`5nd;dvdB2M;Gt$1R!*|xLs7V-Yk}WNQ7+3>wh`-;-9D>A z649$*BJhp`rBA|5CC+wrBYLYzvF%U({eIKjQQTnhexUqDKZV5$0jS0N#uUd7t8_q5 z?RS@X*POQs^Tbou80^Dtgl8eS8uZ^u_Id7zzS?)%$b-BMF^Y2a8sQ+|>!zN#gT9!F zO|qo(`UxfWOR2o#w9S4aSGF6jY4S`J$wlW6?^}}B4{H;BVFCKlo;Gf3Eje*!Q9)`m zM2b#3z7!yTL8zlj{`%h)1YdZfN(0!_nkfxDVj9Mx+cl0bumS`byxKkB6cx0~{48J|1(RpVJNu(VpW zpcls{-L6O&g*Lxm{f6C1d%@iu__2>8szGRdl+iKFBM>7gwIQ3(PaYsfIEWgtR8R8i zBk-a6VyQXLzakMw3DhH~Zv={AnL#o>)hn*TH{bs85RChAvd*o9m=m+=Spsj=GC4aC z!dFLKEL3!L`pWp10;+DzS`+MGPVBIxgz}CL$(g5o71^$jK+`2msK}lO9(QO^O zAMr2`A4BH-nfI2jT-B(DK=aokvGRk9FV^jAss)`FstruF)6yK-h+Enn4sahCRBBaB z#Gip)=1Y%$dnTS?%?|?5_TkuIFz-b%mhto%VYX}p4%<4Q(40PZVct+G;hbk&LE=y< zc~)i&9yKXr6PpV*<+~gHvC|670%TmL2D7fn;OoKYMF@0XlWZU6y*E=|whouERgT#Q zEKF1wZFomrMAN>O->w|@aIMlJd5T8#B(~R-8@tD$ajlUbGj0=XzC>fHPZ-qR&c}vI zH0UZEG((ScQJG?7VT?P~b4ti-mLt5%&ipGB$j02SrttpP^5|h@Q2&BN-!Vq4Tpu5r z%{W{rm;eb-vV04psN>EHKYBjZm`S)Jp%aLW%2&ZsNu$g7=j@Aqq|3TX-%)0KlKcLn z43>p?H6^I20NSE%!0uQL^|ydYo$4bxuH_oC-9DgZWAz<Mv+FSt6ygSH&6`eufFW}KokaI*o)eRnvvq7wkNwm?lr`xWW-^2or`nl{& zo3>X(Y*h9rk}N1!Dbonn3P_lEPyxubV58<*X|ux2NCtc=^N0!^^jRaCX3v$WZFeE+ zr~$5;(B}S4+4*$ci}VgH_iA$3oTc#}r-jFLx;FqT)|Sz_hV_|UF? zYw~#j>&s(1wp?dI=$Xb1Jo%`(98AZvk;qj)RB7bgw((pU^O!7h_KhhekYyZ=lJ)Or z!_3j%n&3EGA9%o>z9zI0L=9al}w{{|kwW|CtCBBjv9S_gss45Uk2?W@EJd85RC_8MgQv zrY+uk7p$`;#j-yc16vjd=?k#Db}SGp7siJcY#c8Bmh=?<_>?yUPis{*|7a@QfTlcI zRikb6dw1|@lYMUY2%o4rHUH{S5QbVd&^=ss9YqrL$Uj-EiK-TSM`rxirgQV3t3-SE zNYQo259o`&lKva@ISmgH7xg!4#sc1v4UTnK(O~l(i{IFkG@W3>#-Cq;JI}Uk?{TvJ|M%D0I2&ZB;@w>R6|8K~G8cr<`V%h-PV*O~ehPD6TMIUhTi zul4SuIjL^|>yq2Q+>Pw+@-CnFlG|&Tqj6_#8ds@e($eMgvB7uPTVLC%HhN%E1fa``VlO7W`CZx6$n*h+Z1Z%A$$NCC?$R@tGF0PTl*ohCK7V(qe zc_@ae4=prd5^=w5ShaYXqP``o0>kZ?2T?^khB}{x)EJ=ub*PKM=mR;Sjck&FPqPEw z-dY4BoMlF^d$LZUkU|tkLWx&wFMM6L{((F#Qs`AS!~HeCQNr2APlF6F$x@=HzV#K1 zj|w9hr^s1IZBhM7@gR9h)=dg0CAVzA7lk);Y?n#r{=EM&uQs%3ij=M!AK_nyS8sgJ zFr#&z5&EsvGZ|5Sp~A%thMhI)p1qTLg<}8$RXm6at-?kvofzQs#wo3vzyzg_@}Wn6 zsA#=$_1~L6g$GNf2@s<;qW5481Vky`)Df*GsR)_^jGcBxQcysJuKskK9)%)Ew4t!@ zjp@M9i4L*JLu>H9)D#lH83u^*sqnXn=X+fUfi?W5Rwm3K71IOBM~9gmY6SRjV6TQ{ zl@uru)tb`7)|muR7ePf1tw1UMU!M(1fMd-H=tL4A78@Tr5c|WSG&vuWEI>?@gyE9u z8}K;#t6+^N>%f+9W%Q(yRE~8(Qk~OCGYAHh-WaX{LP0&eB5&l%Vc1B|C3~%~gr<}0F%^6ut}>_k&CgHhFZ2r3YXA4-a#xP>&R!Rd&hBRIv8!p->g+{ ztSZ`6Qw=0{Cqy z>lygwQ}6%M$0=?Q0oPK2JTbV^=>M|`1;Fz~!X zhg1OQfYk2)>y=o_+QGH$fM}JJZ*!na>cNp4qm%6-eXJccNFiI z|11Qs`S2;+g(a^Q;y7WI?y0}t$>#drcW>v85b<=5nH)U?BhmCHv!t6!X>4k4HZHTl zizpZ!YBowR6iJ0u4L7I6e4N+4I#Vo?{KHB8d0czd0cqi>F1>@ZCGq-n=lshjz+Ww8 zilUwvU#FdT3ag%f#JuEqFqfho^J&C&a-S=gsyPVpGrxD>&Q^dhPk?<1Dx)7+Ydi5q z%Ya84ruSp6B_}o5dz$d@OH-1o-pwF~Pikk^>L(TK#?$6znX=bSMfm@B50P0NIACo{5+3OaZSWixH_UZA|%mxt0W~Q=DDsvC2A8PF1kh1lT%gPZe#>BsK)x_~Ex+UDQ&AI6O+Ac7m;v3)A=D$L`} zMNt3JZ0(_~ZAcU8nTt9&%)c16gOiq(d~p0awZT0<$EV%=nv~wwO7|kHrkf^oO`ycd zb;_}L<$CI>zE8Ah@k=)aRtupK;Gs{amNAcQ6!Khx;XqG-lh>;%lIk?!bap4V0D?Rg zlEEjrI8X2XPfzXx(0-juLsr}YNLYW52Hx}@=uNwA&zf}5zkZgT3VJTPb_P!1S)kHVym z;g3tpA@v?UTt-ENLM3uwi~a47nm-We zFePyNT3x}OP3kcK?V*TsMD?bXdHk(RvZ|ttyTJ|;N{jSlAF>_=0_8brqVCw`)59#& zht(HF@kJV>-@xED$LsXhza}0z1+Vt#kJ977?gN2Z!zYPKm+#1-s}skKkAfd*8>bnB zTMhh{aYNGTS?Xaf!vTpd7;5X9=1dd;Tz|XQC&+#u-8{^Zx>Z+oav$q|d#ESYe-`2! zDP(SeS(r6RX`d!6e&4YyHHy_refom~;H4gY&vs2zW+cyT{fuTbS_l2?XECo&iHAm? z40fYkP9!?|m{@Z!gI2(t{KXjeu)i>!LKS6)`8G4~dWXH4a?RDlK3*sC)WSyktHR+A zr4}lL5T8dwiTvdrNY?_6u6NB(GbecTL)MOo3neeBBuA>ki^RtGbI(cVaXr7XjI@<`EiKdVbk=Hx|@AG&-tQdvfPYm+pWrR)OI)^oT3l9 zE!lZR)jqQM`K82le1g(2+U^1E3WprH@n5Sipymu-o(0W=4mS+*_LlmWFbsY|jQ zav5RL2q=Z+MbOgdWJ?sQ{)znPL78yblN*PLri;gZ|1U)1T)pT%6O=#L`%kP1g3KD`9y)XBAm=#c?@ zKT3Sb@*|ReL(JaQ{6YSLjhD1rn9%v;aNLk%e3X|;<1|`tdP$zkk^4*6x_&U*V3CDA zh398Q)L}Vx8*|;+s_P@Gu4Apt2>UP4KVKl50j2XVvs1~DJ!ogb;?7_)2Z0;nG@-Xj zfl$$qys%vjclZAv!vsp-Xr1rZKR?zolD^p;Kl_ota95#0jxDNf3i7=LW>!`YefvTw6lVfm zK4PPZl>NpCHkbeJ-wjvzExNvNs;oy*-tSnS)6D@1jH;h@_RPTAtig>n zg*N~)&4k)RBEVRcFYd|lM72eK*a|i!m)x8tOR4#1UcC63$Rx)Q5%s8hzM3h*##k{= z6KD=`R#armTe^A8Baku>rl)?)75Js#cV11oQLwxNGqG*sxs1t1!JVrM8UNXa(vqj; z7OE!w(~(<8?Ol`yyc>7U8s=zq9+8yetyJN7uUS(nw0||myj^-$S7VA5wUx+Ac7w9K zSSm9|%>lO4-#_KTbkilvuja_cpz>xoo2GJ&UO{GK0%K3Sfj@qlL#>{-Wr$pB{sE5i z%W%4)g6WKGQFxO)wS|JbJknfI=T^jkL-5{=6T|sB=|p}FZ-ittzDqydI`5-{L|%Qz zTYXpPAFf(#J32-Hee$QfFTFdD%7np(R+a{v zH9U8>pw-nBy$g%XKgs|Ly_OaVh#J_otZBLc;Gn7q0zg?xf&=eJ+vm<-`76gtZS!mz zY=(Fq3{>|mzYx)(mwOlV&AVh{P+r`S1p5cBs5bTw;BU)}QYmo=4ZlW6(nK?Qq{u*< z0CwkZ)f=gEpFcjE1@HBmzs!&CTuq1Y+(mx(NEzTj2Q(A$E)&ZnZ{od=UB7-_qxoI7 z_P*K>x@658)8S-lAPcY}n`ZF8`Wg^`8v=cI<{qOPndW?Y;`~2W_)j=psfajQ45VO= z?<_j~M;$&mK9DOv=K8}lGY`x`?!8M^Y=XMfV$5bR{I%RLOd9giOMB9CsVxkr0=_w- ziun$v0i$NWUW0RcvUZyDiqXJTEYhdDaPD&cP8>}B&?83@LoMG(RrEvA-(%)ll!tn8 zz-P?ce3(ZcuQ9-&`9)$!55S34STmtE%cDZLdN0&fMco!;5`Ce_qjpvZ|0T_Qx=+t6 z1A8chE1mt532{Ty-1v$mlEII=qLF@1-R2Y{SFnzJ$)csfk|{o2aLBipb;#@r-f1Nh z)=t9F{_3TW@z+CLevMGBh83f=kJLB0gHRdZ0Y|dw8(rp?y^iqUG5qNY(rB9WrQ7ywiS&G>eBjAegCLmqbNmHMjX8l9IV1+Oai>3uNZ@N)CCv<>+AQJZrRTAF zHVyXc(z!u>tM5$fX0IPgBH^b+1)V*5(UiI{qp*i;y0XitbWmIqNgXeYAg%`K9Z@h3 zGq#HxlH~T0HRODs%4;Ixz4-YUo&4PuppT6CD5d(E-Tr)1;kvW@hY%sDeEHhn4fcBz z12$;=jItcg3-NW`01CosyAPnv(^M)R8mHCi`@xn^sEmfu+P)xJRdfc|83?(X*$Fdx zU!nh_XW)o4d8SdWTLW$3+o{r4(+4&;8+9cNU;4+MsNA7HIDOQ|C|Bsf4|g6%`PXxfoA zU4&(p(;IiJ+1O$;{{qV*s_N7tv#%iCA}jZ6(=Y6g-$|dy8q*9q&pUSbnj?kgiiul>O5(qQlRRh zR)bfWATCOgwcl_;v{hzf`7s7ZM5j9 zqLmY>P>^LYBoS9jDuW@Yp#4;EE|W%8g~jC5kjf%3_|8X^<-I70?-YbHvTj}mE()h( zQB;}F^ROf>bYHQW-8sCsvTP3&w85v#3l zuG%JJLlwUBi63X@24q=eyTYI9Ky;|o9b7$D^VvvN3hQhSO~h@fC3s* z#ZSdu3L4Jk!GeQ)1XQnaQxPE$@UaIWf4OSY6@ZLlWgja4CjGp7jz#4-F``c--a)*6 zsCse{jwI;wu}3aHyjnRd33OwGobfxc1ouhN6_pQOJ?|&k0~gJd2&^RioO_5)=Fu~* zop@)mPX9y^oEQBs&1|{5S_fvFg}4}k8pTmV3Rvq}T+NeQz!$hQllgCwql_W_S={?e zd?_|fy;5|`jx?mrvmaKvoNS{T%?q>vEV=J;iJK{U<8(<)py2M238#Q`b+@aM9t{^zEu;__Yx?D^l1#Ah_F(G_2n zkFXJ!U`Ykk3jPUK^*k=+O~|Z|IZ)~eKS%Po+CQAu`=!yb*>B>cShk?iiH)x%-`>0R zIfmcAn_&~VN~K_4=v=ro7qhD3NxiL(rRA+56FB^=O@jBgG^yn=;q*;*qA*npA#U+= z1Os7zJYy?a;G&Tb1}3*Nr%!*Q?$@=-!XT;Dls`xl$-iG$9tsGu{ypHl0B zLm&Mhg|s*y>GuF2Y<`aQV{X?}uO`GUomZD)dQV+zO& z&I!7!p<&!m1NWD-| zoT(KQz;!nO|5bg*4r?u-mSIbn1B-?Ev}RrtRBE$@9pdKIRgkxd_A!BEOs2bvEqW%r zY~E)g*u(~zi2~l3H@{#CAkkkSkWMS?xK!iC2D}cB2azhUa;^NW?>;=cL*-uA07G~O zq$4Z~irwj^jJ^wOOi9cEeIxU_aA1Rr0T;cp56T?DjswE|5WI#ymo+}61e30B(?cNtFt+{x zgtPDcGbb-vrF$Y~*^tpixpaaf);kX)!*>t|i|T%jN2Gx-&|?fNwAsoNooA9KttYyQ z=q81w{DJjZMBb%c?WBYKjp|Vvz3NE3o2aTei(ldP_td$v(-VC5_!TxksiapdhL)ij;PmgrT!S2VzyCl*^r~*96C_c51D;0#Tjv5GI|6<40t#x>c27M%q^WZyG07G z_i_`6%&W)*Y0O9nw7K>GI^Y6SMQpW!U{8E2iIsdnKHTU!zM9WJf(IuPKi}+koCO_4 zY_5oVLAyh6l$kmM!vb#RGN&^#$_!1&lL8DC@Zn}u$U1ZENDLOW0d3^Ak@!oD$E&ZG zdTa~Go(x46<`+^3wwxgZ>_JIu_|)B<(c&X|VU+b0+!}yuPmbqV!QWIAaXyMxm0^bv zL-clWkBVM!r$;H`km-d}f&uj1x38Ssf@Zm2H|@RbEcB%m+1b`=foq)I*Qv69(ev5g z2m!qO64TE%X$Rp=j4}*7xB}qWLwQ$x%&a2liAUNj1bZS?a|oASH+Npt^^UuLvt=7? zT+a@*5z`Dc>tL~{vi%<3&!Ca}4^)%R?NvRXw~cC06-(g@Hu4Xks?+-^QkX!!@?9sZ z)Z9yQzAu{8F2CX@O#oRb3%P46& zQYw{>+3Gj;;(2&5@bM>;{7tOd$1o97rQt+nZ!|FNmo5F4GJ!EAl8+LCwJuc-i6-(& zgsn=p%uqSYEj{dyr=?qX+utE-m}SIH9m?%`9PGVQ<1Nfq+@!jAAyL;D+b#$Yg&dyU zBk8KCLua%Rud~!*3ksZ(;D5mX>9WyVh97~QuM864ychqyI8)!=V}NjXR9cd0{g_qE zB&CIN$;veiVDKqiA|pr>$$UwrE&?B=$%4*SQ*dY+3i!CmsUVI?9r3!EwYGsJ6==m< zpisU^B%^|FY%XjmtWb2q~H^FJW0*3xB8R{sxv@tNT?Js2Dy6}z_gGLTW{b!sdY!yq& z`;{GI-{8d0kVTjfXPl__s7}l3W0m z;f}9G4YCCQg#KHSNNuCDv919BJ^DpwRH!k;a#r|+d+DJs&u-z$IZ1W7{Fkw!HM6(# zojfYJB>#4|PO*ce@KfO#-eE7lfFKPZ- zXy4lPBJ!DVI3YgkicN48beZD=9_i3{6xv+9#PCrY)|H2ho^oPaGdERXVj%$)bIV0# zTtYr()S3EFw;bX|xSdh0-@H#PR6knXr2Yvw^F^>I#vEQYzjF;WyjT{meatCcw}A(% zM?TmtntX=kcsjr?)Izum-_?arH2&7BzXnP4YfRI$ogtOx5D;urGX}J32yvRF`aQE6 zgKzM!5j0da3ZM#FGVUKmz1<%70BDBn!*{{9XcM5PwJD(g`9C&E&coe>NC+Lwk7t4p z;$EP{%G}z((IJJqA#|w#P#QYHOU8MjZBxfv4^)*oG}LzipyT(xzu=;;%|z6^MO(<1 zPD)k=Je-fO^lRs=u=&LO`S@Us#ML2l{3t$7;kKEo;KE{4SYPjEPs+XLrCu!w?5A}C zjJdx8??m6xxVMMiMYRsAwiS`7JBfHrA9$HhjOsTc%eE+Qf?^Tkx{>V(Wc3ATm;iEd zZNgM4+0O!_pQcci7+JQDzHa(~A3xtc#(8fPtVPzO%AmSVvmojfG4Vbz2a?Z;aWA!S z)2M-YkkoreS#DA8>-UFw-l4h0`=`0M<9BFCT*^1l4?#yX>ECA@=)N&_z1_g}#(&-| zDCDNaVPBVPo|4^yi)!1!bP{>xhCT^8y1kBfzbb4A<@883ht&WFd*L5en)iRBHC`jG zKoZq)JFdbXJ9dn(M^~vyQ#1IrEyg;C)-PG3fkFQHZOV;wY^H#^+(?G)zt2^)(!Z|+ zU&w41(e?vZw7rv5kD=OrEYvQKZ5QZ@OnUAr6b_I_GwJ#O(@|^@Ka^Z^+CjVK*006N zD(d=5xWY?4=2^~ALqgC@+TZ+SEHO*r*`C7xg}eXlou2Q@S!VuTI7WW_C_QqaayHs% zX??rR+e}TiAm{TE{C&4(ri|ak@wBAmsg~u9+`87NXC^kQf1W8z=z0&+_Ud^5R9Kje z!haF&)@AvcVgE6lM>t>iM|vvH&D^2GW>@Fp1h?Q`juwUfBK)ySHO_Ih)%kU&w?{O+ zN9r&z^kXguYwVeK9%|^vbFIu;?s(xt$#rURP1^A4)jVG*^5?ni0br(h+^fi!UMw+m zyw_O41ODBSl)lN*vahMdJ!t_Y7(p|q!C$=J=J+?N-~kvIHv!M@p`t`2PCLE&V%g(` zVxfkU%Ex!hw{mMdzBjqfRfj12(hwZvl*~uQNF%}(mjuOd?`fY^;ex)-eS1dAApo@< zy|&>#Z^!A#`~1W@=>ro~Gm~KFMGM+gjp3=3-=hg?I~^*yYqDf051hI5t9C!BC+Y81 zczB(%((MxHhcI>N^Yl%`DkfTsX4j_6 zB2MJxU>T|fz{b>wNtc)^9J(XxtH8aedtpp8nN(icjptA~zBdA75bkv!@cy{mnwqXo zml(|&BLRjq=RcvCWZ4355Gn;3=c9BGcLJ7GpwXnBcwG&y z09+OoD!^o8N=fC}L3GE5P*w1`^ zbDO*4n;Uew_2aCQ54Me$bdG)W%TvGtBH5w9vDrj-XknMa9ox^V?1OlilBxI7*6SG{ z1qS-lHkrp7?(0GD3^18J$BuS0E8?3dhAxYcn{AH(;~cxxxXEcPQLvp=Lgz*FPdv!E z?KhvrSa=CsozyU}XifVqWW=vn?Cja?HN?dU_a#5KrDu2@@AnZS>b_{Z%zC%oUZvo> z+LzCKzT}!DM_Zd(B5e3<<7jtzqIWK*x?{i>8)t8w-#XFH#S|0NyY+KSjXunx09+Sg z-;AXzJj5{~F_^)deNpJDh;RYVWc`mB2{cQE6p72v{UFqE2VJMb`89^#XNA_==?_NX z)`Q@i(2|`pXv-QshxyjMFduCOpV?@G7NLOb8BF~L_NN$>Pdq^sVDkkf%+MbH zGcUs}7gnN5@7aOf=sL)9g}*G89uZoe#-P*o7_hiP*r2-!bDMr3yg}Gkgy$UeU<2{a z6-EqLt6lKw*t~}#?N)lP`Io9$GY|Z;1A-9SET;}s^??AGFaogVDBfAae#MfW9Icsm z=5aKzTeC|oNCE`PgZ?vh$_XH;Sqb3Qh}3>#BGN%_Id-JOgNC+HNEpyuH%A$MqIpqv z-uqtghH>Zv0dyJ2s4UWk{J0LO+F%}?IXszfXY5mVG<4oDGrB}SybW#5uX-5aqrdB< zibK3=+Ix#zDt4zF;t4`r7YgAz$K1^z&Th40CH@ zip&v*&dopzt_!fXrT#ECwNfQk7%VuP0@iVH)E&SbLVKBiL(b`C6YzNs$OY5oxx^u% zw&=0;3TzGyiQtBV`l;=me~1DxoL^2JmKgADJg6=q95|d{p70E=3LR+Gv!PEg$_uvZ ztF7wMk;XYStXG>f-rD{%VT%t~(FcM?S#XQB+2i8#?JKHQYOyp>DIIo);+q~u(duFS zi_7Jq*_#j!DEc1o%G3-#cIu}XDv(i1eJ6*nfcS4vT2Pt*Q0|7&TImr?v#3R z0V8Acb&QjivoO28^-zn5f4$-J5;r9je|3KY+a`axz#)0(VkF_MwLL+Fx9Rh^l5G=TdpuydP?b~GL%$##)zVCVR z8c`kVKV{Ixg zO#E$laP3z1OW2!2EXsJa(u+J^$=v?Ku2HBD1!9z>eJDvrRE26*b{)?l!VR_ftB>*X z{UAR@(bC!=-A_-Lxrik!z|z3+(_zY=CJu^~(p!1BFu3>j2J&&YBYl2HrHQv{uEXDQ z!U_i$t6ZG;#-(Wxlc`r^-+$-)Z5+% z!>tmfElfXBA`>ZEAkeK~#s++O+sipNwB@fIzKVH3l25>7C)^%;&*vdzBV9Xei1|c{ z+Cy}-9((F~7gqr+eh#?<=KIl(KdZUFOUj>ukp*yL=EQ@bw9Ds1)_ykvxmHGf$K#eG zAd=qa!lA)lL}_nWX*~d`ReU&qUhD z5`kWO`2meg2LoCv;K}e(sTlm{xPos?dx{0Y_}l7QJB!mu@tL3tt@xtB&T?NyB!)}u zt=i`b!#l^a)TWtsFK2aF2(#OKf`K=i3`vQOmlbGBT+A|y$!fM4^Ho|Q7sJV>aXifj zq)FUaPqt8ERz_g}d&jf(>fxMh@IE)=^g?+O)&q}}!P{$a_S1Z@SkA^Y<^$0MuS3w8 zEr%%k;b%3zut8E2{;yo3EA4TyLcm?2<2y|^lX)Ln4}h&cP~|My^+HG6sI=1x@WI+y zyLK)U9&o%&`p2*>qE|J{UR4NCDU7 zmq#({dMr1>%u)^1t~WnS>(Bt-lNdOXABT2tP| zN5>WyUNyCU3SrU3o;7qYxnS^nWE=?>yX^nqLEEW2lCi5|_k7okgknqwQGYKGXPTslh)_P_8 z(#it-;Vb-ZA$!oE*mkG>+3aOSGQ)Ou73qihVlvl(6P_j}$d3o6n_C-W0}R0-f@|~% z(&GAT5sV+#z)1v^rcPl*XT_?Q)>bpc>5--O1c8-qD!KMkcNnh#L`2*!v{MgQV-k7J zA59G#=Jn={48$=Gc=X&%JnZh#quX2qL#C5}OYQl6vxGlxrEX8~(D9F5itB&0y;Cs& zJW<>e13=@rJ09ruTlZ12Kk0S(J*Db^mLMct0qPnvBMG>EtSeHd7zz!gynRrLdGAS& zwtV#HGG&Gvq=C$5-so^d8i#{@cn>yZd=h3y>(ZjPT0A^MhN@WE)GHmua?g&>9QPq9 zr(r{bII9^RvV4bHU=F4yq=C} z8e|T$xU7IP)<%S!PdmPb4NxHR2^tOWY(d1u!%Z2OEhb+e|ENkvm*9MZFnu+DEUW6EOfu83VuabiIvls0H!+ zqfIif@r&e-L!4{R$D8?v96z$1tfSa`W7yGlGOANk?#-ByKY6@t8L{+)MT~akPLtbE zw<@Og-;9q9C8wsY$=K8@m2%sMld z1td78TqoZF)52>xb@6p}$Y9IlcykN%1CbS^#VM`+!nZVp81)%$#>f6mZwx*>k*Vq8 zBNxU(L3x0TwLt*5PrF)J9 zG^%i9LajBq>%1l^z08GG5}uvOlwW(p*j7VJM0&dlz4bW!-fwqsl3J1c&Y@OH7f*mw z+nx5r*$e`rf*F1V9sM0ZEVt_94K;QHqx(h8(F|>%eDh* zC@hs>);`Ud;d)YFW^0r7E(SVPTi)c=obiO;pzH_F#(6IO!p0_DJ1!Td+l?*nA;S3V zOB|b%$LEo<)t-&I%$%}{c8689pSojyI9e*cMr0c^ByLPO&mzP;9ljV>=2CoR+_GTJ z>y|3TP6wp>2XHLP6tm@FKlv=%+A7B$@FNKZ;`%*ykmsizOO5WF7PjM5J}LDAfj)CX zAyoPh)E2G)T{DjU(^2M?q@C!wb44WAJUGH+sRw!imqm2ZAGOa5W%3DN^c665V|eKv znto`i;FfvsXn{=T-?j*q;UZaA8&N@jM2v8_uvKz|M>s?3W)!w5e&*4wyQZf9rbFo&jC-J3Wp20W7}beN!`58q||v|#h*hRt32*uaW{ zEkXKLG-t{suco5ed=X)c8t<4GVD`GI79CjKE5}v?9c*7-PBYKXoO6E$UN~#~cyd}x zAUNNg#{pYsmPBAR-0r|3N<*}=SCCiS7m80XHS6>JNY?#zYCJu@5vrxPPDy%4%HZPw ztHd_62L75Z|DH+{kq{0w!l?QQB~xD49&%a*>K&LA*;oG={wQ`z=UkyodGCAnVzze? z58QZD5}E}~0ezf3blv%ch3TUa4IK^41E)$OJR;7ONIA)hd zUI-FeEB6|&umvs+?e`FkffOtSgvOL;4<9ZT^MMCNBg1$e(gLa=K-LXwR^%Vl) ziUFy-0GwTZX2HRt`7iQ-2tAvXrVGTXV0?U9>_A81abz#@hrR8(}(iM0J)8ED3S@xz(NncSQLZX^@Up|+*M6UrB; za`GeZF|L}v&FJUXR|O+?;$nqZ*$wkXXDZp@7tm1{{xLMpK^Hf=dk}(a2A1~%AXNIcR8Khhc@Hk?jW*FzaCc7@|I zhX+u6GFel_Qylbq5#49IJd!2I)oqTi82UC5!dqh+=HoY5#=ZWq2H)kzgM&_)REr2p z)SAk=N>w-OAW1bW@v%AY4M$pfeMD#vB^w)G#SndAaIO#=bfPwC{+hDo zd1fJ8&$(NDnec}O%Ece3TnlkYK?wXbE(#*klc zHjFq$WYY(Gey)ogD@7qh%l({mak?H`hTn3SvU7T_iBL_4X}t1LmCeNw4y^g6-Sws9 zcu7==e4bv8lpo3KZT3>yv}i-hex9y=3uX6_et|3aYc%>xpSDjWhcDk~x~KPcGQw0+ z^^ww7H#yu8B?FssXQzKR8Gdq6leNfg8a>hxrl3xEkSVK|4D1`{td62K;~vJa=EX+q z6~(tyzJ5E*c8!|=a#w2Q$?hl1YC`+&;Y1 z6c=XOS&0DL}8r{0Ld1$DhdMrIa^OVljlN4X6@v?3LUrwR8Sf5*Bbc6N2 zPgXOgc`DRIs_=H)5hLY%llqEE7+@+U*B34<`<{nD>nofe%I9vv)MB+u zYQw{5?FHkenHD%n7NEna^9*+KdB#%LTiT6&Q6TWD2wHZJZnQB$O{MV*l9@*}&~}Nm zp3OtlyOsNRl(M~~?aVKW*j9(C&O<-qq>|d;ZSDFXmn2c_;s^ZhN>MzM%^hY1z#U7s4ml>3Io!Im ziqiDU#zbl~&t(@vS@F)rrUbD75>{CAU^_siCOSN6U=117se7S-3?J$1r#4ZD;OTJs z`Za21ZBBm&kV`Ts2WQk*;JS)GUuS|M4LsHhEkI5IH#l#cHl8~3G`GR7Oa%vH-=B{4 zsTojK6CsG9^;yEnSP0Cqrs>U7P2gSji!AR!)$YFDqq~SE3TJb^Aa`_MM5+PwbXM#q@ z^=j(TF^#k5$T(AIbUxxHJXB?*u|xJ-e8)r7`muI}){q#@XErYEX=&t+t`|3-ZR?-8wSHb%vF}PcANz zn240fmOklug$Y7;Tg;CHkKBD@>xagwpk4KbO{1?cY`&gK+$Av|S!(TnT*k+os3+4q zAW0Z;xd;JUBiw#X16d0C_GAg{$c|JxK23sUb2NvGJA4xgY$9r8j=oc^@MHf>HTZ4e z{n*K4ZycfWp}U;m85|k}WiKI)4mxzVcYja8ODS#qt(mV=p}=iPc&i08k|hBe_J$bK z`W1+{#?HHpy*%DLC}d+p$H$&m(a551Q$&Q^YTpuQUL&7-Yx+dO+_2A zGrKxwpsWoG$Z!+8)fGbP5L!-0qjTMn^W9RAFjO?RkvX?bAL?y_ES}>gW-q~nI-bST zyj!*74DEW{y5Uz7Z14(IBCsvmhB(k9p>ddo)CpJ>r$(niWa~!nKWaP}ym?={Sc~{B zF5WDSHA|so+lOgO!Q&uINn4A5e^yP$99EbB$2z+`gQxSdt!{1Bu~yf40lKZ19M7l+ z;CLw1lnFR~S@(LbyqOjm8fO`Lg1uU*g12>-DImmQ`nchBI)SR$YC--^|N>yIg$+u zSgU|o4E<+bZ+8I&fQm?lWK|@(*cChAA>za!=YsL)o}z?V3{It7ogUB@f6 zOaX#O8FTH4W|Bs-TYe1nv{jO^){_@hZ@DyHL-V$|ktFy;*9A6wvw->w=CY!i>GEl2Egkd|Gw0jSiGKf@_d{D$O`N z1V-Vs?ngp8oN`3AYIP*vn3AQwUH!l}4?}N-s!u=RjVjGky|4M2CaYg)Wr%L23IPOm zW~gmH9#d){Q>~j&Zk84b7l#X-4IuTBZVEW8c$R}|p&ZfA!X{T3P!Fi=1+*oGkVfDL zQ~n70DHz5{nEQ?>6@=6QQ=e5TQ7tF5&~Rq`u>+E)bV0UP_3Sa>_BT zCbnUv6`D(AkFQ?B4v9kChwdyNrX6ZTivI?|9D!+5@Pg_x01vW_pW#L1WsHf`ZqDibS7qn|dMkF@-9z)QxxRh_ndKe~f z49?6+?zeV$xD4uuAiU=I-OLcteAl+A6Oy<&6XPA0&7*e1 zTN?wb5)rJrZ}D;v$OK(E91RvlGZcOY=r2vQ9x7oY5e;!q2ST{Qr#9dVj#JF?1fS!; ze}Fn9ihD&IwCdZKs&DkO!Q`u`K{C$twhRp-H=~egF4w~kR)9<2aTM+_w&Ogzzf{k1xk12P?o<2;KSa;AQbZS(`N(iM8y z^bPV;0SU^rp3m=@!+bjFjfod8h&eL4=o1`Zvm;l9eG_=WIg{nry;hFk8RTRGk{dfP zLt0yz5^!QK^*C+Ng3a*4of)cVM2}qeM_%X4QY}w3i%r7K`KOHCVSx-<=J z((f<}(a~SVcXa2m%&We<6x99}1s8^!cS#>`fE~_NO-w0Ow^wvi#6!_w-x`S#ldGNV zLgE#PB%*|bGn!l+J*Z`Mqh-u9TG5@Cgi>4Pq1acAs1w=szK7Y1z}k9FQYrq!*x=ya z+`GIOS3&@`$$exD+HJm>c0(>aAWR%_WlpY=K4UpLk(sf3Cw+rQ0+>VbJDj&1hNOIg zKjCc+e=}+Zc}?`uy$G_FEbYO0S;`v~y=3T^ajtO(bhQivWYwT{n}9g)w?2^+!G}-1 zaMZF6IM2GZ?;5UCbm&Zdl%Rr)%vEGJLlaAxO$>|k56T0+XY4e|tuM);y@7Qe*w|ee zF66lZStDUG3d*5u-4a+WmND7^Wh~!*(|V>`Hcdh#Ta6dF+6oR4Q30=BSikXB=fFlx z7sw6cscr|tb;|k1H5pezg=o@Lf$nCI{%hZUgWmLM;m@)hjqa#QK7yp=YM=n1W&r$) zS3nBf7fm2o?`HH~SP2ciB%zwB2hh}eVSC`h1gGJVPg(ym*zqsoDv!pseRtNG-*nhSg5h2*07Y_r&*+N^0}>iol?9D=5fU&mDos&aW$T`;&0vb%aseB zmZigp05;P0iZ59CSxSRffUnpjo2V)#H=`HjRMsq-+c+EQGMq7R9{w`QWNs*^w>dkY zcl0P>@T4`<@4pCVZJp~>+fDj0>);FTZZ<-Wf1zW2^eoS7t0zeW7TL)ce!xgR+S>CE zW3FM!O(@}Rq@qTTVq6QUY`u^f^0(X);VSAtx!=MJJ?t{DBtk!UUUbM9TnHY98E7EO z*G%e=Qw&s+#gUGb(7+^GNFdO!(J&|;1~$cBymK#^oi`sWp_TGsmBz&Mjzh->rmChz zVm2mm9hkH*ooXVn2dO2l$h)OQ#4tv4tS~z&N~vC9NZn#x$sw-T<5ML_%acG$?ljRc z!et5HG4yGLjO|LxPPEcwD$P(OeUg3SpY0p+;EblEb!Iz>hVYX!jg`aUi&a>dN4YqY zDVYP>3=N?<=6BdTz~q}C2qE(5XI{SFVE3GYyjet31UHjCYf~WQYQIEow7VHKef#{$ ztiC2>0z zGSYOI13y&PXkiC6VlhEtnICds9Fy7^KE#ttSEKlDbsX-^w_^73I{Qw2i`<4GLrTw* z?ojKf3LZJ>#c5~ zeQ~CX05m(1d@9g#i-@Z#Sn+1G7p7)&zHs0|>9J50V(v(tVvvpR&qB|_c}BTMDx3jd z``NOU87pZY-H}?916sHxI6?s!)<(ZH*nr2<%Ga?4OMJ_bJJ*I(Ph&glaB7$rP7 zgB5~+_GDaC1Onnwj4FKImU=M_^kU5$7amgjqd3-KOp5l%q_J#EiusM}*UVkVxn`(FJ~-AyQVq&5`b5pSqT~(i?pUt5myC zoU`JDA%l=$tg`%R0F`s364R7K!dipm;L&bqL}%$8 z)2&m`p?5%daw=}?Lu_Z)G2lPoUX(XdEpl)?tgT8I?o7W} zFRk$Nfp!8<5=_!oO8d)L!zu(~o`CKym0p)XF##)Gptb6_KRg>qIUN;!hbz*AM(ga8 zjySlvs7Mo7H$R1iF0*=c08r7Z$5TU>sJ_Hki`oOgG(ET(#pHv};!ZN~jWnY<{^&P* z@n5+AkQs8MHqfX4iB&KDy)Z<0VshYP_rVPlFA(bm`tWHf)4khlp00@V^FHZIQj?Er zmX$~YHdm{4Oh?bjWY^|dljs0IgI4DzI=I~TZr zu2@PO^V2KXi+a$+&cq1+^a`D@J3v$l*A1|U0B&9hyavb!?0$Oqc;sDRWNKQU8ZvgC zCC|J)x-lL&{{0c2R6)Gz4>+R>ima~QD3Rill$j^Az^<4jQ9p_t;xz=LQ;qKM(Vq4e zs`@+xEFWs;g?CFmr7*TaMl9X!#~`quTD>w!;hsVo78mXC6+uJ}f_@4q*fPmlXfMz3 zb#`d2LT7r>>cun_PR40BnjxpzC<-X_c%hh zLk>RlDI(T*NpTEbFd(yr+->ZX5j zrLi%yL=U}sTdujk7Df%`AY`Q}T|p&e%DJML!;&Z&(q={f_}AB0#i{7f{lfd=K9idg zSEpx_O|=9+vOn1yq(I6rJh4+1`Ys0&!8PWrnBs!q8s)ChcmMKdIbic;g|8Lbt0T%a zuWz&*i1Wbp5OaZ++Sv43qTuFX=whUgR2n~>!7y~%Ct27W!=J2P1`KYDJE^EV_B*v` zIyA9bL0&+A#C*{KjC+}iiynKhlCWM#$_kk8k4k6}WkOw(<6grjl3xh{f?DYPvKmHu zQZsm4b?I*xm=*gqwPsA_+uJ>0pSoE8=T!dM<;J#*1fkhwNEwjv{d02EBoYg+>8%ZN zz|Rv^72)H!Gje}wJs*6T1I-JjB48#2fShm6mt)78hwG>3Wycs6T(srAsILOrHCTKE zOm~bVDm->2$dZOPrV{Muy{#zSnh6R~aisQ12R_9) z)RtMvhDUU1-$`r>F7^zxoH6RDn!&}E+h0ckrwTu%_KAmNcRre7vjn4l8xNa1NIA-L zQIsg332IhnRZ*|}q=kU{=%lIMpaYS`^q4JVgb(S=fk13tNG{kP9|5emdkn-Oo8|gl z0TFkTyihqmxTf%26&k!*BhE+4<2AoGoJW+x}cd>-4@08%D! z%5r~oS`rqChY!;%tF;jvJ-bUGoIZan(My=X>4;<~58PV>iucC)VGYM8^0h zDTE2?OxKs(kHJ*jjEhN+@ID}XJZvO>hJp`-cWXSUMB-2%7cv$aAztD>`)Yt*4RnUs zQm^7M^pIAnx$Len#8TE)sa7w$O_r;gGv3C)=`>oX6B#iBGp^m&Y5+HK@o{S2(sFcq>@pgPEB^v9S ziLCgwBuc$PLgPganBF=H0jZ7{<*BGfq1a7kH_BH=j#+j{Pu|>*ee)~qP8r@%)fLTv z;`ruA)03jOOcl`yBof??CgM+CB`JCW7hdg%?l+_Op+6KzlQ4UL$yG2S!@UJcw{dg) zJFId#*7bV}wdm>F!NTyB5wY!iTHCugTyy>BV<%a~P>Odb=B94UpA;^>CuJ|TO3Uj5 zV}t0n!|)61f`NCeGbSSbT)gUrq2HS1517ERs!cJbA27-lp8dE_d8%I0=w6l#IidBn zs#UVCtt!4nwlsesne9pCQ*~MEz#4d-;lc6jscGFdVVI8)=%TQNgD0IY(V>}Mc;~Mk zEqJICJUOm>&FbLzB0ch9lA6jK_Vr{NR3Hbe`A4tr_iBwyQ zOux@)_tS_#I%MSq>wsRcQqrQ)8HwR1a=5hDx5!$fp*l>63mkE82n#i(r!6fT?avz5 z>i1djY?qV_lNd_jc;;NoqD_@xUWvWy^hKn%<6-X8Bg+3SqjG2P!@v&;;Gh1>A@-3ClD)cWtbTU?t zzLoBPi_`sJ^m03ambJ&QU5=L6gk4{oP+F7Vb%*yB(l*6Q#qA>M-116<)f2KP{3FP0 zunXleaxRIt;m6z_#3wDpA7GCS^{MZ z$So-9ud)!i!hs8Ia=adqBM_Fg3J^!03GqeDa+zCFcl4u3s4N$+&VBG+Sp|F9yI};D za5n)~mU@DAwaZc)LzeIW%r{6d`p}CBHbF@qEooRxnCrR*PZD2{uWB_LhIUcM>BONp z%X4-Y_w|_AUehNP-=%${@*geiRUkD(n51Jhv5Z-+OI;s(uk~qerN;JVG$n8vcmzN9 z6J{vX{p~1L8!-&N{?JW|uC(yDVJ{y*8>~>Ux zH0ahg0a*mtUh;O7ikK=$CKUqks|WW*9FB$1-PY(#HxLf2-7qBi(Qi*)&gL8Fe74Yd zI#;b+Jc+k?@3((hoOuD$<9;u0gmqXv!M^G7!kb&l-AFqsX$Hl$EyPvI4!u10S%Q&1 z#eA?(`fe$ma#}>P6T3Ahx2oE_5z&(*8Qi3=IA4P6TlA?K>~)6Ysn&PdO4x;%L<-f&T1k)=oyY z16n5)7`U3JksQR+m|o(aTR>|A$A&y`oW`BbZv_$GYe?ffL&Rqa4*+}}IRBePdOm_{ z9wK2f_oes|{6^oCf!DdC`WY#=Lq^(9*O^{R#q7iOZJenOROh_uK5=8@3BT`Ii%N5D4%BjUgA_Ui~n6?MLs4cfpJP3yAIm&_HwA1aX zj5ZOPcY`lXN-s)jN1<_@>cE~7VejBLI<;ZU3ap&fsaw!o1E>UX2eYn;qc%TO1rbTe z1v!nmaB)@Nj|q;IRa$;&m1z`fs4K(JFI7C&{BEHLE@62OtcM|>>v5?wmJJ#{+M~0e z$Znuz*vO|0e4FhjK#JA_M;v@VxtaovYDGPgmAc`kKjOD-P#fGSFeCD5#oqUxl?u)pWAN3OB9_MSynMYY_J1? zBNxSYUdoX6baM-H$eit^U!+y(WG*e<0Be(}ejmyk0iPdon6t?i%>5J%Dxt%V5lmfs zkjhQn$286$|wVRfCETxSOU`cmD!U@u?#V}K$(D)ui*W5SCqk(t%=1s(jp=m!L z(aX=XGGQ--7f~uER2k7ct_?CjV0#X${)mt9$)~V=g$;3O9=Tk@uUA%OZxbX_<7R9E zEKdxFvtQ3`M6yNS-(@-gF;+CBaJcSCO?Hc!WVmgd3$;`+qYNICm`qyRd1&#a#xeX-s;8`Gh;V$R`|H)k^gezxlm+!V0AF zAHJY2w>rAt`T#Lm-VvLrwP3xwa=%p!@w6>qaW9P4xAzOcIdqN|DWTjEs2|xSrdE~c zBiMy~U^yH&eaUgVzr}*{(&yIa{o2UsE8jz>=&-|pi+lXhU6V#Opivi8@|5jpd4#2V zr6SI*uRN-pqMP|-ln|ybnc0%EX{sIwzc-v*JsFrm3z>`r9AD~DZC2G0+Fm>J(CJC1 zA|d-x9#^3EJ8o~M0>YJvhLdl1x`b5@N$G1aR`@ho1)qP=wGp-7VosUb_AZ{B^J=B+uRHolD(m;a*gc~~m4Yj4_4U2D3LT;rKdZI0#tG{<&Rowz z@fpHqI9a+V$plqt_5#Uw?GFWd7bYj(TAc?G5}3#|gY+TaxMV3?D6QZnG}A!(yiKv> zFTjhXlnaRFv4d_v47;tq?c0w>hjiyzasFeaAN9yO{3e|?V zAVL@gQ#XMz1Xyvpq)V>v?a|>b|Fr{qOkVkR3XoKJyv#`=I`UzWAD|c8Tt;JP=QgVg{On5GibIJo`brBQLT6< zm-8;q;rZRvsFr)I#BKakV4@$}8=3!nybo z`~*N#t%UpF;%2-W@jCoogiCP3na>R3$;5>xophWrm|9o}JS){$yKplgwcZKDNF;7B zr3J2!KKtMBhr4`#Y|nw&fO&LKb>BY7^W=WSGIDN9--J|M6EIj}~BW3vmf?Yp;! zhB0XtJ25E3Q{AW(zoLDUBO&+I>(cWO^*z!YGWQfa?45npomu;v(VllxS^-$@m$Y~8 zjAAvYfG8hPiB%~MlG$Qec0FFkK$HZ~{K)q_6#NFq5D>5_XE1;(xjDMkON-bpzkW6p z=~jXaAtZ3jvwXs^x5dB|bZCzC0Am{M_c2^!u6DI;GoMpIR)eL)cXr&$yZfrbiXm2I zQjt|l$OZf_UEU~qjZ`oMN}Xov>yco7VsxusKw7D?-#78LscRA-XuL{aEjvScNN=h8 z`odkl{R?Rb_|2>BH{T`~=JNoVp}ugRl~aCfh_s-~5?b3?<~aI;fPFwQxNH@4gLWUb zX3 z3!RxRjdoY=J85tZc+77bbWV1I_VeP{i2c0@wd|I`J5K}|V_2&KER9ND4QsJux$k!Akaq}8{%v8>JO=Kw%v}+^xo?&AJ8bM41^!vDqelm&Qv4fZ zdG8L19;zE33#;9M+05Ry61CkYBQj}4 zGV6KqYUI*))qa^VhA0+CN5d`<_VxyfiPO~@^3rh{tbsuVef!2%#?}#%RGZrrI+>Ka z77xKE=5t(dkA6kB-y@gzVL$8a_<1p2gyTFXQ6K}uI0lnoWuyp;C4mi8D=17u3v#X3 z`Ho@|FNv~MCiVk1VfvVG?Q*}v?_OqtG>uQ7U#}U%j})szJf`+SlmMKJge zB!9jm;OB*y9NG@?%I9EgS~Xg$9z0mav3!C_t%8zRMAa-i{aV5%wf%+?F?B0O+r8`I zERP9_i00CY(gEd2sq5H3nSZSFkBd&jM5Fw;m(SOcFGkjx`413Z>03v(acf$pUDdrk zsJ_3DfBg+Z?R|yQOsNpk#}DheeQthDK(d@!lU?T%@c5r{{u2Q}?3|mk1B~m)D}smg zd*3lrJiU#Rhzu<~K`U}!KJ?`}n%8a+YKr+$`=DU&{M3-YO9Gdu%9jbW)aKaw$&lXH zH8xd){+;3aCw1RTOQ~cf*=|PX#BrW;1$lQ=LSNzsJvX|yZC;5<`(Kuz!JU4N?>B@Y zJW$AETRm6M-Q6zi>~+c+u>+*fpRfsCnQtJSEq@lCty%b5yT+ zw${V7EVm)#u?U7BK-in%e0_z)CKgLXNF8!Q*#9Nf|Cr0A{1<}xBuXt$9Bw$HhPGQP zbPdh;deY4~`6)K#0QMVY>H$7%|MeGw4TOzH@V(3a>CgRy9Il$I)j9ZL2#P6r&5Vg8%wZ!`I; zty}(Ka^K}VSSfX7ieQfOew>d(ds_TU`MvxzDGG${ zf&Qg@bd6pCGHOl*yhq|!Iq8Bd(F6Q47s$=&kIm|cD`#cYY{YzJ|HA5dRt@3Xi4F~x z+CkPetzo!+@w@XK5S9@C(>mgRv6o9ZWB$#?*MtH#p_re1wqB+r@ro-9We@&fyPZYa z24SJshC20qG|T?WAOF=-UO~KJ>rIqQ5m8@9y*n~kVATUBA1AC%(+qMnJ=s$aaYZ_@ z7bC&HHib|9U)q^SR7Si8We|3%4DIpW)LJhGyj45bl{sYmFO3A|Cr00Hh>QkILm}s! z6PL66h46nQ_On%duT2#c3|n)?@;sEPSwWC-vV2zkaI;Ln`rvsGhJAbfKbhDD&5yfc z>6)Dq2fT0B*!E*YTJO*O7gJ8PFcL=T4ny<}Yx1~3WvYlo7*bgR;GwD&hzZ=pV2tJx zs0uYd#`z1K|1wU3b>52B0>(70T+IbkcVpG^S3cj)6f=Z%Bp^_TH_^kTOO4Cx}^|B<9Ly zb@?fVMk&DYrtgTNy5&tcQ`zP=FWI8sgU*|2RB2iHF<1W1NY6l4JuQ|-)^yef76+1-H7vna8~BKP(X z5N@)tV=Vr}BLKVtx<-2l01)nq1#M6A z=1q>Savb4ycG)1a3^@J^D7ws>OPGkZLpWtNUs=Ku{6o5BAhJA%gr)zb>O#zTAw->R z(3W$nv8v2J(HrJq@yUCly?*fIY@x(zxeyMqTS zVq}Bg#|BXO|6i`)v6J|paoIm*_{U)DpD_O2@@lx(h&qZYaS8;^yGvW+Z24W#?pJ}l^}HnX`$L zm7|N5gFX3Uy++0kt}a59l#eg^>-Ps^E>`A$zscU2`6rjmCJuHik55?Gm|0o=sv9UO z_;{CJN6pOH!PfP$eqnYtZWcDdpVa?z|L=GGQAYRwqs)Kb|G&%pT}0E~%0-y{&)584 zxBo09$nwbQf3f}tu0QVrzyd;L`Ae!lAN_^I|EoWLv-AJz4M4rWU-C=f{)X!}T)%|C zFA@LNuHSI|5(2+O{9C(z!}UuD{1WkR?fMPZFCp+t#J{!cH(bAjz%LR1)~?@h{SpGd zMEqO3e#7-k2>cT9Z|(XG*DoRPOT@pm>o;7#gupKm|JJVGaQzYjzeN07yMDv4E3Tf2V4^-Bo+67g^C`VH4FA@EDY zzqRW(T)%|CFA@LNuHSI|5(2+O{9C(z!}UuD{1Wm1uXZ8*ohM;t4EpS@8xOo5oFsyG_-P^W*d|1p&mAOraHR@xWf_EygpU?0~23P{v0jzJB8^HM%xc~9_t?nB=to;A{WBosA;ky8U*Z>p??fE}x ziUR;ZivR$C()EAR7zF_UY*YYXvdhKX&HTUS;ND_b8yf)N_A>y0t`7jV7CPAiq^>GX5rrTf7Tz|ZNX?JcuXN|<_8g%BZfQQy9-|x<#NRZO zvFot*9-;vx)Pp|GbsSqXSaco;&eku__rL1ScdP}FswCI=p7lZH6q#ZV3mnD7Z4aXE zUg{ms`W~;|&srr=32}@~pIGgGynU&cyMchGwq`?fD>9OfT$!~LV@JElJ605xqv~L_ ze_~N`jLn=ZUnnwFe)ny>FtyY@qqa;ScTL~IqlT@o%+S*25M7wBd?D*&`7lm60+-xV zm(v4jUH)UH7#r30wCf7Qw@BE##@!*~(!X%F$>ydjD?A>1ARCNv@g51>?M#cgc5YxB z12!RPw-#3By4&(L7?lsn&qbp5mh6yYo%zGHl(A23 zSoeX+#??4m*>xBBF_7g0E3{C3HYs|qdL%!VsN{;3IYl2(1 zUED>nlgibd?SosSAN}k=lJaK~3gU_$!)vP>7Ybb_(_88bLhRdNif!~j=|`@=26gbU zT;r!4l%WQX+uMVGKX_1-XIeyQUhQ$1sy=R`(Qf$jd1L`;Zpg;NZ#i@^CMgdABMBIa2-txdHX5eg84v@}`DW43ZEa@JMiOjGrf1i; z8kT0YM;5lKVEHh<@}k`(dxhF++xdG0!jIg(@(b(WS^M3qi1w

wu?V{9 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

|-_$#nxLyI|%PKVl;VB;Di8!#HItL_lu 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%0AMuin)z<@;@oB)>lL~N~ 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

jI<1lou?1k!LyBkVl87BK!vk6PM}XJ(F{NJ6 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ïL{ zW$$pb?XIdq$UOX&KI8p3Isbg%DriC7c7DJC&qRal>eV-%Z>=0D8zO5e3s^+`vV1YR zC^8eCEMqiCf+I7NH~5U_CiACum5Ie zaTE84YL08I16mMfHHKg>XDZPx@@U9}KEx)8Xe48qbHbo8qtPd6#ttq6v5#;O^2(Y@ zFfvC6;bndHNT$M`u*8tLklVbDi3cVQGytM~ zIp)yIDVxOp)?%J?-K=7G7UP0JBNqr}s1hta1aB@2C<5NX$OL+}p>M{@&Wt@fr?(cY zY(HX+d=JIz^fqpt22y+2_;g{X>IA_l^z=`5D&9Y}f}K`Uq~AFM2Yeslk%v3l7fvP` zGI=BhTgot%zT44ioN9r5ZZXYNQ9GhCavlx6{Pz1sg2IX3LFH~5Bu z0!!LqS$kz!Uxf4pX|Xua$a3?g8v{OF_-S(@sCf8HW(Sn-HC+~g>r#}ieK2R>fG>ocdjv$If*iFDMpqX*TKn>^ zMV&KTXy7Hh7e7Fb2w#qkfQ5XK$Xe-W;)j=?qVMTQ0t6u4-QqdUv5y`Y)c+`ZSLB&< zWQ5>{W?YK=XBjs>T{!B{7+$vG->bGn$~V8cuBs}Ix@>E3z-7>70|v6uK#TNvX0+U4 zKUaN=_0*i){i5)*PV}5_fsb8L8R&+*T&cvu$ZHO4BCH3%!=O=mj*oVy7ih@yr3E(t zj2;-&{@t}cv#zg<2XaKeV!yO2xb_n9>B3LWOh~k9gCUtB_A0!rD)1r&KaISB1HKe$ zUO_==__WQ|qvla}#O_5h!7+?Ze7DNzV@UHvYxQLDf*P5;m!9Zeo}m}DR3b(;z^Vqk zOMpQmb&d_0UiM(Tpa9O21@VNcDZvq)2i+OX4A07h+ zjprhfjJQ0fPO$VO8>_t5;h;D*Gk$^P5^|MQcUu1 zc`R$J&P5a4roy5$89&;4^y|ieqg2f!=N%D;rk__WC>N&k{M6-Ea97xDd(}6(r%3r{ zfR(#VDO~BGQ8?g$DBRJ$bd+ew1d)m{W5xjdrRoJiH&f-GE_>Nd9WVf8&UkYs;t6E9 zYLN$1CwLe%66=$+&8Mp+2oVbeM5PWmTii=rpRoV#2G0>G3yI9GC&LQd6z>hX zpfMmDbITg(#}#Q6wplpf+zAOC!z7AC1W_aCQ_WPDWb=>9?lw%$^19w|UnzTG!3X5G z3|Bk;^9?u(z^K#fF$p}cCCzGk15m&5#k;f`!@(4%wJK&k_;;NW%!3L_w^^*!4 zbFaYK_1HuWb}Y?^ zBWewb*Qt-r^{PtJ$cik!(44E~1?fg-3 zE3XEX`-uF0q|6w}xCAP4E45bcXRJd8}~{|6F{8D(QfI z+A>g46wzM+W9r__ecBNipt4WedTtp}bRXnmhz3vjZ=uw{(S_5X{>~jZ;H?<=Bs-vn zGcD?)wT3)37P<}6N{Ne?nd4fMvfgAKxm&$nnJZaZw@WsL z$8pgN4w;%7J;>isd9i{HAqrvJY5Q=%d6Exyprxfv4`2F)w0gSCai7yWBPrcx7QZuC zjY2`fpph?4_>_S(0~3($73L>*IQT-6WjYR+NP(oiOI@Bzx?$oOW)@e~{6X*i2R~GP zAaBZqF2Anmha4d(TM>R_6S7FgdJV`(tEV;YvX8uUcicOx#|iPmpm9OoQIN-(xuiOy zyX-Rdgrn|m^Bhi{!-Lny77R0*^8ia`DM@yqp1S>?kMDuiyJnJHK@SaBE{RlZ(BE4` zmoMpH!v#3te|~ceH^OF3i!f4=Mg6ubvgj*EjhN}WJ!~&SZkADcFC+{aW%NlZ8(2GJ zm(hUrO@Y`X-H=N}9dOPwOJ5xxT4~zl2P=>TMfY7WWQQ`4Es|^efNuY{$D%@pxFc@- z$Uh~jGAWtkLA2zwqDmeRavYj$<5XfrHNot;^+l_ry)Ne~La zQki2F-Gq4A5*e|SZZa`MYb)tbx%2f7*w<(Ow^`Q(uE-PjJ6GY*Pz|@XYJtCqR*hao z^YUtlfhEg%+{`f6$P77^8K&)>SD&7PQ&g7l$b4$jnM<3kI+aX)J2%{Lz<=h4Z1j(( zfMi6_!k>O2WuD^Zh0WpvM=(1JJLj;-PnVNs6vvu^AMk3-qvfYXmBJPGXjuXhGhj00 zXNHlJ85%k>d|uUuH`ILcmb%XWZ-3UipKE?O+HmiA=gaxC{@-q~_u|L;vG}sOg28+S z$aNPT$Fmz-F+q@p-k8@~%WuK2)gzy57|WA*z?tFt`lGsn8c7XOY&3M$@@`(%;#yi) zoj-8ErH;QUX061{3r&jPh2%_ik8g!~j|T-(@YyKvSuU#Jx$3;SDl|{Lelf$Y2OMzE zhnJcBB30y^>meWV-h0k>-g)N(4%pRqLCMkgS=zm~ z2Mbz+V)w9u<>})l($wt?TcHA=cL0Jxql-RCsg5;aOS>S*qQVE!sN{og0)z{Dxntz( zK9g9b^CALXrT0=aW>e%pb-uPGZ|T6 zZ6->d$fs0Eki705K)x^xMRT;{iO?0Ye$uG%cm*t*zCxp3d0g5*rSrx425;JEKLCr= z!C{0%hCO;z9r;tWt60Q`L8G)fqlI;3=F`*(Ai8u5AYb^)hcK98(Z~8pqk_$M#8!7q zDX3Q>U5XuV`cSC*rA9CQECw&lG2lZ{W&aK-ALPOqn+lZ?X?PTX$s8;4*{&WPpoa-i zI3+%1)lqQ0&!h+EktocTC)-2>z&Q*NstYf{0G|linH#x%`6720a(z)ai$rkQPEN?^Yon=&9!Lq1vf)g#ZHzXwFb3v{x6mPfXPA%8rIfg9gc9X*WzcEIn@$5@lw@ zJcT%VdxD8$Z-gI6A)8f#rK~0B%*wQnBls4sLlT?{C$sBUt#`#2TnXKf)P=v3(Oj0{ zH<~#TDocf2CNW}Y3l!8k)M@ph?}{{9egg}u*&y1EiL9kyz$vidqnw+V*7ORS(mJMa=Yj~?6KU|f>V_Vd0& z)L8IJlaEmM;Heta&wRN=be=gQZl&3c)=(yilb?3RIpze&IadQLFslgQAn1?6=Vcv~ z&`E7f)9w+_X5WWp{KPeYnC4$tJ^W={uG4r(f;!Y3Iz?id`D)<7OH;CsFv4w(Iak)7Qg=@=Ea3YX9 zf@z|5xyr#f1UJD%((CO$aBdum*1l92!yGiM9q(JWjiifM;KU#%28nxG!@#9?#`+b_ z|9Zk}f(@Oj(8f^gPW7#XYp8N{tf_li5h_z6RH{Xk`7*J{9Kc73 zTMdEuG|_!=U)Hr65$dP9EvvX=2Jhh4O3d=;py%;>^(?Y!!h)iD z|9417{pg^2Qr5=+aJkioF85HR>K+WT5FB=P9_%Js%R+6A6u9emt)y#iMiai-cYu|0t)6Ty z7??PlBavNshebB9fBPwO-vqE5(Yk!?A>9|fGuPyAID#0-;SG6rC)q2qrAH&(i`B)m zFRWGIMwFxH(9lWn8{vswb%QZ|6lN=zmX5y?o#RJ|J$^3X8dywuqO@Lyv4epS zGXn-JZ{N`ZFtT>WNAAO%SbE-8*W>K~DNL znAoo7${Ois>i4r11+IzY(W|^~hFOIb_v~Tt@WyZEf)0Yg)NM@u#{dwacH|Cs=TY!+ zp-XEkcQc9s%1Vt*IvLC0^&X_o;hW6DPEIi@}1O|+5xU$|hL*fO_>!S*S| zYg!w;mxZduN^ke?fq=8V?#ib!V%4`$#TF6uqAI3w2Cho)5ojP}D7+`!iw`@aEsYpm zK+v*0Niyb^%a9OzA9rfR{yZ7f=i~c1Zxd*Ik{jNuwHWltf*3eD4PK4(Hx3S9g1Zp!{P^1_K51-GVmcNW@IBd9WcJbs zR18t%hj(HZX}0Wa89WgH&Qkkq^V2Z zVgRK54<1S~AqXB136d_aG>r@|K4JU{YF?Lun9Q1MDAIXlIw(g zFut%8?G5f754t;qbxTf%=C7+`zl3$kj(+~-FG(jgKmWL2^SzgP6yrx;eJ@9JLsCY) z;UOdw-X;RLn>1FUvQE<{(-=hVAuayPLDM@$-M*s=hxvgabhp|>@@l-NUVjIfB@dX1 zr>Du>CLw{Rl_vtNmGIEppk_Zyxm?dBN(W#b586C_s5XnC$XBFsEKO`iOx={e5;8w4 z7}bv^%>I)gO{p|hXxnfd8NI{K!LaaDo_M57;230+{)N$yb|oa0*5pB?pdpa&{yjF3 z^hJ^xhlfx_P+w@9Phf9P0!4+0%eT-&^M_n18wi#&SZr~XTYlBAy~tu7y2!>-GsqN; z8x}K4OdmLda+?lmM?G%;Yehe;9byr z#DtzEaHL#xq{e5xuhGx^=%gQas>c5Y{~cSlMhr!dUIAs)?A)8m8km)jR)a}Jd=k;bS{H_fJi zS#^g7;YkXkW^)BR&0cHo>8zfqbmJ=^@ydcbmEsY-I@cRi(LwM6V$h$2Xe+6>W_hPd z@vmkO*&T0eTW27jYy|I50@_1MHGUIr_U~~~gJhTT#em~LuLalHk8rs=vJcY4rZ;JF zs^6-EL?D9+70(YqMt$@}s2}DDS=Xb{bN)zs1~jnY&lPMot)(M3fSr|HnE+uvs6fbrlTDiIz(}d5RrEIOoOSf)G}B~G(YfSLz5;nCYox$@4_wU z;(+RjXa4prLQFrR5}O1sM=!6|RV>Ke)>^h{dik>1>W2vY+dTAUFJCWQ@s(n39KrCV zf_T8j8+i*7jCi%;M!KPpM-)t+y{fH|Jc7wld`RV?W^6(em5K~i(; zkIW)|$w==J9~F~Ni*|T;c*D8$9jh7PkL-T$))h}RN9Q_p)#oaXyh1o&%2_tR<--Q^ z>+36+K7{|!64Y@UX5BFY<42;blGQeh3J}=O4&wTBd_#OJ|1q&-V|W#TV+M_j*wMe8 zc@Vi?T}aX&15vHyp^dqGx>Lu>S^0)rbXLHs6B0BbQF|wY7a3?GXFWJD@L2!yI(07V@rHHQbPW?h~^zxYX`n1$dCOD=JxXf=XTcIh_@0W?e@CW_P z%%nFkrsMlh-N+It(9LM&*7~Nn%h|Oi4w=Vp5^#315|$qL;!2Ad!qp(p;uF#xe3jwC zy``kl%Y;4mFrb1*%jzSiOiVLd!yFg_nE+0PambJZD|or!Xzf5S#2fj=;b zW=IONMSgeEsQ3t&t#8nG89lQ+DXV_IUxdVB-U+YFke;nvxwRo=1F<_qC>tPTe=D$- zd=A)fAwToD;c&_N!Bm2F!`2WGQ(t4KI5|FBJanCXi%@J#~Eed&%UG>w;4HnOG$0y(wPJ7>HOw?;m1eHX4DA{jXbNNljPW7mpO>n`& z>zHvdZWikfySSJ+UWasOMbK<+w8;fvS- zGG98#$qVH*T-3D)p<&jjWw2R*KW@F_&|5BNt0=#(+FI!w8`K2$EVuT*!EWs9*xW2D zx(5(T7_1+Fmw`tLY4Z~Cti2tpYYaBO>An+ha3`HH@Jp`wjTP8;SnOhfSsuk0NjHO; zMfZDXBM@?PA!KJI2td+P3*?koT|z$C)C-*U9q9J!T*P1FZgH8?R!?y19vC!Nx`JkA z2x|S-LQ-B2?I*gKOSGwEbPIc6% zJn6?3Uy7satrH6BbyxYz1UDiOCiU{-WrGIYl*X01&6C-P9fnFptC=)9ze#>PtI3ezjAO~4hZPm z!dhUiP`e}HJC17)%(sx>9VIOQR|c$fx+lD8)rlat zP-kJ)mQjS}D|~DOEd~AG4rRCm%q$Q8Mh~bGxf-Nm6}{cZ#t{E?g9@J+u^<7)`BpQ; zGJU?^`SqwC7?E@H0|*6(qArP6!I5I$bS_2QW>;e3@?i*1BhCHuX(599N)k^M#rUI? zp_q4l?#=$xZlN9p(`o{w@9cW}>Y1uqWo4I3)w4PwpUyLkmeL4s*`o-o_w|rK+TWwN zy?lLe*=RFZCD>Wa1~a^L7jiH_XeXsTk!BiTbIHUX`U}H0_jD|q6RE|#{%axVPSmiUX6z87Du>{?Rt3+y7gQO(UYCg@6)lZu&iG8C z4x3GcBL1D?c5a>UA%Z~A!Vg-xLR~yy5^ZSuZMmi62Q6^rULVsu)>^J; z$P71OxyCU3tK2%USFsg55HyqgDt^R!co*;K$*i#*^WJc=;^1i7&aGE!JIZ;iR-!y~zN&VBt!N5I4MyBr5^ zA02+eo3!_9le(esPVMKZfPoUoi+rDuguw}|mMI!wYwuQjH-)p3(8O_kDL#63J_5mD zhpY%<_+*?WU)c$nea1I=U6R^ChmS-b37=TwjPn~u<8kh~n{aS)Zcr^yT-C`uIkK@- zL(w@f9|N-@X)+1z>Qk@}&wa>Uch|zML%nPK=#Tts;d!eN9Q%%^6X`p)wL)+Xv}rh_ z&4pqpywD5`e-Aj67@uaSoC#QqZ7CE67zftTve9~)*0E#CItH%R8vSWRZ8ej1<HFlfJp18;PNgOw4G5twL}v1s041Ibx3TMh3Ow0`O;!<2b&^BjlwX#4k` z?iLNz`fxyxEJc?lGkC-zRq<*Xr)}dAoZRIdi(yR!@1)o`LCa8lOc1*Ri}>ht)f@ z#f*CsaxQwaysV((Vb4QP)vi$jb45Yl3x+gzZTAr(G#uwK{8d>Z)Ir#hTwYbQQ6b z+)tHxRM#to*VtAR`klZQ!ZZeg*{4p7{8k+zPrrb=-b$*)qDdY(E7CPr2XMB1hqeOt zpZoSm*Wl=CnbR)>R`j-HA;LzKtSP)?rw54nt%-d^^E7ALGY1h#$7)k*jIto8z!uC^q#(5s!5!D6iP_9T2ZCQt|#$YJm;b3 z|3-7;13TbUVU@pus+MdrWNzU|(YqqTG+UR9J{*fNbK631yj4~tTK{=e3fOOB`~CEF zRxgI~$x$uo(?G!8mt_n!&?9>dWHk(~{M`tTs4q-Lr7dO}H&BB3r3yT<+dj1a_)w^S_ zGzrb+8?@m{IR7HOQZ_@rs|;jt@b=8;E-|Xdy<)oj8b? zh>rxrt&5MUK}Bz@dpDsE%O=O(`jF=qB$O5*5*EsiYRI+f)#~8H(uE?zZ0o{gt2M02 zsqXr<@l=d295{WOVtpdYO;2Wdeuh!$hbOS0!%tk&obWks&d+%N;hsR{k5y{uPs=kQ`R^D`jQGG43qsx4-O0Q-Cn8zMcP zIt56u++e9K-1O7SdW+$ZCJ{j}Ac6-~!N@@jDH>hYn ztST24`X}HXh(a21w=1pqF^8wma2~vu)Z@i;dtgzui7Jrp#B5HH*4$%wY%T%Cej3>= z8IGX|D{$m4z4&1=kce#dPYQ5@-~^ZMIPr_gjEM)kLQOpsl)o0;mApXR2^U@$J5nwd zk2De^y#*8Tp-}8j&HUL}G;%VA9XV3^qRMLL9RM8-K5{2UbdpYxb^JtC6_Q`-DtV;l zDt7Ew3B0_K%Rf*JEz|vQIZSeg=IEoCXp}vU9-^#hq~?(d^!&v=j@Cn5A~!usD)-w* zQvoi7E=Mj8c;pD-^(exk7_*WROyF1?!9kPKj>ZwsV&Y(0UZAL!Qgj!jT`}yQfbYTX zI*(SJZSwYSAP0gUaj3Lme;qE{(fb^4eg&^daQ1atNFenr>0yC?Nj`zMz)oLdB+zTF z=#y#zYpTkd4j%UU11|yt7kGN0M+5k#4#u#L&fO}L-$LC)^*>AVjlq}1Q>qBiU~`G# z8vIb97InHaCRMK$O`bx`UYz})3(+A?u^m6i9G3rulJU)1A~;1kc7e}V#L=ipYcfnx zhR6dVQ%Y%UB-=#zHi}j|S!|a3_F+1aAqkpMw9!b>zH#P3u4N9jebsNB^DbJu@smbI zk$3K{`fNgerpK#f-(Uk~bH2TJCg~b_&9z`2(|H^8x^!5#(86pQTWY(?;eHi=9<5>a zc+4{X+>M$p0$r2-z}kVdqy&+rhkgr1$t2Ata^1U!i#(LO!t7&xE*d24Z;Np%taI)V z@U4m*ca<7~s!qq6M#`TJ-4Kc$%0jK)K^So?>k&kMtk2zzmy`M#Kgc#nd>SkBMzEo{ z#ME{f81Jcr7DMol0u8f2&7maRy0Yll0_>@=1;A*3L`iDls4!gGjLaiOc|YF*(wk}- z=LoCv(3brZ2W%&$ur%LOENa$9D(n765lUwPYObAF7pGy*QDnX?Q&Tj<%@>r;4(9_K zEL!R~d3)liU7*(YQr)O_Zv8Ap;l~1Ahd|@p5+PoaX6iYzTtwrs-H_1ZNWgQ9v-Rdo z2um=$6S1Vvl0q)|Ii#*PDZ4wuJf?)$Uuo$&QA)A*ai1-N7cPC88wi>T4jcJSw^WJ} z{s-6Ysi45Wn#rRzm4Ln#unbOvu|%P=TwH43QWkE96J4^P6uO6nKr`7X7zf|Onr(7I z@YB>)$uyg@*c>`q+)9wH4Osf2Kv5m9hz}iWC2M2i2fs#mj))sF)!i8Q^xU|1>Y98Qm|jm`xB%AI!N${s~V${0Ob@6xS+v;CQpvQ}?A z74K^kqf}Lv`Wade0TaGe?cgdi_+Fki@DbjVnhvb&LB<>(xM`XhE=(Svvs&D6?HFs7 zF>?px7qnrY3tvJ0MO%X>Yczg^UB%x-nq{O$jM*-(+rSTZ=kycmkeSB0xnumD9zqFN zJ!h$tjKcA2xrt)n*KNVJhpQ%2$D{7b#y{WtOg@c46PbD~GPewo7eY=roFJict!iSQ z3tAxK#V9`U!k=G?%buV?J1u4u0S2EUQi)>LT|SC(IJ>L4MP9L*(?W)WV6X4wy9lhT zp@W}X__Q_Vw)i_?T(^aRcnSBS9teTOH4n>k~K=GUbw}jo;vtBulp;JK*rX!D3?7#l* z#-vd=bZGZ#VqXd}JLq_9^=*%;=k4cU1&S}0nm9&O-dCe9@!-G1Sw^Kgsx``h(V^k^ z#bCGQml9q_f8=ZUyYbbC-8N#rOFv+G*16>mbEEjw=6rr!_!}}cdCu(VDNIn}kX@FA zv)XrLXHE1Hr%QnIJD$Q8)=QmaCAllQ^Ksdl3y@2ftF9Ut_QTPpgdsX~N=XE31H#g` zHa-|{{XFf0RId;xWt>smrrFabwpnc?5F2#>r{Gft0e1_fi6m)u0l&mH>9#OI%_@Gq z9#V0PzG3_l!5w~=R|{Vtv`@5aolH<2{h6=fh1(@TT+3nP?3x1d+ldV*K>~|kx4dWr zB5d=Qj;~BUrBPC5HxLF-{MItNtRmT}`?e4zipmcD*E3$^zXZHrFOkSapZ$rQe5k4bmrHA@DJ0QjyVh{eTgfd^qiZLiOJOCaHi_~*5%P)H(&};9$cm9IdcY4U zGD{oIDnn-PaCUJvX;>z_Ou5EVFPGokk*|2QWCE+I=CbRr{xQ(wX-2MhR%A%ATN4vI zTpgx$_UqXfKPlvkF!X}o=_b!!^An8DAMzv;vWXr@AeRwVqao+2pHTX%e(wwt z3FgY&lK&N$gm8%p}>W_vNwO+3^(;@NLpjm%%{Az8Q{gv56_TM-3~U8-{de$ zJS4G{5jlpMz@*@^Y-h|lXD3{Tf+S-@*r${Ds*)*{E}cX@N>Ssa+EV+%Lz^M=KN@|?l!-iKLxM&4 zvyjr=W2#VGEKv9D{kee<*5ETpo*15-%h+~*&O?SvO% z2P5xPk11Y<>&%yN8LYeZLH`KNMe$kZV=N49BQ z!bh)0ug1$5q@IUcIaX1SotFg&sw;0}?xmv!v6V1JBr%)Y$8 z3Gbclwh&RMezLK?0md^ot<>B3l~9>-eQNisZ)!v0(`dxly4n4K+uoZ}uO|B++3tZc z73+N`5s!Kod`}bSmuA?xdb@2PnJb^umClFTku`7lksIiZicdNVScGlKwWxV}tM(mxD<$>;&8MuUO}Q%8 z`FMKBqQ!1~*6v2l)h9*cmtXWZBsGwHWHNE|#9FsKPcut?gnIYR$%D5Jl{oIs(>e$9 zWvP>gw+lg^`fukONWs+sMRwbn5o)utV66~>4m#HOuPz-i?EV>c_wLc;uC1S zu&LfVPoaIeQTka|=liM-yd_k19qURz}>$WP$>Sd~Q5$Hug3q&ITlIHrBRIJZ}7C2F7-V zK!^q>p1;M6WF-F(akk>N!=G*_P3*dfBt!4&K9Qs>B-iK;U8DYU}R^*`1c=1W(Fq4f42=(<@;O8qoHKtWM}R2 zw|xN?W)4PXzJEynU*-Sl>z{Qr{~vY!d-?yY^PeiJwieC;EdT1}e-!_#79Zo^OZ{(` z{!gy|Cjl@d2s}&pMY~>bJ&VAz zgkQAl1=q6(JWKdRyIycTi@>vlU$pB5*Ru#bOZY{*UT{5&z_Wy3wCe@evj{v(_(i*3 za6OB_vxHx?>jl@d2s}&pMY~>bJ&VAzgkQAl1=q6(JWKdRyIycTi@>vlU$pB5*Ru#b zOZY{*UT{5&z_Wy3wCe@evj{v(_(i*3a6OB_vxHx?>jl@d2s}&pMY~>bJ&VAzgkQAl d1=q6(JWKfht6hly@slvI1)L*5fENVBe*yNNS;_za literal 0 HcmV?d00001 diff --git a/explorer-v2/public/images/placeholder.webp b/explorer-v2/public/images/placeholder.webp new file mode 100644 index 0000000000000000000000000000000000000000..2c5e4325eeeb5840184fecbc18840f1b42fb89d6 GIT binary patch literal 47094 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�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

)Dm6Uci4^BZuP`esr0 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%0ALbQ9ipF+atd2^9CzBWKU*LqW5qxGzMPYgMmu{Z^ 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

    Vi$G9LYZ8*+Im3 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

    %#{YJ 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 UU7Wx7Ng#b`n \ No newline at end of file diff --git a/explorer-v2/public/profileImagePlaceholder.png b/explorer-v2/public/profileImagePlaceholder.png new file mode 100644 index 0000000000000000000000000000000000000000..bbeef84ed00905d134a9f76543c59d425a38a781 GIT binary patch literal 3832 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 uz|G*_l)$TjDsj_l;LYIQl)$TjD)B#|XTNK?7W \ No newline at end of file diff --git a/explorer-v2/public/window.svg b/explorer-v2/public/window.svg new file mode 100644 index 0000000000..b2b2a44f6e --- /dev/null +++ b/explorer-v2/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/explorer-v2/src/app/(pages)/account/[address]/not-found/page.tsx b/explorer-v2/src/app/(pages)/account/[address]/not-found/page.tsx new file mode 100644 index 0000000000..2cbac6434d --- /dev/null +++ b/explorer-v2/src/app/(pages)/account/[address]/not-found/page.tsx @@ -0,0 +1,62 @@ +// import BlogArticlesCards from "@/components/blogs/BlogArticleCards"; +import { ContentLayout } from "@/components/contentLayout/ContentLayout"; +import SectionHeading from "@/components/headings/SectionHeading"; +import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton"; +import { Box, Typography } from "@mui/material"; +import Grid from "@mui/material/Grid2"; +import Markdown from "react-markdown"; + +export default async function Account({ + params, +}: { + params: Promise<{ address: string }>; +}) { + try { + const address = (await params).address; + + return ( + + + + + + + + + + + + This account does’t have a Nym node bonded. Is this your account? + Start [setting up your node](https://nym.com/docs) today! + + + {/* + + + + + */} + + ); + } catch (error) { + let errorMessage = "An error occurred"; + if (error instanceof Error) { + errorMessage = error.message; + } + throw new Error(errorMessage); + } +} diff --git a/explorer-v2/src/app/(pages)/account/[address]/page.tsx b/explorer-v2/src/app/(pages)/account/[address]/page.tsx new file mode 100644 index 0000000000..d426de30bf --- /dev/null +++ b/explorer-v2/src/app/(pages)/account/[address]/page.tsx @@ -0,0 +1,67 @@ +import { fetchNodes } from "@/app/api"; +import type { NodeData } from "@/app/api/types"; +import { Box, Typography } from "@mui/material"; +import Grid from "@mui/material/Grid2"; +import { AccountBalancesCard } from "../../../../components/accountPageComponents/AccountBalancesCard"; +import { AccountInfoCard } from "../../../../components/accountPageComponents/AccountInfoCard"; +import { ContentLayout } from "../../../../components/contentLayout/ContentLayout"; +import SectionHeading from "../../../../components/headings/SectionHeading"; +import ExplorerButtonGroup from "../../../../components/toggleButton/ToggleButton"; + +export default async function Account({ + params, +}: { + params: Promise<{ address: string }>; +}) { + try { + const address = (await params).address; + + const nymNodes: NodeData[] = await fetchNodes(); + + const nymNode = nymNodes.find( + (node) => node.bond_information.owner === address, + ); + + return ( + + + + + + + + + + + + + + + + + + + + + ); + } catch (error) { + console.error("error :>> ", error); + return Error loading account data; + } +} diff --git a/explorer-v2/src/app/(pages)/nym-node/[id]/page.tsx b/explorer-v2/src/app/(pages)/nym-node/[id]/page.tsx new file mode 100644 index 0000000000..84daa3c76c --- /dev/null +++ b/explorer-v2/src/app/(pages)/nym-node/[id]/page.tsx @@ -0,0 +1,128 @@ +import { fetchNodeIdByIdentityKey, fetchNodeInfo } from "@/app/api"; +import { ContentLayout } from "@/components/contentLayout/ContentLayout"; +import SectionHeading from "@/components/headings/SectionHeading"; +import { BasicInfoCard } from "@/components/nymNodePageComponents/BasicInfoCard"; +import { NodeDataCard } from "@/components/nymNodePageComponents/NodeDataCard"; +// import { NodeChatCard } from "@/components/nymNodePageComponents/ChatCard"; +import NodeDelegationsCard from "@/components/nymNodePageComponents/NodeDelegationsCard"; +import { NodeParametersCard } from "@/components/nymNodePageComponents/NodeParametersCard"; +import { NodeProfileCard } from "@/components/nymNodePageComponents/NodeProfileCard"; +import { NodeRoleCard } from "@/components/nymNodePageComponents/NodeRoleCard"; +import ExplorerButtonGroup from "@/components/toggleButton/ToggleButton"; +import { Box } from "@mui/material"; +import Grid from "@mui/material/Grid2"; + +export default async function NymNode({ + params, +}: { + params: Promise<{ id: string }>; // node_id or identity_key +}) { + try { + let id: string | number; + const paramsId = (await params).id; + + // check if the params id is a node_id or identity_key + + if (paramsId.length > 10) { + id = await fetchNodeIdByIdentityKey(paramsId); + } else { + id = Number(paramsId); + } + + const observatoryNymNode = await fetchNodeInfo(id); + + if (!observatoryNymNode) { + return null; + } + + return ( + + + + + + {observatoryNymNode.bonding_address && ( + + )} + + + + + + + + + + + + + + + + + + + + + {/* + + + */} + + + ); + } catch (error) { + let errorMessage = "An error occurred"; + if (error instanceof Error) { + errorMessage = error.message; + } + throw new Error(errorMessage); + } +} diff --git a/explorer-v2/src/app/(pages)/onboarding/[slug]/page.tsx b/explorer-v2/src/app/(pages)/onboarding/[slug]/page.tsx new file mode 100644 index 0000000000..07eed3224c --- /dev/null +++ b/explorer-v2/src/app/(pages)/onboarding/[slug]/page.tsx @@ -0,0 +1,155 @@ +import TableOfContents from "@/components/blogs/TableOfContents"; +import type BlogArticle from "@/components/blogs/types"; +import { Breadcrumbs } from "@/components/breadcrumbs/Breadcrumbs"; +import { ContentLayout } from "@/components/contentLayout/ContentLayout"; +import SectionHeading from "@/components/headings/SectionHeading"; +import { Link } from "@/components/muiLink"; +import { Wrapper } from "@/components/wrapper"; +import { Box, Stack, Typography } from "@mui/material"; +import Grid from "@mui/material/Grid2"; +import { format } from "date-fns"; +import Image from "next/image"; +import Markdown from "react-markdown"; + +export default async function BlogPage({ + params, +}: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await params; + + try { + const blogArticle: BlogArticle = await import(`@/data/${slug}.json`); + + const breadcrumbItems = [ + { + label: "Onboarding", + href: "/onboarding", + }, + { label: blogArticle.title, isCurrentPage: true }, + ]; + return ( + + + + + + + + + + + Author + {(blogArticle?.attributes?.blogAuthors?.length ?? 0) > 1 + ? "s" + : ""} + :{" "} + {blogArticle?.attributes?.blogAuthors?.map( + (author: string) => ( + + {author} + + ) + )} + + + + + {blogArticle.attributes.readingTime}{" "} + {blogArticle.attributes.readingTime > 1 ? "mins" : "min"}{" "} + read + + + blog-image + + {blogArticle.overview.content.map(({ text }) => ( + + + + {text} + + + + ))} + + {blogArticle.sections.map((section) => ( + + + {section.text.map(({ text }) => ( + + + + {text} + + + + ))} + + ))} + + + + ({ + heading: section.heading, + id: section.id, + }))} + /> + + + + + ); + } catch (error) { + console.log(error); + + return ( + + + + + Oops! Looks like the page you’re looking for got mixed up in the + noise. Don’t worry, your privacy is intact. Let’s get you + back to the homepage. + + + + ); + } +} diff --git a/explorer-v2/src/app/(pages)/onboarding/page.tsx b/explorer-v2/src/app/(pages)/onboarding/page.tsx new file mode 100644 index 0000000000..9808a075b0 --- /dev/null +++ b/explorer-v2/src/app/(pages)/onboarding/page.tsx @@ -0,0 +1,15 @@ +import BlogArticlesCards from "@/components/blogs/BlogArticleCards"; +import { ContentLayout } from "@/components/contentLayout/ContentLayout"; +import SectionHeading from "@/components/headings/SectionHeading"; +import Grid from "@mui/material/Grid2"; + +export default function OnboardingPage() { + return ( + + + + + + + ); +} diff --git a/explorer-v2/src/app/(pages)/stake/page.tsx b/explorer-v2/src/app/(pages)/stake/page.tsx new file mode 100644 index 0000000000..0ce0d012ed --- /dev/null +++ b/explorer-v2/src/app/(pages)/stake/page.tsx @@ -0,0 +1,24 @@ +// import BlogArticlesCards from "@/components/blogs/BlogArticleCards"; +// import Grid from "@mui/material/Grid2"; +import { ContentLayout } from "../../../components/contentLayout/ContentLayout"; +import SectionHeading from "../../../components/headings/SectionHeading"; +import OverviewCards from "../../../components/staking/OverviewCards"; +import StakeTableWithAction from "../../../components/staking/StakeTableWithAction"; +import SubHeaderRow from "../../../components/staking/SubHeaderRow"; + +export default async function StakingPage() { + return ( + + + + + + {/* + + + + + */} + + ); +} diff --git a/explorer-v2/src/app/(pages)/table/page.tsx b/explorer-v2/src/app/(pages)/table/page.tsx new file mode 100644 index 0000000000..7e6fb4b62d --- /dev/null +++ b/explorer-v2/src/app/(pages)/table/page.tsx @@ -0,0 +1,30 @@ +// import BlogArticlesCards from "@/components/blogs/BlogArticleCards"; +import { ContentLayout } from "@/components/contentLayout/ContentLayout"; +import SectionHeading from "@/components/headings/SectionHeading"; +import NodeTableWithAction from "@/components/nodeTable/NodeTableWithAction"; +import NodeAndAddressSearch from "@/components/search/NodeAndAddressSearch"; +import { Wrapper } from "@/components/wrapper"; +import { Box, Stack } from "@mui/material"; +// import Grid from "@mui/material/Grid2"; + +export default function ExplorerPage() { + return ( + + + + + + + + + + {/* + + + + + */} + + + ); +} diff --git a/explorer-v2/src/app/api/index.tsx b/explorer-v2/src/app/api/index.tsx new file mode 100644 index 0000000000..a89b17bc49 --- /dev/null +++ b/explorer-v2/src/app/api/index.tsx @@ -0,0 +1,271 @@ +import { addSeconds } from "date-fns"; +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { + CurrentEpochData, + ExplorerData, + GatewayStatus, + IAccountBalancesInfo, + IObservatoryNode, + IPacketsAndStakingData, + NodeData, + NodeRewardDetails, + NymTokenomics, + ObservatoryBalance, +} from "./types"; +import { + CURRENT_EPOCH, + CURRENT_EPOCH_REWARDS, + DATA_OBSERVATORY_BALANCES_URL, + DATA_OBSERVATORY_NODES_DELEGATIONS_URL, + DATA_OBSERVATORY_NODES_URL, + NS_API_MIXNODES_STATS, + NYM_ACCOUNT_ADDRESS, + NYM_NODES, + NYM_PRICES_API, + OBSERVATORY_GATEWAYS_URL, +} from "./urls"; + +// Fetch function for epoch rewards +export const fetchEpochRewards = async (): Promise< + ExplorerData["currentEpochRewardsData"] +> => { + const response = await fetch(CURRENT_EPOCH_REWARDS, { + headers: { + Accept: "application/json", + "Content-Type": "application/json; charset=utf-8", + }, + cache: "no-store", // Ensures fresh data on every request + }); + + if (!response.ok) { + throw new Error("Failed to fetch epoch rewards"); + } + + return response.json(); +}; + +// Fetch gateway status based on identity key +export const fetchGatewayStatus = async ( + identityKey: string, +): Promise => { + const response = await fetch(`${OBSERVATORY_GATEWAYS_URL}/${identityKey}`); + + if (!response.ok) { + throw new Error("Failed to fetch gateway status"); + } + + return response.json(); +}; + +export const fetchNodeInfo = async ( + id: number, +): Promise => { + const nodes = await fetchObservatoryNodes(); + return nodes?.find((node) => node.node_id === id); +}; + +export const fetchNodeIdByIdentityKey = async ( + identity_key: string, +): Promise => { + const nodes = await fetchObservatoryNodes(); + const node = nodes?.find((node) => node.identity_key === identity_key); + return node?.node_id || 0; +}; + +export const fetchNodeDelegations = async ( + id: number, +): Promise => { + const response = await fetch( + `${DATA_OBSERVATORY_NODES_DELEGATIONS_URL}/${id}/delegations`, + { + headers: { + Accept: "application/json", + "Content-Type": "application/json; charset=utf-8", + }, + }, + ); + + if (!response.ok) { + throw new Error("Failed to fetch delegations"); + } + + return response.json(); +}; + +export const fetchCurrentEpoch = async () => { + const response = await fetch(CURRENT_EPOCH, { + headers: { + Accept: "application/json", + "Content-Type": "application/json; charset=utf-8", + }, + cache: "no-store", // Ensures fresh data on every request + }); + + if (!response.ok) { + throw new Error("Failed to fetch current epoch data"); + } + + const data: CurrentEpochData = await response.json(); + const epochEndTime = addSeconds( + new Date(data.current_epoch_start), + data.epoch_length.secs, + ).toISOString(); + + return { ...data, current_epoch_end: epochEndTime }; +}; + +// Fetch balances based on the address +export const fetchBalances = async (address: string): Promise => { + const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, { + headers: { + Accept: "application/json", + "Content-Type": "application/json; charset=utf-8", + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch balances"); + } + + const balances: ObservatoryBalance = await response.json(); + + // Calculate total stake + return ( + Number(balances.rewards.staking_rewards.amount) + + Number(balances.delegated.amount) + ); +}; + +// Fetch function to get total staker rewards +export const fetchTotalStakerRewards = async ( + address: string, +): Promise => { + const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, { + headers: { + Accept: "application/json", + "Content-Type": "application/json; charset=utf-8", + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch balances"); + } + + const balances: ObservatoryBalance = await response.json(); + + // Return the staking rewards amount + return Number(balances.rewards.staking_rewards.amount); +}; + +// Fetch function to get the original stake +export const fetchOriginalStake = async (address: string): Promise => { + const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, { + headers: { + Accept: "application/json", + "Content-Type": "application/json; charset=utf-8", + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch balances"); + } + + const balances: ObservatoryBalance = await response.json(); + + // Return the delegated amount + return Number(balances.delegated.amount); +}; + +export const fetchNoise = async (): Promise => { + const response = await fetch(NS_API_MIXNODES_STATS, { + headers: { + Accept: "application/json", + "Content-Type": "application/json; charset=utf-8", + }, + }); + + const data: IPacketsAndStakingData[] = await response.json(); + return data; +}; + +// Fetch Account Balance +export const fetchAccountBalance = async ( + address: string, +): Promise => { + const res = await fetch(`${NYM_ACCOUNT_ADDRESS}/${address}`, { + headers: { + Accept: "application/json", + "Content-Type": "application/json; charset=utf-8", + }, + }); + if (!res.ok) { + throw new Error("Failed to fetch account balance error from api"); + } + + const data: IAccountBalancesInfo = await res.json(); + return data; +}; + +// 🔹 Fetch Nodes +export const fetchNodes = async (): Promise => { + const res = await fetch(NYM_NODES, { + headers: { + Accept: "application/json", + "Content-Type": "application/json; charset=utf-8", + }, + }); + if (!res.ok) { + throw new Error("Failed to fetch nodes"); + } + const data: NodeData[] = await res.json(); + return data; +}; + +export const fetchObservatoryNodes = async (): Promise => { + const allNodes: IObservatoryNode[] = []; + let page = 1; + const PAGE_SIZE = 200; + let hasMoreData = true; + + while (hasMoreData) { + const response = await fetch( + `${DATA_OBSERVATORY_NODES_URL}?page=${page}&limit=${PAGE_SIZE}`, + { + headers: { + Accept: "application/json", + "Content-Type": "application/json; charset=utf-8", + }, + }, + ); + + if (!response.ok) { + throw new Error(`Failed to fetch observatory nodes (page ${page})`); + } + + const nodes: IObservatoryNode[] = await response.json(); + allNodes.push(...nodes); + + if (nodes.length < PAGE_SIZE) { + hasMoreData = false; // Stop fetching when the last page has fewer than 200 items + } else { + page++; // Move to the next page + } + } + + return allNodes; +}; + +// 🔹 Fetch NYM Price +export const fetchNymPrice = async (): Promise => { + const res = await fetch(NYM_PRICES_API, { + headers: { + Accept: "application/json", + "Content-Type": "application/json; charset=utf-8", + }, + }); + if (!res.ok) { + throw new Error("Failed to fetch NYM price"); + } + const data: NymTokenomics = await res.json(); + return data; +}; diff --git a/explorer-v2/src/app/api/strapi.d.ts b/explorer-v2/src/app/api/strapi.d.ts new file mode 100644 index 0000000000..2c177527cb --- /dev/null +++ b/explorer-v2/src/app/api/strapi.d.ts @@ -0,0 +1,23417 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/about-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/about-page"]; + put: operations["put/about-page"]; + post?: never; + delete: operations["delete/about-page"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/about-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/about-page/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/basic-pages": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/basic-pages"]; + put?: never; + post: operations["post/basic-pages"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/basic-pages/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/basic-pages/{id}"]; + put: operations["put/basic-pages/{id}"]; + post?: never; + delete: operations["delete/basic-pages/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/basic-pages/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/basic-pages/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/blog-authors": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/blog-authors"]; + put?: never; + post: operations["post/blog-authors"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/blog-authors/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/blog-authors/{id}"]; + put: operations["put/blog-authors/{id}"]; + post?: never; + delete: operations["delete/blog-authors/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/blog-authors/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/blog-authors/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/blog-categories": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/blog-categories"]; + put?: never; + post: operations["post/blog-categories"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/blog-categories/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/blog-categories/{id}"]; + put: operations["put/blog-categories/{id}"]; + post?: never; + delete: operations["delete/blog-categories/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/blog-categories/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/blog-categories/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/blog-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/blog-page"]; + put: operations["put/blog-page"]; + post?: never; + delete: operations["delete/blog-page"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/blog-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/blog-page/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/blog-posts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/blog-posts"]; + put?: never; + post: operations["post/blog-posts"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/blog-posts/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/blog-posts/{id}"]; + put: operations["put/blog-posts/{id}"]; + post?: never; + delete: operations["delete/blog-posts/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/blog-posts/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/blog-posts/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/downloads": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/downloads"]; + put?: never; + post: operations["post/downloads"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/downloads/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/downloads/{id}"]; + put: operations["put/downloads/{id}"]; + post?: never; + delete: operations["delete/downloads/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/downloads/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/downloads/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/downloads-menu": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/downloads-menu"]; + put: operations["put/downloads-menu"]; + post?: never; + delete: operations["delete/downloads-menu"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/downloads-menu/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/downloads-menu/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/features": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/features"]; + put?: never; + post: operations["post/features"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/features/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/features/{id}"]; + put: operations["put/features/{id}"]; + post?: never; + delete: operations["delete/features/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/features/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/features/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/feature-categories": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/feature-categories"]; + put?: never; + post: operations["post/feature-categories"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/feature-categories/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/feature-categories/{id}"]; + put: operations["put/feature-categories/{id}"]; + post?: never; + delete: operations["delete/feature-categories/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/feature-categories/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/feature-categories/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/features-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/features-page"]; + put: operations["put/features-page"]; + post?: never; + delete: operations["delete/features-page"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/features-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/features-page/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/footer": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/footer"]; + put: operations["put/footer"]; + post?: never; + delete: operations["delete/footer"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/footer/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/footer/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/glossary-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/glossary-page"]; + put: operations["put/glossary-page"]; + post?: never; + delete: operations["delete/glossary-page"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/glossary-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/glossary-page/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/glossary-terms": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/glossary-terms"]; + put?: never; + post: operations["post/glossary-terms"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/glossary-terms/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/glossary-terms/{id}"]; + put: operations["put/glossary-terms/{id}"]; + post?: never; + delete: operations["delete/glossary-terms/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/glossary-terms/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/glossary-terms/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/home-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/home-page"]; + put: operations["put/home-page"]; + post?: never; + delete: operations["delete/home-page"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/home-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/home-page/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/litepaper": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/litepaper"]; + put: operations["put/litepaper"]; + post?: never; + delete: operations["delete/litepaper"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/litepaper/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/litepaper/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/navigation": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/navigation"]; + put: operations["put/navigation"]; + post?: never; + delete: operations["delete/navigation"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/navigation/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/navigation/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/network-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/network-page"]; + put: operations["put/network-page"]; + post?: never; + delete: operations["delete/network-page"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/network-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/network-page/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/page-not-found": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/page-not-found"]; + put: operations["put/page-not-found"]; + post?: never; + delete: operations["delete/page-not-found"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/page-not-found/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/page-not-found/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/press-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/press-page"]; + put: operations["put/press-page"]; + post?: never; + delete: operations["delete/press-page"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/press-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/press-page/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/press-posts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/press-posts"]; + put?: never; + post: operations["post/press-posts"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/press-posts/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/press-posts/{id}"]; + put: operations["put/press-posts/{id}"]; + post?: never; + delete: operations["delete/press-posts/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/press-posts/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/press-posts/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/team-members": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/team-members"]; + put?: never; + post: operations["post/team-members"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/team-members/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/team-members/{id}"]; + put: operations["put/team-members/{id}"]; + post?: never; + delete: operations["delete/team-members/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/team-members/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/team-members/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/trust-center-advisors-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/trust-center-advisors-page"]; + put: operations["put/trust-center-advisors-page"]; + post?: never; + delete: operations["delete/trust-center-advisors-page"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/trust-center-advisors-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/trust-center-advisors-page/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/trust-center-categories": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/trust-center-categories"]; + put?: never; + post: operations["post/trust-center-categories"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/trust-center-categories/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/trust-center-categories/{id}"]; + put: operations["put/trust-center-categories/{id}"]; + post?: never; + delete: operations["delete/trust-center-categories/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/trust-center-categories/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/trust-center-categories/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/trust-center-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/trust-center-page"]; + put: operations["put/trust-center-page"]; + post?: never; + delete: operations["delete/trust-center-page"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/trust-center-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/trust-center-page/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/trust-center-posts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/trust-center-posts"]; + put?: never; + post: operations["post/trust-center-posts"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/trust-center-posts/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/trust-center-posts/{id}"]; + put: operations["put/trust-center-posts/{id}"]; + post?: never; + delete: operations["delete/trust-center-posts/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/trust-center-posts/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/trust-center-posts/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/use-cases": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/use-cases"]; + put?: never; + post: operations["post/use-cases"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/use-cases/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/use-cases/{id}"]; + put: operations["put/use-cases/{id}"]; + post?: never; + delete: operations["delete/use-cases/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/use-cases/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/use-cases/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/use-cases-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/use-cases-page"]; + put: operations["put/use-cases-page"]; + post?: never; + delete: operations["delete/use-cases-page"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/use-cases-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/use-cases-page/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/upload": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Upload files */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Upload files */ + requestBody: { + content: { + "multipart/form-data": { + /** @description The folder where the file(s) will be uploaded to (only supported on strapi-provider-upload-aws-s3). */ + path?: string; + /** @description The ID of the entry which the file(s) will be linked to */ + refId?: string; + /** @description The unique ID (uid) of the model which the file(s) will be linked to (api::restaurant.restaurant). */ + ref?: string; + /** @description The field of the entry which the file(s) will be precisely linked to. */ + field?: string; + files: string[]; + }; + }; + }; + responses: { + /** @description response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UploadFile"][]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/upload?id={id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Upload file information */ + post: { + parameters: { + query: { + /** @description File id */ + id: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Upload files */ + requestBody: { + content: { + "multipart/form-data": { + fileInfo?: { + name?: string; + alternativeText?: string; + caption?: string; + }; + /** Format: binary */ + files?: string; + }; + }; + }; + responses: { + /** @description response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UploadFile"][]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/upload/files": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Get a list of files */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UploadFile"][]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/upload/files/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Get a specific file */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UploadFile"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Delete a file */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UploadFile"]; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/connect/{provider}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Login with a provider + * @description Redirects to provider login before being redirect to /auth/{provider}/callback + */ + get: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Provider name */ + provider: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Redirect response */ + 301: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/local": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Local login + * @description Returns a jwt token and user info + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** @example { + * "identifier": "foobar", + * "password": "Test1234" + * } */ + "application/json": { + identifier?: string; + password?: string; + }; + }; + }; + responses: { + /** @description Connection */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Users-Permissions-UserRegistration"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/local/register": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Register a user + * @description Returns a jwt token and user info + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** @example { + * "username": "foobar", + * "email": "foo.bar@strapi.io", + * "password": "Test1234" + * } */ + "application/json": { + username?: string; + email?: string; + password?: string; + }; + }; + }; + responses: { + /** @description Successful registration */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Users-Permissions-UserRegistration"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/{provider}/callback": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Default Callback from provider auth */ + get: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Provider name */ + provider: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a jwt token and user info */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Users-Permissions-UserRegistration"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/forgot-password": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Send rest password email */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** @example { + * "email": "foo.bar@strapi.io" + * } */ + "application/json": { + email?: string; + }; + }; + }; + responses: { + /** @description Returns ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @enum {string} */ + ok?: true; + }; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/reset-password": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Rest user password */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** @example { + * "password": "Test1234", + * "passwordConfirmation": "Test1234", + * "code": "zertyoaizndoianzodianzdonaizdoinaozdnia" + * } */ + "application/json": { + password?: string; + passwordConfirmation?: string; + code?: string; + }; + }; + }; + responses: { + /** @description Returns a jwt token and user info */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Users-Permissions-UserRegistration"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/change-password": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Update user's own password */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + password: string; + currentPassword: string; + passwordConfirmation: string; + }; + }; + }; + responses: { + /** @description Returns a jwt token and user info */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Users-Permissions-UserRegistration"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/email-confirmation": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Confirm user email */ + get: { + parameters: { + query?: { + /** @description confirmation token received by email */ + confirmation?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Redirects to the configure email confirmation redirect url */ + 301: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/send-email-confirmation": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Send confirmation email */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + email?: string; + }; + }; + }; + responses: { + /** @description Returns email and boolean to confirm email was sent */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + email?: string; + /** @enum {string} */ + sent?: true; + }; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users-permissions/permissions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get default generated permissions */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns the permissions tree */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + permissions?: components["schemas"]["Users-Permissions-PermissionsTree"]; + }; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users-permissions/roles": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List roles */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns list of roles */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + roles?: (components["schemas"]["Users-Permissions-Role"] & { + nb_users?: number; + })[]; + }; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + /** Create a role */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: components["requestBodies"]["Users-Permissions-RoleRequest"]; + responses: { + /** @description Returns ok if the role was create */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @enum {string} */ + ok?: true; + }; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users-permissions/roles/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get a role */ + get: { + parameters: { + query?: never; + header?: never; + path: { + /** @description role Id */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns the role */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + role?: components["schemas"]["Users-Permissions-Role"]; + }; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users-permissions/roles/{role}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Update a role */ + put: { + parameters: { + query?: never; + header?: never; + path: { + /** @description role Id */ + role: string; + }; + cookie?: never; + }; + requestBody: components["requestBodies"]["Users-Permissions-RoleRequest"]; + responses: { + /** @description Returns ok if the role was udpated */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @enum {string} */ + ok?: true; + }; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + post?: never; + /** Delete a role */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + /** @description role Id */ + role: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns ok if the role was delete */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @enum {string} */ + ok?: true; + }; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get list of users */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns an array of users */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Users-Permissions-User"][]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + /** Create a user */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** @example { + * "username": "foo", + * "email": "foo@strapi.io", + * "password": "foo-password" + * } */ + "application/json": { + email: string; + username: string; + password: string; + }; + }; + }; + responses: { + /** @description Returns created user info */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Users-Permissions-User"] & { + role?: components["schemas"]["Users-Permissions-Role"]; + }; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get a user */ + get: { + parameters: { + query?: never; + header?: never; + path: { + /** @description user Id */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a user */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Users-Permissions-User"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + /** Update a user */ + put: { + parameters: { + query?: never; + header?: never; + path: { + /** @description user Id */ + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + /** @example { + * "username": "foo", + * "email": "foo@strapi.io", + * "password": "foo-password" + * } */ + "application/json": { + email: string; + username: string; + password: string; + }; + }; + }; + responses: { + /** @description Returns updated user info */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Users-Permissions-User"] & { + role?: components["schemas"]["Users-Permissions-Role"]; + }; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + post?: never; + /** Delete a user */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + /** @description user Id */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns deleted user info */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Users-Permissions-User"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get authenticated user info */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns user info */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Users-Permissions-User"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/count": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get user count */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a number */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + Error: { + data?: (Record | Record[]) | null; + error: { + status?: number; + name?: string; + message?: string; + details?: Record; + }; + }; + AboutPageLocalizationRequest: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + )[]; + locale: string; + }; + AboutPageRequest: { + data: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + )[]; + locale?: string; + }; + }; + AboutPageResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["AboutPage"]; + }; + AboutPageLocalizationResponse: { + data?: components["schemas"]["AboutPageResponseDataObjectLocalized"]; + meta?: Record; + }; + AboutPageListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["AboutPage"]; + }; + AboutPageLocalizationListResponse: { + data?: components["schemas"]["AboutPageListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + AboutPageListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["AboutPage"]; + }; + AboutPageListResponse: { + data?: components["schemas"]["AboutPageListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + AboutPage: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + )[]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["AboutPageListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + AboutPageResponseDataObject: { + id?: number; + attributes?: components["schemas"]["AboutPage"]; + }; + AboutPageResponse: { + data?: components["schemas"]["AboutPageResponseDataObject"]; + meta?: Record; + }; + SharedSeoComponent: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: { + name?: string; + pathId?: number; + parent?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + children?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + files?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: { + firstname?: string; + lastname?: string; + username?: string; + /** Format: email */ + email?: string; + resetPasswordToken?: string; + registrationToken?: string; + isActive?: boolean; + roles?: { + data?: { + id?: number; + attributes?: { + name?: string; + code?: string; + description?: string; + users?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + permissions?: { + data?: { + id?: number; + attributes?: { + action?: string; + actionParameters?: unknown; + subject?: string; + properties?: unknown; + conditions?: unknown; + role?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + blocked?: boolean; + preferedLanguage?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + path?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + UiLinkComponent: { + id?: number; + title?: string; + url?: string; + }; + DynamicZoneValuePropositionWithTextComponent: { + id?: number; + __component?: string; + title?: string; + text?: string; + links?: components["schemas"]["UiLinkComponent"][]; + media?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** @enum {string} */ + direction?: "text-right" | "text-left"; + }; + UiBulletWithImageComponent: { + id?: number; + title?: string; + text?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + }; + DynamicZoneValuePropositionWithBulletsComponent: { + id?: number; + __component?: string; + title?: string; + links?: components["schemas"]["UiLinkComponent"][]; + media?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** @enum {string} */ + direction?: "text-right" | "text-left"; + bullets?: components["schemas"]["UiBulletWithImageComponent"][]; + }; + UiHeadingComponent: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + TilesTileComponent: { + id?: number; + title?: string; + subtitle?: string; + description?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + url?: string; + }; + DynamicZoneTilesComponent: { + id?: number; + __component?: string; + heading?: components["schemas"]["UiHeadingComponent"]; + tiles?: components["schemas"]["TilesTileComponent"][]; + }; + DynamicZoneTeamMembersComponent: { + id?: number; + __component?: string; + heading?: components["schemas"]["UiHeadingComponent"]; + subtitle?: string; + teamMembers?: { + data?: { + id?: number; + attributes?: { + name?: string; + role?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + bio?: string; + socialNetworks?: { + id?: number; + links?: { + id?: number; + url?: string; + /** @enum {string} */ + network?: "linkedin" | "twitter" | "wikipedia" | "website"; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + }; + QuotesQuoteComponent: { + id?: number; + title?: string; + quote?: string; + }; + DynamicZoneQuotesComponent: { + id?: number; + __component?: string; + heading?: components["schemas"]["UiHeadingComponent"]; + quotes?: components["schemas"]["QuotesQuoteComponent"][]; + }; + UiHeadingExtendedComponent: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + /** @enum {string} */ + style?: "h1" | "h2" | "h3" | "h4"; + /** @enum {string} */ + alignment?: "left" | "center"; + }; + DynamicZoneParagraphWithTitleComponent: { + id?: number; + __component?: string; + heading?: components["schemas"]["UiHeadingExtendedComponent"]; + text?: string; + primaryLinks?: components["schemas"]["UiLinkComponent"][]; + secondaryLinks?: components["schemas"]["UiLinkComponent"][]; + background?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + }; + DynamicZoneLogoCarouselComponent: { + id?: number; + __component?: string; + heading?: components["schemas"]["UiHeadingComponent"]; + isStatic?: boolean; + images?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + }; + TilesContentTileComponent: { + id?: number; + title?: string; + subtitle?: string; + text?: string; + label1?: string; + label2?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + button?: { + id?: number; + title?: string; + url?: string; + }; + dataTable?: string; + }; + DynamicZoneContentTilesComponent: { + id?: number; + __component?: string; + heading?: components["schemas"]["UiHeadingComponent"]; + tiles?: components["schemas"]["TilesContentTileComponent"][]; + }; + DynamicZoneBlogPostsComponent: { + id?: number; + __component?: string; + blogPosts?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + subtitle?: string; + content?: string; + excerpt?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + blogCategories?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + blogPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + description?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + blogAuthors?: { + data?: { + id?: number; + attributes?: { + name?: string; + bio?: string; + avatar?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + slug?: string; + blogPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + role?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + relatedPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** Format: date */ + date?: string; + readingTime?: number; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + heading?: components["schemas"]["UiHeadingComponent"]; + }; + BlocksBlockComponent: { + id?: number; + title?: string; + description?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + }; + DynamicZoneBlocksComponent: { + id?: number; + __component?: string; + heading?: components["schemas"]["UiHeadingComponent"]; + subtitle?: string; + blocks?: components["schemas"]["BlocksBlockComponent"][]; + /** @enum {string} */ + type?: "high-density" | "low-density"; + }; + FaqsQuestionComponent: { + id?: number; + question?: string; + answer?: string; + }; + FaqsFaQsComponent: { + id?: number; + __component?: string; + heading?: components["schemas"]["UiHeadingComponent"]; + questions?: components["schemas"]["FaqsQuestionComponent"][]; + }; + CarouselCarouselSlideQuoteComponent: { + id?: number; + quote?: string; + source?: string; + }; + DynamicZoneCarouselQuoteComponent: { + id?: number; + __component?: string; + heading?: components["schemas"]["UiHeadingComponent"]; + slides?: components["schemas"]["CarouselCarouselSlideQuoteComponent"][]; + }; + CarouselCarouselSlideTextImageComponent: { + id?: number; + text?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + }; + DynamicZoneCarouselTextImageComponent: { + id?: number; + __component?: string; + heading?: components["schemas"]["UiHeadingComponent"]; + slides?: components["schemas"]["CarouselCarouselSlideTextImageComponent"][]; + }; + BasicPageLocalizationRequest: { + title: string; + slug: string; + dynamicZone?: ( + | components["schemas"]["DynamicZoneRichTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + SEO?: components["schemas"]["SharedSeoComponent"]; + locale: string; + }; + BasicPageRequest: { + data: { + title: string; + slug: string; + dynamicZone?: ( + | components["schemas"]["DynamicZoneRichTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + SEO?: components["schemas"]["SharedSeoComponent"]; + locale?: string; + }; + }; + BasicPageResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["BasicPage"]; + }; + BasicPageLocalizationResponse: { + data?: components["schemas"]["BasicPageResponseDataObjectLocalized"]; + meta?: Record; + }; + BasicPageListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["BasicPage"]; + }; + BasicPageLocalizationListResponse: { + data?: components["schemas"]["BasicPageListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + BasicPageListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["BasicPage"]; + }; + BasicPageListResponse: { + data?: components["schemas"]["BasicPageListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + BasicPage: { + title: string; + slug: string; + dynamicZone?: ( + | components["schemas"]["DynamicZoneRichTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + SEO?: components["schemas"]["SharedSeoComponent"]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["BasicPageListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + BasicPageResponseDataObject: { + id?: number; + attributes?: components["schemas"]["BasicPage"]; + }; + BasicPageResponse: { + data?: components["schemas"]["BasicPageResponseDataObject"]; + meta?: Record; + }; + DynamicZoneRichTextComponent: { + id?: number; + __component?: string; + content?: string; + tableOfContents?: boolean; + heading?: components["schemas"]["UiHeadingComponent"]; + }; + BlogAuthorLocalizationRequest: { + name: string; + bio?: string; + /** @example string or id */ + avatar?: number | string; + slug: string; + blogPosts?: (number | string)[]; + role?: string; + locale: string; + }; + BlogAuthorRequest: { + data: { + name: string; + bio?: string; + /** @example string or id */ + avatar?: number | string; + slug: string; + blogPosts?: (number | string)[]; + role?: string; + locale?: string; + }; + }; + BlogAuthorResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["BlogAuthor"]; + }; + BlogAuthorLocalizationResponse: { + data?: components["schemas"]["BlogAuthorResponseDataObjectLocalized"]; + meta?: Record; + }; + BlogAuthorListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["BlogAuthor"]; + }; + BlogAuthorLocalizationListResponse: { + data?: components["schemas"]["BlogAuthorListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + BlogAuthorListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["BlogAuthor"]; + }; + BlogAuthorListResponse: { + data?: components["schemas"]["BlogAuthorListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + BlogAuthor: { + name: string; + bio?: string; + avatar?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: { + name?: string; + pathId?: number; + parent?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + children?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + files?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: { + firstname?: string; + lastname?: string; + username?: string; + /** Format: email */ + email?: string; + resetPasswordToken?: string; + registrationToken?: string; + isActive?: boolean; + roles?: { + data?: { + id?: number; + attributes?: { + name?: string; + code?: string; + description?: string; + users?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + permissions?: { + data?: { + id?: number; + attributes?: { + action?: string; + actionParameters?: unknown; + subject?: string; + properties?: unknown; + conditions?: unknown; + role?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + blocked?: boolean; + preferedLanguage?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + path?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + slug: string; + blogPosts?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + subtitle?: string; + content?: string; + excerpt?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + blogCategories?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + blogPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + description?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + blogAuthors?: { + data?: { + id?: number; + attributes?: { + name?: string; + bio?: string; + avatar?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + slug?: string; + blogPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + role?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + relatedPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** Format: date */ + date?: string; + readingTime?: number; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + role?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["BlogAuthorListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + BlogAuthorResponseDataObject: { + id?: number; + attributes?: components["schemas"]["BlogAuthor"]; + }; + BlogAuthorResponse: { + data?: components["schemas"]["BlogAuthorResponseDataObject"]; + meta?: Record; + }; + BlogCategoryLocalizationRequest: { + title: string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + blogPosts?: (number | string)[]; + description?: string; + locale: string; + }; + BlogCategoryRequest: { + data: { + title: string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + blogPosts?: (number | string)[]; + description?: string; + locale?: string; + }; + }; + BlogCategoryResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["BlogCategory"]; + }; + BlogCategoryLocalizationResponse: { + data?: components["schemas"]["BlogCategoryResponseDataObjectLocalized"]; + meta?: Record; + }; + BlogCategoryListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["BlogCategory"]; + }; + BlogCategoryLocalizationListResponse: { + data?: components["schemas"]["BlogCategoryListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + BlogCategoryListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["BlogCategory"]; + }; + BlogCategoryListResponse: { + data?: components["schemas"]["BlogCategoryListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + BlogCategory: { + title: string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + blogPosts?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + subtitle?: string; + content?: string; + excerpt?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + blogCategories?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + blogPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + description?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + blogAuthors?: { + data?: { + id?: number; + attributes?: { + name?: string; + bio?: string; + avatar?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + slug?: string; + blogPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + role?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + relatedPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** Format: date */ + date?: string; + readingTime?: number; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + description?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["BlogCategoryListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + BlogCategoryResponseDataObject: { + id?: number; + attributes?: components["schemas"]["BlogCategory"]; + }; + BlogCategoryResponse: { + data?: components["schemas"]["BlogCategoryResponseDataObject"]; + meta?: Record; + }; + BlogPageLocalizationRequest: { + featuredPosts?: (number | string)[]; + SEO?: components["schemas"]["SharedSeoComponent"]; + title: string; + categoriesFilter?: (number | string)[]; + featuredCategories?: (number | string)[]; + locale: string; + }; + BlogPageRequest: { + data: { + featuredPosts?: (number | string)[]; + SEO?: components["schemas"]["SharedSeoComponent"]; + title: string; + categoriesFilter?: (number | string)[]; + featuredCategories?: (number | string)[]; + locale?: string; + }; + }; + BlogPageResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["BlogPage"]; + }; + BlogPageLocalizationResponse: { + data?: components["schemas"]["BlogPageResponseDataObjectLocalized"]; + meta?: Record; + }; + BlogPageListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["BlogPage"]; + }; + BlogPageLocalizationListResponse: { + data?: components["schemas"]["BlogPageListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + BlogPageListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["BlogPage"]; + }; + BlogPageListResponse: { + data?: components["schemas"]["BlogPageListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + BlogPage: { + featuredPosts?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + subtitle?: string; + content?: string; + excerpt?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: { + name?: string; + pathId?: number; + parent?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + children?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + files?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: { + firstname?: string; + lastname?: string; + username?: string; + /** Format: email */ + email?: string; + resetPasswordToken?: string; + registrationToken?: string; + isActive?: boolean; + roles?: { + data?: { + id?: number; + attributes?: { + name?: string; + code?: string; + description?: string; + users?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }[]; + }; + permissions?: { + data?: { + id?: number; + attributes?: { + action?: string; + actionParameters?: unknown; + subject?: string; + properties?: unknown; + conditions?: unknown; + role?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + blocked?: boolean; + preferedLanguage?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + path?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + blogCategories?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + blogPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + description?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + blogAuthors?: { + data?: { + id?: number; + attributes?: { + name?: string; + bio?: string; + avatar?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + slug?: string; + blogPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + role?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + relatedPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** Format: date */ + date?: string; + readingTime?: number; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + SEO?: components["schemas"]["SharedSeoComponent"]; + title: string; + categoriesFilter?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + featuredCategories?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["BlogPageListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + BlogPageResponseDataObject: { + id?: number; + attributes?: components["schemas"]["BlogPage"]; + }; + BlogPageResponse: { + data?: components["schemas"]["BlogPageResponseDataObject"]; + meta?: Record; + }; + BlogPostLocalizationRequest: { + title: string; + slug: string; + subtitle?: string; + content?: string; + excerpt?: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + blogCategories?: (number | string)[]; + blogAuthors?: (number | string)[]; + relatedPosts?: (number | string)[]; + /** @example string or id */ + image?: number | string; + /** Format: date */ + date?: string; + readingTime?: number; + locale: string; + }; + BlogPostRequest: { + data: { + title: string; + slug: string; + subtitle?: string; + content?: string; + excerpt?: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + blogCategories?: (number | string)[]; + blogAuthors?: (number | string)[]; + relatedPosts?: (number | string)[]; + /** @example string or id */ + image?: number | string; + /** Format: date */ + date?: string; + readingTime?: number; + locale?: string; + }; + }; + BlogPostResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["BlogPost"]; + }; + BlogPostLocalizationResponse: { + data?: components["schemas"]["BlogPostResponseDataObjectLocalized"]; + meta?: Record; + }; + BlogPostListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["BlogPost"]; + }; + BlogPostLocalizationListResponse: { + data?: components["schemas"]["BlogPostListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + BlogPostListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["BlogPost"]; + }; + BlogPostListResponse: { + data?: components["schemas"]["BlogPostListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + BlogPost: { + title: string; + slug: string; + subtitle?: string; + content?: string; + excerpt?: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + blogCategories?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + blogPosts?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + subtitle?: string; + content?: string; + excerpt?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + blogCategories?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + blogAuthors?: { + data?: { + id?: number; + attributes?: { + name?: string; + bio?: string; + avatar?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + slug?: string; + blogPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + role?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + relatedPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** Format: date */ + date?: string; + readingTime?: number; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + description?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + blogAuthors?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + relatedPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** Format: date */ + date?: string; + readingTime?: number; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["BlogPostListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + BlogPostResponseDataObject: { + id?: number; + attributes?: components["schemas"]["BlogPost"]; + }; + BlogPostResponse: { + data?: components["schemas"]["BlogPostResponseDataObject"]; + meta?: Record; + }; + DownloadLocalizationRequest: { + title: string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + bullets?: components["schemas"]["UiBulletWithImageComponent"][]; + info?: string; + /** @example string or id */ + image?: number | string; + /** @example string or id */ + menuIcon?: number | string; + buttons?: components["schemas"]["UiButtonComponent"][]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneInstallComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + menuTitle?: string; + minimumSupport?: string; + locale: string; + }; + DownloadRequest: { + data: { + title: string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + bullets?: components["schemas"]["UiBulletWithImageComponent"][]; + info?: string; + /** @example string or id */ + image?: number | string; + /** @example string or id */ + menuIcon?: number | string; + buttons?: components["schemas"]["UiButtonComponent"][]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneInstallComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + menuTitle?: string; + minimumSupport?: string; + locale?: string; + }; + }; + DownloadResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["Download"]; + }; + DownloadLocalizationResponse: { + data?: components["schemas"]["DownloadResponseDataObjectLocalized"]; + meta?: Record; + }; + DownloadListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["Download"]; + }; + DownloadLocalizationListResponse: { + data?: components["schemas"]["DownloadListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + DownloadListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["Download"]; + }; + DownloadListResponse: { + data?: components["schemas"]["DownloadListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + Download: { + title: string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + bullets?: components["schemas"]["UiBulletWithImageComponent"][]; + info?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + menuIcon?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + buttons?: components["schemas"]["UiButtonComponent"][]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneInstallComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + menuTitle?: string; + minimumSupport?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["DownloadListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + DownloadResponseDataObject: { + id?: number; + attributes?: components["schemas"]["Download"]; + }; + DownloadResponse: { + data?: components["schemas"]["DownloadResponseDataObject"]; + meta?: Record; + }; + UiButtonComponent: { + id?: number; + title?: string; + url?: string; + /** @enum {string} */ + type?: "primary" | "outlined"; + /** @enum {string} */ + size?: "large" | "small"; + }; + DownloadsHowToDownloadComponent: { + id?: number; + menuTitle?: string; + menuIcon?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + title?: string; + content?: string; + steps?: { + id?: number; + title?: string; + content?: string; + }[]; + }; + DynamicZoneInstallComponent: { + id?: number; + __component?: string; + heading?: components["schemas"]["UiHeadingComponent"]; + subTitle?: string; + versions?: components["schemas"]["DownloadsHowToDownloadComponent"][]; + }; + DownloadsMenuLocalizationRequest: { + downloads?: (number | string)[]; + locale: string; + }; + DownloadsMenuRequest: { + data: { + downloads?: (number | string)[]; + locale?: string; + }; + }; + DownloadsMenuResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["DownloadsMenu"]; + }; + DownloadsMenuLocalizationResponse: { + data?: components["schemas"]["DownloadsMenuResponseDataObjectLocalized"]; + meta?: Record; + }; + DownloadsMenuListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["DownloadsMenu"]; + }; + DownloadsMenuLocalizationListResponse: { + data?: components["schemas"]["DownloadsMenuListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + DownloadsMenuListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["DownloadsMenu"]; + }; + DownloadsMenuListResponse: { + data?: components["schemas"]["DownloadsMenuListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + DownloadsMenu: { + downloads?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: { + name?: string; + pathId?: number; + parent?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + children?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + files?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: { + firstname?: string; + lastname?: string; + username?: string; + /** Format: email */ + email?: string; + resetPasswordToken?: string; + registrationToken?: string; + isActive?: boolean; + roles?: { + data?: { + id?: number; + attributes?: { + name?: string; + code?: string; + description?: string; + users?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }[]; + }; + permissions?: { + data?: { + id?: number; + attributes?: { + action?: string; + actionParameters?: unknown; + subject?: string; + properties?: unknown; + conditions?: unknown; + role?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + blocked?: boolean; + preferedLanguage?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + path?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + bullets?: { + id?: number; + title?: string; + text?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + }[]; + info?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + menuIcon?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + buttons?: { + id?: number; + title?: string; + url?: string; + /** @enum {string} */ + type?: "primary" | "outlined"; + /** @enum {string} */ + size?: "large" | "small"; + }[]; + dynamicZone?: ( + | { + id?: number; + __component?: string; + title?: string; + text?: string; + links?: { + id?: number; + title?: string; + url?: string; + }[]; + media?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** @enum {string} */ + direction?: "text-right" | "text-left"; + } + | { + id?: number; + __component?: string; + title?: string; + links?: { + id?: number; + title?: string; + url?: string; + }[]; + media?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** @enum {string} */ + direction?: "text-right" | "text-left"; + bullets?: { + id?: number; + title?: string; + text?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + }[]; + } + | { + id?: number; + __component?: string; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + tiles?: { + id?: number; + title?: string; + subtitle?: string; + description?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + url?: string; + }[]; + } + | { + id?: number; + __component?: string; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + subtitle?: string; + teamMembers?: { + data?: { + id?: number; + attributes?: { + name?: string; + role?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + bio?: string; + socialNetworks?: { + id?: number; + links?: { + id?: number; + url?: string; + /** @enum {string} */ + network?: + | "linkedin" + | "twitter" + | "wikipedia" + | "website"; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + } + | { + id?: number; + __component?: string; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + quotes?: { + id?: number; + title?: string; + quote?: string; + }[]; + } + | { + id?: number; + __component?: string; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + /** @enum {string} */ + style?: "h1" | "h2" | "h3" | "h4"; + /** @enum {string} */ + alignment?: "left" | "center"; + }; + text?: string; + primaryLinks?: { + id?: number; + title?: string; + url?: string; + }[]; + secondaryLinks?: { + id?: number; + title?: string; + url?: string; + }[]; + background?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + } + | { + id?: number; + __component?: string; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + isStatic?: boolean; + images?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + } + | { + id?: number; + __component?: string; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + tiles?: { + id?: number; + title?: string; + subtitle?: string; + text?: string; + label1?: string; + label2?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + button?: { + id?: number; + title?: string; + url?: string; + }; + dataTable?: string; + }[]; + } + | { + id?: number; + __component?: string; + blogPosts?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + subtitle?: string; + content?: string; + excerpt?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + blogCategories?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + blogPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + description?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + blogAuthors?: { + data?: { + id?: number; + attributes?: { + name?: string; + bio?: string; + avatar?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + slug?: string; + blogPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + role?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + relatedPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** Format: date */ + date?: string; + readingTime?: number; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + } + | { + id?: number; + __component?: string; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + subtitle?: string; + blocks?: { + id?: number; + title?: string; + description?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + }[]; + /** @enum {string} */ + type?: "high-density" | "low-density"; + } + | { + id?: number; + __component?: string; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + questions?: { + id?: number; + question?: string; + answer?: string; + }[]; + } + | { + id?: number; + __component?: string; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + subTitle?: string; + versions?: { + id?: number; + menuTitle?: string; + menuIcon?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + title?: string; + content?: string; + steps?: { + id?: number; + title?: string; + content?: string; + }[]; + }[]; + } + | { + id?: number; + __component?: string; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + slides?: { + id?: number; + text?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + }[]; + } + | { + id?: number; + __component?: string; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + slides?: { + id?: number; + quote?: string; + source?: string; + }[]; + } + )[]; + menuTitle?: string; + minimumSupport?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["DownloadsMenuListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + DownloadsMenuResponseDataObject: { + id?: number; + attributes?: components["schemas"]["DownloadsMenu"]; + }; + DownloadsMenuResponse: { + data?: components["schemas"]["DownloadsMenuResponseDataObject"]; + meta?: Record; + }; + FeatureLocalizationRequest: { + title: string; + description?: string; + featureCategories?: (number | string)[]; + available?: boolean; + /** @example string or id */ + icon?: number | string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + content?: string; + locale: string; + }; + FeatureRequest: { + data: { + title: string; + description?: string; + featureCategories?: (number | string)[]; + available?: boolean; + /** @example string or id */ + icon?: number | string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + content?: string; + locale?: string; + }; + }; + FeatureResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["Feature"]; + }; + FeatureLocalizationResponse: { + data?: components["schemas"]["FeatureResponseDataObjectLocalized"]; + meta?: Record; + }; + FeatureListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["Feature"]; + }; + FeatureLocalizationListResponse: { + data?: components["schemas"]["FeatureListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + FeatureListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["Feature"]; + }; + FeatureListResponse: { + data?: components["schemas"]["FeatureListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + Feature: { + title: string; + description?: string; + featureCategories?: { + data?: { + id?: number; + attributes?: { + title?: string; + features?: { + data?: { + id?: number; + attributes?: { + title?: string; + description?: string; + featureCategories?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + available?: boolean; + icon?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: { + name?: string; + pathId?: number; + parent?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + children?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + files?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: { + firstname?: string; + lastname?: string; + username?: string; + /** Format: email */ + email?: string; + resetPasswordToken?: string; + registrationToken?: string; + isActive?: boolean; + roles?: { + data?: { + id?: number; + attributes?: { + name?: string; + code?: string; + description?: string; + users?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }[]; + }; + permissions?: { + data?: { + id?: number; + attributes?: { + action?: string; + actionParameters?: unknown; + subject?: string; + properties?: unknown; + conditions?: unknown; + role?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + blocked?: boolean; + preferedLanguage?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + path?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + slug?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + content?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + available?: boolean; + icon?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + content?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["FeatureListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + FeatureResponseDataObject: { + id?: number; + attributes?: components["schemas"]["Feature"]; + }; + FeatureResponse: { + data?: components["schemas"]["FeatureResponseDataObject"]; + meta?: Record; + }; + FeatureCategoryLocalizationRequest: { + title: string; + features?: (number | string)[]; + locale: string; + }; + FeatureCategoryRequest: { + data: { + title: string; + features?: (number | string)[]; + locale?: string; + }; + }; + FeatureCategoryResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["FeatureCategory"]; + }; + FeatureCategoryLocalizationResponse: { + data?: components["schemas"]["FeatureCategoryResponseDataObjectLocalized"]; + meta?: Record; + }; + FeatureCategoryListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["FeatureCategory"]; + }; + FeatureCategoryLocalizationListResponse: { + data?: components["schemas"]["FeatureCategoryListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + FeatureCategoryListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["FeatureCategory"]; + }; + FeatureCategoryListResponse: { + data?: components["schemas"]["FeatureCategoryListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + FeatureCategory: { + title: string; + features?: { + data?: { + id?: number; + attributes?: { + title?: string; + description?: string; + featureCategories?: { + data?: { + id?: number; + attributes?: { + title?: string; + features?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: { + firstname?: string; + lastname?: string; + username?: string; + /** Format: email */ + email?: string; + resetPasswordToken?: string; + registrationToken?: string; + isActive?: boolean; + roles?: { + data?: { + id?: number; + attributes?: { + name?: string; + code?: string; + description?: string; + users?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + permissions?: { + data?: { + id?: number; + attributes?: { + action?: string; + actionParameters?: unknown; + subject?: string; + properties?: unknown; + conditions?: unknown; + role?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + blocked?: boolean; + preferedLanguage?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + available?: boolean; + icon?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: { + name?: string; + pathId?: number; + parent?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + children?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + files?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + path?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + slug?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + content?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["FeatureCategoryListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + FeatureCategoryResponseDataObject: { + id?: number; + attributes?: components["schemas"]["FeatureCategory"]; + }; + FeatureCategoryResponse: { + data?: components["schemas"]["FeatureCategoryResponseDataObject"]; + meta?: Record; + }; + FeaturesPageLocalizationRequest: { + SEO?: components["schemas"]["SharedSeoComponent"]; + featureCategories?: (number | string)[]; + dynamicZoneTop?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + dynamicZoneBottom?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + locale: string; + }; + FeaturesPageRequest: { + data: { + SEO?: components["schemas"]["SharedSeoComponent"]; + featureCategories?: (number | string)[]; + dynamicZoneTop?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + dynamicZoneBottom?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + locale?: string; + }; + }; + FeaturesPageResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["FeaturesPage"]; + }; + FeaturesPageLocalizationResponse: { + data?: components["schemas"]["FeaturesPageResponseDataObjectLocalized"]; + meta?: Record; + }; + FeaturesPageListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["FeaturesPage"]; + }; + FeaturesPageLocalizationListResponse: { + data?: components["schemas"]["FeaturesPageListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + FeaturesPageListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["FeaturesPage"]; + }; + FeaturesPageListResponse: { + data?: components["schemas"]["FeaturesPageListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + FeaturesPage: { + SEO?: components["schemas"]["SharedSeoComponent"]; + featureCategories?: { + data?: { + id?: number; + attributes?: { + title?: string; + features?: { + data?: { + id?: number; + attributes?: { + title?: string; + description?: string; + featureCategories?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + available?: boolean; + icon?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + slug?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + content?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + dynamicZoneTop?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + dynamicZoneBottom?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["FeaturesPageListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + FeaturesPageResponseDataObject: { + id?: number; + attributes?: components["schemas"]["FeaturesPage"]; + }; + FeaturesPageResponse: { + data?: components["schemas"]["FeaturesPageResponseDataObject"]; + meta?: Record; + }; + FooterLocalizationRequest: { + legalContent1?: string; + legalContent2?: string; + linkBlocks?: components["schemas"]["FooterLinkBlockComponent"][]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + locale: string; + }; + FooterRequest: { + data: { + legalContent1?: string; + legalContent2?: string; + linkBlocks?: components["schemas"]["FooterLinkBlockComponent"][]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + locale?: string; + }; + }; + FooterResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["Footer"]; + }; + FooterLocalizationResponse: { + data?: components["schemas"]["FooterResponseDataObjectLocalized"]; + meta?: Record; + }; + FooterListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["Footer"]; + }; + FooterLocalizationListResponse: { + data?: components["schemas"]["FooterListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + FooterListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["Footer"]; + }; + FooterListResponse: { + data?: components["schemas"]["FooterListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + Footer: { + legalContent1?: string; + legalContent2?: string; + linkBlocks?: components["schemas"]["FooterLinkBlockComponent"][]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["FooterListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + FooterResponseDataObject: { + id?: number; + attributes?: components["schemas"]["Footer"]; + }; + FooterResponse: { + data?: components["schemas"]["FooterResponseDataObject"]; + meta?: Record; + }; + FooterLinkBlockComponent: { + id?: number; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + links?: { + id?: number; + title?: string; + url?: string; + }[]; + }; + GlossaryPageLocalizationRequest: { + SEO?: components["schemas"]["SharedSeoComponent"]; + locale: string; + }; + GlossaryPageRequest: { + data: { + SEO?: components["schemas"]["SharedSeoComponent"]; + locale?: string; + }; + }; + GlossaryPageResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["GlossaryPage"]; + }; + GlossaryPageLocalizationResponse: { + data?: components["schemas"]["GlossaryPageResponseDataObjectLocalized"]; + meta?: Record; + }; + GlossaryPageListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["GlossaryPage"]; + }; + GlossaryPageLocalizationListResponse: { + data?: components["schemas"]["GlossaryPageListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + GlossaryPageListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["GlossaryPage"]; + }; + GlossaryPageListResponse: { + data?: components["schemas"]["GlossaryPageListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + GlossaryPage: { + SEO?: components["schemas"]["SharedSeoComponent"]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["GlossaryPageListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + GlossaryPageResponseDataObject: { + id?: number; + attributes?: components["schemas"]["GlossaryPage"]; + }; + GlossaryPageResponse: { + data?: components["schemas"]["GlossaryPageResponseDataObject"]; + meta?: Record; + }; + GlossaryTermLocalizationRequest: { + title?: string; + description?: string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + locale: string; + }; + GlossaryTermRequest: { + data: { + title?: string; + description?: string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + locale?: string; + }; + }; + GlossaryTermResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["GlossaryTerm"]; + }; + GlossaryTermLocalizationResponse: { + data?: components["schemas"]["GlossaryTermResponseDataObjectLocalized"]; + meta?: Record; + }; + GlossaryTermListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["GlossaryTerm"]; + }; + GlossaryTermLocalizationListResponse: { + data?: components["schemas"]["GlossaryTermListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + GlossaryTermListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["GlossaryTerm"]; + }; + GlossaryTermListResponse: { + data?: components["schemas"]["GlossaryTermListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + GlossaryTerm: { + title?: string; + description?: string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["GlossaryTermListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + GlossaryTermResponseDataObject: { + id?: number; + attributes?: components["schemas"]["GlossaryTerm"]; + }; + GlossaryTermResponse: { + data?: components["schemas"]["GlossaryTermResponseDataObject"]; + meta?: Record; + }; + HomePageLocalizationRequest: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + locale: string; + }; + HomePageRequest: { + data: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + locale?: string; + }; + }; + HomePageResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["HomePage"]; + }; + HomePageLocalizationResponse: { + data?: components["schemas"]["HomePageResponseDataObjectLocalized"]; + meta?: Record; + }; + HomePageListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["HomePage"]; + }; + HomePageLocalizationListResponse: { + data?: components["schemas"]["HomePageListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + HomePageListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["HomePage"]; + }; + HomePageListResponse: { + data?: components["schemas"]["HomePageListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + HomePage: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["HomePageListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + HomePageResponseDataObject: { + id?: number; + attributes?: components["schemas"]["HomePage"]; + }; + HomePageResponse: { + data?: components["schemas"]["HomePageResponseDataObject"]; + meta?: Record; + }; + LitepaperLocalizationRequest: { + title: string; + content?: string; + /** @example string or id */ + pdf?: number | string; + SEO?: components["schemas"]["SharedSeoComponent"]; + locale: string; + }; + LitepaperRequest: { + data: { + title: string; + content?: string; + /** @example string or id */ + pdf?: number | string; + SEO?: components["schemas"]["SharedSeoComponent"]; + locale?: string; + }; + }; + LitepaperResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["Litepaper"]; + }; + LitepaperLocalizationResponse: { + data?: components["schemas"]["LitepaperResponseDataObjectLocalized"]; + meta?: Record; + }; + LitepaperListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["Litepaper"]; + }; + LitepaperLocalizationListResponse: { + data?: components["schemas"]["LitepaperListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + LitepaperListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["Litepaper"]; + }; + LitepaperListResponse: { + data?: components["schemas"]["LitepaperListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + Litepaper: { + title: string; + content?: string; + pdf?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: { + name?: string; + pathId?: number; + parent?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + children?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + files?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: { + firstname?: string; + lastname?: string; + username?: string; + /** Format: email */ + email?: string; + resetPasswordToken?: string; + registrationToken?: string; + isActive?: boolean; + roles?: { + data?: { + id?: number; + attributes?: { + name?: string; + code?: string; + description?: string; + users?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + permissions?: { + data?: { + id?: number; + attributes?: { + action?: string; + actionParameters?: unknown; + subject?: string; + properties?: unknown; + conditions?: unknown; + role?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + blocked?: boolean; + preferedLanguage?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + path?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + SEO?: components["schemas"]["SharedSeoComponent"]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["LitepaperListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + LitepaperResponseDataObject: { + id?: number; + attributes?: components["schemas"]["Litepaper"]; + }; + LitepaperResponse: { + data?: components["schemas"]["LitepaperResponseDataObject"]; + meta?: Record; + }; + NavigationLocalizationRequest: { + links?: components["schemas"]["NavigationNavigationItemComponent"][]; + button?: components["schemas"]["UiLinkComponent"]; + backText?: string; + locale: string; + }; + NavigationRequest: { + data: { + links?: components["schemas"]["NavigationNavigationItemComponent"][]; + button?: components["schemas"]["UiLinkComponent"]; + backText?: string; + locale?: string; + }; + }; + NavigationResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["Navigation"]; + }; + NavigationLocalizationResponse: { + data?: components["schemas"]["NavigationResponseDataObjectLocalized"]; + meta?: Record; + }; + NavigationListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["Navigation"]; + }; + NavigationLocalizationListResponse: { + data?: components["schemas"]["NavigationListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + NavigationListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["Navigation"]; + }; + NavigationListResponse: { + data?: components["schemas"]["NavigationListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + Navigation: { + links?: components["schemas"]["NavigationNavigationItemComponent"][]; + button?: components["schemas"]["UiLinkComponent"]; + backText?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["NavigationListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + NavigationResponseDataObject: { + id?: number; + attributes?: components["schemas"]["Navigation"]; + }; + NavigationResponse: { + data?: components["schemas"]["NavigationResponseDataObject"]; + meta?: Record; + }; + NavigationNavigationItemComponent: { + id?: number; + title?: string; + url?: string; + subItems?: { + id?: number; + title?: string; + description?: string; + url?: string; + }[]; + subPanel?: { + id?: number; + title?: string; + url?: string; + links?: { + id?: number; + title?: string; + links?: { + id?: number; + title?: string; + url?: string; + icon?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: { + name?: string; + pathId?: number; + parent?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + children?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + files?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: { + firstname?: string; + lastname?: string; + username?: string; + /** Format: email */ + email?: string; + resetPasswordToken?: string; + registrationToken?: string; + isActive?: boolean; + roles?: { + data?: { + id?: number; + attributes?: { + name?: string; + code?: string; + description?: string; + users?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }[]; + }; + permissions?: { + data?: { + id?: number; + attributes?: { + action?: string; + actionParameters?: unknown; + subject?: string; + properties?: unknown; + conditions?: unknown; + role?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + blocked?: boolean; + preferedLanguage?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + path?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + }[]; + }[]; + }; + }; + NetworkPageLocalizationRequest: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + locale: string; + }; + NetworkPageRequest: { + data: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + locale?: string; + }; + }; + NetworkPageResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["NetworkPage"]; + }; + NetworkPageLocalizationResponse: { + data?: components["schemas"]["NetworkPageResponseDataObjectLocalized"]; + meta?: Record; + }; + NetworkPageListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["NetworkPage"]; + }; + NetworkPageLocalizationListResponse: { + data?: components["schemas"]["NetworkPageListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + NetworkPageListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["NetworkPage"]; + }; + NetworkPageListResponse: { + data?: components["schemas"]["NetworkPageListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + NetworkPage: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["NetworkPageListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + NetworkPageResponseDataObject: { + id?: number; + attributes?: components["schemas"]["NetworkPage"]; + }; + NetworkPageResponse: { + data?: components["schemas"]["NetworkPageResponseDataObject"]; + meta?: Record; + }; + PageNotFoundLocalizationRequest: { + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneRichTextComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + locale: string; + }; + PageNotFoundRequest: { + data: { + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneRichTextComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + locale?: string; + }; + }; + PageNotFoundResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["PageNotFound"]; + }; + PageNotFoundLocalizationResponse: { + data?: components["schemas"]["PageNotFoundResponseDataObjectLocalized"]; + meta?: Record; + }; + PageNotFoundListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["PageNotFound"]; + }; + PageNotFoundLocalizationListResponse: { + data?: components["schemas"]["PageNotFoundListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + PageNotFoundListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["PageNotFound"]; + }; + PageNotFoundListResponse: { + data?: components["schemas"]["PageNotFoundListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + PageNotFound: { + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneRichTextComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["PageNotFoundListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + PageNotFoundResponseDataObject: { + id?: number; + attributes?: components["schemas"]["PageNotFound"]; + }; + PageNotFoundResponse: { + data?: components["schemas"]["PageNotFoundResponseDataObject"]; + meta?: Record; + }; + PressPageLocalizationRequest: { + SEO?: components["schemas"]["SharedSeoComponent"]; + locale: string; + }; + PressPageRequest: { + data: { + SEO?: components["schemas"]["SharedSeoComponent"]; + locale?: string; + }; + }; + PressPageResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["PressPage"]; + }; + PressPageLocalizationResponse: { + data?: components["schemas"]["PressPageResponseDataObjectLocalized"]; + meta?: Record; + }; + PressPageListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["PressPage"]; + }; + PressPageLocalizationListResponse: { + data?: components["schemas"]["PressPageListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + PressPageListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["PressPage"]; + }; + PressPageListResponse: { + data?: components["schemas"]["PressPageListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + PressPage: { + SEO?: components["schemas"]["SharedSeoComponent"]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["PressPageListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + PressPageResponseDataObject: { + id?: number; + attributes?: components["schemas"]["PressPage"]; + }; + PressPageResponse: { + data?: components["schemas"]["PressPageResponseDataObject"]; + meta?: Record; + }; + PressPostLocalizationRequest: { + title: string; + /** Format: date */ + date?: string; + url: string; + locale: string; + }; + PressPostRequest: { + data: { + title: string; + /** Format: date */ + date?: string; + url: string; + locale?: string; + }; + }; + PressPostResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["PressPost"]; + }; + PressPostLocalizationResponse: { + data?: components["schemas"]["PressPostResponseDataObjectLocalized"]; + meta?: Record; + }; + PressPostListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["PressPost"]; + }; + PressPostLocalizationListResponse: { + data?: components["schemas"]["PressPostListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + PressPostListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["PressPost"]; + }; + PressPostListResponse: { + data?: components["schemas"]["PressPostListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + PressPost: { + title: string; + /** Format: date */ + date?: string; + url: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: { + firstname?: string; + lastname?: string; + username?: string; + /** Format: email */ + email?: string; + resetPasswordToken?: string; + registrationToken?: string; + isActive?: boolean; + roles?: { + data?: { + id?: number; + attributes?: { + name?: string; + code?: string; + description?: string; + users?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + permissions?: { + data?: { + id?: number; + attributes?: { + action?: string; + actionParameters?: unknown; + subject?: string; + properties?: unknown; + conditions?: unknown; + role?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + blocked?: boolean; + preferedLanguage?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["PressPostListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + PressPostResponseDataObject: { + id?: number; + attributes?: components["schemas"]["PressPost"]; + }; + PressPostResponse: { + data?: components["schemas"]["PressPostResponseDataObject"]; + meta?: Record; + }; + TeamMemberLocalizationRequest: { + name?: string; + role?: string; + /** @example string or id */ + image?: number | string; + bio?: string; + socialNetworks?: components["schemas"]["SocialSocialsComponent"]; + locale: string; + }; + TeamMemberRequest: { + data: { + name?: string; + role?: string; + /** @example string or id */ + image?: number | string; + bio?: string; + socialNetworks?: components["schemas"]["SocialSocialsComponent"]; + locale?: string; + }; + }; + TeamMemberResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["TeamMember"]; + }; + TeamMemberLocalizationResponse: { + data?: components["schemas"]["TeamMemberResponseDataObjectLocalized"]; + meta?: Record; + }; + TeamMemberListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["TeamMember"]; + }; + TeamMemberLocalizationListResponse: { + data?: components["schemas"]["TeamMemberListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + TeamMemberListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["TeamMember"]; + }; + TeamMemberListResponse: { + data?: components["schemas"]["TeamMemberListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + TeamMember: { + name?: string; + role?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: { + name?: string; + pathId?: number; + parent?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + children?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + files?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: { + firstname?: string; + lastname?: string; + username?: string; + /** Format: email */ + email?: string; + resetPasswordToken?: string; + registrationToken?: string; + isActive?: boolean; + roles?: { + data?: { + id?: number; + attributes?: { + name?: string; + code?: string; + description?: string; + users?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + permissions?: { + data?: { + id?: number; + attributes?: { + action?: string; + actionParameters?: unknown; + subject?: string; + properties?: unknown; + conditions?: unknown; + role?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + blocked?: boolean; + preferedLanguage?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + path?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + bio?: string; + socialNetworks?: components["schemas"]["SocialSocialsComponent"]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["TeamMemberListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + TeamMemberResponseDataObject: { + id?: number; + attributes?: components["schemas"]["TeamMember"]; + }; + TeamMemberResponse: { + data?: components["schemas"]["TeamMemberResponseDataObject"]; + meta?: Record; + }; + SocialSocialsComponent: { + id?: number; + links?: { + id?: number; + url?: string; + /** @enum {string} */ + network?: "linkedin" | "twitter" | "wikipedia" | "website"; + }[]; + }; + TrustCenterAdvisorsPageLocalizationRequest: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + )[]; + title?: string; + locale: string; + }; + TrustCenterAdvisorsPageRequest: { + data: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + )[]; + title?: string; + locale?: string; + }; + }; + TrustCenterAdvisorsPageResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["TrustCenterAdvisorsPage"]; + }; + TrustCenterAdvisorsPageLocalizationResponse: { + data?: components["schemas"]["TrustCenterAdvisorsPageResponseDataObjectLocalized"]; + meta?: Record; + }; + TrustCenterAdvisorsPageListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["TrustCenterAdvisorsPage"]; + }; + TrustCenterAdvisorsPageLocalizationListResponse: { + data?: components["schemas"]["TrustCenterAdvisorsPageListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + TrustCenterAdvisorsPageListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["TrustCenterAdvisorsPage"]; + }; + TrustCenterAdvisorsPageListResponse: { + data?: components["schemas"]["TrustCenterAdvisorsPageListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + TrustCenterAdvisorsPage: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + )[]; + title?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["TrustCenterAdvisorsPageListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + TrustCenterAdvisorsPageResponseDataObject: { + id?: number; + attributes?: components["schemas"]["TrustCenterAdvisorsPage"]; + }; + TrustCenterAdvisorsPageResponse: { + data?: components["schemas"]["TrustCenterAdvisorsPageResponseDataObject"]; + meta?: Record; + }; + TrustCenterCategoryLocalizationRequest: { + title: string; + description?: string; + slug: string; + trustCenterPosts?: (number | string)[]; + locale: string; + }; + TrustCenterCategoryRequest: { + data: { + title: string; + description?: string; + slug: string; + trustCenterPosts?: (number | string)[]; + locale?: string; + }; + }; + TrustCenterCategoryResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["TrustCenterCategory"]; + }; + TrustCenterCategoryLocalizationResponse: { + data?: components["schemas"]["TrustCenterCategoryResponseDataObjectLocalized"]; + meta?: Record; + }; + TrustCenterCategoryListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["TrustCenterCategory"]; + }; + TrustCenterCategoryLocalizationListResponse: { + data?: components["schemas"]["TrustCenterCategoryListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + TrustCenterCategoryListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["TrustCenterCategory"]; + }; + TrustCenterCategoryListResponse: { + data?: components["schemas"]["TrustCenterCategoryListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + TrustCenterCategory: { + title: string; + description?: string; + slug: string; + trustCenterPosts?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + excerpt?: string; + isLink?: boolean; + link?: string; + relatedTrustCenterPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + dynamicZone?: ( + | { + id?: number; + __component?: string; + items?: { + id?: number; + title?: string; + url?: string; + authors?: string; + published?: string; + date?: string; + }[]; + } + | { + id?: number; + __component?: string; + items?: { + id?: number; + title?: string; + url?: string; + company?: string; + authors?: string; + date?: string; + }[]; + } + )[]; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: { + name?: string; + pathId?: number; + parent?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + children?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + files?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: { + firstname?: string; + lastname?: string; + username?: string; + /** Format: email */ + email?: string; + resetPasswordToken?: string; + registrationToken?: string; + isActive?: boolean; + roles?: { + data?: { + id?: number; + attributes?: { + name?: string; + code?: string; + description?: string; + users?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }[]; + }; + permissions?: { + data?: { + id?: number; + attributes?: { + action?: string; + actionParameters?: unknown; + subject?: string; + properties?: unknown; + conditions?: unknown; + role?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + blocked?: boolean; + preferedLanguage?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + path?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + content?: string; + readingTime?: number; + trustCenterCategory?: { + data?: { + id?: number; + attributes?: { + title?: string; + description?: string; + slug?: string; + trustCenterPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }; + }; + subtitle?: string; + /** Format: date */ + date?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["TrustCenterCategoryListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + TrustCenterCategoryResponseDataObject: { + id?: number; + attributes?: components["schemas"]["TrustCenterCategory"]; + }; + TrustCenterCategoryResponse: { + data?: components["schemas"]["TrustCenterCategoryResponseDataObject"]; + meta?: Record; + }; + TrustCenterPageLocalizationRequest: { + SEO?: components["schemas"]["SharedSeoComponent"]; + title?: string; + locale: string; + }; + TrustCenterPageRequest: { + data: { + SEO?: components["schemas"]["SharedSeoComponent"]; + title?: string; + locale?: string; + }; + }; + TrustCenterPageResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["TrustCenterPage"]; + }; + TrustCenterPageLocalizationResponse: { + data?: components["schemas"]["TrustCenterPageResponseDataObjectLocalized"]; + meta?: Record; + }; + TrustCenterPageListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["TrustCenterPage"]; + }; + TrustCenterPageLocalizationListResponse: { + data?: components["schemas"]["TrustCenterPageListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + TrustCenterPageListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["TrustCenterPage"]; + }; + TrustCenterPageListResponse: { + data?: components["schemas"]["TrustCenterPageListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + TrustCenterPage: { + SEO?: components["schemas"]["SharedSeoComponent"]; + title?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["TrustCenterPageListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + TrustCenterPageResponseDataObject: { + id?: number; + attributes?: components["schemas"]["TrustCenterPage"]; + }; + TrustCenterPageResponse: { + data?: components["schemas"]["TrustCenterPageResponseDataObject"]; + meta?: Record; + }; + TrustCenterPostLocalizationRequest: { + title: string; + slug?: string; + excerpt?: string; + isLink?: boolean; + link?: string; + relatedTrustCenterPosts?: (number | string)[]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneDocumentListComponent"] + | components["schemas"]["DynamicZoneAuditListComponent"] + )[]; + SEO?: components["schemas"]["SharedSeoComponent"]; + content?: string; + readingTime?: number; + /** @example string or id */ + trustCenterCategory?: number | string; + subtitle?: string; + /** Format: date */ + date?: string; + /** @example string or id */ + image?: number | string; + locale: string; + }; + TrustCenterPostRequest: { + data: { + title: string; + slug?: string; + excerpt?: string; + isLink?: boolean; + link?: string; + relatedTrustCenterPosts?: (number | string)[]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneDocumentListComponent"] + | components["schemas"]["DynamicZoneAuditListComponent"] + )[]; + SEO?: components["schemas"]["SharedSeoComponent"]; + content?: string; + readingTime?: number; + /** @example string or id */ + trustCenterCategory?: number | string; + subtitle?: string; + /** Format: date */ + date?: string; + /** @example string or id */ + image?: number | string; + locale?: string; + }; + }; + TrustCenterPostResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["TrustCenterPost"]; + }; + TrustCenterPostLocalizationResponse: { + data?: components["schemas"]["TrustCenterPostResponseDataObjectLocalized"]; + meta?: Record; + }; + TrustCenterPostListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["TrustCenterPost"]; + }; + TrustCenterPostLocalizationListResponse: { + data?: components["schemas"]["TrustCenterPostListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + TrustCenterPostListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["TrustCenterPost"]; + }; + TrustCenterPostListResponse: { + data?: components["schemas"]["TrustCenterPostListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + TrustCenterPost: { + title: string; + slug?: string; + excerpt?: string; + isLink?: boolean; + link?: string; + relatedTrustCenterPosts?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + excerpt?: string; + isLink?: boolean; + link?: string; + relatedTrustCenterPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + dynamicZone?: ( + | { + id?: number; + __component?: string; + items?: { + id?: number; + title?: string; + url?: string; + authors?: string; + published?: string; + date?: string; + }[]; + } + | { + id?: number; + __component?: string; + items?: { + id?: number; + title?: string; + url?: string; + company?: string; + authors?: string; + date?: string; + }[]; + } + )[]; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: { + name?: string; + pathId?: number; + parent?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + children?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + files?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: { + firstname?: string; + lastname?: string; + username?: string; + /** Format: email */ + email?: string; + resetPasswordToken?: string; + registrationToken?: string; + isActive?: boolean; + roles?: { + data?: { + id?: number; + attributes?: { + name?: string; + code?: string; + description?: string; + users?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }[]; + }; + permissions?: { + data?: { + id?: number; + attributes?: { + action?: string; + actionParameters?: unknown; + subject?: string; + properties?: unknown; + conditions?: unknown; + role?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + blocked?: boolean; + preferedLanguage?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + path?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + content?: string; + readingTime?: number; + trustCenterCategory?: { + data?: { + id?: number; + attributes?: { + title?: string; + description?: string; + slug?: string; + trustCenterPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }; + }; + subtitle?: string; + /** Format: date */ + date?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + dynamicZone?: ( + | components["schemas"]["DynamicZoneDocumentListComponent"] + | components["schemas"]["DynamicZoneAuditListComponent"] + )[]; + SEO?: components["schemas"]["SharedSeoComponent"]; + content?: string; + readingTime?: number; + trustCenterCategory?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + subtitle?: string; + /** Format: date */ + date?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["TrustCenterPostListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + TrustCenterPostResponseDataObject: { + id?: number; + attributes?: components["schemas"]["TrustCenterPost"]; + }; + TrustCenterPostResponse: { + data?: components["schemas"]["TrustCenterPostResponseDataObject"]; + meta?: Record; + }; + DynamicZoneDocumentListItemComponent: { + id?: number; + title?: string; + url?: string; + authors?: string; + published?: string; + date?: string; + }; + DynamicZoneDocumentListComponent: { + id?: number; + __component?: string; + items?: components["schemas"]["DynamicZoneDocumentListItemComponent"][]; + }; + DynamicZoneAuditListItemComponent: { + id?: number; + title?: string; + url?: string; + company?: string; + authors?: string; + date?: string; + }; + DynamicZoneAuditListComponent: { + id?: number; + __component?: string; + items?: components["schemas"]["DynamicZoneAuditListItemComponent"][]; + }; + UseCaseLocalizationRequest: { + title: string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + excerpt?: string; + features?: (number | string)[]; + locale: string; + }; + UseCaseRequest: { + data: { + title: string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + excerpt?: string; + features?: (number | string)[]; + locale?: string; + }; + }; + UseCaseResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["UseCase"]; + }; + UseCaseLocalizationResponse: { + data?: components["schemas"]["UseCaseResponseDataObjectLocalized"]; + meta?: Record; + }; + UseCaseListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["UseCase"]; + }; + UseCaseLocalizationListResponse: { + data?: components["schemas"]["UseCaseListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + UseCaseListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["UseCase"]; + }; + UseCaseListResponse: { + data?: components["schemas"]["UseCaseListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + UseCase: { + title: string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + excerpt?: string; + features?: { + data?: { + id?: number; + attributes?: { + title?: string; + description?: string; + featureCategories?: { + data?: { + id?: number; + attributes?: { + title?: string; + features?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + available?: boolean; + icon?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + slug?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + content?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["UseCaseListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + UseCaseResponseDataObject: { + id?: number; + attributes?: components["schemas"]["UseCase"]; + }; + UseCaseResponse: { + data?: components["schemas"]["UseCaseResponseDataObject"]; + meta?: Record; + }; + UseCasesPageLocalizationRequest: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + locale: string; + }; + UseCasesPageRequest: { + data: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + locale?: string; + }; + }; + UseCasesPageResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["UseCasesPage"]; + }; + UseCasesPageLocalizationResponse: { + data?: components["schemas"]["UseCasesPageResponseDataObjectLocalized"]; + meta?: Record; + }; + UseCasesPageListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["UseCasesPage"]; + }; + UseCasesPageLocalizationListResponse: { + data?: components["schemas"]["UseCasesPageListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + UseCasesPageListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["UseCasesPage"]; + }; + UseCasesPageListResponse: { + data?: components["schemas"]["UseCasesPageListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + UseCasesPage: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["UseCasesPageListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + UseCasesPageResponseDataObject: { + id?: number; + attributes?: components["schemas"]["UseCasesPage"]; + }; + UseCasesPageResponse: { + data?: components["schemas"]["UseCasesPageResponseDataObject"]; + meta?: Record; + }; + UploadFile: { + id?: number; + name?: string; + alternativeText?: string; + caption?: string; + /** Format: integer */ + width?: number; + /** Format: integer */ + height?: number; + formats?: number; + hash?: string; + ext?: string; + mime?: string; + /** Format: double */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: Record; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + }; + "Users-Permissions-Role": { + id?: number; + name?: string; + description?: string; + type?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + }; + "Users-Permissions-User": { + /** @example 1 */ + id?: number; + /** @example foo.bar */ + username?: string; + /** @example foo.bar@strapi.io */ + email?: string; + /** @example local */ + provider?: string; + /** @example true */ + confirmed?: boolean; + /** @example false */ + blocked?: boolean; + /** + * Format: date-time + * @example 2022-06-02T08:32:06.258Z + */ + createdAt?: string; + /** + * Format: date-time + * @example 2022-06-02T08:32:06.267Z + */ + updatedAt?: string; + }; + "Users-Permissions-UserRegistration": { + /** @example eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c */ + jwt?: string; + user?: components["schemas"]["Users-Permissions-User"]; + }; + "Users-Permissions-PermissionsTree": { + [key: string]: { + /** @description every controller of the api */ + controllers?: { + [key: string]: { + [key: string]: { + enabled?: boolean; + policy?: string; + }; + }; + }; + }; + }; + }; + responses: never; + parameters: never; + requestBodies: { + "Users-Permissions-RoleRequest": { + content: { + /** @example { + * "name": "foo", + * "description": "role foo", + * "permissions": { + * "api::content-type.content-type": { + * "controllers": { + * "controllerA": { + * "find": { + * "enabled": true + * } + * } + * } + * } + * } + * } */ + "application/json": { + name?: string; + description?: string; + type?: string; + permissions?: components["schemas"]["Users-Permissions-PermissionsTree"]; + }; + }; + }; + }; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + "get/about-page": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AboutPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/about-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AboutPageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AboutPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/about-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/about-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AboutPageLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AboutPageLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/basic-pages": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BasicPageListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/basic-pages": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BasicPageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BasicPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/basic-pages/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BasicPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/basic-pages/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BasicPageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BasicPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/basic-pages/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/basic-pages/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BasicPageLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BasicPageLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/blog-authors": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogAuthorListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/blog-authors": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BlogAuthorRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogAuthorResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/blog-authors/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogAuthorResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/blog-authors/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BlogAuthorRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogAuthorResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/blog-authors/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/blog-authors/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BlogAuthorLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogAuthorLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/blog-categories": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogCategoryListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/blog-categories": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BlogCategoryRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogCategoryResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/blog-categories/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogCategoryResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/blog-categories/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BlogCategoryRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogCategoryResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/blog-categories/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/blog-categories/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BlogCategoryLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogCategoryLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/blog-page": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/blog-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BlogPageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/blog-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/blog-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BlogPageLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogPageLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/blog-posts": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogPostListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/blog-posts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BlogPostRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogPostResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/blog-posts/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogPostResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/blog-posts/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BlogPostRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogPostResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/blog-posts/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/blog-posts/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BlogPostLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogPostLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/downloads": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DownloadListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/downloads": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DownloadRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DownloadResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/downloads/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DownloadResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/downloads/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DownloadRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DownloadResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/downloads/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/downloads/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DownloadLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DownloadLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/downloads-menu": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DownloadsMenuResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/downloads-menu": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DownloadsMenuRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DownloadsMenuResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/downloads-menu": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/downloads-menu/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DownloadsMenuLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DownloadsMenuLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/features": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeatureListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/features": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["FeatureRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeatureResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/features/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeatureResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/features/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["FeatureRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeatureResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/features/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/features/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["FeatureLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeatureLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/feature-categories": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeatureCategoryListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/feature-categories": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["FeatureCategoryRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeatureCategoryResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/feature-categories/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeatureCategoryResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/feature-categories/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["FeatureCategoryRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeatureCategoryResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/feature-categories/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/feature-categories/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["FeatureCategoryLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeatureCategoryLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/features-page": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeaturesPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/features-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["FeaturesPageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeaturesPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/features-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/features-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["FeaturesPageLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeaturesPageLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/footer": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FooterResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/footer": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["FooterRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FooterResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/footer": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/footer/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["FooterLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FooterLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/glossary-page": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GlossaryPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/glossary-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GlossaryPageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GlossaryPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/glossary-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/glossary-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GlossaryPageLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GlossaryPageLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/glossary-terms": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GlossaryTermListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/glossary-terms": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GlossaryTermRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GlossaryTermResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/glossary-terms/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GlossaryTermResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/glossary-terms/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GlossaryTermRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GlossaryTermResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/glossary-terms/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/glossary-terms/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GlossaryTermLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GlossaryTermLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/home-page": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HomePageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/home-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["HomePageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HomePageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/home-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/home-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["HomePageLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HomePageLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/litepaper": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LitepaperResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/litepaper": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LitepaperRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LitepaperResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/litepaper": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/litepaper/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LitepaperLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LitepaperLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/navigation": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NavigationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/navigation": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["NavigationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NavigationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/navigation": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/navigation/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["NavigationLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NavigationLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/network-page": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NetworkPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/network-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["NetworkPageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NetworkPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/network-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/network-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["NetworkPageLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NetworkPageLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/page-not-found": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PageNotFoundResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/page-not-found": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PageNotFoundRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PageNotFoundResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/page-not-found": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/page-not-found/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PageNotFoundLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PageNotFoundLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/press-page": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PressPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/press-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PressPageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PressPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/press-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/press-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PressPageLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PressPageLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/press-posts": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PressPostListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/press-posts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PressPostRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PressPostResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/press-posts/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PressPostResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/press-posts/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PressPostRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PressPostResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/press-posts/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/press-posts/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PressPostLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PressPostLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/team-members": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TeamMemberListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/team-members": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TeamMemberRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TeamMemberResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/team-members/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TeamMemberResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/team-members/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TeamMemberRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TeamMemberResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/team-members/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/team-members/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TeamMemberLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TeamMemberLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/trust-center-advisors-page": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterAdvisorsPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/trust-center-advisors-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TrustCenterAdvisorsPageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterAdvisorsPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/trust-center-advisors-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/trust-center-advisors-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TrustCenterAdvisorsPageLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterAdvisorsPageLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/trust-center-categories": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterCategoryListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/trust-center-categories": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TrustCenterCategoryRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterCategoryResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/trust-center-categories/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterCategoryResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/trust-center-categories/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TrustCenterCategoryRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterCategoryResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/trust-center-categories/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/trust-center-categories/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TrustCenterCategoryLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterCategoryLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/trust-center-page": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/trust-center-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TrustCenterPageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/trust-center-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/trust-center-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TrustCenterPageLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterPageLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/trust-center-posts": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterPostListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/trust-center-posts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TrustCenterPostRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterPostResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/trust-center-posts/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterPostResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/trust-center-posts/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TrustCenterPostRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterPostResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/trust-center-posts/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/trust-center-posts/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TrustCenterPostLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterPostLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/use-cases": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UseCaseListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/use-cases": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UseCaseRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UseCaseResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/use-cases/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UseCaseResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/use-cases/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UseCaseRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UseCaseResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/use-cases/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/use-cases/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UseCaseLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UseCaseLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/use-cases-page": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UseCasesPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/use-cases-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UseCasesPageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UseCasesPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/use-cases-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/use-cases-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UseCasesPageLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UseCasesPageLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; +} diff --git a/explorer-v2/src/app/api/types.ts b/explorer-v2/src/app/api/types.ts new file mode 100644 index 0000000000..bc306f1a6e --- /dev/null +++ b/explorer-v2/src/app/api/types.ts @@ -0,0 +1,482 @@ +export type API_RESPONSE = { + data: T[]; +}; + +export type Denom = "unym" | "nym"; + +export interface IPacketsAndStakingData { + date_utc: string; + total_packets_received: number; + total_packets_sent: number; + total_packets_dropped: number; + total_stake: number; +} + +export interface CurrentEpochData { + id: number; + current_epoch_id: number; + current_epoch_start: string; + epoch_length: { secs: number; nanos: number }; + epochs_in_interval: number; + total_elapsed_epochs: number; +} +export interface ExplorerData { + circulatingNymSupplyData: { + circulating_supply: { denom: Denom; amount: string }; + mixmining_reserve: { denom: Denom; amount: string }; + total_supply: { denom: Denom; amount: string }; + vesting_tokens: { denom: Denom; amount: string }; + }; + nymNodesData: { + gateways: { + bonded: { count: number; last_updated_utc: string }; + blacklisted: { count: number; last_updated_utc: string }; + historical: { count: number; last_updated_utc: string }; + explorer: { count: number; last_updated_utc: string }; + }; + mixnodes: { + bonded: { + count: number; + active: number; + inactive: number; + reserve: number; + last_updated_utc: string; + }; + blacklisted: { + count: number; + last_updated_utc: string; + }; + historical: { count: number; last_updated_utc: string }; + }; + }; + packetsAndStakingData: IPacketsAndStakingData[]; + + currentEpochRewardsData: { + interval: { + reward_pool: string; + staking_supply: string; + staking_supply_scale_factor: string; + epoch_reward_budget: string; + stake_saturation_point: string; + active_set_work_factor: string; + interval_pool_emission: string; + sybil_resistance: string; + }; + rewarded_set: { + entry_gateways: number; + exit_gateways: number; + mixnodes: number; + standby: number; + }; + }; +} + +export type NodeDescription = { + last_polled: string; + host_information: { + ip_address: string[]; + hostname: string; + keys: { + ed25519: string; + x25519: string; + x25519_noise: string | null; + }; + }; + declared_role: { + mixnode: boolean; + entry: boolean; + exit_nr: boolean; + exit_ipr: boolean; + }; + auxiliary_details: { + location: string; + announce_ports: { + verloc_port: number | null; + mix_port: number | null; + }; + accepted_operator_terms_and_conditions: boolean; + }; + build_information: { + binary_name: string; + build_timestamp: string; + build_version: string; + commit_sha: string; + commit_timestamp: string; + commit_branch: string; + rustc_version: string; + rustc_channel: string; + cargo_profile: string; + cargo_triple: string; + }; + network_requester: { + address: string; + uses_exit_policy: boolean; + }; + ip_packet_router: { + address: string; + }; + authenticator: { + address: string; + }; + wireguard: string | null; + mixnet_websockets: { + ws_port: number; + wss_port: number | null; + }; +} | null; + +export type BondInformation = { + node_id: number; + owner: string; + original_pledge: { + denom: string; + amount: string; + }; + bonding_height: number; + is_unbonding: boolean; + node: { + host: string; + custom_http_port: number; + identity_key: string; + }; +}; + +export type RewardingDetails = { + cost_params: { + profit_margin_percent: string; + interval_operating_cost: { + denom: string; + amount: string; + }; + }; + operator: string; + delegates: string; + total_unit_reward: string; + unit_delegation: string; + last_rewarded_epoch: number; + unique_delegations: number; +}; + +export type Location = { + two_letter_iso_country_code?: string; + three_letter_iso_country_code?: string; + country_name?: string; + latitude?: number; + longitude?: number; +}; + +export type NodeData = { + node_id: number; + contract_node_type: string; + description: NodeDescription; + bond_information: BondInformation; + rewarding_details: RewardingDetails; + location: Location; +}; + +// ACCOUNT BALANCES + +export interface IRewardDetails { + amount_staked: IAmountDetails; + node_id: number; + node_still_fully_bonded: boolean; + rewards: IAmountDetails; +} + +export interface IAmountDetails { + denom: string; + amount: string; +} + +export interface IDelegationDetails { + node_id: number; + delegated: IAmountDetails; + height: number; + proxy: null | string; +} + +export interface IAccountBalancesInfo { + accumulated_rewards: IRewardDetails[]; + address: string; + balances: IAmountDetails[]; + claimable_rewards: IAmountDetails; + delegations: IDelegationDetails[]; + operator_rewards?: null | IAmountDetails; + total_delegations: IAmountDetails; + total_value: IAmountDetails; + vesting_account?: null | string; +} + +export interface IObservatoryNode { + accepted_tnc: boolean; + bonded: boolean; + bonding_address: string; + description: { + authenticator: { + address: string; + }; + auxiliary_details: { + accepted_operator_terms_and_conditions: boolean; + announce_ports: { + mix_port: number | null; + verloc_port: number | null; + }; + location: string | null; + }; + build_information: { + binary_name: string; + build_timestamp: string; + build_version: string; + cargo_profile: string; + cargo_triple: string; + commit_branch: string; + commit_sha: string; + commit_timestamp: string; + rustc_channel: string; + rustc_version: string; + }; + declared_role: { + entry: boolean; + exit_ipr: boolean; + exit_nr: boolean; + mixnode: boolean; + }; + host_information: { + hostname: string | null; + ip_address: string[]; + }; + keys: { + ed25519: string; + x25519: string; + x25519_noise: string | null; + }; + ip_packet_router: { + address: string; + }; + last_polled: string; + mixnet_websockets: { + ws_port: number; + wss_port: number | null; + }; + network_requester: { + address: string; + uses_exit_policy: boolean; + }; + wireguard: string | null; + geoip: { + city: string; + country: string; + ip_address: string; + loc: string; + node_id: number; + org: string; + postal: string; + region: string; + }; + }; + identity_key: string; + ip_address: string; + node_id: number; + node_type: string; + original_pledge: number; + rewarding_details: { + cost_params: { + interval_operating_cost: { + amount: string; + denom: string; + }; + profit_margin_percent: string; + }; + delegates: string; + last_rewarded_epoch: number; + operator: string; + total_unit_reward: string; + unique_delegations: number; + unit_delegation: string; + }; + self_description: { + details: string; + moniker: string; + security_contact: string; + website: string; + }; + total_stake: number; + uptime: number; +} +export interface NodeRewardDetails { + amount: { + amount: string; + denom: string; + }; + cumulative_reward_ratio: string; + height: number; + node_id: number; + owner: string; +} + +export type LastProbeResult = { + gateway: string; + outcome: { + as_entry: { + can_connect: boolean; + can_route: boolean; + }; + as_exit: { + can_connect: boolean; + can_route_ip_external_v4: boolean; + can_route_ip_external_v6: boolean; + can_route_ip_v4: boolean; + can_route_ip_v6: boolean; + }; + wg: { + can_handshake_v4: boolean; + can_handshake_v6: boolean; + can_register: boolean; + can_resolve_dns_v4: boolean; + can_resolve_dns_v6: boolean; + ping_hosts_performance_v4: number; + ping_hosts_performance_v6: number; + ping_ips_performance_v4: number; + ping_ips_performance_v6: number; + }; + }; +}; + +export type GatewayStatus = { + blacklisted: boolean; + bonded: boolean; + config_score: number; + description: { + details: string; + moniker: string; + security_contact: string; + website: string; + }; + explorer_pretty_bond: { + identity_key: string; + location: { + latitude: number; + longitude: number; + two_letter_iso_country_code: string; + }; + owner: string; + pledge_amount: { + amount: string; + denom: string; + }; + }; + gateway_identity_key: string; + last_probe_log: string; + last_probe_result: LastProbeResult; // Reference to the separate type + last_testrun_utc: string; + last_updated_utc: string; + performance: number; + routing_score: number; + self_described: { + authenticator: { + address: string; + }; + auxiliary_details: { + accepted_operator_terms_and_conditions: boolean; + announce_ports: { + mix_port: number | null; + verloc_port: number | null; + }; + location: string; + }; + build_information: { + binary_name: string; + build_timestamp: string; + build_version: string; + cargo_profile: string; + cargo_triple: string; + }; + declared_role: { + entry: boolean; + exit_ipr: boolean; + exit_nr: boolean; + mixnode: boolean; + }; + host_information: { + hostname: string; + ip_address: string[]; + keys: { + ed25519: string; + x25519: string; + x25519_noise: string | null; + }; + }; + ip_packet_router: { + address: string; + }; + last_polled: string; + mixnet_websockets: { + ws_port: number; + wss_port: number | null; + }; + network_requester: { + address: string; + uses_exit_policy: boolean; + }; + wireguard: { + port: number; + public_key: string; + }; + }; +}; + +type BalanceDetails = { + amount: number; + denom: string; +}; + +export type ObservatoryRewards = { + operator_commissions: BalanceDetails; + staking_rewards: BalanceDetails; + unlocked: BalanceDetails; +}; + +export type ObservatoryBalance = { + delegated: BalanceDetails; + locked: BalanceDetails; + rewards: ObservatoryRewards; + self_bonded: BalanceDetails; + spendable: BalanceDetails; +}; + +export type Quote = { + ath_date: string; + ath_price: number; + market_cap: number; + market_cap_change_24h: number; + percent_change_12h: number; + percent_change_15m: number; + percent_change_1h: number; + percent_change_1y: number; + percent_change_24h: number; + percent_change_30d: number; + percent_change_30m: number; + percent_change_6h: number; + percent_change_7d: number; + percent_from_price_ath: number; + price: number; + volume_24h: number; + volume_24h_change_24h: number; +}; + +export type Quotes = { + USD: Quote; +}; + +export type NymTokenomics = { + beta_value: number; + first_data_at: string; + id: string; + last_updated: string; + max_supply: number; + name: string; + quotes: Quotes; + rank: number; + symbol: string; + total_supply: number; +}; diff --git a/explorer-v2/src/app/api/urls.ts b/explorer-v2/src/app/api/urls.ts new file mode 100644 index 0000000000..0eb0630bef --- /dev/null +++ b/explorer-v2/src/app/api/urls.ts @@ -0,0 +1,28 @@ +export const NYM_NODES = + "https://explorer.nymtech.net/api/v1/tmp/unstable/nym-nodes"; +export const HARBOURMASTER_API_MIXNODES_STATS = + "https://harbourmaster.nymtech.net/v2/mixnodes/stats"; +export const NS_API_MIXNODES_STATS = + "https://staging-node-status-api.nymte.ch/v2/mixnodes/stats"; + +export const CURRENT_EPOCH = + "https://validator.nymtech.net/api/v1/epoch/current"; +export const CURRENT_EPOCH_REWARDS = + "https://validator.nymtech.net/api/v1/epoch/reward_params"; +export const NYM_NODE_BONDED = + "https://validator.nymtech.net/api/v1/nym-nodes/bonded"; +export const NYM_ACCOUNT_ADDRESS = + "https://explorer.nymtech.net/api/v1/tmp/unstable/account"; +export const NYM_PRICES_API = "https://api.nym.spectredao.net/api/v1/nym-price"; +export const VALIDATOR_BASE_URL = + process.env.NEXT_PUBLIC_VALIDATOR_URL || "https://rpc.nymtech.net"; +export const DATA_OBSERVATORY_NODES_URL = + "https://api.nym.spectredao.net/api/v1/nodes"; +export const DATA_OBSERVATORY_NODES_DELEGATIONS_URL = + "https://api.nym.spectredao.net/api/v1/nodes"; +export const DATA_OBSERVATORY_DELEGATIONS_URL = + "https://api.nym.spectredao.net/api/v1/delegations"; +export const DATA_OBSERVATORY_BALANCES_URL = + "https://api.nym.spectredao.net/api/v1/balances"; +export const OBSERVATORY_GATEWAYS_URL = + "https://mainnet-node-status-api.nymtech.cc/v2/gateways"; diff --git a/explorer-v2/src/app/constants.ts b/explorer-v2/src/app/constants.ts new file mode 100644 index 0000000000..0bd154131a --- /dev/null +++ b/explorer-v2/src/app/constants.ts @@ -0,0 +1 @@ +export const TABLET_WIDTH = "(min-width:700px)"; diff --git a/explorer-v2/src/app/error.tsx b/explorer-v2/src/app/error.tsx new file mode 100644 index 0000000000..b15e0e8c58 --- /dev/null +++ b/explorer-v2/src/app/error.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { ContentLayout } from "@/components/contentLayout/ContentLayout"; +import { Link } from "@/components/muiLink"; +import { Button, Stack, Typography } from "@mui/material"; + +const ErrorPage = ({ error }: { error: Error }) => { + return ( + + + + An error occurred: {error.message} + + + Please try again later or contact support + + + + + + + ); +}; + +export default ErrorPage; diff --git a/explorer-v2/src/app/favicon.png b/explorer-v2/src/app/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..9d12bdcdc08b8e8f25d5e18954b10fdd2d211f09 GIT binary patch literal 1144 zcmV-;1c&>HP)Px(GD$>1RCr$Pol8#KKoEwzB^Fs^L8RHtE;#|p34kc);0hq+0D%KUf-B@4Q7|Wn zI6;=P87VBXV1YDAjUV**)m7y#d%(XXga(((*BTS^1Z}77k%@!IA%^_GiCBu>*e(&(>TY+F%E=>=p4ZuA4C+u zVs&vDjC$7TVN@y*2hj@v;-?r&{SZ019q7^~| z0Md?$_=VGEI@$pM@h!z~FU&On=bJI!0?y%2%Nw@}07n0)!=kDO0RW8tQ4zR23bp_+ z`p1>NINLS~3IG`W6ClU|6ze>6(}itx1^gBO)_O)4{&`C3ao0PU`gGz904CKx2Y{Qe z=^y~WblT@19!9ha0mB)l3eQ3(L48abm$(>KB)a2 ze*P&smd^S2+egtcTw~J+Ao1zPVk1!=&)z%wE!}1TfJXe=FTbSPi0x@)N+HYwfaT~9 zDN!ntrlVN^pwXWy@5jR8;m4<#$C0MOh+NYVrVL;%oksYsGKAoVSQE1C~mJ`d33 z@vx9urXc`eL;wItW2t68Gy*Ks5F&sP0RXTE5nz>u5CO~x0Du8ekfH`yr6EKBGXls= z12VQO0RRF37HJ3o1OPP460zq403ZNhQ3n7(2tbpD`sx4xi~yi74FP}a500093q^JV`Fc5$y4TW_8 z044wsPDcS?AOKAo3YQZAU;+T)x)1;)0HCP@Wf}qi0C<_p(RFT`yW*~;?!LdrKIWqF zSst^O|0T)2Ak9q~*LP?a`=p)5=c@pqU80r6ZYYfdf-412cbx z7A6L=@?H9>i4Pig@>NS_xnAD*vWpS`B$JivDDNv-_7S62<>;iu)h>Y@jsERwXz2^h zd=jY2cJbU*HS8scC}0aX%&;8&I}t!HFeM!(Ebr*up2BG8%u_9qRivRa>Omr%+uiV5 zngfCfgjVqzHK2ZK)Pxi;nrSEW+Nc45kV5p*N5A;d2ngQ_qCo^Eb8ahs7CR~$0f>m0 zgw_5+Y@YB{jIiC;4D&|Nj&9zLi(T}3E?Y3N{T;QX<&5?=asC0s{@1#injjwl0000< KMNUMnLSTZhQswsm literal 0 HcmV?d00001 diff --git a/explorer-v2/src/app/features/footer/api/getFooter.ts b/explorer-v2/src/app/features/footer/api/getFooter.ts new file mode 100644 index 0000000000..5c5202d30b --- /dev/null +++ b/explorer-v2/src/app/features/footer/api/getFooter.ts @@ -0,0 +1,34 @@ +// API +import { client } from "../../../lib/strapiClient"; + +// Types +import type { Languages } from "../../../i18n"; + +import type { components } from "@/app/lib/strapi"; +// Constants +import { footerApiPath } from "../../footer/config/constants"; + +// Fetch footer data +export const getFooter = async ( + locale: Languages, +): Promise<{ + id?: number; + attributes?: components["schemas"]["Footer"]; +} | null> => { + const footer = await client.GET(footerApiPath, { + params: { + query: { + locale, + // @ts-expect-error - populate is not typed correctly? + + populate: { + linkBlocks: { + populate: "*", + }, + }, + }, + }, + }); + + return footer?.data?.data ? footer?.data?.data : null; +}; diff --git a/explorer-v2/src/app/features/footer/components/footer-links.tsx b/explorer-v2/src/app/features/footer/components/footer-links.tsx new file mode 100644 index 0000000000..1d2272cc18 --- /dev/null +++ b/explorer-v2/src/app/features/footer/components/footer-links.tsx @@ -0,0 +1,72 @@ +// Types +import type { components } from "../../../lib/strapi"; + +// Components +import { Link } from "@/components/muiLink"; + +// MUI Components +import { Box, Grid2, Typography } from "@mui/material"; + +export const FooterLinks = ({ + linkBlocks = [], +}: { + linkBlocks: components["schemas"]["FooterLinkBlockComponent"][]; +}) => { + return ( + + {linkBlocks?.map((block) => { + return ( + + + {block?.heading?.title} + + + {block?.links?.map((link) => { + const isLinkExternal = link.url?.startsWith("http"); + return ( + + + + {link.title} + {isLinkExternal ? " ↗" : ""} + + + + ); + })} + + + ); + })} + + ); +}; diff --git a/explorer-v2/src/app/features/footer/config/constants.ts b/explorer-v2/src/app/features/footer/config/constants.ts new file mode 100644 index 0000000000..9ec398e59d --- /dev/null +++ b/explorer-v2/src/app/features/footer/config/constants.ts @@ -0,0 +1 @@ +export const footerApiPath = "/footer"; diff --git a/explorer-v2/src/app/globals.css b/explorer-v2/src/app/globals.css new file mode 100644 index 0000000000..2a6043d863 --- /dev/null +++ b/explorer-v2/src/app/globals.css @@ -0,0 +1,68 @@ +:root { + --max-width: 1120px; + --border-radius: 8px; +} + +@media (prefers-color-scheme: dark) { + :root { + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + } +} + +* { + box-sizing: border-box; + padding: 0; + margin: 0; +} + +html, +body { + max-width: 100vw; +} + +a { + color: inherit; + text-decoration: none; +} + +@media (prefers-color-scheme: dark) { + html { + color-scheme: dark; + } +} + +.MuiCardActionArea-focusHighlight { + background: transparent; +} + +::-webkit-scrollbar-track { + background-color: transparent; +} + +::-webkit-scrollbar { + right: 0; + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-thumb { + background-color: #aaa; + border-radius: 8px; + width: 8px; + height: 8px; +} + +.reactMarkDownLink a { + color: #000000; + display: inline; + font-weight: 700; +} + +.reactMarkDownList ul { + margin-left: 20px; +} + +.reactMarkDownList ol { + margin-left: 20px; +} diff --git a/explorer-v2/src/app/i18n/index.ts b/explorer-v2/src/app/i18n/index.ts new file mode 100644 index 0000000000..997cc5d311 --- /dev/null +++ b/explorer-v2/src/app/i18n/index.ts @@ -0,0 +1,41 @@ +import { createInstance } from "i18next"; +import resourcesToBackend from "i18next-resources-to-backend"; +import { initReactI18next } from "react-i18next/initReactI18next"; +import { getOptions } from "./settings"; +import type { Languages } from "./types"; + +const initI18next = async ({ lng, ns }: { lng: Languages; ns?: string }) => { + const i18nInstance = createInstance(); + await i18nInstance + .use(initReactI18next) + .use( + resourcesToBackend( + (language: string, namespace: string) => + import(`./locales/${language}/${namespace}.json`), + ), + ) + .init(getOptions(lng, ns)); + return i18nInstance; +}; + +export const useTranslation = async ({ + lng, + ns, + options, +}: { + lng: Languages; + ns?: string; + options?: { keyPrefix: string }; +}) => { + const i18nextInstance = await initI18next({ lng, ns }); + return { + t: i18nextInstance.getFixedT( + lng, + Array.isArray(ns) ? ns[0] : ns, + options?.keyPrefix, + ), + i18n: i18nextInstance, + }; +}; + +export type { Languages, GeneralTranslations } from "./types"; diff --git a/explorer-v2/src/app/i18n/locales/en/footer.json b/explorer-v2/src/app/i18n/locales/en/footer.json new file mode 100644 index 0000000000..4903474d4b --- /dev/null +++ b/explorer-v2/src/app/i18n/locales/en/footer.json @@ -0,0 +1,92 @@ +{ + "langSelectorLabel": "Language", + "socialLinks": [ + { "name": "Telegram", "link": "https://nymtech.net/go/telegram" }, + { "name": "Twitter", "link": "https://nymtech.net/go/x" }, + { + "name": "Discord", + "link": "https://nymtech.net/go/discord" + }, + { "name": "GitHub", "link": "https://nymtech.net/go/github" }, + { "name": "YouTube", "link": "https://nymtech.net/go/youtube" } + ], + "links": [ + { + "title": "Get started", + "links": [ + { + "name": "Download NymVPN", + "link": "download" + }, + { + "name": "Test NymVPN", + "link": "alpha" + }, + { + "name": "Become an operator", + "link": "https://nymtech.net/operators" + }, + { "name": "Visit nymtech.net", "link": "https://nymtech.net" }, + { + "name": "Subscribe to Nym’s newsletter", + "link": "https://eepurl.com/gdor_f" + } + ] + }, + { + "title": "Popular blog posts", + "links": [ + { "name": "Decentralized VPNs", "link": "blog/decentralized-vpns" }, + { + "name": "Blockchain-based VPNs", + "link": "blog/blockchain-based-vpns-all-you-need-to-know" + }, + { + "name": "Privacy protection with VPN", + "link": "blog/how-does-a-vpn-protect-you-and-your-privacy" + }, + { + "name": "Tracking prevention with VPN", + "link": "blog/can-you-be-tracked-while-using-a-vpn" + }, + { + "name": "Online privacy threats", + "link": "blog/internet-privacy-main-threats-and-protections" + } + ] + }, + { + "title": "Resources", + "links": [ + { "name": "NymVPN blog", "link": "blog" }, + { + "name": "NymVPN public roadmap", + "link": "https://trello.com/b/qVhBo3e2/nymvpn-public-roadmap" + }, + { + "name": "NymVPN localization", + "link": "https://weblate.nymte.ch/projects/nymvpn/" + } + ] + }, + { + "title": "Company", + "links": [ + { "name": "Contact", "link": "contact" }, + { + "name": "Careers", + "link": "https://nym.teamtailor.com/" + }, + { + "name": "Support", + "link": "https://support.nymvpn.com/hc/en-us" + }, + { "name": "Imprint", "link": "imprint" }, + { "name": "Privacy statements", "link": "privacy" }, + { "name": "Terms of use", "link": "terms" } + ] + } + ], + "trademarkText": "WireGuard is a registered trademark of Jason A. Donenfeld", + "rightsText": "© 2024 Nym Technologies S.A., all rights reserved" +} diff --git a/explorer-v2/src/app/i18n/locales/en/social-channels.json b/explorer-v2/src/app/i18n/locales/en/social-channels.json new file mode 100644 index 0000000000..16550a018c --- /dev/null +++ b/explorer-v2/src/app/i18n/locales/en/social-channels.json @@ -0,0 +1,12 @@ +{ + "socialChannels": [ + { "name": "Telegram", "link": "https://nym.com/go/telegram" }, + { "name": "Twitter", "link": "https://nym.com/go/x" }, + { + "name": "Discord", + "link": "https://nym.com/go/discord" + }, + { "name": "GitHub", "link": "https://nym.com/go/github" }, + { "name": "YouTube", "link": "https://nym.com/go/youtube" } + ] +} diff --git a/explorer-v2/src/app/i18n/settings.ts b/explorer-v2/src/app/i18n/settings.ts new file mode 100644 index 0000000000..8f2fc2179a --- /dev/null +++ b/explorer-v2/src/app/i18n/settings.ts @@ -0,0 +1,16 @@ +export const fallbackLng = "en"; +export const languages = [fallbackLng, "es"] as const; +export const defaultNS = "translation"; +export const cookieName = "i18next"; + +export function getOptions(lng = fallbackLng, ns = defaultNS) { + return { + // debug: true, + supportedLngs: languages, + fallbackLng, + lng, + fallbackNS: defaultNS, + defaultNS, + ns, + }; +} diff --git a/explorer-v2/src/app/i18n/types/freepass.ts b/explorer-v2/src/app/i18n/types/freepass.ts new file mode 100644 index 0000000000..056e81767e --- /dev/null +++ b/explorer-v2/src/app/i18n/types/freepass.ts @@ -0,0 +1,46 @@ +export interface RedeemFreePassCopy { + errors: { + invalidCode: string; + alreadyRedeemed: string; + unknown: string; + }; +} + +type GetStarted = { + title: string; + description: string; + ctaText: string; + inputLabel: string; + prompt: string; +}; + +export type AlphaPageSteps = { + title: string; + description: string; +}; + +export type AlphaPageTranslations = { + hero: { + title: { + firstLine: string; + secondLine?: string; + }; + getCredential: GetStarted; + signUp: GetStarted; + imageAlt: string; + stepsTitle: string; + finalSteps: AlphaPageSteps[]; + finalStepsAlert: string; + credentialsTitle: string; + credentialsButton: string; + }; + redeemFreePass: RedeemFreePassCopy; + firstSection: { + title: string; + content: string[]; + }; + secondSection: { + title: string; + content: string[]; + }; +}; diff --git a/explorer-v2/src/app/i18n/types/index.ts b/explorer-v2/src/app/i18n/types/index.ts new file mode 100644 index 0000000000..4e33a30ab1 --- /dev/null +++ b/explorer-v2/src/app/i18n/types/index.ts @@ -0,0 +1,13 @@ +import type { languages } from "../settings"; + +export type Languages = (typeof languages)[number]; + +// They don't belong to any specific page, that's why I defined them here, but open to suggestions. +export type GeneralTranslations = { + alert: string; + copyToClipboard: { + copy: string; + copied: string; + copiedTimeLimitation: string; + }; +}; diff --git a/explorer-v2/src/app/icon.png b/explorer-v2/src/app/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9d12bdcdc08b8e8f25d5e18954b10fdd2d211f09 GIT binary patch literal 1144 zcmV-;1c&>HP)Px(GD$>1RCr$Pol8#KKoEwzB^Fs^L8RHtE;#|p34kc);0hq+0D%KUf-B@4Q7|Wn zI6;=P87VBXV1YDAjUV**)m7y#d%(XXga(((*BTS^1Z}77k%@!IA%^_GiCBu>*e(&(>TY+F%E=>=p4ZuA4C+u zVs&vDjC$7TVN@y*2hj@v;-?r&{SZ019q7^~| z0Md?$_=VGEI@$pM@h!z~FU&On=bJI!0?y%2%Nw@}07n0)!=kDO0RW8tQ4zR23bp_+ z`p1>NINLS~3IG`W6ClU|6ze>6(}itx1^gBO)_O)4{&`C3ao0PU`gGz904CKx2Y{Qe z=^y~WblT@19!9ha0mB)l3eQ3(L48abm$(>KB)a2 ze*P&smd^S2+egtcTw~J+Ao1zPVk1!=&)z%wE!}1TfJXe=FTbSPi0x@)N+HYwfaT~9 zDN!ntrlVN^pwXWy@5jR8;m4<#$C0MOh+NYVrVL;%oksYsGKAoVSQE1C~mJ`d33 z@vx9urXc`eL;wItW2t68Gy*Ks5F&sP0RXTE5nz>u5CO~x0Du8ekfH`yr6EKBGXls= z12VQO0RRF37HJ3o1OPP460zq403ZNhQ3n7(2tbpD`sx4xi~yi74FP}a500093q^JV`Fc5$y4TW_8 z044wsPDcS?AOKAo3YQZAU;+T)x)1;)0HCP@Wf}qi0C<_p(RFT`yW*~;?!LdrKIWqF zSst^O|0T)2Ak9q~*LP?a`=p)5=c@pqU80r6ZYYfdf-412cbx z7A6L=@?H9>i4Pig@>NS_xnAD*vWpS`B$JivDDNv-_7S62<>;iu)h>Y@jsERwXz2^h zd=jY2cJbU*HS8scC}0aX%&;8&I}t!HFeM!(Ebr*up2BG8%u_9qRivRa>Omr%+uiV5 zngfCfgjVqzHK2ZK)Pxi;nrSEW+Nc45kV5p*N5A;d2ngQ_qCo^Eb8ahs7CR~$0f>m0 zgw_5+Y@YB{jIiC;4D&|Nj&9zLi(T}3E?Y3N{T;QX<&5?=asC0s{@1#injjwl0000< KMNUMnLSTZhQswsm literal 0 HcmV?d00001 diff --git a/explorer-v2/src/app/layout.tsx b/explorer-v2/src/app/layout.tsx new file mode 100644 index 0000000000..4d1266f7ed --- /dev/null +++ b/explorer-v2/src/app/layout.tsx @@ -0,0 +1,30 @@ +import { Header } from "@/components/header"; +import { Wrapper } from "@/components/wrapper"; +import Providers from "@/providers"; +import type { Metadata } from "next"; + +import "./globals.css"; +import "@interchain-ui/react/styles"; +import { Footer } from "@/components/footer"; + +export const metadata: Metadata = { + title: "Nym Explorer V2", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + +
    + {children} +
    + + + + ); +} diff --git a/explorer-v2/src/app/lib/strapi.d.ts b/explorer-v2/src/app/lib/strapi.d.ts new file mode 100644 index 0000000000..2c177527cb --- /dev/null +++ b/explorer-v2/src/app/lib/strapi.d.ts @@ -0,0 +1,23417 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/about-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/about-page"]; + put: operations["put/about-page"]; + post?: never; + delete: operations["delete/about-page"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/about-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/about-page/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/basic-pages": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/basic-pages"]; + put?: never; + post: operations["post/basic-pages"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/basic-pages/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/basic-pages/{id}"]; + put: operations["put/basic-pages/{id}"]; + post?: never; + delete: operations["delete/basic-pages/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/basic-pages/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/basic-pages/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/blog-authors": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/blog-authors"]; + put?: never; + post: operations["post/blog-authors"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/blog-authors/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/blog-authors/{id}"]; + put: operations["put/blog-authors/{id}"]; + post?: never; + delete: operations["delete/blog-authors/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/blog-authors/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/blog-authors/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/blog-categories": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/blog-categories"]; + put?: never; + post: operations["post/blog-categories"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/blog-categories/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/blog-categories/{id}"]; + put: operations["put/blog-categories/{id}"]; + post?: never; + delete: operations["delete/blog-categories/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/blog-categories/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/blog-categories/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/blog-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/blog-page"]; + put: operations["put/blog-page"]; + post?: never; + delete: operations["delete/blog-page"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/blog-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/blog-page/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/blog-posts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/blog-posts"]; + put?: never; + post: operations["post/blog-posts"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/blog-posts/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/blog-posts/{id}"]; + put: operations["put/blog-posts/{id}"]; + post?: never; + delete: operations["delete/blog-posts/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/blog-posts/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/blog-posts/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/downloads": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/downloads"]; + put?: never; + post: operations["post/downloads"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/downloads/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/downloads/{id}"]; + put: operations["put/downloads/{id}"]; + post?: never; + delete: operations["delete/downloads/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/downloads/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/downloads/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/downloads-menu": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/downloads-menu"]; + put: operations["put/downloads-menu"]; + post?: never; + delete: operations["delete/downloads-menu"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/downloads-menu/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/downloads-menu/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/features": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/features"]; + put?: never; + post: operations["post/features"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/features/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/features/{id}"]; + put: operations["put/features/{id}"]; + post?: never; + delete: operations["delete/features/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/features/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/features/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/feature-categories": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/feature-categories"]; + put?: never; + post: operations["post/feature-categories"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/feature-categories/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/feature-categories/{id}"]; + put: operations["put/feature-categories/{id}"]; + post?: never; + delete: operations["delete/feature-categories/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/feature-categories/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/feature-categories/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/features-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/features-page"]; + put: operations["put/features-page"]; + post?: never; + delete: operations["delete/features-page"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/features-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/features-page/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/footer": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/footer"]; + put: operations["put/footer"]; + post?: never; + delete: operations["delete/footer"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/footer/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/footer/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/glossary-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/glossary-page"]; + put: operations["put/glossary-page"]; + post?: never; + delete: operations["delete/glossary-page"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/glossary-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/glossary-page/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/glossary-terms": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/glossary-terms"]; + put?: never; + post: operations["post/glossary-terms"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/glossary-terms/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/glossary-terms/{id}"]; + put: operations["put/glossary-terms/{id}"]; + post?: never; + delete: operations["delete/glossary-terms/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/glossary-terms/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/glossary-terms/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/home-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/home-page"]; + put: operations["put/home-page"]; + post?: never; + delete: operations["delete/home-page"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/home-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/home-page/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/litepaper": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/litepaper"]; + put: operations["put/litepaper"]; + post?: never; + delete: operations["delete/litepaper"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/litepaper/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/litepaper/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/navigation": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/navigation"]; + put: operations["put/navigation"]; + post?: never; + delete: operations["delete/navigation"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/navigation/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/navigation/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/network-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/network-page"]; + put: operations["put/network-page"]; + post?: never; + delete: operations["delete/network-page"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/network-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/network-page/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/page-not-found": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/page-not-found"]; + put: operations["put/page-not-found"]; + post?: never; + delete: operations["delete/page-not-found"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/page-not-found/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/page-not-found/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/press-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/press-page"]; + put: operations["put/press-page"]; + post?: never; + delete: operations["delete/press-page"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/press-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/press-page/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/press-posts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/press-posts"]; + put?: never; + post: operations["post/press-posts"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/press-posts/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/press-posts/{id}"]; + put: operations["put/press-posts/{id}"]; + post?: never; + delete: operations["delete/press-posts/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/press-posts/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/press-posts/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/team-members": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/team-members"]; + put?: never; + post: operations["post/team-members"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/team-members/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/team-members/{id}"]; + put: operations["put/team-members/{id}"]; + post?: never; + delete: operations["delete/team-members/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/team-members/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/team-members/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/trust-center-advisors-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/trust-center-advisors-page"]; + put: operations["put/trust-center-advisors-page"]; + post?: never; + delete: operations["delete/trust-center-advisors-page"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/trust-center-advisors-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/trust-center-advisors-page/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/trust-center-categories": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/trust-center-categories"]; + put?: never; + post: operations["post/trust-center-categories"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/trust-center-categories/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/trust-center-categories/{id}"]; + put: operations["put/trust-center-categories/{id}"]; + post?: never; + delete: operations["delete/trust-center-categories/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/trust-center-categories/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/trust-center-categories/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/trust-center-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/trust-center-page"]; + put: operations["put/trust-center-page"]; + post?: never; + delete: operations["delete/trust-center-page"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/trust-center-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/trust-center-page/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/trust-center-posts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/trust-center-posts"]; + put?: never; + post: operations["post/trust-center-posts"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/trust-center-posts/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/trust-center-posts/{id}"]; + put: operations["put/trust-center-posts/{id}"]; + post?: never; + delete: operations["delete/trust-center-posts/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/trust-center-posts/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/trust-center-posts/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/use-cases": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/use-cases"]; + put?: never; + post: operations["post/use-cases"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/use-cases/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/use-cases/{id}"]; + put: operations["put/use-cases/{id}"]; + post?: never; + delete: operations["delete/use-cases/{id}"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/use-cases/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/use-cases/{id}/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/use-cases-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get/use-cases-page"]; + put: operations["put/use-cases-page"]; + post?: never; + delete: operations["delete/use-cases-page"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/use-cases-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["post/use-cases-page/localizations"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/upload": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Upload files */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Upload files */ + requestBody: { + content: { + "multipart/form-data": { + /** @description The folder where the file(s) will be uploaded to (only supported on strapi-provider-upload-aws-s3). */ + path?: string; + /** @description The ID of the entry which the file(s) will be linked to */ + refId?: string; + /** @description The unique ID (uid) of the model which the file(s) will be linked to (api::restaurant.restaurant). */ + ref?: string; + /** @description The field of the entry which the file(s) will be precisely linked to. */ + field?: string; + files: string[]; + }; + }; + }; + responses: { + /** @description response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UploadFile"][]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/upload?id={id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Upload file information */ + post: { + parameters: { + query: { + /** @description File id */ + id: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Upload files */ + requestBody: { + content: { + "multipart/form-data": { + fileInfo?: { + name?: string; + alternativeText?: string; + caption?: string; + }; + /** Format: binary */ + files?: string; + }; + }; + }; + responses: { + /** @description response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UploadFile"][]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/upload/files": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Get a list of files */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UploadFile"][]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/upload/files/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Get a specific file */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UploadFile"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Delete a file */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UploadFile"]; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/connect/{provider}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Login with a provider + * @description Redirects to provider login before being redirect to /auth/{provider}/callback + */ + get: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Provider name */ + provider: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Redirect response */ + 301: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/local": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Local login + * @description Returns a jwt token and user info + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** @example { + * "identifier": "foobar", + * "password": "Test1234" + * } */ + "application/json": { + identifier?: string; + password?: string; + }; + }; + }; + responses: { + /** @description Connection */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Users-Permissions-UserRegistration"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/local/register": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Register a user + * @description Returns a jwt token and user info + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** @example { + * "username": "foobar", + * "email": "foo.bar@strapi.io", + * "password": "Test1234" + * } */ + "application/json": { + username?: string; + email?: string; + password?: string; + }; + }; + }; + responses: { + /** @description Successful registration */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Users-Permissions-UserRegistration"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/{provider}/callback": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Default Callback from provider auth */ + get: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Provider name */ + provider: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a jwt token and user info */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Users-Permissions-UserRegistration"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/forgot-password": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Send rest password email */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** @example { + * "email": "foo.bar@strapi.io" + * } */ + "application/json": { + email?: string; + }; + }; + }; + responses: { + /** @description Returns ok */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @enum {string} */ + ok?: true; + }; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/reset-password": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Rest user password */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** @example { + * "password": "Test1234", + * "passwordConfirmation": "Test1234", + * "code": "zertyoaizndoianzodianzdonaizdoinaozdnia" + * } */ + "application/json": { + password?: string; + passwordConfirmation?: string; + code?: string; + }; + }; + }; + responses: { + /** @description Returns a jwt token and user info */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Users-Permissions-UserRegistration"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/change-password": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Update user's own password */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + password: string; + currentPassword: string; + passwordConfirmation: string; + }; + }; + }; + responses: { + /** @description Returns a jwt token and user info */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Users-Permissions-UserRegistration"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/email-confirmation": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Confirm user email */ + get: { + parameters: { + query?: { + /** @description confirmation token received by email */ + confirmation?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Redirects to the configure email confirmation redirect url */ + 301: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/send-email-confirmation": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Send confirmation email */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + email?: string; + }; + }; + }; + responses: { + /** @description Returns email and boolean to confirm email was sent */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + email?: string; + /** @enum {string} */ + sent?: true; + }; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users-permissions/permissions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get default generated permissions */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns the permissions tree */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + permissions?: components["schemas"]["Users-Permissions-PermissionsTree"]; + }; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users-permissions/roles": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List roles */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns list of roles */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + roles?: (components["schemas"]["Users-Permissions-Role"] & { + nb_users?: number; + })[]; + }; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + /** Create a role */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: components["requestBodies"]["Users-Permissions-RoleRequest"]; + responses: { + /** @description Returns ok if the role was create */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @enum {string} */ + ok?: true; + }; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users-permissions/roles/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get a role */ + get: { + parameters: { + query?: never; + header?: never; + path: { + /** @description role Id */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns the role */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + role?: components["schemas"]["Users-Permissions-Role"]; + }; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users-permissions/roles/{role}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Update a role */ + put: { + parameters: { + query?: never; + header?: never; + path: { + /** @description role Id */ + role: string; + }; + cookie?: never; + }; + requestBody: components["requestBodies"]["Users-Permissions-RoleRequest"]; + responses: { + /** @description Returns ok if the role was udpated */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @enum {string} */ + ok?: true; + }; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + post?: never; + /** Delete a role */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + /** @description role Id */ + role: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns ok if the role was delete */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @enum {string} */ + ok?: true; + }; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get list of users */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns an array of users */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Users-Permissions-User"][]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + /** Create a user */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** @example { + * "username": "foo", + * "email": "foo@strapi.io", + * "password": "foo-password" + * } */ + "application/json": { + email: string; + username: string; + password: string; + }; + }; + }; + responses: { + /** @description Returns created user info */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Users-Permissions-User"] & { + role?: components["schemas"]["Users-Permissions-Role"]; + }; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get a user */ + get: { + parameters: { + query?: never; + header?: never; + path: { + /** @description user Id */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a user */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Users-Permissions-User"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + /** Update a user */ + put: { + parameters: { + query?: never; + header?: never; + path: { + /** @description user Id */ + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + /** @example { + * "username": "foo", + * "email": "foo@strapi.io", + * "password": "foo-password" + * } */ + "application/json": { + email: string; + username: string; + password: string; + }; + }; + }; + responses: { + /** @description Returns updated user info */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Users-Permissions-User"] & { + role?: components["schemas"]["Users-Permissions-Role"]; + }; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + post?: never; + /** Delete a user */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + /** @description user Id */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns deleted user info */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Users-Permissions-User"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get authenticated user info */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns user info */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Users-Permissions-User"]; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users/count": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get user count */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Returns a number */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Error */ + default: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + Error: { + data?: (Record | Record[]) | null; + error: { + status?: number; + name?: string; + message?: string; + details?: Record; + }; + }; + AboutPageLocalizationRequest: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + )[]; + locale: string; + }; + AboutPageRequest: { + data: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + )[]; + locale?: string; + }; + }; + AboutPageResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["AboutPage"]; + }; + AboutPageLocalizationResponse: { + data?: components["schemas"]["AboutPageResponseDataObjectLocalized"]; + meta?: Record; + }; + AboutPageListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["AboutPage"]; + }; + AboutPageLocalizationListResponse: { + data?: components["schemas"]["AboutPageListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + AboutPageListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["AboutPage"]; + }; + AboutPageListResponse: { + data?: components["schemas"]["AboutPageListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + AboutPage: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + )[]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["AboutPageListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + AboutPageResponseDataObject: { + id?: number; + attributes?: components["schemas"]["AboutPage"]; + }; + AboutPageResponse: { + data?: components["schemas"]["AboutPageResponseDataObject"]; + meta?: Record; + }; + SharedSeoComponent: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: { + name?: string; + pathId?: number; + parent?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + children?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + files?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: { + firstname?: string; + lastname?: string; + username?: string; + /** Format: email */ + email?: string; + resetPasswordToken?: string; + registrationToken?: string; + isActive?: boolean; + roles?: { + data?: { + id?: number; + attributes?: { + name?: string; + code?: string; + description?: string; + users?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + permissions?: { + data?: { + id?: number; + attributes?: { + action?: string; + actionParameters?: unknown; + subject?: string; + properties?: unknown; + conditions?: unknown; + role?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + blocked?: boolean; + preferedLanguage?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + path?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + UiLinkComponent: { + id?: number; + title?: string; + url?: string; + }; + DynamicZoneValuePropositionWithTextComponent: { + id?: number; + __component?: string; + title?: string; + text?: string; + links?: components["schemas"]["UiLinkComponent"][]; + media?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** @enum {string} */ + direction?: "text-right" | "text-left"; + }; + UiBulletWithImageComponent: { + id?: number; + title?: string; + text?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + }; + DynamicZoneValuePropositionWithBulletsComponent: { + id?: number; + __component?: string; + title?: string; + links?: components["schemas"]["UiLinkComponent"][]; + media?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** @enum {string} */ + direction?: "text-right" | "text-left"; + bullets?: components["schemas"]["UiBulletWithImageComponent"][]; + }; + UiHeadingComponent: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + TilesTileComponent: { + id?: number; + title?: string; + subtitle?: string; + description?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + url?: string; + }; + DynamicZoneTilesComponent: { + id?: number; + __component?: string; + heading?: components["schemas"]["UiHeadingComponent"]; + tiles?: components["schemas"]["TilesTileComponent"][]; + }; + DynamicZoneTeamMembersComponent: { + id?: number; + __component?: string; + heading?: components["schemas"]["UiHeadingComponent"]; + subtitle?: string; + teamMembers?: { + data?: { + id?: number; + attributes?: { + name?: string; + role?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + bio?: string; + socialNetworks?: { + id?: number; + links?: { + id?: number; + url?: string; + /** @enum {string} */ + network?: "linkedin" | "twitter" | "wikipedia" | "website"; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + }; + QuotesQuoteComponent: { + id?: number; + title?: string; + quote?: string; + }; + DynamicZoneQuotesComponent: { + id?: number; + __component?: string; + heading?: components["schemas"]["UiHeadingComponent"]; + quotes?: components["schemas"]["QuotesQuoteComponent"][]; + }; + UiHeadingExtendedComponent: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + /** @enum {string} */ + style?: "h1" | "h2" | "h3" | "h4"; + /** @enum {string} */ + alignment?: "left" | "center"; + }; + DynamicZoneParagraphWithTitleComponent: { + id?: number; + __component?: string; + heading?: components["schemas"]["UiHeadingExtendedComponent"]; + text?: string; + primaryLinks?: components["schemas"]["UiLinkComponent"][]; + secondaryLinks?: components["schemas"]["UiLinkComponent"][]; + background?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + }; + DynamicZoneLogoCarouselComponent: { + id?: number; + __component?: string; + heading?: components["schemas"]["UiHeadingComponent"]; + isStatic?: boolean; + images?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + }; + TilesContentTileComponent: { + id?: number; + title?: string; + subtitle?: string; + text?: string; + label1?: string; + label2?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + button?: { + id?: number; + title?: string; + url?: string; + }; + dataTable?: string; + }; + DynamicZoneContentTilesComponent: { + id?: number; + __component?: string; + heading?: components["schemas"]["UiHeadingComponent"]; + tiles?: components["schemas"]["TilesContentTileComponent"][]; + }; + DynamicZoneBlogPostsComponent: { + id?: number; + __component?: string; + blogPosts?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + subtitle?: string; + content?: string; + excerpt?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + blogCategories?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + blogPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + description?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + blogAuthors?: { + data?: { + id?: number; + attributes?: { + name?: string; + bio?: string; + avatar?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + slug?: string; + blogPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + role?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + relatedPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** Format: date */ + date?: string; + readingTime?: number; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + heading?: components["schemas"]["UiHeadingComponent"]; + }; + BlocksBlockComponent: { + id?: number; + title?: string; + description?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + }; + DynamicZoneBlocksComponent: { + id?: number; + __component?: string; + heading?: components["schemas"]["UiHeadingComponent"]; + subtitle?: string; + blocks?: components["schemas"]["BlocksBlockComponent"][]; + /** @enum {string} */ + type?: "high-density" | "low-density"; + }; + FaqsQuestionComponent: { + id?: number; + question?: string; + answer?: string; + }; + FaqsFaQsComponent: { + id?: number; + __component?: string; + heading?: components["schemas"]["UiHeadingComponent"]; + questions?: components["schemas"]["FaqsQuestionComponent"][]; + }; + CarouselCarouselSlideQuoteComponent: { + id?: number; + quote?: string; + source?: string; + }; + DynamicZoneCarouselQuoteComponent: { + id?: number; + __component?: string; + heading?: components["schemas"]["UiHeadingComponent"]; + slides?: components["schemas"]["CarouselCarouselSlideQuoteComponent"][]; + }; + CarouselCarouselSlideTextImageComponent: { + id?: number; + text?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + }; + DynamicZoneCarouselTextImageComponent: { + id?: number; + __component?: string; + heading?: components["schemas"]["UiHeadingComponent"]; + slides?: components["schemas"]["CarouselCarouselSlideTextImageComponent"][]; + }; + BasicPageLocalizationRequest: { + title: string; + slug: string; + dynamicZone?: ( + | components["schemas"]["DynamicZoneRichTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + SEO?: components["schemas"]["SharedSeoComponent"]; + locale: string; + }; + BasicPageRequest: { + data: { + title: string; + slug: string; + dynamicZone?: ( + | components["schemas"]["DynamicZoneRichTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + SEO?: components["schemas"]["SharedSeoComponent"]; + locale?: string; + }; + }; + BasicPageResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["BasicPage"]; + }; + BasicPageLocalizationResponse: { + data?: components["schemas"]["BasicPageResponseDataObjectLocalized"]; + meta?: Record; + }; + BasicPageListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["BasicPage"]; + }; + BasicPageLocalizationListResponse: { + data?: components["schemas"]["BasicPageListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + BasicPageListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["BasicPage"]; + }; + BasicPageListResponse: { + data?: components["schemas"]["BasicPageListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + BasicPage: { + title: string; + slug: string; + dynamicZone?: ( + | components["schemas"]["DynamicZoneRichTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + SEO?: components["schemas"]["SharedSeoComponent"]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["BasicPageListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + BasicPageResponseDataObject: { + id?: number; + attributes?: components["schemas"]["BasicPage"]; + }; + BasicPageResponse: { + data?: components["schemas"]["BasicPageResponseDataObject"]; + meta?: Record; + }; + DynamicZoneRichTextComponent: { + id?: number; + __component?: string; + content?: string; + tableOfContents?: boolean; + heading?: components["schemas"]["UiHeadingComponent"]; + }; + BlogAuthorLocalizationRequest: { + name: string; + bio?: string; + /** @example string or id */ + avatar?: number | string; + slug: string; + blogPosts?: (number | string)[]; + role?: string; + locale: string; + }; + BlogAuthorRequest: { + data: { + name: string; + bio?: string; + /** @example string or id */ + avatar?: number | string; + slug: string; + blogPosts?: (number | string)[]; + role?: string; + locale?: string; + }; + }; + BlogAuthorResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["BlogAuthor"]; + }; + BlogAuthorLocalizationResponse: { + data?: components["schemas"]["BlogAuthorResponseDataObjectLocalized"]; + meta?: Record; + }; + BlogAuthorListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["BlogAuthor"]; + }; + BlogAuthorLocalizationListResponse: { + data?: components["schemas"]["BlogAuthorListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + BlogAuthorListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["BlogAuthor"]; + }; + BlogAuthorListResponse: { + data?: components["schemas"]["BlogAuthorListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + BlogAuthor: { + name: string; + bio?: string; + avatar?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: { + name?: string; + pathId?: number; + parent?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + children?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + files?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: { + firstname?: string; + lastname?: string; + username?: string; + /** Format: email */ + email?: string; + resetPasswordToken?: string; + registrationToken?: string; + isActive?: boolean; + roles?: { + data?: { + id?: number; + attributes?: { + name?: string; + code?: string; + description?: string; + users?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + permissions?: { + data?: { + id?: number; + attributes?: { + action?: string; + actionParameters?: unknown; + subject?: string; + properties?: unknown; + conditions?: unknown; + role?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + blocked?: boolean; + preferedLanguage?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + path?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + slug: string; + blogPosts?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + subtitle?: string; + content?: string; + excerpt?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + blogCategories?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + blogPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + description?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + blogAuthors?: { + data?: { + id?: number; + attributes?: { + name?: string; + bio?: string; + avatar?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + slug?: string; + blogPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + role?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + relatedPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** Format: date */ + date?: string; + readingTime?: number; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + role?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["BlogAuthorListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + BlogAuthorResponseDataObject: { + id?: number; + attributes?: components["schemas"]["BlogAuthor"]; + }; + BlogAuthorResponse: { + data?: components["schemas"]["BlogAuthorResponseDataObject"]; + meta?: Record; + }; + BlogCategoryLocalizationRequest: { + title: string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + blogPosts?: (number | string)[]; + description?: string; + locale: string; + }; + BlogCategoryRequest: { + data: { + title: string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + blogPosts?: (number | string)[]; + description?: string; + locale?: string; + }; + }; + BlogCategoryResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["BlogCategory"]; + }; + BlogCategoryLocalizationResponse: { + data?: components["schemas"]["BlogCategoryResponseDataObjectLocalized"]; + meta?: Record; + }; + BlogCategoryListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["BlogCategory"]; + }; + BlogCategoryLocalizationListResponse: { + data?: components["schemas"]["BlogCategoryListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + BlogCategoryListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["BlogCategory"]; + }; + BlogCategoryListResponse: { + data?: components["schemas"]["BlogCategoryListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + BlogCategory: { + title: string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + blogPosts?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + subtitle?: string; + content?: string; + excerpt?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + blogCategories?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + blogPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + description?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + blogAuthors?: { + data?: { + id?: number; + attributes?: { + name?: string; + bio?: string; + avatar?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + slug?: string; + blogPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + role?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + relatedPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** Format: date */ + date?: string; + readingTime?: number; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + description?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["BlogCategoryListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + BlogCategoryResponseDataObject: { + id?: number; + attributes?: components["schemas"]["BlogCategory"]; + }; + BlogCategoryResponse: { + data?: components["schemas"]["BlogCategoryResponseDataObject"]; + meta?: Record; + }; + BlogPageLocalizationRequest: { + featuredPosts?: (number | string)[]; + SEO?: components["schemas"]["SharedSeoComponent"]; + title: string; + categoriesFilter?: (number | string)[]; + featuredCategories?: (number | string)[]; + locale: string; + }; + BlogPageRequest: { + data: { + featuredPosts?: (number | string)[]; + SEO?: components["schemas"]["SharedSeoComponent"]; + title: string; + categoriesFilter?: (number | string)[]; + featuredCategories?: (number | string)[]; + locale?: string; + }; + }; + BlogPageResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["BlogPage"]; + }; + BlogPageLocalizationResponse: { + data?: components["schemas"]["BlogPageResponseDataObjectLocalized"]; + meta?: Record; + }; + BlogPageListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["BlogPage"]; + }; + BlogPageLocalizationListResponse: { + data?: components["schemas"]["BlogPageListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + BlogPageListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["BlogPage"]; + }; + BlogPageListResponse: { + data?: components["schemas"]["BlogPageListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + BlogPage: { + featuredPosts?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + subtitle?: string; + content?: string; + excerpt?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: { + name?: string; + pathId?: number; + parent?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + children?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + files?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: { + firstname?: string; + lastname?: string; + username?: string; + /** Format: email */ + email?: string; + resetPasswordToken?: string; + registrationToken?: string; + isActive?: boolean; + roles?: { + data?: { + id?: number; + attributes?: { + name?: string; + code?: string; + description?: string; + users?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }[]; + }; + permissions?: { + data?: { + id?: number; + attributes?: { + action?: string; + actionParameters?: unknown; + subject?: string; + properties?: unknown; + conditions?: unknown; + role?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + blocked?: boolean; + preferedLanguage?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + path?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + blogCategories?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + blogPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + description?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + blogAuthors?: { + data?: { + id?: number; + attributes?: { + name?: string; + bio?: string; + avatar?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + slug?: string; + blogPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + role?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + relatedPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** Format: date */ + date?: string; + readingTime?: number; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + SEO?: components["schemas"]["SharedSeoComponent"]; + title: string; + categoriesFilter?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + featuredCategories?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["BlogPageListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + BlogPageResponseDataObject: { + id?: number; + attributes?: components["schemas"]["BlogPage"]; + }; + BlogPageResponse: { + data?: components["schemas"]["BlogPageResponseDataObject"]; + meta?: Record; + }; + BlogPostLocalizationRequest: { + title: string; + slug: string; + subtitle?: string; + content?: string; + excerpt?: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + blogCategories?: (number | string)[]; + blogAuthors?: (number | string)[]; + relatedPosts?: (number | string)[]; + /** @example string or id */ + image?: number | string; + /** Format: date */ + date?: string; + readingTime?: number; + locale: string; + }; + BlogPostRequest: { + data: { + title: string; + slug: string; + subtitle?: string; + content?: string; + excerpt?: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + blogCategories?: (number | string)[]; + blogAuthors?: (number | string)[]; + relatedPosts?: (number | string)[]; + /** @example string or id */ + image?: number | string; + /** Format: date */ + date?: string; + readingTime?: number; + locale?: string; + }; + }; + BlogPostResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["BlogPost"]; + }; + BlogPostLocalizationResponse: { + data?: components["schemas"]["BlogPostResponseDataObjectLocalized"]; + meta?: Record; + }; + BlogPostListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["BlogPost"]; + }; + BlogPostLocalizationListResponse: { + data?: components["schemas"]["BlogPostListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + BlogPostListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["BlogPost"]; + }; + BlogPostListResponse: { + data?: components["schemas"]["BlogPostListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + BlogPost: { + title: string; + slug: string; + subtitle?: string; + content?: string; + excerpt?: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + blogCategories?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + blogPosts?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + subtitle?: string; + content?: string; + excerpt?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + blogCategories?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + blogAuthors?: { + data?: { + id?: number; + attributes?: { + name?: string; + bio?: string; + avatar?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + slug?: string; + blogPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + role?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + relatedPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** Format: date */ + date?: string; + readingTime?: number; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + description?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + blogAuthors?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + relatedPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** Format: date */ + date?: string; + readingTime?: number; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["BlogPostListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + BlogPostResponseDataObject: { + id?: number; + attributes?: components["schemas"]["BlogPost"]; + }; + BlogPostResponse: { + data?: components["schemas"]["BlogPostResponseDataObject"]; + meta?: Record; + }; + DownloadLocalizationRequest: { + title: string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + bullets?: components["schemas"]["UiBulletWithImageComponent"][]; + info?: string; + /** @example string or id */ + image?: number | string; + /** @example string or id */ + menuIcon?: number | string; + buttons?: components["schemas"]["UiButtonComponent"][]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneInstallComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + menuTitle?: string; + minimumSupport?: string; + locale: string; + }; + DownloadRequest: { + data: { + title: string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + bullets?: components["schemas"]["UiBulletWithImageComponent"][]; + info?: string; + /** @example string or id */ + image?: number | string; + /** @example string or id */ + menuIcon?: number | string; + buttons?: components["schemas"]["UiButtonComponent"][]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneInstallComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + menuTitle?: string; + minimumSupport?: string; + locale?: string; + }; + }; + DownloadResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["Download"]; + }; + DownloadLocalizationResponse: { + data?: components["schemas"]["DownloadResponseDataObjectLocalized"]; + meta?: Record; + }; + DownloadListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["Download"]; + }; + DownloadLocalizationListResponse: { + data?: components["schemas"]["DownloadListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + DownloadListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["Download"]; + }; + DownloadListResponse: { + data?: components["schemas"]["DownloadListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + Download: { + title: string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + bullets?: components["schemas"]["UiBulletWithImageComponent"][]; + info?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + menuIcon?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + buttons?: components["schemas"]["UiButtonComponent"][]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneInstallComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + menuTitle?: string; + minimumSupport?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["DownloadListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + DownloadResponseDataObject: { + id?: number; + attributes?: components["schemas"]["Download"]; + }; + DownloadResponse: { + data?: components["schemas"]["DownloadResponseDataObject"]; + meta?: Record; + }; + UiButtonComponent: { + id?: number; + title?: string; + url?: string; + /** @enum {string} */ + type?: "primary" | "outlined"; + /** @enum {string} */ + size?: "large" | "small"; + }; + DownloadsHowToDownloadComponent: { + id?: number; + menuTitle?: string; + menuIcon?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + title?: string; + content?: string; + steps?: { + id?: number; + title?: string; + content?: string; + }[]; + }; + DynamicZoneInstallComponent: { + id?: number; + __component?: string; + heading?: components["schemas"]["UiHeadingComponent"]; + subTitle?: string; + versions?: components["schemas"]["DownloadsHowToDownloadComponent"][]; + }; + DownloadsMenuLocalizationRequest: { + downloads?: (number | string)[]; + locale: string; + }; + DownloadsMenuRequest: { + data: { + downloads?: (number | string)[]; + locale?: string; + }; + }; + DownloadsMenuResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["DownloadsMenu"]; + }; + DownloadsMenuLocalizationResponse: { + data?: components["schemas"]["DownloadsMenuResponseDataObjectLocalized"]; + meta?: Record; + }; + DownloadsMenuListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["DownloadsMenu"]; + }; + DownloadsMenuLocalizationListResponse: { + data?: components["schemas"]["DownloadsMenuListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + DownloadsMenuListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["DownloadsMenu"]; + }; + DownloadsMenuListResponse: { + data?: components["schemas"]["DownloadsMenuListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + DownloadsMenu: { + downloads?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: { + name?: string; + pathId?: number; + parent?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + children?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + files?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: { + firstname?: string; + lastname?: string; + username?: string; + /** Format: email */ + email?: string; + resetPasswordToken?: string; + registrationToken?: string; + isActive?: boolean; + roles?: { + data?: { + id?: number; + attributes?: { + name?: string; + code?: string; + description?: string; + users?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }[]; + }; + permissions?: { + data?: { + id?: number; + attributes?: { + action?: string; + actionParameters?: unknown; + subject?: string; + properties?: unknown; + conditions?: unknown; + role?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + blocked?: boolean; + preferedLanguage?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + path?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + bullets?: { + id?: number; + title?: string; + text?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + }[]; + info?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + menuIcon?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + buttons?: { + id?: number; + title?: string; + url?: string; + /** @enum {string} */ + type?: "primary" | "outlined"; + /** @enum {string} */ + size?: "large" | "small"; + }[]; + dynamicZone?: ( + | { + id?: number; + __component?: string; + title?: string; + text?: string; + links?: { + id?: number; + title?: string; + url?: string; + }[]; + media?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** @enum {string} */ + direction?: "text-right" | "text-left"; + } + | { + id?: number; + __component?: string; + title?: string; + links?: { + id?: number; + title?: string; + url?: string; + }[]; + media?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** @enum {string} */ + direction?: "text-right" | "text-left"; + bullets?: { + id?: number; + title?: string; + text?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + }[]; + } + | { + id?: number; + __component?: string; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + tiles?: { + id?: number; + title?: string; + subtitle?: string; + description?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + url?: string; + }[]; + } + | { + id?: number; + __component?: string; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + subtitle?: string; + teamMembers?: { + data?: { + id?: number; + attributes?: { + name?: string; + role?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + bio?: string; + socialNetworks?: { + id?: number; + links?: { + id?: number; + url?: string; + /** @enum {string} */ + network?: + | "linkedin" + | "twitter" + | "wikipedia" + | "website"; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + } + | { + id?: number; + __component?: string; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + quotes?: { + id?: number; + title?: string; + quote?: string; + }[]; + } + | { + id?: number; + __component?: string; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + /** @enum {string} */ + style?: "h1" | "h2" | "h3" | "h4"; + /** @enum {string} */ + alignment?: "left" | "center"; + }; + text?: string; + primaryLinks?: { + id?: number; + title?: string; + url?: string; + }[]; + secondaryLinks?: { + id?: number; + title?: string; + url?: string; + }[]; + background?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + } + | { + id?: number; + __component?: string; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + isStatic?: boolean; + images?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + } + | { + id?: number; + __component?: string; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + tiles?: { + id?: number; + title?: string; + subtitle?: string; + text?: string; + label1?: string; + label2?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + button?: { + id?: number; + title?: string; + url?: string; + }; + dataTable?: string; + }[]; + } + | { + id?: number; + __component?: string; + blogPosts?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + subtitle?: string; + content?: string; + excerpt?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + blogCategories?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + blogPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + description?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + blogAuthors?: { + data?: { + id?: number; + attributes?: { + name?: string; + bio?: string; + avatar?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + slug?: string; + blogPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + role?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + relatedPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** Format: date */ + date?: string; + readingTime?: number; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + } + | { + id?: number; + __component?: string; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + subtitle?: string; + blocks?: { + id?: number; + title?: string; + description?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + }[]; + /** @enum {string} */ + type?: "high-density" | "low-density"; + } + | { + id?: number; + __component?: string; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + questions?: { + id?: number; + question?: string; + answer?: string; + }[]; + } + | { + id?: number; + __component?: string; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + subTitle?: string; + versions?: { + id?: number; + menuTitle?: string; + menuIcon?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + title?: string; + content?: string; + steps?: { + id?: number; + title?: string; + content?: string; + }[]; + }[]; + } + | { + id?: number; + __component?: string; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + slides?: { + id?: number; + text?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + }[]; + } + | { + id?: number; + __component?: string; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + slides?: { + id?: number; + quote?: string; + source?: string; + }[]; + } + )[]; + menuTitle?: string; + minimumSupport?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["DownloadsMenuListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + DownloadsMenuResponseDataObject: { + id?: number; + attributes?: components["schemas"]["DownloadsMenu"]; + }; + DownloadsMenuResponse: { + data?: components["schemas"]["DownloadsMenuResponseDataObject"]; + meta?: Record; + }; + FeatureLocalizationRequest: { + title: string; + description?: string; + featureCategories?: (number | string)[]; + available?: boolean; + /** @example string or id */ + icon?: number | string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + content?: string; + locale: string; + }; + FeatureRequest: { + data: { + title: string; + description?: string; + featureCategories?: (number | string)[]; + available?: boolean; + /** @example string or id */ + icon?: number | string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + content?: string; + locale?: string; + }; + }; + FeatureResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["Feature"]; + }; + FeatureLocalizationResponse: { + data?: components["schemas"]["FeatureResponseDataObjectLocalized"]; + meta?: Record; + }; + FeatureListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["Feature"]; + }; + FeatureLocalizationListResponse: { + data?: components["schemas"]["FeatureListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + FeatureListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["Feature"]; + }; + FeatureListResponse: { + data?: components["schemas"]["FeatureListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + Feature: { + title: string; + description?: string; + featureCategories?: { + data?: { + id?: number; + attributes?: { + title?: string; + features?: { + data?: { + id?: number; + attributes?: { + title?: string; + description?: string; + featureCategories?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + available?: boolean; + icon?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: { + name?: string; + pathId?: number; + parent?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + children?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + files?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: { + firstname?: string; + lastname?: string; + username?: string; + /** Format: email */ + email?: string; + resetPasswordToken?: string; + registrationToken?: string; + isActive?: boolean; + roles?: { + data?: { + id?: number; + attributes?: { + name?: string; + code?: string; + description?: string; + users?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }[]; + }; + permissions?: { + data?: { + id?: number; + attributes?: { + action?: string; + actionParameters?: unknown; + subject?: string; + properties?: unknown; + conditions?: unknown; + role?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + blocked?: boolean; + preferedLanguage?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + path?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + slug?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + content?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + available?: boolean; + icon?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + content?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["FeatureListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + FeatureResponseDataObject: { + id?: number; + attributes?: components["schemas"]["Feature"]; + }; + FeatureResponse: { + data?: components["schemas"]["FeatureResponseDataObject"]; + meta?: Record; + }; + FeatureCategoryLocalizationRequest: { + title: string; + features?: (number | string)[]; + locale: string; + }; + FeatureCategoryRequest: { + data: { + title: string; + features?: (number | string)[]; + locale?: string; + }; + }; + FeatureCategoryResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["FeatureCategory"]; + }; + FeatureCategoryLocalizationResponse: { + data?: components["schemas"]["FeatureCategoryResponseDataObjectLocalized"]; + meta?: Record; + }; + FeatureCategoryListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["FeatureCategory"]; + }; + FeatureCategoryLocalizationListResponse: { + data?: components["schemas"]["FeatureCategoryListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + FeatureCategoryListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["FeatureCategory"]; + }; + FeatureCategoryListResponse: { + data?: components["schemas"]["FeatureCategoryListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + FeatureCategory: { + title: string; + features?: { + data?: { + id?: number; + attributes?: { + title?: string; + description?: string; + featureCategories?: { + data?: { + id?: number; + attributes?: { + title?: string; + features?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: { + firstname?: string; + lastname?: string; + username?: string; + /** Format: email */ + email?: string; + resetPasswordToken?: string; + registrationToken?: string; + isActive?: boolean; + roles?: { + data?: { + id?: number; + attributes?: { + name?: string; + code?: string; + description?: string; + users?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + permissions?: { + data?: { + id?: number; + attributes?: { + action?: string; + actionParameters?: unknown; + subject?: string; + properties?: unknown; + conditions?: unknown; + role?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + blocked?: boolean; + preferedLanguage?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + available?: boolean; + icon?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: { + name?: string; + pathId?: number; + parent?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + children?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + files?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + path?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + slug?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + content?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["FeatureCategoryListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + FeatureCategoryResponseDataObject: { + id?: number; + attributes?: components["schemas"]["FeatureCategory"]; + }; + FeatureCategoryResponse: { + data?: components["schemas"]["FeatureCategoryResponseDataObject"]; + meta?: Record; + }; + FeaturesPageLocalizationRequest: { + SEO?: components["schemas"]["SharedSeoComponent"]; + featureCategories?: (number | string)[]; + dynamicZoneTop?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + dynamicZoneBottom?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + locale: string; + }; + FeaturesPageRequest: { + data: { + SEO?: components["schemas"]["SharedSeoComponent"]; + featureCategories?: (number | string)[]; + dynamicZoneTop?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + dynamicZoneBottom?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + locale?: string; + }; + }; + FeaturesPageResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["FeaturesPage"]; + }; + FeaturesPageLocalizationResponse: { + data?: components["schemas"]["FeaturesPageResponseDataObjectLocalized"]; + meta?: Record; + }; + FeaturesPageListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["FeaturesPage"]; + }; + FeaturesPageLocalizationListResponse: { + data?: components["schemas"]["FeaturesPageListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + FeaturesPageListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["FeaturesPage"]; + }; + FeaturesPageListResponse: { + data?: components["schemas"]["FeaturesPageListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + FeaturesPage: { + SEO?: components["schemas"]["SharedSeoComponent"]; + featureCategories?: { + data?: { + id?: number; + attributes?: { + title?: string; + features?: { + data?: { + id?: number; + attributes?: { + title?: string; + description?: string; + featureCategories?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + available?: boolean; + icon?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + slug?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + content?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + dynamicZoneTop?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + dynamicZoneBottom?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["FeaturesPageListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + FeaturesPageResponseDataObject: { + id?: number; + attributes?: components["schemas"]["FeaturesPage"]; + }; + FeaturesPageResponse: { + data?: components["schemas"]["FeaturesPageResponseDataObject"]; + meta?: Record; + }; + FooterLocalizationRequest: { + legalContent1?: string; + legalContent2?: string; + linkBlocks?: components["schemas"]["FooterLinkBlockComponent"][]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + locale: string; + }; + FooterRequest: { + data: { + legalContent1?: string; + legalContent2?: string; + linkBlocks?: components["schemas"]["FooterLinkBlockComponent"][]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + locale?: string; + }; + }; + FooterResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["Footer"]; + }; + FooterLocalizationResponse: { + data?: components["schemas"]["FooterResponseDataObjectLocalized"]; + meta?: Record; + }; + FooterListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["Footer"]; + }; + FooterLocalizationListResponse: { + data?: components["schemas"]["FooterListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + FooterListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["Footer"]; + }; + FooterListResponse: { + data?: components["schemas"]["FooterListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + Footer: { + legalContent1?: string; + legalContent2?: string; + linkBlocks?: components["schemas"]["FooterLinkBlockComponent"][]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["FooterListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + FooterResponseDataObject: { + id?: number; + attributes?: components["schemas"]["Footer"]; + }; + FooterResponse: { + data?: components["schemas"]["FooterResponseDataObject"]; + meta?: Record; + }; + FooterLinkBlockComponent: { + id?: number; + heading?: { + id?: number; + title?: string; + /** @enum {string} */ + level?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + }; + links?: { + id?: number; + title?: string; + url?: string; + }[]; + }; + GlossaryPageLocalizationRequest: { + SEO?: components["schemas"]["SharedSeoComponent"]; + locale: string; + }; + GlossaryPageRequest: { + data: { + SEO?: components["schemas"]["SharedSeoComponent"]; + locale?: string; + }; + }; + GlossaryPageResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["GlossaryPage"]; + }; + GlossaryPageLocalizationResponse: { + data?: components["schemas"]["GlossaryPageResponseDataObjectLocalized"]; + meta?: Record; + }; + GlossaryPageListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["GlossaryPage"]; + }; + GlossaryPageLocalizationListResponse: { + data?: components["schemas"]["GlossaryPageListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + GlossaryPageListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["GlossaryPage"]; + }; + GlossaryPageListResponse: { + data?: components["schemas"]["GlossaryPageListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + GlossaryPage: { + SEO?: components["schemas"]["SharedSeoComponent"]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["GlossaryPageListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + GlossaryPageResponseDataObject: { + id?: number; + attributes?: components["schemas"]["GlossaryPage"]; + }; + GlossaryPageResponse: { + data?: components["schemas"]["GlossaryPageResponseDataObject"]; + meta?: Record; + }; + GlossaryTermLocalizationRequest: { + title?: string; + description?: string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + locale: string; + }; + GlossaryTermRequest: { + data: { + title?: string; + description?: string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + locale?: string; + }; + }; + GlossaryTermResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["GlossaryTerm"]; + }; + GlossaryTermLocalizationResponse: { + data?: components["schemas"]["GlossaryTermResponseDataObjectLocalized"]; + meta?: Record; + }; + GlossaryTermListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["GlossaryTerm"]; + }; + GlossaryTermLocalizationListResponse: { + data?: components["schemas"]["GlossaryTermListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + GlossaryTermListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["GlossaryTerm"]; + }; + GlossaryTermListResponse: { + data?: components["schemas"]["GlossaryTermListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + GlossaryTerm: { + title?: string; + description?: string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["GlossaryTermListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + GlossaryTermResponseDataObject: { + id?: number; + attributes?: components["schemas"]["GlossaryTerm"]; + }; + GlossaryTermResponse: { + data?: components["schemas"]["GlossaryTermResponseDataObject"]; + meta?: Record; + }; + HomePageLocalizationRequest: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + locale: string; + }; + HomePageRequest: { + data: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + locale?: string; + }; + }; + HomePageResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["HomePage"]; + }; + HomePageLocalizationResponse: { + data?: components["schemas"]["HomePageResponseDataObjectLocalized"]; + meta?: Record; + }; + HomePageListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["HomePage"]; + }; + HomePageLocalizationListResponse: { + data?: components["schemas"]["HomePageListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + HomePageListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["HomePage"]; + }; + HomePageListResponse: { + data?: components["schemas"]["HomePageListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + HomePage: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["HomePageListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + HomePageResponseDataObject: { + id?: number; + attributes?: components["schemas"]["HomePage"]; + }; + HomePageResponse: { + data?: components["schemas"]["HomePageResponseDataObject"]; + meta?: Record; + }; + LitepaperLocalizationRequest: { + title: string; + content?: string; + /** @example string or id */ + pdf?: number | string; + SEO?: components["schemas"]["SharedSeoComponent"]; + locale: string; + }; + LitepaperRequest: { + data: { + title: string; + content?: string; + /** @example string or id */ + pdf?: number | string; + SEO?: components["schemas"]["SharedSeoComponent"]; + locale?: string; + }; + }; + LitepaperResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["Litepaper"]; + }; + LitepaperLocalizationResponse: { + data?: components["schemas"]["LitepaperResponseDataObjectLocalized"]; + meta?: Record; + }; + LitepaperListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["Litepaper"]; + }; + LitepaperLocalizationListResponse: { + data?: components["schemas"]["LitepaperListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + LitepaperListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["Litepaper"]; + }; + LitepaperListResponse: { + data?: components["schemas"]["LitepaperListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + Litepaper: { + title: string; + content?: string; + pdf?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: { + name?: string; + pathId?: number; + parent?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + children?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + files?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: { + firstname?: string; + lastname?: string; + username?: string; + /** Format: email */ + email?: string; + resetPasswordToken?: string; + registrationToken?: string; + isActive?: boolean; + roles?: { + data?: { + id?: number; + attributes?: { + name?: string; + code?: string; + description?: string; + users?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + permissions?: { + data?: { + id?: number; + attributes?: { + action?: string; + actionParameters?: unknown; + subject?: string; + properties?: unknown; + conditions?: unknown; + role?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + blocked?: boolean; + preferedLanguage?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + path?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + SEO?: components["schemas"]["SharedSeoComponent"]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["LitepaperListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + LitepaperResponseDataObject: { + id?: number; + attributes?: components["schemas"]["Litepaper"]; + }; + LitepaperResponse: { + data?: components["schemas"]["LitepaperResponseDataObject"]; + meta?: Record; + }; + NavigationLocalizationRequest: { + links?: components["schemas"]["NavigationNavigationItemComponent"][]; + button?: components["schemas"]["UiLinkComponent"]; + backText?: string; + locale: string; + }; + NavigationRequest: { + data: { + links?: components["schemas"]["NavigationNavigationItemComponent"][]; + button?: components["schemas"]["UiLinkComponent"]; + backText?: string; + locale?: string; + }; + }; + NavigationResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["Navigation"]; + }; + NavigationLocalizationResponse: { + data?: components["schemas"]["NavigationResponseDataObjectLocalized"]; + meta?: Record; + }; + NavigationListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["Navigation"]; + }; + NavigationLocalizationListResponse: { + data?: components["schemas"]["NavigationListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + NavigationListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["Navigation"]; + }; + NavigationListResponse: { + data?: components["schemas"]["NavigationListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + Navigation: { + links?: components["schemas"]["NavigationNavigationItemComponent"][]; + button?: components["schemas"]["UiLinkComponent"]; + backText?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["NavigationListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + NavigationResponseDataObject: { + id?: number; + attributes?: components["schemas"]["Navigation"]; + }; + NavigationResponse: { + data?: components["schemas"]["NavigationResponseDataObject"]; + meta?: Record; + }; + NavigationNavigationItemComponent: { + id?: number; + title?: string; + url?: string; + subItems?: { + id?: number; + title?: string; + description?: string; + url?: string; + }[]; + subPanel?: { + id?: number; + title?: string; + url?: string; + links?: { + id?: number; + title?: string; + links?: { + id?: number; + title?: string; + url?: string; + icon?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: { + name?: string; + pathId?: number; + parent?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + children?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + files?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: { + firstname?: string; + lastname?: string; + username?: string; + /** Format: email */ + email?: string; + resetPasswordToken?: string; + registrationToken?: string; + isActive?: boolean; + roles?: { + data?: { + id?: number; + attributes?: { + name?: string; + code?: string; + description?: string; + users?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }[]; + }; + permissions?: { + data?: { + id?: number; + attributes?: { + action?: string; + actionParameters?: unknown; + subject?: string; + properties?: unknown; + conditions?: unknown; + role?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + blocked?: boolean; + preferedLanguage?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + path?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + }[]; + }[]; + }; + }; + NetworkPageLocalizationRequest: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + locale: string; + }; + NetworkPageRequest: { + data: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + locale?: string; + }; + }; + NetworkPageResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["NetworkPage"]; + }; + NetworkPageLocalizationResponse: { + data?: components["schemas"]["NetworkPageResponseDataObjectLocalized"]; + meta?: Record; + }; + NetworkPageListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["NetworkPage"]; + }; + NetworkPageLocalizationListResponse: { + data?: components["schemas"]["NetworkPageListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + NetworkPageListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["NetworkPage"]; + }; + NetworkPageListResponse: { + data?: components["schemas"]["NetworkPageListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + NetworkPage: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["NetworkPageListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + NetworkPageResponseDataObject: { + id?: number; + attributes?: components["schemas"]["NetworkPage"]; + }; + NetworkPageResponse: { + data?: components["schemas"]["NetworkPageResponseDataObject"]; + meta?: Record; + }; + PageNotFoundLocalizationRequest: { + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneRichTextComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + locale: string; + }; + PageNotFoundRequest: { + data: { + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneRichTextComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + locale?: string; + }; + }; + PageNotFoundResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["PageNotFound"]; + }; + PageNotFoundLocalizationResponse: { + data?: components["schemas"]["PageNotFoundResponseDataObjectLocalized"]; + meta?: Record; + }; + PageNotFoundListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["PageNotFound"]; + }; + PageNotFoundLocalizationListResponse: { + data?: components["schemas"]["PageNotFoundListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + PageNotFoundListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["PageNotFound"]; + }; + PageNotFoundListResponse: { + data?: components["schemas"]["PageNotFoundListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + PageNotFound: { + dynamicZone?: ( + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneRichTextComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["PageNotFoundListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + PageNotFoundResponseDataObject: { + id?: number; + attributes?: components["schemas"]["PageNotFound"]; + }; + PageNotFoundResponse: { + data?: components["schemas"]["PageNotFoundResponseDataObject"]; + meta?: Record; + }; + PressPageLocalizationRequest: { + SEO?: components["schemas"]["SharedSeoComponent"]; + locale: string; + }; + PressPageRequest: { + data: { + SEO?: components["schemas"]["SharedSeoComponent"]; + locale?: string; + }; + }; + PressPageResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["PressPage"]; + }; + PressPageLocalizationResponse: { + data?: components["schemas"]["PressPageResponseDataObjectLocalized"]; + meta?: Record; + }; + PressPageListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["PressPage"]; + }; + PressPageLocalizationListResponse: { + data?: components["schemas"]["PressPageListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + PressPageListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["PressPage"]; + }; + PressPageListResponse: { + data?: components["schemas"]["PressPageListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + PressPage: { + SEO?: components["schemas"]["SharedSeoComponent"]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["PressPageListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + PressPageResponseDataObject: { + id?: number; + attributes?: components["schemas"]["PressPage"]; + }; + PressPageResponse: { + data?: components["schemas"]["PressPageResponseDataObject"]; + meta?: Record; + }; + PressPostLocalizationRequest: { + title: string; + /** Format: date */ + date?: string; + url: string; + locale: string; + }; + PressPostRequest: { + data: { + title: string; + /** Format: date */ + date?: string; + url: string; + locale?: string; + }; + }; + PressPostResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["PressPost"]; + }; + PressPostLocalizationResponse: { + data?: components["schemas"]["PressPostResponseDataObjectLocalized"]; + meta?: Record; + }; + PressPostListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["PressPost"]; + }; + PressPostLocalizationListResponse: { + data?: components["schemas"]["PressPostListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + PressPostListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["PressPost"]; + }; + PressPostListResponse: { + data?: components["schemas"]["PressPostListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + PressPost: { + title: string; + /** Format: date */ + date?: string; + url: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: { + firstname?: string; + lastname?: string; + username?: string; + /** Format: email */ + email?: string; + resetPasswordToken?: string; + registrationToken?: string; + isActive?: boolean; + roles?: { + data?: { + id?: number; + attributes?: { + name?: string; + code?: string; + description?: string; + users?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + permissions?: { + data?: { + id?: number; + attributes?: { + action?: string; + actionParameters?: unknown; + subject?: string; + properties?: unknown; + conditions?: unknown; + role?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + blocked?: boolean; + preferedLanguage?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["PressPostListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + PressPostResponseDataObject: { + id?: number; + attributes?: components["schemas"]["PressPost"]; + }; + PressPostResponse: { + data?: components["schemas"]["PressPostResponseDataObject"]; + meta?: Record; + }; + TeamMemberLocalizationRequest: { + name?: string; + role?: string; + /** @example string or id */ + image?: number | string; + bio?: string; + socialNetworks?: components["schemas"]["SocialSocialsComponent"]; + locale: string; + }; + TeamMemberRequest: { + data: { + name?: string; + role?: string; + /** @example string or id */ + image?: number | string; + bio?: string; + socialNetworks?: components["schemas"]["SocialSocialsComponent"]; + locale?: string; + }; + }; + TeamMemberResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["TeamMember"]; + }; + TeamMemberLocalizationResponse: { + data?: components["schemas"]["TeamMemberResponseDataObjectLocalized"]; + meta?: Record; + }; + TeamMemberListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["TeamMember"]; + }; + TeamMemberLocalizationListResponse: { + data?: components["schemas"]["TeamMemberListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + TeamMemberListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["TeamMember"]; + }; + TeamMemberListResponse: { + data?: components["schemas"]["TeamMemberListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + TeamMember: { + name?: string; + role?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: { + name?: string; + pathId?: number; + parent?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + children?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + files?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: { + firstname?: string; + lastname?: string; + username?: string; + /** Format: email */ + email?: string; + resetPasswordToken?: string; + registrationToken?: string; + isActive?: boolean; + roles?: { + data?: { + id?: number; + attributes?: { + name?: string; + code?: string; + description?: string; + users?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + permissions?: { + data?: { + id?: number; + attributes?: { + action?: string; + actionParameters?: unknown; + subject?: string; + properties?: unknown; + conditions?: unknown; + role?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + blocked?: boolean; + preferedLanguage?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + path?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + bio?: string; + socialNetworks?: components["schemas"]["SocialSocialsComponent"]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["TeamMemberListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + TeamMemberResponseDataObject: { + id?: number; + attributes?: components["schemas"]["TeamMember"]; + }; + TeamMemberResponse: { + data?: components["schemas"]["TeamMemberResponseDataObject"]; + meta?: Record; + }; + SocialSocialsComponent: { + id?: number; + links?: { + id?: number; + url?: string; + /** @enum {string} */ + network?: "linkedin" | "twitter" | "wikipedia" | "website"; + }[]; + }; + TrustCenterAdvisorsPageLocalizationRequest: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + )[]; + title?: string; + locale: string; + }; + TrustCenterAdvisorsPageRequest: { + data: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + )[]; + title?: string; + locale?: string; + }; + }; + TrustCenterAdvisorsPageResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["TrustCenterAdvisorsPage"]; + }; + TrustCenterAdvisorsPageLocalizationResponse: { + data?: components["schemas"]["TrustCenterAdvisorsPageResponseDataObjectLocalized"]; + meta?: Record; + }; + TrustCenterAdvisorsPageListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["TrustCenterAdvisorsPage"]; + }; + TrustCenterAdvisorsPageLocalizationListResponse: { + data?: components["schemas"]["TrustCenterAdvisorsPageListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + TrustCenterAdvisorsPageListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["TrustCenterAdvisorsPage"]; + }; + TrustCenterAdvisorsPageListResponse: { + data?: components["schemas"]["TrustCenterAdvisorsPageListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + TrustCenterAdvisorsPage: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + )[]; + title?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["TrustCenterAdvisorsPageListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + TrustCenterAdvisorsPageResponseDataObject: { + id?: number; + attributes?: components["schemas"]["TrustCenterAdvisorsPage"]; + }; + TrustCenterAdvisorsPageResponse: { + data?: components["schemas"]["TrustCenterAdvisorsPageResponseDataObject"]; + meta?: Record; + }; + TrustCenterCategoryLocalizationRequest: { + title: string; + description?: string; + slug: string; + trustCenterPosts?: (number | string)[]; + locale: string; + }; + TrustCenterCategoryRequest: { + data: { + title: string; + description?: string; + slug: string; + trustCenterPosts?: (number | string)[]; + locale?: string; + }; + }; + TrustCenterCategoryResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["TrustCenterCategory"]; + }; + TrustCenterCategoryLocalizationResponse: { + data?: components["schemas"]["TrustCenterCategoryResponseDataObjectLocalized"]; + meta?: Record; + }; + TrustCenterCategoryListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["TrustCenterCategory"]; + }; + TrustCenterCategoryLocalizationListResponse: { + data?: components["schemas"]["TrustCenterCategoryListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + TrustCenterCategoryListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["TrustCenterCategory"]; + }; + TrustCenterCategoryListResponse: { + data?: components["schemas"]["TrustCenterCategoryListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + TrustCenterCategory: { + title: string; + description?: string; + slug: string; + trustCenterPosts?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + excerpt?: string; + isLink?: boolean; + link?: string; + relatedTrustCenterPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + dynamicZone?: ( + | { + id?: number; + __component?: string; + items?: { + id?: number; + title?: string; + url?: string; + authors?: string; + published?: string; + date?: string; + }[]; + } + | { + id?: number; + __component?: string; + items?: { + id?: number; + title?: string; + url?: string; + company?: string; + authors?: string; + date?: string; + }[]; + } + )[]; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: { + name?: string; + pathId?: number; + parent?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + children?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + files?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: { + firstname?: string; + lastname?: string; + username?: string; + /** Format: email */ + email?: string; + resetPasswordToken?: string; + registrationToken?: string; + isActive?: boolean; + roles?: { + data?: { + id?: number; + attributes?: { + name?: string; + code?: string; + description?: string; + users?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }[]; + }; + permissions?: { + data?: { + id?: number; + attributes?: { + action?: string; + actionParameters?: unknown; + subject?: string; + properties?: unknown; + conditions?: unknown; + role?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + blocked?: boolean; + preferedLanguage?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + path?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + content?: string; + readingTime?: number; + trustCenterCategory?: { + data?: { + id?: number; + attributes?: { + title?: string; + description?: string; + slug?: string; + trustCenterPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }; + }; + subtitle?: string; + /** Format: date */ + date?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["TrustCenterCategoryListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + TrustCenterCategoryResponseDataObject: { + id?: number; + attributes?: components["schemas"]["TrustCenterCategory"]; + }; + TrustCenterCategoryResponse: { + data?: components["schemas"]["TrustCenterCategoryResponseDataObject"]; + meta?: Record; + }; + TrustCenterPageLocalizationRequest: { + SEO?: components["schemas"]["SharedSeoComponent"]; + title?: string; + locale: string; + }; + TrustCenterPageRequest: { + data: { + SEO?: components["schemas"]["SharedSeoComponent"]; + title?: string; + locale?: string; + }; + }; + TrustCenterPageResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["TrustCenterPage"]; + }; + TrustCenterPageLocalizationResponse: { + data?: components["schemas"]["TrustCenterPageResponseDataObjectLocalized"]; + meta?: Record; + }; + TrustCenterPageListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["TrustCenterPage"]; + }; + TrustCenterPageLocalizationListResponse: { + data?: components["schemas"]["TrustCenterPageListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + TrustCenterPageListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["TrustCenterPage"]; + }; + TrustCenterPageListResponse: { + data?: components["schemas"]["TrustCenterPageListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + TrustCenterPage: { + SEO?: components["schemas"]["SharedSeoComponent"]; + title?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["TrustCenterPageListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + TrustCenterPageResponseDataObject: { + id?: number; + attributes?: components["schemas"]["TrustCenterPage"]; + }; + TrustCenterPageResponse: { + data?: components["schemas"]["TrustCenterPageResponseDataObject"]; + meta?: Record; + }; + TrustCenterPostLocalizationRequest: { + title: string; + slug?: string; + excerpt?: string; + isLink?: boolean; + link?: string; + relatedTrustCenterPosts?: (number | string)[]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneDocumentListComponent"] + | components["schemas"]["DynamicZoneAuditListComponent"] + )[]; + SEO?: components["schemas"]["SharedSeoComponent"]; + content?: string; + readingTime?: number; + /** @example string or id */ + trustCenterCategory?: number | string; + subtitle?: string; + /** Format: date */ + date?: string; + /** @example string or id */ + image?: number | string; + locale: string; + }; + TrustCenterPostRequest: { + data: { + title: string; + slug?: string; + excerpt?: string; + isLink?: boolean; + link?: string; + relatedTrustCenterPosts?: (number | string)[]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneDocumentListComponent"] + | components["schemas"]["DynamicZoneAuditListComponent"] + )[]; + SEO?: components["schemas"]["SharedSeoComponent"]; + content?: string; + readingTime?: number; + /** @example string or id */ + trustCenterCategory?: number | string; + subtitle?: string; + /** Format: date */ + date?: string; + /** @example string or id */ + image?: number | string; + locale?: string; + }; + }; + TrustCenterPostResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["TrustCenterPost"]; + }; + TrustCenterPostLocalizationResponse: { + data?: components["schemas"]["TrustCenterPostResponseDataObjectLocalized"]; + meta?: Record; + }; + TrustCenterPostListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["TrustCenterPost"]; + }; + TrustCenterPostLocalizationListResponse: { + data?: components["schemas"]["TrustCenterPostListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + TrustCenterPostListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["TrustCenterPost"]; + }; + TrustCenterPostListResponse: { + data?: components["schemas"]["TrustCenterPostListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + TrustCenterPost: { + title: string; + slug?: string; + excerpt?: string; + isLink?: boolean; + link?: string; + relatedTrustCenterPosts?: { + data?: { + id?: number; + attributes?: { + title?: string; + slug?: string; + excerpt?: string; + isLink?: boolean; + link?: string; + relatedTrustCenterPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + dynamicZone?: ( + | { + id?: number; + __component?: string; + items?: { + id?: number; + title?: string; + url?: string; + authors?: string; + published?: string; + date?: string; + }[]; + } + | { + id?: number; + __component?: string; + items?: { + id?: number; + title?: string; + url?: string; + company?: string; + authors?: string; + date?: string; + }[]; + } + )[]; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: { + name?: string; + pathId?: number; + parent?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + children?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + files?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: { + firstname?: string; + lastname?: string; + username?: string; + /** Format: email */ + email?: string; + resetPasswordToken?: string; + registrationToken?: string; + isActive?: boolean; + roles?: { + data?: { + id?: number; + attributes?: { + name?: string; + code?: string; + description?: string; + users?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }[]; + }; + permissions?: { + data?: { + id?: number; + attributes?: { + action?: string; + actionParameters?: unknown; + subject?: string; + properties?: unknown; + conditions?: unknown; + role?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record< + string, + never + >; + }; + }; + }; + }[]; + }; + blocked?: boolean; + preferedLanguage?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }[]; + }; + path?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + content?: string; + readingTime?: number; + trustCenterCategory?: { + data?: { + id?: number; + attributes?: { + title?: string; + description?: string; + slug?: string; + trustCenterPosts?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }; + }; + subtitle?: string; + /** Format: date */ + date?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + dynamicZone?: ( + | components["schemas"]["DynamicZoneDocumentListComponent"] + | components["schemas"]["DynamicZoneAuditListComponent"] + )[]; + SEO?: components["schemas"]["SharedSeoComponent"]; + content?: string; + readingTime?: number; + trustCenterCategory?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + subtitle?: string; + /** Format: date */ + date?: string; + image?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["TrustCenterPostListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + TrustCenterPostResponseDataObject: { + id?: number; + attributes?: components["schemas"]["TrustCenterPost"]; + }; + TrustCenterPostResponse: { + data?: components["schemas"]["TrustCenterPostResponseDataObject"]; + meta?: Record; + }; + DynamicZoneDocumentListItemComponent: { + id?: number; + title?: string; + url?: string; + authors?: string; + published?: string; + date?: string; + }; + DynamicZoneDocumentListComponent: { + id?: number; + __component?: string; + items?: components["schemas"]["DynamicZoneDocumentListItemComponent"][]; + }; + DynamicZoneAuditListItemComponent: { + id?: number; + title?: string; + url?: string; + company?: string; + authors?: string; + date?: string; + }; + DynamicZoneAuditListComponent: { + id?: number; + __component?: string; + items?: components["schemas"]["DynamicZoneAuditListItemComponent"][]; + }; + UseCaseLocalizationRequest: { + title: string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + excerpt?: string; + features?: (number | string)[]; + locale: string; + }; + UseCaseRequest: { + data: { + title: string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + excerpt?: string; + features?: (number | string)[]; + locale?: string; + }; + }; + UseCaseResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["UseCase"]; + }; + UseCaseLocalizationResponse: { + data?: components["schemas"]["UseCaseResponseDataObjectLocalized"]; + meta?: Record; + }; + UseCaseListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["UseCase"]; + }; + UseCaseLocalizationListResponse: { + data?: components["schemas"]["UseCaseListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + UseCaseListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["UseCase"]; + }; + UseCaseListResponse: { + data?: components["schemas"]["UseCaseListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + UseCase: { + title: string; + slug: string; + SEO?: components["schemas"]["SharedSeoComponent"]; + excerpt?: string; + features?: { + data?: { + id?: number; + attributes?: { + title?: string; + description?: string; + featureCategories?: { + data?: { + id?: number; + attributes?: { + title?: string; + features?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + available?: boolean; + icon?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + slug?: string; + SEO?: { + id?: number; + metaTitle?: string; + metaDescription?: string; + metaImage?: { + data?: { + id?: number; + attributes?: { + name?: string; + alternativeText?: string; + caption?: string; + width?: number; + height?: number; + formats?: unknown; + hash?: string; + ext?: string; + mime?: string; + /** Format: float */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: unknown; + related?: { + data?: { + id?: number; + attributes?: Record; + }[]; + }; + folder?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + folderPath?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + }; + }; + }; + keywords?: string; + metaRobots?: string; + structuredData?: unknown; + canonicalURL?: string; + }; + content?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: unknown[]; + }; + locale?: string; + }; + }[]; + }; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["UseCaseListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + UseCaseResponseDataObject: { + id?: number; + attributes?: components["schemas"]["UseCase"]; + }; + UseCaseResponse: { + data?: components["schemas"]["UseCaseResponseDataObject"]; + meta?: Record; + }; + UseCasesPageLocalizationRequest: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + locale: string; + }; + UseCasesPageRequest: { + data: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + locale?: string; + }; + }; + UseCasesPageResponseDataObjectLocalized: { + id?: number; + attributes?: components["schemas"]["UseCasesPage"]; + }; + UseCasesPageLocalizationResponse: { + data?: components["schemas"]["UseCasesPageResponseDataObjectLocalized"]; + meta?: Record; + }; + UseCasesPageListResponseDataItemLocalized: { + id?: number; + attributes?: components["schemas"]["UseCasesPage"]; + }; + UseCasesPageLocalizationListResponse: { + data?: components["schemas"]["UseCasesPageListResponseDataItemLocalized"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + UseCasesPageListResponseDataItem: { + id?: number; + attributes?: components["schemas"]["UseCasesPage"]; + }; + UseCasesPageListResponse: { + data?: components["schemas"]["UseCasesPageListResponseDataItem"][]; + meta?: { + pagination?: { + page?: number; + pageSize?: number; + pageCount?: number; + total?: number; + }; + }; + }; + UseCasesPage: { + SEO?: components["schemas"]["SharedSeoComponent"]; + dynamicZone?: ( + | components["schemas"]["DynamicZoneBlogPostsComponent"] + | components["schemas"]["FaqsFaQsComponent"] + | components["schemas"]["DynamicZoneBlocksComponent"] + | components["schemas"]["DynamicZoneTilesComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithTextComponent"] + | components["schemas"]["DynamicZoneValuePropositionWithBulletsComponent"] + | components["schemas"]["DynamicZoneTeamMembersComponent"] + | components["schemas"]["DynamicZoneParagraphWithTitleComponent"] + | components["schemas"]["DynamicZoneLogoCarouselComponent"] + | components["schemas"]["DynamicZoneContentTilesComponent"] + | components["schemas"]["DynamicZoneQuotesComponent"] + | components["schemas"]["DynamicZoneCarouselTextImageComponent"] + | components["schemas"]["DynamicZoneCarouselQuoteComponent"] + )[]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + publishedAt?: string; + createdBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + updatedBy?: { + data?: { + id?: number; + attributes?: Record; + }; + }; + localizations?: { + data?: components["schemas"]["UseCasesPageListResponseDataItemLocalized"][]; + }; + locale?: string; + }; + UseCasesPageResponseDataObject: { + id?: number; + attributes?: components["schemas"]["UseCasesPage"]; + }; + UseCasesPageResponse: { + data?: components["schemas"]["UseCasesPageResponseDataObject"]; + meta?: Record; + }; + UploadFile: { + id?: number; + name?: string; + alternativeText?: string; + caption?: string; + /** Format: integer */ + width?: number; + /** Format: integer */ + height?: number; + formats?: number; + hash?: string; + ext?: string; + mime?: string; + /** Format: double */ + size?: number; + url?: string; + previewUrl?: string; + provider?: string; + provider_metadata?: Record; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + }; + "Users-Permissions-Role": { + id?: number; + name?: string; + description?: string; + type?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + }; + "Users-Permissions-User": { + /** @example 1 */ + id?: number; + /** @example foo.bar */ + username?: string; + /** @example foo.bar@strapi.io */ + email?: string; + /** @example local */ + provider?: string; + /** @example true */ + confirmed?: boolean; + /** @example false */ + blocked?: boolean; + /** + * Format: date-time + * @example 2022-06-02T08:32:06.258Z + */ + createdAt?: string; + /** + * Format: date-time + * @example 2022-06-02T08:32:06.267Z + */ + updatedAt?: string; + }; + "Users-Permissions-UserRegistration": { + /** @example eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c */ + jwt?: string; + user?: components["schemas"]["Users-Permissions-User"]; + }; + "Users-Permissions-PermissionsTree": { + [key: string]: { + /** @description every controller of the api */ + controllers?: { + [key: string]: { + [key: string]: { + enabled?: boolean; + policy?: string; + }; + }; + }; + }; + }; + }; + responses: never; + parameters: never; + requestBodies: { + "Users-Permissions-RoleRequest": { + content: { + /** @example { + * "name": "foo", + * "description": "role foo", + * "permissions": { + * "api::content-type.content-type": { + * "controllers": { + * "controllerA": { + * "find": { + * "enabled": true + * } + * } + * } + * } + * } + * } */ + "application/json": { + name?: string; + description?: string; + type?: string; + permissions?: components["schemas"]["Users-Permissions-PermissionsTree"]; + }; + }; + }; + }; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + "get/about-page": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AboutPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/about-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AboutPageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AboutPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/about-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/about-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AboutPageLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AboutPageLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/basic-pages": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BasicPageListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/basic-pages": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BasicPageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BasicPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/basic-pages/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BasicPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/basic-pages/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BasicPageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BasicPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/basic-pages/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/basic-pages/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BasicPageLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BasicPageLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/blog-authors": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogAuthorListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/blog-authors": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BlogAuthorRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogAuthorResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/blog-authors/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogAuthorResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/blog-authors/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BlogAuthorRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogAuthorResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/blog-authors/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/blog-authors/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BlogAuthorLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogAuthorLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/blog-categories": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogCategoryListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/blog-categories": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BlogCategoryRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogCategoryResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/blog-categories/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogCategoryResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/blog-categories/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BlogCategoryRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogCategoryResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/blog-categories/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/blog-categories/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BlogCategoryLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogCategoryLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/blog-page": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/blog-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BlogPageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/blog-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/blog-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BlogPageLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogPageLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/blog-posts": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogPostListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/blog-posts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BlogPostRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogPostResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/blog-posts/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogPostResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/blog-posts/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BlogPostRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogPostResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/blog-posts/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/blog-posts/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BlogPostLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BlogPostLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/downloads": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DownloadListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/downloads": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DownloadRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DownloadResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/downloads/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DownloadResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/downloads/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DownloadRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DownloadResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/downloads/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/downloads/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DownloadLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DownloadLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/downloads-menu": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DownloadsMenuResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/downloads-menu": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DownloadsMenuRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DownloadsMenuResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/downloads-menu": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/downloads-menu/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DownloadsMenuLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DownloadsMenuLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/features": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeatureListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/features": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["FeatureRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeatureResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/features/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeatureResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/features/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["FeatureRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeatureResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/features/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/features/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["FeatureLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeatureLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/feature-categories": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeatureCategoryListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/feature-categories": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["FeatureCategoryRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeatureCategoryResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/feature-categories/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeatureCategoryResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/feature-categories/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["FeatureCategoryRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeatureCategoryResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/feature-categories/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/feature-categories/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["FeatureCategoryLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeatureCategoryLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/features-page": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeaturesPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/features-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["FeaturesPageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeaturesPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/features-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/features-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["FeaturesPageLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FeaturesPageLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/footer": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FooterResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/footer": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["FooterRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FooterResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/footer": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/footer/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["FooterLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["FooterLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/glossary-page": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GlossaryPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/glossary-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GlossaryPageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GlossaryPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/glossary-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/glossary-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GlossaryPageLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GlossaryPageLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/glossary-terms": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GlossaryTermListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/glossary-terms": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GlossaryTermRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GlossaryTermResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/glossary-terms/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GlossaryTermResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/glossary-terms/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GlossaryTermRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GlossaryTermResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/glossary-terms/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/glossary-terms/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GlossaryTermLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GlossaryTermLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/home-page": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HomePageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/home-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["HomePageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HomePageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/home-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/home-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["HomePageLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HomePageLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/litepaper": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LitepaperResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/litepaper": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LitepaperRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LitepaperResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/litepaper": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/litepaper/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LitepaperLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["LitepaperLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/navigation": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NavigationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/navigation": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["NavigationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NavigationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/navigation": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/navigation/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["NavigationLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NavigationLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/network-page": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NetworkPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/network-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["NetworkPageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NetworkPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/network-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/network-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["NetworkPageLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NetworkPageLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/page-not-found": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PageNotFoundResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/page-not-found": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PageNotFoundRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PageNotFoundResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/page-not-found": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/page-not-found/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PageNotFoundLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PageNotFoundLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/press-page": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PressPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/press-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PressPageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PressPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/press-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/press-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PressPageLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PressPageLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/press-posts": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PressPostListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/press-posts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PressPostRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PressPostResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/press-posts/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PressPostResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/press-posts/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PressPostRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PressPostResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/press-posts/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/press-posts/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PressPostLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PressPostLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/team-members": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TeamMemberListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/team-members": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TeamMemberRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TeamMemberResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/team-members/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TeamMemberResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/team-members/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TeamMemberRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TeamMemberResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/team-members/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/team-members/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TeamMemberLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TeamMemberLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/trust-center-advisors-page": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterAdvisorsPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/trust-center-advisors-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TrustCenterAdvisorsPageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterAdvisorsPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/trust-center-advisors-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/trust-center-advisors-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TrustCenterAdvisorsPageLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterAdvisorsPageLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/trust-center-categories": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterCategoryListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/trust-center-categories": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TrustCenterCategoryRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterCategoryResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/trust-center-categories/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterCategoryResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/trust-center-categories/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TrustCenterCategoryRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterCategoryResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/trust-center-categories/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/trust-center-categories/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TrustCenterCategoryLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterCategoryLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/trust-center-page": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/trust-center-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TrustCenterPageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/trust-center-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/trust-center-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TrustCenterPageLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterPageLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/trust-center-posts": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterPostListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/trust-center-posts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TrustCenterPostRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterPostResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/trust-center-posts/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterPostResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/trust-center-posts/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TrustCenterPostRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterPostResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/trust-center-posts/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/trust-center-posts/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["TrustCenterPostLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["TrustCenterPostLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/use-cases": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UseCaseListResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/use-cases": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UseCaseRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UseCaseResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/use-cases/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UseCaseResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/use-cases/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UseCaseRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UseCaseResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/use-cases/{id}": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/use-cases/{id}/localizations": { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UseCaseLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UseCaseLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "get/use-cases-page": { + parameters: { + query?: { + /** @description Sort by attributes ascending (asc) or descending (desc) */ + sort?: string; + /** @description Return page/pageSize (default: true) */ + "pagination[withCount]"?: boolean; + /** @description Page number (default: 0) */ + "pagination[page]"?: number; + /** @description Page size (default: 25) */ + "pagination[pageSize]"?: number; + /** @description Offset value (default: 0) */ + "pagination[start]"?: number; + /** @description Number of entities to return (default: 25) */ + "pagination[limit]"?: number; + /** @description Fields to return (ex: title,author) */ + fields?: string; + /** @description Relations to return */ + populate?: string; + /** @description Filters to apply */ + filters?: { + [key: string]: unknown; + }; + /** @description Locale to apply */ + locale?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UseCasesPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "put/use-cases-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UseCasesPageRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UseCasesPageResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "delete/use-cases-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + "post/use-cases-page/localizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UseCasesPageLocalizationRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UseCasesPageLocalizationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Forbidden */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; +} diff --git a/explorer-v2/src/app/lib/strapiClient.ts b/explorer-v2/src/app/lib/strapiClient.ts new file mode 100644 index 0000000000..6b9326abb2 --- /dev/null +++ b/explorer-v2/src/app/lib/strapiClient.ts @@ -0,0 +1,38 @@ +import createClient from "openapi-fetch"; +import qs from "qs"; +import type { paths } from "./strapi"; + +if (!process.env.NEXT_PUBLIC_CMS_API_URL) { + throw new Error( + "NEXT_PUBLIC_CMS_API_URL environment variable is not defined", + ); +} +if (!process.env.NEXT_PUBLIC_CMS_API_NEXT_REVALIDATE) { + throw new Error( + "NEXT_PUBLIC_CMS_API_NEXT_REVALIDATE environment variable is not defined", + ); +} + +const client = createClient({ + baseUrl: process.env.NEXT_PUBLIC_CMS_API_URL, + headers: { + Accept: "application/json", + }, + fetch: (request: unknown) => { + const req = request as Request; + const url = new URL(req.url, process.env.NEXT_PUBLIC_CMS_API_URL); + + return fetch(new Request(url, req), { + next: { + revalidate: Number(process.env.NEXT_PUBLIC_CMS_API_NEXT_REVALIDATE), + }, + }); + }, + querySerializer(params) { + return qs.stringify(params, { + encodeValuesOnly: true, + }); + }, +}); + +export { client }; diff --git a/explorer-v2/src/app/page.tsx b/explorer-v2/src/app/page.tsx new file mode 100644 index 0000000000..cc6f1e26ab --- /dev/null +++ b/explorer-v2/src/app/page.tsx @@ -0,0 +1,67 @@ +import { Stack, Typography } from "@mui/material"; +import Grid from "@mui/material/Grid2"; +import BlogArticlesCards from "../components/blogs/BlogArticleCards"; +import { ContentLayout } from "../components/contentLayout/ContentLayout"; +import SectionHeading from "../components/headings/SectionHeading"; +import { CurrentEpochCard } from "../components/landingPageComponents/CurrentEpochCard"; +import { NetworkStakeCard } from "../components/landingPageComponents/NetworkStakeCard"; +import { NoiseCard } from "../components/landingPageComponents/NoiseCard"; +import { RewardsCard } from "../components/landingPageComponents/StakersNumberCard"; +import { TokenomicsCard } from "../components/landingPageComponents/TokenomicsCard"; +import NodeTable from "../components/nodeTable/NodeTableWithAction"; +import NodeAndAddressSearch from "../components/search/NodeAndAddressSearch"; + +export default async function Home() { + return ( + + + + Mixnet in your hands + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/explorer-v2/src/components/accountPageComponents/AccountBalancesCard.tsx b/explorer-v2/src/components/accountPageComponents/AccountBalancesCard.tsx new file mode 100644 index 0000000000..a7f07be196 --- /dev/null +++ b/explorer-v2/src/components/accountPageComponents/AccountBalancesCard.tsx @@ -0,0 +1,219 @@ +"use client"; +import { fetchAccountBalance, fetchNymPrice } from "@/app/api"; +import { Skeleton, Stack, Typography } from "@mui/material"; +import { useQuery } from "@tanstack/react-query"; +import type { IRewardDetails } from "../../app/api/types"; +import ExplorerCard from "../cards/ExplorerCard"; +import { AccountBalancesTable } from "./AccountBalancesTable"; + +export interface IAccontStatsRowProps { + type: string; + allocation: number; + amount: number; + value: number; + history?: { type: string; amount: number }[]; + isLastRow?: boolean; + progressBarColor?: string; +} + +interface IAccountBalancesCardProps { + address: string; +} + +const getNymsFormated = (unyms: number): number => { + if (unyms === 0) { + return 0; + } + const balance = unyms / 1000000; + return balance; +}; +const getPriceInUSD = (unyms: number, usdPrice: number): number => { + if (unyms === 0) { + return 0; + } + const balanceInUSD = (unyms / 1000000) * usdPrice; + const balanceFormated = Number(balanceInUSD.toFixed(2)); + return balanceFormated; +}; + +const getAllocation = (unyms: number, totalUnyms: number): number => { + if (unyms === 0) { + return 0; + } + const allocationPercentage = (unyms * 100) / totalUnyms; + return Number(allocationPercentage.toFixed(2)); +}; + +const calculateStakingRewards = ( + accumulatedRewards: IRewardDetails[], +): number => { + if (accumulatedRewards.length > 0) { + const totalRewards = accumulatedRewards.reduce((total, rewardDetail) => { + return total + Number.parseFloat(rewardDetail.rewards.amount); + }, 0); + + const result = getNymsFormated(totalRewards); + + return result; + } + return 0; +}; + +export const AccountBalancesCard = (props: IAccountBalancesCardProps) => { + const { address } = props; + + const { + data: accountInfo, + isLoading, + isError, + } = useQuery({ + queryKey: ["accountBalance", address], + queryFn: () => fetchAccountBalance(address), + enabled: !!address, + }); + + const { + data: nymPrice, + isLoading: isLoadingPrice, + error: priceError, + } = useQuery({ + queryKey: ["nymPrice"], + queryFn: fetchNymPrice, + }); + + if (isLoading || isLoadingPrice) { + return ( + + + + + + + ); + } + + if (isError || priceError || !accountInfo || !nymPrice) { + return ( + + + Failed to account data. + + + + ); + } + + const nymPriceData = nymPrice.quotes.USD.price; + + const totalBalanceUSD = getPriceInUSD( + Number(accountInfo.total_value.amount), + nymPriceData, + ); + const spendableNYM = + accountInfo.balances.length > 0 + ? getNymsFormated(Number(accountInfo.balances[0].amount)) + : 0; + const spendableUSD = + accountInfo.balances.length > 0 + ? getPriceInUSD(Number(accountInfo.balances[0].amount), nymPriceData) + : 0; + const spendableAllocation = + accountInfo.balances.length > 0 + ? getAllocation( + Number(accountInfo.balances[0].amount), + Number(accountInfo.total_value.amount), + ) + : 0; + + const delegationsNYM = getNymsFormated( + Number(accountInfo.total_delegations.amount), + ); + const delegationsUSD = getPriceInUSD( + Number(accountInfo.total_delegations.amount), + nymPriceData, + ); + const delegationsAllocation = getAllocation( + Number(accountInfo.total_delegations.amount), + Number(accountInfo.total_value.amount), + ); + + const operatorRewardsAllocation = getAllocation( + Number(accountInfo.operator_rewards?.amount || 0), + Number(accountInfo.total_value.amount), + ); + + const operatorRewardsNYM = getNymsFormated( + Number(accountInfo.operator_rewards?.amount || 0), + ); + + const operatorRewardsUSD = getPriceInUSD( + Number(accountInfo.operator_rewards?.amount || 0), + nymPriceData, + ); + + const claimableNYM = getNymsFormated( + Number(accountInfo.claimable_rewards.amount), + ); + const claimableUSD = getPriceInUSD( + Number(accountInfo.claimable_rewards.amount), + nymPriceData, + ); + const claimableAllocation = getAllocation( + Number(accountInfo.claimable_rewards.amount), + Number(accountInfo.total_value.amount), + ); + + const stakingRewards = + accountInfo.accumulated_rewards.length > 0 + ? calculateStakingRewards(accountInfo.accumulated_rewards) + : 0; + + const tableRows = [ + { + type: "Spendable", + allocation: spendableAllocation, + amount: spendableNYM, + value: spendableUSD, + }, + { + type: "Delegated", + allocation: delegationsAllocation, + amount: delegationsNYM, + value: delegationsUSD, + // history: [ + // { type: "Liquid", amount: 6900 }, + // { type: "Locked", amount: 6900 }, + // ], + }, + { + type: "Claimable", + allocation: claimableAllocation, + amount: claimableNYM, + value: claimableUSD, + history: [ + // { type: "Unlocked", amount: 6900 }, + { + type: "Staking rewards", + amount: stakingRewards, + }, + { type: "Operator comission", amount: 0 }, + ], + }, + { + type: "Operator Rewards", + allocation: operatorRewardsAllocation, + amount: operatorRewardsNYM, + value: operatorRewardsUSD, + }, + ]; + + return ( + + + + ); +}; diff --git a/explorer-v2/src/components/accountPageComponents/AccountBalancesTable.tsx b/explorer-v2/src/components/accountPageComponents/AccountBalancesTable.tsx new file mode 100644 index 0000000000..d91b186917 --- /dev/null +++ b/explorer-v2/src/components/accountPageComponents/AccountBalancesTable.tsx @@ -0,0 +1,321 @@ +"use client"; + +import CircleIcon from "@mui/icons-material/Circle"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; +import Box from "@mui/material/Box"; +import IconButton from "@mui/material/IconButton"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import Typography from "@mui/material/Typography"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import * as React from "react"; +import { TABLET_WIDTH } from "../../app/constants"; +import { MultiSegmentProgressBar } from "../progressBars/MultiSegmentProgressBar"; +import { StaticProgressBar } from "../progressBars/StaticProgressBar"; + +export interface IAccontStatsRowProps { + type: string; + allocation: number; + amount: number; + value: number; + history?: { type: string; amount: number }[]; + isLastRow?: boolean; + progressBarColor?: string; +} + +const progressBarColours = [ + "#BEF885", + "#7FB0FF", + "#00D17D", + "#004650", + "#FEECB3", +]; + +const Row = (props: IAccontStatsRowProps) => { + const tablet = useMediaQuery(TABLET_WIDTH); + + const { + type, + allocation, + amount, + value, + history, + isLastRow, + progressBarColor, + } = props; + const [open, setOpen] = React.useState(false); + + return ( + + {/* Main Row */} + + {tablet ? ( + + + + {type} + + + + + + {allocation}% + + + + + + + {amount.toFixed(4)} NYM + + + + + ${value} + + + + {history && ( + setOpen(!open)} + > + {open ? : } + + )} + + + ) : ( + // MOBILE VIEW + + + + + {type} + + + + + {amount.toFixed(4)} NYM + $ {value} + + + + {history && ( + setOpen(!open)} + > + {open ? : } + + )} + + + )} + + {/* History Rows */} + {history && + open && + history.map((historyRow) => ( + + + + + {historyRow.type} + + + {tablet && ( + + )} + + + + {historyRow.amount.toFixed(4)} NYM + + + + + + ))} + + ); +}; + +export interface IAccountBalancesTableProps { + rows: Array; +} + +export const AccountBalancesTable = (props: IAccountBalancesTableProps) => { + const { rows } = props; + const tablet = useMediaQuery(TABLET_WIDTH); + const progressBarPercentages = () => { + return rows.map((row) => row.allocation); + }; + const getProgressValues = () => { + const percentages = progressBarPercentages(); + const result: Array<{ percentage: number; color: string }> = []; + percentages.map((value, i) => { + result.push({ + percentage: value, + color: progressBarColours[i], + }); + }); + return result; + }; + + const progressValues = getProgressValues(); + + return ( + + {!tablet && } + + + + {tablet ? ( + + + + Type + + + + + Allocation + + + + + Amount + + + + + Value + + + + + ) : ( + + + + Type + + + + + Amount / Value + + + + + )} + + + {rows.map((row, i) => ( + + ))} + +
    +
    +
    + ); +}; diff --git a/explorer-v2/src/components/accountPageComponents/AccountInfoCard.tsx b/explorer-v2/src/components/accountPageComponents/AccountInfoCard.tsx new file mode 100644 index 0000000000..a252ef1ba2 --- /dev/null +++ b/explorer-v2/src/components/accountPageComponents/AccountInfoCard.tsx @@ -0,0 +1,84 @@ +"use client"; +import { fetchAccountBalance } from "@/app/api"; +import { Box, Skeleton, Stack, Typography } from "@mui/material"; +import { useQuery } from "@tanstack/react-query"; +import ExplorerCard from "../cards/ExplorerCard"; +import CopyToClipboard from "../copyToClipboard/CopyToClipboard"; +import ExplorerListItem from "../list/ListItem"; +import { CardQRCode } from "../qrCode/QrCode"; + +interface IAccountInfoCardProps { + address: string; +} + +export const AccountInfoCard = (props: IAccountInfoCardProps) => { + const { address } = props; + + const { data, isLoading, isError } = useQuery({ + queryKey: ["accountBalance", address], + queryFn: () => fetchAccountBalance(address), + enabled: !!address, + }); + + if (isLoading) { + return ( + + + + + + + + ); + } + + if (isError || !data) { + return ( + + + Failed to account data. + + + + ); + } + + const balance = + data.balances.length > 0 ? Number(data.total_value.amount) / 1000000 : 0; + const balanceFormated = `${balance.toFixed(4)} NYM`; + + return ( + + + + + + + + + {data.address} + + + + } + /> + + + ); +}; diff --git a/explorer-v2/src/components/blogs/BlogArticleCards.tsx b/explorer-v2/src/components/blogs/BlogArticleCards.tsx new file mode 100644 index 0000000000..cb2506a0e4 --- /dev/null +++ b/explorer-v2/src/components/blogs/BlogArticleCards.tsx @@ -0,0 +1,86 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import Grid from "@mui/material/Grid2"; +import ExplorerHeroCard from "../cards/ExplorerHeroCard"; +import type { BlogArticleWithLink } from "./types"; +import { icons, IconName } from "@/utils/getIconByName"; + +// TODO: Articles should be sorted by date + +const BlogArticlesCards = async ({ + limit, + ids, +}: { + limit?: number; + ids?: Array; +}) => { + const blogsDir = path.join(process.cwd(), "/src/data"); + const blogsDirFilenames = await fs.readdir(blogsDir); + + // Read all blog articles from the data directory + const blogArticles: BlogArticleWithLink[] = await Promise.all( + blogsDirFilenames.map(async (filename) => { + const filePath = path.join(blogsDir, filename); + const fileContent = await fs.readFile(filePath, "utf-8"); + const blogArticle = JSON.parse(fileContent); + return { + ...blogArticle, + link: `/onboarding/${filename.replace(".json", "")}`, + }; + }), + ); + + const limitedOrFilteredBlogArticles = ( + blogArticles: BlogArticleWithLink[], + limit?: number, + ids?: number[], + ): BlogArticleWithLink[] => { + let filteredArticles = blogArticles; + + // Filter by IDs if provided + if (ids && ids.length > 0) { + filteredArticles = filteredArticles.filter((article) => + ids.includes(article.id), + ); + } + + // Apply limit if provided + if (limit) { + filteredArticles = filteredArticles.slice(0, limit); + } + + return filteredArticles; + }; + const articles = limitedOrFilteredBlogArticles(blogArticles, limit, ids); + + return articles + .sort((a, b) => { + // sort by date + return ( + new Date(b.attributes.date).getTime() - + new Date(a.attributes.date).getTime() + ); + }) + .map((blogArticle) => { + return ( + + + + ); + }); +}; + +export default BlogArticlesCards; diff --git a/explorer-v2/src/components/blogs/TableOfContents.tsx b/explorer-v2/src/components/blogs/TableOfContents.tsx new file mode 100644 index 0000000000..cc95cc9c3c --- /dev/null +++ b/explorer-v2/src/components/blogs/TableOfContents.tsx @@ -0,0 +1,37 @@ +import { Card, CardContent, CardHeader, Typography } from "@mui/material"; +import { Link } from "../muiLink"; + +const TableOfContents = ({ + headings, +}: { + headings: { id: string; heading: string }[]; +}) => { + return ( + + + + {headings.map((heading) => ( + + + {heading.heading} + + + ))} + + + ); +}; + +export default TableOfContents; diff --git a/explorer-v2/src/components/blogs/types.ts b/explorer-v2/src/components/blogs/types.ts new file mode 100644 index 0000000000..211448f49d --- /dev/null +++ b/explorer-v2/src/components/blogs/types.ts @@ -0,0 +1,29 @@ +type Content = { type: string; text: string }; + +type BlogArticle = { + id: number; + title: string; + label: string; + description: string; + image: string; + icon: string; + attributes: { + blogAuthors: string[]; + date: Date; + readingTime: number; + }; + overview: { + content: Content[]; + }; + sections: { + id: string; + heading: string; + text: Content[]; + }[]; +}; + +export type BlogArticleWithLink = BlogArticle & { + link: string; +}; + +export default BlogArticle; diff --git a/explorer-v2/src/components/breadcrumbs/Breadcrumbs.tsx b/explorer-v2/src/components/breadcrumbs/Breadcrumbs.tsx new file mode 100644 index 0000000000..4c5474d6e9 --- /dev/null +++ b/explorer-v2/src/components/breadcrumbs/Breadcrumbs.tsx @@ -0,0 +1,70 @@ +import BreadcrumbsMUI from "@mui/material/Breadcrumbs"; +import Typography from "@mui/material/Typography"; +import Link from "next/link"; + +interface BreadcrumbComponentProps { + items: BreadcrumbItemType[]; +} + +interface BreadcrumbItemType { + label: string; + href?: string; + isCurrentPage?: boolean; +} + +export const Breadcrumbs = ({ items }: BreadcrumbComponentProps) => { + return ( + + / + + } + aria-label="breadcrumb" + > + {items.map((item) => { + // Check if it's the current page + if (item.isCurrentPage) { + return ( + + {item.label} + + ); + } + + // If it's not the current page, render a clickable link + return ( + + + {item.label} + + + ); + })} + + ); +}; diff --git a/explorer-v2/src/components/button/index.tsx b/explorer-v2/src/components/button/index.tsx new file mode 100644 index 0000000000..5deee5e75e --- /dev/null +++ b/explorer-v2/src/components/button/index.tsx @@ -0,0 +1,124 @@ +"use client"; +import { Button as MUIButton } from "@mui/material"; +import CircularProgress from "@mui/material/CircularProgress"; +import Typography from "@mui/material/Typography"; +import type { Theme } from "@mui/material/styles"; +import Link from "next/link"; +import { colours } from "../../theme/colours"; +import type { ButtonProps } from "./types"; + +export const Button = ({ + text, + handleClick, + goToLink, + isSuccess, + isDisabled, + isTextButton, + isSmall, + isXtraSmall, + isFullWidth, + isSecondary, + isContrast, + sx, + buttonId, + type, + isLoading, + startIcon, + endIcon, +}: ButtonProps) => { + const onClick = () => { + if (goToLink) { + } else if (handleClick) { + handleClick(); + } + }; + + const fontColor = (t: Theme) => { + if (isTextButton) return "primary.main"; + if (isSecondary) + return isContrast ? "common.white" : "primary.contrastText"; + return t.palette.mode === "light" + ? t.palette.common.white + : t.palette.common.black; + }; + const bgColor = (t: Theme) => { + if (isTextButton || isSecondary) return "transparent"; + if (isDisabled) + return t.palette.mode === "light" ? colours.pine[200] : colours.pine[800]; + return isSuccess ? "success.main" : "primary.main"; + }; + const hoverBgColor = () => { + if (isTextButton || isSecondary) return "transparent"; + return isSuccess + ? colours.green[500] + : colours.pine[isDisabled ? 200 : 950]; + }; + + const padding = isTextButton + ? "none" + : isXtraSmall + ? "10px 23px" + : isSmall + ? "11px 23px" + : "15px 23px"; + + const CustomButtom = ( + fontColor(t), + letterSpacing: 0.45, + backgroundColor: (t) => bgColor(t), + border: isSecondary + ? (t) => + isDisabled + ? `1px solid ${t.palette.primary.main}` + : `1px solid ${t.palette.primary.contrastText}` + : "1px solid transparent", + "&:hover": { + backgroundColor: hoverBgColor, + }, + "&:disabled": { + color: "text.disabled", + }, + ...sx, + }} + disabled={isDisabled} + onClick={onClick} + id={buttonId} + startIcon={startIcon} + endIcon={endIcon} + > + {!isLoading ? ( + + {text} + + ) : ( + fontColor(t) }} + /> + )} + + ); + return goToLink ? ( + + {CustomButtom} + + ) : ( + CustomButtom + ); +}; diff --git a/explorer-v2/src/components/button/types.ts b/explorer-v2/src/components/button/types.ts new file mode 100644 index 0000000000..2c715a78b4 --- /dev/null +++ b/explorer-v2/src/components/button/types.ts @@ -0,0 +1,31 @@ +import type { SxProps } from "@mui/system"; // Asegúrate de importar SxProps si estás usando @mui/system + +export type CommonButtonProps = { + text: string; + isSuccess?: boolean; + isDisabled?: boolean; + isTextButton?: boolean; + isSmall?: boolean; + isXtraSmall?: boolean; + isFullWidth?: boolean; + isSecondary?: boolean; + isContrast?: boolean; + sx?: SxProps; + isLoading?: boolean; + buttonId?: string; + type?: "button" | "submit" | "reset"; + startIcon?: React.ReactNode; + endIcon?: React.ReactNode; +}; + +export type ButtonWithClick = CommonButtonProps & { + handleClick: (() => void) | (() => Promise) | (() => null); + goToLink?: never; +}; + +export type ButtonWithLink = CommonButtonProps & { + goToLink: { path: string; target: "_self" | "_blank" | "_parent" | "_top" }; + handleClick?: never; +}; + +export type ButtonProps = ButtonWithClick | ButtonWithLink; diff --git a/explorer-v2/src/components/buttonOrLink/index.tsx b/explorer-v2/src/components/buttonOrLink/index.tsx new file mode 100644 index 0000000000..031e6d3f15 --- /dev/null +++ b/explorer-v2/src/components/buttonOrLink/index.tsx @@ -0,0 +1,63 @@ +import Button, { + type ButtonProps as MUIButtonProps, +} from "@mui/material/Button"; +import Link from "next/link"; +import type React from "react"; + +interface ButtonOrLinkProps extends Omit { + href?: string; + variant?: "outlined" | "contained" | "text"; + isSelected?: boolean; +} + +export const ButtonOrLink: React.FC = ({ + href, + variant = "contained", + isSelected, + children, + ...props +}) => { + const selectedStyle = { + contained: { + outline: "1px solid", + outlineColor: "primary.main", + borderColor: "primary.main", + }, + outlined: { + borderStyle: "solid", + backgroundColor: "primary.main", + color: "background.main", + "&:hover": { + backgroundColor: "primary.main", + }, + }, + text: { + color: "primary.main", + }, + }; + return href ? ( + + ) : ( + + ); +}; diff --git a/explorer-v2/src/components/cards/ExplorerCard.tsx b/explorer-v2/src/components/cards/ExplorerCard.tsx new file mode 100644 index 0000000000..a710aa7047 --- /dev/null +++ b/explorer-v2/src/components/cards/ExplorerCard.tsx @@ -0,0 +1,42 @@ +import { Card, CardContent, CardHeader, type SxProps } from "@mui/material"; + +const cardStyles = { + p: 2, +}; + +const cardTitleStyles: SxProps = { + variant: "h5", + color: "pine.600", + letterSpacing: 0.7, +}; +const cardSubtitleStyles: SxProps = { + variant: "h2", + mt: 3, + color: "pine.400", +}; + +const ExplorerCard = ({ + label, + title, + children, + sx, +}: { + label: string | React.ReactNode; + title?: string; + children: React.ReactNode; + sx?: SxProps; +}) => { + return ( + + + {children} + + ); +}; + +export default ExplorerCard; diff --git a/explorer-v2/src/components/cards/ExplorerHeroCard.tsx b/explorer-v2/src/components/cards/ExplorerHeroCard.tsx new file mode 100644 index 0000000000..218b01d89b --- /dev/null +++ b/explorer-v2/src/components/cards/ExplorerHeroCard.tsx @@ -0,0 +1,71 @@ +import { + Card, + CardContent, + CardHeader, + Stack, + type SxProps, + Typography, +} from "@mui/material"; +import Image from "next/image"; +import ArrowUpRight from "../../components/icons/ArrowUpRight"; +import { Link } from "../muiLink"; + +const cardStyles = { + p: 3, + cursor: "pointer", + "&:hover": { + bgcolor: "accent.main", + }, +}; +const cardContentStyles = { + mt: 10, +}; +const titleStyles = { + letterSpacing: 0.7, +}; + +const ExplorerHeroCard = ({ + title, + label, + description, + icon, + link, + sx, +}: { + title: string; + label: string; + description: string; + icon: string; + link: string; + sx?: SxProps; +}) => { + return ( + + + + + {label} + + + + } + /> + + + {"explorer-blog-image"} + {title} + {description} + + + + + ); +}; +export default ExplorerHeroCard; diff --git a/explorer-v2/src/components/clientOnly/ClientOnly.tsx b/explorer-v2/src/components/clientOnly/ClientOnly.tsx new file mode 100644 index 0000000000..8fc3a418c0 --- /dev/null +++ b/explorer-v2/src/components/clientOnly/ClientOnly.tsx @@ -0,0 +1,18 @@ +"use client"; +import { useIsClient } from "@uidotdev/usehooks"; +/** + * Hack to work around next.js hydration + * @see https://github.com/uidotdev/usehooks/issues/218 + */ +import type React from "react"; + +type ClientOnlyProps = { + children: React.ReactNode; +}; + +export const ClientOnly: React.FC = ({ children }) => { + const isClient = useIsClient(); + + // Render children if on client side, otherwise return null + return isClient ? <>{children} : null; +}; diff --git a/explorer-v2/src/components/comments/index.tsx b/explorer-v2/src/components/comments/index.tsx new file mode 100644 index 0000000000..6c060c8956 --- /dev/null +++ b/explorer-v2/src/components/comments/index.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { Box } from "@mui/material"; +import Script from "next/script"; +import { useEffect } from "react"; + +declare global { + interface Window { + remark_config: { + host: string; + site_id: string; + components: string[]; + max_shown_comments: number; + theme: string; + locale: string; + show_email_subscription: boolean; + simple_view: boolean; + no_footer: boolean; + }; + REMARK42: { + createInstance: (config: typeof window.remark_config) => void; + changeTheme: (theme: "light" | "dark") => void; + }; + } +} + +export const Remark42Comments = () => { + useEffect(() => { + if (typeof window !== "undefined") { + // Set Remark42 configuration on the window object + window.remark_config = { + host: "https://remark42.nymte.ch", + site_id: "remark", + components: ["embed", "last-comments"], + max_shown_comments: 100, + theme: "light", + locale: "en", + show_email_subscription: false, + simple_view: true, + no_footer: true, + }; + + // Dynamically load the Remark42 script if it doesn't exist + if (!document.getElementById("remark42-script")) { + const script = document.createElement("script"); + script.src = `${window.remark_config.host}/web/embed.js`; + script.async = true; + script.defer = true; + script.id = "remark42-script"; + document.body.appendChild(script); + } else if (window.REMARK42) { + // Re-initialize if the script is already loaded + window.REMARK42.createInstance(window.remark_config); + } + } + }, []); + + // React to mode changes and update Remark42 theme + // useEffect(() => { + // if (window.REMARK42 && window.REMARK42.changeTheme) { + // window.REMARK42.changeTheme(mode === "dark" ? "dark" : "light"); + // } + // }, [mode]); + + return ( + +
    +

    &B9?Ft~r3$U1#|i)t+vsz%M{0LovuO2T6o zFLiIl-6&`(L)sz`l0H$YlPPNK)_)xj>6Szd$^o3;9ckFdKzD)fF82=YMqcK|g)B#J z&<2=7*}OMI8?n?JuDm@o&f9tO7n~?ag^BPqJ|B+Vub>zNbjm@=;XI5%k1t+({(?kH z6eJHla#@*YCs*ZYL5pEux&O?Y8t3r7M-BP9;VRcJ@gy^hEQs2Zi$T*cDhFngt+u0+ z6kTB^!j{`+MSc;Dap$N4j;F3BGmaQR$pIZ2OuTKD)v|;G1<0g4Q}2Dl91AlkKo5e? zTapEaL1^G6=!ctZ{$GxON8_l%kH46nAX^Gh@xD#7{8geNGa<^IFcKZ$oUe9p=PD2& zGX_NX=Q5p+BFsDveGf=pKZu+|NB8Zt{wr!k-i@I%DiX#?rxp5-Dkgg|CgMtpYAny|_TfP&yk zG)^_mMM3gRPo68G$Vfft^xS$zc#gu(+F4tkx3ozh_z^^Zm6R=28Cz4zL8a)*XC_*H z5sg8b41G3~ROd$tL{w7B)r`&247jk6DXdbIWHja+nXv)vnnndKdfa+|GREfwHmSF}S=t5*;S z)9I{X=S$=V_pAf`{!2!fzrSRsU;GhBf0XeDN&QDpC#FeO0HC2)z6d;Fks*ep%80Vw z-UE97q5w|YUq#5Vr8&Z)iA~L_vFfwEh|;yq%^#3of9PrcZ>B+4UcBdXK-rkkG$Z{Y z8V5iHZ;pi`h6iL$qV!L?*YTtLErq1-z<{22iWuKLn2zBT1y!qUWq?f{KW3?@c!;q8RhB_`23!ET zm4T_)ncl^p10jS<9zLSusEMnklL8L{gQ#HYIlxk8Bi(l)o|!HX7hoPix8H*ES>6w0l ziG7Bc@Rq|{1-$_A_>CDuaE6h{8FB%7xW#}l^n=Ml5H8PFoCAqRAcx*BC zeK`$kg_H0+oh3?&~Vv6Oh-W5j~aYhC})CFF>xl18^l+D})P zv=&*5$SDj8p^H7|o2ZSt8;AoONvLi?+Wm*e(8P(PsNzK~^LqvoHN%l<@nHN+Ipd_G zYexs~?1kommh24{_s&rSg;Cz|Hf&fGs2Mb|bS`*NCc&JdoaG{Ift;~bE3$`ScH zri`C~LKs67WjRg7&uq1#sr%xfoD}SD>|lxOo1{LkEd;QrrmMj{^>jby7sE!5TEr^H z5H)BbGw-QOBGCeLhfqUC0|gP5t5$TSNchz~0DtbbFmiT2mMKs&`QvDdgGdycMmodN ziEaJ9s1K}`p*;o3oS!NI6Km=!WxF~!z=h49$cXO9kR;92GuN}0X(4QH;n(0@OPXUx zrf^L-h()w+-?eJ*t0mbFzIp*)#7hr6U98ED#ni}kHZVAMO#WC>yIH7GJ_gXY-}EV5 zc@_&f8AuJDR){{g&dMUBzf7W=bXchZTraK*2OWGng=E~-BU*q3wK&PmaRvdBRC21Q z@SZt5exRg)D~8rS-S;pdDIh$p{$2){7|Y@MPbHBk;n5k^=_@R*fFrR48kdvM9%L^J zuo=tHaaW;;%511=M-ThzXBpdG9DvTB$Y=?Ys8!!&^%}fuo5BQCdsmVO0g|JBTv{<& zNOXuHS%S==LhxSOkg0jt$iyqsrJ13{36_BcY*j-W8wLR+H3tJONim9w8KTX>@df-1 zA?33SzgEFd`(TmZ`0gcJcEH^g<}Q-O0O&2t-h?#>B&KT{$Zc>jcQgiElOAvT0Pj*B zH;L{=UY_eGA94ikO&i6;NK_HVMt#>}&i*i?|0Xw(_Ybd!=F)GZmG{C1SQ*4Ys)ArT zJ(vp?v8;U9u@OLHqyD7=^JVq_Phx#3eMzBz7rhoJZ62xFzkr$hCmDXNf}f8T`Qf(Q zRUK*Cgd=6w9T?;p%ns7Ejf;HBlSBp;OcNAJMqWjObW7UC$h>No%N%zt0vEVWo}v^O z*4yzxZChZ3h09`xW676jSHQ>m(iRl@cgbs!(&nKE)3gOb6MV!$JYHuUKF5zSu6*;D zy(1r9AIdR$<7fghv;|0vKMBp0p&lj)a(o=*YG@@LkWnW z@s9_gUC}mD4yYq<1A^?%T6@Ew>|Mc>L{3X+_8q?W^Xcd`GPOirAW)^Mt5MsTd1ei0 z|BfZwgn@Jeb117|%cCWV1XX$fPHp{39(=}bDj$NC9ym}t;pEy%SpjnQ2qsjGysjtRyR2GY? zP23UX8;9t3E@G#(sjnl8VTYsIADD#SkcRoH|J{MIM3I?3M;vvc#20~<7sF0de%^!+ z4S!g!1MvqMxQCx^n77OZi{8yuTXMitslg(bYVv({a~r zGOAqA*9``*p_nMVHBWyDgMlnvo+p)8C*X+Ho}>Td z5V!iWpw|)O&zV1x!h1RF0}B;~q}Pe*_oXS2raXQ=?Dvtea|@;cOAEZ&1XKRH?(JK! zUqZWvicL3*&Ba&^Bhe1p9n}5szXHEH&fs6^yl8jM>v-Y$_xP<@z!H?~Ls3Z`5D$jI zn3TK|l7}EpzFjADkprI=Bo#o=?lzgqRU$kKEgw+Zd>(2)w{+SPOdjQAWt`OdV^#ZLm+dW4|IK0@a2A$hz zjLW$?(228p(`Vx2K^!?u{y$L;#9)PbD+(c}-ZlYGcik`;g6f zTEGWT`LG|9eW6ip1m;wlH8(o1ELw?#Ym${l@gL%qDf|IO17E-gL5{|-ABrDfe+W~k z=_+A``41Vy4<(~sNoA@ygN3+>%=c*(a=SEwnsc&V18^=HBiP10>+K-MX5*RE>`zbl zd?tyK?g+ti&_oEcwI~SpeiN;QA>{lt(mwrMKWc>OGhQ0J9^N%`cf}m>!k_ZG3%;B> zs&09{_UrXKoJ%`B!DGLxcO5)3msn66q^#&p0tT97@jGq~=G$X|R;cguUvWjtbbMp8x$+VUE zNU^xb4sfy8=5KYBKL_pwdb`&l;Az;Ntp!2A_sEk2#`LNejfjX>Glz#pC%q50>0n; zz1ouaRn0y`SZTfwddS41hj?@$j)<}Q6|V`m5-%54ynI!N5qJ)_CONvka7H!A!!f4=q$8A#8hEzu|lziK|HClXV0T zt~Ld+k{t{At&3q;ihM#~-1$Ef_$IZ4_O(B*m=cL&=KML^Y4-Funy;w7NSKq$7eZD&4u_ zzWLYA8Wu^3Xhf1rY5k$RnY>@y+^;(9+p7ZiHxrImkEyxCdoOs>*G*oi7@E4zCvaWF z$n#)N);vk_zJ(5JrP18c_U$!+9KV$?UOcAuAt~A6I4*Sbb(0(R6iwadku9;7bBQ)q z+L#9;+Vdo7`xZpfF?YPIuK*l}{l`yYnA%6Act%1Mecj}TW;=a?psD-3A*fcqg|(VX zjImPY!k+Azr?ky~+P(#mb?&&V0|?;oMC!#}(EKn2Audq8Cp~!xI2fNRWH2-Pc{vcq zm5U_*Hal3a@<>MRD$Z5LF!Jzl86N_``0sD_{MfYnfN5zvcXWHi7@955#B$iwyEb%Y zydDW|PsJzhaX5^C@keDK=*|LzDxWdYgzfDF;z&$X&WbRRKr>JwMm-*hq_T`SKYHqi zkP%^B%$3|Cg2XLKMj`;*j3+=g=IP0QiFesdULvd^{YuprFpa8Z3S2zpg<;6yoS$H7 z{k$TE3`O}5a_+12oh-rqej*9)n(qq?>;ea#Z!E;4Qf6W-7AhurKj}-{OK^oh_9uZFWCpbGYsg44U{*n3V6;{Z zupp*V*37);bB-bLqC>0zIYU@=RI%B%&6bZfBvaW=$_0o<(*zerY4ml7PQ?2vJ3!1O2X9mrRZ;s;)WNwNb#Ykoh`HoU!oKOo z#S$3ojY!YMd+iOY0jqZJ)r4eJQ6!KgfHP1K4B+!H@r#G(;W=p}8kxdFT3#3!Y&WI$j)QcRT+S7#TxQ#2ycqq=E?`-Jw1R*ggqL+eSbc$4NT zK&FoZb8kfQI->08N|LBIt%jw|(lZi`I$8h)y#SLME&sJMro8)PD#&= zE@JHMm1fHl+Lt}KgGnho;yt}GdpyAm}&R=q#!?}oF3-ZJ6{=&s(AW}jZ_4IAYB z6T5ZBOnlzK2;a!_ZySBjgS*9j4@gkwnh(4uzL#~!*{<|0886tjkt<;NJ99S%H5eygufPLVT;2K z&Ol3rxx<`znE1^@bPTaa*3lqC(^+SAhSZ3sH;Kig=kpK1!X*9;zIN&Kf+g9KDgv4J zN@R?8#65tA91P4D83YO$!Boj@s_iHTaUk{3TQ%)SE_yv|9C1g{Wi>#y(O)|^0xx7h zdP1Ofphr+gI&zo}u8pfoG8v?Lrn=kAU6v2xgBl6__+`_jHLxc=* zQ%*Uhihy*!o7j%=lFTE9HgP^#^4`9QVC_#7el^K*G&iIHA$lt4?8=Tzbx|@FEaj9} z$`}X^VdP}x!ITR-!*-09X7Z>8aXwx0-d=zD=LhC9PwRS$0IM;2+RhDWknM1vfYpAQ zoN$|O*@g|QFhPUxt-QZ_@t zf%phQ*h3zHC9K%!Af=~@Vb3}On(L#^6Pg7)zU`7ZY`|pHf!t0Y84^1CpSk##qc* ze2?Q-CVBA_ZCf(e+X7~QY_`~1v=yY*fvH=kfStLN4H$WbK;%6DQwwNdgr7|{ge`QK zKsWJSuK+cGr7+Qykt+-j6$L%Pt0ptkBS7(Zz41 z_3Ub|RDwp0j8L0aS5o(g%o8=LMg^Hg%}7Ib*f(-r3Ye^Bb@lPk zQ8FlR&JB|u&6*WLUT>CU_k?K;5Csk8@h4o8Z6^B?^k z&ohKy*eetl^5K+=kz*eCwCo8B_sCB#1D~57O_=PF6~Ayqn~bhOSEA-LF$O(1at%Dk z)?bhy)&-CJ&7AB9(;6a58XXZ1;$>ubERM{4ve6F0p|4yLbOyzvQZ!28pgiRCL3LjA zMeL0Xmd5b`wl#|kbI56}zE}AX&l|xi*%@k1lY^ccy#^d3C|_VO^hKnTYOg*Q@a!(z zA@)V&5y%(>RPMbVyB}VVh?Ag+?|UDv*SDZTSyEhPh1`T?b>T~j1LPjJ1PZU`n63yU z?2G>oOh$EztmwnICFFqR0-C~YV*-s6%4-mpAjgl+P&mdRyu_Cr<0uA*)u)!`H4}o1 zDPn-e%Rsx^5twmQQIgGqGEU4&cw^!iP4uvyc-TC4gq549_k*_L=>7sv@r^`589cCt z?>VUg>r%A2%oy$FAxQ9Xrou7G8yMn3dE$rBZcIQ2FBYX7>2lU{chP)ZBe3wp)!$N) zc0~?RbF5gPLs8+{S1n`o1Wc9-R1#&1bi$o07duN~v=7}!^>}{_E0M=Wx6F1?ng`(+ zMdfIZkFZ62E{vox!_4UyuBC`Y2!Rz!Z)mnEVBh6r^It`|Zd+`^bO@&tH44IBIygo&> zCJHQN)iLKSDNp}${x-D_qLl%3qDKbEmx)OeDStI$?5&xfWb&ZMHW%MUOpNgIRGa z^HcsN4*aY%PtgXhFA$^XKxpy?0Cr^_jr%?TU~txi*Fj1-j-Lj8lgP@~WDMJXh*=pxSveS#6H-5{c2tDVk#dHZ?wT@jSM*=9i8am;&Y>#2(3rpp0homo5 z4iBv;rDi|5Bj1QB3vF&v`O3)_!1xs>AUP?=wjt3dS!l4AD{o0dkrM@pULp{4+O&M8 zje)a{P1hAQp2mS=pYzYP*1n-e6~sj-TK+o*6Ij_HC$4d_re`SFcIeO<^X!0-`iblR zDCX8=pi^oQIW%||$YzrNhnoD95Dv3qw{ei*R3=@@kvcktp|{N&6<$OeUDQ`ER{?dd zAuNxvMKl6%*?o@1*WN=1vEj7Q!|4mSKT6xmNf3trnQkr3eP^K%tXy?>0^80h01?I> zL<-{wLk-0^{wrQ7hLE3+OzdmcRz8pTx&2tAz7YonGq)2G1}9Ni7`2E&UAwoDQmCuU zF8N6-Mn%o=pwxtd!TNk7Sw?~yRaE7<#WoxC&q0>^jnf(Eh*3r4B@P9a3uboO4yb%# z2`3vVP@JokTF!MToqzWVKzj~Adq8MQBhE?5;Qkupp4zmR-w>$$7{YjK(K#`WpAi1Z zgq=YlSU+Whl<*^3FnrCrG(gn`DJEfC1Avz+AV~S*ng6s&f2m;l{Q`lS;IG3;!)Gv9 z=$}3Rq$>Q!uE+D68oXz#$645XKyqH%^33(o<@xz`&KZ7A%)~Dly3aj9vVM>&=lA@w6+eJG&OJD;702vIFb#pvVlb=qihqb zyE4%8Z~Y5x-b795x9sb9pjqRSpu_{ib~M3T>rSI;v>lG0D0GF?aNg0Nn~N#$6IY58G?lYwRr%>D?5x zn^P*<2Tz6(fp|#)rq*h}M8X15OQEDl?(B>Nc_7T~OGQ_Cardl}VlYco)f*Rg zGb*Yqqd8g|QII{i>|V(G6M*>W_x%;wk@h+2ONZ)*gysU%H?+>r9jKE^=Oxx~j!@Ry+ zVP)47^miy>K`!>uKQv;>1({_xg}{^izARYUOevf6&A-XZI_Mn#@IuGG88y0rX0&Ej z0xMzagPALAh^^zIyU-&t*PDQqkkJfKJJY$l-Qyw(CAAPU^ePz6`xMmK6CWn37d;Qf zfU!`ZX4e;EzdI5?L!hW9EV7V@d~^XKI?-ss;$s|BktY)oi33G(@`Z_l>>))g2&Z%Y z$&pR$F$fetE0HY&{@@&d*F1zQY|O;};#rF5j!dH%fn>~LAiXHU@e^kJ{7XQk4t_hH zq}{Q2D(M4&`zluQ7cG}?PP+lGPgFBEKTriT|Fi&KK%l=$Mz;ZagAon6O8AX{G(k?c!?KQNWZL*x!u3t|J>Aw>b#CBT@m=k>qB zhl~z+y}$?N9hh=w4A59^G812mVF(MU8^XR8!B`H6r(RE<$n097`etPoAmjX?qMCy# zJ2)fdm>l|NU2-VEiC#-~iiZ$pzrWoWdUi+~V?t%K3+s@%LP0w?;SO6-w<)YD%)kY} zd#z2UsaRNj&qy~2$a!Rw#E9~b90qiHEb)|oK!^=^LJR-3; zbDn~(ZGzxQjT0ZvR%}e9Q{(T!NtRLk6!5xfm(R19DsJgb&}Lh{hD(xdUG2pL%X0Jc z9s;o7j;hIIIaEbg+57+v-?v^-5M$!IH74iu&+c-E**2N$!*p%mBiPnhw4&dl7FN}XV6Bs3Ww zY#+8MXKflbyB=sK+lSW%sE;(A1T}r6M0^qy2W9VDPH_69B7VP9NtRK_NC-u#3t*rn zGm#9J8%;=Ij`Szu9D`)81b}VPKF;E3>VXy1N_ON)?2h>~bSj*W8Q}r-KIQu(4W^Qb z<5v@dO-M0iw_2Q|ccVPqD^I+vP0)2!(p%cdeT9IV3f!gDGGB5D_{ z9kkQ{en|cw(==^>LvC$wh!~GSkR(xTC0mc|I;XlrNriB`*RfEb&6PPA2T(B2+kh~L zLd!pvT0w|Rh1}at>|o!9h#tx<#lOl|i)Wd0ba6#h;J(70tg4HhQ~!V{ETVeQ*ZTI> z5SIJ^(B)djv>*rCLBcqs_*Q&+lki6kSgtqjS!{^oi>Mnv*}fgQ(1)HWd+1ntU{?8ge?TP6pkToTq2BB6-2U!czX&8ekP{~o{ zM*Sy5QFBg9(Y%vm{@O2@bN`XRg4TPlI9P~GW|{@ihWQLlCw9c`UyEN_k0OdbM)pLR z%KkA8zf7e`10zUC)A5pti(<!x>?|8mi`gnJ#4QFs4brOv8#+d0pl;sCbqQw&_lj} zHP8T9qPQtxox0vY*NF{>$lnxVsc@jTi2Amb(un2Cfp{3x(A!NtH%-DA8F66~`_95e zRQvej#T*6G`KTE~#%)0-HYqF)uz0w#>yf((O9q9#bYQMB4&u(za=Z6GFklb_U;&me zVvOEFnF7>~<+sWYb^dM7*O8tuGF8%ml4mJ1J{EgCm1N=2|i-N8$8LF=I<+u@-B72z*hk{naP8t)r#_~T^7NedfO4Ax{; zgK(u31Ks&+<(M^qS;Dy&wipH=hphk?s233^4R>7A6P`McoPT}1v=P<(qD38TzVHcU z&-}7qLnLpRV&Rek|LrZ5Klc)`#$|ctl#sRhP)ea^XCCFUO&3YhsL;kSPIX!W{504RldgNffgz@XVK_pa3aVH!hxOWC{Xc^-nU|zsjn$Ky2VH>p|d3 z$F?47n#f4Bly!=o#)iWSUF@mR!TFxbz3NO^>Os31y{c~1*!0MY`vTFatb9Kx7QH0M zOq!edp0V?z?}1X0->Uxxm$SZ7WjYqHC>4*b6+#!F3@LhS!uTGgN43ETRkP+@p=E zRo;dvvixiUbdwbupN2(rD?798VP@VzWVZb^VSfd9e@eI-sTJw1`X9PGH@UZd>iWI% z4*~q7UPGar-xR^^(PD`^NL8HWeX#Xecs>~=z3<}7bJsBv1~*7gfk4{^wO>izDDM$6 zgCDPN-dWu@^R+j$eKScNSD9@OGjTbrPMMMAdJV|!1z`V_;Jdx+ii_aX<4}!b z?b5Z4J*|cB9$hrFT@?6(hQ4OV+z_en2zo!Yh-n5Ovg{=nW`M72U0g7&BONYoN00n0 z9IJ1|N6SFEgg4s{oFD!amI17a{gns_J)AYW%81PjL`F=s4n~gAjv8?sJ4MahqT5p@ z(`fC&Awu7LGW2Fc+(zsGZ)QTB-Q&Go4EuX?EOVE>AucEJBQX2#8u~yd2(3;T#L=i_ z^_CfgV7zcnZVu%!F{1KG1**&m22E~D_@jN%Ee_Iu>i@U+zrySI0ax6(s|ge5eRiAy z6%3NF<Y?_&`WcW`fjXfsO&5h>w%a{iZBqx?uD$x}+Ro1!%{o02r;k$WyNwTG$~u zyRVj`sL z+(Kj|j$a32Q$!$=M?aPZp%;n>)Y@xPUie-JXuIit0@|>C7{KR1^2;@aEwoXf z2ILCW=N^EtkErl7v3I|{amJrbLETR}#zl;D zy%yw$S3UT-s9xW%A}LeUCqqtR>be7*pCYypa$GkJ%G{&GvHfUAZtLe?Kk-P18*Jwc zm%%7QvkFMRv}1Wa3ham1L1s1ocZGE9gfc-L*@zKKU{;s8&v4Uxi(MGwTZcw9+ZfOU zKt|9Yj4@J6!1|7FsAD6XYbVT5wxn#)>|`Ohm_Of`y!Zwge}N&;~;!iA$-PB@+F1&O#bCQi^+UOw4CYt zG8dXl<>qi15hIe1X>v1tmEfzNZvi#{pzG4!M8j)J6rv`3LKrb@GWWOmPy4E%uiXur zR!5=6HI3Ahyf&`xjkKtJj&bIZWl2#IPQIO|_`w!Okz_3dX*+@SqAFFIXgNFpz*zxI z=wNY)Q?`b0V5a8)M{2^|?6+XCsJQ9nt6Vwb(gq^B%F(72hNO14gL&Y#VBCDmj2%fx zrh~3|sK;KoBVL7}pevt{tq4Z3o)}JVnwUtxHqm4>VsCoHFUbywISi>7vCK_m%;0MH z!1bCx(=MKU{d3e_hPcEc7WS2y0b}CPs=9Zw5P{DZsY(TBI#5pCkg)p-s^8qjy={uR zuaS`&pz>Pet*}xh^o}S(FGWW>4|%lEpTvS9Ax?{>u7j*4U(0AT3Az_Ooqfq&)<}o$ z601O10n|c0ZHlOQgwdQ-ThVave`rZ<&HhHhK`jgwGTLldC`GXAo)F)7hdKMBM(6zB zRu4PyXMc1ISqOHiG%1u+DPv{5lyyX)kl87zNw3m~Md|^jEcU){s{*k?q=WHWTzHE* z*w#QBX&qyRNE#8*l{dIX%<>&+6lWnyEJdO53vilEBAkImwquOXH~Z&>?ht2o%Pcqh z?5T}pwq`#yaP;KqdVLR|>Iq;3kNws_xlQoQA37*eICr+xehk!^tcYQ*t{dwMku$OtS054H@k=nIWD{uWw*4ZNxq32RJ( z@*iN4eGgRGc4bfs9d)uE6|J@6A=NFqoH)WAT32T!kDsQ1IQ|R9Y1Ea~(*G*rPclswNMU=r)EI%0F{>D0R2t%yc-w{2n#UIa*R}wVTJK6y zlpwJ58FQ&bkX+euJweh zi>iPXjn@}MFTj`@!x>bk0utvpKJBSp?7zf*oB`Xm!3S9+R6#36QUQn@SnKE)ZYZ#P zzFikoZA6N+a9`8%L^E_c*%%pVcTPsH7&@xLLC{^6heXs!m>j7_FKc*Yi(U%5=s$(~ zzfqKm>XPB4w$Pnv_Ya|WoYuf~yUfi&Ppd&;O%TK8B;xnlzT-a~g7r8LXqwO~E^Y4W z>j2$j$cP=1r9eWIwZc-HcLr8nkxHqm7-r(v4lI5BGy~N70M|vVs|Ar>z?(M3-G^ZSyJ*uUFphezFtD3-P1`xl3Q&zEf=?;E~5BSw$+H za34etofgz-gYt|6i-40Jh#BN3vqg1hUDs-O}p_!_yAj;v2 z`oN1YWW#xX;#3T~r|Bu~=^VbgR<0N=)Xsb3%tWqJyikUJu_@@&iK786@R#Ov=0z@4 z(+y?rO#ncCw<&15>Z#YZ2*nJkuXJ z=tyX3#MqqVLfKq0_y&cuMAsUsU$zBHhrX8J(!!;bTPCL82&FJ($#S_D~7Dbm^-zx#_^Z z2FQ~3pE1y?K*pF>3Y{M#0F#X*(wC})v~qh)gJ^@4Ao%W~{85_q`v@Md<>j#o zm2WJ%=2mf*J7=N8dsPzD*r@lYzX~+KO5yN!l5XPY+9p<}zE+auFV?@AL2rXBFj8Cy zFbU}X>Pd9MlJ@n>m^fbp*5z=+w5n3(-VDHGqiD*UrlhpesE}(LjSVUyjFCxs$myfW zT_$)FPn%!~#-9qJ>SK3xk1V|$YF*n<3t?iT*E!mbRDHX)i3#Oko|l37%bX@c*q0Q7 zW?CTBS38L3{5>(n2}|0~uS;%4?B1pG2?MTT4uol?(fBb0Fxk9`641&ojl;}KQyJjC ztS>CuZsO?LCT6C-j+3s~NO5^E@@pQRaqLM8eZXxb7J zU6n_^9#}9%YUN8*2Klxx30FbQjyF*Q@vPk3FcVXYIU2Rv+M7!bQ?fuT^5L{)r>418 zT$j4A(2dmElm&^^L=#J2V<$eCxl4^@uq}<%shY1u)t}|qNKA~Hu4)X~GDDaE7#MAa zHq{Y9g^BF~FEro~k!|5ZjjynFMdT!#mqsT*zO=QO7sI+^)!Rh0nf1szioq^k+d+J6_b8%7H7imVcOfi4 zbiNmF#a;{!sB-vQ2DdG9AB!|Vk#Bo}p8J7pl1@Ic@N+X&$!778Zg#;kB)UZH96@ej zK7u59xGiI&);jSYB~4V#)Yn{b<0x2y7~9wu^w3#O%~ax&t|X)R{JqD>Ldw9Ag&>b32B)??F?C$}53SVkZO&t>1p8(6=7tX$|o?u%Y z%4F=Ral~KV2s0>C89ze^W^0rPmcU#ym3oGoDLHHdEIU&Rq#0_;*9Bhe=cdiu0c{tt z@O{zj+v*F})qYbTV=&Q&Hgn>vMMSpFHKtO{MR4{XNF%}aXP2#ygCBqunb#+P11t;r zf8uT%SKv@ND6C?7=Jx{%#5nK4*!bR<;T$1m))P9CCrsF=!w=$s2WJ>LlAd17EiW_L zYWD!J0_QE{lUam_mam9#a7(a;8?t~22YSX zJ<_Te1jU_IpErUr!wHjLc1LRSmc;r2CIPeqL8%io2&6m$Z`YV;0S`HtHY$-akYHPo zyciR?618)nhmDN#I1wEnju4OHQ+wE8*5_AV4MmY{p`!;^&!T|SK>-H26)su2HngPx zrxeT25d#k|H=utz_j2!mjNuaNc3%H=iJAphk;+-n<;e~EfB*hpEP7{-9%(4@rYoJU zNLZVBQ5u;=M`~)~E3(d5q-#~@_Y?@Z_BPTZCrVGZ>eITK^RT7?mr-3xB!@s(dx)M! zjC8|ZMe5VAf`LSR8t%Ox;d!6r&a~$@!;t^DhFG~nuF)VX?tExAhX<(t;Qzl{#rk_c z9Q$uL{4;swQT@k8TJe(#i^T<{g_w8EBpcfT-_Dxk-$d;pfZ@d8VEa}kVyRTh^jzO7)CS}a>0*Ddr0~!@{rdTWvVPafr|~+#VPRm;<6EQHWM7rC`G~R5z`TEejb?)C`%*F5{%Vd7a{taoE^T#Hl3G+^!I%r#vj)!jPU2W z8xLo9`O^B5;GH(uc)1cCaCZB$;qOAe_*v99D*(TsFB`?j<{LW$*)c5uzhec=yLUgG zHq9S^pCL#b+WhpLeML83z=Uc_o7BT_!Zx2c=>?!K1IAaTK&)@k$l+@JpZC7&cK_o( z4Ev`)wfj2oZ}H1(HW=kms|r%c_TimO8CpuDb$f{7m3^Ymls+048~rmrWADQZ@IB+C zWAltbbC6np9=a!JJ%E?aRq6h~|D-9CXT+fnH6ZVnCX609QV-n)@A+g|Nz*1reYHvR zgCMN%=eVN#pXmL=zwJXQ!oLW7{m(Y$g0h(d@GolifpPfEa0^VPee;*|b}()vR>U+Kv(wRddVeTrXCMr2NtxG8i)wMM7542 z+L=qR8eyFlz()9nhP9A`u&`_1Ar>K*;WYxZCr|X6m{s&UgWtr}?!ZAY zIjdl~CJv%W9>!tD0=`f#qySPc%H=02^P?vRQDK0(|AmqHx&QFSmjnlr-8n7#!sGSW zP@o1r-Ty-iLw*tk*#Wz#r%c?H<*I?U00HPe8_X;dmU?S%dn^B-39Jm^E8^^5 z#JrnRjw)}|UNn|Z`QUWV#ZVzp*$a@Kospx6-7)L>6XM(6{|8@vj{dKBFaQ3ke_xYN z602WvCyvj}zHB|_u@9Yk5R=oPh`+h4cH)Sj{n-Y*kvA5V<2H-{W` zK6~KP4>;GW?8Qz`7c}Rrzw!G&0Q=3KdRF?t8c^P2P)(G3OzbUk>K110bHe8@cklm_gLEx!$Tfx zRRbtW9Ng=8W}tdSun&+V7Nq@*gc;6lkew)i@K{7oVH$|}CyZeDpmimYd%oVI#iCq( zLVvl*L!*h4e{H$*zt4#J9UV1YOtK^bJu>lW8`otDLq>c(dX?+-0+Ix-CV1Pm6XbfS z0YC38O(66OB0wnnOEO+NS1XPkCi->-*6m_tcLt-yA?>xh@qv|Z_iSfX(~-Qkwz`7d#s{8 z=MAn!+856IdW&9Ci(0fhPU_$Ib)x$9KmUUtYW=r==JV(7*E#YTPL5cF8xRp4p zyg@Xm8}FN~pYYX<%|>B-5z_&&p=kn=8af5noNe#0wF!5sS+h$zr#Fx)5E@#VH2L=B zh__}V0lh-N?$3@ZKV{0^yrogOp5r5?pD)(+AMj8A?-PIgCx6>VS?;c6|Gvk6W%g3< zT7wj>IxCbcMYvlNjCJynv6-o4A6`(>8x731KRKc+&fs#eS#EE>Qcxc8{FJe-WGkTm z{_p=2fAHh0zyDwPe@nj$gK1x-yA;7qP-tu=@wx4Fk~?K9VX2H@8s0qnLm|ipN>R*$ zog6{Gux~*%KPF*Mm!NNuD-#g2ao zKI{4$fBXgFF+l$K-}_r+80*?=|bk20{(mp(f zlZFJk>N&;bnoQ8Ehpfgob$SP2DYhh^IOmx_Ej9zW=@K~ddu3I`b)#A1OhX5z^Ve4T zpopl^>TKcH9jGa%gPKC5C*ui=c0_@YL{a?Ykx-Fi1!U9=W848_-}?YJ4&+1+T^{>{ z`zLTxdokuM4UU8e!k%>$sz5M?*o&6F3K1@XdRcMg?%L16AonBdv_~Y|=h1Vtr6XMo z1e`_h;_}FsL-KLGB(?Hh7=9*rdinJ~@%R45@B3KlsSf|XKm4cuVbUwR9}1?YPPWj? z8yCZHA-SI+D+S^i+NkK)=&>NTSCSGj#%Ve$XRHEAJ12CxLKA^NC-3c{JeVAfig~pN zJ_A1VuP~V42`HREhIUp-Lj)x~Am51F)F1)DB<+onA^kG8!%I zq!u-3qCqOkAU&OW0`H=J_5{9{WLjX=qkPEI98st);D|2zaSgG2UdT0C3tZ*w z*Z=1~`N>cI0l)E))AGY-%TM$Zd4Cr1!+tdQ_uF3o>axj@YT`z+X(C4sWL&7?!V?Do zS&v6hY0=&kwvm00!6`&wL}DI_`vD3~;y8wNHez*7SyOA70Psq=g~b{L)ovRz2$CPg zTP-xSxb~TKR)N@(;m=qo(l62{!gi<=Ul;OzzPjXu<&yqfi8C-1L+2s=zJI_U|NsB@ zKf+pm82)~bs++7CdA9X@B#xJS)+yX{2_wul>pZHE%Q9)Z-p>!zqy+IBhV=Eqkn^fT zQsE3PVT)uDpH_XRb?UQ{5OmHUklk+##*!|WjU+QoVerZ?pj3}+F#{TfZD%AFq$MWW%%|^2(i0Y!k*!AbMt#Q9ufX!i zpMb0i9n)}uc)2`I;)hiH5#HWv{AwGGUo0Bfsf|^S&9UsO%hsm{fL8pGSZZFQD}Yyt zn1Gt zw2t41(SDIjuoy-MIv0p#^_McJ1dl#`F&XCt=keq;1M2*@0l3?$JkD!^EjNb4>R_V) z?yejUGvAlp5xFxU5B;gTgYc*cN@;xjk6-WUzd$>@f#|HP*`1L50RO-knmK)8&O$? zW#nAl7&@CnCn@y*s!D4EyAu6hdQ23+&fS>%&fBn}# zhT?DWXZ$n%R6o_9?taY0`j>u`&%mxf?tl+2otT6Xa4*jSLV!Ya%AxFyu?soapi@*70`A7W* zviKdEf7E5O|6TH6N%8y7{~Kn@Z~SXeN9cFHm}P-;gYY-bg6xGv89yL{Um@U!;!hkb zNsxU0p?)a-?pY}Q^MB$$-w*$v3*q{^vl9Ey>m|x8`jAGjM3Dw~aYGVt`S*0CFU|{; zeC`)IhxV7h`LZx-rIRt)O|rZEQ~UY;AJxgK1rNM~;}`|9D`>Ep$p3$FX_2!In!o2y zE9{1iNoqX>{!f+fADl@ey%2%0wRdx&M!VT9EX!2F`d{evqQRczN&6p0Bt+UJgtPzX z@A@Z*CaN9*One&uZ`&UEt;-qrHGuF#$6v%e$c61c65hZg?+U_5-JaKA(2WM@;j3IVD>>&`}}6jJ!8(DjI2 zLJGexMDjTbdh2?aobTb)$?vFCr!?}@hcxHQnt72M>}4;Lj$cwhDch($$SW^Q?SiY< zSXPsS?(b*Sp(sW{XV>o4zbJC@n-{g}thjG>vwu)G+z^2f?%ixJU-d3{p3h&I(1rhe z4|K8{Jtz{^5-Un3bUj7uNNtREiIEh3XYO}qxUH0iN*m%bY# z&qQG{Ic~6F>X3n#;Cz1S%IHniYZX;)Vp^wA=`DqUar^`RZ2J6+;7xz#ReQhkZ1>Bi z7Ko;bw5HwltLxSEKY3`07Pk*90z*>=^ z5;11V&kOont`x7&3LiVe`OS|KSF-g+3!SV=O)OLCf451BG*yZpX%Q!8p*VHLD0WX@ zyG7!XA)v4f7>$?|_+C1Xt3KF9ry^`GFi@&RYis=P-6F-$JhW1h5MJH`@xP*d_RVLW zLjoVxvz6JR$&p9?O9j|_u90sxP{dc9PD&nV;dc429O!#D3k?&SBJ?nvHOJyEa*FQPu)u{RfoLSSGi`&Z#^YUd`l`VM8-wADU&@Z6a zW&=ASWe)grh(^daRrbB4Y3uHe%V=@d>vH7Vb5YodGf!Z1$%ap0p#EoK2W;$)S|eq1 zc`@?E_mkjRYhVpyP!a3?*;QJ=D8I;s4tI>iQ~&NT6xaDk2EP-+H zpIt!gZfS0W_@@xeF`GlbWl$DIZh=SCgXja+nKb^7wq(druSF`qvuitb==;pKXMmS= zi4!yl{uPpc=>U+wLaV-Dc0FzEz>WV3edpnZIXRZnd31w_yU9)E`?7(-KYyR|mdXi( zy4wz185-a%jGwmyNbChz8tk8llysPn_%)rO%SUCm7X^4UOJZ7Hr*#+sR$U-*0dSV- zHW+@yg|M5cJ_$q)IEU(J0gwx{jAF$`LS&vzIzKg2kc-A=o64iWY4w4bC)0>^g=-~# z?PT{I{v&l87>wy~D@lU)GSdH^r7EZ!GvY%-(j{HI4elvZ{y(G~cAb2~3Clr8xGb4S z5-By2jWJBN^z55YB3kkc%&u2xR!)u_zaT5K7LDNzR8cGdKsE6o#rGU5Y2|Q z*Su_@?T3&IQA0q5(&BUHbE5;uV{m5{lFkG7*`Z}%>%{%H?5OZRY zvtiI4dqY0gb$wRWG#}tsk`Pd#LghpvsvLpFh#qyIQ?4b;1&|aS@CWLAGM@i*&Ao;Q zbB+}Sb4P81lAOZH>N)RcPMRPwKUww{n;1$KzbI+JJxhjY@z?8a++9#Zz=;(qCjzm) z3bPErun3Z(K0g~TtgOpnhkd5#I@nP#bL4?ZP7J&0kILm0+?5S!MH6mDs*W*m5szUl zk`&@f3lVUaeFWtZJwK*Eu6EdU#wvI)m#hhh-*7tW-_+}mgTzeoof)*??+gwyP(v@I z(!O*azLcDVXb^}gl&;E;L?ry;Bq7X*zZof??m|mw3_B)UkiOjKI-YOa)w5TaMeX!pA zxdtZb)@)8<&ZZFqeAOoaC)t)QIKyNX^r_TrR6v>P{t+Z&xq0xmU4v2kSBVJSBt*04_x%2L=rcku&>1rG}rb2qFqR12CLr zLJ9~(vw4@z#kCC+y0`@#p8XhbBs^9ly_|K4mOi?&4Hdu{A6gsL!~1z#P85EhBQYFB ztD|ZQS<0$DVxF3E+|%ZPN9pN@zeJI#p7q4N;JM zAa7StZjMD{gun&B)!c3MP}n(1(o2HFull|yEkH{rnbdi}-vJyQN?Y80hk$-528y6) z?YQ9KK!Lk@c1*ZBV{s2jvt)8Zib4~C(vk&@p&mYC`!f0NRzh|a8t2jo9ldoNh#!*R zuQ^IU6**h0V989QHS-ucExg&+tb;~BN=osq35fd&98M`fs5h!IpfYvTGpdwFTy&5t_+m4-r}8EtGhF5F#$Fn`inq(E?bImpT=n0T2q{#X9FL zLXd|Duwn&dE4!$Nf$|w(#S30s^Ha5V4Q+QC;8qrjS{Z_vUE!cejKHN16j^yNl2ZU- z+IIb<9{H7G^_bhpB{hWtXn$tRYe-4iG1D4)av)*t6ix*;f$>oYz+-pJg?L6ip^mkW zg&Muy0|9XH>c-h542YYg|N23`bb)cZlq|NSk#tQTHN?ukDo0XE3<)M~8iTTtZIpWd zAXcO-5)L5CS$29k%>j6JQIUBiA>62TF=rG2));9<$5xm2jAYJN0{pvZD5s*qSxreH zX<{-_;kaHLU_g)scu;4&I1rL^2H|A)&G>VsGlbGSHL@bL4M%cXA~P`5BfnG(e~EUs zOo0H~pBM8QQc`xzv<9CR3>H;{g#o0ngtd`rFhP}iv0`YaEl}o0{iH-huJD3DfWGt2 zMvz@ck3DSsPkvSPPS*U+g_fLKMj`lY<%)7Ry1|-Z0t#xGp8}*K57BVX_l@N|4d{0U zto`LKv7900L=!se?U-lo1{q>9e}+=42=0Ly#Xtd^@SlHYbn$4=)uB7W2vGBWSM#+h zjB3B*`3}j6jwv98jFjLGZ0xs`Rpu}xB%g?$3A$SIY>`^QL8*FKrvPl?t8tpgKuXZvgHx%Mn)H!w_LZufm;YK(})h zotagj;sDE!N9Yd*&K7)dP~NFi5WpJ#f?&FV09;M1k02X{+JA?$C1TAv;0Qqx{Ym5! z;_}rYIo4MgKWuKqN_M7%dgQ7DV9)w6BFe~_5V(sf0Zsfr=~L(O*D;rF<4vL_XT9Fz za|6(hufoQ}23RtiKe&su2MGJYB*BJCv_HbmNYYa$2^>iM?e<*U?lT2x zF?Tj=X8Bcm(CnWZ0#n$hMfJS5OM;6stwt?k>dhbUMs+^``a~HcbD$)FxLp*U3*Er& zGi`z1hA8LG<{%Dj%*ob8=Io)bfyvoUh!yj!(|wuFwlmD=(C;IjGhF<9TF@jYOi{;y zHhDWBR*MQqRSQ?X zu+EKcGg75~V;IpdzlZIA&l>%q^S#Adc+?w}d=CL$fB<(u81GNK(&)0=B9eP0f;%9R zpBf-;(mk~)`X{a0>A)>vf(Zp@fdfOsBr^5}|Jz&rSoZyseFQyq< z;SNa9U9FCnDxrqM0{!Xlwl*6n*D`RT>_FymmCluS9h61ldAPL~;=t8y&dX=o(^e!6 z&L%DijJK)keDc)h3fe_cT*P^a@UmYrdm*f-bIcW7U(x^UKR0R@1{#mb-=#N$k_d%zX4_;)O{+DcFzcDZh-3oLT$y~ z79RGcXZgi~b2mwwX9_p~D}$1wPC4TVKu&kydgRXn(^u}-n%dcb!3z=J^);4x4K~lg zL>sI`zX)k|!0Y0{XhmEe+>|jdzTqzvh^G&8a!9-y%q>Y^oWO9O;OI=V1!fxR&IDip zBT~Jq%n8(Yofe1@{KUeb<6z87_ufu_=x+LfL`$3&mYgidFjmQMYcna_#28^;Ah59X zSRa9845a^!Bnb#2I!4?^j8$l;0sECHOI(@dtT_koM;|}n*b-x^{Zv`rZRn#AUlp@Z zH%4+_q)ui1Vi0JAk(n_BCT@!-4mbc#bg#25j(0yB7YIhxy4=k<*qt6$*$9bchM}0` z(by{Be!4A)xc)_4%h8dW|Kx>#^cobDG_s-0_r3WG2(fWpqU%#9rO_$rH zkC=3LrUcY1+YL_{514!a6?V~0H9$8Rgtt0khKl62aL~P=H@aiH!A0ekU4a&p6R;9Z@Qy9fiME=so8w}haz@O-=cabN&aWuYui$S!msx}k5lJ~Us#Bz5D5$c6+C)X;|Jd7N>3ExK@`d|#AlDa|;UD>^FDyCY zv`LArvmY`;rSp=PY@YF>?#rm2WFsl-dk8m!T@dQCALNfY775^9i65npWmVFZFmz}G zgjDj}gf*J6a!Vr;k}(3_w`6baUcl))~fJnW(;%nOd!7Gou{WL*-O zo22EAJWCf4RldAy_f!8!2XchYt$^9|`I#wPYkVt!wo-<7@2GxPi7-@H3m}7-h2G(8 zS0I;*HBr7pN|w2Rl`Hm*zCW%*F9VFFp=3{p+hRv`B&a$0 zi*B;q-J|h7re&Z2ArgWiBxAXi_Z^x|HGas_OF4yf$)4s1AmrUF%jSe!J-fbaMTNYE zoUF?erMaYL$^aH$pkE}xD{|~{u{$CadXs}#+P<%r;}~P?_sO&tBJPrK5dbXRj|}M% zuHDW-%m{y6IzYCn@9(V>aPxhDFr**Al56ibFjy=VG;h0cppqAjcS=E@5+!tk>|&-n z!?9t2@8BtNm_$KtMlw`d*kv}Az9f7tHT*V#QPp^>z6B-b!T#8B}xto z;uK5-@3!<$^VDG&1(PVe4l2#FfEy5qnb3|zSM*_&`x0-5!VEM$0b*J|a)ZDmX2mpj zr@-tpGc!^8q?5WL!hX?-5DwCU+aD1HBW=)OYOxIEql6e{+%gK|LW4OEhGB6b$nL8U zAUeTsn)qDDkF?hkHv;&!+k&_oAmMs@E@o&GvVWKZynkol+)7f`{VouNSO<6_JqXJY zg<{@&rM*s7)0MJO_2M^7dlNlz^RPib*IV5@)Vv{{qL2)-*!#dD)P!EwewdgA9KgtypX08PsRE;{69 z$|nq}ER29NE%Qc~yVtZ}p~nCz2r!dj8Z~h}9L*x>g^)%WM1@0V7-LlXDj3Std6X(Y zVsGU{Q$a;VM*C0-4Rl5?P;?5Won&I>umNS63^`7A2e@*%S{jK zZ%2EGg=txVbc&@KnZq9?W-i9aIfk$5)ylC!B=4(nAFyqF%J4Euj2xqK5gINtP=2j+ z@4$B=m!&7t%Ggd5G^eO&gdL3d3RCAI>jHqgXv`C%6#Z(lEZR}%Aa9yt8EOxyb9h~} z*HOBOLU_W$+{-3)BzUU10}LA>+H%w!BSHV*F9d?746=dd$N^CGIpi;_zO4iu5I%3h#D1QEYz z-f>~XWhLDhK6&12B>!ed6wN(B6!oTacK~(-St13eaEDHbu3kxH^0*DO^wle-1V0Gm zN}9NH#?XFGB4~R~$^uTyxp`dYr#)DNgyN66g` z2o#9@=OVXE6!(R~O+i`vlPh3hw*leE zf;qh5em8g6)46^DLMev^3?==-fVi8Z0xIKE7uS2@8`FM>aSmZ)0(vR{Ooj%eljfp0 zLMH#aQxr$7M6YfWDuC)=L_q8D8l>n4+zAGWh+j0nJS3Du_xvV9r2MM2@NG zw^KHA+oK+FYi8#d%I<`4*keTB20c%xdp6e>CWZNdmGDw@n&LPu#WMS{LZWZ#BFtk* zseuTVE(?Zh9{L_J0FTWH)mZE85FX_igf1boft4wFLAes<9!WiZpnuJ?WLeaEVQ?qb zNLqUNVFUACpZr<{zB80#TH?jtX!DYIRM{3xIz?lGC?!AeRW};Y35{Ql^X0xn1$#^(KloOrM*Pii25?Iw07d7H@ zl%|V@>RJnPeqmu`HIVLD_Kp$fmYOcquWu&D$Jb|+HH^&YFE4q?6!V%0gg8QTc;vX* z;x9bL3$a?GF9U5!6iea3m?;EV1~Fi6swmevO~#SzFgi|#yHhM{OuqJ*-=Ri-SJPDj ztMK(_)xPYwTzigk1}A=NMuXio_x%f z|HS-0Ifkym$HEOY4V{en0un5}*-5r#-ROOydmU)^gLFDxGK3VH&Q6WFb7dltKbYiVIPF23FJXL?95JZA6w?hO`f?D0AKD8;yXD9G<`SxYrf3!h zc0bD5k4YB~fvk;_}gV(j9D<0y`ObU z;#E6q3KxJOMKTZx)g~6QF3Dc9`Wv8R;DmC*ZF2^47G)A;At4>0(riJE|2dyyD1Owo z0OtC$OgRIB?hdG8ZYud8$lhC6FN_Gd>L1NO%(NRvX!lY_*``26e1SnH=lFvoa<4-) z(ze)D6n2ZRng)L8T8Ke=T+Naf1wTp9IlUfK<;S_zcb=&=6R~j-QLcN)A2}kCa1kvg z7eLYSbnpwOsM9_ydP;s2)u1YecSo9bb*Ha7B4;Taf&<}$IX+}jupI3Ts`Kd0ij6y5 zw$Zc^Sq{>?FwFzt9`@js58@7i{C?-S&>s>1@g>ANRY;+5j&B_g=gH(B3$aBXYrg1b zAI*I_lc=|H`1$4(GrE@DwkL*W&31<9A-;o&SSeOJrDdcwo?bi8?4bCdy`u=c4&d3t zxHamA=@28E+}*pyYvDkzrA^$W;08%9fi4NYXWmTMm4t}UnU=e#P!KfuM>Ua;T{J^f zeQqPvj#ateUIWe%YzHA;wF}2#?rSB}bP|ztZ41QW{)0-ttywBiV~d4I)g^T#(FRt4 z_c_Crl88(|OMLRQnyy?)MKfk_Dcud{r8I|RfmG0~dAchM7NR?$8Q@%2vHb=P^O6a~ zmnjh;YDv_qD1{A|F>pdWTXY-;TZljbGO?zz2Kr|3RMlPMt!|>w`eIJ)Iq`!?mobBK zI3K_v1RUYVQ^yjO2~J*$HI^n3ro-$&J$|AT4L+B}1 z$1@U`33ozGH2RC;;1V?2M7l1i!wqAKo>iqKW?UY=s8Mctmk$W(MV)Vf8>Zn!d0}|Z4Cng94)trZ*5(a zo&lo)14a>siqr7#!y9&Vx!+}F2uF1S1w2j^k8Au+NuId}qA)BdJ+-`Cn{^hyK%M8v z&Lvtr+zu0?rzn`J*;}$JVrGIxm$Knfq^Vbh!?%il11&%VHTB0jb*DkT{59I)WCqIS zaY5uo6_kbXK>Tr#ONp6+&8f@DZ88#Tp=9gy!-d5$>{*ux)Z=di@;u8(vmA)vhxEb_ z!<~;m+T#wi{7z4vKMMv z^%?)@AvB>Ls3Bxr*AZpg;uo$bRw&|_8IZ!4vwH=B>v$NZgPT51(0C)OZlb9UIfvP- zGj3KqXQy!EJ!H%E1Rfn>`}3k8uoTMLV5%>3`gSrd|_ zI2>$ypxRv-648;`&D1Z1sy%@uC&bceg!}qYXH|?+@x!$=X7L#%I>25_0TYsfFp1id z2^?0()u+G;6n;c3-dXfSxN$0G$trN6#IFWR_y>CB@uKyM9c@xwwrVKcCL^&H%oJvh zd#Eh1khMDE8wOg_%d@)MrYic6QMHo5NmpE&C|-?1;@QlA-jC|bZhGPRo8d0qs(!eqGh^w$N)d~j+PZ?MHT^9)z)|V+K z7g1gU+nTLZ@g)kZ_XIvGPhlHkWZCNzWK5K`PEVMxIOzodY1k}bf$$5|3yzn9filr5 zxyRJ}A@5sqklh#xADy!lhtJ>KN1Pr4EuVtA{l|#zOCWY^0sTpTvkmgzkL2DiwtGi@ zLTodHC;gM#r$<<#@ffYCvsXvN?TI5lMzm0D?{HFfugB)}=Na}S15rdq=%t5G&|33Ui@%nbvW_ZNHa0v-hGvw$+6 zAO+;{^PIj=UUB+M0Pu=4L1I=fzVpkk5RLq$beWn@w0%nrm={b(rY;>)jWdFl;9ZnAkPRgb{nb1Gmggkn41ojp@(WImAT7{JVC&QN$`@w;uqH(;KL_3 z`h0?n$#M0MK;31|FBn=_h%<0d#r*9pCC(f$6T6XlYJTyuz9ZvtdoB@%sc~N^812kl zL+s@9hxFYUItF>ev_j5O#lS8iQn^cM@&qOyykbyLEd9|QP7cwu$SBOlOcSTdaMM}V z2q5Ifh%uXoSM%y$qUgNWxP{bdGlRm>fsH8lO9UJb6(!Rokva+q2|NfI7ZEviU>xcR5JXKb z@nq&kZo)Gx#4pS925BXi7te@}8wa`huo9UH0vX>9UG_4UiSb-xmp~}I4RPORw1M5N ziY6tFkEwY_QPB%OV@5a4?@)l(JXOxQLSEN|gx)Yyn@hQ{dsn9fILwrCYw5td?=qvh z(75<8Ml%&~bLvniTlJO)$p5I6()$bx8$#AntS}->jX^7fC=oT41C~t@A(e0mBi%Lh zvV((+HnwB8k=T+_Id2iBHlgM?St0Us2#uatc`%@9Lq-Le7CpvQilingf@)Ifx{4Do zS13^&Jq&V%T+2xKMc5lo=3xB>BJq49V=nk&qz;@yKwLp(lt;E3PaYJ^0083*%9hZrs@Wr zW;8B`5*h+iN zN`#s>AN!E@-b>Wd$8xlcJ^Md|s4aM;*sa%hEf2dNf&f)UA~k6ibY!I19j44HaFlg* z?Mg(-0Umu1?S)OpcNq=l_%M-7-3>UT8mSnK@a)&nw8_NMQG5I=(DJNF$Y}Vy%_}?O z4$m?{&`cA@yBwHqq3iP``_QPq-{lfcpZtnn>~jET8HwfnS&-@W`)1-M(AVCncessz zB%i4u`=9(hKPMt-hx57F;G95iWKzu#dXMBkm=Hx-1;Z61@dJBd1B4GP2O4elUqlV; zB*8~dZ#=+!M1gyqhho_ZOC%HMxwX}+}5E5u!A;njNkrLPP&uSBM><{r!(GB zig>HbE)2Er3AEJ$NPIE|X@N2N@Mse+CrQN?n&y!IZ0e}_IYi@R4m;h{8YZ783(G9q z&H^ufCI*r*AwE;V*X7z3c@CY=t-kE%oaZhBmFh$Hz7B`bMS0f2=FfJFGX>f8Pthf+ zkV6Q5yllG>p6#z-KvF@5dJ=cHDWfF+2EWYl$$pmL+e7G3-r7bZkem9mZ!2Pw}T-f)P!s!F23 zsSXdY7g0J)HVCVsLtNb7J%sVw&vY3y)kvQ&THMDQDm-D z^wtoAm=LcuJG)AMqYkh%ChTv*BJBmD;{Xd!V_p!h6&ppbPq?9FK-^sk5E5avNAkFM z3qlEYC2%lFeQV7E=-o&}oaVU+*3DjiTacYiXRu#fabH#~#B;>T^Tb1>2AfcR@FMym zR_Yp7a}^@BSBsE!>w2W9^Mu^yEyN}S+;eDc_ORC)TqNP=mO&I+37MA<2?eD@Bn~s2 zHo*3~;6S}_em{hQ?I{3G;zU8Y6U%r1*o^OT0tiI0!T<;`HwwU#nM04qQyR!z#YTk| z`pEOh=f!A8Ej`6>g#hev^2X=Hl z7!Nr}{h5sUf$7zyD?lOBm{CI$Z_LJFl!YK0VyoM9i1wm=1351tu=@_`$yz7WU(=zP zLXJ+8;U;Jm@u06)#3D-PO-%H&oNw~rY2!={*n7J&nA196^`F;qP^U0W^1}{_E#w7N z?qA|(n>3Q638$GPTtJ{U#meq>SXH~IFaxc(!xk!q6{EU0rOFC9Sj7p&V=e)j-MM-x zf^wz4B5CgPUPsc1Bs&#Lbi8+vU`Y=QW_QpN0`<^=0%!Tbi{BwuE+*8bN{F4hG%Sc4 zpc?XnWTqiEAz-LVL0>=x8m{5v7aef{>J{;9&BdwO0ONT?WwacB_6;6t%NMR6tj zeo>!1f^(+SpCCgnwS(Cm^tQcRBylriFxdwePFF{`5&=)KzsKW;9{k2;vMd1LQTgPF zdw*50>5I=`fNYxdOXznA4}Xnf-!n+wT}AzrG!B`NLH>w7fGZ~nWI?UfoF2~Nh9g-` zgFI|E_q2a5QMd#03ZUj;=EzG3kl80K^3SD<9bY(1_|CFoZ(fNZA73~xY(V@!otRss ziVPcbciJKTMwAR9sTDsva77N|f!`h=d4^#~5rV{j2+cYBl)k=s~A}iY5I}p)kP@b`q$`6Sq4I-;^0*#=ehk|QUF6tbJvC`+~n7>;3 zfr4A45{c*(m+X*%vekFJ+l%w(uUx+YIcn7F45j#u~8L-ah!DfMK>sG^!~5+h#{T2PLU9cAJ?03KdKP70W5=St+) z@p*&VG8ES9AOu&a^*IUfvA5j(MpevHTL52KW{HD@a!!x}6VwF)F+ZJJDI0YBAk4eB z)Ar3-D9;Slf&9CB!!&IN7HLDc3OleQ&Wp-jTxHgc7bszNHV_GSZ*IthT^=NEd{>Bk zW810Dg$))K3L$V~uu7Qh@&@L%mV%w|68~t}h}yZHTbn9=0grhL@+(IqCoFJ`N)87@ zhF~PsaPHyAQve-J1f;lsKYRZJqmB}dZp!>XURn8L(|mC~oamcf+>N9}e{dac|3zY( zZhePs`VblB|G*653WxmT+JsJ;gPt>-M6jLhY?ydP!#5KV>QXG9yym*W5FY;M=_Y{( z!{9LtL4iqd)bL=~f(qZqMV0N*J;^_m81WtlkBSgEvsX@{#z&>wmR7@xZu6Z0EoQSy z-VgJNN$1l7CsX{Z>BB}Sfd69OYXZ)F0_O$b!h|MdNK|c&{1~vPMEKGzF5P=&{md7j zAY=93DB}Cb|fakIOb)wqpkLOJ@U7oPC+=_hb2vY&^r9xhY0`M$uuvRI-GH zjSFf~c}WG9?Ir842-rc&g;bz2eFrz*VL9xv2@@)}5Rd|e1+wnSs%?Pwtm|G${W^X% zc(3De+;l)RK*G#c*+@m}f>T!d(?BVz@W5TAjrL+M1s(y)2~J$5FlF&)R3 zJWR1-6oG!R7BdZ&6<~)wiqz3B>6p`DAP|+;*<|2X%o~)#s=sK6Y^owYuf@)s!LL)5 z9#yBp%F{xhA#$8cz11L~_D;f&o-$9Zq2+>n=x7OLlz=$GEn9 zh9k|HYzXyuW2X~euX$NUvuah&n6|QNRy#yrooEy3OhM-1lTptkaWr%RT;;6@ObZOr zBr6B+M;PVy#XYGNRZO@i^i$5;{?vbQk{INV=H5=KvKI02_)aDb)$0|~>63egUYAf? z7tJ?S1?@(7k%CKbw`@CCP(xILNC*Sa;F@`)LJC`Gf>~T|!7bzpH+nB%v zd9+cVRRL&%BSd1)YUk2^TjapK2veH_B2z#+8pAY!6O`0YsBez(0}qmy{34-=P^Z8w zl9W^^fYNt9v8L^#gk8Xo@cEN&5FMgi9hdhS69Kclov6;HM313J%0E5grATx6PGH-e za1TCiS1ZV2hs2<8Q$K)Rytpy1BMC$7S~^rkfa)ro+%in6pJkr4;$%w`N2;9FFmF8= zR{26A)P|`|3_Gp-z3U(Icy2MaG0V|ZXs#$$m{-4v1BQxx2NYeJH$<)`>?!43p_*6x zf75FvY0>~m_i+jEg-gkLr2Nxv)@|1W$(W(W@oIyMYZms`Bsl&Ve^`#DMD?0^^_w`n zButXfCqbGkl+my(8s1$O@~ahX0W747XY%XT^x*&QWFoiKjRO}(X=S`xGFNQeiMh6)U1QOm;c(F8Rnc2 z?Yv+c3vIX1(qDJD^}HN^VZ4iBT^|CsrqgU;x&&ph0?xR#`*!31T+}DRZM!P|8a0vR z4KDzfLCkyqL<7NL<&u$ zinw0VL2L088dAznALwXEh&Xccl?8&&pQvaKWQD>jP80J&+f(cnehM=$|McGTj^ju@ z7-LYAuzo0nT=M^Dn!WDPokJcTLem6!;i-8b$Xy5ZTSGjFaBVKQ&_1GIa)(1couevuJZt3M&hpEEEPuz->h`~u{GsHnxs;6dyvIc!>{ z*)Cx8&opa7DF!%z6csyQFsAS3GIKS=YZPG8NOoqT*ivM|_y4=qTUFohIRNw&kc(LI zWucdZ1B`L%qQi>l`?xCUKQ&4(a}g5QLZXzSF+PKWH{`v z0(yFaUFYk?!KuO-f%I_n5shRjRzcJ?@Whkio6rj9KA5LpEynW=X}D{Oc#j|`P9%gN ze=Lien^CdXB|f*a=kAj=7_*wT&3kg0Jx4X3UA)Ia`P;+~o7eBpBI&xB;`^8}OzxBR z@9P4G2cUO8#{tnjp4A;K`dyjWk}!TZb@);8=~2IgR?MnMY&aic5k!JH7DiDT*k`XQNAu^bkRH{;e0OmhL>$irAuDZzN7^Gk@4eBjK%`Jyb zK<|0f*8@A=EHwG9~8MpSGkO0jfbtevEoYYpN1LA0c9b?1GP35y7iD_J^8;FAd@a z<(c_VuyYucm{3<^f;ohVQ4$cFVZvMJ(TS1_acrn|w?a#TWvnNj^QiS1sEdRlM<$^O z!s>|H2}EyIr$2AnJBR1I$jt~k-(4x4Kz}uP(d8Q>3jVbB@89F_c77V)2`PWvl0Ao{ zj!NhotAZ`TLXU>W#|Qr7eK@XJ>v66h@8_Z58;+9d+8FCvxtuwTve{iHc0ObO;<=N+3!4j9i0QuYe%{X>td zJVR1eq9Cm1xpj$OXdG+tk(fUNJkyRnvnRNg<>KZE`l~+Fj>1dSGPe_GBSAE={kJk6 zy(=}vu;-=8{I!(dCh@p)6kw|lVH0iu=#bFCE^v8fd3Kd;C;I)En`9+Y zaT7*l3r7Qm9h;Tn(k+u(O3`dG2R>u63d3cNM1d61T(ml3f5Rgjfo2Zs2JOX2L`Qs; z2mqSGjh};HS4A=h6L!Y-cAYhcl96@rVZcZp34k0?;f2G_SV+!r*&!LsgR$x$T=LL! z6-QfGzFYK8*{WXY0wcabZH?PTG?FexQI<@Nr_NZZ&n-(9n+@pjePynWkU8M!m7Ge% zJU2ceOjZRZ!PZ6(y~I9;DVZvfPqee4?k3IPhX#y;$z2(^zFac50ff50Kyd_uImi?f5P5N{UcPw?sSYtW*~u0+9zp3P2JPg{5AC zhUZtf-5okct`TWm?2;k!#I&mK9k@V=-I*t;JtB}!n*$sJa$VFXO_}C#FqZ~1)XxyF z5dEPtm^7oTOsZ!(TIP@}Dx(9tSNa8}>?n*&PM~&aR0cSkjgS%WrejH$*bk}9SXUt3 z9-m3kh;)*|c>S^gdRyB`E8K^&PMj!6it5IGyl1c^g$vigA_CGgbGt<@Hi~lOax9Q~ zNNj3McRt0D2y$Oj&N}+QR96NPfMxZ{`cthC;*F^G1$>;E@#YFuhbswJPQOMv0MLQd zsJfcg+{OI8!~MDiD?-g{Bt!+ua;U3rzl1|P%QqD$aE;suf9!pd)i+xi*bAj>$qJO- zS*g=v^dl+B9f0>yVn~9PrI2!o9bxv;buVn8FB}D%({rhfXZx7+$8;e~T&Pqk$E$67 z7PV4+G9^@T?Vy;kW*ApE1f>>Xj7!uSr}9!U$^ULOV46mIW;wR^;{fLh(Lb=(^K`qjDMAiH>e3cARy7(V0i z+YF1TmxW}ILzX#Qg$^T_MvJnB9E~2oz@o~e`E%LrA&9K(oOGdE0y}RA91!^)9?vUw z9W6RorBV2t<_aWgA))Hii63Xt65nZJCzDXqHg)I1A10=zOKl=nKlxKH8A!}m#(PU3 z5c_1~hL6JleRJrv0N5{Prlw47BASRjZfw8~Lf01>_BxgkkUr7a$zomF^PA!Y$CfPY*)L89bd)%z zM=&prp!169=3H-+fw}=ecwVpwsNqx+8^`X#RTYt??J#0{>;EGAi)kj_lQwop&He-|3kd(oE%D(cr=V#OTTGvG=?;5IsC-$EOak)4Z7i+bRjV zv*MUIo{qGeX`X)k6btlI4zBw(8kTa4_qfsa_F7Ma(wYI2TH5E@`J)1A!_PVfuyGyf zPaIJBbV*n0JM=ZVI`mFp(4tz$S`K1}l9bEw(!Oo>bQJ2=hv-7qxguAYk@4Z}B9~Q_4tEZzN5mUQd71%gG9{R zATu;2iWfwD-o$rHHV$-flBmDG)oqk=mG7T*6*W$V(T78wgxR~7f_b)8;ye^jvH{0)SJItfsU1p1Mmd%3ynBDlW&jvBqHM#Zj1Jsz*W81U5-iyP#(li=FEdUeG;yZ8;k^3wyr=59GH%n*ZxlN3wCA_ZB8FfKNYo127+ zO@B03DifnRXiau7I@J^j)&MJ>nGD%>E2kjpH}{-KVi0IY3r!%#BgGE3wMcTB`C9m) zzHb&XQL2$plkjwp@2hIIX_%k;_ipnyt|4}C87OLhVSL4)_l4lB9$!%5aas)Y3!A=H z!u|+Lbn0oLRMKGoA?`&MS6o-GrdL|VW*3B^S=+H4LrBfVi{{m368ItS!5-BeQ*VWI zt2=ThGES2S693j%{`Yy zoECAuD$Mwei;A)sM2H_%p@`Zk$b3df5haWBLYNYYIHY2bnZ2XU9#%L99b^VxGbeFD zB!`DI(qU9IRFU8V0rz^42D3ZQ5qVh!zcslTI^*s~#J3-32>VtqrC#W(>afre36N9t ztMcwE-m-lw@>B{#LWowA+?#h&8pGhu5 zv)}oSo{*H1zo;5sqGv?0)KspluUublUJ|UqGmAd+u%@r!a_7rPhWS!X*|%eW5MKKm z)ug3Bb3zJk$x3d(jOMJ5Sg>*&b;>CD^@Y1gz78-3@pWG`aY@8!5$Ef|jNi7nqEruQ z-o}<|x?qggZCLm5lI3Womy(wxwbR$B9H&#$O_s+oPzbNRUv_MVI~0v2V z@?neH+UgXr&8jg&K3&?MozX=RZgW^BlgG*V$sitK6Ux&P^!`=_!&rOHflT!-VlHSQ zj|{2=+#Qvlb8dz4@52s%GeDP3Jz=XlRu+$SgT2A<6~g-s^iNJ|KrX4M&nn0uK6Joc zk_~vd?hCnX);pgVPWI@t>_*A)2SVZoNKRs$tsyXM9VhZQlGzap%L#Wn8k4a7BaV!@ zjj5zR}#VA6yaq~jYHJB_Xf~aVe@BhVIZe#+0Ky2hjz{I>L`ieIr5256X0keeD zJu*7pE6(fYj@eKg0ki(<>;7(`fZrG+@&fcypYi(ShRXOGgsU;&$InJcd;x5KeG~bf zU_%A=V_s{ozqv+W ziWSC0FfLT6z7Rrf{^9I+VRSZtI^Zti0c5I5(2sz5^nlZhpyzJC9Hu09P9et3m@aoB z5P7b3E?9nhb}sGz&;xgSFI7*FIUA7V&EfRYipu*6(GE@yxoDWGnxP|UA=WZBzK>Jv zXBQW!&f)781-}dsDho@cS61QlV>oD{Mx%8SD)jz9WP8aSPXbn=oN{4BAoTzlt*HJ& zU&Ieh=;V55iVS)+&Hxk{5aciKktq5)dOI)$0HVwWb>lAUrvM$#2c@w(Z*n*A+pK&D zM+LXhK1u_-}vu4%#0fE$;J2yc4AWUXT;z+>Y}_uPYRX7wA4c6Bmp`S0(5LV zdtyQd;=&R(j=NLY1mVP(!AJ3?rLwHPCu)%?i!}z*i6V)ef>!oW>8`-CiY?lUXhM&^ zleNF?Xxls5wV$aN_6#NpIMmsaP6AcoVxqvHVFM+OsywbLIX@Z{*gbOnU`BChaY`3Y zYE3i=Ty$jiXH*t6(p44G8#h}tWl8PNkj z?*?1aoLrwNDX74Ih92Ua$wvl7${sPJ0%duu!Dc_BKu$sD-Z5sy9(Uz^`tTI@k7O;# z3>8Pet*uYmGuk{noisHJ#zDGGsgD{+dgIbw#^)da>QMOMufLOeW}k)Oe=Y*&7|f3=#do z&JH7ZTbcMmM6>U22BM{0U2-hzu;B_fn*(}yi9FW}LncVsD`pH3J=i?gIFM7w*;4`K z`M+9&Glipt-EwgJd_F{rCNOxBkai?kX)i38MN8Az<M-Ldc)U zBk>+a={nB3G-PoLdCCqA4#8!;1wsT^CApEZIvma}j<V?-I4 zK{vo_Bm2Z_|9~_49lgsu650D!EK6u^)awi=uI<+_1+M%p9^nLrIM){=H!{)(GHP^g zVLnmHiQ7UO%+5Hw(67NvOy-3(tm&G$%Uvm2!6}@C0cxkQ&{>taElFpUDuUu9@EgcI z`Dn-FwuZ(d2X{NL`N4>xL_;^z&ZuRihZv2=XN)FfcL3OHsW9?7BSbsEkQH1=^u`P3 zrnLYKazmx3&v@2Ogg{siI5`Kb!ipzaA~13Jml}%?Z=n1jl6Z~9mDp4`n9|zKPIiYZ zu|n&th|EEh1dy>O($a)mR2EU~l61glikr^&z9rzVg-Z;3$Xya@dzIEDWFBO#7hYB~ zKh{Aq!L0ohp!?}Isy(-9)gF#06s3dDBji!Wp8sPnTwPo>o`HlQt>nCecHmSCCefOV z3R~~qfLdKMbxt+;HbNqN<~jH*6r9M6l1^MW#wy%Mk?pZz1cCZ5X&4|S3AX<>HMA*ik3>8Wk@=fFsR|DN&>XG#z;t#Y9 zD(E!6iF&lEHHe{Q$wT@AQT{G;SgD1VB`Tvb)3Z z*~po2Ddc00lNTAV;ZjCsbfxTe)G2v?B-iitaFC~knKdgTw=TFNbOYrhnPVv+lG3bP zIr<^>aK!RNLHm<0|-AhB(ZQUZUUuo8O4+>v-Wx2pV=$-*}10Y@vEuJST-rK z!05V0qH)vk7cOF0ZqoC@MiKdB?OHun1q?i=f+S3KD8JmQkpbLniQ{$$ADo{EYOMDNx=_D!kN7#miP<=-Rn6=kd z1l6Mj^;y9VZsk1Q4{KU|W`d+)MQ9;GG#N53{DVn1QzvQ3w-FZNyEH>RJdznZUgnJf zoQ8DgnRZN6DbcU@W~B7)%Hm`+>7PL?WBft|w)=$hFJ+=DwoL8Mg?l0F*YdfddFc!@ zIR%;8fyCQI%b6iYtx9*z;k3|Xj?(IG?<1OR{{nY`WcyE9GKfFPwHu{6jc ztmdv>OFs>KiSn>7plPwd1g~EfJc0%7fQTWC#fI0$*J^=1V=`5iGr+N|B>T1+!!9Pj z^AhTNU;b}@XDlEbvC>~YDIs7}Nod#pScG1$r117{Ue@nE*_w!ho0x^7GvJx$If}m= zk4K2?8;A=3ftm97I5t45r0jNU7e75-z;-@6`(fJ??0xrLUUz|wv~p|)Iqx9)pwM5_ zDQ(`r|IP|N7&-_28gU5=5qv-Y{&!V4zyIF)Hdv|LD=B^vaWS5yvuqwwM}wZ-%^a!L z?d6-DxqE7tKmqO^;rdGxsyWsHEtC6GAuoh;E)6978_FDLatbiZ9w6Fr^MZ6y&pk?S zXZ%Bg*3E-V1~`_LW?qBJM`97^lqMEwgr<_Xui*fOp78IwEMX^>IhGQ()pNc}ED*IW zHhC#FHVHHUCG&-mJCQqCB)vd zLr9Df{D;fhAgh$}%|nkPTzM1WbPwsqH3>+9zb3LfGH@KqEV=mg&UR3dT_UT7;dp@N z>#ZbgTdf7`t+2N@97<^5KGzL$YXNZ62S(CywaGuwHRaNUuv654Zo61*pfbDQR*MR$ z{`@{G^~PIa7aEcdU?;F83EY5)_=b(ieg$m1$Hp47bGxLu|D4S|&t;`?o-0AI=*tU# zkb>GY$)`@fy{X4M3?#gsx$(y?f#INMOa7SO#M+IZmvzzK{8xNw=+f68iT(^ z-S&|f7T?IPqcZ9L60QQiFGj-F9qQF3d5^e69b_OqkjSefynE${OU^f4l9P(gcLq92 z7f9-9=xKNTI)`cJHKx=Ts4=2cy8z_RKBSHX(-ViWT#!(QFd{egNl5^ML2x$Dv#@z$j6 ziX787uKe|dxpkzqd+rCbCG0{17mlEYEcxYQI0Ebm^aR*=O_>u5T-Y0our4DP6Sr&G zcK6&Zml1r&%%Kw_g-P&-HCg79Ezjr$U^^dJEzB4W?>@4P1LXQ_q`WR9pGnyl;FoQ% z4uR%h3}Sw1$X%VyORStu?4D6}(xS#2V5co&?9&mOBK62h%+kl|9W#E~Sn3i=%&m=% z@n&;Qe%G??E;%o*e_Z_}&NeAbfxpSo!x)}RI&AXE#FIR#K#XHuEL ztfK(iF`kW3lR}H}d&NT=!@Ho-8msPI4X298T=s?<3=af=it%G$T2Sgu4{FLtdf&xR z=_x|6HO#vddDBPUjVzTH3_4w>BH?3=j^y2DLQy(y?;ZZbmP%3dn*_M04E%nT0K0ip zvUE*gxg7`y@@)+ZGFS<8*sw8?c+>Z|HH@|m0z059%$6Y{VMM(=+c33>$YgqXkcQ5U za14I*s$Yw{|szv8axZfm6UyVi^!3!n~8Nslq zNnW?;;5-w`QU{Hzw1=MUmnPX86_TS0fHTl4Zg}%zARyq(E~)^4JU;*~#!R(N6wHJ& zAR=W%y|VZebED4TOG+>1J;k#~gj~zf(}UdFy4?VZofY>(ZNxi(+g`O@u`4R&R08;PMQ!|tCs-i8^ecGSOM}i%+{cRqTI%*; z`I+~wzgW5&TXeHf2}YZ$V($?0|H7z+1KdPf1hF`Yl&3P7oj684Q6v@58&;sbBVh^v z-(gE-DT9zY0|Q6`@;}7Ak^+&i0cN8N;(zq!AWUbBBXBWVI~58=mpIG?@|v36=cmlpU5 z0|Ggsq}nOS-l2ad_5mdUvh!?C z-{?bR+jw*I_R7kweayGKQP=;f@H8(&Xr5b9vw=8vfEveM2^fpC8Pl{&70v4}W|ldT zo`f$>AZ1HzQ5s?^fjs*@( zg=L)gii|SHrsB&+!Mzo+vmB8Lk*v$)JyEsL)s5+dG&EG!MAQCnhvePzT) z4Hg7Ztw`ATGF^TUB1Sv5-hJmyykfKdYQJD>qIw-7#~fZ?PPw})iUU(28Of8XgRqkT zTj*AEky1oIl3iYmOE}cppAY-^fC-ZA{p@KS@m+xQYL}ZfE|8-_anh<=(xq~9nRENy zx13Zkt>AJ4_zx4?)+tFn>B}$OJiY`IAwmehV+@qHwmV&uojR4H3lOya&t~Z!rd66r zx`g3h!o8Au4#;HOZP^wua)SNe%(OeT-!dR4bpj>UnpX@pNpc0+Zzbh%5JV$2e-HNI zs}dCVBsAcO8|SA0H#A$MJU$Nx74lwS3^p;5DJj4}=VGO1!sZyLjM6p0!`7*6E+F&* zsMRjTAE-hlQ#u(e$cj|16 zQZ2f@lx10W!jCM`0LXfIDE;K&QX+Lf!EIVlzb@WFqB$ zagZP`cU|KdRF60+mw}Qrsg2W|2IMH}dj(yFSknhhPrmQPbvq`Sfl#_e!OUH^THzA; zREb0uXQAvA=J=`3jgR}qMMtfOWE|GHh!%FFRljp!Ng+zYw)iF^_Ncfe``Zg>gZz{RYSz~F51uR53 zW@Pg{arvUyZWW~qcSC~wIO(aAn8L-v31%~F>Pg|16yfc-@^!qij^zjoBNjuHP#X}K z?TuD7=v=Rh&06E?`8hB~82Lbe42Y1(VI$-qs}eUKmWK})0M(Q}u`$|_fW)=Ni?*6Z zb54`Ure(@?*0GE>T`>|%3O#wj+|x6T%4eXYQ8Dk0JV9C@bF#a)y#0qy;t~w51dnX_ z0&%1cw{eGpbz7?Wwhwh8Y{0kjsmIplS`OgF-^~3Asv(2>VQ$#qTC;M%9)W4!W z*Ecq^`}2|g&O;fpYaD2z{lvjlUh#R*WB(O_-{l_Elz5@rL^%H=om3G$*(5hS!NL#q z7p+d|X&pUB&R>b&tA*FmT%tb`9B6fK@{d^4R#do4_yPnp!gYzd0PE~#i$v_Kwy|@u zn8VuzQDJCd*?ZRCAiy@=k-7cKp=T_Ze^v7FH8G<<>T$x7Rtg)u_i_qnhscODSk@Oie zP$77AbXG6?l1gxi>BCIM)9Myr0HR%am1Q8O9otI+T&CLRXdt&x_=Lxe>scY*(1ATV8h46Iz=2wRzf$((Gy5g{*15%WYvG^=1DOC);=@;G z5|%W8zQi6&!7SYeS~m-tOGK=qHNS!~qz*i7?1`SF2LlMn2{yj&W{$>4a_j%Os#@Z3 z$jyD!+@NcB1j$JrS+9PK6`D;_K*1EAPQh;N0NX+5@dN23LzC&W0~4C#K|Gt@0-!#h zE5poWY;TXK1Y}}v)MJ_R)o40*w%I zF^kq48iQs_ING&hEEYm$7u2F!$b>^cjL2`eY!i(g4?qs_sAHAxDY-{$Bbf@dIcDs1 z86{~e!MUw@d_L`TWB`u2?=ArN7+#9LxMQ5k1rX8~<9%OZ*`ikh3 zS}%TGtie$xzZW!@h#1Jpz6QAph}k7k()gM&5|$Psl#WkTTB&*i(!xSjG$N@fofwHV z0B%NuLxlN5$%vFJrb#K(U*>@e3i1Xov=UK>8Z($|q0XF8tKrSFKZ)2sp|2Cg+hXP^ZeWksW?3lYcM<`MZpk+5P*AG^MK!G80ENq#5uP~D*O!eMGm1v zJOex+7R!=5h8aR*6m++s06H22nIgNf!QLLyYoL8zRj8kBj~#81sSdvxsk|P~RUzk% zKt$M9p;$C~!=k9F6lQ?+RV7eo7ag@iW76jmRs*7RIm1mg28_s$BH6}6?v6IAYk*`n zA2hTA_1q+^Q4@861;c3p2YrY!9FdEJyf?o|ZlMEyWf^AQcF1yOKfb^;^kTah&N z(b^YEZp$Hm%9vVe;dt|<09MJ>h9hB6-f=x@lreolt$SzW(zCN4i`gv2O4K@|f~mrX z+G`PCk>yBNpeRAwU9{RP-AoqlF9QD{VR*5CCFI;u7p)Yyxe633n#c9 zCtS!E7kzJPQl$8xVGPbDgL^vBe|ETk%$5&*frDt06%D7)XNmDJbL*sUFt|Nh44|9@ zc{|D94C!)Vc;>fm%BO+DC;+PIU63IGW@=gcFOnA$kVV9)!nGXTinK98c%^F)dC zrj>4(tC|!QW80`na@d(y9z6w7+FbQIz5pJ3DdUYP z>;qV4ALbbwQ!r(qZtbw9*oxDH%I zLDDp7%{~-#Bv>SYiy;}uO-eoBs%3r}5O!i3o4!@XIobj%kw}2eOtBccgK)oY*lrPJd*Dj~jJ=(BGq^9*+>_>Du5G z+0oF|*}lOfTBfLG+V7|r{6vvnF8K-j)-1h-?B&MRe+}M=VZQ^Y2Z0{G2!r>4u9D7= z6MygjJWr$0NI4i+#g=l3pdr!-x*W~8+6;Rt*WZD!t%6xtZ$>?EFBM7bbbKDJl*FE# z+4VpVG*R%zQ*psph}&wV4n)$G34MV72vKoo9H8WOX}yw=wBAH=xaGnUtJY?nPG6l! zz&ah)tE%h`%{LUfef3?Q*>%&iAd2~l%Ei7@B$>UyN#IgB07(>1Cjh1D)eeW>G;{#6 zD&7nVR$nuKt45Tnonn|D^!QBSxFLbxy)QMxoy?ioRFBYiU=Pj`YKy%br)ks>BoZn> zzF|V5KfJ3Qi_3T0)V0KwiQU`ft z6$@I$ruAreC~ifxG&RvMQZNNTCYxAckyxqEbk>E3oK~8UjY~u~Ffa94yw*gCD%x_G zr0YimazET&yDUeA+XD~%1zxBjN(MxIrHt|dxaqPS5(BGK`G_^p{^Uthi5!E(@R)go z7E?drrRDz0gj@rK;|UAIJEo!wJ{G0GSvd1KZW}odUWbqM@Onf53PO=3ig zL}R4)9G>lHF82`Dwk$TxFpp!=qYO;hT;s7EZDd7zPD!x+sgtG}IVusCMJ|%N{IYY_ zphC=(MZbuz2E$VhBR78eK(8T!`Ppo*yg-M(1cA(bu0f zEJquOxJ?NC8Lm9W8y9cZk`9}CQcv2F*6Vo_io?ji0?sAiL0o(-=4JH%$w#n>7uH9n zdYAdWL`<~ce+77#h2Bj=;NRcT*A18bE0S%ZAXlu4m?8iS1)T50g?_`VO+-4d`RR2a zv)Z8Vgoz&1zhX#$eRmNKc8uf6I9apxyp;AHsSgHf=zf{EFreQmh$YegN?8Z>V)5~^ za#IlvYdiN&4D`S9zUWLCx%b9oApS!GCgd;&GZTqR7i{bY@fL&2sXU#CSI+q2qP}y08GxPPq4V;lVs1t6T^-S{hk;BD=J@UQ>W~fl2zI zCP8Zk1cW_G6zw=X6o_tGb*(0 zJ4Ve#K#Z)hF5q>bck__ZPe3_Leq#|0_&d>^V0&K_zD*n9b-buhlj9I`_ODQEg;QMD z#fl&i@S>6i=;DGWEW)d;l}{^nZs{|3^6duvt%1u%1oCJ_^dJa`H0zO($3%rt`EwC% zHU#FA2|O+*GLL|@BcWmiMh=H%n>7bK2|kjxRnOP#7Theh1Qog;9CDN(^LsA?@@ zs*n{9zCM;K>^kkt6YJPQpeoqh86T1axSsCbWD%_gKbDN}^>S`I?H3>^Q@k^1LzA%s z2SwzF1PtA8283R>UDB0|q`7ieGOiN>A|1_w$mm(TKTh{F5(QEk^;wkYCL?7RX2+`81r9Ek-W8*qX)$da zk`YXvGTtJz5Zyg=V_83VW1LX*h{SJFZ|17G!N?p1*##27hLeSgPEXZ+#d_R zT}b#r;Y7lDw^J2KNuoq1{ASkF=meS4NB~UO{>_)v?I7|bBrrEmnE-~4q&4Ebzu3#7 zB?%EP&bgH2U!zV6B19Yd%v3$z*~W(w9r8*r=Lv3N4~#CZ*SdJM=qy6%VUa?sc_hqB zs*=f;17*4r;Yy>ALLOI^b`X#T%APf!76FGEktRg-Yh+7Qolx_TfaFK*c2&p@yscBqB|lD$9v)zu-*eq(kzg$PjyzMikib5kqM^ zDH>V?FegeunvNwUybMVN(2h?!zU*GIP6L?iIuJ!c2=`4&UpF@Fb?khH!q&89+2_P7 zxqh9ST|74lMT4^MIUB_Dr!Z$Vxd`e4_(bdiDO0#Al8i)?lsPm;C`u!V=lo1(+iY}W z!s`TA(nY|J@9?=9p4^KAIn2;q!ga4@L&B7jl1dKY6bho#gAv?5U3vb%AUgkS(3rGA z%Hg2tHegKav)HoUeco%86rv}9{cRglZl6w`WFgKcunUA=69ZsO!ukETmfM`L%^|B7 zbS;lF0Clz0k5Y7sI9QNU&_n*z^VRc|$|V+~4~eXV4iVi$>VagQdOS_`CXOhu3&u>a z?H~i@f|F5~IpZ%NNX420Lw|j_Jz}=Em#ovMflR)boq&$MZj5MRXTKma-aJTHP;7Z% z18(y$wY(fPnzg;v(%K_(OQ;u6s4Vu8;(|e?xR1pG)(PLDZYp%bV-7j|p!?9T0F>MB zqZBJa90*(Wodi5n(RGQ7ae+HD=c&?LDyV$AdpgTv#O8YcQ>2wc#$O$?rd` z!lnSOq>!(KBvWqGRjJhU!m^4k77zp@H7=rJe)C9tChLBG z?MPk?#b5!3Uz0uSLZjZa+or1lX-h zY6NrP#6HpR=gH=gL$`N%B#ci&E5^V$LL0JWCXBs=+rs39iuB6Em`1_xyj=qlNn4AD zy~Gb9YAL2L#Z^J(3%rT})Oos?4P-x|4!EnpJ#$ie=qAhtFSKU~`iB|Eq1u}BbgV-H za*~iOH#{%IQ(9II%W-AQe|8q^|7JV@16P?!B@S`ZQQv5NGvw`2nJV#)UfWKHz>fq#Rm7X*D{58GZMt z;+ZO3K@K7=bYIbs5SnL5v|M!>c1d$k7q@{>5k(~eorf?d2nfpkngm6XjL<2WZu}w< zKXH00~?b`XX6G!DTtLV8TIC{>cX}?C=JJ z-XDjTqLW2*c%9j#V{v$BoDW2iZRx&=^OEVZb#}E%{p#DA23W7rR(`g}J8-Qr;@gcE zjQ8OAWVAg{uoDUDXN(DzbqPfj?r~mU2^c;LSNzC?M;)y)Djm$->H)YWtXj896R%wXMG-{){@t@m){e<(}u9 zGd#|u)VHDCAkUOsvT;;=oN?3V^EjK;FuzhK!im@M`b6y5pdbtAhr1?(45{559`l3P zoUQ~-v_6UoCK6AaehDo{5=zvg(svc;Kk(&YHf1nwx9E5h;-jM@s0B`J=!oKov6%J? zBrrs6<_t2tCxMBU0d)ApD1Me6!?du`*J%-@YzKgEvvb-!`jJxHJ)%ux2}O_1Lx9VK z7(FE}0?{NrYO17#F&wbISPY-(UlGu&5%xvArTb&2`40^mU_S zSm1b?S^S$_(;g!Y)hEm!yrE4g#0FeNwt_kff?SI!*rsx09OMyu$QeIEHo`{`8!i%n zFCrIZ0Xw}7d-xMNvsxuZi%yd;Z`+FKbs6{1O60O|A*|r_H7os{%*(unn4k{N!(r-U zS)jKmB7)bkj)+L(EKf%v(gGRXbtEK_a2N#@7IC4?kn5HPwFndTtiP?tdNMXGY|k}l zw=k+1QPf=$&xXp+DZ?u~L=WJz3M0|~^>&aQpJ%e_4cSrzmA^OaCbad!v*CBOLKkE3NC~$fgEoC(fmCWbS4jhcFO|bVw{-{KQS!TbO1RSYV?6#z4qxJ=+ZIn1J zvHYk@F@@T-yd}bEJwnA+uv5p-tBbRu?a>1~`WQgv2Y7&RGY1IYmw)>2f*@LyH3dT$*CGhL#=3 z)oaFU(-K7=zBCLo^L2%6n>-GM4kE7+P{JXjIt0qx!hs(AK2GCgG{Pk0PDL46@>*ja z%nQqz1nB74BC63$4~V>KgQLZL`z_MQRQpiyKQ6+nb3jH2@J}5f`rHUJk%B1V0O+X! zb0pOs4Y#RJf<}W_*yE!C^XU@QkBz)zw zgc|t`TC}OEp0#aP1B(Fit5>`PiCY#j8Ccyt8y?lbu z5WDYWU5Fr8$xj2#dlAg!KdUNd7Slt=wlK{7b3%7X{`%*ENtkC! zYT&RPqh|x-Pz~R6O(hy^JTTO}PTQ#tsiB}3bv=Qj5T&!)45-6!m(F3iwXxG4ek>_< zgk1%!iHL=5c{K9P73CMsje+=Gd*FTj+BZ@}_-q@e@@p1S?eb&dq64N$)57ZAl&{`@ zVEc$3NI+ z-dt0tpkG!$Y4;S-m4NTr5aXcE#~2Y9VPc9%4q3GzVzY4t^eP74Id6sbz3uEDydy;xx?+Q|vL5vg`s#dxqZUH~z^P~? z8CFzO$Kecg%eMxcSY&cx8sEnlze>k1m>)F$0Iq+cr-ZC@XioP7~Q%1#_s$ zUDF6hO`a|XkS!0cn+cuat_Y%GL}r}2`QaA_kzHH?kA?NqV0n_i(W6^rs#?ye6tdhW z*;Ujrj&~xEz;E=_DiwoqdAU$Win1ldy z-TnzyUZb&eJJnc+K5)fGCXn@3iUaZ`L>WFYXYHTuwvXO09)u1kJ~O93t|!b?!mmz@kZ zK%I(di+0LeUECkxn~q8L*gM&*JX3%SoPHFbYrODkzzSWFT^udFV_`=P6XUDH{D$IcwVGaP-sQ#Jp~bWcMXOrkWy#Z^Lix$E0h*sCb++TwDOM ze?zOg4XvH%$sB=sbVe#RHA@5~z2H-<$NM2zFj|rVs4fb!GSz)TJ(i8K2L0)Z2Ff8*E8a`*)y__?j@D%WaB&&5L)kf?;hLkn-4FYAp2c- z_O{RIN;|#rtNS0=p19hIeT+Gl89F>D2q^;lNP>Lju;ulGC;FEU$U~5hz>ybY;&A#z z#81r(@bZk-^LfY{%x8dxT(pBfgjKNthv6go@VEl7W%O|Y|r1?VR&R3B#==u7P{9${n2M6OM5ywVw_BS&~`*LLi zEX6(tXlJ?sV^uo*++bmokcmIQjfDvV<7mW&+Nk}0KnOzg2xUPS;eG?4_sE7!bGAlNpR>ojFO~7fB~gBQL>UtVCOkp&h&%s>o7)m z<^T&&PC8Q$y=khr=0iN*>EaB;)Q(;3%gw3m4*;@!;2gj}*ztnoM*`9Eu38{HP-=4w zHVMWg@-_Ze93$rWlSQ{-vM+UDs+h9*xs*F8KCsY z2C>sUVdIdW11&%?F17`NS;oYj(t3VJEQbPF-PIV}+KlOCl{YGd5p%$0T-4&d=zZY|zhEi<^sz#yx6o9kk18y3BN;!#T;CU`JTDAgyToAK(oO@S2ryAy`PA)ZLuH&BA8f z0*!nW_9wo-3mqb1~3!3oiZ1QI>;Ua-I-WZejMMOO8ROxFfQMft2Z(94eblo!;~>S@4o~gpZ|~ zTrhV@Ty)buY5k5{>uu@|bd(4ryn}z<9jn$5b2LNMZ$xaPXH|4xaLmV z2Cxj@i5jHCKqrRm&oGwo!w*)Rem`$DU;(fCMKsiQAoJZD{bQq zZq_tJw_(~b6*2VUHRBQJQ(;kKhs#ET{&I=7uUH2E!_F+|z2^&5SPfKT=(7Z>MY>AM zyFV<14YgO1ieM;&R- zLLIGln~U%+#Q_%+Qm5Wi0OopE*!K&mAR>})I1ejaA&(94=gv4mQ_!rd3}>=AJj+_` zqmOaZxRXP8g8>81Bi@0)dBWlpEBr;yC44{k9#rzX2(*MEzgCkmF-PbvhDum3XvDal32QbKXb2Tst%EP2|g>Y))dAP?j0n$!ia z$CLcBt8DrmMBJZ(f(sMK{T3S@sa$cfVNK7C^ zVn>0s9v%)hUH+v7HM)_VL)iPqkX5|Z7+X;o0)s~`(3^-@Zh*WUtDTS}h;_4^A{WW3 zDL|oxy>bI);GsS+oB^?K^U1C&w4th_jgf^pKY)9G#r@xzi544nv(Cd00HV`67D+A6 zNUrM%%SBQ%(JI;AMG316)4J9rZOF)<0}+iMeXOFBQpIlwkPM{LK`m=jWm4EFaLp-c z0ne`Su*TJqOehiG5G5moUc%~74?v(gv2lu{zWNwq1`v{qB>_ngnU|c7AZ0cucjB>O zOl$?6PBL$7((7uzp(Ban1oz~EZ4~lBX2S#_DF}=fE!58k#x2F3&4nxxC58yj#PW)S zSB@Etvqd~|!DQwC%59u)D2a@-9v9w1#pf=qRNjyleJ+z&=T<5(hfKn+B0xe-k?9=tQJGx_MNebVP<-0CBzyuD5?YwbME6pa zit?uEIyp~Atckq(noP;8w?$$_Rc>^KEOivMaB{IE+TKU_e+am@Sc(B)w%rMHcnaTe zx(NCNxV>NQG~hNF*uejJ#Rw_%VBLpe zT^7>hSb8X^?dlb%8D2;@fiPn=QNgHfmEjS)PzziZxW^6kRuT9S&wAnWEJzcj%`oHe zgtQ&gAhzcXBdS8bN}3zY>ie*O_=zvMkZkmT87v%YcS3m@j7dSf*tP%UxLne*DW6Sj z@%E+iv2&zXkpXdOkU#!H1t}s7hZuhe8-j(6vK+NvV`*knHN}e?69D54J?~8*b+TGdR?VMJ8|DSW?Rk`ekpF=pVkUR*Zz?t(dF+*=DJ zdu2HnLXJ!2%FAZ%i(&s9^Vm6Iy@(XBs9qWve<6Yt0lGuXzq~)AteRJUG9FFB}MWtHAf%=q0+kRyb%f?3C15=H+^M6p5VIMfM@)I%YhSK-Fo1o1}& zut=Nm0Adgjc5X9G1)N##jLRMya5GvVVqAcy3#84C)=4bpO~&h!iF-alz6uL7d@SF8 z&_TW)F(#(dGLzH|9^Vy!s}y!fLEy>Bq>~$=u%T|zAVa@$A@0hcbNv}Hz#K_xbb^LW z#*FoiiQgrj1I&yy?i>=oS8gI(64_s5sd*(Dq`A$O$iWmHV9G)#;g`Vci+G{aMP;j_X^BD)fQEvosA0Tgp{W;GL_($V3wEk(~17MZpTqy}!#{jI2O>NI)<^Q@Z+z7r2PnJNZ z>=bcA-GXKE+1hT8V0eo$Q%iKa8bB*QhFE)NhE$nDezGGV{jQ?Hcf~+rb;ga0Si&+e znbkGutOGnm+W0^(q4Cor(a>L9^r?y(Wx*D~e(Pr47qR!vIZtL{kHbE5puV%n>^jUu z2r!J|g$OlH9pDd66CPNzmst_@v%@Oq1eKRs0X3pmVZUZ5HYhEWGoeNXQY|^c{x>H-#m}{a zb(bHAgGV(v;Lg~3kmrJ(5nWc+=VqkdSrcq=4mA-7qo``HQ4m!1kwX|FHCbh)Q)^koqLWK}b@&@zX3C4}2*wW5WXr^$ z42obI`~WUET&W3hb80mrkcg0oSMLdLIm7`~!eMY>Z6cJS1KLRaU`3&~c1X_EpR(5n z!X`P}vn>BZiuHIM*_1a^OISk%D?e6)u0V&GLp~1<(gDi_9WAvG zg`sE=5ee!QgD6w!0A~nMng~GJLhJnSmPC|Xo(|`#n1aH>hP3Z)KmxY0` z7@W45DW7r1vB}^oKkqKKrWmG@xS7HGmkPjp+vb@6;=kto-_JA*M!^7zpMCtb82D24 zh6A43CzPC3RE?nllc#k`)T?kA0%2z8!*tOzi2;57s^|<7Fr1uDY*paY7eGc>OpHJ? z1k4e79u1tU4x*OY&j}j)gB{1-_hv_;JT)T2>43-g7cszm?5?@?Cr|N%IK#|{2pz%H z8ui2gO({p?GNcj_6+flb3~E~ny%W))Qu9S^L>22`E^Lk?MWLX|P@y`qP%FQmc-%=}bb}vidlR(5wc@mc+8K@b_=qE1>}^L;ZH4b-`Ov@?G%_A2 z`o~8J=ax=1NzlFoGQzUpyJWcnUW`rUe&COV~T~^@)Vq<`vvDwQ57@;5!oNj1v_W3KeCSa z2;XOjN|A{K=O2v@Ohi{Mm#CAn2tx8Qm|B{fvtLVvaSj0(TbIckewqltC7m0swc@nl zgG}Cl5Pb&O_kBGHB_a?@E;k$vsp0PKsVQLV!k}Ew1Rwytuod?#z`q?Pv&YKwqLC0U zA7Y5lfP{31GsCOIa%?yHwuU*7bA?N~%Gis-%mLKn!{E`LfrVN?3Cy=tE=>^ugcH*V z2k6i?*7rwVZ)r3C1{Q$)ra=WwE;Pz%6|wNvJXWL5F+tqUgS1jZAJpz9p6FXzPDO4IqXg&L2Lbf$;HQ#B^zEUm1HICFGKwbFGeaAXy&a^nDe=GhRp z+hJ*97I%tgLtvI7NbJwu00-b*3R`i-Ze8qNc#x*cK@)<%#g|92hSgN9o;t#SPBt~G@ah)wrDH@99A<`cDh3*8KJUM1S?@ZP<$^4 znnSkOo5=f+7Zfgiib?5vZ^^(NTw+zWLu;!)R4Ty5QGj;2@UI~YG#e~_bV9@_?mdBs zf2I)d-z?Z%!)*nj96K`oAd{E4l!S_;>CQlbR-HtAiO4}FP7UIG=9t-1hY?V0gbyx{ zsw7rMNs}5##;Zik!C?1{5m^!s?ZOFg@dN~bYU&hWT9_fY?Hq3rwC0MThaQ7`e`)bV z2jfnKAPvShiwg&D7xOZ^xKY`0`7je?ZHDd$s7xAkMVnuM^A0h3+sSHryF5GuG5tv#eGu5Ii$n&p$Ia93S*sF;zYTn z0i9y2uhXOjQ~1`;mxvZ%;?y9{7l~Y+CWj{^Kt}1=5c7*f%;8`-Lojn>jU}X`VS)my zpyVo5fKn()DTCb%Pqx*A7>KgkJXm~`SeTGVYQA5E4jtS*Z9#Mkd7XQcs|Q70w0&}+ zp|-`d8gA!Su^dgUHX=+nlGEuk#*pGRDcT&;@Wo^V8NW}3@t9ZIM0utGiK30yX@>8t zn&kL;Z~&C>5~l`nE`axwU_4E-A0$XdNsr?SA?GjfA`$bmL3B`gC(5w+@V9hDn_i>J zq`ng*ru6yHMpyqCc zm;!@41;dP9gm7(~oW&^4O4tSBluXPh!m}45btuULdHm!`Xg65X)bYs0X(0g4_aX)0 z93q7ok|fOwmgSQ$im>H^fD`&(j~ zy*bgjP>A&8=Iqy!ze{E6Tx7r+3R5KjKJpxlXFX@8LGHEp1SgSo8JG!YRlo%nli_+B z@Mz)eK0?cOq(oAZQf!Nm3J?XA4WLH-8ZL%W;>vk7Q`<87-nLMVED!-G%T-F18F#=Q zCzXScb7=$53IHKr_z=B^|@91rW}B`X;n&q$>qloN^|SanLox=oCh`fb<*4LE4c;l2s${cA_CGWkC#ALc^*0dh3J{N-*ld+qh%tyf>AX`l|(wdtwe-Fw3D4- z`iCNXVsAl5_~+DDga{C=VDAj2t(dX{(qZscf90%VOj~61WvIn-QC<=-v(_gNx7s)E zrOg~uHk@41S;JreGs`oEW^Pnj6;anM>6KBC77kU4F=;$AAeoASHW|(FaRFSWw6?54 zMCfp*lkTRd(}-qk_GClcWo?JFsp=ypT;)}ytcgSmNiQN{872}fPOHonqTDD+g5k8g z8SS_#ie1(~c3IFGNkVcxDn32BxIQjOE)eB$I9eXIb8!w*Ysvb^MY=M`OssMx5lLn* zNbRVn%bTM|8ex0udQux9JanQJO%`=o!xcEAm4qn4LLQj{6)U_HVhH4FG=$gXjj{on zi$W180BitT4uPeaoU_~JC!_SmvbMKEyY zrR!C~n(GdO>^B=%{)2-LM61p!uKsbj3?pf!gv5{cV~SFW=p%($K9VWcMs(x1>1H`iJA#g(V$fpryX^WDP10Nt?TehlXL1S-IeNq1 z<|~BkWq^+=WY#0t$lh}R?>c~+9S~wFzI5?4W@?r$5Ny1Jn4;~(Ixb~>@Q-?eJy6|_=gSkP3ZPLoeuZLhJ ztV?;PGV>5UmqOeaAUgvZ)*nCqHi@b+H2#x>{H|?k%&*ygSNZ9eQx#&gR(|m%0Ceak zS~+R>AKa7cxSlLW8$R3yDX=o9&!o8^eu4Q#PWPIee55*o+&y7$^GVZ`feoRsgCS(r z!#TI968h&mg|9<17&nwa{*5egeIobdsM+|B{Dlq{V%p`X52g=H8bnav!x5;?;alwf5Q-c8so5Y2Uoixk+ z*rOfMXuu5l0$~w42jf;Z2^1VZ0xo{aVQ1vH47jYlU`}EpN->$xS31MOU<1+EOEA=7!+2wYI5y&joG276 zhiAOm0W$kF7(@fG0wzk-!ICrg%3_a~@H2(Bx2XnF`HIG(#eHt{Yu12d=S^$2eN2^%k~Aq+5j&TItO^DV zg5E(Rt8VwuLmjLP1L|r-bt?!|<}DrE`HLf}WFa+S9bzWsh3uTo8TRY}DJo#sVrQB_ zvupA@JqNmUs?S~kD;XfpO0c?DGAKHkQ6M0q#zqxikD@r{yAB|nA9bknfeyZpY=CZL zp}GMj0c7^0Hp{N`zZiglwruh)gppSG75tC;8fbr-(rxX!5chtBy z*WWl%mgQofC`iyZV;84+@j^z3oKq#={h})NDJu(Wsksp?OM>#&%mk2R zOSD7isG?|zjt`3ZG%{fTTxJdg)Ds7$NSEjTFedG?0S zONpM+XUmcp{&^L_uwv7W%o^O%i8-mP7MAos~8L zoT#%R+6I?L9r~!hNl@MG=qf-R2w)-_NQsjsWidU;nos-% z$w`8+U&$#WBB}t!9#-0(J!J@qnc#kk%o!L`nBdfapE`5u&{>W;{YN%W4*8JMml@1K zs5M6v-q1LaP{E#YrAs9r`N;dTz*!>1O^cTKc8-OQjW}dhNzLo<}ib9A6BWTPB0`kbvK^C-&49}v#=$(gUOFG+` zSaKbHk=HK-*?*{qb(vX!iDAKM=;klINQDB7Fy@BkXi3hQ0Q8`egG<*WPMVYz$SHI@ z{^F9%?>(D)NNIasBr#KrSDY9o=8MNs=du}LYRHx9*kv%)@KBh0mc)-R(iFpi0|-J& zTb2cBD0d0cmZK2@lS_nZ4uq`Q!y10+ekpg1bMc$>s1t)56SeS`T5fw8$z_2h9%D3I z5V)D;YfGJG6(uQzXpAu77oXrBWZKg=Ji4zrjV?T^X!m{z@q+O6zaAXi~M-fPUC z91zCbupBMT*@hKm5e&ar6^WB3WfgK-JeLsjFAm}No~=Ecv^_79$VBTE=lXXGy>})DsLp05W<6r)*eT9Mg7^pnCNTV|Kw(0Z%GS4yc#MpY%;ly>(2U4hTQov( zK7#v~m0|>Gqf8hd%$(sWry1nl!BHzo=xxZ5^G*c*g>;2m04KnwiSe5dFJS90X`XPmLv9$kWEOul#$4}W=e_iRb0LKZNhbtOXP1f~%Ad@!VWq@JW*ifp z!47c-kOa;m+Gs&t+@glJLG5baTzO}}}VVE zzj5QP!U)SnZX}!-aKOen5X97uc5F_O&p&p4{{4^t-t;AfTmSYAwJ4AO8BR^Cx-w4`bmM0@if zU@y(TyYPEliG!Tf@C=-~V~&9wZuFn&;p&Q|&g>Jls`qIbN!7wibO_MO-}wbjw0~Ee z_k#~13d%ve4gPJKGj=be*L24M^!S z7?feD_;4FMAtrQA#N$?^GEtyYzt&~QLA>PzFdUmomAGBYy?@}4 zH;pDTrlY^M8ng?*N+P+|YN8wYH&nVq&o->0vOPspR=%BRO40$bu)#t}Z&i2Qms}Ag zK_r;mine%xq`e&jV~G{X=bDU=>AWy5Wwg#Z%-tsDWsuWCh+@jpcryDdM1~GDM-3r4 z_b*@8S?9O9J^8(ofaogF@5;^;8yw}DSrcNsI36GCz-Q)-?%V|~egJU*9y=ig0YnI= ztqq*3IuXAM6fmdKZp8jcK*;9bkUEgT3(2DfFK%Dsv6(QpKROHTQvnc(-}&YH`s1#V z(bp4>!^Kn=P}tIZw@cx)1V5&B!t_=HZ!gJH2O;Z8Gq5e46hd6!!-_}PKNHv|7e+CI z(Bcrza{&?=4l#}P_-A=ZIntsayGRtvC9{2EeErcLzF^MzUe~ghU;pr>6(2&}T-v;* zc7~Qn5}Gc*{`k?fFy^;Zxoqw4bVnH`{n(B%4f^wisfCDkEDKQ-ofw4{JKckmgA!fdQz`NSh0xU!i`ec zN8$L@cH85?oHiwS(y=S4mt|~HgJVx-ogC_GUnSyS9Cs;(d54aWVm#_V{QWJwbyI$N81O zJ^nz#HFkHLPle-5VVQ6dIs5J=S$ujTJf6dB8DiU5mRgTAW;=!omQ^TOjnhNISglMg z7%Z&cGDmdz)$xFGX-B%rT-tdU7;dE63Am*sbDakT8EYnZynbMx42gpT=k*d@3H|WG zakzsSL^5%hhWeyO5V(AEsbpQ$ zXQ1^X4jX((2nIkAay#f+(a5^S198k-mf1VpUMg-IM4gsA!h&d*e%Pgr5!OziQzf_u zDfdW7xaL@3oeznQW5w{v(9w4n$XIJBIzPrZ300Vr0r|Z}E`DctZ^alNiRI$tJ4XVGj(|R&`BP(uoVJx*P1#CeE`qc**q0 zK^KWApuAX9fie(g014a9#8G2VTGKm4oGo9l{D7Hyyo2s?u8TF?Z~Ur&_a{z>vYW`R#-W#=$bf5;63 zbg~Sh))RbQiarVTirsKm@3)l!8O(L3E7d>JU3(IWLf$yDm<){}Sa<`k2BrC6w1 z!~t9t@tpKQv~_7tj9p@XB_Ag5;~y>kQ@Rd`rgRd>99-{ zV40haHuKOCjdPS0F{xuNDn5pLN>KT%m*~MnR8*DJ#YMU!inwY{2pu7p8d}eZDOnj; z{oi1Y9XxU3fZM(B3VesOpBSkT5)mQNPjwC^B@7j{SAGht3d)p_6H{CqJ^R8{*6`*_o|MhrY8JiZ3gX z<_3a6`g}0MQYiunBNuIXoid{SJ7ioj5W$_$;vjY%>#Pd3;>ZV|1h=<1 zOdZp&bY%X>+-pqWp$O{=I02!tCoinjH@_62u~PIv9>>3`G6ml*TNn+r)3(?;X-iVj ztZ*O_jk>>j`#&z5aQG(M{jT`zD-ODK9AuC4aT90js^!{%~afj zw%MrcLWUtV3cC+hLbAV;7;(XHn>9vs%6<4U zD}di^dC2*yqo3R`wU`jnvyoX|K1R6N-g}O(6*Imtj8~gD^|=GUDP{zly_pePmp0Qo zRSY-d(kGB^5HTB;Tw@nI?{^YU^KzkUbIjQs($yum;mxc^Ihp&-6mvQ%%nOfWT3ARf zm|9E-X_@oejv8()Ud8kgG47hqMd$H=hz$`5E`Ee>B@%A#CUhxTU+1t1@dp92v@bjx zNf00s>AlmIrxQ7uF!&yr?onipFBCic6tu?-+DCD)w95$y!}6iZLF8BrGqxvmPHOVj z8p8t%%lLb9=7MoBu|a|i|5%Z%!06UyiV@|C5IV)dE^%zAn@5DG2!R|-$uWE$L8fU} z5J4krD{8YTD;z_UAQBezm~+9%Sv!~bV21o41`Fal_n70&z`0-mZ*g(Z#ccZyc)4QK zh48?`fM&?I9t(M6zGM<#sTKygP`I(rI3A$YWQiwROWq4Op$4!NNl9J^w*jTrvQR-k z&3)XT_!xJOM7|brkXp@S>+#!Hf?(TW14Y7&=hBN38lsdGU5sOH>S0<~O)(3#KcT+< zZV@7LoK9D$bO}jCZxCOI{HmGkP0KbZOYm`aoS4*Qim?xBfy97VuE7b9TxX*&w`DYc zAnH}0!29m;-MxhsfRiuP!8;={#VZP57P9sDbv4v1=q=f~Qy)ifcAmuf?03R}E*ljk z#)6%oU@Ey;EcG)qB$4R*f?=DTxtj z3THE%<#cF>!rAu11o?h3p=-DS2eeBNhn+{L5XD45Bm->4A+%PD04rtFiW+EK?2Nt> zF;%RJdwhk+`&>Obf`3gxu~e)~Hn?x|nIJ1`sa6J96@ZE8U~tq|F_ao^97V$T3|2!r z+~Ix|mO-(~HTraVe7JJvg?g@g7eXa!_+43D+>ee&=Mj}lO)1}qiV#|sAf?f#vuWz6 zyg(aVZ!?7o&ErDuB{E!;avk*+_upaCkCufyS8Iv7=+SC2kY&48#L632acK*T#;Q<3 zKQkp4z%ks)K~S=KZYq>F`*;9*NUxpe188$XGY8|S}xJ<;CGdZFLMPh{bXPo-R0vn3bmR8mP{@(q`;DNT7AFye7XE(HCjOyu}dKqWzQ%|1!W1{3~L zsp*IGm<3Q8?wSm=za3m8akq)4z+rds{lh>IF0(wqjid4MAs(%pkL{q-;3^yPBN8T_ zn{=FhkWF1@2O=WBx69<79?y$4FVr3aSq6;88W~~H%y8+ffM=3Nhb9PbGE300T_#qW zYl0_YBXm7b`5*t;k?)*=4w`Cbl?825UKmErIE{ercNImNW$}q|{|6AFItIR!F>j9a zUp?he9XnCf8Ie1GN#^HfkK*+7xaR^?!fBrcET7%+^pFvqP=+h;Sphbi=IRclSV<}r zeO4mftg_RSbQv!T0Y{3M3#0l&bD!seQBPLRG1P$)1&0G*;qhNVjN_1xeQ zHW;Y^R!FOu)I4;9P+C)mU2Y^e=BQ+9_!K>xD zy1^=)U9jqN5Iku3PsZt8O{Ra6k@L_eJsXN^aM7 z){zlnLcA!VLzcq8=4v3UfR7+#?Ly`25Z`e4GyYsYp}Q&P8>Dk%NG49)E*)E-r&c?P z-NJZb5M@+gw0bTkk3-)riK<%kn`zJB4r<5|QdH-{6#z56>-@rSqD$oSPhqUomlxLv zkVqsMNOv2d`goAGA#FQTQC~ITQ0ZVv9Fg_s*l=tA@5K#eDQS=7L+`hJX*_bBB_NO- zbI^BMIg{R-x4(IZr&v46#-N(=0$5!2CR~P8Y&JAvDEnCVfI_rdh`}hFbRejo87_Gc z6$+*_y$_ZG4l&>p56fhU?f^64|x`+v21?X77ZDq6OT%dM; z)iLnKD!z>g$HtVfVo9bD3Lq8D$gQ>8$y6#gpB~C9b3~Hp*D4LUax_5|gS$qPV1#>x8h;g{1QES^f_fm)kU&L{ z=_m4o8P#bLsbv|=p~s)bSGb=YQTkm3T`|p>%pMN>2)mRErQ|QHji?FGItT`?SeFA) zijgCvsLn+y07T$^VZN#y8b3s8^5R+q+{6Nolw+fpZVY2EF5NqIQ{TY~OPr84_YiEe zujp_j&kpT2L*u$6jkc2kR|ZI%b}2FHYq%^TXJBM5o;bkm_?e)NL>!eGaD$+j30PB& z(m;|6C|L`IrVc?)6tSFYV(7&}HxX5A77>{i7o>8Y06f~bvFGn89c{Vm6N_eo?H?D> zrf{KBkMC0_ad;}pbR}^FnWG?#uz#^ft`LnEIen-k`?ev%5)?5wMU$t5cLW{wp3;Y^sFZrs3S}780=!Kn#MAU-d5h>Hm!+RD-%turrSUt2ao$!$3g>YPjA*sgqbFb|I^Vvi1+2m3 z@6(nVmA8QnYVBp_bQfU}y2=6$FYw~GAX1p26?!a1(1CGyix*;s3p3WCDFFu9WzN|6 z9-R*0g3D14xfrP-X|6pWG9!o)2c`%z^wq=*Mlr=t z+QmSqeG)|Ao+cKT3}135pMO_i(YgR-)nJ^`;;@s=nn2XYEI?yQlbt_bbHa2$EUP<_ z?tejHI$vmq%@_d`6{j9tf6*Xk5k4W(Fa-tshwBB8s_$%R@P%QcjjfkD6|rm~QV$u< zmH+5xPlc7?tlfqO?&~Y{9(vPX zEK?$Z%M!#TC`*e+e8@UI2zhpq>U4CJ1)bvXRf2ixyhRm&xS}~6wT{jqk_gScH7=w_ zC2BTUqg`j&tk>MQNhmB%CeA?Qqb~>@fcVu>>c?R;Ja@8`J>S$fB{(X@LW_>B4B=6< zqV3TV-_>m%Mf8QbG)>IFngj{i>UoJE7?XCykjAyspR!dSVQm_%X0X6n{3pN5piI@Q9r3 zEZe)K1nJpB?f}OAvN7j;!ld~XNedj^F(rEqbO<{t5u`g9+`NFerfNRMT)Ow~UD%ro z%ApM+Bzt3LGo-d#Ap)>h`{%M|b0Q@lK^pYE9Dvj$D*!t|UL0}xEo4h&spi1<5Ci^1 z0FEq7AV_56c)##gl3B(_rDmeCA-YzS9+NL2$pzd{ERsd4PL}fol~`MXSiPAeNKJhA zq=?7BmvOZr7NTPVio|V8jNi69zXdn}>hJojvriOL_n< z+C=0F7AfO(U?v`O|4QNd=0?32#!*ROEgs*~%>@9|>lv5HjlT>j5r89ILR=bRSHbq6 zDv6rBOyxr=37e+()d{ymyCml;}ae^0c|b{kk;$TrIh zTAkwq>+BnSXv|lW>^N_rQwcHe@@}-W;Xua+Ic?fe1JVoW+#XPTrn~rtr2IR;k z{(+(ksB=*j^AZQqeGr&Dn?vh(&c9 ztS4+#YQ$Hkf-e+IdQuQuY64@&Hm?csYmNXSV=694S&#Lx@k?v#1Gb3!Ix}%mj#Cab zCb|^u98;G89KGdg6Ue#^z)+||6f|h<~5`t&8kl@k^n?)mdG5i(46R zbyPh>R3k%O?nGP4F;ZWF66$FfQ=7nbGfbwH45*^1UEq2hMcI-yL^^P#!*EcY-3T8e zU_HK4j^R&MMUoV=zBcZgfGu9M0TIqy|BimJEE2O|)oTPvyr6O6rtU6NJ?|Fszn1GN z;bUVFXFz8YG;j~H8c9zZHY138s=L80X;6_g+!O{;ubvCdT^CBsI{LtH1B44)MN|>J zmLwM@)h$_jTTxh0$=Up+)PrL4UO#q7;9j{(MFo4&h?R?#e)LCLDHa#N$tb1(kw7Wv z5)zJcq(nkP_2E50A~Nrk*cJ-CV;G8zpvL4k0feHJaT;p#0ZThfFELNg2<*mfl*IXL zal2WBkqS*eNeOLm?HPpsmI-_U3Q4E;QsgpjmT)GtLfFJAp@yVrWqm1;L#NyX8v#~E zdYB=QKdOPT1gW9m)-P)mFG+&pfEILWE-q8OgLd#esmRd_fGE5@<;qWl_otNnuHHf) z>e|-{`T5g5r9l5!INej04*0wI!7MlGQA$@)1mVlYPsY`C21>u9{Y$>&&3H45-zDu7 zAqaqQ|16VmB#({|7}O5PQLuv4f%HU#am7$(rF1b0Q+fzH_0+-(N3+{?zN$xF1z%WC zXXFy)Yz7f!EBVt2LC0E|hMC0E9E8=MJN*!@lcLvB2qw9B8aX<>Cf5ksDeyJr8%mx^H zkOE;P=mwj-Pv?s$a;t@lsDtU|G?}c+Ie~##6fRE2i-`o0V~LSikvKlqDo2LSd28XVEY3OC{_CK&i=gU57~$_)*4UcShUM7o*Pl_DcE zUrv*lZUgd}qczWNl$>s%+-EB2BCHe32OHb`s0US#M{4$ZM9y#~+M18mhsrDd1!Iw^ zldhb%mISNtAs~Qs$gLoDA5`(`g*5-C|a_H)Z_Kk0hLMY zh+T1A52~AlfQaSnigSTDaj>DcOmxjZgILE(==Z5>OL;LbT^PX0t|ui9z_peGhM3GS z+_#~=;8Jta2a$o{41=m|;R`BXVpW_Yg$2`1~pGD0FM7mW+ubEcP4ft*+8 z*>W%?6cHA79{KgKA=B3 ziITulTWk!cZynKGwWEfeEK-PwgQn?H6f3XJ#XNCTE15s68*h*mi@qA6S>6a;q*y~s z&B*}{RENcN0?G-)tOwQ@;wgP$kb8(3#3G8$pASywyuaKSM%G1OLb5$ch6wwON82J; z0oLi__i(GjGZ$fiY!bd(Xt>Shul^?iS~47I=;wh;8dF5A zN%_aAAu)v9W&SnFh{eFR+$eKq+nal8ps6`s1wK={Gs4Vz@!Z!_fY}3#?Q=~3d~i%L z>8p&PM@Kl3@3{5_P=fFubU63WaPysXQ#}DEXa z?l~=Ueq~t3EtY!gg|2&alG%C(I`xKAkv9=snUup!%PXQ$n+eImjE94$Qbi_j6td>? zo7g`S<+-4xT&T{iDH(!(?<#aLglHgPg>F+G-xa`2`3A`OO{B2A#v<~#Yz4kZ{fTJ< z*f>M_0qf%jK7b%!6FcJ(!Hqt4yLQ-;_tIlQ%*AlU{@&V^K$jDYGn2ksAeD%#;hRA^ zjgb1(Nw*mlu7Vw-S2{iS5KoRSchUJxzb56CpOTE^F{4&v!7!?|7^tfmAX+e#pH5zed@LRwYl+o=gkM$k+Y@k)t> zk)25|32cmc3sssh2}lkQYX5drsp7K*hJ~iUK_iappr`St)6J&+_$5a94~S~9`D3$c zW?{l=fy>93E)Kx>wh^%=f~^7@ z?m7rN(kJBNtONcol*sWqxr`x6db^9ueidcjIqz>I>`=8N<yOQA0QO&;;(N8`&2dPhe{JD*Ku!pca0qU&= ziq46P0t8Vw7EJ|hInh7DROQBPzwM4?*XXg0TTrBLNh}WJHpg+8zBf0nr>5M}&Sze; z@Z6=e$*w@1WbC+*bHHlK?kz~rI0jtET}u?kyWZ2q72-`eCqn7YnqW6wDsgb35&ZBU z(lFGznWZCn7wJNY?kGA0_=%TwDR30fN$LS_N2{!&Oa+FS#tR4enS>kyx+6B%K+XSN zP+l1#mIRCAi}NpS&%S{F#g5kFFTq5(v)_98#i~P7q@04_n`+b}90)Wg7Uj;0Vc(1s z=8qZH&~@pIi%kS;NfK<)($l@4AOs){$rf#AUUN!s4@TGUgMR!F8{lC0tNvD<(Yb;u z+;;t3bf+FP_7NHbQx)EI+KvN6f#Xr6kuwT_lQl;pTAbRm4Q1;&q|JunAw7A^IG=A! zEzKxBrD;&?gWLa7L0gg#PJk~O2M=Hkepw20T`J7oyE$UhUrnC*vw=ZO8_oEhD) zyB7Rd8mt^ma=-0DrWIID{0O%+B;PO+K}5ra1o32s%TkCNDju#Gl_GU4rs%|&8z%!M z>PQk8kwBuQ12t2p0Q1uO7@bI+4aqm3U%VI}l#ZByuR9CM1{ST|EDVIX^VSekLEMOF zy7WLXn6fT<|H1wH6?w6L6oWAXPd#W)2hb&`7%9Hr#*)LoErSmTss^j8Lm~ zKpc<$Ac7A&p*ngi+^NT}LHwDi&FmGBJV1&cc7Wv~QiSd8p#$TgN_jCn&ok~7_3>sS z*yV$39@DdY;_@f(P1^O-X``_a*Qpr-COsA8BsrlBP0~T{sSSf+^#|xC6vI?i85znY zXG1K#@HqiZ6r+YJ=GH6&ic3UbMBjnhoW^AL&QwET{L(h;jCC;gCx1i0!xv0+i3nq2 zg%L+8-RUgt02Ep)W55h3e%+YMs2*DqgV>-%agzXZ$d_T8k#i#~PJmd>fO~rd zaA4+Z&eb3PaI(AAgA)}+hUo1FsnTnNZF!=;eqyAAvtz{zw&N;eT?Yr;uTtO_=>U~R zyNbG;qGER!_f-rVLtk4YET}74)Nu?%YdTj#*Na3)d(|@#s%BBG%ukF1jI?O+(MU|6>{=Xp^3w=hu8VEX5_3YHJPGk zdMY7oXWhg^SNoafK#zsicx%Mi6j-wh`r;v5AYK`c`E3|@_m%ft3T@8;hTVe%A2hBg z8*98<_DhM0n07E3hfSoA5R(k%j_Lhg0x$0q?yUGc%Yw9Ld(hWo#vv0%2q4J7-wrmU z4I4L=hbf&vaL$y#UX|!fgD+PfnJeOjc?dA-AS-%m3 z?|yRErO@^qV)#8o072u1vN6Z&G#o0X#FQ1pA)#CVXUjN^lH`fvkSL;d<$ft42tm$i z8R#!AT8R76Uyrd0^F{|tqSrkSHF*^XRNfO$t)3`Yc=+9^Yv_t$ljBl**5a5X`-)-D z;vW0<4yM<+9YwxV#6dizw%Gdbw>@w;hk)n@u@2kRVT-)JNO95>CoX`;f@^m}3<&wC zK2rjX{S#s5H(_S!1Kz!)&4tkR9Atn!Kp;Wmg0hK)@#b`nF-`7eqRs;MExiH}B{VOH zbzt#r@(FI-Ry5O5~=_DqF;+* znW(pSU)cd`V4^jz(pU#rq)D(M=}D!q%PrBI{Gwdh8)NMO6cN3oUf(^@c9?G$6gH2i z0#sYp+=HrbIwv8H(m;=J6>#O>1%t)HaF|Ki-#^47%HYSnZ7jewC;Sb;v%LsBgSdMU zD>rdA1j6$I`B`u+wXyY>ap31cjzh=@V~7jicO-0k7SNdcc`{l@DY?W#$Osv?49P*Q zqhW6}w{lyxtf*D)<{Mlj*UmX{-{e7b^Y`!Pt30>o#%XzH9T8!7osfvQXC zTLQ}{ro&7T710ff>pWE3x$Dl+(FCUJm6CBA=3FzHRSq0mHB!dL1BWUAIq zN6M$M7q0i8PQ!tSx6uAw(P;+5T-g12c%@KC*DNQ$bS_~o&?KlpO=g9?B||@azJ~1H zUjSRC#Y!Shx>L5SPP==h`V~KJ3~eHGuS!C-%9Ie-dLrJ9FY^WMZPNxl8$eXY@O7}sMA_2IX7(LFQSb76?W`n;!~|YK63oDP(;<$gI8hWgvgV8^KDFcA0bIyWk3xU`pJwA!I2xA$$KrFn z(vQZ{ejL;XH$$EMxy25* z5B{oRKEzg3e83EhPYd?%rBv9XlVSg+s0yv|3v`WUkWd#`iFNehpBud=$?!r%E?RU* zikNiSA8hmhckl1_WebNh4V^%=OjkI8B63DU!dW2y2=2#!z4qG%Xqbi!;#C|_mID|T zl!lDW3L0d@bV=}NS^xv|Fz6A+ z_}<^fttV`9x6EVXs3U?48y&5KOLOM1up&?W7idjzpi6~=IoKrTzU+lkOBMLn!nmO_ z%$zeP>MB6tqMl5FmmRf(Xjks$8={m3tZDf~_~v2y#_A6a2GxEH?N774R?Yza(fA{> z-r3a_7(Y_=^FeqORNb_K6Tdswi%Z-UHajagR7OxDtj-`n7WByGl49^9${Sg;2&p7; zww_&ta-+^Fu3hScQPJ)3mFO#$h~14!3}-JI#+RlgESL01a6ibqJbvD-y4Df9b=x|F zE5O&AvglQEjMXavPY}u<&I&a#_h_7k^tJ4aZb9BtN=%LWerqil^?nqn$AM^RXh#L5 zs8@(&21#F-tPCq+DvnqWLKxq^lAWjWQh@kN3E_(66;qKIfFSqUfKN0Qo8C@<*?>-c zAyBMLKeABjR3LUPPPzd^#8ed|Dabou1_OctT#JOP24+i498g%oD6MajM!&L+N1bovU*ISJ>fp{4uZrpbA}%6O-_+BCFwQ6nOyj8? z=!&(uK4%9JzDB`BA`{5$DbR}rd6ME1p;%g#KcGbVkj64ObADR97Y~vI<@s~EX6r;8 z$o*>%SM8Wfp)D1p+`Hjk498ZcMH7U3*^#DiyZ9&m$i+L$ND%?1O5*Sc6SRzkaKvD1 zgdp<=ofXHZ)5pAZdgmdg2)&2Ll<{K2qVEn`(+o%9<|#us3>T}oLvF&p4J}8Det#yC zSD`!={PygHyM1~(r95jPQqr*iFh6GH_b!WxDL~YSbIfakK+XD$xKQaVDMRcFU|l?K zkLdwoJa>!(fNS1+wL?ws-z=DW(8Mtnp5`oodMus(RLSMl zW48h|GPi$LspopUc)Y{T?KfnR<%H&vHZGc*#7Vmj+p9T`=OOc&IEFmst&?ix2=53lb1N6-Tk#X^-vWa~W)R3`27rkR#V*VBG*F$6G721*5WbnqhQV5VM zB4xaU`bSW{zu(|9m<({s>hC~|FYbltZ9P4%a(l?U?t#cnJiM+%fmPLtIoAI`N*FGZ?2F$ z<#x%B$?!+?e&Ig;LFedG%tWfCA!Z({24+-GWw zp+4b$F5qgHcSg`XNTIwCPlqyp&r2GD;f`3Gz9gah(}&(T%T znd(K%aR?qL#0z+VA8(AB+?>PI7lH?KI3^j5)6VFi5Y40^=D{m22%(Nqoay~1nW>U) zF-76qQF;HVxH}B)9+G!_Nc6djN>}7iadl+pVuJ_^koaq zT|7~yaE-8J6>Od30~eA6fLu2+QB6J^lqFf|V$xIjQ0Yo##6{~1@uHxh%a|@HtV+B6 z3rgxxHk+VWWvO${phTm}(Kqi<_6;nmI-)tP$BPz~#O_?mzg5m>18VB^cu0<(3z$?8 z5P7Mf#{~{X)Kni$7iGCL!Ol#g_*(g3QjW6B?^zS%T%$I3)TwckPWyrH#P5TfD>6f%4ZR1`Xt{8f^8TSY ztayY#yd3Pn*iqLc4!N3mi9R%HG?zaQzIn=&I`<45sUSpmFo60rSfrxKb%I6+Cdbsy zbGYu1Xt6OJ4CpHW^e=%wL+QLYaY%$u5;MKC2)y&QgJAQF@SGyG>xVTvr9a0A&oz$l zG28xxalDt82JgR3oHM0korW~{qYb+xs1XV)~vDUL3ON*k%KFvLYGk;$l* z3Xhw&bUxSChCmN+@_uG1ZQ9Uje#kF-j5u3Dqi3zMb>cw0ZJW1*YhTGa>Xg(3=F7S_ z=0Y4X*FEKCII{qv9LSN?TKq+~*>~}kZZSXxj2Jm$h>s7pu#yr)J>~@mTDlMhFb{lT z6zg0x652;@Ny1?z>{|eQVBhZ;6u~2T06D7`1(k>M+qqxJ0Qg=EELB z0IKh&Uu^7Q6$7VD8RT-TqL(Z;hfrf#H-Mo_y>G2ywF1FWj)SaUm%=4NWW*9Pp&Du_ zON5P()#uUtWaAE3(#?8jWeRZ_<)IGa0A;9@0j1cf$x#YZBv1n!#37kD@?%a9fEX(N zo60)hbUON;;95($#}`!TB?n#r(@ISzy(u0;Jjo#Jdwn2710m9>(#w|qWZmLd zt}kM>W*zki$yKS-IMRFZpBAsv7lV8|h6}rCBg8zEucx3@2{odMO z$B!R);k{&X{ComRe*Z*}VA_sFE8a_CZ}a6NPJc*;3c=vMB^|0KL{;!mu(O@mO$fkU zb^&ov8GH6RKus8^YE0PJz-T$dQx^$&L|JUhT6{oiP%q1&Q$9xV$pwyfgnxV_4oGS* zGmM6;7Y56|v84u+gyb(Ns1OoM07;qV(j(X5C(`*b9UKOqQ*H*la!?C?k9e*uV5H{+ z$Hn9+etd$H7mV@Hy|*u#96!ah{E48Wv>lFCycfaV<`ZP}GJPga7nHVL0Myef3@h>< z>mwy4M!rsL>%yVAxrcpQJs>2XNkM^tRL%*~nawhvHzqqE=!QGXsfn=0JlQ@ z6MZy1x|o@q%)wZX{|Cv61UPE$2m69*Dl>o}A=mtkkr8@{)*_LNwUX{ytW3ft0Qr@z zd!(M>1O|HX?TicrBXWuZj-MEAniil$!2WYiYDfU6DFlq(yj8e6P(AQTDYiYbrH3RY zTIA1$88J4;mgTfW^EQAd_yMf$8ENF^DRp1QZw67C1I$Z@Fl=3oy)jvL5HR-S?~Y+ z5~`gb(61vcqG+MDW9j^!?03r@mi-XY@jiS$?34-x4ZD2U!%19}?;#5vyyXIT9Km^4 zR8`K;W_=2W;i0;VBfwkKeX*(SbBQE*K@Q8CU7&Xq-S1d>%s%G1(h%-c$zN$iadqjS zC7Bygd96(Sx=8Brce^saGY~oH%$*e(s$_^S`92Z^60(jn%|%0u0cKPVBkJ*rMzZKc ztYGLbhwMZ>6Rg8nS;cUJliLTf*ff`jjue9~Au_591GWP7-EzR<$N41Jc_|x3f(S;~ z@`qhik08$d}P?KxB1F9&G)MfJsJN3Qv01lF(Hl9em8dbs8QLP<%_U-I>}NlD^Pcrfh?;ml=?C%PMYge*d7~etK#nPJD=!w zwKvOO7Y~!oR3)?DYM4AyM&%r9ik^sd}>==5Xx^yjWo+jMPhY!u4s2uQ6hOQZTu4*{ER zya+~mvA5|zOjYX^P)0$Z_(Ac8;XB0_z{Fm>EyE%qZ4eXQ-T0*2(BKXs?X$y#cyP01J%n9m ztc-9GEpXY?ZOM@~qdzk_Y2sth-c`WJ%gj1ig0o!G=a-knpzfBZjWk~r#g%NvwY2?5;?x>t-)^EBg>O$3D2F8He1Nk7f=Qp9y9l-}M+k`Gl{Gkob+ zTVea>n}1APH~?5P;tZcgejIh_C^dGN^5}>KpLawe7=f$8$9jQtOeO4o=-H!xRSU2< z{+Tht7<4+R@9$*lOliDj9%;5nxxVURj)6Ak2}y;yjj+JpQONy%kQtdxzs?w94LD~Y zw)L(0#=_jJKdSYjM7@^^aXf^DVA$Snx~lT-!=`aY$BUqaWy1La;|J)?34|HJTf%)8 zMrtc^3PZ>du|j`=?yijdvFpW8$>etNfafZKX3GI&DgVK|!59P1^&CL9lnuEsn%#T# ztf+%{)+2(jP`*tXbo_?f10Y&M4p1q+>jVMDi8!KJu1*Zd=O02?L~KL{8!t#=Zq&`2 zn_&O|?i>vQ2lnKzlEORSL_q`V@S9N|Xl0>n%m5oz9)M{vpD|P@dDUjUaS(fhn8eCWWqFL-tgNYq1C?su?9B^^sTZ+ctGPnJW0dZwD9n)YhuG!ftuqf3?#Wn+tO*Ao}iwv4Nz6u; z5~oU23fJLp+S*iR{umzc4)}^m&#QqBCS*zmvm(XfpR@QG19sFs+)3L zAWf7+850poa>ayv6=%$3+^SZRG;vp-6^S9qGj*{rO*BPRhZMAV$PsE*o{O$efM@nB5>ZoOq# z^c@?Q2*4V6rJ)Y;#~7s6_%=jvjU*(04SZw06a~@=I{y_7$DxPc@u)Zr;+OI`3`&~= z!kIJ^kd}lA+WmT!)zg77w=R-l5O>Dm2_fAGxsY|G>no*|`|1al83?+{WXtcN~zXfx^zCwXrW$q`)R0dyW2AZiAL zil|H31R*-r?k;P{KiO5fW5-ydhydILHghS+(G`}y&~}=3M55b`j>%{`tk@- zQJfF)wa)b5;bHiW`d%0p8eh?Bxo#X@uP$7F5b}2RplYrK7 z&JucWC#_Z~(Sk|i*I(KhUgSxUtuwb|BM!F!T0o`0+y@JrE?tjvnN;J6g;yYvBw~yw zPTpHExkE@7ant3vJ(n`D>2rpZyf>e)_Ha^j0!UGlswp0(uDyAZ=cRY)PlmbK!>)Kf z{2(NXKFNsyK!w!Pwq@+Nf(HGLD_5GX7LRo|GUr z`4u#qz@If+z%=zeMUS`2OhkV$%Xia&F!`I28Z-x=(UpLRxYbf21I@|p04#?;N{_aX zuB2>Jc?aA-Ne|#jx(u=tE>KXXhKNfuqP2kt5BC8A6Luhe2G?n)nvSLIg%OR$3}$X8 zVW&SLBg8irYG^~4sZaa}&+jusT(5Iqfq{oVUeWq^W_(^G5j}TnNkhgP{J;=iH`J*c zXh`qr1#n{LzC@CP@z>{MoEAZHQbZRHB;hUsGCuh55H`5@{asQ7QK%50+Qbn^bxrx# zqU$%wMjSDeu)aYqz))2*qNtvaNC2bL{{xvv1w@R|fpI8A9fkPF!wfP!Ue~T;KaHNy zznAr(*_i^M==Hb^vm^E^V@<>fUOHGRQ(O|fAOZXCQG z1NynN%qQ=)1mz%*9!M<+9qkYm?Ny>3ATo=7rb2%s7A?McPBfg1M4K89VpHpmq5S(* zdYy<9Fi1U~ul1b~jlIsC!Z*aSjESA7$6B~4#7%GPLg>jhNhCT6p_9K#tvvvD zEwg|q?~Lj@Gb*(6GXc~979AtyR=OU45a<*uBGm;X71QE#a5YGJMO8P&GY$0LPK=dm z361yyP79@u3|b6gRZ~KV2~|ZI`1S*;qP}v(6aW{)I*d@Ra5N50_KXpFn4vQjUP(sH_k z!a|yiG*@k^A`ruouctkfm(^RE-w=cny)|5m| z>J0cIte`b+#qAa1J29$FGNMhj)hs%Bwj|c6X6jL(YQM*|2BZP2i#@vIYEohcKvh+w zjr$_Un{7F%h7%rMFCtXrqdiVbHih(MXaRu8A|0ui&_9McXlo8pup)+oqz%NyT~SQu z5Y^X*7Dex;Nr+=^poj}&Z*$SWW69_tM50`-0FGn9q8`~J#6P1TR>kR-Z82=Tu)HJ1 zOAL$VW9zQyVu5Y^l${R7*$m;<2O#s0#W_5P)RUo!0PHlq47!mf%6chU4Lx8RXn#EG0lK#jYA~ z&sasj;pSke4nz{bz(a3w(~th(X_`~1D%`%nZ@rW>{s>-F2_PY z5PwBV)mtR2-vCb`d{mY~P2NX9ra>#@V~rkw8Tl*H9(^-#3pkGfmYb0McxwGWmH2Ax zhXg8HE`giNx@h)Ca?%Q9fA@nVgGOQnM+Rnf1~bMQ%%EW040hOlgTR!ozeQYGA>wPU zPSjChafS-HuCd1sxP)Hnu&nG@`k+?iSVZ~PzhPhl{5raFPMEkXx}9_#?t=(qkxUl( zXJ|}K7=frPC~mJ1--)q@n{eW!xT+(Q7bIs9Wbuo7>SNS-TtV6QA!P%i@rGc_{11i? zTvb(KZ`if~)z?HKbcGw^)4C>psXuPhfX+)dNM8mNRuO>vQj5TqMg&k$_>H~`Jj73t z2Tj_#Y_OOvM^azkB8{jI2(JFnE+%=aB=~K9Qfk>0ciI=RrX8hnr_=G_w8cuNImmIxgW@k>BjP{L@)8>0J=(>aQTN`ZyAtjk<6m|;gSNlsi_dQm z5EUOdlb}PQaVn#r8=z{`<5e46i<==fo$v3EcoeHHxS)>2$y{mKR2&6eB;C0rdQq(| z&Xa!^lJD|pgyTX-R9qZQuzeLmXCh5W7So$hCb;EF5zD3x5s#=LkTZ7Z2{NVc7^#q_ zl|^iZP5b~6-NX_v8mye)r1_*#`^wcdo9vV<3K(fQ#JoV_!jLzrb+3^8LQXKrVkL1v zY=C{)DE?7qJwik|*u8GkVjv6PD#SYO8_d`&qWs=<{qCzbH<-<*mk0Pfd19J6cLj1S z69?>^9le5&|CS!SFzE}Ql=awB4&%o6@GKaZCQIT@>0JplBn5Kw4 z-2f3YC%hx}?#J1fv|E;xw1@~UvnzhbNI2RU&qUIE+UU(m;OG>MQ?e)^r)5x&yhQG> zuXM0RpamLPd?8>B~72niQcTX^2WJ;mdcr z!nA=i5l*cU!Mied%X=%fGWZbL!hmS(>%=*h6)9H{5ATU-mf)jhBE;tyeHqbRCa+%} zWh_x7=D}}(ar2h}csqeC1jwB4;^qbzc=x@Ggdy4-=Z2#E2+woYyrroA`ISNH&-Bw>bOJ_hKA=xGQ?a znb@C_JBVck2Ly)g>Av?6*8lr)oU;oZg*c+yWqOmtF}RiLNe%0RuNzY3(3J=>wlyN4 z!BY{9F#}p9a9$4~vy+UW6F#1x5Y>t%gN*tKxT^AlIc6dd)65L2Uzkcploq&^^W5I3 z#J-{iupHf+#Arvso$Htiq3Jt9(A7l%6x{=X%bU{ZxJ$x#Y|`H;-m}Ys&05OAj;uAN z5{A^{D=p^jMnItUK@k*s;$%~FGbJ;U#xsSW#3Q}M9k@+ma4@=MLJTunATu_esECsp zA<*@wlUTkJF~itQrCXn1YbV*2lby;7K#~eZ8mB#RXSzv<*hx%S{6Zj-E^A*IJ$(R@ zZevVySyUiGsF?P|LoN;xU^qzzFiR(fqnPhH z9gLyv<`f`m#X;vw*xV2QEfr{N$F}GXkU21_4`kan=Hl{1e49yv|By=edVy5|q@DId z6cQuYj7|W;5I69N`g#ct?-e9{j#kvt^W3{QVbPA#E>7X$K$%k>KNrf4@~9^=K?|3Gh514*Kd3C=)^jbCKI{sx>WA zw{j4h0=HEo6#xQLLbc$f(8UKEQ75-Ze3vsTmF%5qHs)5Zv+1UEkCt~$e7%SW!KTSA zw53y^xhaiRvfrJYS00fClKgXF&_!iK9=&8lqK1lNVkb7;IDM9p;gH_i6CPW7(?^+33WN}#+YoLtbVTPk;&LvgxuS@cE(^v{fHfI0ID zRKPy$yM+HZzZ~l(cg(Oy|)Pfvi-aiJ0LndWmO}`gId{iYSMkQ(yblNvxePmIlJV)By|7XjrJ z1ml@wkjW}%lF*+6h>V*jM&|=$-)&&Ku#qM~#2RqaF6_t*tch$kWuY)q`(Yq97Tq(v z@g^do-0X9O1qd>6!nlM50&qA-9zQhSG}oSZBxZIpGCpSHJH;!CkUZMo72-W@#ZvqJ zK(tH_f8!`eSHZdUu@c8HQF)vhiM-WXU2?)BR3TPhOJ)adS0D};VMWJd6S!`NsHIY| z5yRDf4v5U~vFzejbW(tikgn<00U4nfk@%e2>2K7Kq7Qs6Aa)FAWiPl%p8J#b)|KG* zs@T~z!4W!Zmb}+;P6YH=7Xnx!FK&_}s4QGrn~eZl!d9A!eB#9>*^Xx-#@M(yd{VNh zNR7KoAam5fl=cyZ zs8Y=gvtcWkHym_59&+1Fq%LqJABi+ux@?<2h=u?C?N6qRK3(A1{RE&g3QrNl*Ry1R zu)s(;i@-9!&PV`DC5l{|nPczOOhU;ifvau~xc{37iJ-rDux%h@B!tb6zE%g`W^{=f za0Vzq0JU6_65?tE{cuOx1yla(Mc^wy(9h@w$IEkOh`}#k&Y(y@aC7S zn<6=+cfF4APFlF_m1r(?MVji1)z^|S%v?cx@7t4b?y7lYkReiFR=X?NLABEK#FpUL z=-D5#TcWw(g2^3_&~9(8s7e~o(Zk$ON^^8U;pyUMC+f|IwkTD~&Fij#E^-GtMPs2b zWRnTlO-IeSJBA#@qc2A=ffZhdA%8Vp(&AbaB_=bffQfaj^P%MmAub#_Sa#EU(T%nBFbD8{S(iAIwB(wp9n<7CC+p7UFH@Ht|8{J zyju4H!w3yvF4n5> zK}8z7_Sg$UJdP_ds*0P+{}H0=GhEYWLwdKc9YdY{1&eXMLP8h}PmCl8YV!BVFKi2n zWSP!RmN{m)IjD89i9YP#@nNR@oG7f7^Je&Lj~^P#olf5UlW5|6p>s&MiH9aH?jHKW zIojeJof~Qj_biqaPx4{af(QT=WLZ*g;%W>tU8f*H87N_qe81gCElljJI(0YD%9VN)oW~Jjvx&oD8(WD~ z>`05*7Xp$*ZeD<3)xg)K(c%FODBzM+WiW)k54QA&o;)#kMq=6-&`Rab$So}KHvht! z3EgWTy{5y&vT*<)+t>eKX8J*dj``?dKzm4~b4`4KamPiOuf*8aXBzgGKcyQ*f00|Yzyp>6o zoB%_G(Y=)A0a0f42d)oM2+->TC@1<7hI;APktM3fb1oHYEEY>iiHA_T&1jNGZmgQ|PrK@4B_(^TP2Pir=6YCINcM-jl119+;$G&VoY_TJF-fdvE#}O62V<3kW#{nv z@B`_l{Xbc1x2YteH<7xE-N16qB|7XEPd$iGzz*{=Y3^z*VszI>gTZNeFzRK{(ZH2P zgI<9_T%r@scP1SDSaKC!fu$V7Tbpjq0JK^i4`@KqJ!Au$)^4Dvact=Fy*M(-`L2@h z%_4PB4w9ExA9d8%S1vrll`yawY4Ls8-H`UAUQ{2No@7>Z_sX|0=?0Qb9SQ}zj`-Y| zBR;)6Fji(o@_{&GdN>vulZ!^#;=uB#LXQ?!h)WcA4np!pK8S0r>nWy^B!9fAEYzCQ zJsFhf+&t{Sf4&5xcndQjc%CY=9&&aMLDIB8K`u|JE&jX%&zPPwS?ZAo%u%P1w&x|Y z2bXaz2;qB(jhiTfJ0SWXOP-n7v_6XU3}tE4j3Z75Wkmj#D5f||zhFxuXXzK3F_vb) ziGc|bRUsyvgz#yE$QHCwK~B)MN1hVr!xuh$&>jpvODmf2gSM&+lb#h8!l%A4CPqG0 z#1uEsC<)`(l3%o8iBS%+#0o%Z3DT!WRHnSh()8G9{O1WRRCM3Dg27l2AG?_BJN;QQ zUskr4ih7X25M4A$R$WJ6Y2WH8Z|Ga1ps-&PqAts-DtHJAL(?T}V=d;bie!F{VEfNU znv#3p4i#~WXe1&+EAvOR$NXhW?Ura}X-llP9;_xJtizZ9riFGpta6Sm%><(ys$l8b zQT(EDx&E0Wy#U+??TJ zU~*$agoR{(Mvj~yMDvbIwscuYcWB0;gJ?0c5Gky9B0+R{-v*96nHeIup-J$aCUVvV zN$PMwc(bG9A&!pS`rP=Spwxq|z(kjg-S##|_I30!r>Au8+c%;d=-`Ky^Ng!Th)b$d z)Ql?HkcT@X9fS)-b-@$TIfM^6oo#Ee<9?$IwAAH!8)I1s4YSn2ZOq_Dm09K%;tt@p z&2RO`ek}F&wy-U{!C2s2*=RAg5CVTtAx*NZr^UHh}fAWCqA$K6H>EsmEs_ zkwsF`?#rDs-ufoVnA+&o{DX{(8FCSF#iWBV zpSQw_E5q$HMg4`W5<;Ull{jNjNE}}x+enC&`AgJS!IV6mZhW{(VUFa5;f*$^ETPm$ zxnuEylW6x<>dcdzx!_%d{8s74zoP+-GWFoubURWD+GgIFwrY89L_4>jEJt!}4zmAL zE<&8tB-t|QQQUV&yUvgDLs5OZl0t{%bo;f67py5>BB5yT@9BmA}*07C%nkU}ci^_f$=RIQ`YzBzBt9KjU zGtpL#i9lW{nP>|}50$+440=mP!RP9nYcFUsM4;viQBYYfE$it{@DQt&n3AB6$xNc; zFB6}U^2O&Sys#Yv{X->kZP&ziD_o$PU{7>p=hswXM(*&1u`PhqMQmZ>j=t0a#Sz&h zQ>+y@>%r3XXiMfwc`SmcLCT>AsR;##M816skl-%~Mc#6lG`Phnyn4utp0h`<&Thn% z-;lyuePH;2AyIZ4h(+CcqtxsP-8bU%g&UO$-sE9M1(N@yETe@7a~S1);ana~hP6AR z0$kohPFjYAa4L}U%S#fR@4PHbafFrZ_PnH7`|yz&y{6asS|T`cfvjeNb1H)x7} zyZR}&MHmC85R#`pD7Jn8expMp?3HL*GVKbH{9qO?RV3FGQXgMfz-al`6Ao{P?$wx_ z6;jXG8zHX*ojZ3&qdVU7520Zx=zt0<@}zWw#EwJ+eL}Da?J%Ya=~W-%jnubC1HxHi zXrBwTf9V~w-!g*cq;LusK>;Thq$eVt(b{y$B%3J{#N=h-ex(>S&k=iizK{vVVxwE& z^$Ed@a)$S$jC1vo_Zc4U=u<85cEs(H!ceipNj3Oxo88}h3vrD?7=H)0`x_3?)dGF79vcQ^!Wr(X#R2oTL3oKY``da-2pXw!v08^-?y{nnuY9-KvtS+#B0v-CcJiGimFokJyHSa(V%<6rM)rV#mL<<*(5iZ zOZPDK%RBQ#)DsI;|c z_0Zs!qKYiW8cswU2aq|8kaYUsS&(8TF*?56htum(hf^@c1~{li5%DaAa}K9jHS&fG z-wZVF91?*iBEK+lEW~dwmLgF1c_@gr;SwfRIz%@30VK}`h1BPaeKEKa_%QO(#gqJf zH6OX54O%GS#)T(zur47r=0>+{JrQF($t_q_73ugQQv)?QTCWX?UVV z@2||*y1+7gfpK7FW?EzATh9N4+}rp&!^(()2wXpia7ov4GxhbD>`v6J7;jw_;*5>O zu6gd|?960C5eH%D9HDx01#0!lE}~(!u@p+71=u`N7=zB7A&H9+%v@=T`WTo=ERiW; zNzK-JDSrZ)KADApPJgv*UiiGF56Ytp)5#ek2=Pw>m|h`d`9O)f4q%iY?YoHhanTaR zPtdD}DHtSL)4;PsWYJb5^=os{5qGNy3kivi!k|bah3HQgb$J#&g=@N6YP*UW#vs{S z2W2$ivJ}BCPgx#ECbu+n1mm|uhhu{FSh%m zFv(#0x26^cFb+&qT>=a^_hC5hP7Ft)uB~1!QN(E+@#@i$Uy=!W1>B3XVM5g!?AkKW zh=GZBi`PVQNF!N3tS~m!tT-khrWB!bBgqlAo;({r2i0S4zQWJ4($$xfW zA$mty=Y(;J#d(S=N}mprVw~(UT2$_R5Ls z4GXGirj48TXbVO<5VH}`2m>3gbM;G@F=WWs$nhT0Xvris!>IWDZn$^)m z-)y^Lx^;L&^$#@KVr~kNFE}4*e=8|>X4>Pr6L4^?Gn2Foa!E*B$vooFP`9rTVNvc_ z-L^pWTC8k;Kd9_Nq$e`$K!nsHGm=0?vlCFLz5^v^Fxhe!AGPeJ+&^O=Re~J3&p-&` zlixk-p|n-=tsIcEV6H&uoZ}-&@V*7^IPT)GBPq;;{m{WPPb1A<05q zn3(y9)d5cdZ|&GHfVo6|lAT27JBpqyLf(V2W$EBTLL`0zi2Omg9wLF?V+_wx;^(bBy4ht;HbgikpV8-EDv@PW4Ug*ijL|o7ErrOA zDaN5mcQfC=5gBuYYz5fn#@Ej%b7D2?@ub^xgF)ohQ%6voIv&2<(CSMOIQsebzUshbR90Ntf}Je zSy)3~r(IUoh9%JnP()G3mP=vfhBw8`4oYMwf`r5$vR`nS9c`0yicAs30gFEhgWTGB z)=Umn#8}&d+f~-~my}UNm6bfb(8Es$C}`c$*$?xldf);FLoy^sw;=}+P1xGN-kH#9 zD>xDNrxMNp8rs;LO!D zv$X@oQadWTu7pP@FQ87k3e09;iKJ0fw`pDoshE%H!arfqM2Ze+s^c;9 z2{axr8tO{*#n&@p3Gquu68;KdMxgU7o-TlIw=!-K{}E?w$Izzu0YA%WGX~kl;MkH| zaviT)#4AXbv(txdVp+27Eg9&>jOrUbz-v=HJYvo*3?!SwUrgJs^;lJiT8!z0l{nMA z+VR>FJ!XFz3~$&1MtSi_Ib5uh7$q2O(i1~zv7l3-Bs0($b6wr&GNK73fcJ=4ZPq_O zG)TvxdYF3Qr41b(8|RHNd*$wt`gw*HU(bjo(N~@?q~NGz9=6NRtgJVl> z$#u*x>pgUo7X{8MNRz?4z2252+ul+RYX&l%*6=1sIZD|!YQ@Jh!wtX;l?pdx4~%i% z)d$oJOTbx795TkOTEcWe^mbkD%5YNMR-uoF6 zy8kKa0E?1KEKGWG54C!Zpr%@w3(#>8L8kSX;!+Da^o7;AacZDqv-Rk!qXSI-9Ed+L z%+OpwFf{>OcnR4PU*R3Z2DtC9JgRdkgYS_wzv77cs6Kvdv==LYBLp{GSuuXx?50aW zwcL`s6<0w&d&CYAk{?cE*K4Eb`!a~Gu!NIggBXn*@ z`Jz|BOk-+NCvPT&T^WxjK>+=|-Jje?EFllZo)P4ESFiIGs5`-pfOgwI+IX%bXYBK0 z7W4&NVw6wfAps__WhxL+O_)@|L}Q^%fH5Jgc_Od^+%)*BGJ-B5FI1rB(SY-2=A6** zSknvOm998RoZ6^*qdc;rT^Axc!D)yrbOVMiTfWy?W*VdpKC%87Au7}^u9F<5UU_uf zIq6FzGZHJzwjBnNkT0}~tLU-Qxo_j-XlO93p&}gB>o3-{a#-rP=A_4ENZ;(Z`8<;R zSZI-y6QOoYUMS^Y+>Su0bVfNN(wG3uOyzMGS#g*hBeEp~8E`(0sg&O$Mt9ITStQ!b z@~$7bY9GKrRY1E)VbGNt;&NW8F)_XBaG$ccR}V;(&Y{@TDbh!js~ZneMqSjI$A+Yv zyqR7-(UWMjlAg7)hA9^S``QJ)1p<#EO_3t%5l3X?k4}J+36{#GT-hEEKU98{kt?Ez zB*N8>&V@yU9qxJe5V)#EyT&1yvl~~1m&;woiq(Wj$&=avkg()B9f%J;fYadUw~KdMh*)ODv9As)zfzWxLqQzOWU~bO+r!^;#wrw_iL1Y znH@A^lNX>|?SNIby#>i4c_mA7NOMWa9Gq4XsSXiM)Kr$5jd8L}oD@AUcu6X11zyUc zlC@$zo?$571$A{qQV-LoH)YA98xfu6P&)Jbpk#SXrZT}susT%@*z|-*iF`|le4>|^ z-0Y3?c7RTbP(tMd51q{IgHRsXNRbG-NbYL7ikB`Ci{K>4kTfR1!c`t^^p{)Q;CehF zCof4$q#)E|NTCkjU4^m??6`FS3FD|Ugxud`$jO3fn~f4Kw;Xl4APPVaLpMhc0$zNf z$ICz|1Q>FRog0W?&%VaW+(pI zM|4M|&l(fmDHjep5B^7EK+p2N=$GUj>HlqB69zlOgTN>;pgOW=d7m&ge(B9=4z=7o z_pp$BmAFp1j6yUt+@{TlFy%SUv89(EtQSTc3anEh?aww|eufEW2)KepP9Cx^1KY4p zg91>GEQAqp2@1P72`<1W(xc9A*l>HwVvXMsGx6~Ss+Qc4jdM%U zZM0wqZo>q32C7+Fjo$`XyTZF2oanKcln?t3I|krQ13OxB1i6K-+eg+-VV$gM1Gr`JV+CF5)N=W9vh_hK8OkR7Kj?P%eqF{ZW+W2;wSz2x3!jpyIrlgBs#DPzR50 zjfqwhfwV-3*mY~zF02K8wz+GtCytijsfI^4Z?Mj;VTwAxsYs_R!nl}46`=Y~Y4J6j zV8Sik6zlO40$m5c@uT^~Tp&{YiX{?97*X+)M0jkd2U&blbKCAb-59C%V^bvt&fs^fTlyWnI})n`VDPoz5vU}mC3Wj ztx-)rdRL-k$=QM8!5I`p1#16*pfYv_Pea#sGu ziKz{j(ENZ94Ij`zm4hy+CWVwiRkcO0dmRR@($V9g z?lF-RjgYo;yGohEjYn#G>zqG;5SE1Pann-k})6bUkgI#~9tNB_vo zmf|$yOw1qh>qlo>fnpd)p3)zj&zGalMKY6%GnZ?4!TH>XH`6l|V5ED8D@7Ni$wJ!n z%yKZCidfcPIOFn*(-YdBNm33G#FYXwn-BBme}~id<@@*VP3%KyN8Cwk+EdKFf119$ zR5oD?Q!bg)TnBv2V9$^pa>d-J6Z9pm5D#^}dkwSp6vYY*N0-er z_QkM1FA}eY7=ys@nf2*%)VWY*a&qQUegO}tVMg>=Oe0JfWNh#B(0 zMD^M$4R@39h4UvfU@7Q&qaP@uiRy*;N7I?ij(|)wu)^eVJ2=fIU0?=!FJ~kr^r~27 zsyJOl29iu|s1420#21&;fPXaKcRd+NDl0A*RrDQ7r9+3ZLebg*2v1lTZw4-4TvuT6 zVRZw!eRrM-xi!bah-MIe765)DOk_%3F`PGFzi>{_h+yo=e~91X%JjP5PdN)BPS}Em zCh^66e*?8+q{ncj-zu1IK_Fwl2v&P$kr7qx%8WY)2jkkpIp}i0`FsEzV+xSwoRcio z5!FZ;+LfP9fO!7G9Ks54a}n!1ougAGIfzos6xEaQQ~-{#9x`infTH$hT{eopi~QpD zI|3pN-*S2*hCfv39q_z4;_9lbgWhn=y1OlDIljbxt2SA5QlKC;`))4{KG93i0|vF^ z=me}|5>>Q6(Ng3m&ZH;C0mNb*B9ayDLuAkr65xo%UX;}m7qiWfv-SizAxsb`^IZTl zr#=DW<*m=yNA|h&+ahy2P|u`2L?k^Kh{4i@+N;%<60XH%V{+&ny(5@Sj6#C}la$+k zMQB$Lqf+EL8^$HDEjOHAM;T!8bXo?i-Azp%A&@Z?CK^$F3#5u6WL(5_!KB5(-1r63 z1Mn(R!Br$;1psa^1J9EV4y9OQND}q5Fr5h~U91+Xp@JQA7HN+nQ%GOZQ{7$J6mbJ` z9-;;nFM4Z+7ai2z^vz&to@Px3NWv<2dvonID4h18veE?tthPX5wdL$pnt}Eflumm# zQCv#|)jxq#*qMo;2%kg!BBu@RCto5%_rNIecNT8Q{i<$j$iy*L0?Z5JJ5M5=MCWIXKbX+4OULNpyr@c)F15??64Sh!$R*+`Rnn79Wfg5n{j7+6 z1A$i55N{zeBT^TURZ7ICO&DA$L4;JUCVVaTbU~I-#Whr~qbYIPq#}kUxNhT5B2?Bm zd0i9sLldN@T}jetY>pbcSKQF#U`YU_D6;}gYN4sI!`zon+qIa{U&bo|Q3Rh1z}}4v zB->vDX4}1H(`&2Vv1s*|fb4*rUKHLIalTCCh|xm;XN(4N>r+7uOHq&F!As`y{zxN4 zn?mJbO6aXQhDt1VC|+e4z-5!zUdI!W(2-*9FZ}bUBi3Al!sqvxL9(0zk_|*Xr1=cJ z%3+qCwUg-XDNN8`SP@Yq;Yf6yQvZ+T_W5zEaX4{EBMi%a`0Ux{1y!Cj4(JJ># z_K%#C{}k(NezKdy`9hH+OkW-{H6WnT`Ym*?HYn4V`j5q5y^TCU#N#l^3ZufudXyD~ zjcOx)ydEVJzb6v~H-cKN5@w^>9=DRiB}TrbH*RJLwr}Tj!3j zy6u>69hXX&EM8OuF+`1oNZkRCF@qvZ?MR45(We0PTRf)Ba?jX!9lb9GD3mVyop!3= zo3V+wI>J>By7NUX>(nloG|$wN&7lfhH1ZcB#|{YV<6$aL^AWLu0N1lQe4hvak{~H| zucE(#GU=V4??fI%rqCXjqmfCQr_=v}5H-k1GF9SuDD8@Bez9J9Bj?Q<;41`!DyA&! z`7pqsAj?-6<|g-g~ap)Oy9nQ6igS_ zGaRtEFgQd{O{NUv+W-syP9Y>wk<~nM!+yj?twgr~iAywb)mlr@NhHRK%33uNfUbj1 zLlu*91?@prgeaZP>5TZ?E4DcYIZaiDD>ceEZ*$dn!JMSg=@bRX)e!x$Zf#iUuYtKc zp?L12W;{onGj1L?q79 z+9e`P!QvNOXT8cwrlw1Rt5i!FxL}Gd4p5e(HEZ_n=8${@Eu&yL8jfM0s8s@0;Aw%0 zCfGxr`vpXyI!`($r7;-nRRL>va*z)=J-a3+?-*Ign<&n&0!4?M7DwARi)<+C)8f4( z%-so9gtuR3CBO~EV7g=lH7rHvj0WofONgGytnV|>@OsjO?_K@6yA39+FojCliZxpe z@(3Lr4{G4q_;l?L)#rkk%n2PSOIQlL7esorhv`KTXGz5~kgqIFt)VSUSvkTl zq=ht{RDnk5MTOtJc;16Ud@|hY=S{g#VM_66TC8&nh)K>dWjH5Cij8$L!0q)D_j5=u zJ``0vzZJNxeq=q)*X!dVpfgpDU?EPtQEQ6dD#%}Wt6ML}8X>qiDGluB9nDc3| z8!=uIGrFJ%@$>>jPjfG6Vy(lSVY?{qH(;206{Mmu{^qKAYsAJD%Y3=e?j zACFfjoX;UmpYRMO7)w&1m5jo`eb5*qW7lEW2@jB%WzE6xwnI`2xK-3u6%nxC76^zL z^6=OpAj`j;n0{L#&_Ufu=;GpFtepRWjCz46Hi-BJJ*X*(hy;T%o&#P`H}HNP%qcfUpm0_d&Q2V zt2~n~a$La+QffCvjJOLU>Tm@2&jVJ~1++Ykp>I8Rt4oXh~8+|diX1#ov| z?|6y81oX5D@;tgHS!y9CQ)O9Wwm7h~=U za-yqHCV8NJNF*_m?7v@#04IOz21w1mg7x@D0N6X#AP6Cq5Qb?_5GndGuWv~#uaGgu zU}d1r0}sC~w06nWBPrj2!sDF2JOEEhHf6EY8RBjSP>-)#obhxt-$lZ|KMj0R zu`Pv(;leB^5b2!p;ft$GaaOCeVVUxst*;}N0XULGdrA~weMOLt4>LNype-wZ{V|$3BgH2Fkp(38Y<-yG~`8# zbyO^BUK~ox0@Ry|WC7fC>SsuR4(qqg(7M4j`zWdEpn+wx6H(of1u&LM;+RDgOE}!N zWP0ik*ngt0qfv;rk`!r)d(Mu;GHGwKt%5Nuy`!@%~>g`BERQ6H4NuC?Ypb$bR zeJ;GT7ZG`8CoXc|$`nq8T*}gfTn2kdIY0u^7c*i+=f;r9Ww1z*jwOP9#HH&P5=L}J zOi`IQ779d4dd4ufP_UU!3l7u)FAzM329QyCX_B~7o3%sdg7Jz{QC5P@L(5hDbD1!C?+gv#U%X&_6OjpJS}LafHyM4cO93Fj zZ)Jcmvm(4>qYk_2!WUcS77D1c=k%1b_ZJUtNdYo)1{6BZS@0IUUx=OJNJ#8g0_URz z+aoX8a7(`}(+`qpd`XyyerE=OVHFLv32MwX(ykyfBz(}r?Qn>zzaMh9J{V#7Gjax_ z_w)ly*m|RtDq))a&N$$KTCeVO><|D~K%i)?fI~LQ)w%rJ8Oo4%n%fWK^ts_jVNzJ< z^pPo42V<$ZKKT}}{k)ryJUsL(h!Z;!)820s^2L7XvLrgYsgi&Un)N_&og5t(V46yG4ST%9r3E?!5hzcqa~x{)zV7IGG28=%%zG^4 zSccS&E)T#F{am_<_dX&mn)(KQ6aRyszs%8Zoik9a6h0)@?^_!{*c;dMp)8hju@tb2!1NnhYyVZ4)m7ZjC{9ra_q<^F#+y{;%4mN{fyqK}UrQl%9-XL0`UeHEfJ&5S zYbwI1CXpXmE~_2#|j%} zyE-&%Rf-2~ffW845ZzN={fZNCPJ}_uG2|M3NQZ#QPt;=l+9Yse9uP>MrRCberpiAG$%$aOlrJNZ51{^k#qtoY2TQI6lRLdmhK z)#C-zjxN+hGDnhB!o)JviAbVy&_(4TYMOa$n7ro)$QDv;#Xj;CC?+ln9Vvazuv)3p z!DTp>1cl5(umW%z2t>M@7J6y?94>*iV{l-(Jp2`$CP)s9DMd)nrii0P2xnPnwmUpCC5B$kj24p}D~R>@13k zFnimxfRBBFbf?g}Iv@KYS?5$>g{VhfIJ3RO3lN<=NWPaedX-8F+n2pu8x{=>RXd4} zI!avHo+Abt2{INta+f{^I(;2rEp)}%go8%W;YkOe@>R-dQJ_wW=q2FnEx^phV3aam z7ui*4^UG~~gR-m|UDO){lvVHaI43k^bUg96N~srM;CDo5fQ|Nu)R>WeD2$g6i5Yhe zBckZa6_3Egn&ON&XY2>R0IxH~1IBv%qHN4IVoyF1QCL`V9E_(4Yri>ULLFg9P(u;WUoJ`39s)T;S=Lg4nzCkMDM-MYxBj=ta_^>CYgdVg@ zIvp6vlWOD}!q$IuIp`y`77+m1D{M2>HF%n{pTH60f%-Ed2bgA9tix zgjT?nl?xcljg7z#NfWt*gv9tEeS~yml1RCkvW{*5cvw;BXx&iN1Pq`AvSvS+yjK~E z><3Vm5wQtGe^8ij+DH4u{)pe=Q?!NCLwHj!d+e9eJauT7gpa;~z10%@K zyIX@+R2vrqQa|Ykdy$CtoIp$gX(+RO1(7}%%zg17&DKZN#fLq5%-tjxG7c@{(A|#M49;ng!sH*s|caO<_Gq0x5SeKAt=RIvsk0}8m=0M}j_&o`=KYEgomg&@w$f{6@0YVzzaYFY(G*xj5-p`p^}r@0yPWsYlMqmS_?aZKMiG?}a5)V%$GbSifOyk^6RW_l1`Olat}i(* zgaOfB-01$$VU8{N#k{w4V_MzQ5{?9Z4=rMU-W;Lh9=ig}a>#JOq01#9U!M!MM?cv9%;9+Mzldv(C86epxAOkgo#wX{f zgY;w@^4!Wq4v!P-=*xhj#bYo4&d0NkFkXI0yAPcHu%R4V@`_9EqtlIPT@&qpxP^8i znr2vk)>u1cVYg|kx)h^?_LYoZM*M;Rh^S58y}wD{Apn0I@E2}pMu-D3*4~I8;|&Hp zF{6l(5J660*gIf^dwYb|l`-2zBFBb0d$Kz`@!5!oa*_w`hs~>kf!`N$UG>tcd<-gV zma|MYM{1aH-&l>qq{U;rK3H?-#qAqN{#>R0#873*4+mIu9hgbt-(c`M;;IVsAe1+B zhUGE56PRie0UgqlgGt8kxX&tUryq4M zjP1?V*YAF^#SHO#g#qz0fbnLhNP4MXa;ebn`YFx43mqeyd3OUkQ2BP_>`>Bi__p(U zMP_J}$ay1zn+PxCmVyYb0+8g-f2k)n5{2={fjPPzk|f|<7rM|nMg_wS1S26VkcPON z07nb0FX4QTDk+-J`flk}!wqxt%V+*#V|0hf#Aq>dV>CH;dlpbG$6x7kd`1icL+WST z*{T8tR)-DsVt}rm1rl#rPo%IJ-&~ieBo}d)4#9{_fRUfPCa6bmd^GL^!G4m=_#mhW zZXZbOeLK8R zr>WcBZtL1wWMsx@?zDr5%!u*^jxNM!=90@2ZGv1G)d8DhAS=csAzymkyVbzecY5@Q zd3QtXDjD?!+Xow87FoErpQe=GnF!CL}}~-=nAw=jFg<9W~+ZSrOjIsCcNOi6E}WvP>UJP2hK9(i{kT(b5Or zH&IekhxvHsRg`%}3fwAa{8j-en;aD~7ZNE#P$i$(Z%Bs{;_pI{6c!zumCGyzQ*vae zoQV}We-52!5$Vkxf11g|MC;}a?4K3_7gJo79#lf%3Sk%FKnF>#@$DD9xjT@g*C zO5Ef%Zh=Tc{tt;T!r2Z{^?madwjLIwr8zVVy=m6tp%)S-8JAfB|59UtggOVhT`ps) zNzI*L=WsOTux<*7iWNpE5OH-5DpGOIypVlR=b+_4B8$msdCc@=7;=*RzF$iRSy84c zl;?$MXOl9Nj%oVDaO%itsBnw92%#8Icg$-LDG2D7odjAi-_dUVF+62Dpm+-N9e$Z1 z%&zfbdwM9Guzl`}_F@ITOxu48 zYS|E(DA`1x2>lmeJJ&Fc_UM?L@LnUcM+(W`7&24O(l1DehCfR?0sPiCpw~O!ArYx_ zVX^qg7B>!`c}#_fP+_)XhFJ(JpbGQNaKZK7pA`3$BlLSaJz=a&=o8UXiVmv^NzA^jVk*YG1{?H?P(s&&{xYYwHRN zpC0#cb$XX(;A%!9G3qK`sCe0Ie z!V@r-i*JPc82}X+A(dpBsy)V=38|3;gf;ctP&o`sj?FJFMp%*ed*!~pz1No-OVLT79gm7& zv8ybImVTj*63C&3z=Y3wMoCZb_^vQnTqVtKWRaXBL-IgnLLWE`fuI(>bFS|&S+^4v zD^oC$K`}ulE7(nkedCH5;BdiZ*QJd(P%w)Rh4T^!_4?$pA&~BvnIn6s<2OfNFiRyJ z2)Z*0{Pv3oWZlR(TtIaO2tJzY&z+9MiMYf<$M2+G#F{t&gY1;M4@^ZvJ1Iy<3e{w$ET-Il%I?)8?;JGyu^@XMDk@4ZbZZEkV41Cv)`H8-Np>^5L%9Z z0Oj~W0Q9WRirc9nDAFHjFaFq`_z*Aexy+5gHWi49>3tl=SzuBr(HHI4AEf(q5F4U6 z;2t9)Dr#aIGZ^W-Fx9_CRMr6lkZy&S+_<`C1`VsA>gYaDGv|_(jX@-_Q!_(*FiHEf z?1(IM`Pd;YFTwunQ?4Dkd2}xd`$VL4K4uN*0EoU>EKtJ@n*dEvfQM-pS6HDorvYXA z!6<+_z-YZchV=gBE=o&a1N0u zaQwtpo)H*QQ+-K}1cgl}63RXEPEb@DU25OjT`e4KI=3%6Q6{X1q=Qa(>g_@%X7U8MU?dxzWSk9H;4tO>cpC?^^-rKS+}EkTrUzuE4!3_3!}9+ z?huA1L1Ki39N*zXrCvtu%35-g!)*~o3l2NNDLa;>R5O-b4wZECFrHY)oGvj));DS) zbfE*WMrMt@&bEuo@o?K^XG{F3*5KN8Jw8`G*#?%Hfw<_(^+09+!S#5!Qle4KtXVm6 zDAuhko&aYo5H5EvLfd~#S`aCmkn^cgh{Pp3A{{Dgf*d%hC>srw>C|gkt&i$WgJwqB|qkp+(JUNplPbZliAV z&-RMKwMb_4kTnQAEMD%D-n}QqITy&@rZ1$q5YqGEJzc_PAXm(KD3e3Nd!dY1o3T*% zGOfkxJ7Y0S6;?Ty*$tcR6_8nJ$ZTB%J1Qb{Bq(%CO?)i&c+^2*MH=fE=#ap#Saz(B~fwL83F}y@s>IoOle^KN#pW0-MEpb;F zsv=Wbn0pCz3VStSBJVuGOx7#rYSFPp4Ea}pck$}no9BHL-+!|Abr>+hqT=_kklcP} zwuZb*hF#SEE+QoBYDnZN$Tin07bVcuh<`*rxxmo(hjXw<9uMM;0{FLui^$N$cTx2x zpY6~q5d!k!u~F|WWxQxRlaq=8^X^)VcrOO@GUpZI-QbX{J()MeVkFNeL7^b#$^r6{ z|BTd($PhI7>>g&`9W5CD3Ub}Ok{6f#&Ma3Jw-E{Eje*3A*Re1(bWR@FSq^$D3+P`_ zfm_IyaFd|%erc_g|J1Hp3!MpfKM#4m3_2&7{@@g=2pdu#A8GigGdo)6cJv~P6=rr1 zGbL11flu(g6{{VRr)b-ceqIA65c1xDSAy=rp)y4kWa?O_^fB*bQy)UI&AjKb?!{%x zX1zC`XciGa!uEScM&;hxQ@0Z-47_!5ddL8IE7OpZH zdtV>=N$jE!0SZKOGZciXWk8CmRA+T}pd1hEceh(u8&dsqQD=o)8vqC2u5sRC>hWBC zg?7LefKPO!9IzWzqT9_BY3K<#CXbW@EHS(NrX_b8OLwC&JN0Spk4SvVI{6Xp$mgmP zWJD5`*5eDa27_B30-yHXGm|*{s3H^3o@|jI{1M`);wkLC|D#`^?^`UcwLXayPF#pj z+eclJFU1S_+Zl*zD6LdE?>Zp=2}b;fXKq@8?$b~|P58?c8D(GXuwU*n6IfX}Ok zb$g|nmdKe>CbKW&o<$Z-3ocOyBn=Tb8i0u^ zLx@xe#eIcEA!!PfutQJ1vn%EaZV}i9!?YL=V*_^)I#X|fFX@4D52OluYQO{174`TW z?nGZv0`znOI+87A!a_u&SAoNf zf>#njB; z+YNeXrEIY&oP&+bwdoS0(!W|_o<#2(@H09L7-Nq6FUJO{L(pVi59$=x1Cl7Il|yUnPvB55 z4>x^s12`q^!{d3;eMSZXW^`x@`Eo`Cv=JGTm(-UP<0ZK2m7iYV17nk1NT(Cc(X zxU}UX;3}L#b8EvC-7RCNhDp}}JmI`@%s((;o=+mgsl+Qx(YR$C*0a6S`vwgs{Gwar zjAsx{X0CA|ZL1E^4^ntelZOv?vI_YHflASlV;eqWxFEhVvN8Gp5xDv~vw6pyn~`tA zgZytEp|-zRz!w&M+4Nw%5z5-tH;bk#Xi#IiPI>=zlMyR$~2M+AnK?9sjF+Ne>PjK}ac#xWw_bi;7( zXzhOKfcZT#q;wVyf+wW#nn)!Q(HWTR9vL3~6JDwKRlFV$7oG)yxvn3^m3_ggnX*k3 zW<0cY#tt{4m-vO6{I#|-65MaXDC>I+YEZWAAHL_}H|z~YJg&Rkm3u)4`S*B4%O4_m zSEuYExGOCL!(5&vG~dKwx~8%IfO8T(Fw>MOxcwTtMgSVkZiEEok3L?WS7j2ZGAF<= zRMwVo?+jd8h;|QQVtnY}&)pwnFoQMj9Ac)u%D?hJu}9iMw8{jDN`$D#kcgt!d8IR@ z*n1jq#gA-ZnE9NM+AZKhMw$73B&<%F%LlL=4f0XvB5K*R6Qgg=L8PZTkKV_%2@@a{ zjZum@&?~(|z*X`{Bv1yBfi{MZD6ue?jYm|a9~{KRa>zb2-$c+N3@&7WB zW2TrYrXnD9I}CjUtulfnFLl|-QJ)mw>yIA5&)mcU@+u_>cFD(W2lmZNmz+E58hOpT zlmHG_65KurIRyYAx2qh6A~H0X>mYwUOt_=2)XqydK3W&`+cg4kaG4n6RUU^P#xj#T z5@K%H;hNNTlfFwdA&yK3NRom=V5*tQ|DahFOc&#y^V9e%tNy!hfaae>*D1FT43(_h_PHWy4zX7>dSvE z0W{Yuo_;V{O1W2EJ9R`=t%@F6xZBc=3sq~PZY0)|C_3M2lGNTrkwBvkWuU~IH3GDq zFH~^e5><-aH_Gy=R;3c@di9ug!PTwXn?)Q**RL>);c;ma1<(DVfOY5Q6gHr=U37NI z^5(`a3UFRCEv(1O-LCGN2dqUe%9Sw0TU@;f714B{eXM?a$q7uQ67iNDG(|@oxe5)g z#hJ>Z-S7}4b;4mH^4AhRlt_$ED%uCOJS*L&Wqb`#3`H}ldF|b@B?710J z++*%SX4`!uJ{OqRn^}0UrU2W#qF%KM3Y_Cb9z#Bou^nTXFluHvwiik^|5&}}meLmi zVWo9_4H;qv!d_Kjy@Pr2$pxkRNIvQkS#@~_nHFfK2y1E}`GBIf8j>?1!T+yBlO9o@ox4I?( z>6lI2NS#4Wf1O}E#!_L_Omb}Lg_7bQhxcSD=OQ3NQeJj)SB3Eo=JjVRiG0)rawvc6 z6=v%8@^9mUF*CjVz}{s%_P`T;CJc+3)2yK~$UO)7Mr8&x{10h}4<=O;wV_ey5u}y? zo`76ijsW112_>goTL$5mqq8>UR@Vezol%?!6X8{c;~2|@QSTt>eaTrB*)uf2a!i9YHje(wcWcb48aT8yefyp-= zxZZ0Q;r9vyBff5D-T#ytw{+l-X6JUoKa*&NV*>}p-p0OOVaf}rF%h!^+COeRzgIS< zN-#w1>=d=r7er1nhYb!pBfsN~6^KMr(GQHR!hP??1j0`9l>#U^1az%MW;>+h5XGIl ze@ObrfF`+sB^oN%BZUfPjMbd~&;(5CTs+Beea9(G#o5GewM;;obDON=hYdGVp}Dbz z_Z6ak8N|_m55Rk;Pf;2rXsTdZPZwe$nAr}E93O~mDVp!Yl&3(Lqei$wISV6fvCCGc_}A znmH#GNb1>{YZIt;8y+JVcUupmm$zU?f(}(Bg^o>C>NHf; z6S=Opkd?SEyEKc0!mtP%uTP95FhYOXR|W%M#Off@BU6 zvV+ZXv=PMRqyw2-bSUJTxIK6G40#k7{wX%z2zQQDDtp8(SSig+Hk5h3BdnS;JrnjG{?6QKE2{Z-DB6 zg3yaVNhH@e`2idkXf!$4?uIkIr4xOrS6-R)9EOx^?%M2r(ZfUvGRXgPf;pcdX%Wnu z%%kxZQIEVpW>=+zhuZTbErK zAXo`yeTMX95Si}{;*C>}yh!T4Xoho=11MKV!oWI3iEQ3_(zj5uz1TE@d5tv;?g)eN zU7clF+naiZ3mFpKU3xfrc+WCd$cRyGjL?uTa`-1AhJR#6#sLY0$YxSEgMEgh#c!t7 zLmm?$3_po;%D&V{sh8&?DUhhl-hwplf6;(mlY?I!9E45ttu&4SZmZ3Wj;(h?{9Y0H zZ9&Y_SrM?aClW5?*&Gu2z~7R4ltFlX;sR=#Czu1fu$jJJ=%BJVlaTpdAEM-b;y z}XdS>HL^Df}PXt%zGG4AU zAby3)5Bt78ADGAuvq4+SwE5#{Yl3O@k#w#FtH&360ep(-gaBik)xu#egFGeeq^TTo zh%iC`C0_hd(?x7{S7QvlqE7!sK)^h~fiB#q0U#wups+n$xODT3BLxbb83EDa=s=eQ z{7D%w1_|OVkZn^TGk2H)9YlfU43Q^6oX0I`M0=(Nvd*B5A&M0uxQ ze)22IpWJEA^F8swiO`_Ab+zw}28>R_8(I!K~-VTIp6&THT}Qtq3NU%+Ya+4>v~iYjJY?R1B{ zMTxkbHJ2TEwxc@;M+YTA5yb^Eg5I2F_jEln6nXAObIUmDTo$MA6kYP(q#(i3<-J&_ zJ+#;XeI0rHk!DEH5W((0yc|PA2f4CqxbCGh!784Ua&*l{nk0X?=PO1E^T^(f{|$2f z)*(mD$q8wnaUN+Npw?Fuwqwi->$Z=O7=I;8XOwW%xiCst$a~W!>C*Ms{DRI$$pvZA z>}9^1#C*Bq@Dhzb8p^dOc25~DDgICaZa!l+=tt)~{^ASiH6w*tq;e7NZDp7fO2?js zJ6-p9X0EASM=cdypE>4&)8SY0j#?m_^d+i0A+8mF9M*^~`JGc*%D~5nRjwI+^Nfap z=V)~FqR`yy3b_}THvWz>lju^~p^^(VH1mex`(CJXcC3pp~u<**a-3>c45lC+UnLxGx;E7I%3j+J< zz@T*9yYNb4vNkto2&hR{0Mvy<2?flMPt0VBuE67jeq|Dc0`N1W5{Ud>5ko{4l?~|& zWK%Ow!n-+VTxI}PG!q&jA$&s6IkQA;(if$06xW659pq&bV`N?KF7(1!|Bnopgq