From afef1552b82658bd222620810134b21b717a64ac Mon Sep 17 00:00:00 2001 From: mdecimus Date: Thu, 7 Nov 2024 16:32:54 +0100 Subject: [PATCH] v0.10.6 --- CHANGELOG.md | 14 ++ Cargo.lock | 28 ++-- crates/cli/Cargo.toml | 2 +- crates/common/Cargo.toml | 2 +- crates/common/src/config/mod.rs | 3 +- crates/common/src/enterprise/config.rs | 83 ++++++++++-- crates/common/src/enterprise/license.rs | 164 ++++++++++++++++-------- crates/common/src/manager/mod.rs | 44 +++++-- crates/directory/Cargo.toml | 2 +- crates/imap/Cargo.toml | 2 +- crates/jmap-proto/Cargo.toml | 2 +- crates/jmap/Cargo.toml | 2 +- crates/jmap/src/services/housekeeper.rs | 34 +++-- crates/main/Cargo.toml | 2 +- crates/managesieve/Cargo.toml | 2 +- crates/nlp/Cargo.toml | 2 +- crates/pop3/Cargo.toml | 2 +- crates/smtp/Cargo.toml | 2 +- crates/store/Cargo.toml | 2 +- crates/trc/Cargo.toml | 2 +- crates/utils/Cargo.toml | 2 +- 21 files changed, 283 insertions(+), 115 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c387fafe0..a7f137582 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [0.10.6] - 2024-11-07 + +To upgrade replace the `stalwart-mail` binary and then upgrade to the latest web-admin. + +### Added +- Enterprise license automatic renewals before expiration (disabled by default). +- Allow to LDAP search using bind dn instead of auth bind connection when bind auth is enabled (#873) + +### Changed + +### Fixed +- Include `preferred_username` and `email` in OIDC `id_token`. +- Verify roles and permissions when creating or modifying accounts (#874) + ## [0.10.5] - 2024-10-15 To upgrade replace the `stalwart-mail` binary. diff --git a/Cargo.lock b/Cargo.lock index e78a971f3..34ed3f578 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1051,7 +1051,7 @@ dependencies = [ [[package]] name = "common" -version = "0.10.5" +version = "0.10.6" dependencies = [ "aes-gcm-siv", "ahash 0.8.11", @@ -1667,7 +1667,7 @@ dependencies = [ [[package]] name = "directory" -version = "0.10.5" +version = "0.10.6" dependencies = [ "ahash 0.8.11", "argon2", @@ -3010,7 +3010,7 @@ checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" [[package]] name = "imap" -version = "0.10.5" +version = "0.10.6" dependencies = [ "ahash 0.8.11", "common", @@ -3222,7 +3222,7 @@ dependencies = [ [[package]] name = "jmap" -version = "0.10.5" +version = "0.10.6" dependencies = [ "aes", "aes-gcm", @@ -3303,7 +3303,7 @@ dependencies = [ [[package]] name = "jmap_proto" -version = "0.10.5" +version = "0.10.6" dependencies = [ "ahash 0.8.11", "fast-float", @@ -3654,7 +3654,7 @@ dependencies = [ [[package]] name = "mail-server" -version = "0.10.5" +version = "0.10.6" dependencies = [ "common", "directory", @@ -3673,7 +3673,7 @@ dependencies = [ [[package]] name = "managesieve" -version = "0.10.5" +version = "0.10.6" dependencies = [ "ahash 0.8.11", "bincode", @@ -3951,7 +3951,7 @@ dependencies = [ [[package]] name = "nlp" -version = "0.10.5" +version = "0.10.6" dependencies = [ "ahash 0.8.11", "bincode", @@ -4502,7 +4502,7 @@ dependencies = [ [[package]] name = "pop3" -version = "0.10.5" +version = "0.10.6" dependencies = [ "common", "directory", @@ -6059,7 +6059,7 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smtp" -version = "0.10.5" +version = "0.10.6" dependencies = [ "ahash 0.8.11", "bincode", @@ -6175,7 +6175,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "stalwart-cli" -version = "0.10.5" +version = "0.10.6" dependencies = [ "clap", "console", @@ -6206,7 +6206,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "store" -version = "0.10.5" +version = "0.10.6" dependencies = [ "ahash 0.8.11", "arc-swap", @@ -6850,7 +6850,7 @@ dependencies = [ [[package]] name = "trc" -version = "0.10.5" +version = "0.10.6" dependencies = [ "ahash 0.8.11", "base64 0.22.1", @@ -7093,7 +7093,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "utils" -version = "0.10.5" +version = "0.10.6" dependencies = [ "ahash 0.8.11", "base64 0.22.1", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 9c471be9a..be81eb468 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Stalwart Labs Ltd. "] license = "AGPL-3.0-only OR LicenseRef-SEL" repository = "https://github.com/stalwartlabs/cli" homepage = "https://github.com/stalwartlabs/cli" -version = "0.10.5" +version = "0.10.6" edition = "2021" readme = "README.md" resolver = "2" diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index b6802c682..cbc887ae4 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "common" -version = "0.10.5" +version = "0.10.6" edition = "2021" resolver = "2" diff --git a/crates/common/src/config/mod.rs b/crates/common/src/config/mod.rs index 692ccdb46..570324772 100644 --- a/crates/common/src/config/mod.rs +++ b/crates/common/src/config/mod.rs @@ -74,7 +74,8 @@ impl Core { // SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd // SPDX-License-Identifier: LicenseRef-SEL #[cfg(feature = "enterprise")] - let enterprise = crate::enterprise::Enterprise::parse(config, &stores, &data).await; + let enterprise = + crate::enterprise::Enterprise::parse(config, &config_manager, &stores, &data).await; #[cfg(feature = "enterprise")] let is_enterprise = enterprise.is_some(); diff --git a/crates/common/src/enterprise/config.rs b/crates/common/src/enterprise/config.rs index f79b06974..60403013e 100644 --- a/crates/common/src/enterprise/config.rs +++ b/crates/common/src/enterprise/config.rs @@ -17,30 +17,93 @@ use trc::{EventType, MetricType, TOTAL_EVENT_COUNT}; use utils::config::{ cron::SimpleCron, utils::{AsKey, ParseValue}, - Config, + Config, ConfigKey, }; -use crate::expr::{tokenizer::TokenMap, Expression}; +use crate::{ + expr::{tokenizer::TokenMap, Expression}, + manager::config::ConfigManager, +}; use super::{ - license::LicenseValidator, llm::AiApiConfig, AlertContent, AlertContentToken, AlertMethod, + license::LicenseKey, llm::AiApiConfig, AlertContent, AlertContentToken, AlertMethod, Enterprise, MetricAlert, MetricStore, TraceStore, Undelete, }; impl Enterprise { - pub async fn parse(config: &mut Config, stores: &Stores, data: &Store) -> Option { - let license = match LicenseValidator::new() - .try_parse(config.value("enterprise.license-key")?) - .and_then(|key| { - key.into_validated_key(config.value("lookup.default.hostname").unwrap_or_default()) - }) { - Ok(key) => key, + pub async fn parse( + config: &mut Config, + config_manager: &ConfigManager, + stores: &Stores, + data: &Store, + ) -> Option { + let server_hostname = config.value("lookup.default.hostname")?; + let mut update_license = None; + + let license_result = match ( + config.value("enterprise.license-key"), + config.value("enterprise.api-key"), + ) { + (Some(license_key), maybe_api_key) => { + match (LicenseKey::new(license_key, server_hostname), maybe_api_key) { + (Ok(license), Some(api_key)) if license.is_near_expiration() => Ok(license + .try_renew(api_key) + .await + .map(|result| { + update_license = Some(result.encoded_key); + result.key + }) + .unwrap_or(license)), + (Ok(license), None) => Ok(license), + (Err(_), Some(api_key)) => LicenseKey::invalid(server_hostname) + .try_renew(api_key) + .await + .map(|result| { + update_license = Some(result.encoded_key); + result.key + }), + (maybe_license, _) => maybe_license, + } + } + (None, Some(api_key)) => LicenseKey::invalid(server_hostname) + .try_renew(api_key) + .await + .map(|result| { + update_license = Some(result.encoded_key); + result.key + }), + (None, None) => { + return None; + } + }; + + // Report error + let license = match license_result { + Ok(license) => license, Err(err) => { config.new_build_warning("enterprise.license-key", err.to_string()); return None; } }; + // Update the license if a new one was obtained + if let Some(license) = update_license { + config + .keys + .insert("enterprise.license-key".to_string(), license.clone()); + if let Err(err) = config_manager + .set([ConfigKey { + key: "enterprise.license-key".to_string(), + value: license.to_string(), + }]) + .await + { + trc::error!(err + .caused_by(trc::location!()) + .details("Failed to update license key")); + } + } + match data .count_principals(None, Type::Individual.into(), None) .await diff --git a/crates/common/src/enterprise/license.rs b/crates/common/src/enterprise/license.rs index 136dcfd22..d1a8f6517 100644 --- a/crates/common/src/enterprise/license.rs +++ b/crates/common/src/enterprise/license.rs @@ -21,21 +21,26 @@ use std::{ fmt::{Display, Formatter}, - time::{Duration, SystemTime}, + time::Duration, }; -use ring::signature::{Ed25519KeyPair, UnparsedPublicKey, ED25519}; +use hyper::{header::AUTHORIZATION, HeaderMap}; +use ring::signature::{UnparsedPublicKey, ED25519}; use base64::{engine::general_purpose::STANDARD, Engine}; +use store::write::now; +use trc::ServerEvent; + +use crate::manager::fetch_resource; + +//const LICENSING_API: &str = "https://localhost:444/api/license/"; +const LICENSING_API: &str = "https://license.stalw.art/api/license/"; +const RENEW_THRESHOLD: u64 = 60 * 60 * 24 * 4; // 4 days pub struct LicenseValidator { public_key: UnparsedPublicKey>, } -pub struct LicenseGenerator { - key_pair: Ed25519KeyPair, -} - #[derive(Debug, Clone)] pub struct LicenseKey { pub valid_to: u64, @@ -47,11 +52,18 @@ pub struct LicenseKey { #[derive(Debug)] pub enum LicenseError { Expired, + InvalidDomain { domain: String }, DomainMismatch { issued_to: String, current: String }, Parse, Validation, Decode, InvalidParameters, + RenewalFailed { reason: String }, +} + +pub struct RenewedLicense { + pub key: LicenseKey, + pub encoded_key: String, } const U64_LEN: usize = std::mem::size_of::(); @@ -142,68 +154,102 @@ impl LicenseValidator { } impl LicenseKey { - pub fn new(domain: String, accounts: u32, expires_in: u64) -> Self { - let now = SystemTime::UNIX_EPOCH - .elapsed() - .unwrap_or_default() - .as_secs(); + pub fn new( + license_key: impl AsRef, + hostname: impl AsRef, + ) -> Result { + LicenseValidator::new() + .try_parse(license_key) + .and_then(|key| { + let local_domain = Self::base_domain(hostname)?; + let license_domain = Self::base_domain(&key.domain)?; + if local_domain == license_domain { + Ok(key) + } else { + Err(LicenseError::DomainMismatch { + issued_to: license_domain, + current: local_domain, + }) + } + }) + } + + pub fn invalid(domain: impl AsRef) -> Self { LicenseKey { - valid_from: now - 300, - valid_to: now + expires_in + 300, - domain, - accounts, + valid_from: 0, + valid_to: 0, + domain: Self::base_domain(domain).unwrap_or_default(), + accounts: 0, } } - pub fn expires_in(&self) -> Duration { - Duration::from_secs( - self.valid_to.saturating_sub( - SystemTime::UNIX_EPOCH - .elapsed() - .unwrap_or_default() - .as_secs(), - ), - ) + pub async fn try_renew(&self, api_key: &str) -> Result { + let mut headers = HeaderMap::new(); + headers.insert( + AUTHORIZATION, + format!("Bearer {api_key}") + .parse() + .map_err(|_| LicenseError::Validation)?, + ); + + trc::event!( + Server(ServerEvent::Licensing), + Details = "Attempting to renew Enterprise license from license.stalw.art", + ); + + match fetch_resource(&format!("{}{}", LICENSING_API, self.domain), headers.into()) + .await + .and_then(|bytes| { + String::from_utf8(bytes) + .map_err(|_| String::from("Failed to UTF-8 decode server response")) + }) { + Ok(encoded_key) => match LicenseKey::new(&encoded_key, &self.domain) { + Ok(key) => Ok(RenewedLicense { key, encoded_key }), + Err(err) => { + trc::event!( + Server(ServerEvent::Licensing), + Details = "Failed to decode license renewal", + Reason = err.to_string(), + ); + Err(err) + } + }, + Err(err) => { + trc::event!( + Server(ServerEvent::Licensing), + Details = "Failed to renew Enterprise license", + Reason = err.clone(), + ); + Err(LicenseError::RenewalFailed { reason: err }) + } + } } - pub fn is_expired(&self) -> bool { - let now = SystemTime::UNIX_EPOCH - .elapsed() - .unwrap_or_default() - .as_secs(); - now >= self.valid_to || now < self.valid_from + pub fn is_near_expiration(&self) -> bool { + let now = now(); + self.valid_to.saturating_sub(now) <= RENEW_THRESHOLD } - pub fn into_validated_key(self, hostname: impl AsRef) -> Result { - let local_domain = psl::domain_str(hostname.as_ref()).unwrap_or("invalid-hostname"); - let license_domain = psl::domain_str(&self.domain).expect("Invalid license domain"); - if local_domain != license_domain { - Err(LicenseError::DomainMismatch { - issued_to: license_domain.to_string(), - current: local_domain.to_string(), - }) - } else { - Ok(self) - } + pub fn expires_in(&self) -> Duration { + Duration::from_secs(self.valid_to.saturating_sub(now())) } -} -impl LicenseGenerator { - pub fn new(pkcs8_der: impl AsRef<[u8]>) -> Self { - Self { - key_pair: Ed25519KeyPair::from_pkcs8(pkcs8_der.as_ref()).unwrap(), - } + pub fn renew_in(&self) -> Duration { + Duration::from_secs(self.valid_to.saturating_sub(now() + RENEW_THRESHOLD)) + } + + pub fn is_expired(&self) -> bool { + let now = now(); + now >= self.valid_to || now < self.valid_from } - pub fn generate(&self, key: LicenseKey) -> String { - let mut bytes = Vec::new(); - bytes.extend_from_slice(&key.valid_from.to_le_bytes()); - bytes.extend_from_slice(&key.valid_to.to_le_bytes()); - bytes.extend_from_slice(&key.accounts.to_le_bytes()); - bytes.extend_from_slice(&(key.domain.len() as u32).to_le_bytes()); - bytes.extend_from_slice(key.domain.as_bytes()); - bytes.extend_from_slice(self.key_pair.sign(&bytes).as_ref()); - STANDARD.encode(&bytes) + pub fn base_domain(domain: impl AsRef) -> Result { + let domain = domain.as_ref(); + psl::domain_str(domain) + .map(|d| d.to_string()) + .ok_or(LicenseError::InvalidDomain { + domain: domain.to_string(), + }) } } @@ -221,6 +267,12 @@ impl Display for LicenseError { "License issued to domain {issued_to:?} does not match {current:?}", ) } + LicenseError::InvalidDomain { domain } => { + write!(f, "Invalid domain {domain:?}") + } + LicenseError::RenewalFailed { reason } => { + write!(f, "Failed to renew license: {reason}") + } } } } diff --git a/crates/common/src/manager/mod.rs b/crates/common/src/manager/mod.rs index a58ab779e..e62f76940 100644 --- a/crates/common/src/manager/mod.rs +++ b/crates/common/src/manager/mod.rs @@ -6,6 +6,8 @@ use std::time::Duration; +use hyper::HeaderMap; + use crate::USER_AGENT; use self::config::ConfigManager; @@ -32,35 +34,57 @@ impl ConfigManager { format!("Failed to fetch configuration key 'resource.{resource_id}': {err}",) })? { - fetch_resource(&url).await + fetch_resource(&url, None).await } else { match resource_id { - "spam-filter" => fetch_resource(DEFAULT_SPAMFILTER_URL).await, - "webadmin" => fetch_resource(DEFAULT_WEBADMIN_URL).await, + "spam-filter" => fetch_resource(DEFAULT_SPAMFILTER_URL, None).await, + "webadmin" => fetch_resource(DEFAULT_WEBADMIN_URL, None).await, _ => Err(format!("Unknown resource: {resource_id}")), } } } } -async fn fetch_resource(url: &str) -> Result, String> { +pub async fn fetch_resource(url: &str, headers: Option) -> Result, String> { if let Some(path) = url.strip_prefix("file://") { tokio::fs::read(path) .await .map_err(|err| format!("Failed to read {path}: {err}")) } else { - reqwest::Client::builder() + let response = reqwest::Client::builder() .timeout(Duration::from_secs(60)) + .danger_accept_invalid_certs(is_localhost_url(url)) .user_agent(USER_AGENT) .build() .unwrap_or_default() .get(url) + .headers(headers.unwrap_or_default()) .send() .await - .map_err(|err| format!("Failed to fetch {url}: {err}"))? - .bytes() - .await - .map_err(|err| format!("Failed to fetch {url}: {err}")) - .map(|bytes| bytes.to_vec()) + .map_err(|err| format!("Failed to fetch {url}: {err}"))?; + + if response.status().is_success() { + response + .bytes() + .await + .map_err(|err| format!("Failed to fetch {url}: {err}")) + .map(|bytes| bytes.to_vec()) + } else { + let code = response.status().canonical_reason().unwrap_or_default(); + let reason = response.text().await.unwrap_or_default(); + + Err(format!( + "Failed to fetch {url}: Code: {code}, Details: {reason}", + )) + } } } + +pub fn is_localhost_url(url: &str) -> bool { + url.split_once("://") + .map(|(_, url)| url.split_once('/').map_or(url, |(host, _)| host)) + .map_or(false, |host| { + let host = host.rsplit_once(':').map_or(host, |(host, _)| host); + host == "localhost" || host == "127.0.0.1" || host == "[::1]" + }) +} diff --git a/crates/directory/Cargo.toml b/crates/directory/Cargo.toml index d34ca68a5..b38c63a36 100644 --- a/crates/directory/Cargo.toml +++ b/crates/directory/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "directory" -version = "0.10.5" +version = "0.10.6" edition = "2021" resolver = "2" diff --git a/crates/imap/Cargo.toml b/crates/imap/Cargo.toml index cd61f8da9..c3a08d7c2 100644 --- a/crates/imap/Cargo.toml +++ b/crates/imap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "imap" -version = "0.10.5" +version = "0.10.6" edition = "2021" resolver = "2" diff --git a/crates/jmap-proto/Cargo.toml b/crates/jmap-proto/Cargo.toml index 299c41cb8..ac33703b4 100644 --- a/crates/jmap-proto/Cargo.toml +++ b/crates/jmap-proto/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jmap_proto" -version = "0.10.5" +version = "0.10.6" edition = "2021" resolver = "2" diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml index 50ffd8a1b..db8c28e33 100644 --- a/crates/jmap/Cargo.toml +++ b/crates/jmap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jmap" -version = "0.10.5" +version = "0.10.6" edition = "2021" resolver = "2" diff --git a/crates/jmap/src/services/housekeeper.rs b/crates/jmap/src/services/housekeeper.rs index 4387146ae..fa65dddb9 100644 --- a/crates/jmap/src/services/housekeeper.rs +++ b/crates/jmap/src/services/housekeeper.rs @@ -50,7 +50,7 @@ enum ActionClass { #[cfg(feature = "enterprise")] AlertMetrics, #[cfg(feature = "enterprise")] - ValidateLicense, + RenewLicense, } #[derive(Default)] @@ -123,8 +123,8 @@ pub fn spawn_housekeeper(inner: Arc, mut rx: mpsc::Receiver, mut rx: mpsc::Receiver, mut rx: mpsc::Receiver { + ActionClass::RenewLicense => { match server.reload().await { Ok(result) => { if let Some(new_core) = result.new_core { if let Some(enterprise) = &new_core.enterprise { + let renew_in = + if enterprise.license.is_near_expiration() { + // Something went wrong during renewal, try again in 1 day or 1 hour, + // depending on the time left on the license + if enterprise.license.expires_in() + < Duration::from_secs(86400) + { + Duration::from_secs(3600) + } else { + Duration::from_secs(86400) + } + } else { + enterprise.license.renew_in() + }; + queue.schedule( - Instant::now() - + enterprise.license.expires_in(), - ActionClass::ValidateLicense, + Instant::now() + renew_in, + ActionClass::RenewLicense, ); } diff --git a/crates/main/Cargo.toml b/crates/main/Cargo.toml index 72a3c692f..3056a5b3d 100644 --- a/crates/main/Cargo.toml +++ b/crates/main/Cargo.toml @@ -7,7 +7,7 @@ homepage = "https://stalw.art" keywords = ["imap", "jmap", "smtp", "email", "mail", "server"] categories = ["email"] license = "AGPL-3.0-only OR LicenseRef-SEL" -version = "0.10.5" +version = "0.10.6" edition = "2021" resolver = "2" diff --git a/crates/managesieve/Cargo.toml b/crates/managesieve/Cargo.toml index 3ac56bbd6..ea5a25e77 100644 --- a/crates/managesieve/Cargo.toml +++ b/crates/managesieve/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "managesieve" -version = "0.10.5" +version = "0.10.6" edition = "2021" resolver = "2" diff --git a/crates/nlp/Cargo.toml b/crates/nlp/Cargo.toml index 2dfd51196..7e776830a 100644 --- a/crates/nlp/Cargo.toml +++ b/crates/nlp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nlp" -version = "0.10.5" +version = "0.10.6" edition = "2021" resolver = "2" diff --git a/crates/pop3/Cargo.toml b/crates/pop3/Cargo.toml index 534673ce2..eb8f788cf 100644 --- a/crates/pop3/Cargo.toml +++ b/crates/pop3/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pop3" -version = "0.10.5" +version = "0.10.6" edition = "2021" resolver = "2" diff --git a/crates/smtp/Cargo.toml b/crates/smtp/Cargo.toml index 0f0cf9854..06d485d0b 100644 --- a/crates/smtp/Cargo.toml +++ b/crates/smtp/Cargo.toml @@ -7,7 +7,7 @@ homepage = "https://stalw.art/smtp" keywords = ["smtp", "email", "mail", "server"] categories = ["email"] license = "AGPL-3.0-only OR LicenseRef-SEL" -version = "0.10.5" +version = "0.10.6" edition = "2021" resolver = "2" diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index 1ce9d391f..5fb55576f 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "store" -version = "0.10.5" +version = "0.10.6" edition = "2021" resolver = "2" diff --git a/crates/trc/Cargo.toml b/crates/trc/Cargo.toml index 6e9eaafca..acd7bf9b3 100644 --- a/crates/trc/Cargo.toml +++ b/crates/trc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "trc" -version = "0.10.5" +version = "0.10.6" edition = "2021" resolver = "2" diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 6d1b2e40b..f6596345d 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "utils" -version = "0.10.5" +version = "0.10.6" edition = "2021" resolver = "2"