From b6192bc52f3e01e7186995bed32a43be10d2cbac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 14 Apr 2026 11:54:59 +0200 Subject: [PATCH 01/46] add migration for core client certificates --- .../20260414120000_[2.0.0]_core_grpc_cert.down.sql | 12 ++++++++++++ .../20260414120000_[2.0.0]_core_grpc_cert.up.sql | 12 ++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 migrations/20260414120000_[2.0.0]_core_grpc_cert.down.sql create mode 100644 migrations/20260414120000_[2.0.0]_core_grpc_cert.up.sql diff --git a/migrations/20260414120000_[2.0.0]_core_grpc_cert.down.sql b/migrations/20260414120000_[2.0.0]_core_grpc_cert.down.sql new file mode 100644 index 000000000..a0d1ea4d2 --- /dev/null +++ b/migrations/20260414120000_[2.0.0]_core_grpc_cert.down.sql @@ -0,0 +1,12 @@ +ALTER TABLE gateway RENAME COLUMN certificate_serial TO certificate; +ALTER TABLE proxy RENAME COLUMN certificate_serial TO certificate; + +ALTER TABLE gateway + DROP COLUMN IF EXISTS core_client_cert_der, + DROP COLUMN IF EXISTS core_client_cert_key_der, + DROP COLUMN IF EXISTS core_client_cert_expiry; + +ALTER TABLE proxy + DROP COLUMN IF EXISTS core_client_cert_der, + DROP COLUMN IF EXISTS core_client_cert_key_der, + DROP COLUMN IF EXISTS core_client_cert_expiry; diff --git a/migrations/20260414120000_[2.0.0]_core_grpc_cert.up.sql b/migrations/20260414120000_[2.0.0]_core_grpc_cert.up.sql new file mode 100644 index 000000000..b9d1e0a6b --- /dev/null +++ b/migrations/20260414120000_[2.0.0]_core_grpc_cert.up.sql @@ -0,0 +1,12 @@ +ALTER TABLE gateway RENAME COLUMN certificate TO certificate_serial; +ALTER TABLE proxy RENAME COLUMN certificate TO certificate_serial; + +ALTER TABLE gateway + ADD COLUMN core_client_cert_der bytea DEFAULT NULL, + ADD COLUMN core_client_cert_key_der bytea DEFAULT NULL, + ADD COLUMN core_client_cert_expiry timestamp without time zone NULL; + +ALTER TABLE proxy + ADD COLUMN core_client_cert_der bytea DEFAULT NULL, + ADD COLUMN core_client_cert_key_der bytea DEFAULT NULL, + ADD COLUMN core_client_cert_expiry timestamp without time zone NULL; From 9375e71f2c5085212c9c54857c91cf5d1f073fa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 14 Apr 2026 18:12:06 +0200 Subject: [PATCH 02/46] update existing structs for updated DB fields --- .../defguard_common/src/db/models/gateway.rs | 10 +++- crates/defguard_common/src/db/models/proxy.rs | 49 ++++++++++--------- crates/defguard_common/src/types/proxy.rs | 2 +- .../src/handlers/component_setup.rs | 12 +++-- crates/defguard_core/src/handlers/gateway.rs | 32 +++++++----- crates/defguard_core/src/handlers/mail.rs | 2 +- crates/defguard_gateway_manager/src/certs.rs | 10 ++-- crates/defguard_proxy_manager/src/certs.rs | 2 +- crates/defguard_setup/src/auto_adoption.rs | 22 +++------ 9 files changed, 75 insertions(+), 66 deletions(-) diff --git a/crates/defguard_common/src/db/models/gateway.rs b/crates/defguard_common/src/db/models/gateway.rs index 3a387eb43..aee3cb4d7 100644 --- a/crates/defguard_common/src/db/models/gateway.rs +++ b/crates/defguard_common/src/db/models/gateway.rs @@ -16,12 +16,15 @@ pub struct Gateway { pub port: i32, pub connected_at: Option, pub disconnected_at: Option, - pub certificate: Option, + pub certificate_serial: Option, pub certificate_expiry: Option, pub version: Option, pub enabled: bool, pub modified_at: NaiveDateTime, pub modified_by: String, + pub core_client_cert_der: Option>, + pub core_client_cert_key_der: Option>, + pub core_client_cert_expiry: Option, } impl Gateway { @@ -67,12 +70,15 @@ impl Gateway { port, connected_at: None, disconnected_at: None, - certificate: None, + certificate_serial: None, certificate_expiry: None, version: None, enabled: true, modified_by: modified_by.into(), modified_at, + core_client_cert_der: None, + core_client_cert_key_der: None, + core_client_cert_expiry: None, } } } diff --git a/crates/defguard_common/src/db/models/proxy.rs b/crates/defguard_common/src/db/models/proxy.rs index 6e7530aaf..4036e63de 100644 --- a/crates/defguard_common/src/db/models/proxy.rs +++ b/crates/defguard_common/src/db/models/proxy.rs @@ -3,7 +3,7 @@ use std::fmt; use chrono::{NaiveDateTime, Utc}; use model_derive::Model; use serde::{Deserialize, Serialize}; -use sqlx::PgPool; +use sqlx::{PgExecutor, PgPool, query, query_as}; use utoipa::ToSchema; use crate::{ @@ -21,10 +21,13 @@ pub struct Proxy { pub disconnected_at: Option, pub version: Option, pub enabled: bool, - pub certificate: Option, + pub certificate_serial: Option, pub certificate_expiry: Option, pub modified_at: NaiveDateTime, pub modified_by: String, + pub core_client_cert_der: Option>, + pub core_client_cert_key_der: Option>, + pub core_client_cert_expiry: Option, } impl fmt::Display for Proxy { @@ -56,12 +59,15 @@ impl Proxy { port, connected_at: None, disconnected_at: None, - certificate: None, + certificate_serial: None, certificate_expiry: None, version: None, enabled: true, modified_by: modified_by.into(), modified_at: Utc::now().naive_utc(), + core_client_cert_der: None, + core_client_cert_key_der: None, + core_client_cert_expiry: None, } } } @@ -81,11 +87,8 @@ impl Proxy { } /// Mark all proxies currently considered connected as disconnected. - pub async fn mark_all_disconnected<'e, E>(executor: E) -> sqlx::Result<()> - where - E: sqlx::PgExecutor<'e>, - { - sqlx::query( + pub async fn mark_all_disconnected<'e, E: PgExecutor<'e>>(executor: E) -> sqlx::Result<()> { + query( "UPDATE proxy \ SET disconnected_at = NOW() \ WHERE connected_at IS NOT NULL \ @@ -98,11 +101,8 @@ impl Proxy { } /// Fetch all enabled Proxies. - pub async fn all_enabled<'e, E>(executor: E) -> sqlx::Result> - where - E: sqlx::PgExecutor<'e>, - { - sqlx::query_as!(Self, "SELECT * FROM proxy WHERE enabled") + pub async fn all_enabled<'e, E: PgExecutor<'e>>(executor: E) -> sqlx::Result> { + query_as!(Self, "SELECT * FROM proxy WHERE enabled") .fetch_all(executor) .await } @@ -112,8 +112,8 @@ impl Proxy { address: &str, port: i32, ) -> sqlx::Result> { - sqlx::query_as!( - Proxy, + query_as!( + Self, "SELECT * FROM proxy WHERE address = $1 AND port = $2", address, port @@ -123,9 +123,15 @@ impl Proxy { } pub async fn list(pool: &PgPool) -> sqlx::Result> { - sqlx::query_as!(ProxyInfo, "SELECT * FROM proxy",) - .fetch_all(pool) - .await + query_as!( + ProxyInfo, + "SELECT id, name, address, port, connected_at, disconnected_at, \ + version, enabled, certificate_serial, certificate_expiry, \ + modified_at, modified_by \ + FROM proxy", + ) + .fetch_all(pool) + .await } pub async fn mark_connected(&mut self, pool: &PgPool, version: String) -> sqlx::Result<()> { @@ -144,11 +150,8 @@ impl Proxy { } /// Fetch all enabled, but one. Used for expired licence. - pub async fn leave_one_enabled<'e, E>(executor: E) -> sqlx::Result> - where - E: sqlx::PgExecutor<'e>, - { - sqlx::query_as!( + pub async fn leave_one_enabled<'e, E: PgExecutor<'e>>(executor: E) -> sqlx::Result> { + query_as!( Self, "SELECT * FROM proxy WHERE enabled AND id NOT IN (\ SELECT id FROM proxy WHERE enabled LIMIT 1 diff --git a/crates/defguard_common/src/types/proxy.rs b/crates/defguard_common/src/types/proxy.rs index 503f5996f..2d60d1a43 100644 --- a/crates/defguard_common/src/types/proxy.rs +++ b/crates/defguard_common/src/types/proxy.rs @@ -27,7 +27,7 @@ pub struct ProxyInfo { pub disconnected_at: Option, pub version: Option, pub enabled: bool, - pub certificate: Option, + pub certificate_serial: Option, pub certificate_expiry: Option, pub modified_at: NaiveDateTime, pub modified_by: String, diff --git a/crates/defguard_core/src/handlers/component_setup.rs b/crates/defguard_core/src/handlers/component_setup.rs index 2c1a2bf22..688623020 100644 --- a/crates/defguard_core/src/handlers/component_setup.rs +++ b/crates/defguard_core/src/handlers/component_setup.rs @@ -30,7 +30,7 @@ use defguard_common::{ utils::strip_scheme, }; use defguard_proto::{ - common::{CertificateInfo, DerPayload}, + common::{CertBundle, CertificateInfo, DerPayload}, gateway::gateway_setup_client::GatewaySetupClient, proxy::{ AcmeChallenge, AcmeLogs, AcmeStep, acme_issue_event, proxy_client::ProxyClient, @@ -550,7 +550,8 @@ pub async fn setup_proxy_tls_stream( // Step 6: Configure TLS yield Ok(flow.step(SetupStep::ConfiguringTls)); - if let Err(e) = client.send_cert(DerPayload { der_data: cert.der().to_vec() }).await { + let bundle: CertBundle = todo!(); + if let Err(e) = client.send_cert(bundle).await { yield Ok(flow.error(&format!("Failed to send certificate: {e}"))); return; } @@ -574,7 +575,7 @@ pub async fn setup_proxy_tls_stream( i32::from(request.grpc_port), session.user.fullname().as_str(), ); - proxy.certificate = Some(serial); + proxy.certificate_serial = Some(serial); proxy.certificate_expiry = Some(expiry); let proxy = match proxy.save(&pool).await { @@ -1000,7 +1001,8 @@ pub async fn setup_gateway_tls_stream( der_data: cert.der().to_vec(), }; - if let Err(e) = client.send_cert(response).await { + let bundle: CertBundle = todo!(); + if let Err(e) = client.send_cert(bundle).await { yield Ok(flow.error(&format!("Failed to send certificate: {e}"))); return; } @@ -1031,7 +1033,7 @@ pub async fn setup_gateway_tls_stream( session.user.fullname(), ); - gateway.certificate = Some(serial); + gateway.certificate_serial = Some(serial); gateway.certificate_expiry = Some(expiry); if let Err(err) = gateway.save(&pool).await { diff --git a/crates/defguard_core/src/handlers/gateway.rs b/crates/defguard_core/src/handlers/gateway.rs index a36d142f6..da6a7ac9c 100644 --- a/crates/defguard_core/src/handlers/gateway.rs +++ b/crates/defguard_core/src/handlers/gateway.rs @@ -28,7 +28,7 @@ pub struct GatewayInfo { pub connected_at: Option, pub disconnected_at: Option, pub connected: bool, - pub certificate: Option, + pub certificate_serial: Option, pub certificate_expiry: Option, pub version: Option, pub enabled: bool, @@ -41,16 +41,19 @@ impl GatewayInfo { pub async fn list(pool: &PgPool) -> sqlx::Result> { query_as!( Self, - "SELECT gateway.*, \ + "SELECT \ + g.id, g.location_id, g.name, g.address, g.port, g.connected_at, g.disconnected_at, \ CASE \ - WHEN gateway.connected_at IS NULL THEN false \ - WHEN gateway.disconnected_at IS NULL THEN true \ - WHEN gateway.connected_at >= gateway.disconnected_at THEN true \ + WHEN g.connected_at IS NULL THEN false \ + WHEN g.disconnected_at IS NULL THEN true \ + WHEN g.connected_at >= g.disconnected_at THEN true \ ELSE false \ END AS \"connected!\", \ + g.certificate_serial, g.certificate_expiry, g.version, \ + g.enabled, g.modified_at, g.modified_by, \ wn.name AS location_name \ - FROM gateway \ - JOIN wireguard_network wn ON gateway.location_id = wn.id", + FROM gateway g \ + JOIN wireguard_network wn ON g.location_id = wn.id", ) .fetch_all(pool) .await @@ -59,16 +62,19 @@ impl GatewayInfo { pub async fn find_by_location_id(pool: &PgPool, location_id: Id) -> sqlx::Result> { query_as!( Self, - "SELECT gateway.*, \ + "SELECT \ + g.id, g.location_id, g.name, g.address, g.port, g.connected_at, g.disconnected_at, \ CASE \ - WHEN gateway.connected_at IS NULL THEN false \ - WHEN gateway.disconnected_at IS NULL THEN true \ - WHEN gateway.connected_at >= gateway.disconnected_at THEN true \ + WHEN g.connected_at IS NULL THEN false \ + WHEN g.disconnected_at IS NULL THEN true \ + WHEN g.connected_at >= g.disconnected_at THEN true \ ELSE false \ END AS \"connected!\", \ + g.certificate_serial, g.certificate_expiry, g.version, \ + g.enabled, g.modified_at, g.modified_by, \ wn.name AS location_name \ - FROM gateway \ - JOIN wireguard_network wn ON gateway.location_id = wn.id \ + FROM gateway g \ + JOIN wireguard_network wn ON g.location_id = wn.id \ WHERE location_id = $1", location_id ) diff --git a/crates/defguard_core/src/handlers/mail.rs b/crates/defguard_core/src/handlers/mail.rs index 2a02d3ef8..0ec588d9f 100644 --- a/crates/defguard_core/src/handlers/mail.rs +++ b/crates/defguard_core/src/handlers/mail.rs @@ -90,7 +90,7 @@ pub async fn send_support_data( "version": g.version.as_deref().unwrap_or("unknown"), "address": g.address, "port": g.port, - "certificate": g.certificate, + "certificate": g.certificate_serial, "name": g.name, "connected_at": g.connected_at, })).collect::>(), diff --git a/crates/defguard_gateway_manager/src/certs.rs b/crates/defguard_gateway_manager/src/certs.rs index 5e2367e17..2f4bb118c 100644 --- a/crates/defguard_gateway_manager/src/certs.rs +++ b/crates/defguard_gateway_manager/src/certs.rs @@ -22,7 +22,7 @@ pub(super) async fn refresh_certs(pool: &PgPool, tx: &watch::Sender Date: Tue, 14 Apr 2026 20:29:08 +0200 Subject: [PATCH 03/46] add helpers for signing server/client certs --- crates/defguard_certs/src/lib.rs | 42 ++++++++++++++----- crates/defguard_core/src/cert_settings.rs | 4 +- .../src/handlers/component_setup.rs | 4 +- .../tests/integration/api/common/mod.rs | 2 +- crates/defguard_setup/src/auto_adoption.rs | 4 +- .../tests/auto_wizard_url_settings.rs | 2 +- 6 files changed, 39 insertions(+), 19 deletions(-) diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs index 2751875d8..32f51853c 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -92,16 +92,37 @@ impl CertificateAuthority<'_> { Self::from_key_cert_params(ca_key_pair, ca_params) } - pub fn sign_csr(&self, csr: &Csr) -> Result { - // TODO: make validity configurable? - self.sign_csr_with_validity(csr, DEFAULT_CERT_VALIDITY_DAYS) + /// Sign a server-facing component certificate (`ServerAuth` EKU only). + /// + /// Use [`sign_client_cert`] for Core gRPC client certificates, or + /// [`sign_csr_with_validity`] when custom validity is needed. + pub fn sign_server_cert(&self, csr: &Csr) -> Result { + self.sign_csr_with_validity( + csr, + DEFAULT_CERT_VALIDITY_DAYS, + &[ExtendedKeyUsagePurpose::ServerAuth], + ) } - /// Sign CSR with explicit validity in days. + /// Sign a Core gRPC client certificate (`ClientAuth` EKU only). + pub fn sign_client_cert(&self, csr: &Csr) -> Result { + self.sign_csr_with_validity( + csr, + DEFAULT_CERT_VALIDITY_DAYS, + &[ExtendedKeyUsagePurpose::ClientAuth], + ) + } + + /// Sign a CSR with explicit validity in days and extended key usages. + /// + /// `extended_key_usages` controls which EKUs are encoded in the signed + /// certificate. Pass `&[ServerAuth]` for component server certs and + /// `&[ClientAuth]` for Core gRPC client certs. pub fn sign_csr_with_validity( &self, csr: &Csr, days_valid: i64, + extended_key_usages: &[ExtendedKeyUsagePurpose], ) -> Result { let mut csr_params = csr.params()?; @@ -116,10 +137,7 @@ impl CertificateAuthority<'_> { KeyUsagePurpose::DigitalSignature, KeyUsagePurpose::KeyEncipherment, ]; - csr_params.params.extended_key_usages = vec![ - ExtendedKeyUsagePurpose::ServerAuth, - ExtendedKeyUsagePurpose::ClientAuth, - ]; + csr_params.params.extended_key_usages = extended_key_usages.to_vec(); let cert = csr_params.signed_by(&self.issuer)?; Ok(cert) @@ -329,7 +347,7 @@ mod tests { } #[test] - fn test_sign_csr() { + fn test_sign_server_cert() { let ca = CertificateAuthority::new("Defguard CA", "email@email.com", 10).unwrap(); let cert_key_pair = generate_key_pair().unwrap(); let csr = Csr::new( @@ -341,7 +359,7 @@ mod tests { ], ) .unwrap(); - let signed_cert: Certificate = ca.sign_csr(&csr).unwrap(); + let signed_cert: Certificate = ca.sign_server_cert(&csr).unwrap(); assert!(signed_cert.pem().contains("BEGIN CERTIFICATE")); } @@ -357,7 +375,9 @@ mod tests { vec![(rcgen::DnType::CommonName, "example.com")], ) .unwrap(); - let signed_cert: Certificate = ca.sign_csr_with_validity(&csr, 90).unwrap(); + let signed_cert: Certificate = ca + .sign_csr_with_validity(&csr, 90, &[ExtendedKeyUsagePurpose::ServerAuth]) + .unwrap(); let der = signed_cert.der(); let (_rem, parsed) = parse_x509_certificate(der).unwrap(); let validity = parsed.tbs_certificate.validity; diff --git a/crates/defguard_core/src/cert_settings.rs b/crates/defguard_core/src/cert_settings.rs index 586468e68..fa3b0c287 100644 --- a/crates/defguard_core/src/cert_settings.rs +++ b/crates/defguard_core/src/cert_settings.rs @@ -120,7 +120,7 @@ pub async fn apply_internal_url_settings( let san = vec![hostname.clone()]; let dn = vec![(DnType::CommonName, hostname.as_str())]; let csr = Csr::new(&key_pair, &san, dn)?; - let server_cert = ca.sign_csr(&csr)?; + let server_cert = ca.sign_server_cert(&csr)?; let cert_der = server_cert.der().to_vec(); let cert_pem = der_to_pem(&cert_der, PemLabel::Certificate)?; @@ -244,7 +244,7 @@ pub async fn apply_external_url_settings( let san = vec![hostname.clone()]; let dn = vec![(DnType::CommonName, hostname.as_str())]; let csr = Csr::new(&key_pair, &san, dn)?; - let server_cert = ca.sign_csr(&csr)?; + let server_cert = ca.sign_server_cert(&csr)?; let cert_der = server_cert.der().to_vec(); let cert_pem = der_to_pem(&cert_der, PemLabel::Certificate)?; diff --git a/crates/defguard_core/src/handlers/component_setup.rs b/crates/defguard_core/src/handlers/component_setup.rs index 688623020..22b19e7eb 100644 --- a/crates/defguard_core/src/handlers/component_setup.rs +++ b/crates/defguard_core/src/handlers/component_setup.rs @@ -537,7 +537,7 @@ pub async fn setup_proxy_tls_stream( debug!("Certificate authority loaded and ready to sign certificates"); - let cert = match ca.sign_csr(&csr) { + let cert = match ca.sign_server_cert(&csr) { Ok(c) => c, Err(e) => { yield Ok(flow.error(&format!("Failed to sign CSR: {e}"))); @@ -984,7 +984,7 @@ pub async fn setup_gateway_tls_stream( debug!("Certificate authority loaded and ready to sign certificates"); - let cert = match ca.sign_csr(&csr) { + let cert = match ca.sign_server_cert(&csr) { Ok(c) => c, Err(e) => { yield Ok(flow.error(&format!("Failed to sign CSR: {e}"))); diff --git a/crates/defguard_core/tests/integration/api/common/mod.rs b/crates/defguard_core/tests/integration/api/common/mod.rs index 700924dc2..4a14eff4d 100644 --- a/crates/defguard_core/tests/integration/api/common/mod.rs +++ b/crates/defguard_core/tests/integration/api/common/mod.rs @@ -255,7 +255,7 @@ pub(crate) fn generate_test_cert_pem(common_name: &str) -> (String, String) { let san = vec![common_name.to_string()]; let dn = vec![(DnType::CommonName, common_name)]; let csr = Csr::new(&key_pair, &san, dn).unwrap(); - let cert = ca.sign_csr(&csr).unwrap(); + let cert = ca.sign_server_cert(&csr).unwrap(); let cert_pem = der_to_pem(cert.der(), PemLabel::Certificate).unwrap(); let key_pem = der_to_pem(key_pair.serialize_der().as_slice(), PemLabel::PrivateKey).unwrap(); (cert_pem, key_pem) diff --git a/crates/defguard_setup/src/auto_adoption.rs b/crates/defguard_setup/src/auto_adoption.rs index 5789c6581..57a8dfcca 100644 --- a/crates/defguard_setup/src/auto_adoption.rs +++ b/crates/defguard_setup/src/auto_adoption.rs @@ -457,7 +457,7 @@ async fn run_edge_adoption_attempt_scoped( } }; - let cert = match ca.sign_csr(&csr) { + let cert = match ca.sign_server_cert(&csr) { Ok(cert) => cert, Err(err) => { return merge_failure_logs( @@ -762,7 +762,7 @@ async fn run_gateway_adoption_attempt_scoped( } }; - let cert = match ca.sign_csr(&csr) { + let cert = match ca.sign_server_cert(&csr) { Ok(cert) => cert, Err(err) => { return merge_failure_logs( diff --git a/crates/defguard_setup/tests/auto_wizard_url_settings.rs b/crates/defguard_setup/tests/auto_wizard_url_settings.rs index f5cb72be5..8f0272e24 100644 --- a/crates/defguard_setup/tests/auto_wizard_url_settings.rs +++ b/crates/defguard_setup/tests/auto_wizard_url_settings.rs @@ -58,7 +58,7 @@ fn generate_test_cert_pem(common_name: &str) -> (String, String) { let san = vec![common_name.to_string()]; let dn = vec![(DnType::CommonName, common_name)]; let csr = Csr::new(&key_pair, &san, dn).unwrap(); - let cert = ca.sign_csr(&csr).unwrap(); + let cert = ca.sign_server_cert(&csr).unwrap(); let cert_pem = der_to_pem(cert.der(), PemLabel::Certificate).unwrap(); let key_pem = der_to_pem(key_pair.serialize_der().as_slice(), PemLabel::PrivateKey).unwrap(); (cert_pem, key_pem) From 8885635131f9d8d6b97dc19a099837fb39d542b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 14 Apr 2026 21:12:36 +0200 Subject: [PATCH 04/46] update query data --- ...78d61eebe6a79945324e98ca452b50d6abc90.json | 86 ------------- ...ec1ab4c6f0ead212fc74bb881e1d5c0d96080.json | 86 ------------- ...afda3fb17e9b28034593b960dcb3856460c0.json} | 9 +- ...be02cd4b54d4ec042f238c854637a6b101d0.json} | 54 ++++----- ...6f31e2b9ced9aef4268752f6e51d4a83eab99.json | 112 +++++++++++++++++ ...c07340f6c0993ac4aef58cea272ff3c27ef8.json} | 26 +++- ...21c8ee0fe9d2cf0b574332bddc24a0ab4a37.json} | 9 +- ...e3d57dd57fbeead263d5a4c915ea25bd5205.json} | 66 +++++----- ...ab658d841589e7670a4b3fcbc9d53cd1c250.json} | 30 ++--- ...d731e8c60d5284b8a7ed29b1e32601b667f6.json} | 26 +++- ...c81e888b03cb8d158ed3770a4bc19bae6b22.json} | 26 +++- ...7745a48080f1e8faa7f7bf7ffae65f2b6ebf.json} | 54 ++++----- ...55218f9c51b0ae143a9a1c5ff9e13dab9c75.json} | 9 +- ...d82a34d3bfa4364cbe9b8368e71445bc20877.json | 89 -------------- ...399dec458cb9250fc448bc6edf94b5e90805b.json | 113 ++++++++++++++++++ ...9f11c97ab5d68c7ccd1c173aa8212a659770.json} | 26 +++- ...8e85a165c0e03167abd4940b248e8d29ccf1.json} | 26 +++- ...f6354bd99ff1f1f9adc02837ccbc458ee8be9.json | 104 ++++++++++++++++ ...5afc8c9c0ae34ffe29bf6adb2a89f1ae6edb.json} | 26 +++- ...cfb5a2496f961ead8474e8ed5e290cabec85.json} | 9 +- ...a2e564bdf787b0f02dd244b0d01186d56e0b.json} | 62 ++++++---- 21 files changed, 629 insertions(+), 419 deletions(-) delete mode 100644 .sqlx/query-27e7e18a7014af541fe5f8f051f78d61eebe6a79945324e98ca452b50d6abc90.json delete mode 100644 .sqlx/query-2ce93887379d80ff03753caaf94ec1ab4c6f0ead212fc74bb881e1d5c0d96080.json rename .sqlx/{query-938c250b35e5b2b46cff9efbe41fce3100fe0ff1a86be48b7a22b58ef3da5bf1.json => query-2ff663d549b92de999cf87960dd3afda3fb17e9b28034593b960dcb3856460c0.json} (59%) rename .sqlx/{query-d66aaea81e06f0b0fffe8103ffa9384fa989a4bfd58b3490d7b94f2547f261b1.json => query-4b6305c0cc7e4bff7f918f9113dcbe02cd4b54d4ec042f238c854637a6b101d0.json} (75%) create mode 100644 .sqlx/query-633bf991c7480a892f99c99175d6f31e2b9ced9aef4268752f6e51d4a83eab99.json rename .sqlx/{query-19ad6391ddfb43ab2f1100aefab048e985a9c6ba7c02475756c3e40e9a66b450.json => query-6c0570ec090a92e22b111cdbb131c07340f6c0993ac4aef58cea272ff3c27ef8.json} (68%) rename .sqlx/{query-6b1506441fd24aff832ee8ee9edb6d8423cfc61bf59ceaf0364c07ddde47127e.json => query-702eeefc7607721e6bf4e84fad0c21c8ee0fe9d2cf0b574332bddc24a0ab4a37.json} (53%) rename .sqlx/{query-3c6a119f2f10046bd9e42314df953a0a0b3b44d0a87d43f69425729c15e1a400.json => query-751b6f6c3bfeaa91685107c94c92e3d57dd57fbeead263d5a4c915ea25bd5205.json} (66%) rename .sqlx/{query-472e3903cf3df3c5938527c5584a5a53edc5b492a6bb12eac3d97f3ebc5f8506.json => query-8a9afd0b7e2f96be85230b35190fab658d841589e7670a4b3fcbc9d53cd1c250.json} (83%) rename .sqlx/{query-f14171d837b8ac91e765e9b86153186ac78bf78ce3cfc5af7441d84be52749d2.json => query-8d142b160cee06ddc6b3578b3808d731e8c60d5284b8a7ed29b1e32601b667f6.json} (67%) rename .sqlx/{query-0fc80b6949eaaeda77dabad7093bca70bd327c14eea4b8db1c9f11c722a00bf4.json => query-93a4240e469c663e4038cee30aa9c81e888b03cb8d158ed3770a4bc19bae6b22.json} (68%) rename .sqlx/{query-127e21275639df135aaa9fce8dd1c2b3dffb146a8eacd47faced4894772cfacf.json => query-9ec638cdabc0500b54cedc5ba18c7745a48080f1e8faa7f7bf7ffae65f2b6ebf.json} (74%) rename .sqlx/{query-8d21a38672059e820d355590df83c1c9c5f75956f8b7c2a1a235189e1583a599.json => query-a05a752af1643a1d9f9b9544df2055218f9c51b0ae143a9a1c5ff9e13dab9c75.json} (59%) delete mode 100644 .sqlx/query-a41787c8c8307414165ab23ef96d82a34d3bfa4364cbe9b8368e71445bc20877.json create mode 100644 .sqlx/query-bda99bacb002f781dd276c5623d399dec458cb9250fc448bc6edf94b5e90805b.json rename .sqlx/{query-beffd1aad66ce9d9a179f14e224d9ca63f0c0aa378460bd344f7a7daa8985bad.json => query-d4c6847ef8197f425c3cb19964509f11c97ab5d68c7ccd1c173aa8212a659770.json} (68%) rename .sqlx/{query-c2bf39775cc4142f18165b1e342731b0d6b7370543b5c5e8cabdfd47487a3056.json => query-d9dc6788c19efa7b1ec9060651398e85a165c0e03167abd4940b248e8d29ccf1.json} (67%) create mode 100644 .sqlx/query-e2f43cdd1dd50ab2b97b1cdae80f6354bd99ff1f1f9adc02837ccbc458ee8be9.json rename .sqlx/{query-bcb405dc3159cd72c5ccebf29bf4b6163ee0e324cd95cbf3e32d025b5ba7fcbb.json => query-eb50eae3a1786cf685b8ec084e905afc8c9c0ae34ffe29bf6adb2a89f1ae6edb.json} (67%) rename .sqlx/{query-780d66e4628d13c6c2f489cc87c7358945b93628104ac57aac207b8ec74be08a.json => query-f0bd5b48faffc4152e2683d4aecfcfb5a2496f961ead8474e8ed5e290cabec85.json} (54%) rename .sqlx/{query-4d9c4562a138038ba054b5b83b646341ee18e24f0d32399e6ce2ebaedef64cea.json => query-fd40448d1bff2b70f119211c44b9a2e564bdf787b0f02dd244b0d01186d56e0b.json} (67%) diff --git a/.sqlx/query-27e7e18a7014af541fe5f8f051f78d61eebe6a79945324e98ca452b50d6abc90.json b/.sqlx/query-27e7e18a7014af541fe5f8f051f78d61eebe6a79945324e98ca452b50d6abc90.json deleted file mode 100644 index 92440ceb4..000000000 --- a/.sqlx/query-27e7e18a7014af541fe5f8f051f78d61eebe6a79945324e98ca452b50d6abc90.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT * FROM proxy WHERE enabled AND id NOT IN (SELECT id FROM proxy WHERE enabled LIMIT 1\n )", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "name", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "address", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "port", - "type_info": "Int4" - }, - { - "ordinal": 4, - "name": "connected_at", - "type_info": "Timestamp" - }, - { - "ordinal": 5, - "name": "disconnected_at", - "type_info": "Timestamp" - }, - { - "ordinal": 6, - "name": "certificate_expiry", - "type_info": "Timestamp" - }, - { - "ordinal": 7, - "name": "version", - "type_info": "Text" - }, - { - "ordinal": 8, - "name": "modified_at", - "type_info": "Timestamp" - }, - { - "ordinal": 9, - "name": "certificate", - "type_info": "Text" - }, - { - "ordinal": 10, - "name": "modified_by", - "type_info": "Text" - }, - { - "ordinal": 11, - "name": "enabled", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false, - false, - true, - true, - true, - true, - false, - true, - false, - false - ] - }, - "hash": "27e7e18a7014af541fe5f8f051f78d61eebe6a79945324e98ca452b50d6abc90" -} diff --git a/.sqlx/query-2ce93887379d80ff03753caaf94ec1ab4c6f0ead212fc74bb881e1d5c0d96080.json b/.sqlx/query-2ce93887379d80ff03753caaf94ec1ab4c6f0ead212fc74bb881e1d5c0d96080.json deleted file mode 100644 index aa509dfc3..000000000 --- a/.sqlx/query-2ce93887379d80ff03753caaf94ec1ab4c6f0ead212fc74bb881e1d5c0d96080.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT * FROM proxy WHERE enabled", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "name", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "address", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "port", - "type_info": "Int4" - }, - { - "ordinal": 4, - "name": "connected_at", - "type_info": "Timestamp" - }, - { - "ordinal": 5, - "name": "disconnected_at", - "type_info": "Timestamp" - }, - { - "ordinal": 6, - "name": "certificate_expiry", - "type_info": "Timestamp" - }, - { - "ordinal": 7, - "name": "version", - "type_info": "Text" - }, - { - "ordinal": 8, - "name": "modified_at", - "type_info": "Timestamp" - }, - { - "ordinal": 9, - "name": "certificate", - "type_info": "Text" - }, - { - "ordinal": 10, - "name": "modified_by", - "type_info": "Text" - }, - { - "ordinal": 11, - "name": "enabled", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false, - false, - true, - true, - true, - true, - false, - true, - false, - false - ] - }, - "hash": "2ce93887379d80ff03753caaf94ec1ab4c6f0ead212fc74bb881e1d5c0d96080" -} diff --git a/.sqlx/query-938c250b35e5b2b46cff9efbe41fce3100fe0ff1a86be48b7a22b58ef3da5bf1.json b/.sqlx/query-2ff663d549b92de999cf87960dd3afda3fb17e9b28034593b960dcb3856460c0.json similarity index 59% rename from .sqlx/query-938c250b35e5b2b46cff9efbe41fce3100fe0ff1a86be48b7a22b58ef3da5bf1.json rename to .sqlx/query-2ff663d549b92de999cf87960dd3afda3fb17e9b28034593b960dcb3856460c0.json index 2b5f406c8..e9ac6d8cc 100644 --- a/.sqlx/query-938c250b35e5b2b46cff9efbe41fce3100fe0ff1a86be48b7a22b58ef3da5bf1.json +++ b/.sqlx/query-2ff663d549b92de999cf87960dd3afda3fb17e9b28034593b960dcb3856460c0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"proxy\" (\"name\",\"address\",\"port\",\"connected_at\",\"disconnected_at\",\"version\",\"enabled\",\"certificate\",\"certificate_expiry\",\"modified_at\",\"modified_by\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) RETURNING id", + "query": "INSERT INTO \"proxy\" (\"name\",\"address\",\"port\",\"connected_at\",\"disconnected_at\",\"version\",\"enabled\",\"certificate_serial\",\"certificate_expiry\",\"modified_at\",\"modified_by\",\"core_client_cert_der\",\"core_client_cert_key_der\",\"core_client_cert_expiry\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING id", "describe": { "columns": [ { @@ -21,12 +21,15 @@ "Text", "Timestamp", "Timestamp", - "Text" + "Text", + "Bytea", + "Bytea", + "Timestamp" ] }, "nullable": [ false ] }, - "hash": "938c250b35e5b2b46cff9efbe41fce3100fe0ff1a86be48b7a22b58ef3da5bf1" + "hash": "2ff663d549b92de999cf87960dd3afda3fb17e9b28034593b960dcb3856460c0" } diff --git a/.sqlx/query-d66aaea81e06f0b0fffe8103ffa9384fa989a4bfd58b3490d7b94f2547f261b1.json b/.sqlx/query-4b6305c0cc7e4bff7f918f9113dcbe02cd4b54d4ec042f238c854637a6b101d0.json similarity index 75% rename from .sqlx/query-d66aaea81e06f0b0fffe8103ffa9384fa989a4bfd58b3490d7b94f2547f261b1.json rename to .sqlx/query-4b6305c0cc7e4bff7f918f9113dcbe02cd4b54d4ec042f238c854637a6b101d0.json index 6d9d8cf06..a58578c83 100644 --- a/.sqlx/query-d66aaea81e06f0b0fffe8103ffa9384fa989a4bfd58b3490d7b94f2547f261b1.json +++ b/.sqlx/query-4b6305c0cc7e4bff7f918f9113dcbe02cd4b54d4ec042f238c854637a6b101d0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT gateway.*, CASE WHEN gateway.connected_at IS NULL THEN false WHEN gateway.disconnected_at IS NULL THEN true WHEN gateway.connected_at >= gateway.disconnected_at THEN true ELSE false END AS \"connected!\", wn.name AS location_name FROM gateway JOIN wireguard_network wn ON gateway.location_id = wn.id", + "query": "SELECT g.id, g.location_id, g.name, g.address, g.port, g.connected_at, g.disconnected_at, CASE WHEN g.connected_at IS NULL THEN false WHEN g.disconnected_at IS NULL THEN true WHEN g.connected_at >= g.disconnected_at THEN true ELSE false END AS \"connected!\", g.certificate_serial, g.certificate_expiry, g.version, g.enabled, g.modified_at, g.modified_by, wn.name AS location_name FROM gateway g JOIN wireguard_network wn ON g.location_id = wn.id", "describe": { "columns": [ { @@ -15,48 +15,48 @@ }, { "ordinal": 2, - "name": "connected_at", - "type_info": "Timestamp" + "name": "name", + "type_info": "Text" }, { "ordinal": 3, - "name": "disconnected_at", - "type_info": "Timestamp" + "name": "address", + "type_info": "Text" }, { "ordinal": 4, - "name": "certificate_expiry", - "type_info": "Timestamp" + "name": "port", + "type_info": "Int4" }, { "ordinal": 5, - "name": "version", - "type_info": "Text" + "name": "connected_at", + "type_info": "Timestamp" }, { "ordinal": 6, - "name": "name", - "type_info": "Text" + "name": "disconnected_at", + "type_info": "Timestamp" }, { "ordinal": 7, - "name": "certificate", - "type_info": "Text" + "name": "connected!", + "type_info": "Bool" }, { "ordinal": 8, - "name": "address", + "name": "certificate_serial", "type_info": "Text" }, { "ordinal": 9, - "name": "port", - "type_info": "Int4" + "name": "certificate_expiry", + "type_info": "Timestamp" }, { "ordinal": 10, - "name": "modified_at", - "type_info": "Timestamp" + "name": "version", + "type_info": "Text" }, { "ordinal": 11, @@ -65,13 +65,13 @@ }, { "ordinal": 12, - "name": "modified_by", - "type_info": "Text" + "name": "modified_at", + "type_info": "Timestamp" }, { "ordinal": 13, - "name": "connected!", - "type_info": "Bool" + "name": "modified_by", + "type_info": "Text" }, { "ordinal": 14, @@ -83,22 +83,22 @@ "Left": [] }, "nullable": [ + false, + false, + false, false, false, true, true, + null, true, true, - false, true, false, false, false, - false, - false, - null, false ] }, - "hash": "d66aaea81e06f0b0fffe8103ffa9384fa989a4bfd58b3490d7b94f2547f261b1" + "hash": "4b6305c0cc7e4bff7f918f9113dcbe02cd4b54d4ec042f238c854637a6b101d0" } diff --git a/.sqlx/query-633bf991c7480a892f99c99175d6f31e2b9ced9aef4268752f6e51d4a83eab99.json b/.sqlx/query-633bf991c7480a892f99c99175d6f31e2b9ced9aef4268752f6e51d4a83eab99.json new file mode 100644 index 000000000..d264b32a7 --- /dev/null +++ b/.sqlx/query-633bf991c7480a892f99c99175d6f31e2b9ced9aef4268752f6e51d4a83eab99.json @@ -0,0 +1,112 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, location_id, name, address, port, connected_at, disconnected_at, certificate_serial, certificate_expiry, version, enabled, modified_at, modified_by, core_client_cert_der, core_client_cert_key_der, core_client_cert_expiry FROM gateway WHERE location_id = $1 ORDER BY id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "location_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "address", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "port", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "connected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 6, + "name": "disconnected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 7, + "name": "certificate_serial", + "type_info": "Text" + }, + { + "ordinal": 8, + "name": "certificate_expiry", + "type_info": "Timestamp" + }, + { + "ordinal": 9, + "name": "version", + "type_info": "Text" + }, + { + "ordinal": 10, + "name": "enabled", + "type_info": "Bool" + }, + { + "ordinal": 11, + "name": "modified_at", + "type_info": "Timestamp" + }, + { + "ordinal": 12, + "name": "modified_by", + "type_info": "Text" + }, + { + "ordinal": 13, + "name": "core_client_cert_der", + "type_info": "Bytea" + }, + { + "ordinal": 14, + "name": "core_client_cert_key_der", + "type_info": "Bytea" + }, + { + "ordinal": 15, + "name": "core_client_cert_expiry", + "type_info": "Timestamp" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + true, + true, + true, + true, + false, + false, + false, + true, + true, + true + ] + }, + "hash": "633bf991c7480a892f99c99175d6f31e2b9ced9aef4268752f6e51d4a83eab99" +} diff --git a/.sqlx/query-19ad6391ddfb43ab2f1100aefab048e985a9c6ba7c02475756c3e40e9a66b450.json b/.sqlx/query-6c0570ec090a92e22b111cdbb131c07340f6c0993ac4aef58cea272ff3c27ef8.json similarity index 68% rename from .sqlx/query-19ad6391ddfb43ab2f1100aefab048e985a9c6ba7c02475756c3e40e9a66b450.json rename to .sqlx/query-6c0570ec090a92e22b111cdbb131c07340f6c0993ac4aef58cea272ff3c27ef8.json index febbd693e..ca515c2bb 100644 --- a/.sqlx/query-19ad6391ddfb43ab2f1100aefab048e985a9c6ba7c02475756c3e40e9a66b450.json +++ b/.sqlx/query-6c0570ec090a92e22b111cdbb131c07340f6c0993ac4aef58cea272ff3c27ef8.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"location_id\",\"name\",\"address\",\"port\",\"connected_at\",\"disconnected_at\",\"certificate\",\"certificate_expiry\",\"version\",\"enabled\",\"modified_at\",\"modified_by\" FROM \"gateway\" LIMIT $1 OFFSET $2", + "query": "SELECT id, \"location_id\",\"name\",\"address\",\"port\",\"connected_at\",\"disconnected_at\",\"certificate_serial\",\"certificate_expiry\",\"version\",\"enabled\",\"modified_at\",\"modified_by\",\"core_client_cert_der\",\"core_client_cert_key_der\",\"core_client_cert_expiry\" FROM \"gateway\" LIMIT $1 OFFSET $2", "describe": { "columns": [ { @@ -40,7 +40,7 @@ }, { "ordinal": 7, - "name": "certificate", + "name": "certificate_serial", "type_info": "Text" }, { @@ -67,6 +67,21 @@ "ordinal": 12, "name": "modified_by", "type_info": "Text" + }, + { + "ordinal": 13, + "name": "core_client_cert_der", + "type_info": "Bytea" + }, + { + "ordinal": 14, + "name": "core_client_cert_key_der", + "type_info": "Bytea" + }, + { + "ordinal": 15, + "name": "core_client_cert_expiry", + "type_info": "Timestamp" } ], "parameters": { @@ -88,8 +103,11 @@ true, false, false, - false + false, + true, + true, + true ] }, - "hash": "19ad6391ddfb43ab2f1100aefab048e985a9c6ba7c02475756c3e40e9a66b450" + "hash": "6c0570ec090a92e22b111cdbb131c07340f6c0993ac4aef58cea272ff3c27ef8" } diff --git a/.sqlx/query-6b1506441fd24aff832ee8ee9edb6d8423cfc61bf59ceaf0364c07ddde47127e.json b/.sqlx/query-702eeefc7607721e6bf4e84fad0c21c8ee0fe9d2cf0b574332bddc24a0ab4a37.json similarity index 53% rename from .sqlx/query-6b1506441fd24aff832ee8ee9edb6d8423cfc61bf59ceaf0364c07ddde47127e.json rename to .sqlx/query-702eeefc7607721e6bf4e84fad0c21c8ee0fe9d2cf0b574332bddc24a0ab4a37.json index 0703ef263..a5a69d0a1 100644 --- a/.sqlx/query-6b1506441fd24aff832ee8ee9edb6d8423cfc61bf59ceaf0364c07ddde47127e.json +++ b/.sqlx/query-702eeefc7607721e6bf4e84fad0c21c8ee0fe9d2cf0b574332bddc24a0ab4a37.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"gateway\" SET \"location_id\" = $2,\"name\" = $3,\"address\" = $4,\"port\" = $5,\"connected_at\" = $6,\"disconnected_at\" = $7,\"certificate\" = $8,\"certificate_expiry\" = $9,\"version\" = $10,\"enabled\" = $11,\"modified_at\" = $12,\"modified_by\" = $13 WHERE id = $1", + "query": "UPDATE \"gateway\" SET \"location_id\" = $2,\"name\" = $3,\"address\" = $4,\"port\" = $5,\"connected_at\" = $6,\"disconnected_at\" = $7,\"certificate_serial\" = $8,\"certificate_expiry\" = $9,\"version\" = $10,\"enabled\" = $11,\"modified_at\" = $12,\"modified_by\" = $13,\"core_client_cert_der\" = $14,\"core_client_cert_key_der\" = $15,\"core_client_cert_expiry\" = $16 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -17,10 +17,13 @@ "Text", "Bool", "Timestamp", - "Text" + "Text", + "Bytea", + "Bytea", + "Timestamp" ] }, "nullable": [] }, - "hash": "6b1506441fd24aff832ee8ee9edb6d8423cfc61bf59ceaf0364c07ddde47127e" + "hash": "702eeefc7607721e6bf4e84fad0c21c8ee0fe9d2cf0b574332bddc24a0ab4a37" } diff --git a/.sqlx/query-3c6a119f2f10046bd9e42314df953a0a0b3b44d0a87d43f69425729c15e1a400.json b/.sqlx/query-751b6f6c3bfeaa91685107c94c92e3d57dd57fbeead263d5a4c915ea25bd5205.json similarity index 66% rename from .sqlx/query-3c6a119f2f10046bd9e42314df953a0a0b3b44d0a87d43f69425729c15e1a400.json rename to .sqlx/query-751b6f6c3bfeaa91685107c94c92e3d57dd57fbeead263d5a4c915ea25bd5205.json index fc05918a2..965dfaa87 100644 --- a/.sqlx/query-3c6a119f2f10046bd9e42314df953a0a0b3b44d0a87d43f69425729c15e1a400.json +++ b/.sqlx/query-751b6f6c3bfeaa91685107c94c92e3d57dd57fbeead263d5a4c915ea25bd5205.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT * FROM gateway WHERE location_id = $1 ORDER BY id", + "query": "SELECT id, name, address, port, connected_at, disconnected_at, version, enabled, certificate_serial, certificate_expiry, modified_at, modified_by, core_client_cert_der, core_client_cert_key_der, core_client_cert_expiry FROM proxy WHERE enabled", "describe": { "columns": [ { @@ -10,48 +10,48 @@ }, { "ordinal": 1, - "name": "location_id", - "type_info": "Int8" + "name": "name", + "type_info": "Text" }, { "ordinal": 2, - "name": "connected_at", - "type_info": "Timestamp" + "name": "address", + "type_info": "Text" }, { "ordinal": 3, - "name": "disconnected_at", - "type_info": "Timestamp" + "name": "port", + "type_info": "Int4" }, { "ordinal": 4, - "name": "certificate_expiry", + "name": "connected_at", "type_info": "Timestamp" }, { "ordinal": 5, - "name": "version", - "type_info": "Text" + "name": "disconnected_at", + "type_info": "Timestamp" }, { "ordinal": 6, - "name": "name", + "name": "version", "type_info": "Text" }, { "ordinal": 7, - "name": "certificate", - "type_info": "Text" + "name": "enabled", + "type_info": "Bool" }, { "ordinal": 8, - "name": "address", + "name": "certificate_serial", "type_info": "Text" }, { "ordinal": 9, - "name": "port", - "type_info": "Int4" + "name": "certificate_expiry", + "type_info": "Timestamp" }, { "ordinal": 10, @@ -60,35 +60,45 @@ }, { "ordinal": 11, - "name": "enabled", - "type_info": "Bool" + "name": "modified_by", + "type_info": "Text" }, { "ordinal": 12, - "name": "modified_by", - "type_info": "Text" + "name": "core_client_cert_der", + "type_info": "Bytea" + }, + { + "ordinal": 13, + "name": "core_client_cert_key_der", + "type_info": "Bytea" + }, + { + "ordinal": 14, + "name": "core_client_cert_expiry", + "type_info": "Timestamp" } ], "parameters": { - "Left": [ - "Int8" - ] + "Left": [] }, "nullable": [ false, false, - true, + false, + false, true, true, true, false, true, + true, false, false, - false, - false, - false + true, + true, + true ] }, - "hash": "3c6a119f2f10046bd9e42314df953a0a0b3b44d0a87d43f69425729c15e1a400" + "hash": "751b6f6c3bfeaa91685107c94c92e3d57dd57fbeead263d5a4c915ea25bd5205" } diff --git a/.sqlx/query-472e3903cf3df3c5938527c5584a5a53edc5b492a6bb12eac3d97f3ebc5f8506.json b/.sqlx/query-8a9afd0b7e2f96be85230b35190fab658d841589e7670a4b3fcbc9d53cd1c250.json similarity index 83% rename from .sqlx/query-472e3903cf3df3c5938527c5584a5a53edc5b492a6bb12eac3d97f3ebc5f8506.json rename to .sqlx/query-8a9afd0b7e2f96be85230b35190fab658d841589e7670a4b3fcbc9d53cd1c250.json index 68e6a6c07..eed19b980 100644 --- a/.sqlx/query-472e3903cf3df3c5938527c5584a5a53edc5b492a6bb12eac3d97f3ebc5f8506.json +++ b/.sqlx/query-8a9afd0b7e2f96be85230b35190fab658d841589e7670a4b3fcbc9d53cd1c250.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT * FROM proxy", + "query": "SELECT id, name, address, port, connected_at, disconnected_at, version, enabled, certificate_serial, certificate_expiry, modified_at, modified_by FROM proxy", "describe": { "columns": [ { @@ -35,33 +35,33 @@ }, { "ordinal": 6, - "name": "certificate_expiry", - "type_info": "Timestamp" + "name": "version", + "type_info": "Text" }, { "ordinal": 7, - "name": "version", - "type_info": "Text" + "name": "enabled", + "type_info": "Bool" }, { "ordinal": 8, - "name": "modified_at", - "type_info": "Timestamp" + "name": "certificate_serial", + "type_info": "Text" }, { "ordinal": 9, - "name": "certificate", - "type_info": "Text" + "name": "certificate_expiry", + "type_info": "Timestamp" }, { "ordinal": 10, - "name": "modified_by", - "type_info": "Text" + "name": "modified_at", + "type_info": "Timestamp" }, { "ordinal": 11, - "name": "enabled", - "type_info": "Bool" + "name": "modified_by", + "type_info": "Text" } ], "parameters": { @@ -75,12 +75,12 @@ true, true, true, - true, false, true, + true, false, false ] }, - "hash": "472e3903cf3df3c5938527c5584a5a53edc5b492a6bb12eac3d97f3ebc5f8506" + "hash": "8a9afd0b7e2f96be85230b35190fab658d841589e7670a4b3fcbc9d53cd1c250" } diff --git a/.sqlx/query-f14171d837b8ac91e765e9b86153186ac78bf78ce3cfc5af7441d84be52749d2.json b/.sqlx/query-8d142b160cee06ddc6b3578b3808d731e8c60d5284b8a7ed29b1e32601b667f6.json similarity index 67% rename from .sqlx/query-f14171d837b8ac91e765e9b86153186ac78bf78ce3cfc5af7441d84be52749d2.json rename to .sqlx/query-8d142b160cee06ddc6b3578b3808d731e8c60d5284b8a7ed29b1e32601b667f6.json index 448d27be4..f0683b573 100644 --- a/.sqlx/query-f14171d837b8ac91e765e9b86153186ac78bf78ce3cfc5af7441d84be52749d2.json +++ b/.sqlx/query-8d142b160cee06ddc6b3578b3808d731e8c60d5284b8a7ed29b1e32601b667f6.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"name\",\"address\",\"port\",\"connected_at\",\"disconnected_at\",\"version\",\"enabled\",\"certificate\",\"certificate_expiry\",\"modified_at\",\"modified_by\" FROM \"proxy\"", + "query": "SELECT id, \"name\",\"address\",\"port\",\"connected_at\",\"disconnected_at\",\"version\",\"enabled\",\"certificate_serial\",\"certificate_expiry\",\"modified_at\",\"modified_by\",\"core_client_cert_der\",\"core_client_cert_key_der\",\"core_client_cert_expiry\" FROM \"proxy\"", "describe": { "columns": [ { @@ -45,7 +45,7 @@ }, { "ordinal": 8, - "name": "certificate", + "name": "certificate_serial", "type_info": "Text" }, { @@ -62,6 +62,21 @@ "ordinal": 11, "name": "modified_by", "type_info": "Text" + }, + { + "ordinal": 12, + "name": "core_client_cert_der", + "type_info": "Bytea" + }, + { + "ordinal": 13, + "name": "core_client_cert_key_der", + "type_info": "Bytea" + }, + { + "ordinal": 14, + "name": "core_client_cert_expiry", + "type_info": "Timestamp" } ], "parameters": { @@ -79,8 +94,11 @@ true, true, false, - false + false, + true, + true, + true ] }, - "hash": "f14171d837b8ac91e765e9b86153186ac78bf78ce3cfc5af7441d84be52749d2" + "hash": "8d142b160cee06ddc6b3578b3808d731e8c60d5284b8a7ed29b1e32601b667f6" } diff --git a/.sqlx/query-0fc80b6949eaaeda77dabad7093bca70bd327c14eea4b8db1c9f11c722a00bf4.json b/.sqlx/query-93a4240e469c663e4038cee30aa9c81e888b03cb8d158ed3770a4bc19bae6b22.json similarity index 68% rename from .sqlx/query-0fc80b6949eaaeda77dabad7093bca70bd327c14eea4b8db1c9f11c722a00bf4.json rename to .sqlx/query-93a4240e469c663e4038cee30aa9c81e888b03cb8d158ed3770a4bc19bae6b22.json index 6bac2b9d1..21323cb26 100644 --- a/.sqlx/query-0fc80b6949eaaeda77dabad7093bca70bd327c14eea4b8db1c9f11c722a00bf4.json +++ b/.sqlx/query-93a4240e469c663e4038cee30aa9c81e888b03cb8d158ed3770a4bc19bae6b22.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"location_id\",\"name\",\"address\",\"port\",\"connected_at\",\"disconnected_at\",\"certificate\",\"certificate_expiry\",\"version\",\"enabled\",\"modified_at\",\"modified_by\" FROM \"gateway\"", + "query": "SELECT id, \"location_id\",\"name\",\"address\",\"port\",\"connected_at\",\"disconnected_at\",\"certificate_serial\",\"certificate_expiry\",\"version\",\"enabled\",\"modified_at\",\"modified_by\",\"core_client_cert_der\",\"core_client_cert_key_der\",\"core_client_cert_expiry\" FROM \"gateway\"", "describe": { "columns": [ { @@ -40,7 +40,7 @@ }, { "ordinal": 7, - "name": "certificate", + "name": "certificate_serial", "type_info": "Text" }, { @@ -67,6 +67,21 @@ "ordinal": 12, "name": "modified_by", "type_info": "Text" + }, + { + "ordinal": 13, + "name": "core_client_cert_der", + "type_info": "Bytea" + }, + { + "ordinal": 14, + "name": "core_client_cert_key_der", + "type_info": "Bytea" + }, + { + "ordinal": 15, + "name": "core_client_cert_expiry", + "type_info": "Timestamp" } ], "parameters": { @@ -85,8 +100,11 @@ true, false, false, - false + false, + true, + true, + true ] }, - "hash": "0fc80b6949eaaeda77dabad7093bca70bd327c14eea4b8db1c9f11c722a00bf4" + "hash": "93a4240e469c663e4038cee30aa9c81e888b03cb8d158ed3770a4bc19bae6b22" } diff --git a/.sqlx/query-127e21275639df135aaa9fce8dd1c2b3dffb146a8eacd47faced4894772cfacf.json b/.sqlx/query-9ec638cdabc0500b54cedc5ba18c7745a48080f1e8faa7f7bf7ffae65f2b6ebf.json similarity index 74% rename from .sqlx/query-127e21275639df135aaa9fce8dd1c2b3dffb146a8eacd47faced4894772cfacf.json rename to .sqlx/query-9ec638cdabc0500b54cedc5ba18c7745a48080f1e8faa7f7bf7ffae65f2b6ebf.json index 2f03d82e6..259fafa7d 100644 --- a/.sqlx/query-127e21275639df135aaa9fce8dd1c2b3dffb146a8eacd47faced4894772cfacf.json +++ b/.sqlx/query-9ec638cdabc0500b54cedc5ba18c7745a48080f1e8faa7f7bf7ffae65f2b6ebf.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT gateway.*, CASE WHEN gateway.connected_at IS NULL THEN false WHEN gateway.disconnected_at IS NULL THEN true WHEN gateway.connected_at >= gateway.disconnected_at THEN true ELSE false END AS \"connected!\", wn.name AS location_name FROM gateway JOIN wireguard_network wn ON gateway.location_id = wn.id WHERE location_id = $1", + "query": "SELECT g.id, g.location_id, g.name, g.address, g.port, g.connected_at, g.disconnected_at, CASE WHEN g.connected_at IS NULL THEN false WHEN g.disconnected_at IS NULL THEN true WHEN g.connected_at >= g.disconnected_at THEN true ELSE false END AS \"connected!\", g.certificate_serial, g.certificate_expiry, g.version, g.enabled, g.modified_at, g.modified_by, wn.name AS location_name FROM gateway g JOIN wireguard_network wn ON g.location_id = wn.id WHERE location_id = $1", "describe": { "columns": [ { @@ -15,48 +15,48 @@ }, { "ordinal": 2, - "name": "connected_at", - "type_info": "Timestamp" + "name": "name", + "type_info": "Text" }, { "ordinal": 3, - "name": "disconnected_at", - "type_info": "Timestamp" + "name": "address", + "type_info": "Text" }, { "ordinal": 4, - "name": "certificate_expiry", - "type_info": "Timestamp" + "name": "port", + "type_info": "Int4" }, { "ordinal": 5, - "name": "version", - "type_info": "Text" + "name": "connected_at", + "type_info": "Timestamp" }, { "ordinal": 6, - "name": "name", - "type_info": "Text" + "name": "disconnected_at", + "type_info": "Timestamp" }, { "ordinal": 7, - "name": "certificate", - "type_info": "Text" + "name": "connected!", + "type_info": "Bool" }, { "ordinal": 8, - "name": "address", + "name": "certificate_serial", "type_info": "Text" }, { "ordinal": 9, - "name": "port", - "type_info": "Int4" + "name": "certificate_expiry", + "type_info": "Timestamp" }, { "ordinal": 10, - "name": "modified_at", - "type_info": "Timestamp" + "name": "version", + "type_info": "Text" }, { "ordinal": 11, @@ -65,13 +65,13 @@ }, { "ordinal": 12, - "name": "modified_by", - "type_info": "Text" + "name": "modified_at", + "type_info": "Timestamp" }, { "ordinal": 13, - "name": "connected!", - "type_info": "Bool" + "name": "modified_by", + "type_info": "Text" }, { "ordinal": 14, @@ -85,22 +85,22 @@ ] }, "nullable": [ + false, + false, + false, false, false, true, true, + null, true, true, - false, true, false, false, false, - false, - false, - null, false ] }, - "hash": "127e21275639df135aaa9fce8dd1c2b3dffb146a8eacd47faced4894772cfacf" + "hash": "9ec638cdabc0500b54cedc5ba18c7745a48080f1e8faa7f7bf7ffae65f2b6ebf" } diff --git a/.sqlx/query-8d21a38672059e820d355590df83c1c9c5f75956f8b7c2a1a235189e1583a599.json b/.sqlx/query-a05a752af1643a1d9f9b9544df2055218f9c51b0ae143a9a1c5ff9e13dab9c75.json similarity index 59% rename from .sqlx/query-8d21a38672059e820d355590df83c1c9c5f75956f8b7c2a1a235189e1583a599.json rename to .sqlx/query-a05a752af1643a1d9f9b9544df2055218f9c51b0ae143a9a1c5ff9e13dab9c75.json index 4ea6dccd4..e207617bf 100644 --- a/.sqlx/query-8d21a38672059e820d355590df83c1c9c5f75956f8b7c2a1a235189e1583a599.json +++ b/.sqlx/query-a05a752af1643a1d9f9b9544df2055218f9c51b0ae143a9a1c5ff9e13dab9c75.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"gateway\" (\"location_id\",\"name\",\"address\",\"port\",\"connected_at\",\"disconnected_at\",\"certificate\",\"certificate_expiry\",\"version\",\"enabled\",\"modified_at\",\"modified_by\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) RETURNING id", + "query": "INSERT INTO \"gateway\" (\"location_id\",\"name\",\"address\",\"port\",\"connected_at\",\"disconnected_at\",\"certificate_serial\",\"certificate_expiry\",\"version\",\"enabled\",\"modified_at\",\"modified_by\",\"core_client_cert_der\",\"core_client_cert_key_der\",\"core_client_cert_expiry\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) RETURNING id", "describe": { "columns": [ { @@ -22,12 +22,15 @@ "Text", "Bool", "Timestamp", - "Text" + "Text", + "Bytea", + "Bytea", + "Timestamp" ] }, "nullable": [ false ] }, - "hash": "8d21a38672059e820d355590df83c1c9c5f75956f8b7c2a1a235189e1583a599" + "hash": "a05a752af1643a1d9f9b9544df2055218f9c51b0ae143a9a1c5ff9e13dab9c75" } diff --git a/.sqlx/query-a41787c8c8307414165ab23ef96d82a34d3bfa4364cbe9b8368e71445bc20877.json b/.sqlx/query-a41787c8c8307414165ab23ef96d82a34d3bfa4364cbe9b8368e71445bc20877.json deleted file mode 100644 index 87ce4e720..000000000 --- a/.sqlx/query-a41787c8c8307414165ab23ef96d82a34d3bfa4364cbe9b8368e71445bc20877.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT * FROM proxy WHERE address = $1 AND port = $2", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "name", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "address", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "port", - "type_info": "Int4" - }, - { - "ordinal": 4, - "name": "connected_at", - "type_info": "Timestamp" - }, - { - "ordinal": 5, - "name": "disconnected_at", - "type_info": "Timestamp" - }, - { - "ordinal": 6, - "name": "certificate_expiry", - "type_info": "Timestamp" - }, - { - "ordinal": 7, - "name": "version", - "type_info": "Text" - }, - { - "ordinal": 8, - "name": "modified_at", - "type_info": "Timestamp" - }, - { - "ordinal": 9, - "name": "certificate", - "type_info": "Text" - }, - { - "ordinal": 10, - "name": "modified_by", - "type_info": "Text" - }, - { - "ordinal": 11, - "name": "enabled", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Text", - "Int4" - ] - }, - "nullable": [ - false, - false, - false, - false, - true, - true, - true, - true, - false, - true, - false, - false - ] - }, - "hash": "a41787c8c8307414165ab23ef96d82a34d3bfa4364cbe9b8368e71445bc20877" -} diff --git a/.sqlx/query-bda99bacb002f781dd276c5623d399dec458cb9250fc448bc6edf94b5e90805b.json b/.sqlx/query-bda99bacb002f781dd276c5623d399dec458cb9250fc448bc6edf94b5e90805b.json new file mode 100644 index 000000000..0b870a60c --- /dev/null +++ b/.sqlx/query-bda99bacb002f781dd276c5623d399dec458cb9250fc448bc6edf94b5e90805b.json @@ -0,0 +1,113 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, location_id, name, address, port, connected_at, disconnected_at, certificate_serial, certificate_expiry, version, enabled, modified_at, modified_by, core_client_cert_der, core_client_cert_key_der, core_client_cert_expiry FROM gateway WHERE address = $1 AND port = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "location_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "address", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "port", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "connected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 6, + "name": "disconnected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 7, + "name": "certificate_serial", + "type_info": "Text" + }, + { + "ordinal": 8, + "name": "certificate_expiry", + "type_info": "Timestamp" + }, + { + "ordinal": 9, + "name": "version", + "type_info": "Text" + }, + { + "ordinal": 10, + "name": "enabled", + "type_info": "Bool" + }, + { + "ordinal": 11, + "name": "modified_at", + "type_info": "Timestamp" + }, + { + "ordinal": 12, + "name": "modified_by", + "type_info": "Text" + }, + { + "ordinal": 13, + "name": "core_client_cert_der", + "type_info": "Bytea" + }, + { + "ordinal": 14, + "name": "core_client_cert_key_der", + "type_info": "Bytea" + }, + { + "ordinal": 15, + "name": "core_client_cert_expiry", + "type_info": "Timestamp" + } + ], + "parameters": { + "Left": [ + "Text", + "Int4" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + true, + true, + true, + true, + false, + false, + false, + true, + true, + true + ] + }, + "hash": "bda99bacb002f781dd276c5623d399dec458cb9250fc448bc6edf94b5e90805b" +} diff --git a/.sqlx/query-beffd1aad66ce9d9a179f14e224d9ca63f0c0aa378460bd344f7a7daa8985bad.json b/.sqlx/query-d4c6847ef8197f425c3cb19964509f11c97ab5d68c7ccd1c173aa8212a659770.json similarity index 68% rename from .sqlx/query-beffd1aad66ce9d9a179f14e224d9ca63f0c0aa378460bd344f7a7daa8985bad.json rename to .sqlx/query-d4c6847ef8197f425c3cb19964509f11c97ab5d68c7ccd1c173aa8212a659770.json index 3c397182d..249ff49ab 100644 --- a/.sqlx/query-beffd1aad66ce9d9a179f14e224d9ca63f0c0aa378460bd344f7a7daa8985bad.json +++ b/.sqlx/query-d4c6847ef8197f425c3cb19964509f11c97ab5d68c7ccd1c173aa8212a659770.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"location_id\",\"name\",\"address\",\"port\",\"connected_at\",\"disconnected_at\",\"certificate\",\"certificate_expiry\",\"version\",\"enabled\",\"modified_at\",\"modified_by\" FROM \"gateway\" WHERE id = $1", + "query": "SELECT id, \"location_id\",\"name\",\"address\",\"port\",\"connected_at\",\"disconnected_at\",\"certificate_serial\",\"certificate_expiry\",\"version\",\"enabled\",\"modified_at\",\"modified_by\",\"core_client_cert_der\",\"core_client_cert_key_der\",\"core_client_cert_expiry\" FROM \"gateway\" WHERE id = $1", "describe": { "columns": [ { @@ -40,7 +40,7 @@ }, { "ordinal": 7, - "name": "certificate", + "name": "certificate_serial", "type_info": "Text" }, { @@ -67,6 +67,21 @@ "ordinal": 12, "name": "modified_by", "type_info": "Text" + }, + { + "ordinal": 13, + "name": "core_client_cert_der", + "type_info": "Bytea" + }, + { + "ordinal": 14, + "name": "core_client_cert_key_der", + "type_info": "Bytea" + }, + { + "ordinal": 15, + "name": "core_client_cert_expiry", + "type_info": "Timestamp" } ], "parameters": { @@ -87,8 +102,11 @@ true, false, false, - false + false, + true, + true, + true ] }, - "hash": "beffd1aad66ce9d9a179f14e224d9ca63f0c0aa378460bd344f7a7daa8985bad" + "hash": "d4c6847ef8197f425c3cb19964509f11c97ab5d68c7ccd1c173aa8212a659770" } diff --git a/.sqlx/query-c2bf39775cc4142f18165b1e342731b0d6b7370543b5c5e8cabdfd47487a3056.json b/.sqlx/query-d9dc6788c19efa7b1ec9060651398e85a165c0e03167abd4940b248e8d29ccf1.json similarity index 67% rename from .sqlx/query-c2bf39775cc4142f18165b1e342731b0d6b7370543b5c5e8cabdfd47487a3056.json rename to .sqlx/query-d9dc6788c19efa7b1ec9060651398e85a165c0e03167abd4940b248e8d29ccf1.json index a1db3a8fb..5e4f9de11 100644 --- a/.sqlx/query-c2bf39775cc4142f18165b1e342731b0d6b7370543b5c5e8cabdfd47487a3056.json +++ b/.sqlx/query-d9dc6788c19efa7b1ec9060651398e85a165c0e03167abd4940b248e8d29ccf1.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"name\",\"address\",\"port\",\"connected_at\",\"disconnected_at\",\"version\",\"enabled\",\"certificate\",\"certificate_expiry\",\"modified_at\",\"modified_by\" FROM \"proxy\" LIMIT $1 OFFSET $2", + "query": "SELECT id, \"name\",\"address\",\"port\",\"connected_at\",\"disconnected_at\",\"version\",\"enabled\",\"certificate_serial\",\"certificate_expiry\",\"modified_at\",\"modified_by\",\"core_client_cert_der\",\"core_client_cert_key_der\",\"core_client_cert_expiry\" FROM \"proxy\" LIMIT $1 OFFSET $2", "describe": { "columns": [ { @@ -45,7 +45,7 @@ }, { "ordinal": 8, - "name": "certificate", + "name": "certificate_serial", "type_info": "Text" }, { @@ -62,6 +62,21 @@ "ordinal": 11, "name": "modified_by", "type_info": "Text" + }, + { + "ordinal": 12, + "name": "core_client_cert_der", + "type_info": "Bytea" + }, + { + "ordinal": 13, + "name": "core_client_cert_key_der", + "type_info": "Bytea" + }, + { + "ordinal": 14, + "name": "core_client_cert_expiry", + "type_info": "Timestamp" } ], "parameters": { @@ -82,8 +97,11 @@ true, true, false, - false + false, + true, + true, + true ] }, - "hash": "c2bf39775cc4142f18165b1e342731b0d6b7370543b5c5e8cabdfd47487a3056" + "hash": "d9dc6788c19efa7b1ec9060651398e85a165c0e03167abd4940b248e8d29ccf1" } diff --git a/.sqlx/query-e2f43cdd1dd50ab2b97b1cdae80f6354bd99ff1f1f9adc02837ccbc458ee8be9.json b/.sqlx/query-e2f43cdd1dd50ab2b97b1cdae80f6354bd99ff1f1f9adc02837ccbc458ee8be9.json new file mode 100644 index 000000000..c81bbaf92 --- /dev/null +++ b/.sqlx/query-e2f43cdd1dd50ab2b97b1cdae80f6354bd99ff1f1f9adc02837ccbc458ee8be9.json @@ -0,0 +1,104 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, name, address, port, connected_at, disconnected_at, version, enabled, certificate_serial, certificate_expiry, modified_at, modified_by, core_client_cert_der, core_client_cert_key_der, core_client_cert_expiry FROM proxy WHERE enabled AND id NOT IN (SELECT id FROM proxy WHERE enabled LIMIT 1\n )", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "address", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "port", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "connected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 5, + "name": "disconnected_at", + "type_info": "Timestamp" + }, + { + "ordinal": 6, + "name": "version", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "enabled", + "type_info": "Bool" + }, + { + "ordinal": 8, + "name": "certificate_serial", + "type_info": "Text" + }, + { + "ordinal": 9, + "name": "certificate_expiry", + "type_info": "Timestamp" + }, + { + "ordinal": 10, + "name": "modified_at", + "type_info": "Timestamp" + }, + { + "ordinal": 11, + "name": "modified_by", + "type_info": "Text" + }, + { + "ordinal": 12, + "name": "core_client_cert_der", + "type_info": "Bytea" + }, + { + "ordinal": 13, + "name": "core_client_cert_key_der", + "type_info": "Bytea" + }, + { + "ordinal": 14, + "name": "core_client_cert_expiry", + "type_info": "Timestamp" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + true, + true, + true, + false, + true, + true, + false, + false, + true, + true, + true + ] + }, + "hash": "e2f43cdd1dd50ab2b97b1cdae80f6354bd99ff1f1f9adc02837ccbc458ee8be9" +} diff --git a/.sqlx/query-bcb405dc3159cd72c5ccebf29bf4b6163ee0e324cd95cbf3e32d025b5ba7fcbb.json b/.sqlx/query-eb50eae3a1786cf685b8ec084e905afc8c9c0ae34ffe29bf6adb2a89f1ae6edb.json similarity index 67% rename from .sqlx/query-bcb405dc3159cd72c5ccebf29bf4b6163ee0e324cd95cbf3e32d025b5ba7fcbb.json rename to .sqlx/query-eb50eae3a1786cf685b8ec084e905afc8c9c0ae34ffe29bf6adb2a89f1ae6edb.json index ce8b4b42a..7f8ec2a13 100644 --- a/.sqlx/query-bcb405dc3159cd72c5ccebf29bf4b6163ee0e324cd95cbf3e32d025b5ba7fcbb.json +++ b/.sqlx/query-eb50eae3a1786cf685b8ec084e905afc8c9c0ae34ffe29bf6adb2a89f1ae6edb.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"name\",\"address\",\"port\",\"connected_at\",\"disconnected_at\",\"version\",\"enabled\",\"certificate\",\"certificate_expiry\",\"modified_at\",\"modified_by\" FROM \"proxy\" WHERE id = $1", + "query": "SELECT id, \"name\",\"address\",\"port\",\"connected_at\",\"disconnected_at\",\"version\",\"enabled\",\"certificate_serial\",\"certificate_expiry\",\"modified_at\",\"modified_by\",\"core_client_cert_der\",\"core_client_cert_key_der\",\"core_client_cert_expiry\" FROM \"proxy\" WHERE id = $1", "describe": { "columns": [ { @@ -45,7 +45,7 @@ }, { "ordinal": 8, - "name": "certificate", + "name": "certificate_serial", "type_info": "Text" }, { @@ -62,6 +62,21 @@ "ordinal": 11, "name": "modified_by", "type_info": "Text" + }, + { + "ordinal": 12, + "name": "core_client_cert_der", + "type_info": "Bytea" + }, + { + "ordinal": 13, + "name": "core_client_cert_key_der", + "type_info": "Bytea" + }, + { + "ordinal": 14, + "name": "core_client_cert_expiry", + "type_info": "Timestamp" } ], "parameters": { @@ -81,8 +96,11 @@ true, true, false, - false + false, + true, + true, + true ] }, - "hash": "bcb405dc3159cd72c5ccebf29bf4b6163ee0e324cd95cbf3e32d025b5ba7fcbb" + "hash": "eb50eae3a1786cf685b8ec084e905afc8c9c0ae34ffe29bf6adb2a89f1ae6edb" } diff --git a/.sqlx/query-780d66e4628d13c6c2f489cc87c7358945b93628104ac57aac207b8ec74be08a.json b/.sqlx/query-f0bd5b48faffc4152e2683d4aecfcfb5a2496f961ead8474e8ed5e290cabec85.json similarity index 54% rename from .sqlx/query-780d66e4628d13c6c2f489cc87c7358945b93628104ac57aac207b8ec74be08a.json rename to .sqlx/query-f0bd5b48faffc4152e2683d4aecfcfb5a2496f961ead8474e8ed5e290cabec85.json index 332b4b8b4..62d5dfa72 100644 --- a/.sqlx/query-780d66e4628d13c6c2f489cc87c7358945b93628104ac57aac207b8ec74be08a.json +++ b/.sqlx/query-f0bd5b48faffc4152e2683d4aecfcfb5a2496f961ead8474e8ed5e290cabec85.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"proxy\" SET \"name\" = $2,\"address\" = $3,\"port\" = $4,\"connected_at\" = $5,\"disconnected_at\" = $6,\"version\" = $7,\"enabled\" = $8,\"certificate\" = $9,\"certificate_expiry\" = $10,\"modified_at\" = $11,\"modified_by\" = $12 WHERE id = $1", + "query": "UPDATE \"proxy\" SET \"name\" = $2,\"address\" = $3,\"port\" = $4,\"connected_at\" = $5,\"disconnected_at\" = $6,\"version\" = $7,\"enabled\" = $8,\"certificate_serial\" = $9,\"certificate_expiry\" = $10,\"modified_at\" = $11,\"modified_by\" = $12,\"core_client_cert_der\" = $13,\"core_client_cert_key_der\" = $14,\"core_client_cert_expiry\" = $15 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -16,10 +16,13 @@ "Text", "Timestamp", "Timestamp", - "Text" + "Text", + "Bytea", + "Bytea", + "Timestamp" ] }, "nullable": [] }, - "hash": "780d66e4628d13c6c2f489cc87c7358945b93628104ac57aac207b8ec74be08a" + "hash": "f0bd5b48faffc4152e2683d4aecfcfb5a2496f961ead8474e8ed5e290cabec85" } diff --git a/.sqlx/query-4d9c4562a138038ba054b5b83b646341ee18e24f0d32399e6ce2ebaedef64cea.json b/.sqlx/query-fd40448d1bff2b70f119211c44b9a2e564bdf787b0f02dd244b0d01186d56e0b.json similarity index 67% rename from .sqlx/query-4d9c4562a138038ba054b5b83b646341ee18e24f0d32399e6ce2ebaedef64cea.json rename to .sqlx/query-fd40448d1bff2b70f119211c44b9a2e564bdf787b0f02dd244b0d01186d56e0b.json index 0f433d31d..47128bca0 100644 --- a/.sqlx/query-4d9c4562a138038ba054b5b83b646341ee18e24f0d32399e6ce2ebaedef64cea.json +++ b/.sqlx/query-fd40448d1bff2b70f119211c44b9a2e564bdf787b0f02dd244b0d01186d56e0b.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT * FROM gateway WHERE address = $1 AND port = $2", + "query": "SELECT id, name, address, port, connected_at, disconnected_at, version, enabled, certificate_serial, certificate_expiry, modified_at, modified_by, core_client_cert_der, core_client_cert_key_der, core_client_cert_expiry FROM proxy WHERE address = $1 AND port = $2", "describe": { "columns": [ { @@ -10,48 +10,48 @@ }, { "ordinal": 1, - "name": "location_id", - "type_info": "Int8" + "name": "name", + "type_info": "Text" }, { "ordinal": 2, - "name": "connected_at", - "type_info": "Timestamp" + "name": "address", + "type_info": "Text" }, { "ordinal": 3, - "name": "disconnected_at", - "type_info": "Timestamp" + "name": "port", + "type_info": "Int4" }, { "ordinal": 4, - "name": "certificate_expiry", + "name": "connected_at", "type_info": "Timestamp" }, { "ordinal": 5, - "name": "version", - "type_info": "Text" + "name": "disconnected_at", + "type_info": "Timestamp" }, { "ordinal": 6, - "name": "name", + "name": "version", "type_info": "Text" }, { "ordinal": 7, - "name": "certificate", - "type_info": "Text" + "name": "enabled", + "type_info": "Bool" }, { "ordinal": 8, - "name": "address", + "name": "certificate_serial", "type_info": "Text" }, { "ordinal": 9, - "name": "port", - "type_info": "Int4" + "name": "certificate_expiry", + "type_info": "Timestamp" }, { "ordinal": 10, @@ -60,13 +60,23 @@ }, { "ordinal": 11, - "name": "enabled", - "type_info": "Bool" + "name": "modified_by", + "type_info": "Text" }, { "ordinal": 12, - "name": "modified_by", - "type_info": "Text" + "name": "core_client_cert_der", + "type_info": "Bytea" + }, + { + "ordinal": 13, + "name": "core_client_cert_key_der", + "type_info": "Bytea" + }, + { + "ordinal": 14, + "name": "core_client_cert_expiry", + "type_info": "Timestamp" } ], "parameters": { @@ -78,18 +88,20 @@ "nullable": [ false, false, - true, + false, + false, true, true, true, false, true, + true, false, false, - false, - false, - false + true, + true, + true ] }, - "hash": "4d9c4562a138038ba054b5b83b646341ee18e24f0d32399e6ce2ebaedef64cea" + "hash": "fd40448d1bff2b70f119211c44b9a2e564bdf787b0f02dd244b0d01186d56e0b" } From 72a6569c31e4dff71cab5d27b7f2e4afc1ec47c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 14 Apr 2026 21:27:44 +0200 Subject: [PATCH 05/46] generate and store core client certs during component setup --- crates/defguard_certs/src/lib.rs | 37 ++++++ .../src/handlers/component_setup.rs | 36 +++++- crates/defguard_setup/src/auto_adoption.rs | 112 +++++++++++++++--- 3 files changed, 164 insertions(+), 21 deletions(-) diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs index 32f51853c..dec24486c 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -143,6 +143,31 @@ impl CertificateAuthority<'_> { Ok(cert) } + /// Issue a Core gRPC client certificate for a specific gateway or proxy. + /// + /// Generates a fresh key pair, creates a CSR with `common_name` as both + /// the Subject CN and the SAN DNS name, signs it with `ClientAuth` EKU, + /// and returns all materials needed to store in the database and build a + /// [`CertBundle`]. + pub fn issue_core_client_cert( + &self, + common_name: &str, + ) -> Result { + let key_pair = generate_key_pair()?; + let csr = Csr::new( + &key_pair, + &[common_name.to_string()], + vec![(rcgen::DnType::CommonName, common_name)], + )?; + let cert = self.sign_client_cert(&csr)?; + let expiry = CertificateInfo::from_der(cert.der())?.not_after; + Ok(CoreClientCert { + cert_der: cert.der().to_vec(), + key_der: key_pair.serialized_der().to_vec(), + expiry, + }) + } + pub fn cert_pem(&self) -> Result { der_to_pem(self.cert_der.as_ref(), PemLabel::Certificate) } @@ -163,6 +188,18 @@ impl CertificateAuthority<'_> { } } +/// A Core gRPC client certificate issued for a specific gateway or proxy component. +/// +/// The DER bytes are stored in the database; the key bytes never leave Core. +pub struct CoreClientCert { + /// DER-encoded client certificate signed with `ClientAuth` EKU. + pub cert_der: Vec, + /// DER-encoded private key for the client certificate. + pub key_der: Vec, + /// Certificate expiry timestamp (UTC). + pub expiry: NaiveDateTime, +} + pub struct CertificateInfo { pub subject_common_name: String, pub subject_email: Option, diff --git a/crates/defguard_core/src/handlers/component_setup.rs b/crates/defguard_core/src/handlers/component_setup.rs index 22b19e7eb..1603a936b 100644 --- a/crates/defguard_core/src/handlers/component_setup.rs +++ b/crates/defguard_core/src/handlers/component_setup.rs @@ -30,7 +30,7 @@ use defguard_common::{ utils::strip_scheme, }; use defguard_proto::{ - common::{CertBundle, CertificateInfo, DerPayload}, + common::{CertBundle, CertificateInfo}, gateway::gateway_setup_client::GatewaySetupClient, proxy::{ AcmeChallenge, AcmeLogs, AcmeStep, acme_issue_event, proxy_client::ProxyClient, @@ -550,7 +550,19 @@ pub async fn setup_proxy_tls_stream( // Step 6: Configure TLS yield Ok(flow.step(SetupStep::ConfiguringTls)); - let bundle: CertBundle = todo!(); + let core_client = match ca.issue_core_client_cert(&request.common_name) { + Ok(c) => c, + Err(e) => { + yield Ok(flow.error(&format!("Failed to issue Core client certificate: {e}"))); + return; + } + }; + + let bundle = CertBundle { + component_cert_der: cert.der().to_vec(), + ca_cert_der: ca_cert_der.clone(), + core_client_cert_der: core_client.cert_der.clone(), + }; if let Err(e) = client.send_cert(bundle).await { yield Ok(flow.error(&format!("Failed to send certificate: {e}"))); return; @@ -577,6 +589,9 @@ pub async fn setup_proxy_tls_stream( ); proxy.certificate_serial = Some(serial); proxy.certificate_expiry = Some(expiry); + proxy.core_client_cert_der = Some(core_client.cert_der); + proxy.core_client_cert_key_der = Some(core_client.key_der); + proxy.core_client_cert_expiry = Some(core_client.expiry); let proxy = match proxy.save(&pool).await { Ok(p) => p, @@ -997,11 +1012,19 @@ pub async fn setup_gateway_tls_stream( // Step 6: Configure TLS yield Ok(flow.step(SetupStep::ConfiguringTls)); - let response = DerPayload { - der_data: cert.der().to_vec(), + let core_client = match ca.issue_core_client_cert(&request.common_name) { + Ok(c) => c, + Err(e) => { + yield Ok(flow.error(&format!("Failed to issue Core client certificate: {e}"))); + return; + } }; - let bundle: CertBundle = todo!(); + let bundle = CertBundle { + component_cert_der: cert.der().to_vec(), + ca_cert_der: ca_cert_der.clone(), + core_client_cert_der: core_client.cert_der.clone(), + }; if let Err(e) = client.send_cert(bundle).await { yield Ok(flow.error(&format!("Failed to send certificate: {e}"))); return; @@ -1035,6 +1058,9 @@ pub async fn setup_gateway_tls_stream( gateway.certificate_serial = Some(serial); gateway.certificate_expiry = Some(expiry); + gateway.core_client_cert_der = Some(core_client.cert_der); + gateway.core_client_cert_key_der = Some(core_client.key_der); + gateway.core_client_cert_expiry = Some(core_client.expiry); if let Err(err) = gateway.save(&pool).await { yield Ok(flow.error(&format!("Failed to save Gateway to database: {err}"))); diff --git a/crates/defguard_setup/src/auto_adoption.rs b/crates/defguard_setup/src/auto_adoption.rs index 57a8dfcca..5406506be 100644 --- a/crates/defguard_setup/src/auto_adoption.rs +++ b/crates/defguard_setup/src/auto_adoption.rs @@ -5,7 +5,9 @@ use std::{ }; use anyhow::Context; -use defguard_certs::{CertificateAuthority, CertificateInfo, Csr, PemLabel, der_to_pem}; +use defguard_certs::{ + CertificateAuthority, CertificateInfo, CoreClientCert, Csr, PemLabel, der_to_pem, +}; use defguard_common::{ VERSION, auth::claims::{Claims, ClaimsType}, @@ -26,7 +28,7 @@ use defguard_core::{ version::{MIN_GATEWAY_VERSION, MIN_PROXY_VERSION}, }; use defguard_proto::{ - common::{CertBundle, CertificateInfo as ProtoCertificateInfo, DerPayload as ProtoDerPayload}, + common::{CertBundle, CertificateInfo as ProtoCertificateInfo}, gateway::gateway_setup_client::GatewaySetupClient, proxy::proxy_setup_client::ProxySetupClient, }; @@ -188,7 +190,7 @@ fn merge_failure_logs( message: impl Into, log_buffer: &SetupLogBuffer, log_rx: &mut UnboundedReceiver, -) -> (bool, Vec, Option) { +) -> (bool, Vec, Option) { let msg = message.into(); error!("{msg}"); let mut logs = collect_core_logs(log_buffer); @@ -200,11 +202,21 @@ fn logs_to_persist(success: bool, logs: Vec) -> Vec { if success { Vec::new() } else { logs } } +/// Carries the result of a successful component adoption attempt. +/// +/// Bundles the parsed certificate metadata with the Core gRPC client +/// certificate materials so that both can be persisted in the same DB +/// transaction without re-issuing the client cert. +struct ComponentAdoptionResult { + cert_info: CertificateInfo, + core_client: CoreClientCert, +} + async fn run_edge_adoption_attempt( pool: &PgPool, host: &str, port: u16, -) -> (bool, Vec, Option) { +) -> (bool, Vec, Option) { let log_buffer = Arc::new(Mutex::new(VecDeque::new())); let certs = match Certificates::get_or_default(pool).await { Ok(c) => c, @@ -236,7 +248,7 @@ async fn run_edge_adoption_attempt_scoped( log_buffer: SetupLogBuffer, ca_cert_der: Vec, ca_key_der: Vec, -) -> (bool, Vec, Option) { +) -> (bool, Vec, Option) { debug!("Starting edge adoption attempt host={host} port={port}"); let (log_tx, mut log_rx) = tokio::sync::mpsc::unbounded_channel::(); let endpoint_str = format!("http://{host}:{port}"); @@ -469,7 +481,22 @@ async fn run_edge_adoption_attempt_scoped( }; debug!("CSR signed for proxy hostname={hostname}; sending certificate"); - let bundle: CertBundle = todo!(); + let core_client = match ca.issue_core_client_cert(hostname) { + Ok(c) => c, + Err(err) => { + return merge_failure_logs( + format!("Failed to issue Core client certificate for proxy: {err}"), + &log_buffer, + &mut log_rx, + ); + } + }; + + let bundle = CertBundle { + component_cert_der: cert.der().to_vec(), + ca_cert_der: ca_cert_der.clone(), + core_client_cert_der: core_client.cert_der.clone(), + }; if let Err(err) = client.send_cert(bundle).await { return merge_failure_logs( format!("Failed to send certificate to proxy: {err}"), @@ -499,14 +526,21 @@ async fn run_edge_adoption_attempt_scoped( logs = vec!["No runtime logs received from edge component".to_string()]; } - (true, logs, Some(cert_info)) + ( + true, + logs, + Some(ComponentAdoptionResult { + cert_info, + core_client, + }), + ) } async fn run_gateway_adoption_attempt( pool: &PgPool, host: &str, port: u16, -) -> (bool, Vec, Option) { +) -> (bool, Vec, Option) { let log_buffer = Arc::new(Mutex::new(VecDeque::new())); let certs = match Certificates::get_or_default(pool).await { Ok(c) => c, @@ -538,7 +572,7 @@ async fn run_gateway_adoption_attempt_scoped( log_buffer: SetupLogBuffer, ca_cert_der: Vec, ca_key_der: Vec, -) -> (bool, Vec, Option) { +) -> (bool, Vec, Option) { debug!("Starting gateway adoption attempt host={host} port={port}"); let (log_tx, mut log_rx) = tokio::sync::mpsc::unbounded_channel::(); @@ -774,7 +808,22 @@ async fn run_gateway_adoption_attempt_scoped( }; debug!("CSR signed for gateway hostname={hostname}; sending certificate"); - let bundle: CertBundle = todo!(); + let core_client = match ca.issue_core_client_cert(hostname) { + Ok(c) => c, + Err(err) => { + return merge_failure_logs( + format!("Failed to issue Core client certificate for gateway: {err}"), + &log_buffer, + &mut log_rx, + ); + } + }; + + let bundle = CertBundle { + component_cert_der: cert.der().to_vec(), + ca_cert_der: ca_cert_der.clone(), + core_client_cert_der: core_client.cert_der.clone(), + }; if let Err(err) = client.send_cert(bundle).await { return merge_failure_logs( format!("Failed to send certificate to gateway: {err}"), @@ -804,7 +853,14 @@ async fn run_gateway_adoption_attempt_scoped( logs = vec!["No runtime logs received from gateway component".to_string()]; } - (true, logs, Some(cert_info)) + ( + true, + logs, + Some(ComponentAdoptionResult { + cert_info, + core_client, + }), + ) } // Default WireGuard network address and port used when auto-adopting a gateway without an @@ -831,9 +887,16 @@ async fn process_startup_auto_adoption( if status { match component { SetupAutoAdoptionComponent::Gateway => { - if let Some(cert_info) = cert_info { - if let Err(err) = - create_network_and_gateway(pool, &host, port, GATEWAY_NAME, cert_info).await + if let Some(result) = cert_info { + if let Err(err) = create_network_and_gateway( + pool, + &host, + port, + GATEWAY_NAME, + result.cert_info, + result.core_client, + ) + .await { warn!( "Gateway adoption TLS handshake succeeded but failed to persist \ @@ -843,8 +906,17 @@ async fn process_startup_auto_adoption( } } SetupAutoAdoptionComponent::Edge => { - if let Some(cert_info) = cert_info { - if let Err(err) = create_proxy(pool, &host, port, PROXY_NAME, cert_info).await { + if let Some(result) = cert_info { + if let Err(err) = create_proxy( + pool, + &host, + port, + PROXY_NAME, + result.cert_info, + result.core_client, + ) + .await + { warn!( "Edge adoption TLS handshake succeeded but failed to persist \ proxy record: {err}" @@ -881,6 +953,7 @@ async fn create_network_and_gateway( grpc_port: u16, common_name: &str, cert_info: CertificateInfo, + core_client: CoreClientCert, ) -> Result<(), anyhow::Error> { // Re-use or create the network location. let network = if let Some(existing) = WireguardNetwork::find_by_name(pool, common_name) @@ -961,6 +1034,9 @@ id={} for new gateway", ); gateway.certificate_serial = Some(cert_info.serial); gateway.certificate_expiry = Some(cert_info.not_after); + gateway.core_client_cert_der = Some(core_client.cert_der); + gateway.core_client_cert_key_der = Some(core_client.key_der); + gateway.core_client_cert_expiry = Some(core_client.expiry); gateway .save(pool) @@ -983,6 +1059,7 @@ async fn create_proxy( port: u16, common_name: &str, cert_info: CertificateInfo, + core_client: CoreClientCert, ) -> Result<(), anyhow::Error> { if let Some(existing) = Proxy::find_by_address_port(pool, host, i32::from(port)) .await @@ -999,6 +1076,9 @@ async fn create_proxy( let mut proxy = Proxy::new(common_name, host, i32::from(port), "Automatic setup"); proxy.certificate_serial = Some(cert_info.serial); proxy.certificate_expiry = Some(cert_info.not_after); + proxy.core_client_cert_der = Some(core_client.cert_der); + proxy.core_client_cert_key_der = Some(core_client.key_der); + proxy.core_client_cert_expiry = Some(core_client.expiry); proxy .save(pool) From 5dfbcc0fc069205ca755e2484a14178cda3356a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 14 Apr 2026 21:32:42 +0200 Subject: [PATCH 06/46] add helper for generating grpc server TLS config --- Cargo.lock | 1 + crates/defguard_grpc_tls/Cargo.toml | 1 + crates/defguard_grpc_tls/src/certs.rs | 22 ++++++++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 233c07917..6cf3d9975 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1526,6 +1526,7 @@ dependencies = [ "rustls", "thiserror 2.0.18", "tokio", + "tonic", "tower-service", "tracing", "x509-parser 0.18.1", diff --git a/crates/defguard_grpc_tls/Cargo.toml b/crates/defguard_grpc_tls/Cargo.toml index 3d79cb562..e6dcf87e2 100644 --- a/crates/defguard_grpc_tls/Cargo.toml +++ b/crates/defguard_grpc_tls/Cargo.toml @@ -13,6 +13,7 @@ http = "1.1" rustls = { version = "0.23", features = ["ring"] } thiserror.workspace = true tokio.workspace = true +tonic.workspace = true tower-service = "0.3" x509-parser = "0.18" tracing.workspace = true diff --git a/crates/defguard_grpc_tls/src/certs.rs b/crates/defguard_grpc_tls/src/certs.rs index e9f9f44aa..0b158d0d3 100644 --- a/crates/defguard_grpc_tls/src/certs.rs +++ b/crates/defguard_grpc_tls/src/certs.rs @@ -22,6 +22,7 @@ use rustls::{ }; use thiserror::Error; use tokio::sync::watch; +use tonic::transport::{Certificate, Identity, ServerTlsConfig}; use tracing::error; use x509_parser::parse_x509_certificate; @@ -138,6 +139,27 @@ fn root_store_from_ca(ca_cert_der: &[u8]) -> Result Result { + let identity = Identity::from_pem(component_cert_pem, component_key_pem); + let ca = Certificate::from_pem(ca_cert_pem); + Ok(ServerTlsConfig::new().identity(identity).client_ca_root(ca)) +} + /// Create a rustls client config that enforces the pinned component certificate serial. pub fn client_config( ca_cert_der: &[u8], From 7e78ab472a503869408276f8056cebf5e94c4934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 14 Apr 2026 21:52:32 +0200 Subject: [PATCH 07/46] add core client identity to component connections --- .../defguard_gateway_manager/src/handler.rs | 24 +++++++++++-- crates/defguard_grpc_tls/src/certs.rs | 22 ++++++++++-- crates/defguard_proxy_manager/src/handler.rs | 34 +++++++++++++++++-- 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/crates/defguard_gateway_manager/src/handler.rs b/crates/defguard_gateway_manager/src/handler.rs index 21f8b1252..6af5a1bb8 100644 --- a/crates/defguard_gateway_manager/src/handler.rs +++ b/crates/defguard_gateway_manager/src/handler.rs @@ -149,9 +149,27 @@ impl GatewayHandler { "Core CA is not setup, can't create a Gateway endpoint".to_string(), )); }; - let tls_config = - tls_certs::client_config(&ca_cert_der, self.certs_rx.clone(), self.gateway.id) - .map_err(|err| GatewayError::EndpointError(err.to_string()))?; + let Some(core_client_cert_der) = self.gateway.core_client_cert_der.as_deref() else { + return Err(GatewayError::EndpointError(format!( + "Core client certificate not provisioned for gateway id={}", + self.gateway.id + ))); + }; + let Some(core_client_cert_key_der) = self.gateway.core_client_cert_key_der.as_deref() + else { + return Err(GatewayError::EndpointError(format!( + "Core client certificate key not provisioned for gateway id={}", + self.gateway.id + ))); + }; + let tls_config = tls_certs::client_config( + &ca_cert_der, + self.certs_rx.clone(), + self.gateway.id, + core_client_cert_der, + core_client_cert_key_der, + ) + .map_err(|err| GatewayError::EndpointError(err.to_string()))?; let connector = HttpsConnectorBuilder::new() .with_tls_config(tls_config) .https_only() diff --git a/crates/defguard_grpc_tls/src/certs.rs b/crates/defguard_grpc_tls/src/certs.rs index 0b158d0d3..892c828bb 100644 --- a/crates/defguard_grpc_tls/src/certs.rs +++ b/crates/defguard_grpc_tls/src/certs.rs @@ -18,7 +18,7 @@ use rustls::{ danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, }, crypto, - pki_types::{CertificateDer, ServerName, UnixTime}, + pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime}, }; use thiserror::Error; use tokio::sync::watch; @@ -160,11 +160,18 @@ pub fn server_tls_config( Ok(ServerTlsConfig::new().identity(identity).client_ca_root(ca)) } -/// Create a rustls client config that enforces the pinned component certificate serial. +/// Create a rustls client config that enforces the pinned component certificate serial +/// and presents the Core client certificate for mutual TLS authentication. +/// +/// `core_client_cert_der` and `core_client_cert_key_der` are the DER-encoded client +/// certificate and its private key that Core presents to the gateway/proxy during the +/// TLS handshake. The gateway/proxy verifies this cert against `ca_cert_der`. pub fn client_config( ca_cert_der: &[u8], certs_rx: watch::Receiver>>, component_id: Id, + core_client_cert_der: &[u8], + core_client_cert_key_der: &[u8], ) -> Result { let provider = Arc::new(crypto::ring::default_provider()); let roots = root_store_from_ca(ca_cert_der)?; @@ -175,10 +182,19 @@ pub fn client_config( ) .build() .map_err(|err| CertConfigError::TlsConfig(err.to_string()))?; + + let client_cert = CertificateDer::from(core_client_cert_der.to_vec()); + let client_key = PrivateKeyDer::try_from(core_client_cert_key_der.to_vec()) + .map_err(|err| CertConfigError::TlsConfig(format!("invalid client key DER: {err}")))?; + let builder = rustls::ClientConfig::builder_with_provider(provider) .with_safe_default_protocol_versions() .map_err(|err| CertConfigError::TlsConfig(err.to_string()))?; - let mut config = builder.with_root_certificates(roots).with_no_client_auth(); + let mut config = builder + .with_root_certificates(roots) + .with_client_auth_cert(vec![client_cert], client_key) + .map_err(|err| CertConfigError::TlsConfig(format!("client auth cert error: {err}")))?; + let verifier: Arc = verifier; config .dangerous() diff --git a/crates/defguard_proxy_manager/src/handler.rs b/crates/defguard_proxy_manager/src/handler.rs index 09c81397f..9bfad866f 100644 --- a/crates/defguard_proxy_manager/src/handler.rs +++ b/crates/defguard_proxy_manager/src/handler.rs @@ -256,8 +256,38 @@ impl ProxyHandler { "Core CA is not setup, can't create a Proxy endpoint.".to_string(), ) })?; - let tls_config = tls_certs::client_config(&ca_cert_der, certs_rx, self.proxy_id) - .map_err(|err| ProxyError::TlsConfigError(err.to_string()))?; + + // Load the Proxy model to retrieve the per-component Core client cert. + let proxy = Proxy::find_by_id(&self.pool, self.proxy_id) + .await + .map_err(ProxyError::SqlxError)? + .ok_or_else(|| { + ProxyError::MissingConfiguration(format!( + "Proxy id={} not found in DB, can't load Core client certificate", + self.proxy_id + )) + })?; + let core_client_cert_der = proxy.core_client_cert_der.ok_or_else(|| { + ProxyError::MissingConfiguration(format!( + "Core client certificate not provisioned for proxy id={}", + self.proxy_id + )) + })?; + let core_client_cert_key_der = proxy.core_client_cert_key_der.ok_or_else(|| { + ProxyError::MissingConfiguration(format!( + "Core client certificate key not provisioned for proxy id={}", + self.proxy_id + )) + })?; + + let tls_config = tls_certs::client_config( + &ca_cert_der, + certs_rx, + self.proxy_id, + &core_client_cert_der, + &core_client_cert_key_der, + ) + .map_err(|err| ProxyError::TlsConfigError(err.to_string()))?; let connector = HttpsConnectorBuilder::new() .with_tls_config(tls_config) .https_only() From 2fc96dc9a027150a8c76c927435da3a0cecb18af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 15 Apr 2026 07:30:31 +0200 Subject: [PATCH 08/46] update query data --- ...8d61eebe6a79945324e98ca452b50d6abc90.json} | 30 ++++++------ ...c1ab4c6f0ead212fc74bb881e1d5c0d96080.json} | 30 ++++++------ ...3a0a0b3b44d0a87d43f69425729c15e1a400.json} | 46 +++++++++---------- ...6341ee18e24f0d32399e6ce2ebaedef64cea.json} | 46 +++++++++---------- ...82a34d3bfa4364cbe9b8368e71445bc20877.json} | 30 ++++++------ 5 files changed, 91 insertions(+), 91 deletions(-) rename .sqlx/{query-751b6f6c3bfeaa91685107c94c92e3d57dd57fbeead263d5a4c915ea25bd5205.json => query-27e7e18a7014af541fe5f8f051f78d61eebe6a79945324e98ca452b50d6abc90.json} (84%) rename .sqlx/{query-e2f43cdd1dd50ab2b97b1cdae80f6354bd99ff1f1f9adc02837ccbc458ee8be9.json => query-2ce93887379d80ff03753caaf94ec1ab4c6f0ead212fc74bb881e1d5c0d96080.json} (81%) rename .sqlx/{query-633bf991c7480a892f99c99175d6f31e2b9ced9aef4268752f6e51d4a83eab99.json => query-3c6a119f2f10046bd9e42314df953a0a0b3b44d0a87d43f69425729c15e1a400.json} (84%) rename .sqlx/{query-bda99bacb002f781dd276c5623d399dec458cb9250fc448bc6edf94b5e90805b.json => query-4d9c4562a138038ba054b5b83b646341ee18e24f0d32399e6ce2ebaedef64cea.json} (84%) rename .sqlx/{query-fd40448d1bff2b70f119211c44b9a2e564bdf787b0f02dd244b0d01186d56e0b.json => query-a41787c8c8307414165ab23ef96d82a34d3bfa4364cbe9b8368e71445bc20877.json} (84%) diff --git a/.sqlx/query-751b6f6c3bfeaa91685107c94c92e3d57dd57fbeead263d5a4c915ea25bd5205.json b/.sqlx/query-27e7e18a7014af541fe5f8f051f78d61eebe6a79945324e98ca452b50d6abc90.json similarity index 84% rename from .sqlx/query-751b6f6c3bfeaa91685107c94c92e3d57dd57fbeead263d5a4c915ea25bd5205.json rename to .sqlx/query-27e7e18a7014af541fe5f8f051f78d61eebe6a79945324e98ca452b50d6abc90.json index 965dfaa87..c6e366dae 100644 --- a/.sqlx/query-751b6f6c3bfeaa91685107c94c92e3d57dd57fbeead263d5a4c915ea25bd5205.json +++ b/.sqlx/query-27e7e18a7014af541fe5f8f051f78d61eebe6a79945324e98ca452b50d6abc90.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, name, address, port, connected_at, disconnected_at, version, enabled, certificate_serial, certificate_expiry, modified_at, modified_by, core_client_cert_der, core_client_cert_key_der, core_client_cert_expiry FROM proxy WHERE enabled", + "query": "SELECT * FROM proxy WHERE enabled AND id NOT IN (SELECT id FROM proxy WHERE enabled LIMIT 1\n )", "describe": { "columns": [ { @@ -35,33 +35,33 @@ }, { "ordinal": 6, - "name": "version", - "type_info": "Text" + "name": "certificate_expiry", + "type_info": "Timestamp" }, { "ordinal": 7, - "name": "enabled", - "type_info": "Bool" + "name": "version", + "type_info": "Text" }, { "ordinal": 8, - "name": "certificate_serial", - "type_info": "Text" + "name": "modified_at", + "type_info": "Timestamp" }, { "ordinal": 9, - "name": "certificate_expiry", - "type_info": "Timestamp" + "name": "certificate_serial", + "type_info": "Text" }, { "ordinal": 10, - "name": "modified_at", - "type_info": "Timestamp" + "name": "modified_by", + "type_info": "Text" }, { "ordinal": 11, - "name": "modified_by", - "type_info": "Text" + "name": "enabled", + "type_info": "Bool" }, { "ordinal": 12, @@ -90,8 +90,8 @@ true, true, true, - false, true, + false, true, false, false, @@ -100,5 +100,5 @@ true ] }, - "hash": "751b6f6c3bfeaa91685107c94c92e3d57dd57fbeead263d5a4c915ea25bd5205" + "hash": "27e7e18a7014af541fe5f8f051f78d61eebe6a79945324e98ca452b50d6abc90" } diff --git a/.sqlx/query-e2f43cdd1dd50ab2b97b1cdae80f6354bd99ff1f1f9adc02837ccbc458ee8be9.json b/.sqlx/query-2ce93887379d80ff03753caaf94ec1ab4c6f0ead212fc74bb881e1d5c0d96080.json similarity index 81% rename from .sqlx/query-e2f43cdd1dd50ab2b97b1cdae80f6354bd99ff1f1f9adc02837ccbc458ee8be9.json rename to .sqlx/query-2ce93887379d80ff03753caaf94ec1ab4c6f0ead212fc74bb881e1d5c0d96080.json index c81bbaf92..97c45d80c 100644 --- a/.sqlx/query-e2f43cdd1dd50ab2b97b1cdae80f6354bd99ff1f1f9adc02837ccbc458ee8be9.json +++ b/.sqlx/query-2ce93887379d80ff03753caaf94ec1ab4c6f0ead212fc74bb881e1d5c0d96080.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, name, address, port, connected_at, disconnected_at, version, enabled, certificate_serial, certificate_expiry, modified_at, modified_by, core_client_cert_der, core_client_cert_key_der, core_client_cert_expiry FROM proxy WHERE enabled AND id NOT IN (SELECT id FROM proxy WHERE enabled LIMIT 1\n )", + "query": "SELECT * FROM proxy WHERE enabled", "describe": { "columns": [ { @@ -35,33 +35,33 @@ }, { "ordinal": 6, - "name": "version", - "type_info": "Text" + "name": "certificate_expiry", + "type_info": "Timestamp" }, { "ordinal": 7, - "name": "enabled", - "type_info": "Bool" + "name": "version", + "type_info": "Text" }, { "ordinal": 8, - "name": "certificate_serial", - "type_info": "Text" + "name": "modified_at", + "type_info": "Timestamp" }, { "ordinal": 9, - "name": "certificate_expiry", - "type_info": "Timestamp" + "name": "certificate_serial", + "type_info": "Text" }, { "ordinal": 10, - "name": "modified_at", - "type_info": "Timestamp" + "name": "modified_by", + "type_info": "Text" }, { "ordinal": 11, - "name": "modified_by", - "type_info": "Text" + "name": "enabled", + "type_info": "Bool" }, { "ordinal": 12, @@ -90,8 +90,8 @@ true, true, true, - false, true, + false, true, false, false, @@ -100,5 +100,5 @@ true ] }, - "hash": "e2f43cdd1dd50ab2b97b1cdae80f6354bd99ff1f1f9adc02837ccbc458ee8be9" + "hash": "2ce93887379d80ff03753caaf94ec1ab4c6f0ead212fc74bb881e1d5c0d96080" } diff --git a/.sqlx/query-633bf991c7480a892f99c99175d6f31e2b9ced9aef4268752f6e51d4a83eab99.json b/.sqlx/query-3c6a119f2f10046bd9e42314df953a0a0b3b44d0a87d43f69425729c15e1a400.json similarity index 84% rename from .sqlx/query-633bf991c7480a892f99c99175d6f31e2b9ced9aef4268752f6e51d4a83eab99.json rename to .sqlx/query-3c6a119f2f10046bd9e42314df953a0a0b3b44d0a87d43f69425729c15e1a400.json index d264b32a7..1504472b1 100644 --- a/.sqlx/query-633bf991c7480a892f99c99175d6f31e2b9ced9aef4268752f6e51d4a83eab99.json +++ b/.sqlx/query-3c6a119f2f10046bd9e42314df953a0a0b3b44d0a87d43f69425729c15e1a400.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, location_id, name, address, port, connected_at, disconnected_at, certificate_serial, certificate_expiry, version, enabled, modified_at, modified_by, core_client_cert_der, core_client_cert_key_der, core_client_cert_expiry FROM gateway WHERE location_id = $1 ORDER BY id", + "query": "SELECT * FROM gateway WHERE location_id = $1 ORDER BY id", "describe": { "columns": [ { @@ -15,28 +15,28 @@ }, { "ordinal": 2, - "name": "name", - "type_info": "Text" + "name": "connected_at", + "type_info": "Timestamp" }, { "ordinal": 3, - "name": "address", - "type_info": "Text" + "name": "disconnected_at", + "type_info": "Timestamp" }, { "ordinal": 4, - "name": "port", - "type_info": "Int4" + "name": "certificate_expiry", + "type_info": "Timestamp" }, { "ordinal": 5, - "name": "connected_at", - "type_info": "Timestamp" + "name": "version", + "type_info": "Text" }, { "ordinal": 6, - "name": "disconnected_at", - "type_info": "Timestamp" + "name": "name", + "type_info": "Text" }, { "ordinal": 7, @@ -45,23 +45,23 @@ }, { "ordinal": 8, - "name": "certificate_expiry", - "type_info": "Timestamp" + "name": "address", + "type_info": "Text" }, { "ordinal": 9, - "name": "version", - "type_info": "Text" + "name": "port", + "type_info": "Int4" }, { "ordinal": 10, - "name": "enabled", - "type_info": "Bool" + "name": "modified_at", + "type_info": "Timestamp" }, { "ordinal": 11, - "name": "modified_at", - "type_info": "Timestamp" + "name": "enabled", + "type_info": "Bool" }, { "ordinal": 12, @@ -90,23 +90,23 @@ ] }, "nullable": [ - false, - false, - false, false, false, true, true, true, true, + false, true, false, false, false, + false, + false, true, true, true ] }, - "hash": "633bf991c7480a892f99c99175d6f31e2b9ced9aef4268752f6e51d4a83eab99" + "hash": "3c6a119f2f10046bd9e42314df953a0a0b3b44d0a87d43f69425729c15e1a400" } diff --git a/.sqlx/query-bda99bacb002f781dd276c5623d399dec458cb9250fc448bc6edf94b5e90805b.json b/.sqlx/query-4d9c4562a138038ba054b5b83b646341ee18e24f0d32399e6ce2ebaedef64cea.json similarity index 84% rename from .sqlx/query-bda99bacb002f781dd276c5623d399dec458cb9250fc448bc6edf94b5e90805b.json rename to .sqlx/query-4d9c4562a138038ba054b5b83b646341ee18e24f0d32399e6ce2ebaedef64cea.json index 0b870a60c..6de76f513 100644 --- a/.sqlx/query-bda99bacb002f781dd276c5623d399dec458cb9250fc448bc6edf94b5e90805b.json +++ b/.sqlx/query-4d9c4562a138038ba054b5b83b646341ee18e24f0d32399e6ce2ebaedef64cea.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, location_id, name, address, port, connected_at, disconnected_at, certificate_serial, certificate_expiry, version, enabled, modified_at, modified_by, core_client_cert_der, core_client_cert_key_der, core_client_cert_expiry FROM gateway WHERE address = $1 AND port = $2", + "query": "SELECT * FROM gateway WHERE address = $1 AND port = $2", "describe": { "columns": [ { @@ -15,28 +15,28 @@ }, { "ordinal": 2, - "name": "name", - "type_info": "Text" + "name": "connected_at", + "type_info": "Timestamp" }, { "ordinal": 3, - "name": "address", - "type_info": "Text" + "name": "disconnected_at", + "type_info": "Timestamp" }, { "ordinal": 4, - "name": "port", - "type_info": "Int4" + "name": "certificate_expiry", + "type_info": "Timestamp" }, { "ordinal": 5, - "name": "connected_at", - "type_info": "Timestamp" + "name": "version", + "type_info": "Text" }, { "ordinal": 6, - "name": "disconnected_at", - "type_info": "Timestamp" + "name": "name", + "type_info": "Text" }, { "ordinal": 7, @@ -45,23 +45,23 @@ }, { "ordinal": 8, - "name": "certificate_expiry", - "type_info": "Timestamp" + "name": "address", + "type_info": "Text" }, { "ordinal": 9, - "name": "version", - "type_info": "Text" + "name": "port", + "type_info": "Int4" }, { "ordinal": 10, - "name": "enabled", - "type_info": "Bool" + "name": "modified_at", + "type_info": "Timestamp" }, { "ordinal": 11, - "name": "modified_at", - "type_info": "Timestamp" + "name": "enabled", + "type_info": "Bool" }, { "ordinal": 12, @@ -91,23 +91,23 @@ ] }, "nullable": [ - false, - false, - false, false, false, true, true, true, true, + false, true, false, false, false, + false, + false, true, true, true ] }, - "hash": "bda99bacb002f781dd276c5623d399dec458cb9250fc448bc6edf94b5e90805b" + "hash": "4d9c4562a138038ba054b5b83b646341ee18e24f0d32399e6ce2ebaedef64cea" } diff --git a/.sqlx/query-fd40448d1bff2b70f119211c44b9a2e564bdf787b0f02dd244b0d01186d56e0b.json b/.sqlx/query-a41787c8c8307414165ab23ef96d82a34d3bfa4364cbe9b8368e71445bc20877.json similarity index 84% rename from .sqlx/query-fd40448d1bff2b70f119211c44b9a2e564bdf787b0f02dd244b0d01186d56e0b.json rename to .sqlx/query-a41787c8c8307414165ab23ef96d82a34d3bfa4364cbe9b8368e71445bc20877.json index 47128bca0..aefb3da70 100644 --- a/.sqlx/query-fd40448d1bff2b70f119211c44b9a2e564bdf787b0f02dd244b0d01186d56e0b.json +++ b/.sqlx/query-a41787c8c8307414165ab23ef96d82a34d3bfa4364cbe9b8368e71445bc20877.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, name, address, port, connected_at, disconnected_at, version, enabled, certificate_serial, certificate_expiry, modified_at, modified_by, core_client_cert_der, core_client_cert_key_der, core_client_cert_expiry FROM proxy WHERE address = $1 AND port = $2", + "query": "SELECT * FROM proxy WHERE address = $1 AND port = $2", "describe": { "columns": [ { @@ -35,33 +35,33 @@ }, { "ordinal": 6, - "name": "version", - "type_info": "Text" + "name": "certificate_expiry", + "type_info": "Timestamp" }, { "ordinal": 7, - "name": "enabled", - "type_info": "Bool" + "name": "version", + "type_info": "Text" }, { "ordinal": 8, - "name": "certificate_serial", - "type_info": "Text" + "name": "modified_at", + "type_info": "Timestamp" }, { "ordinal": 9, - "name": "certificate_expiry", - "type_info": "Timestamp" + "name": "certificate_serial", + "type_info": "Text" }, { "ordinal": 10, - "name": "modified_at", - "type_info": "Timestamp" + "name": "modified_by", + "type_info": "Text" }, { "ordinal": 11, - "name": "modified_by", - "type_info": "Text" + "name": "enabled", + "type_info": "Bool" }, { "ordinal": 12, @@ -93,8 +93,8 @@ true, true, true, - false, true, + false, true, false, false, @@ -103,5 +103,5 @@ true ] }, - "hash": "fd40448d1bff2b70f119211c44b9a2e564bdf787b0f02dd244b0d01186d56e0b" + "hash": "a41787c8c8307414165ab23ef96d82a34d3bfa4364cbe9b8368e71445bc20877" } From f6a2b96821a543ec4315d19c71aecdeba2a8a30f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 15 Apr 2026 07:42:25 +0200 Subject: [PATCH 09/46] tokio::time cleanup --- .../src/handlers/component_setup.rs | 15 ++-- .../tests/integration/api/acl/aliases.rs | 6 +- .../tests/integration/api/acl/destinations.rs | 6 +- .../tests/integration/api/acl/rules.rs | 6 +- .../tests/integration/api/proxy_certs.rs | 4 +- crates/defguard_gateway_manager/src/lib.rs | 3 +- .../src/tests/common/mod.rs | 6 +- crates/defguard_mail/src/tests.rs | 33 ++++---- .../src/tests/common/mod.rs | 4 +- .../proxy_manager/handler/password_reset.rs | 9 +- crates/defguard_setup/src/auto_adoption.rs | 84 +++++++++---------- .../tests/auto_adoption_wizard.rs | 6 +- .../tests/auto_wizard_url_settings.rs | 2 +- crates/defguard_setup/tests/initial_setup.rs | 13 ++- .../defguard_setup/tests/migration_wizard.rs | 6 +- 15 files changed, 106 insertions(+), 97 deletions(-) diff --git a/crates/defguard_core/src/handlers/component_setup.rs b/crates/defguard_core/src/handlers/component_setup.rs index 1603a936b..41288d1bb 100644 --- a/crates/defguard_core/src/handlers/component_setup.rs +++ b/crates/defguard_core/src/handlers/component_setup.rs @@ -42,7 +42,10 @@ use futures::Stream; use reqwest::Url; use serde::{Deserialize, Serialize}; use sqlx::PgPool; -use tokio::sync::mpsc::{Sender, UnboundedReceiver, UnboundedSender, unbounded_channel}; +use tokio::{ + sync::mpsc::{Sender, UnboundedReceiver, UnboundedSender, unbounded_channel}, + time::{Instant, sleep_until, timeout}, +}; use tokio_stream::StreamExt; use tonic::{ Request, Status, @@ -376,7 +379,7 @@ pub async fn setup_proxy_tls_stream( request.grpc_port ); - let response_with_metadata = match tokio::time::timeout(CONNECTION_TIMEOUT, client.start(())).await { + let response_with_metadata = match timeout(CONNECTION_TIMEOUT, client.start(())).await { Ok(Ok(response)) => response, Ok(Err(status)) => { let error_msg = status.message(); @@ -822,7 +825,7 @@ pub async fn setup_gateway_tls_stream( debug!("Initiating connection to Gateway at {ip_or_domain}:{}", request.grpc_port); - let response_with_metadata = match tokio::time::timeout( + let response_with_metadata = match timeout( CONNECTION_TIMEOUT, client.start(()) ).await { @@ -1349,8 +1352,8 @@ pub async fn stream_proxy_acme( }); let mut current_step: &'static str = "Connecting"; - let deadline = tokio::time::Instant::now() - + tokio::time::Duration::from_secs(ACME_TIMEOUT_SECS); + let deadline = Instant::now() + + Duration::from_secs(ACME_TIMEOUT_SECS); // Drain progress steps until the ACME task finishes (channel closed) or times out. loop { @@ -1368,7 +1371,7 @@ pub async fn stream_proxy_acme( } } - () = tokio::time::sleep_until(deadline) => { + () = sleep_until(deadline) => { yield Ok(acme_error_event( current_step, format!( diff --git a/crates/defguard_core/tests/integration/api/acl/aliases.rs b/crates/defguard_core/tests/integration/api/acl/aliases.rs index 16f0be96f..11e43f13a 100644 --- a/crates/defguard_core/tests/integration/api/acl/aliases.rs +++ b/crates/defguard_core/tests/integration/api/acl/aliases.rs @@ -1,3 +1,5 @@ +use tokio::time::sleep; + use super::*; #[sqlx::test] @@ -399,7 +401,7 @@ async fn test_alias_audit_fields_track_acting_user_across_mutations( assert_ne!(created_alias_row.modified_by, "admin"); let created_modified_at = created_alias_row.modified_at; - tokio::time::sleep(std::time::Duration::from_millis(2)).await; + sleep(std::time::Duration::from_millis(2)).await; let mut alias_update = created_alias.clone(); alias_update.name = "alias updated by hpotter".to_string(); @@ -421,7 +423,7 @@ async fn test_alias_audit_fields_track_acting_user_across_mutations( assert!(updated_alias_row.modified_at > created_modified_at); let updated_modified_at = updated_alias_row.modified_at; - tokio::time::sleep(std::time::Duration::from_millis(2)).await; + sleep(std::time::Duration::from_millis(2)).await; let response = client .put("/api/v1/acl/alias/apply") diff --git a/crates/defguard_core/tests/integration/api/acl/destinations.rs b/crates/defguard_core/tests/integration/api/acl/destinations.rs index f1d69a109..fdc6f42b7 100644 --- a/crates/defguard_core/tests/integration/api/acl/destinations.rs +++ b/crates/defguard_core/tests/integration/api/acl/destinations.rs @@ -1,3 +1,5 @@ +use tokio::time::sleep; + use super::*; #[sqlx::test] @@ -551,7 +553,7 @@ async fn test_destination_audit_fields_track_acting_user_across_mutations( assert_ne!(created_destination_row.modified_by, "admin"); let created_modified_at = created_destination_row.modified_at; - tokio::time::sleep(std::time::Duration::from_millis(2)).await; + sleep(std::time::Duration::from_millis(2)).await; let mut destination_update = created_destination.clone(); destination_update.name = "destination updated by hpotter".to_string(); @@ -580,7 +582,7 @@ async fn test_destination_audit_fields_track_acting_user_across_mutations( assert!(updated_destination_row.modified_at > created_modified_at); let updated_modified_at = updated_destination_row.modified_at; - tokio::time::sleep(std::time::Duration::from_millis(2)).await; + sleep(std::time::Duration::from_millis(2)).await; let response = client .put("/api/v1/acl/destination/apply") diff --git a/crates/defguard_core/tests/integration/api/acl/rules.rs b/crates/defguard_core/tests/integration/api/acl/rules.rs index bb82c340b..a69d6c897 100644 --- a/crates/defguard_core/tests/integration/api/acl/rules.rs +++ b/crates/defguard_core/tests/integration/api/acl/rules.rs @@ -1,3 +1,5 @@ +use tokio::time::sleep; + use super::*; use crate::api::PaginatedApiResponse; @@ -1409,7 +1411,7 @@ async fn test_rule_audit_fields_track_acting_user_across_mutations( assert_ne!(created_rule_row.modified_by, "admin"); let created_modified_at = created_rule_row.modified_at; - tokio::time::sleep(std::time::Duration::from_millis(2)).await; + sleep(std::time::Duration::from_millis(2)).await; let mut updated_rule = created_rule.clone(); updated_rule.name = "rule updated by hpotter".to_string(); @@ -1429,7 +1431,7 @@ async fn test_rule_audit_fields_track_acting_user_across_mutations( assert!(updated_rule_row.modified_at > created_modified_at); let updated_modified_at = updated_rule_row.modified_at; - tokio::time::sleep(std::time::Duration::from_millis(2)).await; + sleep(std::time::Duration::from_millis(2)).await; let response = client .put("/api/v1/acl/rule/apply") diff --git a/crates/defguard_core/tests/integration/api/proxy_certs.rs b/crates/defguard_core/tests/integration/api/proxy_certs.rs index 619906077..60be48957 100644 --- a/crates/defguard_core/tests/integration/api/proxy_certs.rs +++ b/crates/defguard_core/tests/integration/api/proxy_certs.rs @@ -61,7 +61,7 @@ impl ProxyBroadcastCapture { async fn drain_broadcast_certs(&mut self) -> Vec<(String, String)> { let mut results = Vec::new(); // Give the handler a brief moment to enqueue the message. - tokio::time::sleep(std::time::Duration::from_millis(50)).await; + sleep(Duration::from_millis(50)).await; loop { match self.rx.try_recv() { Ok(ProxyControlMessage::BroadcastHttpsCerts { cert_pem, key_pem }) => { @@ -76,7 +76,7 @@ impl ProxyBroadcastCapture { async fn drain_clear_https_certs(&mut self) -> usize { let mut results = 0; - tokio::time::sleep(std::time::Duration::from_millis(50)).await; + sleep(Duration::from_millis(50)).await; loop { match self.rx.try_recv() { Ok(ProxyControlMessage::ClearHttpsCerts) => { diff --git a/crates/defguard_gateway_manager/src/lib.rs b/crates/defguard_gateway_manager/src/lib.rs index 2b1d79e27..5f005303c 100644 --- a/crates/defguard_gateway_manager/src/lib.rs +++ b/crates/defguard_gateway_manager/src/lib.rs @@ -22,6 +22,7 @@ use tokio::sync::Notify; use tokio::{ sync::{broadcast::Sender, mpsc::UnboundedSender, watch::Receiver}, task::{AbortHandle, JoinHandle, JoinSet}, + time::sleep, }; use tonic::{Request, service::interceptor::InterceptedService, transport::Channel}; @@ -347,7 +348,7 @@ impl GatewayManager { let _refresh_certs_task = AbortTaskOnDrop::new(tokio::spawn(async move { loop { certs::refresh_certs(&refresh_pool, &certs_tx).await; - tokio::time::sleep(TEN_SECS).await; + sleep(TEN_SECS).await; } })); let mut abort_handles = HashMap::new(); diff --git a/crates/defguard_gateway_manager/src/tests/common/mod.rs b/crates/defguard_gateway_manager/src/tests/common/mod.rs index ea19cc617..62c7f6efc 100644 --- a/crates/defguard_gateway_manager/src/tests/common/mod.rs +++ b/crates/defguard_gateway_manager/src/tests/common/mod.rs @@ -33,7 +33,7 @@ use tokio::{ oneshot, watch, }, task::JoinHandle, - time::timeout, + time::{sleep, timeout}, }; use tokio_stream::{once, wrappers::UnboundedReceiverStream}; use tonic::{Request, Response, Status, Streaming, transport::Server}; @@ -573,7 +573,7 @@ impl HandlerTestContext { wait_for_gateway_connection_state(&self.pool, self.gateway.id, true).await; timeout(TEST_TIMEOUT, async { while self.events_tx().receiver_count() <= initial_event_receivers { - tokio::time::sleep(Duration::from_millis(20)).await; + sleep(Duration::from_millis(20)).await; } }) .await @@ -649,7 +649,7 @@ pub(crate) async fn wait_for_gateway_connection_state( return gateway; } - tokio::time::sleep(Duration::from_millis(20)).await; + sleep(Duration::from_millis(20)).await; } }) .await diff --git a/crates/defguard_mail/src/tests.rs b/crates/defguard_mail/src/tests.rs index b857e395e..ca49edf59 100644 --- a/crates/defguard_mail/src/tests.rs +++ b/crates/defguard_mail/src/tests.rs @@ -18,6 +18,7 @@ use sqlx::{ postgres::{PgConnectOptions, PgPoolOptions}, }; use tera::Context; +use tokio::time::sleep; use super::{Attachment, mail::MailMessage, templates}; @@ -67,7 +68,7 @@ fn send_desktop_start(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - tokio::time::sleep(Duration::from_secs(2)).await; + sleep(Duration::from_secs(2)).await; } #[ignore = "requires SMTP server"] @@ -102,7 +103,7 @@ fn send_new_device_added(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - tokio::time::sleep(Duration::from_secs(2)).await; + sleep(Duration::from_secs(2)).await; } #[ignore = "requires SMTP server"] @@ -125,7 +126,7 @@ fn send_mfa_code(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - tokio::time::sleep(Duration::from_secs(2)).await; + sleep(Duration::from_secs(2)).await; } #[ignore = "requires SMTP server"] @@ -149,7 +150,7 @@ fn send_new_account(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - tokio::time::sleep(Duration::from_secs(2)).await; + sleep(Duration::from_secs(2)).await; } #[ignore = "requires SMTP server"] @@ -172,7 +173,7 @@ fn send_mfa_activation(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - tokio::time::sleep(Duration::from_secs(2)).await; + sleep(Duration::from_secs(2)).await; } #[ignore = "requires SMTP server"] @@ -197,7 +198,7 @@ fn send_enrollment_admin_notification(_: PgPoolOptions, options: PgConnectOption .unwrap(); // Delay, so send_and_forget() can process the message. - tokio::time::sleep(Duration::from_secs(2)).await; + sleep(Duration::from_secs(2)).await; } #[ignore = "requires SMTP server"] @@ -221,7 +222,7 @@ fn send_gateway_disconnected_mail(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - tokio::time::sleep(Duration::from_secs(2)).await; + sleep(Duration::from_secs(2)).await; } #[ignore = "requires SMTP server"] @@ -245,7 +246,7 @@ fn send_gateway_reconnected_mail(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - tokio::time::sleep(Duration::from_secs(2)).await; + sleep(Duration::from_secs(2)).await; } #[ignore = "requires SMTP server"] @@ -265,7 +266,7 @@ fn send_mfa_configured_mail(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - tokio::time::sleep(Duration::from_secs(2)).await; + sleep(Duration::from_secs(2)).await; } #[ignore = "requires SMTP server"] @@ -281,7 +282,7 @@ fn send_new_device_login_mail(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - tokio::time::sleep(Duration::from_secs(2)).await; + sleep(Duration::from_secs(2)).await; } #[ignore = "requires SMTP server"] @@ -304,7 +305,7 @@ fn send_new_device_oidc_login_mail(_: PgPoolOptions, options: PgConnectOptions) .unwrap(); // Delay, so send_and_forget() can process the message. - tokio::time::sleep(Duration::from_secs(2)).await; + sleep(Duration::from_secs(2)).await; } #[ignore = "requires SMTP server"] @@ -328,7 +329,7 @@ fn send_password_reset_mail(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - tokio::time::sleep(Duration::from_secs(2)).await; + sleep(Duration::from_secs(2)).await; } #[ignore = "requires SMTP server"] @@ -343,7 +344,7 @@ fn send_password_reset_success_mail(_: PgPoolOptions, options: PgConnectOptions) .unwrap(); // Delay, so send_and_forget() can process the message. - tokio::time::sleep(Duration::from_secs(2)).await; + sleep(Duration::from_secs(2)).await; } #[ignore = "requires SMTP server"] @@ -358,7 +359,7 @@ fn send_test_mail(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - tokio::time::sleep(Duration::from_secs(2)).await; + sleep(Duration::from_secs(2)).await; } #[ignore = "requires SMTP server"] @@ -377,7 +378,7 @@ fn send_support_data_mail(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - tokio::time::sleep(Duration::from_secs(2)).await; + sleep(Duration::from_secs(2)).await; } #[ignore = "requires SMTP server"] @@ -391,7 +392,7 @@ fn send_enrollment_welcome_mail(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - tokio::time::sleep(Duration::from_secs(2)).await; + sleep(Duration::from_secs(2)).await; } #[test] diff --git a/crates/defguard_proxy_manager/src/tests/common/mod.rs b/crates/defguard_proxy_manager/src/tests/common/mod.rs index 404c53602..4e4a30bf0 100644 --- a/crates/defguard_proxy_manager/src/tests/common/mod.rs +++ b/crates/defguard_proxy_manager/src/tests/common/mod.rs @@ -41,7 +41,7 @@ use tokio::{ oneshot, watch, }, task::JoinHandle, - time::timeout, + time::{sleep, timeout}, }; use tokio_stream::{once, wrappers::UnboundedReceiverStream}; use tonic::{Request, Response, Status, Streaming, transport::Server}; @@ -662,7 +662,7 @@ pub(crate) async fn wait_for_proxy_connection_state( if proxy.is_connected() == expected_connected { return proxy; } - tokio::time::sleep(PROXY_CONNECT_DELAY).await; + sleep(PROXY_CONNECT_DELAY).await; } }) .await diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/password_reset.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/password_reset.rs index 715f63626..70e3d64a8 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/password_reset.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/password_reset.rs @@ -2,6 +2,7 @@ use defguard_common::db::models::User; use defguard_core::events::{BidiStreamEventType, PasswordResetEvent}; use defguard_proto::proxy::core_response; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; +use tokio::time::timeout; use super::support::{ STRONG_PASSWORD, assert_error_response, complete_proxy_handshake, create_enrollment_token, @@ -64,7 +65,7 @@ async fn test_password_reset_start_returns_deadline(_: PgPoolOptions, options: P assert!(deadline > 0, "deadline_timestamp must be positive"); // A BidiStreamEvent::PasswordReset(PasswordResetStarted) must have been emitted. - let event = tokio::time::timeout(TEST_TIMEOUT, context.bidi_events_rx.recv()) + let event = timeout(TEST_TIMEOUT, context.bidi_events_rx.recv()) .await .expect("timed out waiting for BidiStreamEvent") .expect("bidi_events_rx closed"); @@ -104,7 +105,7 @@ async fn test_password_reset_completes_successfully(_: PgPoolOptions, options: P ), "start must succeed" ); - let _ = tokio::time::timeout(TEST_TIMEOUT, context.bidi_events_rx.recv()).await; + let _ = timeout(TEST_TIMEOUT, context.bidi_events_rx.recv()).await; // Reset the password. const NEW_PASSWORD: &str = "NewPass2!"; @@ -134,7 +135,7 @@ async fn test_password_reset_completes_successfully(_: PgPoolOptions, options: P ); // A BidiStreamEvent::PasswordReset(PasswordResetCompleted) must have been emitted. - let event = tokio::time::timeout(TEST_TIMEOUT, context.bidi_events_rx.recv()) + let event = timeout(TEST_TIMEOUT, context.bidi_events_rx.recv()) .await .expect("timed out waiting for BidiStreamEvent") .expect("bidi_events_rx closed"); @@ -176,7 +177,7 @@ async fn test_password_reset_weak_password_returns_error( ), "start must succeed" ); - let _ = tokio::time::timeout(TEST_TIMEOUT, context.bidi_events_rx.recv()).await; + let _ = timeout(TEST_TIMEOUT, context.bidi_events_rx.recv()).await; // Submit a weak password. let response = send_password_reset(&mut context, &token.id, "weak").await; diff --git a/crates/defguard_setup/src/auto_adoption.rs b/crates/defguard_setup/src/auto_adoption.rs index 5406506be..eea96fbb0 100644 --- a/crates/defguard_setup/src/auto_adoption.rs +++ b/crates/defguard_setup/src/auto_adoption.rs @@ -36,7 +36,7 @@ use defguard_version::{Version, client::ClientVersionInterceptor}; use ipnetwork::IpNetwork; use reqwest::Url; use sqlx::PgPool; -use tokio::sync::mpsc::UnboundedReceiver; +use tokio::{sync::mpsc::UnboundedReceiver, time::timeout}; use tonic::{ Request, Status, service::Interceptor, @@ -345,27 +345,26 @@ async fn run_edge_adoption_attempt_scoped( auth_interceptor.clone().call(req) }); - let response_with_metadata = - match tokio::time::timeout(STARTUP_ADOPTION_TIMEOUT, client.start(())).await { - Ok(Ok(response)) => response, - Ok(Err(err)) => { - return merge_failure_logs( - format!("Failed to start edge setup stream: {err}"), - &log_buffer, - &mut log_rx, - ); - } - Err(_) => { - return merge_failure_logs( - format!( - "Timed out connecting to edge setup endpoint after {} seconds", - STARTUP_ADOPTION_TIMEOUT.as_secs() - ), - &log_buffer, - &mut log_rx, - ); - } - }; + let response_with_metadata = match timeout(STARTUP_ADOPTION_TIMEOUT, client.start(())).await { + Ok(Ok(response)) => response, + Ok(Err(err)) => { + return merge_failure_logs( + format!("Failed to start edge setup stream: {err}"), + &log_buffer, + &mut log_rx, + ); + } + Err(_) => { + return merge_failure_logs( + format!( + "Timed out connecting to edge setup endpoint after {} seconds", + STARTUP_ADOPTION_TIMEOUT.as_secs() + ), + &log_buffer, + &mut log_rx, + ); + } + }; debug!("Successfully connected to Edge setup stream"); let edge_version = response_with_metadata @@ -672,27 +671,26 @@ async fn run_gateway_adoption_attempt_scoped( }, ); - let response_with_metadata = - match tokio::time::timeout(STARTUP_ADOPTION_TIMEOUT, client.start(())).await { - Ok(Ok(response)) => response, - Ok(Err(err)) => { - return merge_failure_logs( - format!("Failed to start gateway setup stream: {err}"), - &log_buffer, - &mut log_rx, - ); - } - Err(_) => { - return merge_failure_logs( - format!( - "Timed out connecting to gateway setup endpoint after {} seconds", - STARTUP_ADOPTION_TIMEOUT.as_secs() - ), - &log_buffer, - &mut log_rx, - ); - } - }; + let response_with_metadata = match timeout(STARTUP_ADOPTION_TIMEOUT, client.start(())).await { + Ok(Ok(response)) => response, + Ok(Err(err)) => { + return merge_failure_logs( + format!("Failed to start gateway setup stream: {err}"), + &log_buffer, + &mut log_rx, + ); + } + Err(_) => { + return merge_failure_logs( + format!( + "Timed out connecting to gateway setup endpoint after {} seconds", + STARTUP_ADOPTION_TIMEOUT.as_secs() + ), + &log_buffer, + &mut log_rx, + ); + } + }; debug!("Successfully connected to Gateway setup stream"); let gateway_version = response_with_metadata diff --git a/crates/defguard_setup/tests/auto_adoption_wizard.rs b/crates/defguard_setup/tests/auto_adoption_wizard.rs index f5cc34c91..a33ab85d6 100644 --- a/crates/defguard_setup/tests/auto_adoption_wizard.rs +++ b/crates/defguard_setup/tests/auto_adoption_wizard.rs @@ -1,4 +1,4 @@ -use std::sync::Once; +use std::{sync::Once, time::Duration}; use defguard_common::{ config::DefGuardConfig, @@ -23,6 +23,7 @@ use reqwest::{ }; use serde_json::json; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; +use tokio::time::timeout; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; mod common; @@ -212,8 +213,7 @@ async fn test_auto_adoption_full_flow(_: PgPoolOptions, options: PgConnectOption assert!(wizard.completed); assert_eq!(wizard.active_wizard, ActiveWizard::None); - let shutdown_signal = - tokio::time::timeout(std::time::Duration::from_secs(1), shutdown_rx).await; + let shutdown_signal = timeout(Duration::from_secs(1), shutdown_rx).await; assert!( matches!(shutdown_signal, Ok(Ok(()))), "Setup server should have sent shutdown signal after finish" diff --git a/crates/defguard_setup/tests/auto_wizard_url_settings.rs b/crates/defguard_setup/tests/auto_wizard_url_settings.rs index 8f0272e24..91d86563f 100644 --- a/crates/defguard_setup/tests/auto_wizard_url_settings.rs +++ b/crates/defguard_setup/tests/auto_wizard_url_settings.rs @@ -541,6 +541,6 @@ async fn test_auto_adoption_full_flow_new_url_steps(_: PgPoolOptions, options: P assert!(wizard.completed); assert_eq!(wizard.active_wizard, ActiveWizard::None); - let shutdown = tokio::time::timeout(std::time::Duration::from_secs(1), shutdown_rx).await; + let shutdown = timeout(Duration::from_secs(1), shutdown_rx).await; assert!(matches!(shutdown, Ok(Ok(())))); } diff --git a/crates/defguard_setup/tests/initial_setup.rs b/crates/defguard_setup/tests/initial_setup.rs index 0b2eef118..2ae88454e 100644 --- a/crates/defguard_setup/tests/initial_setup.rs +++ b/crates/defguard_setup/tests/initial_setup.rs @@ -1,6 +1,7 @@ use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, sync::Arc, + time::Duration, }; use axum::serve; @@ -31,6 +32,7 @@ use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use tokio::{ net::TcpListener, sync::{Notify, oneshot}, + time::timeout, }; mod common; @@ -468,8 +470,7 @@ async fn test_finish_setup(_: PgPoolOptions, options: PgConnectOptions) { assert_setup_step(&pool, InitialSetupStep::Finished).await; - let shutdown_signal = - tokio::time::timeout(std::time::Duration::from_secs(1), shutdown_rx).await; + let shutdown_signal = timeout(std::time::Duration::from_secs(1), shutdown_rx).await; assert!(matches!(shutdown_signal, Ok(Ok(())))); } @@ -628,13 +629,9 @@ async fn test_setup_flow(_: PgPoolOptions, options: PgConnectOptions) { .expect("Session not created"); assert_eq!(session.user_id, admin_user.id); - let shutdown_signal = tokio::time::timeout( - std::time::Duration::from_secs(1), - shutdown_notify.notified(), - ) - .await; + let shutdown_signal = timeout(Duration::from_secs(1), shutdown_notify.notified()).await; assert!(shutdown_signal.is_ok()); - let server_result = tokio::time::timeout(std::time::Duration::from_secs(1), server_task).await; + let server_result = timeout(Duration::from_secs(1), server_task).await; assert!(matches!(server_result, Ok(Ok(())))); } diff --git a/crates/defguard_setup/tests/migration_wizard.rs b/crates/defguard_setup/tests/migration_wizard.rs index 53a8ddfa2..1ba6ffaeb 100644 --- a/crates/defguard_setup/tests/migration_wizard.rs +++ b/crates/defguard_setup/tests/migration_wizard.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use defguard_common::{ config::DefGuardConfig, db::{ @@ -18,6 +20,7 @@ use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; mod common; use common::{init_settings_with_secret_key, make_migration_test_client, seed_admin_user}; +use tokio::time::timeout; async fn assert_migration_step(pool: &sqlx::PgPool, expected_variant: &str) { let state = MigrationWizardState::get(pool) @@ -170,8 +173,7 @@ async fn test_migration_full_flow(_: PgPoolOptions, options: PgConnectOptions) { "Migration wizard state should be cleared after finish" ); - let shutdown_signal = - tokio::time::timeout(std::time::Duration::from_secs(1), shutdown_rx).await; + let shutdown_signal = timeout(Duration::from_secs(1), shutdown_rx).await; assert!( matches!(shutdown_signal, Ok(Ok(()))), "Migration server should have sent shutdown signal after finish" From aef397833145770149e4230a9d2ecab08bf20e5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 15 Apr 2026 08:23:09 +0200 Subject: [PATCH 10/46] update protos --- proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto b/proto index 7adfe3bfd..7e1b82934 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 7adfe3bfd1b7b701e58d25ddadd0c0c7a4a3e046 +Subproject commit 7e1b829343832d261fcfefc4587a795a975dec7e From 564dc72c8c18c6b25ff1401e2e18dc40e1ff39a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 15 Apr 2026 08:25:46 +0200 Subject: [PATCH 11/46] cleanup --- crates/defguard_core/tests/integration/api/proxy_certs.rs | 2 ++ crates/defguard_setup/tests/auto_wizard_url_settings.rs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/crates/defguard_core/tests/integration/api/proxy_certs.rs b/crates/defguard_core/tests/integration/api/proxy_certs.rs index 60be48957..d47e9d5e3 100644 --- a/crates/defguard_core/tests/integration/api/proxy_certs.rs +++ b/crates/defguard_core/tests/integration/api/proxy_certs.rs @@ -9,6 +9,7 @@ use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, sync::{Arc, Mutex}, + time::Duration, }; use axum_extra::extract::cookie::Key; @@ -46,6 +47,7 @@ use tokio::{ broadcast, mpsc::{Receiver, Sender, channel, unbounded_channel}, }, + time::sleep, }; use super::common::{client::TestClient, generate_test_cert_pem}; diff --git a/crates/defguard_setup/tests/auto_wizard_url_settings.rs b/crates/defguard_setup/tests/auto_wizard_url_settings.rs index 91d86563f..7f6662d04 100644 --- a/crates/defguard_setup/tests/auto_wizard_url_settings.rs +++ b/crates/defguard_setup/tests/auto_wizard_url_settings.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use defguard_certs::{CertificateAuthority, Csr, PemLabel, der_to_pem, generate_key_pair}; use defguard_common::{ config::DefGuardConfig, @@ -25,6 +27,7 @@ use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; mod common; use common::make_setup_test_client; +use tokio::time::timeout; const SESSION_COOKIE_NAME: &str = "defguard_session"; From 9e1054e34b479dd978baec19bb8024ee15865f22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Wed, 15 Apr 2026 17:09:51 +0200 Subject: [PATCH 12/46] add shared interceptor to check client cert serial --- crates/defguard_grpc_tls/src/lib.rs | 1 + crates/defguard_grpc_tls/src/server.rs | 62 ++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 crates/defguard_grpc_tls/src/server.rs diff --git a/crates/defguard_grpc_tls/src/lib.rs b/crates/defguard_grpc_tls/src/lib.rs index b7a37f7f9..2d3ab7fc2 100644 --- a/crates/defguard_grpc_tls/src/lib.rs +++ b/crates/defguard_grpc_tls/src/lib.rs @@ -1,2 +1,3 @@ pub mod certs; pub mod connector; +pub mod server; diff --git a/crates/defguard_grpc_tls/src/server.rs b/crates/defguard_grpc_tls/src/server.rs new file mode 100644 index 000000000..c06b03545 --- /dev/null +++ b/crates/defguard_grpc_tls/src/server.rs @@ -0,0 +1,62 @@ +//! Server-side mTLS utilities for gateway and proxy gRPC servers. + +use tonic::{ + Request, Status, + transport::server::{TcpConnectInfo, TlsConnectInfo}, +}; +use x509_parser::prelude::*; + +/// Returns a tonic interceptor closure that enforces the Core client certificate serial. +/// +/// On every incoming RPC the interceptor: +/// 1. Reads the peer certificate from [`TlsConnectInfo`] (populated by tonic's TLS stack). +/// 2. Parses its serial via `x509_parser`. +/// 3. Rejects the request with [`Status::unauthenticated`] if the serial does not match +/// `expected_serial` (case-insensitive, colon-separated hex comparison). +/// +/// When `expected_serial` is `None` the check is skipped entirely, which allows the +/// same service builder chain to be used in plain-HTTP (no-TLS) development mode. +/// +/// # Usage +/// +/// Place this interceptor **outermost** in the `ServiceBuilder` chain so that +/// authentication runs before any other middleware: +/// +/// ```rust,ignore +/// ServiceBuilder::new() +/// .layer(tonic::service::interceptor(certificate_serial_interceptor(Some(serial)))) +/// .layer(/* version layer */) +/// .service(/* gRPC service */) +/// ``` +pub fn certificate_serial_interceptor( + expected_serial: Option, +) -> impl Fn(Request<()>) -> Result, Status> + Clone + Send + 'static { + move |req| { + let Some(ref serial) = expected_serial else { + return Ok(req); + }; + + let certs = req + .extensions() + .get::>() + .and_then(|info| info.peer_certs()) + .ok_or_else(|| Status::unauthenticated("Missing client certificate"))?; + + let der = certs + .first() + .ok_or_else(|| Status::unauthenticated("Empty client certificate chain"))?; + + let (_, cert) = parse_x509_certificate(der) + .map_err(|_| Status::unauthenticated("Invalid client certificate"))?; + + let peer_serial = cert.tbs_certificate.raw_serial_as_string(); + + if !peer_serial.eq_ignore_ascii_case(serial) { + return Err(Status::unauthenticated( + "Client certificate serial mismatch", + )); + } + + Ok(req) + } +} From cb7231f5f0e72ddead5aa162e09047939fa1939f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 16 Apr 2026 09:55:58 +0200 Subject: [PATCH 13/46] change interceptor signature --- crates/defguard_grpc_tls/src/server.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/defguard_grpc_tls/src/server.rs b/crates/defguard_grpc_tls/src/server.rs index c06b03545..e07c47b3b 100644 --- a/crates/defguard_grpc_tls/src/server.rs +++ b/crates/defguard_grpc_tls/src/server.rs @@ -29,13 +29,9 @@ use x509_parser::prelude::*; /// .service(/* gRPC service */) /// ``` pub fn certificate_serial_interceptor( - expected_serial: Option, + expected_serial: String, ) -> impl Fn(Request<()>) -> Result, Status> + Clone + Send + 'static { move |req| { - let Some(ref serial) = expected_serial else { - return Ok(req); - }; - let certs = req .extensions() .get::>() @@ -51,7 +47,7 @@ pub fn certificate_serial_interceptor( let peer_serial = cert.tbs_certificate.raw_serial_as_string(); - if !peer_serial.eq_ignore_ascii_case(serial) { + if !peer_serial.eq_ignore_ascii_case(&expected_serial) { return Err(Status::unauthenticated( "Client certificate serial mismatch", )); From c12c46ccca1b3d254226f37166ce8403e2990abb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 16 Apr 2026 10:59:54 +0200 Subject: [PATCH 14/46] handle proxy handler shutdown during reconnect backoff --- crates/defguard_proxy_manager/src/handler.rs | 27 +++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/crates/defguard_proxy_manager/src/handler.rs b/crates/defguard_proxy_manager/src/handler.rs index 9bfad866f..c995ce63a 100644 --- a/crates/defguard_proxy_manager/src/handler.rs +++ b/crates/defguard_proxy_manager/src/handler.rs @@ -330,7 +330,14 @@ impl ProxyHandler { self.retry_delay() ); self.mark_disconnected().await?; - sleep(self.retry_delay()).await; + let mut shutdown = self.shutdown_signal.lock().await; + select! { + _ = sleep(self.retry_delay()) => {} + _ = &mut *shutdown => { + debug!("Shutdown signal received during reconnect backoff (connect_channel failure), stopping"); + break; + } + } continue; } }; @@ -372,7 +379,14 @@ impl ProxyHandler { map.remove(&self.proxy_id); } self.mark_disconnected().await?; - sleep(self.retry_delay()).await; + let mut shutdown = self.shutdown_signal.lock().await; + select! { + _ = sleep(self.retry_delay()) => {} + _ = &mut *shutdown => { + debug!("Shutdown signal received during reconnect backoff (bidi failure), stopping"); + break; + } + } continue; } }; @@ -397,7 +411,14 @@ impl ProxyHandler { data.insert(&incompatible_components); // Sleep before trying to reconnect - sleep(self.retry_delay()).await; + let mut shutdown = self.shutdown_signal.lock().await; + select! { + _ = sleep(self.retry_delay()) => {} + _ = &mut *shutdown => { + debug!("Shutdown signal received during reconnect backoff (version incompatible), stopping"); + break; + } + } continue; } IncompatibleComponents::remove_proxy(&incompatible_components); From dfbf7a52465043b773b735d0e1059738e9ad4c37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 16 Apr 2026 14:18:51 +0200 Subject: [PATCH 15/46] remove more paths --- crates/defguard_core/tests/integration/api/acl/aliases.rs | 2 +- .../defguard_core/tests/integration/api/acl/destinations.rs | 6 ++++-- crates/defguard_core/tests/integration/api/acl/rules.rs | 4 ++-- crates/defguard_setup/tests/initial_setup.rs | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/defguard_core/tests/integration/api/acl/aliases.rs b/crates/defguard_core/tests/integration/api/acl/aliases.rs index 11e43f13a..ca147cbb7 100644 --- a/crates/defguard_core/tests/integration/api/acl/aliases.rs +++ b/crates/defguard_core/tests/integration/api/acl/aliases.rs @@ -401,7 +401,7 @@ async fn test_alias_audit_fields_track_acting_user_across_mutations( assert_ne!(created_alias_row.modified_by, "admin"); let created_modified_at = created_alias_row.modified_at; - sleep(std::time::Duration::from_millis(2)).await; + sleep(Duration::from_millis(2)).await; let mut alias_update = created_alias.clone(); alias_update.name = "alias updated by hpotter".to_string(); diff --git a/crates/defguard_core/tests/integration/api/acl/destinations.rs b/crates/defguard_core/tests/integration/api/acl/destinations.rs index fdc6f42b7..5771e15df 100644 --- a/crates/defguard_core/tests/integration/api/acl/destinations.rs +++ b/crates/defguard_core/tests/integration/api/acl/destinations.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use tokio::time::sleep; use super::*; @@ -553,7 +555,7 @@ async fn test_destination_audit_fields_track_acting_user_across_mutations( assert_ne!(created_destination_row.modified_by, "admin"); let created_modified_at = created_destination_row.modified_at; - sleep(std::time::Duration::from_millis(2)).await; + sleep(Duration::from_millis(2)).await; let mut destination_update = created_destination.clone(); destination_update.name = "destination updated by hpotter".to_string(); @@ -582,7 +584,7 @@ async fn test_destination_audit_fields_track_acting_user_across_mutations( assert!(updated_destination_row.modified_at > created_modified_at); let updated_modified_at = updated_destination_row.modified_at; - sleep(std::time::Duration::from_millis(2)).await; + sleep(Duration::from_millis(2)).await; let response = client .put("/api/v1/acl/destination/apply") diff --git a/crates/defguard_core/tests/integration/api/acl/rules.rs b/crates/defguard_core/tests/integration/api/acl/rules.rs index a69d6c897..072faa485 100644 --- a/crates/defguard_core/tests/integration/api/acl/rules.rs +++ b/crates/defguard_core/tests/integration/api/acl/rules.rs @@ -1411,7 +1411,7 @@ async fn test_rule_audit_fields_track_acting_user_across_mutations( assert_ne!(created_rule_row.modified_by, "admin"); let created_modified_at = created_rule_row.modified_at; - sleep(std::time::Duration::from_millis(2)).await; + sleep(Duration::from_millis(2)).await; let mut updated_rule = created_rule.clone(); updated_rule.name = "rule updated by hpotter".to_string(); @@ -1431,7 +1431,7 @@ async fn test_rule_audit_fields_track_acting_user_across_mutations( assert!(updated_rule_row.modified_at > created_modified_at); let updated_modified_at = updated_rule_row.modified_at; - sleep(std::time::Duration::from_millis(2)).await; + sleep(Duration::from_millis(2)).await; let response = client .put("/api/v1/acl/rule/apply") diff --git a/crates/defguard_setup/tests/initial_setup.rs b/crates/defguard_setup/tests/initial_setup.rs index 2ae88454e..621fd1a06 100644 --- a/crates/defguard_setup/tests/initial_setup.rs +++ b/crates/defguard_setup/tests/initial_setup.rs @@ -470,7 +470,7 @@ async fn test_finish_setup(_: PgPoolOptions, options: PgConnectOptions) { assert_setup_step(&pool, InitialSetupStep::Finished).await; - let shutdown_signal = timeout(std::time::Duration::from_secs(1), shutdown_rx).await; + let shutdown_signal = timeout(Duration::from_secs(1), shutdown_rx).await; assert!(matches!(shutdown_signal, Ok(Ok(())))); } From 32101efbaa1d795d27415e75b7ce42fb7e05c15d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 16 Apr 2026 17:01:01 +0200 Subject: [PATCH 16/46] review cleanup --- crates/defguard_certs/src/lib.rs | 4 ++-- crates/defguard_core/tests/integration/api/acl/aliases.rs | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs index dec24486c..59c8aab7a 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -143,7 +143,7 @@ impl CertificateAuthority<'_> { Ok(cert) } - /// Issue a Core gRPC client certificate for a specific gateway or proxy. + /// Issue a Core gRPC client certificate for a specific Gateway or Proxy. /// /// Generates a fresh key pair, creates a CSR with `common_name` as both /// the Subject CN and the SAN DNS name, signs it with `ClientAuth` EKU, @@ -188,7 +188,7 @@ impl CertificateAuthority<'_> { } } -/// A Core gRPC client certificate issued for a specific gateway or proxy component. +/// A Core gRPC client certificate issued for a specific Gateway or Proxy component. /// /// The DER bytes are stored in the database; the key bytes never leave Core. pub struct CoreClientCert { diff --git a/crates/defguard_core/tests/integration/api/acl/aliases.rs b/crates/defguard_core/tests/integration/api/acl/aliases.rs index ca147cbb7..8cdcefe09 100644 --- a/crates/defguard_core/tests/integration/api/acl/aliases.rs +++ b/crates/defguard_core/tests/integration/api/acl/aliases.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use tokio::time::sleep; use super::*; @@ -423,7 +425,7 @@ async fn test_alias_audit_fields_track_acting_user_across_mutations( assert!(updated_alias_row.modified_at > created_modified_at); let updated_modified_at = updated_alias_row.modified_at; - sleep(std::time::Duration::from_millis(2)).await; + sleep(Duration::from_millis(2)).await; let response = client .put("/api/v1/acl/alias/apply") From cde7255507b11e2f578450f993064f8a093e96e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 17 Apr 2026 07:59:23 +0200 Subject: [PATCH 17/46] add more timeout constants --- .../defguard_core/tests/integration/api/acl/rules.rs | 2 ++ crates/defguard_proxy_manager/src/tests/common/mod.rs | 2 ++ .../src/tests/proxy_manager/handler/acme.rs | 6 +++--- .../src/tests/proxy_manager/handler/mfa.rs | 11 ++++------- .../src/tests/proxy_manager/handler/support.rs | 8 +++----- crates/defguard_setup/tests/auto_adoption_wizard.rs | 6 +++--- .../defguard_setup/tests/auto_wizard_url_settings.rs | 6 ++---- crates/defguard_setup/tests/common/mod.rs | 5 +++++ crates/defguard_setup/tests/initial_setup.rs | 9 ++++----- crates/defguard_setup/tests/migration_wizard.rs | 8 ++++---- 10 files changed, 32 insertions(+), 31 deletions(-) diff --git a/crates/defguard_core/tests/integration/api/acl/rules.rs b/crates/defguard_core/tests/integration/api/acl/rules.rs index 072faa485..75257d9a3 100644 --- a/crates/defguard_core/tests/integration/api/acl/rules.rs +++ b/crates/defguard_core/tests/integration/api/acl/rules.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use tokio::time::sleep; use super::*; diff --git a/crates/defguard_proxy_manager/src/tests/common/mod.rs b/crates/defguard_proxy_manager/src/tests/common/mod.rs index 4e4a30bf0..95a333b50 100644 --- a/crates/defguard_proxy_manager/src/tests/common/mod.rs +++ b/crates/defguard_proxy_manager/src/tests/common/mod.rs @@ -54,6 +54,8 @@ pub(crate) const CORE_RESPONSE_TIMEOUT: Duration = Duration::from_millis(200); pub(crate) const PROXY_CONNECT_DELAY: Duration = Duration::from_millis(20); +pub(crate) const RECEIVE_TIMEOUT: Duration = Duration::from_secs(5); + /// Minimum proxy version that passes `is_proxy_version_supported()`. const MOCK_PROXY_VERSION: defguard_version::Version = defguard_version::Version::new(2, 0, 0); diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/acme.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/acme.rs index e9bae41f3..66da653ef 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/acme.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/acme.rs @@ -3,11 +3,11 @@ use defguard_proto::proxy::{ AcmeCertificate as AcmeCertPayload, CoreRequest, core_request, core_response, }; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -use tokio::time::{Duration, timeout}; +use tokio::time::timeout; use super::support::complete_proxy_handshake; use crate::tests::common::{ - HandlerTestContext, ManagerTestContext, MockProxyHarness, create_proxy, + HandlerTestContext, ManagerTestContext, MockProxyHarness, RECEIVE_TIMEOUT, create_proxy, }; /// A minimal but syntactically valid PEM certificate block (content is @@ -188,7 +188,7 @@ async fn test_acme_certificate_broadcasts_to_connected_proxy( ("proxy A (sender)", &mut mock_a), ("proxy B (bystander)", &mut mock_b), ] { - let response = timeout(Duration::from_secs(5), mock.recv_outbound()) + let response = timeout(RECEIVE_TIMEOUT, mock.recv_outbound()) .await .unwrap_or_else(|_| panic!("timed out waiting for HttpsCerts broadcast on {label}")); diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa.rs index 57b946312..3b8758549 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa.rs @@ -1,5 +1,3 @@ -use std::time::Duration; - use defguard_common::db::Id; use defguard_core::grpc::GatewayEvent; use defguard_proto::{ @@ -17,9 +15,8 @@ use super::support::{ send_mfa_finish_no_recv, send_mfa_finish_raw, send_mfa_start, send_token_validation, setup_user_email_mfa, setup_user_totp_mfa, }; -use crate::tests::common::HandlerTestContext; +use crate::tests::common::{HandlerTestContext, RECEIVE_TIMEOUT}; -const EVENT_RECEIVE_TIMEOUT: Duration = Duration::from_secs(5); const WRONG_REQUEST_ID: u64 = 9991; const AWAIT_ID: u64 = 8000; @@ -141,7 +138,7 @@ async fn test_mfa_finish_succeeds_with_totp_code(_: PgPoolOptions, options: PgCo // Verify GatewayEvent::MfaSessionAuthorized was broadcast. // Use the already-subscribed receiver — subscribing after send_mfa_finish would miss the event. - let event = timeout(EVENT_RECEIVE_TIMEOUT, gateway_rx.recv()) + let event = timeout(RECEIVE_TIMEOUT, gateway_rx.recv()) .await .expect("timed out waiting for GatewayEvent::MfaSessionAuthorized") .expect("gateway event channel closed"); @@ -316,7 +313,7 @@ async fn test_mfa_finish_succeeds_and_creates_session(_: PgPoolOptions, options: assert!(session.preshared_key.is_some()); // Verify GatewayEvent::MfaSessionAuthorized was broadcast - let event = timeout(EVENT_RECEIVE_TIMEOUT, gateway_rx.recv()) + let event = timeout(RECEIVE_TIMEOUT, gateway_rx.recv()) .await .expect("timed out waiting for GatewayEvent::MfaSessionAuthorized") .expect("gateway event channel closed"); @@ -579,7 +576,7 @@ async fn test_mfa_finish_replaces_existing_session_disconnects_old( let mut got_disconnected = false; let mut got_authorized = false; for _ in 0..2 { - let event = timeout(EVENT_RECEIVE_TIMEOUT, gw_rx2.recv()) + let event = timeout(RECEIVE_TIMEOUT, gw_rx2.recv()) .await .expect("timed out waiting for gateway event after second MFA finish") .expect("gateway event channel closed"); diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/support.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/support.rs index 9536271c8..916900bb4 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/support.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/support.rs @@ -2,7 +2,7 @@ use std::{ mem::discriminant, str::FromStr, sync::atomic::{AtomicU16, AtomicU64, Ordering}, - time::{Duration, SystemTime}, + time::SystemTime, }; use defguard_common::{ @@ -45,9 +45,7 @@ use sqlx::PgPool; use tokio::{sync::mpsc::UnboundedReceiver, time::timeout}; use tonic::Code; -use crate::tests::common::{HandlerTestContext, MockOidcProvider}; - -const BIDI_RECEIVE_TIMEOUT: Duration = Duration::from_secs(5); +use crate::tests::common::{HandlerTestContext, MockOidcProvider, RECEIVE_TIMEOUT}; /// A strong password satisfying all `check_password_strength` requirements: /// ≥8 chars, digit, upper, lower, special character. @@ -621,7 +619,7 @@ pub(crate) async fn send_token_validation(context: &mut HandlerTestContext, toke pub(crate) async fn expect_bidi_mfa_success( bidi_rx: &mut UnboundedReceiver, ) -> Id { - let event = timeout(BIDI_RECEIVE_TIMEOUT, bidi_rx.recv()) + let event = timeout(RECEIVE_TIMEOUT, bidi_rx.recv()) .await .expect("timed out waiting for BidiStreamEvent DesktopClientMfa(Success)") .expect("bidi event channel closed"); diff --git a/crates/defguard_setup/tests/auto_adoption_wizard.rs b/crates/defguard_setup/tests/auto_adoption_wizard.rs index a33ab85d6..07f8b7265 100644 --- a/crates/defguard_setup/tests/auto_adoption_wizard.rs +++ b/crates/defguard_setup/tests/auto_adoption_wizard.rs @@ -1,4 +1,4 @@ -use std::{sync::Once, time::Duration}; +use std::sync::Once; use defguard_common::{ config::DefGuardConfig, @@ -27,7 +27,7 @@ use tokio::time::timeout; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; mod common; -use common::make_setup_test_client; +use common::{SHUTDOWN_TIMEOUT, make_setup_test_client}; const SESSION_COOKIE_NAME: &str = "defguard_session"; @@ -213,7 +213,7 @@ async fn test_auto_adoption_full_flow(_: PgPoolOptions, options: PgConnectOption assert!(wizard.completed); assert_eq!(wizard.active_wizard, ActiveWizard::None); - let shutdown_signal = timeout(Duration::from_secs(1), shutdown_rx).await; + let shutdown_signal = timeout(SHUTDOWN_TIMEOUT, shutdown_rx).await; assert!( matches!(shutdown_signal, Ok(Ok(()))), "Setup server should have sent shutdown signal after finish" diff --git a/crates/defguard_setup/tests/auto_wizard_url_settings.rs b/crates/defguard_setup/tests/auto_wizard_url_settings.rs index 7f6662d04..74f2e9e92 100644 --- a/crates/defguard_setup/tests/auto_wizard_url_settings.rs +++ b/crates/defguard_setup/tests/auto_wizard_url_settings.rs @@ -1,5 +1,3 @@ -use std::time::Duration; - use defguard_certs::{CertificateAuthority, Csr, PemLabel, der_to_pem, generate_key_pair}; use defguard_common::{ config::DefGuardConfig, @@ -26,7 +24,7 @@ use serde_json::json; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; mod common; -use common::make_setup_test_client; +use common::{SHUTDOWN_TIMEOUT, make_setup_test_client}; use tokio::time::timeout; const SESSION_COOKIE_NAME: &str = "defguard_session"; @@ -544,6 +542,6 @@ async fn test_auto_adoption_full_flow_new_url_steps(_: PgPoolOptions, options: P assert!(wizard.completed); assert_eq!(wizard.active_wizard, ActiveWizard::None); - let shutdown = timeout(Duration::from_secs(1), shutdown_rx).await; + let shutdown = timeout(SHUTDOWN_TIMEOUT, shutdown_rx).await; assert!(matches!(shutdown, Ok(Ok(())))); } diff --git a/crates/defguard_setup/tests/common/mod.rs b/crates/defguard_setup/tests/common/mod.rs index e65650772..62eb14ab7 100644 --- a/crates/defguard_setup/tests/common/mod.rs +++ b/crates/defguard_setup/tests/common/mod.rs @@ -173,3 +173,8 @@ pub async fn seed_admin_user(pool: &PgPool, username: &str, password: &str) -> U user } + +use std::time::Duration; + +#[allow(dead_code)] +pub(crate) const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(1); diff --git a/crates/defguard_setup/tests/initial_setup.rs b/crates/defguard_setup/tests/initial_setup.rs index 621fd1a06..57689af71 100644 --- a/crates/defguard_setup/tests/initial_setup.rs +++ b/crates/defguard_setup/tests/initial_setup.rs @@ -1,7 +1,6 @@ use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, sync::Arc, - time::Duration, }; use axum::serve; @@ -36,7 +35,7 @@ use tokio::{ }; mod common; -use common::make_setup_test_client; +use common::{SHUTDOWN_TIMEOUT, make_setup_test_client}; const SESSION_COOKIE_NAME: &str = "defguard_session"; @@ -470,7 +469,7 @@ async fn test_finish_setup(_: PgPoolOptions, options: PgConnectOptions) { assert_setup_step(&pool, InitialSetupStep::Finished).await; - let shutdown_signal = timeout(Duration::from_secs(1), shutdown_rx).await; + let shutdown_signal = timeout(SHUTDOWN_TIMEOUT, shutdown_rx).await; assert!(matches!(shutdown_signal, Ok(Ok(())))); } @@ -629,9 +628,9 @@ async fn test_setup_flow(_: PgPoolOptions, options: PgConnectOptions) { .expect("Session not created"); assert_eq!(session.user_id, admin_user.id); - let shutdown_signal = timeout(Duration::from_secs(1), shutdown_notify.notified()).await; + let shutdown_signal = timeout(SHUTDOWN_TIMEOUT, shutdown_notify.notified()).await; assert!(shutdown_signal.is_ok()); - let server_result = timeout(Duration::from_secs(1), server_task).await; + let server_result = timeout(SHUTDOWN_TIMEOUT, server_task).await; assert!(matches!(server_result, Ok(Ok(())))); } diff --git a/crates/defguard_setup/tests/migration_wizard.rs b/crates/defguard_setup/tests/migration_wizard.rs index 1ba6ffaeb..ca3ab0aa8 100644 --- a/crates/defguard_setup/tests/migration_wizard.rs +++ b/crates/defguard_setup/tests/migration_wizard.rs @@ -1,5 +1,3 @@ -use std::time::Duration; - use defguard_common::{ config::DefGuardConfig, db::{ @@ -19,7 +17,9 @@ use serde_json::json; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; mod common; -use common::{init_settings_with_secret_key, make_migration_test_client, seed_admin_user}; +use common::{ + SHUTDOWN_TIMEOUT, init_settings_with_secret_key, make_migration_test_client, seed_admin_user, +}; use tokio::time::timeout; async fn assert_migration_step(pool: &sqlx::PgPool, expected_variant: &str) { @@ -173,7 +173,7 @@ async fn test_migration_full_flow(_: PgPoolOptions, options: PgConnectOptions) { "Migration wizard state should be cleared after finish" ); - let shutdown_signal = timeout(Duration::from_secs(1), shutdown_rx).await; + let shutdown_signal = timeout(SHUTDOWN_TIMEOUT, shutdown_rx).await; assert!( matches!(shutdown_signal, Ok(Ok(()))), "Migration server should have sent shutdown signal after finish" From 4f6c19baf85beb270ffb3cfbe51a4b16e787eb1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 17 Apr 2026 08:34:01 +0200 Subject: [PATCH 18/46] reorganize setup integration tests to fix code sharing --- .../{ => integration}/auto_adoption_wizard.rs | 19 +++---- .../auto_wizard_url_settings.rs | 9 ++-- .../{common/mod.rs => integration/common.rs} | 14 ++---- .../tests/{ => integration}/initial_setup.rs | 5 +- .../defguard_setup/tests/integration/main.rs | 8 +++ .../{ => integration}/migration_wizard.rs | 3 +- .../tests/{ => integration}/session_info.rs | 5 +- .../tests/{ => integration}/wizard_init.rs | 0 .../tests/{ => integration}/wizard_state.rs | 50 ++++++++----------- 9 files changed, 54 insertions(+), 59 deletions(-) rename crates/defguard_setup/tests/{ => integration}/auto_adoption_wizard.rs (96%) rename crates/defguard_setup/tests/{ => integration}/auto_wizard_url_settings.rs (98%) rename crates/defguard_setup/tests/{common/mod.rs => integration/common.rs} (96%) rename crates/defguard_setup/tests/{ => integration}/initial_setup.rs (99%) create mode 100644 crates/defguard_setup/tests/integration/main.rs rename crates/defguard_setup/tests/{ => integration}/migration_wizard.rs (99%) rename crates/defguard_setup/tests/{ => integration}/session_info.rs (98%) rename crates/defguard_setup/tests/{ => integration}/wizard_init.rs (100%) rename crates/defguard_setup/tests/{ => integration}/wizard_state.rs (86%) diff --git a/crates/defguard_setup/tests/auto_adoption_wizard.rs b/crates/defguard_setup/tests/integration/auto_adoption_wizard.rs similarity index 96% rename from crates/defguard_setup/tests/auto_adoption_wizard.rs rename to crates/defguard_setup/tests/integration/auto_adoption_wizard.rs index 07f8b7265..433fccd8b 100644 --- a/crates/defguard_setup/tests/auto_adoption_wizard.rs +++ b/crates/defguard_setup/tests/integration/auto_adoption_wizard.rs @@ -7,7 +7,9 @@ use defguard_common::{ models::{ Settings, WireguardNetwork, settings::initialize_current_settings, - setup_auto_adoption::{AutoAdoptionWizardState, AutoAdoptionWizardStep}, + setup_auto_adoption::{ + AutoAdoptionWizardState, AutoAdoptionWizardStep, SetupAutoAdoptionComponent, + }, wireguard::{LocationMfaMode, ServiceLocationMode}, wizard::{ActiveWizard, Wizard}, }, @@ -26,10 +28,9 @@ use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use tokio::time::timeout; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; -mod common; -use common::{SHUTDOWN_TIMEOUT, make_setup_test_client}; +use crate::common::SESSION_COOKIE_NAME; -const SESSION_COOKIE_NAME: &str = "defguard_session"; +use super::common::{SHUTDOWN_TIMEOUT, make_setup_test_client}; fn init_tracing_once() { static ONCE: Once = Once::new(); @@ -436,7 +437,7 @@ async fn test_attempt_auto_adoption_requires_both_flags( _: PgPoolOptions, options: PgConnectOptions, ) { - let pool = defguard_common::db::setup_pool(options).await; + let pool = setup_pool(options).await; initialize_current_settings(&pool) .await .expect("Failed to initialize settings"); @@ -473,7 +474,7 @@ async fn test_attempt_auto_adoption_persists_actionable_edge_failure_logs( ) { init_tracing_once(); - let pool = defguard_common::db::setup_pool(options).await; + let pool = setup_pool(options).await; initialize_current_settings(&pool) .await .expect("Failed to initialize settings"); @@ -496,7 +497,7 @@ async fn test_attempt_auto_adoption_persists_actionable_edge_failure_logs( let edge_result = state .adoption_result - .get(&defguard_common::db::models::setup_auto_adoption::SetupAutoAdoptionComponent::Edge) + .get(&SetupAutoAdoptionComponent::Edge) .expect("Expected edge adoption result"); assert!(!edge_result.success, "Edge auto-adoption should fail"); @@ -530,7 +531,7 @@ async fn test_attempt_auto_adoption_persists_actionable_gateway_failure_logs( ) { init_tracing_once(); - let pool = defguard_common::db::setup_pool(options).await; + let pool = setup_pool(options).await; initialize_current_settings(&pool) .await .expect("Failed to initialize settings"); @@ -553,7 +554,7 @@ async fn test_attempt_auto_adoption_persists_actionable_gateway_failure_logs( let gateway_result = state .adoption_result - .get(&defguard_common::db::models::setup_auto_adoption::SetupAutoAdoptionComponent::Gateway) + .get(&SetupAutoAdoptionComponent::Gateway) .expect("Expected gateway adoption result"); assert!(!gateway_result.success, "Gateway auto-adoption should fail"); diff --git a/crates/defguard_setup/tests/auto_wizard_url_settings.rs b/crates/defguard_setup/tests/integration/auto_wizard_url_settings.rs similarity index 98% rename from crates/defguard_setup/tests/auto_wizard_url_settings.rs rename to crates/defguard_setup/tests/integration/auto_wizard_url_settings.rs index 74f2e9e92..289558ded 100644 --- a/crates/defguard_setup/tests/auto_wizard_url_settings.rs +++ b/crates/defguard_setup/tests/integration/auto_wizard_url_settings.rs @@ -23,15 +23,14 @@ use reqwest::{ use serde_json::json; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -mod common; -use common::{SHUTDOWN_TIMEOUT, make_setup_test_client}; -use tokio::time::timeout; +use crate::common::{SESSION_COOKIE_NAME, TestClient}; -const SESSION_COOKIE_NAME: &str = "defguard_session"; +use super::common::{SHUTDOWN_TIMEOUT, make_setup_test_client}; +use tokio::{sync::oneshot, time::timeout}; async fn bootstrap_wizard_to_url_settings( pool: &sqlx::PgPool, -) -> (common::TestClient, tokio::sync::oneshot::Receiver<()>) { +) -> (TestClient, oneshot::Receiver<()>) { Wizard::init(pool, true, &DefGuardConfig::new_test_config()) .await .expect("Failed to init wizard"); diff --git a/crates/defguard_setup/tests/common/mod.rs b/crates/defguard_setup/tests/integration/common.rs similarity index 96% rename from crates/defguard_setup/tests/common/mod.rs rename to crates/defguard_setup/tests/integration/common.rs index 62eb14ab7..a14ccd46c 100644 --- a/crates/defguard_setup/tests/common/mod.rs +++ b/crates/defguard_setup/tests/integration/common.rs @@ -1,6 +1,7 @@ use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, sync::Arc, + time::Duration, }; use axum::serve; @@ -26,7 +27,9 @@ use semver::Version; use sqlx::PgPool; use tokio::{net::TcpListener, sync::oneshot, task::JoinHandle}; -#[allow(dead_code)] +pub const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(1); +pub const SESSION_COOKIE_NAME: &str = "defguard_session"; + pub const TEST_SECRET_KEY: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; @@ -84,7 +87,6 @@ impl TestClient { } } -#[allow(dead_code)] pub async fn make_setup_test_client(pool: PgPool) -> (TestClient, oneshot::Receiver<()>) { let (setup_shutdown_tx, setup_shutdown_rx) = oneshot::channel::<()>(); let app = build_setup_webapp( @@ -99,7 +101,6 @@ pub async fn make_setup_test_client(pool: PgPool) -> (TestClient, oneshot::Recei (TestClient::new(app, listener), setup_shutdown_rx) } -#[allow(dead_code)] pub async fn make_migration_test_client( pool: PgPool, ) -> ( @@ -127,7 +128,6 @@ pub async fn make_migration_test_client( /// Initialise settings with a known secret key so `build_migration_webapp` can /// call `secret_key_required()` without panicking. Also initialises SERVER_CONFIG /// so the auth handler can call `server_config()`. -#[allow(dead_code)] pub async fn init_settings_with_secret_key(pool: &PgPool) { initialize_current_settings(pool) .await @@ -146,7 +146,6 @@ pub async fn init_settings_with_secret_key(pool: &PgPool) { /// Creates an admin group + admin user and returns the user. /// `User::is_admin()` checks group membership, not a column flag. -#[allow(dead_code)] pub async fn seed_admin_user(pool: &PgPool, username: &str, password: &str) -> User { let mut admin_group = Group::new("admins"); admin_group.is_admin = true; @@ -173,8 +172,3 @@ pub async fn seed_admin_user(pool: &PgPool, username: &str, password: &str) -> U user } - -use std::time::Duration; - -#[allow(dead_code)] -pub(crate) const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(1); diff --git a/crates/defguard_setup/tests/initial_setup.rs b/crates/defguard_setup/tests/integration/initial_setup.rs similarity index 99% rename from crates/defguard_setup/tests/initial_setup.rs rename to crates/defguard_setup/tests/integration/initial_setup.rs index 57689af71..08816b2b3 100644 --- a/crates/defguard_setup/tests/initial_setup.rs +++ b/crates/defguard_setup/tests/integration/initial_setup.rs @@ -34,10 +34,9 @@ use tokio::{ time::timeout, }; -mod common; -use common::{SHUTDOWN_TIMEOUT, make_setup_test_client}; +use crate::common::SESSION_COOKIE_NAME; -const SESSION_COOKIE_NAME: &str = "defguard_session"; +use super::common::{SHUTDOWN_TIMEOUT, make_setup_test_client}; async fn assert_setup_step(pool: &sqlx::PgPool, expected: InitialSetupStep) { let step = InitialSetupState::get(pool) diff --git a/crates/defguard_setup/tests/integration/main.rs b/crates/defguard_setup/tests/integration/main.rs new file mode 100644 index 000000000..29d12a976 --- /dev/null +++ b/crates/defguard_setup/tests/integration/main.rs @@ -0,0 +1,8 @@ +mod auto_adoption_wizard; +mod auto_wizard_url_settings; +mod common; +mod initial_setup; +mod migration_wizard; +mod session_info; +mod wizard_init; +mod wizard_state; diff --git a/crates/defguard_setup/tests/migration_wizard.rs b/crates/defguard_setup/tests/integration/migration_wizard.rs similarity index 99% rename from crates/defguard_setup/tests/migration_wizard.rs rename to crates/defguard_setup/tests/integration/migration_wizard.rs index ca3ab0aa8..d7f9d72c4 100644 --- a/crates/defguard_setup/tests/migration_wizard.rs +++ b/crates/defguard_setup/tests/integration/migration_wizard.rs @@ -16,8 +16,7 @@ use reqwest::{ use serde_json::json; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -mod common; -use common::{ +use super::common::{ SHUTDOWN_TIMEOUT, init_settings_with_secret_key, make_migration_test_client, seed_admin_user, }; use tokio::time::timeout; diff --git a/crates/defguard_setup/tests/session_info.rs b/crates/defguard_setup/tests/integration/session_info.rs similarity index 98% rename from crates/defguard_setup/tests/session_info.rs rename to crates/defguard_setup/tests/integration/session_info.rs index b9a7308fb..ce1835559 100644 --- a/crates/defguard_setup/tests/session_info.rs +++ b/crates/defguard_setup/tests/integration/session_info.rs @@ -14,8 +14,9 @@ use reqwest::StatusCode; use serde_json::json; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -mod common; -use common::{init_settings_with_secret_key, make_migration_test_client, make_setup_test_client}; +use super::common::{ + init_settings_with_secret_key, make_migration_test_client, make_setup_test_client, +}; #[sqlx::test] async fn test_session_info_setup_server(_: PgPoolOptions, options: PgConnectOptions) { diff --git a/crates/defguard_setup/tests/wizard_init.rs b/crates/defguard_setup/tests/integration/wizard_init.rs similarity index 100% rename from crates/defguard_setup/tests/wizard_init.rs rename to crates/defguard_setup/tests/integration/wizard_init.rs diff --git a/crates/defguard_setup/tests/wizard_state.rs b/crates/defguard_setup/tests/integration/wizard_state.rs similarity index 86% rename from crates/defguard_setup/tests/wizard_state.rs rename to crates/defguard_setup/tests/integration/wizard_state.rs index 30a806ad2..492675b0b 100644 --- a/crates/defguard_setup/tests/wizard_state.rs +++ b/crates/defguard_setup/tests/integration/wizard_state.rs @@ -3,7 +3,7 @@ use defguard_common::{ db::{ models::{ settings::initialize_current_settings, - setup_auto_adoption::AutoAdoptionWizardStep, + setup_auto_adoption::{AutoAdoptionWizardState, AutoAdoptionWizardStep}, wireguard::{LocationMfaMode, ServiceLocationMode, WireguardNetwork}, wizard::{ActiveWizard, Wizard}, }, @@ -14,8 +14,7 @@ use reqwest::StatusCode; use serde_json::json; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; -mod common; -use common::make_setup_test_client; +use super::common::make_setup_test_client; #[sqlx::test] async fn test_wizard_state_initial(_: PgPoolOptions, options: PgConnectOptions) { @@ -209,11 +208,10 @@ async fn test_wizard_state_auto_adoption(_: PgPoolOptions, options: PgConnectOpt .expect("Failed to parse wizard state"); assert_eq!(state["active_wizard"], "auto_adoption"); - let auto_state = - defguard_common::db::models::setup_auto_adoption::AutoAdoptionWizardState::get(&pool) - .await - .expect("Failed to get auto adoption state") - .unwrap_or_default(); + let auto_state = AutoAdoptionWizardState::get(&pool) + .await + .expect("Failed to get auto adoption state") + .unwrap_or_default(); assert_eq!(auto_state.step, AutoAdoptionWizardStep::UrlSettings); let resp = client @@ -227,11 +225,10 @@ async fn test_wizard_state_auto_adoption(_: PgPoolOptions, options: PgConnectOpt .expect("Failed to set internal URL settings"); assert_eq!(resp.status(), StatusCode::CREATED); - let auto_state = - defguard_common::db::models::setup_auto_adoption::AutoAdoptionWizardState::get(&pool) - .await - .expect("Failed to get auto adoption state") - .expect("Auto adoption state should be set"); + let auto_state = AutoAdoptionWizardState::get(&pool) + .await + .expect("Failed to get auto adoption state") + .expect("Auto adoption state should be set"); assert_eq!(auto_state.step, AutoAdoptionWizardStep::ExternalUrlSettings); let resp = client @@ -245,11 +242,10 @@ async fn test_wizard_state_auto_adoption(_: PgPoolOptions, options: PgConnectOpt .expect("Failed to set external URL settings"); assert_eq!(resp.status(), StatusCode::CREATED); - let auto_state = - defguard_common::db::models::setup_auto_adoption::AutoAdoptionWizardState::get(&pool) - .await - .expect("Failed to get auto adoption state") - .expect("Auto adoption state should be set"); + let auto_state = AutoAdoptionWizardState::get(&pool) + .await + .expect("Failed to get auto adoption state") + .expect("Auto adoption state should be set"); assert_eq!(auto_state.step, AutoAdoptionWizardStep::VpnSettings); let resp = client @@ -266,11 +262,10 @@ async fn test_wizard_state_auto_adoption(_: PgPoolOptions, options: PgConnectOpt .expect("Failed to set VPN settings"); assert_eq!(resp.status(), StatusCode::CREATED); - let auto_state = - defguard_common::db::models::setup_auto_adoption::AutoAdoptionWizardState::get(&pool) - .await - .expect("Failed to get auto adoption state") - .expect("Auto adoption state should be set"); + let auto_state = AutoAdoptionWizardState::get(&pool) + .await + .expect("Failed to get auto adoption state") + .expect("Auto adoption state should be set"); assert_eq!(auto_state.step, AutoAdoptionWizardStep::MfaSettings); let resp = client @@ -281,11 +276,10 @@ async fn test_wizard_state_auto_adoption(_: PgPoolOptions, options: PgConnectOpt .expect("Failed to set MFA settings"); assert_eq!(resp.status(), StatusCode::CREATED); - let auto_state = - defguard_common::db::models::setup_auto_adoption::AutoAdoptionWizardState::get(&pool) - .await - .expect("Failed to get auto adoption state") - .expect("Auto adoption state should be set"); + let auto_state = AutoAdoptionWizardState::get(&pool) + .await + .expect("Failed to get auto adoption state") + .expect("Auto adoption state should be set"); assert_eq!(auto_state.step, AutoAdoptionWizardStep::Summary); let resp = client From 164b1daa3848220a36dc60798cab0892ff4f444e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 17 Apr 2026 11:10:38 +0200 Subject: [PATCH 19/46] explicitly require client auth --- crates/defguard_grpc_tls/src/certs.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/defguard_grpc_tls/src/certs.rs b/crates/defguard_grpc_tls/src/certs.rs index 892c828bb..34c45372a 100644 --- a/crates/defguard_grpc_tls/src/certs.rs +++ b/crates/defguard_grpc_tls/src/certs.rs @@ -157,7 +157,10 @@ pub fn server_tls_config( ) -> Result { let identity = Identity::from_pem(component_cert_pem, component_key_pem); let ca = Certificate::from_pem(ca_cert_pem); - Ok(ServerTlsConfig::new().identity(identity).client_ca_root(ca)) + Ok(ServerTlsConfig::new() + .identity(identity) + .client_ca_root(ca) + .client_auth_optional(false)) } /// Create a rustls client config that enforces the pinned component certificate serial From 710b1bfd5c10328b201bcf43ed9b99483d462012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 17 Apr 2026 11:47:40 +0200 Subject: [PATCH 20/46] update signature --- crates/defguard_grpc_tls/src/certs.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/defguard_grpc_tls/src/certs.rs b/crates/defguard_grpc_tls/src/certs.rs index 34c45372a..7ba2ff543 100644 --- a/crates/defguard_grpc_tls/src/certs.rs +++ b/crates/defguard_grpc_tls/src/certs.rs @@ -151,12 +151,12 @@ fn root_store_from_ca(ca_cert_der: &[u8]) -> Result, + component_key_pem: impl AsRef<[u8]>, + ca_cert_pem: impl AsRef<[u8]>, ) -> Result { - let identity = Identity::from_pem(component_cert_pem, component_key_pem); - let ca = Certificate::from_pem(ca_cert_pem); + let identity = Identity::from_pem(component_cert_pem.as_ref(), component_key_pem.as_ref()); + let ca = Certificate::from_pem(ca_cert_pem.as_ref()); Ok(ServerTlsConfig::new() .identity(identity) .client_ca_root(ca) From 3c76f4f4f5a53bb23907c635f59b1e7c2be7ea94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 17 Apr 2026 12:25:43 +0200 Subject: [PATCH 21/46] avoid leaking certs in logs --- .../defguard_common/src/db/models/gateway.rs | 30 ++++++++++++++++++- crates/defguard_common/src/db/models/proxy.rs | 29 +++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/crates/defguard_common/src/db/models/gateway.rs b/crates/defguard_common/src/db/models/gateway.rs index aee3cb4d7..3e57f5098 100644 --- a/crates/defguard_common/src/db/models/gateway.rs +++ b/crates/defguard_common/src/db/models/gateway.rs @@ -7,7 +7,7 @@ use sqlx::{PgExecutor, query, query_as, query_scalar}; use crate::db::{Id, NoId}; -#[derive(Clone, Debug, Deserialize, Model, Serialize, PartialEq)] +#[derive(Clone, Deserialize, Model, Serialize, PartialEq)] pub struct Gateway { pub id: I, pub location_id: Id, @@ -22,11 +22,39 @@ pub struct Gateway { pub enabled: bool, pub modified_at: NaiveDateTime, pub modified_by: String, + #[serde(skip)] pub core_client_cert_der: Option>, + #[serde(skip)] pub core_client_cert_key_der: Option>, pub core_client_cert_expiry: Option, } +impl fmt::Debug for Gateway { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Gateway") + .field("id", &self.id) + .field("location_id", &self.location_id) + .field("name", &self.name) + .field("address", &self.address) + .field("port", &self.port) + .field("connected_at", &self.connected_at) + .field("disconnected_at", &self.disconnected_at) + .field("certificate_serial", &self.certificate_serial) + .field("certificate_expiry", &self.certificate_expiry) + .field("version", &self.version) + .field("enabled", &self.enabled) + .field("modified_at", &self.modified_at) + .field("modified_by", &self.modified_by) + .field( + "core_client_cert_der", + &self.core_client_cert_der.as_ref().map(|_| ""), + ) + .field("core_client_cert_key_der", &"") + .field("core_client_cert_expiry", &self.core_client_cert_expiry) + .finish() + } +} + impl Gateway { pub fn is_connected(&self) -> bool { if let (Some(connected_at), Some(disconnected_at)) = diff --git a/crates/defguard_common/src/db/models/proxy.rs b/crates/defguard_common/src/db/models/proxy.rs index 4036e63de..dd0244cb2 100644 --- a/crates/defguard_common/src/db/models/proxy.rs +++ b/crates/defguard_common/src/db/models/proxy.rs @@ -11,7 +11,7 @@ use crate::{ types::proxy::ProxyInfo, }; -#[derive(Clone, Debug, Deserialize, Model, Serialize, ToSchema, PartialEq)] +#[derive(Clone, Deserialize, Model, Serialize, ToSchema, PartialEq)] pub struct Proxy { pub id: I, pub name: String, @@ -25,11 +25,38 @@ pub struct Proxy { pub certificate_expiry: Option, pub modified_at: NaiveDateTime, pub modified_by: String, + #[serde(skip)] pub core_client_cert_der: Option>, + #[serde(skip)] pub core_client_cert_key_der: Option>, pub core_client_cert_expiry: Option, } +impl fmt::Debug for Proxy { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Proxy") + .field("id", &self.id) + .field("name", &self.name) + .field("address", &self.address) + .field("port", &self.port) + .field("connected_at", &self.connected_at) + .field("disconnected_at", &self.disconnected_at) + .field("version", &self.version) + .field("enabled", &self.enabled) + .field("certificate_serial", &self.certificate_serial) + .field("certificate_expiry", &self.certificate_expiry) + .field("modified_at", &self.modified_at) + .field("modified_by", &self.modified_by) + .field( + "core_client_cert_der", + &self.core_client_cert_der.as_ref().map(|_| ""), + ) + .field("core_client_cert_key_der", &"") + .field("core_client_cert_expiry", &self.core_client_cert_expiry) + .finish() + } +} + impl fmt::Display for Proxy { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.name) From 0641a8978e2a6c879574db4ad288e57ad9961ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 17 Apr 2026 12:26:32 +0200 Subject: [PATCH 22/46] adjust down migration --- .../20260414120000_[2.0.0]_core_grpc_cert.down.sql | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/migrations/20260414120000_[2.0.0]_core_grpc_cert.down.sql b/migrations/20260414120000_[2.0.0]_core_grpc_cert.down.sql index a0d1ea4d2..ff2b6383a 100644 --- a/migrations/20260414120000_[2.0.0]_core_grpc_cert.down.sql +++ b/migrations/20260414120000_[2.0.0]_core_grpc_cert.down.sql @@ -2,11 +2,11 @@ ALTER TABLE gateway RENAME COLUMN certificate_serial TO certificate; ALTER TABLE proxy RENAME COLUMN certificate_serial TO certificate; ALTER TABLE gateway - DROP COLUMN IF EXISTS core_client_cert_der, - DROP COLUMN IF EXISTS core_client_cert_key_der, - DROP COLUMN IF EXISTS core_client_cert_expiry; + DROP COLUMN core_client_cert_der, + DROP COLUMN core_client_cert_key_der, + DROP COLUMN core_client_cert_expiry; ALTER TABLE proxy - DROP COLUMN IF EXISTS core_client_cert_der, - DROP COLUMN IF EXISTS core_client_cert_key_der, - DROP COLUMN IF EXISTS core_client_cert_expiry; + DROP COLUMN core_client_cert_der, + DROP COLUMN core_client_cert_key_der, + DROP COLUMN core_client_cert_expiry; From 40eb0eb46ed303a21e36b055725fbbf877ff262f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 17 Apr 2026 13:56:10 +0200 Subject: [PATCH 23/46] used shared helper for setting up Edge connections --- Cargo.lock | 3 + crates/defguard_core/Cargo.toml | 2 + .../src/handlers/component_setup.rs | 53 +++++----- crates/defguard_grpc_tls/Cargo.toml | 1 + crates/defguard_grpc_tls/src/certs.rs | 61 +++++++++++- crates/defguard_proxy_manager/src/handler.rs | 97 +++++-------------- 6 files changed, 116 insertions(+), 101 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6cf3d9975..262434982 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1380,6 +1380,7 @@ dependencies = [ "claims", "defguard_certs", "defguard_common", + "defguard_grpc_tls", "defguard_mail", "defguard_proto", "defguard_static_ip", @@ -1387,6 +1388,7 @@ dependencies = [ "defguard_web_ui", "futures", "humantime", + "hyper-rustls", "hyper-util", "ipnetwork", "jsonwebkey", @@ -1523,6 +1525,7 @@ version = "0.0.0" dependencies = [ "defguard_common", "http", + "hyper-rustls", "rustls", "thiserror 2.0.18", "tokio", diff --git a/crates/defguard_core/Cargo.toml b/crates/defguard_core/Cargo.toml index b089f91a4..f1df1e3d7 100644 --- a/crates/defguard_core/Cargo.toml +++ b/crates/defguard_core/Cargo.toml @@ -16,6 +16,7 @@ defguard_web_ui = { workspace = true } defguard_version = { workspace = true } model_derive = { workspace = true } defguard_certs = { workspace = true } +defguard_grpc_tls = { workspace = true } defguard_static_ip = { workspace = true } # external dependencies @@ -30,6 +31,7 @@ bytes = { workspace = true } chrono = { workspace = true } futures = { workspace = true } humantime = { workspace = true } +hyper-rustls = { workspace = true } # match version used by sqlx ipnetwork = { workspace = true } jsonwebkey = { workspace = true } diff --git a/crates/defguard_core/src/handlers/component_setup.rs b/crates/defguard_core/src/handlers/component_setup.rs index 41288d1bb..e3f092ac3 100644 --- a/crates/defguard_core/src/handlers/component_setup.rs +++ b/crates/defguard_core/src/handlers/component_setup.rs @@ -1,5 +1,5 @@ use std::{ - collections::VecDeque, + collections::{HashMap, VecDeque}, convert::Infallible, sync::{Arc, Mutex, PoisonError}, time::Duration, @@ -29,6 +29,7 @@ use defguard_common::{ types::proxy::ProxyControlMessage, utils::strip_scheme, }; +use defguard_grpc_tls::certs::proxy_mtls_channel; use defguard_proto::{ common::{CertBundle, CertificateInfo}, gateway::gateway_setup_client::GatewaySetupClient, @@ -43,7 +44,10 @@ use reqwest::Url; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use tokio::{ - sync::mpsc::{Sender, UnboundedReceiver, UnboundedSender, unbounded_channel}, + sync::{ + mpsc::{Sender, UnboundedReceiver, UnboundedSender, unbounded_channel}, + oneshot, watch, + }, time::{Instant, sleep_until, timeout}, }; use tokio_stream::StreamExt; @@ -1175,8 +1179,7 @@ fn public_proxy_hostname() -> Result { /// collected during the ACME run (sent by the proxy via an [`AcmeLogs`] event). async fn call_proxy_trigger_acme( pool: &PgPool, - proxy_host: &str, - proxy_port: u16, + proxy: &Proxy, domain: String, account_credentials_json: String, progress_tx: UnboundedSender, @@ -1191,32 +1194,29 @@ async fn call_proxy_trigger_acme( ) })?; - let cert_pem = der_to_pem(&ca_cert_der, defguard_certs::PemLabel::Certificate) - .map_err(|e| (format!("Failed to convert CA cert to PEM: {e}"), Vec::new()))?; - - let endpoint_str = format!("https://{proxy_host}:{proxy_port}"); - let endpoint = Endpoint::from_shared(endpoint_str) - .map_err(|e| (format!("Failed to build Edge endpoint: {e}"), Vec::new()))? - .http2_keep_alive_interval(Duration::from_secs(5)) - .tcp_keepalive(Some(Duration::from_secs(5))) - .keep_alive_while_idle(true); - - let tls = ClientTlsConfig::new().ca_certificate(Certificate::from_pem(cert_pem)); - let endpoint = endpoint.tls_config(tls).map_err(|e| { + let cert_serial = proxy.certificate_serial.as_deref().ok_or_else(|| { ( - format!("Failed to configure TLS for Edge endpoint: {e}"), + "Edge certificate serial not provisioned".to_string(), Vec::new(), ) })?; + // Seed a one-shot serial map so the rustls verifier validates the server cert serial. + let (_, certs_rx) = watch::channel(Arc::new(HashMap::from([( + proxy.id, + cert_serial.to_string(), + )]))); + + let channel = proxy_mtls_channel(proxy, &ca_cert_der, certs_rx) + .map_err(|e| (format!("Failed to build mTLS channel: {e}"), Vec::new()))?; + let version = Version::parse(VERSION) .map_err(|e| (format!("Failed to parse core version: {e}"), Vec::new()))?; let version_interceptor = ClientVersionInterceptor::new(version); - let mut client = - ProxyClient::with_interceptor(endpoint.connect_lazy(), move |req: Request<()>| { - version_interceptor.clone().call(req) - }); + let mut client = ProxyClient::with_interceptor(channel, move |req: Request<()>| { + version_interceptor.clone().call(req) + }); let mut stream = client .trigger_acme(AcmeChallenge { @@ -1300,7 +1300,7 @@ pub async fn stream_proxy_acme( let account_credentials_json = certs.acme_account_credentials.clone().unwrap_or_default(); - let proxies = match Proxy::list(&pool).await { + let proxies = match Proxy::all_enabled(&pool).await { Ok(list) => list, Err(e) => { yield Ok(acme_error_event( @@ -1323,8 +1323,8 @@ pub async fn stream_proxy_acme( return; }; - let proxy_host = proxy.address.clone(); - let proxy_port = proxy.port as u16; + let proxy_host = &proxy.address; + let proxy_port = proxy.port; info!( "Triggering ACME HTTP-01 via Edge gRPC TriggerAcme for domain: {domain} \ Edge={proxy_host}:{proxy_port}" @@ -1333,7 +1333,7 @@ pub async fn stream_proxy_acme( let (progress_tx, mut progress_rx) = unbounded_channel::(); let (result_tx, result_rx) = - tokio::sync::oneshot::channel::)>>(); + oneshot::channel::)>>(); let pool_clone = pool.clone(); let domain_clone = domain.clone(); @@ -1341,8 +1341,7 @@ pub async fn stream_proxy_acme( tokio::spawn(async move { let result = call_proxy_trigger_acme( &pool_clone, - &proxy_host, - proxy_port, + &proxy, domain_clone, acct_creds_clone, progress_tx, diff --git a/crates/defguard_grpc_tls/Cargo.toml b/crates/defguard_grpc_tls/Cargo.toml index e6dcf87e2..d07f47233 100644 --- a/crates/defguard_grpc_tls/Cargo.toml +++ b/crates/defguard_grpc_tls/Cargo.toml @@ -10,6 +10,7 @@ rust-version.workspace = true [dependencies] defguard_common.workspace = true http = "1.1" +hyper-rustls.workspace = true rustls = { version = "0.23", features = ["ring"] } thiserror.workspace = true tokio.workspace = true diff --git a/crates/defguard_grpc_tls/src/certs.rs b/crates/defguard_grpc_tls/src/certs.rs index 7ba2ff543..04b319811 100644 --- a/crates/defguard_grpc_tls/src/certs.rs +++ b/crates/defguard_grpc_tls/src/certs.rs @@ -8,9 +8,10 @@ //! - A lightweight in-memory cache (refreshed periodically) avoids database access //! during the handshake and keeps verification synchronous. -use std::{collections::HashMap, sync::Arc}; +use std::{collections::HashMap, sync::Arc, time::Duration}; -use defguard_common::db::Id; +use defguard_common::db::{Id, models::proxy::Proxy}; +use hyper_rustls::HttpsConnectorBuilder; use rustls::{ CertificateError, DistinguishedName, Error as RustlsError, RootCertStore, SignatureScheme, client::{ @@ -22,10 +23,14 @@ use rustls::{ }; use thiserror::Error; use tokio::sync::watch; -use tonic::transport::{Certificate, Identity, ServerTlsConfig}; +use tonic::transport::{Certificate, Channel, Endpoint, Identity, ServerTlsConfig}; use tracing::error; use x509_parser::parse_x509_certificate; +use crate::connector::HttpsSchemeConnector; + +const TEN_SECS: Duration = Duration::from_secs(10); + /// Errors that can occur while building a TLS config with a pinned verifier. #[derive(Debug, Error)] pub enum CertConfigError { @@ -208,3 +213,53 @@ pub fn client_config( ))); Ok(config) } + +/// Build an mTLS [`Channel`] to a proxy using its stored per-component client certificate. +/// +/// * `proxy` — the full `Proxy` row from the database; `core_client_cert_der`, +/// `core_client_cert_key_der`, and `certificate_serial` must all be `Some`. +/// * `ca_cert_der` — the core CA certificate in DER form, used as the only trusted root. +/// * `certs_rx` — watch channel carrying the current `{ proxy_id → cert_serial }` map. +/// Pass a long-lived receiver for persistent connections (serial revocation is picked up +/// dynamically) or a one-shot channel seeded with the proxy's current serial for +/// short-lived calls. +/// +/// The returned channel uses an `http://` endpoint scheme; TLS is applied by the +/// internal [`HttpsSchemeConnector`](crate::connector::HttpsSchemeConnector). +pub fn proxy_mtls_channel( + proxy: &Proxy, + ca_cert_der: &[u8], + certs_rx: watch::Receiver>>, +) -> Result { + let cert_der = proxy.core_client_cert_der.as_deref().ok_or_else(|| { + CertConfigError::TlsConfig(format!( + "core client certificate not provisioned for proxy id={}", + proxy.id + )) + })?; + let key_der = proxy.core_client_cert_key_der.as_deref().ok_or_else(|| { + CertConfigError::TlsConfig(format!( + "core client certificate key not provisioned for proxy id={}", + proxy.id + )) + })?; + + let tls_config = client_config(ca_cert_der, certs_rx, proxy.id, cert_der, key_der)?; + + let connector = HttpsConnectorBuilder::new() + .with_tls_config(tls_config) + .https_only() + .enable_http2() + .build(); + let connector = HttpsSchemeConnector::new(connector); + + // Use http:// scheme — the HttpsSchemeConnector rewrites it to https:// internally. + let endpoint_str = format!("http://{}:{}", proxy.address, proxy.port); + let endpoint = Endpoint::from_shared(endpoint_str) + .map_err(|e| CertConfigError::TlsConfig(format!("invalid proxy endpoint URL: {e}")))? + .http2_keep_alive_interval(TEN_SECS) + .tcp_keepalive(Some(TEN_SECS)) + .keep_alive_while_idle(true); + + Ok(endpoint.connect_with_connector_lazy(connector)) +} diff --git a/crates/defguard_proxy_manager/src/handler.rs b/crates/defguard_proxy_manager/src/handler.rs index c995ce63a..76eb1c5de 100644 --- a/crates/defguard_proxy_manager/src/handler.rs +++ b/crates/defguard_proxy_manager/src/handler.rs @@ -36,7 +36,7 @@ use defguard_core::{ }, version::{IncompatibleComponents, IncompatibleProxyData, is_proxy_version_supported}, }; -use defguard_grpc_tls::{certs as tls_certs, connector::HttpsSchemeConnector}; +use defguard_grpc_tls::certs::proxy_mtls_channel; use defguard_proto::{ client_types::AuthFlowType as ProtoAuthFlowType, proxy::{ @@ -47,7 +47,6 @@ use defguard_proto::{ use defguard_version::{ ComponentInfo, DefguardComponent, client::ClientVersionInterceptor, get_tracing_variables, }; -use hyper_rustls::HttpsConnectorBuilder; use openidconnect::{AuthorizationCode, Nonce, Scope, core::CoreAuthenticationFlow}; use reqwest::Url; use semver::Version; @@ -64,9 +63,7 @@ use tokio::{ }; use tokio_stream::wrappers::UnboundedReceiverStream; use tonic::{ - Code, Request, Streaming, - service::interceptor::InterceptedService, - transport::{Channel, Endpoint}, + Code, Request, Streaming, service::interceptor::InterceptedService, transport::Channel, }; #[cfg(test)] @@ -75,6 +72,8 @@ use crate::{ HandlerTxMap, ProxyError, ProxyTxSet, TEN_SECS, servers::{EnrollmentServer, PasswordResetServer}, }; +#[cfg(test)] +use tonic::transport::Endpoint; const VERSION_ZERO: Version = Version::new(0, 0, 0); @@ -222,25 +221,8 @@ impl ProxyHandler { TEN_SECS } - fn endpoint(&self) -> Result { - let mut url = self.url.clone(); - - // Using HTTP here because the connector upgrades to TLS internally. - url.set_scheme("http").map_err(|()| { - ProxyError::UrlError(format!("Failed to set HTTP scheme on URL {url}")) - })?; - let endpoint = Endpoint::from_shared(url.to_string())?; - let endpoint = endpoint - .http2_keep_alive_interval(TEN_SECS) - .tcp_keepalive(Some(TEN_SECS)) - .keep_alive_while_idle(true); - - Ok(endpoint) - } - - async fn connect_tls_channel( + async fn connect_channel_mtls( &self, - endpoint: &Endpoint, certs_rx: watch::Receiver>>, ) -> Result { let certs = Certificates::get(&self.pool) @@ -267,43 +249,17 @@ impl ProxyHandler { self.proxy_id )) })?; - let core_client_cert_der = proxy.core_client_cert_der.ok_or_else(|| { - ProxyError::MissingConfiguration(format!( - "Core client certificate not provisioned for proxy id={}", - self.proxy_id - )) - })?; - let core_client_cert_key_der = proxy.core_client_cert_key_der.ok_or_else(|| { - ProxyError::MissingConfiguration(format!( - "Core client certificate key not provisioned for proxy id={}", - self.proxy_id - )) - })?; - let tls_config = tls_certs::client_config( - &ca_cert_der, - certs_rx, - self.proxy_id, - &core_client_cert_der, - &core_client_cert_key_der, - ) - .map_err(|err| ProxyError::TlsConfigError(err.to_string()))?; - let connector = HttpsConnectorBuilder::new() - .with_tls_config(tls_config) - .https_only() - .enable_http2() - .build(); - let connector = HttpsSchemeConnector::new(connector); - Ok(endpoint.connect_with_connector_lazy(connector)) + proxy_mtls_channel(&proxy, &ca_cert_der, certs_rx) + .map_err(|e| ProxyError::TlsConfigError(e.to_string())) } #[cfg(not(test))] async fn connect_channel( &self, - endpoint: &Endpoint, certs_rx: watch::Receiver>>, ) -> Result { - self.connect_tls_channel(endpoint, certs_rx).await + self.connect_channel_mtls(certs_rx).await } /// Establishes and maintains a gRPC bidirectional stream to the proxy. @@ -319,14 +275,12 @@ impl ProxyHandler { ) -> Result<(), ProxyError> { let parsed_version = Version::parse(VERSION)?; loop { - let endpoint = self.endpoint()?; - - let channel = match self.connect_channel(&endpoint, certs_rx.clone()).await { + let channel = match self.connect_channel(certs_rx.clone()).await { Ok(ch) => ch, Err(err) => { error!( "Failed to create proxy channel for {}: {err}, retrying in {:?}", - endpoint.uri(), + self.url, self.retry_delay() ); self.mark_disconnected().await?; @@ -342,7 +296,7 @@ impl ProxyHandler { } }; - debug!("Connecting to proxy at {}", endpoint.uri()); + debug!("Connecting to proxy at {}", self.url); let interceptor = ClientVersionInterceptor::new(parsed_version.clone()); let mut client = ProxyClient::with_interceptor(channel, interceptor); self.client = Some(client.clone()); @@ -361,7 +315,7 @@ impl ProxyHandler { error!( "Failed to connect to proxy @ {}, version check failed, retrying in \ {:?}: {err}", - endpoint.uri(), + self.url, self.retry_delay() ); // TODO push event @@ -369,7 +323,7 @@ impl ProxyHandler { err => { error!( "Failed to connect to proxy @ {}, retrying in {:?}: {err}", - endpoint.uri(), + self.url, self.retry_delay() ); } @@ -423,7 +377,7 @@ impl ProxyHandler { } IncompatibleComponents::remove_proxy(&incompatible_components); - info!("Connected to proxy at {}", endpoint.uri()); + info!("Connected to proxy at {}", self.url); let mut resp_stream = response.into_inner(); // Send initial info with private cookies key. @@ -479,17 +433,17 @@ impl ProxyHandler { res = &mut *shutdown_signal.lock().await => { match res { Err(err) => { - error!("An error occurred when trying to wait for a shutdown signal for Proxy: {err}. Reconnecting to: {}", endpoint.uri()); + error!("An error occurred when trying to wait for a shutdown signal for Proxy: {err}. Reconnecting to: {}", self.url); } Ok(purge) => { - info!("Shutdown signal received, purge: {purge}, stopping proxy connection to {}", endpoint.uri()); + info!("Shutdown signal received, purge: {purge}, stopping proxy connection to {}", self.url); if purge { if let Some(client) = self.client.as_mut() { - debug!("Sending purge request to proxy {}", endpoint.uri()); + debug!("Sending purge request to proxy {}", self.url); if let Err(err) = client.purge(Request::new(())).await { - error!("Error sending purge request to proxy {}: {err}", endpoint.uri()); + error!("Error sending purge request to proxy {}: {err}", self.url); } else { - info!("Sent purge request to proxy {}", endpoint.uri()); + info!("Sent purge request to proxy {}", self.url); } } } @@ -1092,10 +1046,12 @@ impl ProxyHandler { async fn connect_channel( &self, - endpoint: &Endpoint, certs_rx: watch::Receiver>>, ) -> Result { if let Some(socket_path) = self.test_transport.socket_path().cloned() { + // Build a minimal endpoint for the Unix socket connector. + // The scheme and host are irrelevant — the connector ignores the URI. + let endpoint = Endpoint::from_shared(self.url.to_string())?; return Ok(endpoint.connect_with_connector_lazy(tower::service_fn( move |_: tonic::transport::Uri| { let socket_path = socket_path.clone(); @@ -1108,7 +1064,7 @@ impl ProxyHandler { ))); } - self.connect_tls_channel(endpoint, certs_rx).await + self.connect_channel_mtls(certs_rx).await } /// Single-iteration version of `run()` for use in tests. @@ -1123,12 +1079,11 @@ impl ProxyHandler { certs_rx: watch::Receiver>>, ) -> Result<(), ProxyError> { let parsed_version = Version::parse(VERSION)?; - let endpoint = self.endpoint()?; - let channel = self.connect_channel(&endpoint, certs_rx).await?; + let channel = self.connect_channel(certs_rx).await?; debug!( "Connecting to proxy at {} (test, single iteration)", - endpoint.uri() + self.url ); let interceptor = ClientVersionInterceptor::new(parsed_version); let mut client = ProxyClient::with_interceptor(channel, interceptor); @@ -1163,7 +1118,7 @@ impl ProxyHandler { } IncompatibleComponents::remove_proxy(&incompatible_components); - info!("Connected to proxy at {} (test)", endpoint.uri()); + info!("Connected to proxy at {} (test)", self.url); let mut resp_stream = response.into_inner(); let initial_info = InitialInfo { From 5d24cb5ea46e8a02c6c94a856212a7d9f641fffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 17 Apr 2026 14:11:23 +0200 Subject: [PATCH 24/46] limit setup token validity --- crates/defguard_core/src/handlers/component_setup.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/defguard_core/src/handlers/component_setup.rs b/crates/defguard_core/src/handlers/component_setup.rs index e3f092ac3..6c399c9ad 100644 --- a/crates/defguard_core/src/handlers/component_setup.rs +++ b/crates/defguard_core/src/handlers/component_setup.rs @@ -67,6 +67,11 @@ use crate::{ const TOKEN_CLIENT_ID: &str = "Defguard Core"; const CONNECTION_TIMEOUT: Duration = Duration::from_secs(10); +/// Maximum lifetime of a one-time setup session token. +/// The setup handshake must complete within this window; tokens that outlive +/// it are useless and limiting the expiry reduces the damage window if the +/// token is captured from the plaintext setup channel. +const SETUP_TOKEN_EXPIRY_SECS: u64 = 300; /// Guard that aborts a tokio task when dropped struct TaskGuard(tokio::task::JoinHandle<()>); @@ -358,7 +363,7 @@ pub async fn setup_proxy_tls_stream( defguard_common::auth::claims::ClaimsType::Gateway, url.to_string(), TOKEN_CLIENT_ID.to_string(), - u32::MAX.into(), + SETUP_TOKEN_EXPIRY_SECS, ) .to_jwt() { @@ -802,7 +807,7 @@ pub async fn setup_gateway_tls_stream( defguard_common::auth::claims::ClaimsType::Gateway, url.to_string(), TOKEN_CLIENT_ID.to_string(), - u32::MAX.into(), + SETUP_TOKEN_EXPIRY_SECS, ) .to_jwt() { From 3863968d84aafd54e3b523279132c69bb4e623e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 17 Apr 2026 21:19:20 +0200 Subject: [PATCH 25/46] verify CSR hostname during setup --- crates/defguard_certs/src/lib.rs | 85 +++++++++++++++++++ .../src/handlers/component_setup.rs | 10 +++ 2 files changed, 95 insertions(+) diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs index 59c8aab7a..58a582c30 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -26,6 +26,8 @@ pub enum CertificateError { ParsingError(String), #[error(transparent)] IoError(#[from] std::io::Error), + #[error("CSR hostname mismatch: {0}")] + HostnameMismatch(String), } pub struct CertificateAuthority<'a> { @@ -296,6 +298,41 @@ impl Csr<'_> { Ok(params) } + /// Verify that the CSR's SAN list contains exactly `expected_hostname` and + /// nothing else. The hostname may be a DNS name or an IP address literal. + /// + /// This is used during component setup to ensure the component has not + /// substituted a different hostname in the CSR it returns to Core. + pub fn verify_hostname(&self, expected_hostname: &str) -> Result<(), CertificateError> { + let params = self.params()?; + let sans = ¶ms.params.subject_alt_names; + + if sans.is_empty() { + return Err(CertificateError::HostnameMismatch(format!( + "CSR contains no SANs; expected {expected_hostname:?}" + ))); + } + + let expected_ip: Option = expected_hostname.parse().ok(); + + for san in sans { + let matches = match san { + rcgen::SanType::IpAddress(ip) => expected_ip.is_some_and(|e| &e == ip), + rcgen::SanType::DnsName(name) => { + expected_ip.is_none() && name.as_str() == expected_hostname + } + _ => false, + }; + if !matches { + return Err(CertificateError::HostnameMismatch(format!( + "CSR SAN does not match expected hostname {expected_hostname:?}" + ))); + } + } + + Ok(()) + } + #[must_use] pub fn to_der(&self) -> &[u8] { self.csr.as_ref() @@ -522,4 +559,52 @@ mod tests { let parsed = parse_pem_certificate(&pem).unwrap(); assert_eq!(parsed, ca.cert_der); } + + #[test] + fn test_csr_verify_hostname_dns_ok() { + let key = generate_key_pair().unwrap(); + let csr = Csr::new(&key, &["proxy.example.com".to_string()], vec![]).unwrap(); + assert!( + csr.verify_hostname("proxy.example.com").is_ok(), + "matching DNS SAN should pass" + ); + } + + #[test] + fn test_csr_verify_hostname_ip_ok() { + let key = generate_key_pair().unwrap(); + let csr = Csr::new(&key, &["10.0.0.1".to_string()], vec![]).unwrap(); + assert!( + csr.verify_hostname("10.0.0.1").is_ok(), + "matching IP SAN should pass" + ); + } + + #[test] + fn test_csr_verify_hostname_mismatch() { + let key = generate_key_pair().unwrap(); + let csr = Csr::new(&key, &["evil.attacker.com".to_string()], vec![]).unwrap(); + assert!( + csr.verify_hostname("proxy.example.com").is_err(), + "mismatched DNS SAN should fail" + ); + } + + #[test] + fn test_csr_verify_hostname_extra_san_rejected() { + let key = generate_key_pair().unwrap(); + let csr = Csr::new( + &key, + &[ + "proxy.example.com".to_string(), + "evil.extra.com".to_string(), + ], + vec![], + ) + .unwrap(); + assert!( + csr.verify_hostname("proxy.example.com").is_err(), + "CSR with extra SANs beyond the expected hostname should fail" + ); + } } diff --git a/crates/defguard_core/src/handlers/component_setup.rs b/crates/defguard_core/src/handlers/component_setup.rs index 6c399c9ad..ce4cc5419 100644 --- a/crates/defguard_core/src/handlers/component_setup.rs +++ b/crates/defguard_core/src/handlers/component_setup.rs @@ -525,6 +525,11 @@ pub async fn setup_proxy_tls_stream( } }; + if let Err(e) = csr.verify_hostname(hostname) { + yield Ok(flow.error(&format!("CSR hostname validation failed: {e}"))); + return; + } + debug!("Received certificate signing request from Edge for hostname: {hostname}"); // Step 5: Sign certificate @@ -983,6 +988,11 @@ pub async fn setup_gateway_tls_stream( } }; + if let Err(e) = csr.verify_hostname(hostname) { + yield Ok(flow.error(&format!("CSR hostname validation failed: {e}"))); + return; + } + debug!("Received certificate signing request from Gateway for hostname: {hostname}"); // Step 5: Sign certificate From 1819db7a6f9a4841a7ac2fd128da2f2bb924ee60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 17 Apr 2026 21:44:03 +0200 Subject: [PATCH 26/46] remove LDAP password from support dump --- crates/defguard_core/src/support.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/defguard_core/src/support.rs b/crates/defguard_core/src/support.rs index 806ed6e9c..3b26f3370 100644 --- a/crates/defguard_core/src/support.rs +++ b/crates/defguard_core/src/support.rs @@ -30,6 +30,7 @@ pub(crate) async fn dump_config(conn: &mut PgConnection) -> Result { settings.smtp_password = None; + settings.ldap_bind_password = None; json!(settings) } Ok(None) => json!({"error": "Settings not found"}), From 9ce3fa3014aba4d1c308c76081f2b879e1356a43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 17 Apr 2026 22:10:00 +0200 Subject: [PATCH 27/46] cleanup --- crates/defguard_common/src/db/models/user.rs | 2 +- .../src/enrollment_management.rs | 2 +- .../src/enterprise/ldap/tests.rs | 10 +++---- .../src/enterprise/ldap/utils.rs | 2 +- .../tests/integration/api/acl/rules.rs | 2 +- .../tests/integration/api/auth.rs | 2 +- .../tests/integration/api/enrollment.rs | 2 +- .../tests/integration/api/user.rs | 2 +- crates/defguard_grpc_tls/src/certs.rs | 8 ++--- crates/defguard_proxy_manager/src/handler.rs | 2 +- .../src/tests/common/mod.rs | 4 +-- .../src/tests/proxy_manager/handler/acme.rs | 6 ++-- .../tests/proxy_manager/handler/enrollment.rs | 10 +++---- .../src/tests/proxy_manager/handler/mfa.rs | 14 ++++----- .../src/tests/proxy_manager/handler/oidc.rs | 4 +-- .../tests/proxy_manager/handler/polling.rs | 2 +- .../tests/proxy_manager/handler/support.rs | 8 ++--- .../src/tests/proxy_manager/manager.rs | 30 +++++++++---------- .../tests/integration/auto_adoption_wizard.rs | 2 +- .../tests/integration/common.rs | 2 +- 20 files changed, 58 insertions(+), 58 deletions(-) diff --git a/crates/defguard_common/src/db/models/user.rs b/crates/defguard_common/src/db/models/user.rs index 2d8392552..4726e93b9 100644 --- a/crates/defguard_common/src/db/models/user.rs +++ b/crates/defguard_common/src/db/models/user.rs @@ -1708,7 +1708,7 @@ mod test { settings.ldap_group_search_base = Some("ou=groups,dc=example,dc=com".into()); settings.ldap_remote_enrollment_enabled = true; update_current_settings(&pool, settings).await.unwrap(); - // user fields unchanged from the previous case — only the setting changed + // user fields unchanged from the previous case - only the setting changed assert!( !user.is_enrolled(), "LDAP user should not be enrolled when remote enrollment is enabled but not completed" diff --git a/crates/defguard_core/src/enrollment_management.rs b/crates/defguard_core/src/enrollment_management.rs index 04758b064..014544328 100644 --- a/crates/defguard_core/src/enrollment_management.rs +++ b/crates/defguard_core/src/enrollment_management.rs @@ -186,7 +186,7 @@ pub async fn clear_unused_enrollment_tokens<'e, E: PgExecutor<'e>>( /// Sends an enrollment invitation to a newly-created LDAP user when both /// `ldap_remote_enrollment_enabled` and `ldap_remote_enrollment_send_invite` settings are enabled. /// -/// Errors are logged and swallowed — this must not disrupt the caller's flow. +/// Errors are logged and swallowed - this must not disrupt the caller's flow. pub async fn try_send_ldap_enrollment_invite(user: &mut User, conn: &mut PgConnection) { let settings = Settings::get_current_settings(); if !settings.ldap_remote_enrollment_enabled || !settings.ldap_remote_enrollment_send_invite { diff --git a/crates/defguard_core/src/enterprise/ldap/tests.rs b/crates/defguard_core/src/enterprise/ldap/tests.rs index af0a08115..0a6d64237 100644 --- a/crates/defguard_core/src/enterprise/ldap/tests.rs +++ b/crates/defguard_core/src/enterprise/ldap/tests.rs @@ -3446,7 +3446,7 @@ async fn test_sync_does_not_send_invite_when_flags_disabled( let pool = setup_pool(options).await; let _ = initialize_current_settings(&pool).await; - // Create an admin so find_admins() would have something to return — we want to prove + // Create an admin so find_admins() would have something to return - we want to prove // the early-return on the flag guard, not the no-admin guard. make_test_admin(&pool, "sync_admin_nodisabled").await; @@ -3522,7 +3522,7 @@ async fn test_sync_invite_skipped_when_send_invite_flag_disabled( /// syncing a new LDAP user must create an enrollment token and set `enrollment_pending = true`. /// /// SMTP is configured in settings but no real SMTP server is reachable, so `new_account_mail` -/// will fail — but the token and flag are persisted before the mail attempt, so the DB side +/// will fail - but the token and flag are persisted before the mail attempt, so the DB side /// effects are still observable. #[sqlx::test] async fn test_sync_sends_invite_when_flags_enabled(_: PgPoolOptions, options: PgConnectOptions) { @@ -3572,7 +3572,7 @@ async fn test_sync_sends_invite_when_flags_enabled(_: PgPoolOptions, options: Pg "Token should belong to the synced user" ); - // Second sync: user already exists in Defguard — must NOT create a second token. + // Second sync: user already exists in Defguard - must NOT create a second token. ldap_conn.sync(&pool, false).await.unwrap(); let tokens = Token::fetch_all(&pool).await.unwrap(); @@ -3584,7 +3584,7 @@ async fn test_sync_sends_invite_when_flags_enabled(_: PgPoolOptions, options: Pg } /// When both invite flags are on but there are no active admins in Defguard, the sync must -/// succeed and the user must be saved — the invite is silently skipped with a logged error. +/// succeed and the user must be saved - the invite is silently skipped with a logged error. #[sqlx::test] async fn test_sync_invite_skipped_when_no_admin_exists( _: PgPoolOptions, @@ -3687,7 +3687,7 @@ async fn test_ldap_login_sends_invite_when_flags_enabled( "Token should belong to the logged-in user" ); - // Second login: user now exists in Defguard — must NOT create a second token. + // Second login: user now exists in Defguard - must NOT create a second token. let result = login_through_ldap_with_connection(&pool, &mut ldap_conn, "login_invite_user", PASSWORD) .await; diff --git a/crates/defguard_core/src/enterprise/ldap/utils.rs b/crates/defguard_core/src/enterprise/ldap/utils.rs index 9d38d468f..c16e10f1b 100644 --- a/crates/defguard_core/src/enterprise/ldap/utils.rs +++ b/crates/defguard_core/src/enterprise/ldap/utils.rs @@ -96,7 +96,7 @@ pub(crate) async fn login_through_ldap_with_connection( // Attempt to send enrollment invite after the original DB transaction is committed, // so that the user row is visible to the new transaction inside try_send_ldap_enrollment_invite. - // Only send for newly-created users — returning users must not receive a second invite. + // Only send for newly-created users - returning users must not receive a second invite. if is_new_user { let mut transaction = pool.begin().await?; try_send_ldap_enrollment_invite(&mut user, &mut transaction).await; diff --git a/crates/defguard_core/tests/integration/api/acl/rules.rs b/crates/defguard_core/tests/integration/api/acl/rules.rs index 75257d9a3..7667160cb 100644 --- a/crates/defguard_core/tests/integration/api/acl/rules.rs +++ b/crates/defguard_core/tests/integration/api/acl/rules.rs @@ -572,7 +572,7 @@ async fn test_empty_strings(_: PgPoolOptions, options: PgConnectOptions) { let (mut client, _) = make_test_client(pool).await; authenticate_admin(&mut client).await; - // rule — empty address and port strings are parse-safe; use any_* flags so validation passes + // rule - empty address and port strings are parse-safe; use any_* flags so validation passes let mut rule = make_rule(); rule.addresses = String::new(); rule.ports = String::new(); diff --git a/crates/defguard_core/tests/integration/api/auth.rs b/crates/defguard_core/tests/integration/api/auth.rs index 2feaf2816..af2e8f8b9 100644 --- a/crates/defguard_core/tests/integration/api/auth.rs +++ b/crates/defguard_core/tests/integration/api/auth.rs @@ -1166,7 +1166,7 @@ async fn test_totp_enable_persists(_: PgPoolOptions, options: PgConnectOptions) client.login_user("hpotter", "pass123").await; - // Init TOTP — the secret is returned directly, no SMTP required. + // Init TOTP - the secret is returned directly, no SMTP required. let response = client.post("/api/v1/auth/totp/init").send().await; assert_eq!(response.status(), StatusCode::OK); let auth_totp: AuthTotp = response.json().await; diff --git a/crates/defguard_core/tests/integration/api/enrollment.rs b/crates/defguard_core/tests/integration/api/enrollment.rs index a1be4c3ba..96678e689 100644 --- a/crates/defguard_core/tests/integration/api/enrollment.rs +++ b/crates/defguard_core/tests/integration/api/enrollment.rs @@ -407,7 +407,7 @@ async fn test_ldap_user_enrolled_via_api_when_remote_enrollment_disabled( user.ldap_remote_enrollment_completed = false; user.save(&pool).await.unwrap(); - // ldap_remote_enrollment_enabled is false by default — no settings change needed. + // ldap_remote_enrollment_enabled is false by default - no settings change needed. let details = fetch_user_details(&client, &new_user.username).await; assert!( details.user.enrolled, diff --git a/crates/defguard_core/tests/integration/api/user.rs b/crates/defguard_core/tests/integration/api/user.rs index d83bf1790..881a69287 100644 --- a/crates/defguard_core/tests/integration/api/user.rs +++ b/crates/defguard_core/tests/integration/api/user.rs @@ -1165,7 +1165,7 @@ async fn test_modify_user_admin_updates_other_user(_: PgPoolOptions, options: Pg assert_eq!(updated.last_name, "UpdatedLast"); assert_eq!(updated.email, "updated@hogwart.edu.uk"); assert_eq!(updated.phone, Some("+48999888777".into())); - // mfa_method must NOT have changed — admin is not updating self + // mfa_method must NOT have changed - admin is not updating self assert_eq!(updated.mfa_method, old_user.mfa_method); client.verify_api_events(&[ApiEventType::UserModified { diff --git a/crates/defguard_grpc_tls/src/certs.rs b/crates/defguard_grpc_tls/src/certs.rs index 04b319811..446bbf959 100644 --- a/crates/defguard_grpc_tls/src/certs.rs +++ b/crates/defguard_grpc_tls/src/certs.rs @@ -216,10 +216,10 @@ pub fn client_config( /// Build an mTLS [`Channel`] to a proxy using its stored per-component client certificate. /// -/// * `proxy` — the full `Proxy` row from the database; `core_client_cert_der`, +/// * `proxy` - the full `Proxy` row from the database; `core_client_cert_der`, /// `core_client_cert_key_der`, and `certificate_serial` must all be `Some`. -/// * `ca_cert_der` — the core CA certificate in DER form, used as the only trusted root. -/// * `certs_rx` — watch channel carrying the current `{ proxy_id → cert_serial }` map. +/// * `ca_cert_der` - the core CA certificate in DER form, used as the only trusted root. +/// * `certs_rx` - watch channel carrying the current `{ proxy_id → cert_serial }` map. /// Pass a long-lived receiver for persistent connections (serial revocation is picked up /// dynamically) or a one-shot channel seeded with the proxy's current serial for /// short-lived calls. @@ -253,7 +253,7 @@ pub fn proxy_mtls_channel( .build(); let connector = HttpsSchemeConnector::new(connector); - // Use http:// scheme — the HttpsSchemeConnector rewrites it to https:// internally. + // Use http:// scheme - the HttpsSchemeConnector rewrites it to https:// internally. let endpoint_str = format!("http://{}:{}", proxy.address, proxy.port); let endpoint = Endpoint::from_shared(endpoint_str) .map_err(|e| CertConfigError::TlsConfig(format!("invalid proxy endpoint URL: {e}")))? diff --git a/crates/defguard_proxy_manager/src/handler.rs b/crates/defguard_proxy_manager/src/handler.rs index 76eb1c5de..81d804ff5 100644 --- a/crates/defguard_proxy_manager/src/handler.rs +++ b/crates/defguard_proxy_manager/src/handler.rs @@ -1050,7 +1050,7 @@ impl ProxyHandler { ) -> Result { if let Some(socket_path) = self.test_transport.socket_path().cloned() { // Build a minimal endpoint for the Unix socket connector. - // The scheme and host are irrelevant — the connector ignores the URI. + // The scheme and host are irrelevant - the connector ignores the URI. let endpoint = Endpoint::from_shared(self.url.to_string())?; return Ok(endpoint.connect_with_connector_lazy(tower::service_fn( move |_: tonic::transport::Uri| { diff --git a/crates/defguard_proxy_manager/src/tests/common/mod.rs b/crates/defguard_proxy_manager/src/tests/common/mod.rs index 95a333b50..bbc07ca07 100644 --- a/crates/defguard_proxy_manager/src/tests/common/mod.rs +++ b/crates/defguard_proxy_manager/src/tests/common/mod.rs @@ -608,7 +608,7 @@ impl ManagerTestContext { ); let manager_task = tokio::spawn(async move { manager.run().await }); - // No PgListener in proxy manager — just yield to let the manager start. + // No PgListener in proxy manager - just yield to let the manager start. tokio::task::yield_now().await; self.manager_task = Some(manager_task); @@ -697,7 +697,7 @@ pub(crate) fn build_proxy_with_enabled(enabled: bool) -> Proxy { } // --------------------------------------------------------------------------- -// MockOidcProvider — a minimal OIDC identity provider for tests +// MockOidcProvider - a minimal OIDC identity provider for tests // --------------------------------------------------------------------------- /// Shared state injected into axum route handlers. diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/acme.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/acme.rs index 66da653ef..6104f3b12 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/acme.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/acme.rs @@ -11,7 +11,7 @@ use crate::tests::common::{ }; /// A minimal but syntactically valid PEM certificate block (content is -/// arbitrary bytes — the handler stores it verbatim without parsing). +/// arbitrary bytes - the handler stores it verbatim without parsing). const TEST_CERT_PEM: &str = "-----BEGIN CERTIFICATE-----\nMIIBkTCB+wIJ\n-----END CERTIFICATE-----\n"; const TEST_KEY_PEM: &str = @@ -154,7 +154,7 @@ async fn test_acme_certificate_overwrites_existing(_: PgPoolOptions, options: Pg /// in `handler_tx_map`. After processing `AcmeCertificate`, it broadcasts /// `HttpsCerts` to ALL registered handlers, which forward it to their /// respective proxy streams. This test verifies that every connected mock -/// proxy receives the `HttpsCerts` response — including proxies other than the +/// proxy receives the `HttpsCerts` response - including proxies other than the /// one that sent the certificate. #[sqlx::test] async fn test_acme_certificate_broadcasts_to_connected_proxy( @@ -182,7 +182,7 @@ async fn test_acme_certificate_broadcasts_to_connected_proxy( TEST_ACCOUNT_JSON, )); - // The handler must broadcast HttpsCerts to ALL registered proxies — both + // The handler must broadcast HttpsCerts to ALL registered proxies - both // the sender (proxy A) and the bystander (proxy B). for (label, mock) in [ ("proxy A (sender)", &mut mock_a), diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/enrollment.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/enrollment.rs index 39867b0c2..25825e192 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/enrollment.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/enrollment.rs @@ -221,7 +221,7 @@ async fn test_activate_user_already_activated_returns_error( let token = create_enrollment_token(&context.pool, user.id, Some(user.id)).await; start_enrollment_session(&mut context, &token.id).await; - // First activation — must succeed. + // First activation - must succeed. let first = send_activate_user(&mut context, &token.id, STRONG_PASSWORD, None).await; match &first.payload { Some(core_response::Payload::Empty(())) => {} @@ -234,7 +234,7 @@ async fn test_activate_user_already_activated_returns_error( let token2 = create_enrollment_token(&context.pool, user.id, Some(user.id)).await; start_enrollment_session(&mut context, &token2.id).await; - // Second activation — must fail with InvalidArgument. + // Second activation - must fail with InvalidArgument. let second = send_activate_user(&mut context, &token2.id, STRONG_PASSWORD, None).await; let code = assert_error_response(&second); assert_eq!( @@ -393,7 +393,7 @@ async fn test_existing_device_wrong_user_returns_error( let _ = user_a; // suppress unused warning // Enrollment token belonging to user_b, NOT user_a (device owner). - // No admin needed — this test only checks that an error is returned; + // No admin needed - this test only checks that an error is returned; // the session validation will fail before the welcome-page template renders. let wrong_token = create_enrollment_token(&context.pool, user_b.id, None).await; @@ -678,7 +678,7 @@ async fn test_register_mobile_auth_invalid_device_pubkey( /// `device_pub_key` (not `auth_pub_key`), and WireGuard keys are 32 bytes so /// the check always passes for syntactically valid WireGuard keys. The first /// error path that exercises `auth_pub_key` rejection would require a key -/// whose WireGuard decode fails — but a valid WireGuard key passes both +/// whose WireGuard decode fails - but a valid WireGuard key passes both /// `Device::validate_pubkey` and `BiometricAuth::validate_pubkey`. The /// interesting third error path is therefore "key valid but no device found". #[sqlx::test] @@ -829,7 +829,7 @@ async fn test_activate_ldap_user_sets_ldap_remote_enrollment_completed( } /// When `ldap_remote_enrollment_enabled` is set, a non-LDAP user who completes -/// activation must NOT have `ldap_remote_enrollment_completed` set — the flag is +/// activation must NOT have `ldap_remote_enrollment_completed` set - the flag is /// LDAP-specific and must remain `false`. #[sqlx::test] async fn test_activate_non_ldap_user_does_not_set_ldap_remote_enrollment_completed( diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa.rs index 3b8758549..73f17a464 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/mfa.rs @@ -53,7 +53,7 @@ async fn test_mfa_start_fails_for_unknown_location(_: PgPoolOptions, options: Pg let mut context = HandlerTestContext::new(options).await; complete_proxy_handshake(&mut context).await; - // Create a device so the pubkey lookup succeeds — the handler checks the + // Create a device so the pubkey lookup succeeds - the handler checks the // location_id first, but using a real pubkey avoids masking the error. let (_, device) = create_user_with_device(&context.pool).await; @@ -137,7 +137,7 @@ async fn test_mfa_finish_succeeds_with_totp_code(_: PgPoolOptions, options: PgCo assert!(session.preshared_key.is_some()); // Verify GatewayEvent::MfaSessionAuthorized was broadcast. - // Use the already-subscribed receiver — subscribing after send_mfa_finish would miss the event. + // Use the already-subscribed receiver - subscribing after send_mfa_finish would miss the event. let event = timeout(RECEIVE_TIMEOUT, gateway_rx.recv()) .await .expect("timed out waiting for GatewayEvent::MfaSessionAuthorized") @@ -232,7 +232,7 @@ async fn test_mfa_start_fails_when_email_mfa_not_enabled( let network = create_mfa_network(&context.pool).await; // device is created after the network so add_to_all_networks picks it up let (_, device) = create_user_with_device(&context.pool).await; - // user.email_mfa_enabled is false by default — no setup call + // user.email_mfa_enabled is false by default - no setup call context.mock_proxy().send_request(CoreRequest { id: 1, @@ -281,7 +281,7 @@ async fn test_mfa_finish_succeeds_and_creates_session(_: PgPoolOptions, options: let network = create_mfa_network(&context.pool).await; let (mut user, device) = create_user_with_device(&context.pool).await; - // Setup email MFA — the code is the same one that start_client_mfa_login + // Setup email MFA - the code is the same one that start_client_mfa_login // will regenerate internally, so we can generate it once here. let code = setup_user_email_mfa(&context.pool, &mut user).await; @@ -384,7 +384,7 @@ async fn test_mfa_finish_fails_with_wrong_code(_: PgPoolOptions, options: PgConn ) .await; - // Send a clearly wrong code — use _raw so we can inspect the error response + // Send a clearly wrong code - use _raw so we can inspect the error response let response = send_mfa_finish_raw(&mut context, &token, Some("000000")).await; let code = assert_error_response(&response); // invalid code → InvalidArgument or Unauthenticated @@ -448,7 +448,7 @@ async fn test_mfa_await_remote_receives_psk_after_finish( ) .await; - // Send AwaitRemoteMfaFinish first — no immediate response expected + // Send AwaitRemoteMfaFinish first - no immediate response expected context.mock_proxy().send_request(CoreRequest { id: AWAIT_ID, device_info: None, @@ -473,7 +473,7 @@ async fn test_mfa_await_remote_receives_psk_after_finish( send_mfa_finish_no_recv(&mut context, &token, Some(&code)).await; // Two responses should arrive: one ClientMfaFinish and one - // AwaitRemoteMfaFinish — order is not guaranteed. + // AwaitRemoteMfaFinish - order is not guaranteed. let r1 = context.mock_proxy_mut().recv_outbound().await; let r2 = context.mock_proxy_mut().recv_outbound().await; diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/oidc.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/oidc.rs index cb152ba30..a1bea483e 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/oidc.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/oidc.rs @@ -241,7 +241,7 @@ async fn test_auth_info_requires_oidc_provider(_: PgPoolOptions, options: PgConn complete_proxy_handshake(&mut context).await; set_test_license_business(); - // No OIDC provider is inserted — but we still need a valid public proxy URL + // No OIDC provider is inserted - but we still need a valid public proxy URL // so that edge_callback_url() does not fail before the provider lookup. set_public_proxy_url(&context.pool, "http://proxy.example.com").await; @@ -323,7 +323,7 @@ async fn test_mfa_oidc_full_flow(_: PgPoolOptions, options: PgConnectOptions) { response.payload.as_ref().map(std::mem::discriminant) ); - // ---- Step 3: ClientMfaFinish (no TOTP code — session is OIDC-completed) ---- + // ---- Step 3: ClientMfaFinish (no TOTP code - session is OIDC-completed) ---- let (_, psk) = send_mfa_finish(&mut context, &mfa_token, None).await; assert!( !psk.is_empty(), diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/polling.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/polling.rs index 726f87b36..11c24f4b7 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/polling.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/polling.rs @@ -55,7 +55,7 @@ async fn test_polling_requires_business_license(_: PgPoolOptions, options: PgCon let mut context = HandlerTestContext::new(options).await; complete_proxy_handshake(&mut context).await; - // Explicitly clear any license — polling should be refused. + // Explicitly clear any license - polling should be refused. clear_test_license(); let (_user, device) = create_user_with_device(&context.pool).await; diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/support.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/support.rs index 916900bb4..82aae8c3f 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/support.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/handler/support.rs @@ -172,7 +172,7 @@ pub(crate) async fn create_network(pool: &PgPool) -> WireguardNetwork { /// Pre-generated valid 32-byte WireGuard public keys (base64, 44 chars each). /// Used by `create_device_for_user` so that `Device::validate_pubkey` passes. -/// 64 entries — enough headroom so the per-function counter modulo never wraps +/// 64 entries - enough headroom so the per-function counter modulo never wraps /// within a single test and causes unique-key constraint violations. static DEVICE_PUBKEYS: &[&str] = &[ "HCk2Q1BdaneEkZ6ruMXS3+z5BhMgLTpHVGFue4iVoq8=", @@ -325,7 +325,7 @@ pub(crate) async fn create_polling_token(pool: &PgPool, device_id: Id) -> String /// after `complete_proxy_handshake` to open the enrollment session. /// /// The function sends a single `EnrollmentStartRequest` with the given token -/// ID and waits for the `EnrollmentStartResponse` (or any payload — panicking +/// ID and waits for the `EnrollmentStartResponse` (or any payload - panicking /// if the stream closes without a response). pub(crate) async fn start_enrollment_session(context: &mut HandlerTestContext, token_id: &str) { static ENROLL_CTR: AtomicU64 = AtomicU64::new(1000); @@ -414,7 +414,7 @@ pub(crate) async fn setup_user_email_mfa(pool: &PgPool, user: &mut User) -> user.new_email_secret(pool).await.expect("new_email_secret"); user.enable_email_mfa(pool).await.expect("enable_email_mfa"); // generate_email_mfa_code uses the in-memory secret; note that - // start_client_mfa_login also calls generate_email_mfa_code internally — + // start_client_mfa_login also calls generate_email_mfa_code internally - // the two calls will produce the same code because the in-memory secret // hasn't changed. But we need the code *after* the start call, so the // caller should call this helper before start and pass the code to finish. @@ -563,7 +563,7 @@ pub(crate) async fn send_mfa_finish_no_recv( /// Send `ClientMfaFinish` and return the raw `CoreResponse`. /// -/// Like `send_mfa_finish` but does not panic on error — the caller is +/// Like `send_mfa_finish` but does not panic on error - the caller is /// responsible for inspecting `response.payload`. Use this for error-path /// tests where an error response is expected. pub(crate) async fn send_mfa_finish_raw( diff --git a/crates/defguard_proxy_manager/src/tests/proxy_manager/manager.rs b/crates/defguard_proxy_manager/src/tests/proxy_manager/manager.rs index 97f03fadc..456637566 100644 --- a/crates/defguard_proxy_manager/src/tests/proxy_manager/manager.rs +++ b/crates/defguard_proxy_manager/src/tests/proxy_manager/manager.rs @@ -40,7 +40,7 @@ async fn test_manager_starts_all_enabled_proxies_on_startup( context.finish().await; } -/// Two enabled proxies at startup — both complete their handshake and both +/// Two enabled proxies at startup - both complete their handshake and both /// appear as connected in the DB. Verifies that the manager spawns independent /// handler tasks and that they do not interfere with each other. #[sqlx::test] @@ -57,7 +57,7 @@ async fn test_two_proxies_connect_independently(_: PgPoolOptions, options: PgCon context.start().await; - // Both handshakes must complete — order is not guaranteed. + // Both handshakes must complete - order is not guaranteed. complete_manager_proxy_handshake(&mut mock_a).await; complete_manager_proxy_handshake(&mut mock_b).await; @@ -105,7 +105,7 @@ async fn test_start_connection_adds_proxy_at_runtime(_: PgPoolOptions, options: let mut mock_b = MockProxyHarness::start().await; context.register_proxy_mock(&proxy_b, &mock_b); - // Third proxy: disabled — manager must not start it. + // Third proxy: disabled - manager must not start it. let proxy_c = create_proxy_with_enabled(&context.pool, false).await; let mut mock_c = MockProxyHarness::start().await; context.register_proxy_mock(&proxy_c, &mock_c); @@ -167,7 +167,7 @@ async fn test_start_connection_adds_proxy_at_runtime(_: PgPoolOptions, options: } /// One proxy's stream closes and reconnects to a replacement mock server at the -/// same socket path. The other proxy must remain connected throughout — the +/// same socket path. The other proxy must remain connected throughout - the /// reconnect must be fully isolated to the affected handler task. #[sqlx::test] async fn test_one_proxy_reconnects_while_other_stays_connected( @@ -177,7 +177,7 @@ async fn test_one_proxy_reconnects_while_other_stays_connected( let mut context = ManagerTestContext::new(options).await; context.set_retry_delay(FAST_RETRY_DELAY); - // Proxy A: reconnects — use a fixed socket path so we can start a replacement. + // Proxy A: reconnects - use a fixed socket path so we can start a replacement. let proxy_a = create_proxy(&context.pool).await; let socket_a = mock_proxy_socket_path(); context.register_proxy_socket_path( @@ -185,7 +185,7 @@ async fn test_one_proxy_reconnects_while_other_stays_connected( socket_a.clone(), ); - // Proxy B: stable — standard mock. + // Proxy B: stable - standard mock. let proxy_b = create_proxy(&context.pool).await; let mut mock_b = MockProxyHarness::start().await; context.register_proxy_mock(&proxy_b, &mock_b); @@ -195,7 +195,7 @@ async fn test_one_proxy_reconnects_while_other_stays_connected( .wait_for_handler_spawn_attempt_count(proxy_a.id, 1) .await; - // First mock for proxy A — will be closed to trigger a reconnect. + // First mock for proxy A - will be closed to trigger a reconnect. let mut mock_a1 = MockProxyHarness::start_at(socket_a.clone()).await; mock_a1.wait_for_connection_count(1).await; complete_manager_proxy_handshake(&mut mock_a1).await; @@ -207,7 +207,7 @@ async fn test_one_proxy_reconnects_while_other_stays_connected( let initial_spawn_count_a = context.handler_spawn_attempt_count(proxy_a.id); - // Close proxy A's stream — triggers internal retry loop in handler A. + // Close proxy A's stream - triggers internal retry loop in handler A. mock_a1.close_stream(); wait_for_proxy_connection_state(&context.pool, proxy_a.id, false).await; @@ -217,7 +217,7 @@ async fn test_one_proxy_reconnects_while_other_stays_connected( complete_manager_proxy_handshake(&mut mock_a2).await; wait_for_proxy_connection_state(&context.pool, proxy_a.id, true).await; - // Handler A reused its existing task — no new supervisor spawned. + // Handler A reused its existing task - no new supervisor spawned. assert_eq!( context.handler_spawn_attempt_count(proxy_a.id), initial_spawn_count_a, @@ -296,7 +296,7 @@ async fn test_shutdown_control_message_disconnects_without_purge( complete_manager_proxy_handshake(&mut mock_proxy).await; wait_for_proxy_connection_state(&context.pool, proxy.id, true).await; - // Send ShutdownConnection — purge() RPC must NOT be called. + // Send ShutdownConnection - purge() RPC must NOT be called. context .proxy_control_tx .send(ProxyControlMessage::ShutdownConnection(proxy.id)) @@ -338,7 +338,7 @@ async fn test_purge_control_message_calls_purge_rpc(_: PgPoolOptions, options: P wait_for_proxy_connection_state(&context.pool, proxy_a.id, true).await; wait_for_proxy_connection_state(&context.pool, proxy_b.id, true).await; - // Send Purge targeting proxy A only — purge() RPC MUST be called on A. + // Send Purge targeting proxy A only - purge() RPC MUST be called on A. context .proxy_control_tx .send(ProxyControlMessage::Purge(proxy_a.id)) @@ -353,7 +353,7 @@ async fn test_purge_control_message_calls_purge_rpc(_: PgPoolOptions, options: P "proxy A should be disconnected after Purge" ); - // Proxy B must be completely unaffected — not purged, still connected. + // Proxy B must be completely unaffected - not purged, still connected. assert_eq!( mock_b.purge_count(), 0, @@ -388,7 +388,7 @@ async fn test_manager_retries_after_stream_close_single_supervisor( .wait_for_handler_spawn_attempt_count(proxy.id, 1) .await; - // First mock server — accept one connection, then close the stream. + // First mock server - accept one connection, then close the stream. let mut mock_proxy = MockProxyHarness::start_at(socket_path.clone()).await; mock_proxy.wait_for_connection_count(1).await; complete_manager_proxy_handshake(&mut mock_proxy).await; @@ -421,7 +421,7 @@ async fn test_manager_retries_after_stream_close_single_supervisor( /// /// 1. Start the manager with two enabled proxies (both mocked and connected). /// 2. Verify both report as connected in the DB. -/// 3. Send `ShutdownConnection` for the second proxy — exactly what +/// 3. Send `ShutdownConnection` for the second proxy - exactly what /// `trim_gateways_and_edges` would send after calling /// `Proxy::leave_one_enabled`. /// 4. Assert the second proxy is now disconnected in the DB. @@ -475,7 +475,7 @@ async fn test_license_expiry_shuts_down_excess_proxy_only( "ShutdownConnection must not trigger a purge RPC on the excess proxy" ); - // The retained proxy must still be connected — license expiry must not + // The retained proxy must still be connected - license expiry must not // affect proxies that are allowed to remain. let after_keep = wait_for_proxy_connection_state(&context.pool, proxy_keep.id, true).await; assert!( diff --git a/crates/defguard_setup/tests/integration/auto_adoption_wizard.rs b/crates/defguard_setup/tests/integration/auto_adoption_wizard.rs index 433fccd8b..deaec3553 100644 --- a/crates/defguard_setup/tests/integration/auto_adoption_wizard.rs +++ b/crates/defguard_setup/tests/integration/auto_adoption_wizard.rs @@ -378,7 +378,7 @@ async fn test_auto_adoption_vpn_settings_missing_network( .expect("Failed to create admin"); assert_eq!(resp.status(), StatusCode::CREATED); - // Set URL settings (requires auth — cookie jar carries session) + // Set URL settings (requires auth - cookie jar carries session) let resp = client .post("/api/v1/initial_setup/auto_wizard/internal_url_settings") .json(&json!({ diff --git a/crates/defguard_setup/tests/integration/common.rs b/crates/defguard_setup/tests/integration/common.rs index a14ccd46c..684028bd7 100644 --- a/crates/defguard_setup/tests/integration/common.rs +++ b/crates/defguard_setup/tests/integration/common.rs @@ -115,7 +115,7 @@ pub async fn make_migration_test_client( setup_shutdown_tx, ); // We must keep `webapp` alive to prevent its event receiver channels from - // being dropped — if they are dropped the `emit_event` call in the auth + // being dropped - if they are dropped the `emit_event` call in the auth // handler will fail with "channel closed". let router = webapp.router.clone(); let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0); From 5c674727d666b44f86667fa7f5143e8771fb230c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 17 Apr 2026 22:16:59 +0200 Subject: [PATCH 28/46] remove token from log message --- crates/defguard_core/src/auth/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/defguard_core/src/auth/mod.rs b/crates/defguard_core/src/auth/mod.rs index 685ebeede..0ff36b117 100644 --- a/crates/defguard_core/src/auth/mod.rs +++ b/crates/defguard_core/src/auth/mod.rs @@ -52,7 +52,7 @@ where })?; if let Some(header) = maybe_auth_header { let token_string = header.token(); - debug!("Trying to authorize request using API token: {token_string}"); + debug!("Trying to authorize request using API token"); return match ApiToken::try_find_by_auth_token(&pool, token_string).await { Ok(Some(api_token)) => { // create a dummy session and don't store it in the DB From 18f95139673043aae764bf151cb0f32c2f581ec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Sun, 19 Apr 2026 21:20:55 +0200 Subject: [PATCH 29/46] use query macros for validation --- ...14f043b3b3fccb2d956776dbb409d157cb53b2b8ac.json | 12 ++++++++++++ ...9047333d1c3f2b3a4f20a93089bad74a8d560b1843.json | 12 ++++++++++++ ...0e662b99e657fce3a80d9eb42760728db17d8d844e.json | 12 ++++++++++++ ...e7eef6c942054e73522b9814323391009adfbd5e69.json | 12 ++++++++++++ ...4925f0ac762ebb9bfcedbc24a104c7237802e3a70b.json | 14 ++++++++++++++ crates/defguard_common/src/db/models/gateway.rs | 2 +- .../src/db/models/initial_setup_wizard.rs | 2 +- .../src/db/models/migration_wizard.rs | 8 ++++---- crates/defguard_common/src/db/models/proxy.rs | 2 +- .../src/db/models/setup_auto_adoption.rs | 2 +- 10 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 .sqlx/query-304b0e9ef4c04cc998581114f043b3b3fccb2d956776dbb409d157cb53b2b8ac.json create mode 100644 .sqlx/query-3f61241d9934c717b7c5f89047333d1c3f2b3a4f20a93089bad74a8d560b1843.json create mode 100644 .sqlx/query-f2eab45b5d87910672e8970e662b99e657fce3a80d9eb42760728db17d8d844e.json create mode 100644 .sqlx/query-fa84c8e5a9db1d10c78a73e7eef6c942054e73522b9814323391009adfbd5e69.json create mode 100644 .sqlx/query-fff6c48d97533e3b6b82954925f0ac762ebb9bfcedbc24a104c7237802e3a70b.json diff --git a/.sqlx/query-304b0e9ef4c04cc998581114f043b3b3fccb2d956776dbb409d157cb53b2b8ac.json b/.sqlx/query-304b0e9ef4c04cc998581114f043b3b3fccb2d956776dbb409d157cb53b2b8ac.json new file mode 100644 index 000000000..8a646219d --- /dev/null +++ b/.sqlx/query-304b0e9ef4c04cc998581114f043b3b3fccb2d956776dbb409d157cb53b2b8ac.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE wizard\n SET initial_setup_state = NULL\n WHERE is_singleton", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "304b0e9ef4c04cc998581114f043b3b3fccb2d956776dbb409d157cb53b2b8ac" +} diff --git a/.sqlx/query-3f61241d9934c717b7c5f89047333d1c3f2b3a4f20a93089bad74a8d560b1843.json b/.sqlx/query-3f61241d9934c717b7c5f89047333d1c3f2b3a4f20a93089bad74a8d560b1843.json new file mode 100644 index 000000000..c7b2c08e8 --- /dev/null +++ b/.sqlx/query-3f61241d9934c717b7c5f89047333d1c3f2b3a4f20a93089bad74a8d560b1843.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE proxy SET disconnected_at = NOW() WHERE connected_at IS NOT NULL AND (disconnected_at IS NULL OR disconnected_at < connected_at)", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "3f61241d9934c717b7c5f89047333d1c3f2b3a4f20a93089bad74a8d560b1843" +} diff --git a/.sqlx/query-f2eab45b5d87910672e8970e662b99e657fce3a80d9eb42760728db17d8d844e.json b/.sqlx/query-f2eab45b5d87910672e8970e662b99e657fce3a80d9eb42760728db17d8d844e.json new file mode 100644 index 000000000..70fbe11a0 --- /dev/null +++ b/.sqlx/query-f2eab45b5d87910672e8970e662b99e657fce3a80d9eb42760728db17d8d844e.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE wizard\n SET auto_adoption_state = NULL\n WHERE is_singleton", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "f2eab45b5d87910672e8970e662b99e657fce3a80d9eb42760728db17d8d844e" +} diff --git a/.sqlx/query-fa84c8e5a9db1d10c78a73e7eef6c942054e73522b9814323391009adfbd5e69.json b/.sqlx/query-fa84c8e5a9db1d10c78a73e7eef6c942054e73522b9814323391009adfbd5e69.json new file mode 100644 index 000000000..4f2ac9dcb --- /dev/null +++ b/.sqlx/query-fa84c8e5a9db1d10c78a73e7eef6c942054e73522b9814323391009adfbd5e69.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE gateway SET disconnected_at = NOW() WHERE connected_at IS NOT NULL AND (disconnected_at IS NULL OR disconnected_at <= connected_at)", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "fa84c8e5a9db1d10c78a73e7eef6c942054e73522b9814323391009adfbd5e69" +} diff --git a/.sqlx/query-fff6c48d97533e3b6b82954925f0ac762ebb9bfcedbc24a104c7237802e3a70b.json b/.sqlx/query-fff6c48d97533e3b6b82954925f0ac762ebb9bfcedbc24a104c7237802e3a70b.json new file mode 100644 index 000000000..d650aa3d1 --- /dev/null +++ b/.sqlx/query-fff6c48d97533e3b6b82954925f0ac762ebb9bfcedbc24a104c7237802e3a70b.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE wizard\n SET migration_wizard_state = $1\n WHERE is_singleton", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Jsonb" + ] + }, + "nullable": [] + }, + "hash": "fff6c48d97533e3b6b82954925f0ac762ebb9bfcedbc24a104c7237802e3a70b" +} diff --git a/crates/defguard_common/src/db/models/gateway.rs b/crates/defguard_common/src/db/models/gateway.rs index 3e57f5098..b13328f4c 100644 --- a/crates/defguard_common/src/db/models/gateway.rs +++ b/crates/defguard_common/src/db/models/gateway.rs @@ -117,7 +117,7 @@ impl Gateway { where E: PgExecutor<'e>, { - query( + query!( "UPDATE gateway \ SET disconnected_at = NOW() \ WHERE connected_at IS NOT NULL \ diff --git a/crates/defguard_common/src/db/models/initial_setup_wizard.rs b/crates/defguard_common/src/db/models/initial_setup_wizard.rs index 749f924b8..03e46b627 100644 --- a/crates/defguard_common/src/db/models/initial_setup_wizard.rs +++ b/crates/defguard_common/src/db/models/initial_setup_wizard.rs @@ -79,7 +79,7 @@ impl InitialSetupState { where E: PgExecutor<'e>, { - query( + query!( "UPDATE wizard SET initial_setup_state = NULL WHERE is_singleton", diff --git a/crates/defguard_common/src/db/models/migration_wizard.rs b/crates/defguard_common/src/db/models/migration_wizard.rs index e0acb5a2e..3ea0ad23c 100644 --- a/crates/defguard_common/src/db/models/migration_wizard.rs +++ b/crates/defguard_common/src/db/models/migration_wizard.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use sqlx::PgExecutor; +use sqlx::{PgExecutor, query}; #[derive(Debug, Serialize, Deserialize, Default)] pub enum MigrationWizardStep { @@ -76,12 +76,12 @@ impl MigrationWizardState { let state = serde_json::to_value(self).map_err(|error| sqlx::Error::Decode(Box::new(error)))?; - sqlx::query( + query!( "UPDATE wizard SET migration_wizard_state = $1 WHERE is_singleton", + state ) - .bind(state) .execute(executor) .await?; @@ -92,7 +92,7 @@ impl MigrationWizardState { where E: PgExecutor<'e>, { - sqlx::query!( + query!( "Update wizard \ SET migration_wizard_state = NULL \ WHERE is_singleton" diff --git a/crates/defguard_common/src/db/models/proxy.rs b/crates/defguard_common/src/db/models/proxy.rs index dd0244cb2..6d005bc38 100644 --- a/crates/defguard_common/src/db/models/proxy.rs +++ b/crates/defguard_common/src/db/models/proxy.rs @@ -115,7 +115,7 @@ impl Proxy { /// Mark all proxies currently considered connected as disconnected. pub async fn mark_all_disconnected<'e, E: PgExecutor<'e>>(executor: E) -> sqlx::Result<()> { - query( + query!( "UPDATE proxy \ SET disconnected_at = NOW() \ WHERE connected_at IS NOT NULL \ diff --git a/crates/defguard_common/src/db/models/setup_auto_adoption.rs b/crates/defguard_common/src/db/models/setup_auto_adoption.rs index 7d7502238..f9d0ebb18 100644 --- a/crates/defguard_common/src/db/models/setup_auto_adoption.rs +++ b/crates/defguard_common/src/db/models/setup_auto_adoption.rs @@ -85,7 +85,7 @@ impl AutoAdoptionWizardState { where E: PgExecutor<'e>, { - query( + query!( "UPDATE wizard SET auto_adoption_state = NULL WHERE is_singleton", From 926ebd52139f9092eb7b58c7a43cccc0f6a948de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Sun, 19 Apr 2026 21:26:04 +0200 Subject: [PATCH 30/46] remove more magic values --- crates/defguard_mail/src/tests.rs | 34 ++++++++++++++++--------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/crates/defguard_mail/src/tests.rs b/crates/defguard_mail/src/tests.rs index ca49edf59..949cef0dd 100644 --- a/crates/defguard_mail/src/tests.rs +++ b/crates/defguard_mail/src/tests.rs @@ -22,6 +22,8 @@ use tokio::time::sleep; use super::{Attachment, mail::MailMessage, templates}; +const SEND_DELAY: Duration = Duration::from_secs(2); + #[test] fn dg25_8_server_side_template_injection() { let mut tera = templates::safe_tera(); @@ -68,7 +70,7 @@ fn send_desktop_start(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - sleep(Duration::from_secs(2)).await; + sleep(SEND_DELAY).await; } #[ignore = "requires SMTP server"] @@ -103,7 +105,7 @@ fn send_new_device_added(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - sleep(Duration::from_secs(2)).await; + sleep(SEND_DELAY).await; } #[ignore = "requires SMTP server"] @@ -126,7 +128,7 @@ fn send_mfa_code(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - sleep(Duration::from_secs(2)).await; + sleep(SEND_DELAY).await; } #[ignore = "requires SMTP server"] @@ -150,7 +152,7 @@ fn send_new_account(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - sleep(Duration::from_secs(2)).await; + sleep(SEND_DELAY).await; } #[ignore = "requires SMTP server"] @@ -173,7 +175,7 @@ fn send_mfa_activation(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - sleep(Duration::from_secs(2)).await; + sleep(SEND_DELAY).await; } #[ignore = "requires SMTP server"] @@ -198,7 +200,7 @@ fn send_enrollment_admin_notification(_: PgPoolOptions, options: PgConnectOption .unwrap(); // Delay, so send_and_forget() can process the message. - sleep(Duration::from_secs(2)).await; + sleep(SEND_DELAY).await; } #[ignore = "requires SMTP server"] @@ -222,7 +224,7 @@ fn send_gateway_disconnected_mail(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - sleep(Duration::from_secs(2)).await; + sleep(SEND_DELAY).await; } #[ignore = "requires SMTP server"] @@ -246,7 +248,7 @@ fn send_gateway_reconnected_mail(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - sleep(Duration::from_secs(2)).await; + sleep(SEND_DELAY).await; } #[ignore = "requires SMTP server"] @@ -266,7 +268,7 @@ fn send_mfa_configured_mail(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - sleep(Duration::from_secs(2)).await; + sleep(SEND_DELAY).await; } #[ignore = "requires SMTP server"] @@ -282,7 +284,7 @@ fn send_new_device_login_mail(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - sleep(Duration::from_secs(2)).await; + sleep(SEND_DELAY).await; } #[ignore = "requires SMTP server"] @@ -305,7 +307,7 @@ fn send_new_device_oidc_login_mail(_: PgPoolOptions, options: PgConnectOptions) .unwrap(); // Delay, so send_and_forget() can process the message. - sleep(Duration::from_secs(2)).await; + sleep(SEND_DELAY).await; } #[ignore = "requires SMTP server"] @@ -329,7 +331,7 @@ fn send_password_reset_mail(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - sleep(Duration::from_secs(2)).await; + sleep(SEND_DELAY).await; } #[ignore = "requires SMTP server"] @@ -344,7 +346,7 @@ fn send_password_reset_success_mail(_: PgPoolOptions, options: PgConnectOptions) .unwrap(); // Delay, so send_and_forget() can process the message. - sleep(Duration::from_secs(2)).await; + sleep(SEND_DELAY).await; } #[ignore = "requires SMTP server"] @@ -359,7 +361,7 @@ fn send_test_mail(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - sleep(Duration::from_secs(2)).await; + sleep(SEND_DELAY).await; } #[ignore = "requires SMTP server"] @@ -378,7 +380,7 @@ fn send_support_data_mail(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - sleep(Duration::from_secs(2)).await; + sleep(SEND_DELAY).await; } #[ignore = "requires SMTP server"] @@ -392,7 +394,7 @@ fn send_enrollment_welcome_mail(_: PgPoolOptions, options: PgConnectOptions) { .unwrap(); // Delay, so send_and_forget() can process the message. - sleep(Duration::from_secs(2)).await; + sleep(SEND_DELAY).await; } #[test] From 86cf3fbe76c3d3f09747d43476593a88e2ecb1bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Sun, 19 Apr 2026 21:27:06 +0200 Subject: [PATCH 31/46] update docstring --- crates/defguard_grpc_tls/src/server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/defguard_grpc_tls/src/server.rs b/crates/defguard_grpc_tls/src/server.rs index e07c47b3b..6405d5fb5 100644 --- a/crates/defguard_grpc_tls/src/server.rs +++ b/crates/defguard_grpc_tls/src/server.rs @@ -24,7 +24,7 @@ use x509_parser::prelude::*; /// /// ```rust,ignore /// ServiceBuilder::new() -/// .layer(tonic::service::interceptor(certificate_serial_interceptor(Some(serial)))) +/// .layer(tonic::service::interceptor(certificate_serial_interceptor(serial))) /// .layer(/* version layer */) /// .service(/* gRPC service */) /// ``` From 5bb5e8018c969a4e39aedb8421fb2b00e10a92e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 20 Apr 2026 08:27:43 +0200 Subject: [PATCH 32/46] remove unnecessary dependency --- Cargo.lock | 1 - crates/defguard_core/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 262434982..a29255b0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1388,7 +1388,6 @@ dependencies = [ "defguard_web_ui", "futures", "humantime", - "hyper-rustls", "hyper-util", "ipnetwork", "jsonwebkey", diff --git a/crates/defguard_core/Cargo.toml b/crates/defguard_core/Cargo.toml index f1df1e3d7..fb24a8776 100644 --- a/crates/defguard_core/Cargo.toml +++ b/crates/defguard_core/Cargo.toml @@ -31,7 +31,6 @@ bytes = { workspace = true } chrono = { workspace = true } futures = { workspace = true } humantime = { workspace = true } -hyper-rustls = { workspace = true } # match version used by sqlx ipnetwork = { workspace = true } jsonwebkey = { workspace = true } From 4da330973606ec570790a1094835fcb9f6604f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 20 Apr 2026 10:01:31 +0200 Subject: [PATCH 33/46] update dependencies --- Cargo.lock | 255 +++++++++--------- flake.lock | 12 +- web/package.json | 28 +- web/pnpm-lock.yaml | 641 ++++++++++++++++++++++----------------------- 4 files changed, 461 insertions(+), 475 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a29255b0a..4712785f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -387,9 +387,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.16.2" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" dependencies = [ "aws-lc-sys", "zeroize", @@ -397,9 +397,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.39.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" dependencies = [ "cc", "cmake", @@ -409,9 +409,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", "bytes", @@ -472,9 +472,9 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76" +checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970" dependencies = [ "axum", "axum-core", @@ -595,20 +595,20 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" dependencies = [ "serde_core", ] [[package]] name = "bitstream-io" -version = "4.9.0" +version = "4.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" dependencies = [ - "core2", + "no_std_io2", ] [[package]] @@ -748,9 +748,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.59" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "jobserver", @@ -833,9 +833,9 @@ checksum = "bba18ee93d577a8428902687bcc2b6b45a56b1981a1f6d779731c86cc4c5db18" [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -855,9 +855,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -963,7 +963,7 @@ dependencies = [ "base64 0.22.1", "hkdf", "percent-encoding", - "rand 0.8.5", + "rand 0.8.6", "sha2", "subtle", "time", @@ -1014,15 +1014,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" -dependencies = [ - "memchr", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -1340,7 +1331,7 @@ dependencies = [ "matches", "model_derive", "openidconnect", - "rand 0.8.5", + "rand 0.8.6", "reqwest", "rsa", "secrecy", @@ -1401,7 +1392,7 @@ dependencies = [ "paste", "pgp", "prost", - "rand 0.8.5", + "rand 0.8.6", "regex", "reqwest", "rsa", @@ -1511,7 +1502,7 @@ dependencies = [ "clap", "defguard_common", "defguard_core", - "rand 0.8.5", + "rand 0.8.6", "sqlx", "tokio", "tracing", @@ -1592,7 +1583,7 @@ dependencies = [ "jsonwebkey", "jsonwebtoken", "openidconnect", - "rand 0.8.5", + "rand 0.8.6", "reqwest", "rsa", "semver", @@ -1863,7 +1854,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", ] @@ -2502,7 +2493,7 @@ version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "libc", "libgit2-sys", "log", @@ -2528,7 +2519,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "ignore", "walkdir", ] @@ -2782,9 +2773,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", @@ -2792,7 +2783,6 @@ dependencies = [ "log", "rustls", "rustls-native-certs", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -3167,9 +3157,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.94" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "cfg-if", "futures-util", @@ -3184,7 +3174,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba781c43eb46c3bbf5bfda541139eed9a52b78d7c3aa74d516918885ecd63c40" dependencies = [ "base64 0.22.1", - "bitflags 2.11.0", + "bitflags 2.11.1", "num-bigint", "serde", "serde_json", @@ -3207,7 +3197,7 @@ dependencies = [ "p256", "p384", "pem", - "rand 0.8.5", + "rand 0.8.6", "rsa", "serde", "serde_json", @@ -3322,9 +3312,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.184" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "libfuzzer-sys" @@ -3360,7 +3350,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "libc", "plain", "redox_syscall 0.7.4", @@ -3639,12 +3629,21 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "libc", ] +[[package]] +name = "no_std_io2" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b51ed7824b6e07d354605f4abb3d9d300350701299da96642ee084f5ce631550" +dependencies = [ + "memchr", +] + [[package]] name = "nom" version = "7.1.3" @@ -3706,7 +3705,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "serde", "smallvec", "zeroize", @@ -3811,7 +3810,7 @@ dependencies = [ "chrono", "getrandom 0.2.17", "http", - "rand 0.8.5", + "rand 0.8.6", "reqwest", "serde", "serde_json", @@ -3836,7 +3835,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", "objc2-foundation", ] @@ -3857,7 +3856,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "dispatch2", "objc2", ] @@ -3868,7 +3867,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "dispatch2", "objc2", "objc2-core-foundation", @@ -3901,7 +3900,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -3919,7 +3918,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2", "libc", "objc2", @@ -3932,7 +3931,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", "objc2-core-foundation", ] @@ -3943,7 +3942,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -3955,7 +3954,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "block2", "objc2", "objc2-cloud-kit", @@ -4045,7 +4044,7 @@ dependencies = [ "oauth2", "p256", "p384", - "rand 0.8.5", + "rand 0.8.6", "rsa", "serde", "serde-value", @@ -4061,11 +4060,11 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.76" +version = "0.10.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cfg-if", "foreign-types", "libc", @@ -4093,9 +4092,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.112" +version = "0.9.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" dependencies = [ "cc", "libc", @@ -4216,9 +4215,9 @@ dependencies = [ [[package]] name = "parse_link_header" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc23fdb8bbf668d582b0c17120bf6b7f91d85ccad3a5b39706f019a4efda005" +checksum = "4eb9c7a76731a2792f9b97fb34c1e774fff008badcd449d61fcdacb7a912392b" dependencies = [ "http", "lazy_static", @@ -4378,7 +4377,7 @@ dependencies = [ "p256", "p384", "p521", - "rand 0.8.5", + "rand 0.8.6", "regex", "replace_with", "ripemd", @@ -4422,7 +4421,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -4496,9 +4495,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plain" @@ -4512,7 +4511,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "crc32fast", "fdeflate", "flate2", @@ -4692,7 +4691,7 @@ version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c3a14896dfa883796f1cb410461aef38810ea05f2b2c33c5aded3649095fdad" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "getopts", "memchr", "pulldown-cmark-escape", @@ -4716,9 +4715,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" [[package]] name = "qoi" @@ -4773,7 +4772,7 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.2", + "rand 0.9.4", "ring", "rustc-hash", "rustls", @@ -4834,9 +4833,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -4845,9 +4844,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -4918,7 +4917,7 @@ dependencies = [ "num-traits", "paste", "profiling", - "rand 0.9.2", + "rand 0.9.4", "rand_chacha 0.9.0", "simd_helpers", "thiserror 2.0.18", @@ -4943,9 +4942,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -4981,7 +4980,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] @@ -4990,7 +4989,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] @@ -5233,7 +5232,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys", @@ -5242,9 +5241,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" dependencies = [ "aws-lc-rs", "log", @@ -5280,9 +5279,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" dependencies = [ "aws-lc-rs", "ring", @@ -5381,7 +5380,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -5661,9 +5660,9 @@ dependencies = [ [[package]] name = "sha3" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" dependencies = [ "digest", "keccak", @@ -5899,7 +5898,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.11.0", + "bitflags 2.11.1", "byteorder", "bytes", "chrono", @@ -5921,7 +5920,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand 0.8.5", + "rand 0.8.6", "rsa", "serde", "sha1", @@ -5943,7 +5942,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.11.0", + "bitflags 2.11.1", "byteorder", "chrono", "crc", @@ -5962,7 +5961,7 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand 0.8.5", + "rand 0.8.6", "serde", "serde_json", "sha2", @@ -6174,7 +6173,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -6233,7 +6232,7 @@ dependencies = [ "percent-encoding", "pest", "pest_derive", - "rand 0.8.5", + "rand 0.8.6", "regex", "serde", "serde_json", @@ -6373,9 +6372,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.51.1" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes", "libc", @@ -6594,7 +6593,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "bytes", "futures-core", "futures-util", @@ -6718,9 +6717,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "uaparser" @@ -6898,9 +6897,9 @@ checksum = "e2eebbbfe4093922c2b6734d7c679ebfebd704a0d7e56dfcb0d05818ce28977d" [[package]] name = "uuid" -version = "1.23.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -7002,11 +7001,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -7015,7 +7014,7 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] @@ -7026,9 +7025,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -7039,9 +7038,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.67" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ "js-sys", "wasm-bindgen", @@ -7049,9 +7048,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7059,9 +7058,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", @@ -7072,9 +7071,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.117" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -7120,7 +7119,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "hashbrown 0.15.5", "indexmap 2.14.0", "semver", @@ -7128,9 +7127,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.94" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -7232,7 +7231,7 @@ dependencies = [ "nom 7.1.3", "openssl", "openssl-sys", - "rand 0.9.2", + "rand 0.9.4", "rand_chacha 0.9.0", "serde", "serde_cbor_2", @@ -7261,9 +7260,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" dependencies = [ "rustls-pki-types", ] @@ -7603,6 +7602,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -7652,7 +7657,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", + "bitflags 2.11.1", "indexmap 2.14.0", "log", "serde", diff --git a/flake.lock b/flake.lock index d2092c130..924c8016e 100644 --- a/flake.lock +++ b/flake.lock @@ -32,11 +32,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1775423009, - "narHash": "sha256-vPKLpjhIVWdDrfiUM8atW6YkIggCEKdSAlJPzzhkQlw=", + "lastModified": 1776169885, + "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "68d8aa3d661f0e6bd5862291b5bb263b2a6595c9", + "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9", "type": "github" }, "original": { @@ -74,11 +74,11 @@ ] }, "locked": { - "lastModified": 1775617983, - "narHash": "sha256-2NWGA/I4j/qlx6qbg86QvJiK1/GyH9gnf0hFiARWVwE=", + "lastModified": 1776654897, + "narHash": "sha256-Vqi4AiJVCcBGn/RmBtRCgyH5rCxqm/w0xV9diJWF1Ic=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "d98b91b1feae7ef07fa2ccb3aa3f83f11abfae54", + "rev": "25d75be8139815a53560745fa060909777495105", "type": "github" }, "original": { diff --git a/web/package.json b/web/package.json index fb7361751..5817d00b7 100644 --- a/web/package.json +++ b/web/package.json @@ -18,18 +18,18 @@ "dependencies": { "@axa-ch/react-polymorphic-types": "^1.4.1", "@floating-ui/react": "^0.27.19", - "@inlang/paraglide-js": "^2.15.3", + "@inlang/paraglide-js": "^2.16.0", "@react-hook/resize-observer": "^2.0.2", "@shortercode/webzip": "1.1.1-0", "@stablelib/base64": "^2.0.1", "@stablelib/x25519": "^2.0.1", - "@tanstack/react-form": "^1.28.6", - "@tanstack/react-query": "^5.97.0", - "@tanstack/react-router": "^1.168.10", + "@tanstack/react-form": "^1.29.0", + "@tanstack/react-query": "^5.99.2", + "@tanstack/react-router": "^1.168.23", "@tanstack/react-table": "^8.21.3", - "@tanstack/react-virtual": "^3.13.23", + "@tanstack/react-virtual": "^3.13.24", "@uidotdev/usehooks": "^2.4.1", - "axios": "^1.15.0", + "axios": "^1.15.1", "byte-size": "^9.0.1", "clsx": "^2.1.1", "dayjs": "^1.11.20", @@ -58,9 +58,9 @@ "@inlang/paraglide-js": "2.15.0", "@tanstack/devtools-vite": "^0.6.0", "@tanstack/react-devtools": "^0.10.2", - "@tanstack/react-query-devtools": "^5.97.0", - "@tanstack/react-router-devtools": "^1.166.11", - "@tanstack/router-plugin": "^1.167.12", + "@tanstack/react-query-devtools": "^5.99.2", + "@tanstack/react-router-devtools": "^1.166.13", + "@tanstack/router-plugin": "^1.167.22", "@types/byte-size": "^8.1.2", "@types/humanize-duration": "^3.27.4", "@types/lodash-es": "^4.17.12", @@ -70,16 +70,16 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", "@vitest/ui": "^4.1.4", - "autoprefixer": "^10.4.27", - "globals": "^17.4.0", - "prettier": "^3.8.2", + "autoprefixer": "^10.5.0", + "globals": "^17.5.0", + "prettier": "^3.8.3", "sass": "^1.99.0", "sharp": "^0.34.5", - "stylelint": "^17.6.0", + "stylelint": "^17.8.0", "stylelint-config-standard-scss": "^17.0.0", "stylelint-scss": "^7.0.0", "typescript": "~5.9.3", - "vite": "^8.0.8", + "vite": "^8.0.9", "vite-plugin-image-optimizer": "^2.0.3", "vitest": "^4.1.4" } diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index b251f8678..2226555be 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: specifier: ^0.27.19 version: 0.27.19(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@inlang/paraglide-js': - specifier: ^2.15.3 - version: 2.15.3 + specifier: ^2.16.0 + version: 2.16.0 '@react-hook/resize-observer': specifier: ^2.0.2 version: 2.0.2(react@19.2.5) @@ -30,26 +30,26 @@ importers: specifier: ^2.0.1 version: 2.0.1 '@tanstack/react-form': - specifier: ^1.28.6 - version: 1.28.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: ^1.29.0 + version: 1.29.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tanstack/react-query': - specifier: ^5.97.0 - version: 5.97.0(react@19.2.5) + specifier: ^5.99.2 + version: 5.99.2(react@19.2.5) '@tanstack/react-router': - specifier: ^1.168.10 - version: 1.168.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: ^1.168.23 + version: 1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tanstack/react-virtual': - specifier: ^3.13.23 - version: 3.13.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: ^3.13.24 + version: 3.13.24(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@uidotdev/usehooks': specifier: ^2.4.1 version: 2.4.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) axios: - specifier: ^1.15.0 - version: 1.15.0 + specifier: ^1.15.1 + version: 1.15.1 byte-size: specifier: ^9.0.1 version: 9.0.1 @@ -122,19 +122,19 @@ importers: version: 2.4.7 '@tanstack/devtools-vite': specifier: ^0.6.0 - version: 0.6.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)) + version: 0.6.0(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)) '@tanstack/react-devtools': specifier: ^0.10.2 version: 0.10.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(solid-js@1.9.9) '@tanstack/react-query-devtools': - specifier: ^5.97.0 - version: 5.97.0(@tanstack/react-query@5.97.0(react@19.2.5))(react@19.2.5) + specifier: ^5.99.2 + version: 5.99.2(@tanstack/react-query@5.99.2(react@19.2.5))(react@19.2.5) '@tanstack/react-router-devtools': - specifier: ^1.166.11 - version: 1.166.11(@tanstack/react-router@1.168.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@tanstack/router-core@1.168.9)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + specifier: ^1.166.13 + version: 1.166.13(@tanstack/react-router@1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@tanstack/router-core@1.168.15)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tanstack/router-plugin': - specifier: ^1.167.12 - version: 1.167.12(@tanstack/react-router@1.168.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)) + specifier: ^1.167.22 + version: 1.167.22(@tanstack/react-router@1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)) '@types/byte-size': specifier: ^8.1.2 version: 8.1.2 @@ -158,19 +158,19 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)) + version: 6.0.1(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)) '@vitest/ui': specifier: ^4.1.4 version: 4.1.4(vitest@4.1.4) autoprefixer: - specifier: ^10.4.27 - version: 10.4.27(postcss@8.5.9) + specifier: ^10.5.0 + version: 10.5.0(postcss@8.5.10) globals: - specifier: ^17.4.0 - version: 17.4.0 + specifier: ^17.5.0 + version: 17.5.0 prettier: - specifier: ^3.8.2 - version: 3.8.2 + specifier: ^3.8.3 + version: 3.8.3 sass: specifier: ^1.99.0 version: 1.99.0 @@ -178,26 +178,26 @@ importers: specifier: ^0.34.5 version: 0.34.5 stylelint: - specifier: ^17.6.0 - version: 17.6.0(typescript@5.9.3) + specifier: ^17.8.0 + version: 17.8.0(typescript@5.9.3) stylelint-config-standard-scss: specifier: ^17.0.0 - version: 17.0.0(postcss@8.5.9)(stylelint@17.6.0(typescript@5.9.3)) + version: 17.0.0(postcss@8.5.10)(stylelint@17.8.0(typescript@5.9.3)) stylelint-scss: specifier: ^7.0.0 - version: 7.0.0(stylelint@17.6.0(typescript@5.9.3)) + version: 7.0.0(stylelint@17.8.0(typescript@5.9.3)) typescript: specifier: ~5.9.3 version: 5.9.3 vite: - specifier: ^8.0.8 - version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0) + specifier: ^8.0.9 + version: 8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0) vite-plugin-image-optimizer: specifier: ^2.0.3 - version: 2.0.3(sharp@0.34.5)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)) + version: 2.0.3(sharp@0.34.5)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)) vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@25.6.0)(@vitest/ui@4.1.4)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)) + version: 4.1.4(@types/node@25.6.0)(@vitest/ui@4.1.4)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)) packages: @@ -353,8 +353,8 @@ packages: '@cacheable/utils@2.4.1': resolution: {integrity: sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==} - '@csstools/css-calc@3.1.1': - resolution: {integrity: sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==} + '@csstools/css-calc@3.2.0': + resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==} engines: {node: '>=20.19.0'} peerDependencies: '@csstools/css-parser-algorithms': ^4.0.0 @@ -366,8 +366,8 @@ packages: peerDependencies: '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.1.2': - resolution: {integrity: sha512-5GkLzz4prTIpoyeUiIu3iV6CSG3Plo7xRVOFPKI7FVEJ3mZ0A8SwK0XU3Gl7xAkiQ+mDyam+NNp875/C5y+jSA==} + '@csstools/css-syntax-patches-for-csstree@1.1.3': + resolution: {integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==} peerDependencies: css-tree: ^3.2.1 peerDependenciesMeta: @@ -400,6 +400,9 @@ packages: '@emnapi/core@1.9.2': resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.9.2': resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} @@ -736,8 +739,8 @@ packages: cpu: [x64] os: [win32] - '@inlang/paraglide-js@2.15.3': - resolution: {integrity: sha512-gneANUhYEPnSjxbKp3QCwmMqQecG+1QWuJSAl3jiPprn2+LeaZu3BgnofRKpo8gkYzB6oE3AY2ecZBXu3UrpOw==} + '@inlang/paraglide-js@2.16.0': + resolution: {integrity: sha512-O7KKvVoTsGqPRt1VfSvd0UyfSjU2qHiABx968M2decgG7Af6TddW3dTJrTS3I78nOUgRAlYwCYfKefSGD4rGMA==} hasBin: true '@inlang/recommend-sherlock@0.2.1': @@ -779,8 +782,8 @@ packages: '@lix-js/server-protocol-schema@0.1.1': resolution: {integrity: sha512-jBeALB6prAbtr5q4vTuxnRZZv1M2rKe8iNqRQhFJ4Tv7150unEa0vKyz0hs8Gl3fUGsWaNJBh3J8++fpbrpRBQ==} - '@napi-rs/wasm-runtime@1.1.3': - resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==} + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 @@ -797,8 +800,8 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@oxc-project/types@0.124.0': - resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} + '@oxc-project/types@0.126.0': + resolution: {integrity: sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==} '@parcel/watcher-android-arm64@2.5.6': resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} @@ -917,103 +920,103 @@ packages: react-redux: optional: true - '@rolldown/binding-android-arm64@1.0.0-rc.15': - resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==} + '@rolldown/binding-android-arm64@1.0.0-rc.16': + resolution: {integrity: sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.15': - resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==} + '@rolldown/binding-darwin-arm64@1.0.0-rc.16': + resolution: {integrity: sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.15': - resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==} + '@rolldown/binding-darwin-x64@1.0.0-rc.16': + resolution: {integrity: sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.15': - resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==} + '@rolldown/binding-freebsd-x64@1.0.0-rc.16': + resolution: {integrity: sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': - resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16': + resolution: {integrity: sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': - resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.16': + resolution: {integrity: sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': - resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==} + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': - resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} + '@rolldown/binding-linux-x64-musl@1.0.0-rc.16': + resolution: {integrity: sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': - resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} + '@rolldown/binding-openharmony-arm64@1.0.0-rc.16': + resolution: {integrity: sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': - resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==} - engines: {node: '>=14.0.0'} + '@rolldown/binding-wasm32-wasi@1.0.0-rc.16': + resolution: {integrity: sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': - resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16': + resolution: {integrity: sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': - resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==} + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.16': + resolution: {integrity: sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-rc.15': - resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} + '@rolldown/pluginutils@1.0.0-rc.16': + resolution: {integrity: sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==} '@rolldown/pluginutils@1.0.0-rc.7': resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} @@ -1125,8 +1128,8 @@ packages: peerDependencies: solid-js: '>=1.9.7' - '@tanstack/form-core@1.28.6': - resolution: {integrity: sha512-4zroxL6VDj5O+w7l3dYZnUeL/h30KtNSV7UWzKAL7cl+8clMFdISPDlDlluS37As7oqvPVKo8B83VlIBvgmRog==} + '@tanstack/form-core@1.29.0': + resolution: {integrity: sha512-uyeKEdJBfbj0bkBSwvSYVRtWLOaXvfNX3CeVw1HqGOXVLxpBBGAqWdYLc+UoX/9xcoFwFXrjR9QqMPzvwm2yyQ==} '@tanstack/history@1.161.6': resolution: {integrity: sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg==} @@ -1136,11 +1139,11 @@ packages: resolution: {integrity: sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w==} engines: {node: '>=18'} - '@tanstack/query-core@5.97.0': - resolution: {integrity: sha512-QdpLP5VzVMgo4VtaPppRA2W04UFjIqX+bxke/ZJhE5cfd5UPkRzqIAJQt9uXkQJjqE8LBOMbKv7f8HCsZltXlg==} + '@tanstack/query-core@5.99.2': + resolution: {integrity: sha512-1HunU0bXVsR1ZJMZbcOPE6VtaBJxsW809RE9xPe4Gz7MlB0GWwQvuTPhMoEmQ/hIzFKJ/DWAuttIe7BOaWx0tA==} - '@tanstack/query-devtools@5.97.0': - resolution: {integrity: sha512-ZMjAuYhQCKwKLKFMrD+HJDehHwWBVTGOuWBf4vEjR9unO+UGUjQ1mw2TuVbQKoLN/eRwB7qtlPsWBqobBoRBMQ==} + '@tanstack/query-devtools@5.99.2': + resolution: {integrity: sha512-TEF1d+RYO9l8oeCwgzmOHIgKwAzXQmw2s/ny2bW8qeg2OMkkLjALfVEivgCMR3OL/jVdMmeTPX56WrV+uvYJFg==} '@tanstack/react-devtools@0.10.2': resolution: {integrity: sha512-1BmZyxOrI5SqmRJ5MgkYZNNdnlLsJxQRI2YgorrAvcF2MxK6x5RcuStvD8+YlXoMw3JtNukPxoITirKAnKYDQA==} @@ -1151,8 +1154,8 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-form@1.28.6': - resolution: {integrity: sha512-dRxwKeNW3uuJvf0sXsIQ2compFMnIJNk9B436Lx0fqkqK+CBvA1tNmEdX+faoCpuQ5Wua3c8ahVibJ65cpkijA==} + '@tanstack/react-form@1.29.0': + resolution: {integrity: sha512-jj425NNX0QKqbUzqSNiYI3HCPHSk2df47acXCJyXczWOTmG81ECZGkgofgqamFsSU9kMiH6Di5RLUnftrlhWSw==} peerDependencies: '@tanstack/react-start': '*' react: ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1160,31 +1163,31 @@ packages: '@tanstack/react-start': optional: true - '@tanstack/react-query-devtools@5.97.0': - resolution: {integrity: sha512-X4/VZKCbBIRj8cVD/oZCKTwwPmFXrY1VOfwUT5qI/+/JZYAUS+8vGNMqwBXbaAu1ZsVzzDzkT/wtBE/5OtQYGg==} + '@tanstack/react-query-devtools@5.99.2': + resolution: {integrity: sha512-8txkK9A9XBNTB8RoxVgfp6W3qwBr25tNP10L4yu3KuyhAdEvccECfIRzesSwMVk/wpVVioAr+hbMtUkMMF+WVw==} peerDependencies: - '@tanstack/react-query': ^5.97.0 + '@tanstack/react-query': ^5.99.2 react: ^18 || ^19 - '@tanstack/react-query@5.97.0': - resolution: {integrity: sha512-y4So4eGcQoK2WVMAcDNZE9ofB/p5v1OlKvtc1F3uqHwrtifobT7q+ZnXk2mRkc8E84HKYSlAE9z6HXl2V0+ySQ==} + '@tanstack/react-query@5.99.2': + resolution: {integrity: sha512-vM91UEe45QUS9ED6OklsVL15i8qKcRqNwpWzPTVWvRPRSEgDudDgHpvyTjcdlwHcrKNa80T+xXYcchT2noPnZA==} peerDependencies: react: ^18 || ^19 - '@tanstack/react-router-devtools@1.166.11': - resolution: {integrity: sha512-WYR3q4Xui5yPT/5PXtQh8i03iUA7q8dONBjWpV3nsGdM8Cs1FxpfhLstW0wZO1dOvSyElscwTRCJ6nO5N8r3Lg==} + '@tanstack/react-router-devtools@1.166.13': + resolution: {integrity: sha512-6yKRFFJrEEOiGp5RAAuGCYsl81M4XAhJmLcu9PKj+HZle4A3dsP60lwHoqQYWHMK9nKKFkdXR+D8qxzxqtQbEA==} engines: {node: '>=20.19'} peerDependencies: - '@tanstack/react-router': ^1.168.2 - '@tanstack/router-core': ^1.168.2 + '@tanstack/react-router': ^1.168.15 + '@tanstack/router-core': ^1.168.11 react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' peerDependenciesMeta: '@tanstack/router-core': optional: true - '@tanstack/react-router@1.168.10': - resolution: {integrity: sha512-/RmDlOwDkCug609KdPB3U+U1zmrtadJpvsmRg2zEn8TRCKRNri7dYZIjQZbNg8PgUiRL4T6njrZBV1ChzblNaA==} + '@tanstack/react-router@1.168.23': + resolution: {integrity: sha512-+GblieDnutG6oipJJPNtRJjrWF8QTZEG/l0532+BngFkVK48oHNOcvIkSoAFYftK1egAwM7KBxXsb0Ou+X6/MQ==} engines: {node: '>=20.19'} peerDependencies: react: '>=18.0.0 || >=19.0.0' @@ -1203,40 +1206,40 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-virtual@3.13.23': - resolution: {integrity: sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ==} + '@tanstack/react-virtual@3.13.24': + resolution: {integrity: sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/router-core@1.168.9': - resolution: {integrity: sha512-18oeEwEDyXOIuO1VBP9ACaK7tYHZUjynGDCoUh/5c/BNhia9vCJCp9O0LfhZXOorDc/PmLSgvmweFhVmIxF10g==} + '@tanstack/router-core@1.168.15': + resolution: {integrity: sha512-Wr0424NDtD8fT/uALobMZ9DdcfsTyXtW5IPR++7zvW8/7RaIOeaqXpVDId8ywaGtqPWLWOfaUg2zUtYtukoXYA==} engines: {node: '>=20.19'} hasBin: true - '@tanstack/router-devtools-core@1.167.1': - resolution: {integrity: sha512-ECMM47J4KmifUvJguGituSiBpfN8SyCUEoxQks5RY09hpIBfR2eswCv2e6cJimjkKwBQXOVTPkTUk/yRvER+9w==} + '@tanstack/router-devtools-core@1.167.3': + resolution: {integrity: sha512-fJ1VMhyQgnoashTrP763c2HRc9kofgF61L7Jb3F6eTHAmCKtGVx8BRtiFt37sr3U0P0jmaaiiSPGP6nT5JtVNg==} engines: {node: '>=20.19'} peerDependencies: - '@tanstack/router-core': ^1.168.2 + '@tanstack/router-core': ^1.168.11 csstype: ^3.0.10 peerDependenciesMeta: csstype: optional: true - '@tanstack/router-generator@1.166.24': - resolution: {integrity: sha512-vdaGKwuH+r+DPe6R1mjk+TDDmDH6NTG7QqwxHqGEvOH4aGf9sPjhmRKNJZqQr8cPIbfp6u5lXyZ1TeDcSNMVEA==} + '@tanstack/router-generator@1.166.32': + resolution: {integrity: sha512-VuusKwEXcgKq+myq1JQfZogY8scTXIIeFls50dJ/UXgCXWp5n14iFreYNlg41wURcak2oA3M+t2TVfD0xUUD6g==} engines: {node: '>=20.19'} - '@tanstack/router-plugin@1.167.12': - resolution: {integrity: sha512-StEHcctCuFI5taSjO+lhR/yQ+EK63BdyYa+ne6FoNQPB3MMrOUrz2ZVnbqILRLkh2b+p2EfBKt65sgAKdKygPQ==} + '@tanstack/router-plugin@1.167.22': + resolution: {integrity: sha512-wYPzIvBK8bcmXVUpZfSgGBXOrfBAdF4odKevz6rejio5rEd947NtKDF5R7eYdwlAOmRqYpLJnJ1QHkc5t8bY4w==} engines: {node: '>=20.19'} hasBin: true peerDependencies: '@rsbuild/core': '>=1.0.2' - '@tanstack/react-router': ^1.168.10 - vite: '>=5.0.0 || >=6.0.0 || >=7.0.0' - vite-plugin-solid: ^2.11.10 + '@tanstack/react-router': ^1.168.21 + vite: '>=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0' + vite-plugin-solid: ^2.11.10 || ^3.0.0-0 webpack: '>=5.92.0' peerDependenciesMeta: '@rsbuild/core': @@ -1261,8 +1264,8 @@ packages: resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} - '@tanstack/virtual-core@3.13.23': - resolution: {integrity: sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==} + '@tanstack/virtual-core@3.14.0': + resolution: {integrity: sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q==} '@tanstack/virtual-file-routes@1.161.7': resolution: {integrity: sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ==} @@ -1457,10 +1460,6 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - ast-types@0.16.1: - resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} - engines: {node: '>=4'} - astral-regex@2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} @@ -1468,15 +1467,15 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - autoprefixer@10.4.27: - resolution: {integrity: sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==} + autoprefixer@10.5.0: + resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: postcss: ^8.1.0 - axios@1.15.0: - resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==} + axios@1.15.1: + resolution: {integrity: sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg==} babel-dead-code-elimination@1.0.12: resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} @@ -1484,8 +1483,8 @@ packages: bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} - baseline-browser-mapping@2.10.17: - resolution: {integrity: sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA==} + baseline-browser-mapping@2.10.20: + resolution: {integrity: sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -1526,8 +1525,8 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - caniuse-lite@1.0.30001787: - resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==} + caniuse-lite@1.0.30001788: + resolution: {integrity: sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -1596,8 +1595,8 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-es@2.0.1: - resolution: {integrity: sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA==} + cookie-es@3.1.1: + resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} cosmiconfig@9.0.1: resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} @@ -1720,8 +1719,8 @@ packages: easy-file-picker@1.2.0: resolution: {integrity: sha512-GJxOW5s+g/pBr8Ha86a768yx0UZ6fYw+iAOrxK5HOzQ8q9hZxEJF0C8ztdAsH0mcze58FSpzv/d9flRCAuUKHg==} - electron-to-chromium@1.5.334: - resolution: {integrity: sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==} + electron-to-chromium@1.5.340: + resolution: {integrity: sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1831,8 +1830,8 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - follow-redirects@1.15.11: - resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} engines: {node: '>=4.0'} peerDependencies: debug: '*' @@ -1885,8 +1884,8 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - get-tsconfig@4.13.7: - resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -1900,8 +1899,8 @@ packages: resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} engines: {node: '>=6'} - globals@17.4.0: - resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==} + globals@17.5.0: + resolution: {integrity: sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==} engines: {node: '>=18'} globby@16.2.0: @@ -1936,8 +1935,8 @@ packages: resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} engines: {node: '>=20'} - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} hast-util-from-parse5@8.0.3: @@ -2065,8 +2064,8 @@ packages: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} - isbot@5.1.37: - resolution: {integrity: sha512-5bcicX81xf6NlTEV8rWdg7Pk01LFizDetuYGHx6d/f6y3lR2/oo8IfxjzJqn1UdDEyCcwT9e7NRloj8DwCYujQ==} + isbot@5.1.39: + resolution: {integrity: sha512-obH0yYahGXdzNxo+djmHhBYThUKDkz565cxkIlt2L9hXfv1NlaLKoDBHo6KxXsYrIXx2RK3x5vY36CfZcobxEw==} engines: {node: '>=18'} isexe@2.0.0: @@ -2108,8 +2107,8 @@ packages: known-css-properties@0.37.0: resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} - kysely@0.28.15: - resolution: {integrity: sha512-r2clcf7HLWvDXaVUEvQymXJY4i3bSOIV3xsL/Upy3ZfSv5HeKsk9tsqbBptLvth5qHEIhxeHTA2jNLyQABkLBA==} + kysely@0.28.16: + resolution: {integrity: sha512-3i5pmOiZvMDj00qhrIVbH0AnioVTx22DMP7Vn5At4yJO46iy+FM8Y/g61ltenLVSo3fiO8h8Q3QOFgf/gQ72ww==} engines: {node: '>=20.0.0'} launch-editor@2.13.2: @@ -2241,6 +2240,9 @@ packages: mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + mdn-data@2.28.0: + resolution: {integrity: sha512-uy9AS1yt+wW5eUEefgE3lOpqPghanUttycV0GXKbiXyBjwvbeE8XPj4u1C+voRfz7dEjwU4NDHTMfZ/s/JtZrQ==} + meow@14.1.0: resolution: {integrity: sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==} engines: {node: '>=20'} @@ -2426,12 +2428,12 @@ packages: postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.5.9: - resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} + postcss@8.5.10: + resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} engines: {node: ^10 || ^12 || >=14} - prettier@3.8.2: - resolution: {integrity: sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==} + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} engines: {node: '>=14'} hasBin: true @@ -2514,10 +2516,6 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - recast@0.23.11: - resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} - engines: {node: '>= 4'} - recharts@3.8.1: resolution: {integrity: sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==} engines: {node: '>=18'} @@ -2561,8 +2559,8 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rolldown@1.0.0-rc.15: - resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==} + rolldown@1.0.0-rc.16: + resolution: {integrity: sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -2659,14 +2657,6 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - - source-map@0.7.6: - resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} - engines: {node: '>= 12'} - space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -2678,8 +2668,8 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@4.0.0: - resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} @@ -2744,8 +2734,8 @@ packages: peerDependencies: stylelint: ^16.8.2 || ^17.0.0 - stylelint@17.6.0: - resolution: {integrity: sha512-tokrsMIVAR9vAQ/q3UVEr7S0dGXCi7zkCezPRnS2kqPUulvUh5Vgfwngrk4EoAoW7wnrThqTdnTFN5Ra7CaxIg==} + stylelint@17.8.0: + resolution: {integrity: sha512-oHkld9T60LDSaUQ4CSVc+tlt9eUoDlxhaGWShsUCKyIL14boZfmK5bSphZqx64aiC5tCqX+BsQMTMoSz8D1zIg==} engines: {node: '>=20.19.0'} hasBin: true @@ -2954,8 +2944,8 @@ packages: svgo: optional: true - vite@8.0.8: - resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} + vite@8.0.9: + resolution: {integrity: sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -3265,7 +3255,7 @@ snapshots: hashery: 1.5.1 keyv: 5.6.0 - '@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + '@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 @@ -3274,7 +3264,7 @@ snapshots: dependencies: '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.1.2(css-tree@3.2.1)': + '@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)': optionalDependencies: css-tree: 3.2.1 @@ -3299,6 +3289,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.9.2': dependencies: tslib: 2.8.1 @@ -3496,7 +3491,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.9.2 + '@emnapi/runtime': 1.10.0 optional: true '@img/sharp-win32-arm64@0.34.5': @@ -3508,7 +3503,7 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true - '@inlang/paraglide-js@2.15.3': + '@inlang/paraglide-js@2.16.0': dependencies: '@inlang/recommend-sherlock': 0.2.1 '@inlang/sdk': 2.9.1 @@ -3528,8 +3523,8 @@ snapshots: dependencies: '@lix-js/sdk': 0.4.9 '@sinclair/typebox': 0.31.28 - kysely: 0.28.15 - sqlite-wasm-kysely: 0.3.0(kysely@0.28.15) + kysely: 0.28.16 + sqlite-wasm-kysely: 0.3.0(kysely@0.28.16) uuid: 13.0.0 transitivePeerDependencies: - babel-plugin-macros @@ -3567,15 +3562,15 @@ snapshots: dedent: 1.5.1 human-id: 4.1.3 js-sha256: 0.11.1 - kysely: 0.28.15 - sqlite-wasm-kysely: 0.3.0(kysely@0.28.15) + kysely: 0.28.16 + sqlite-wasm-kysely: 0.3.0(kysely@0.28.16) uuid: 10.0.0 transitivePeerDependencies: - babel-plugin-macros '@lix-js/server-protocol-schema@0.1.1': {} - '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: '@emnapi/core': 1.9.2 '@emnapi/runtime': 1.9.2 @@ -3594,7 +3589,7 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@oxc-project/types@0.124.0': {} + '@oxc-project/types@0.126.0': {} '@parcel/watcher-android-arm64@2.5.6': optional: true @@ -3685,56 +3680,56 @@ snapshots: react: 19.2.5 react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1) - '@rolldown/binding-android-arm64@1.0.0-rc.15': + '@rolldown/binding-android-arm64@1.0.0-rc.16': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + '@rolldown/binding-darwin-arm64@1.0.0-rc.16': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.15': + '@rolldown/binding-darwin-x64@1.0.0-rc.16': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + '@rolldown/binding-freebsd-x64@1.0.0-rc.16': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.16': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.16': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + '@rolldown/binding-linux-x64-musl@1.0.0-rc.16': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + '@rolldown/binding-openharmony-arm64@1.0.0-rc.16': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.16': dependencies: '@emnapi/core': 1.9.2 '@emnapi/runtime': 1.9.2 - '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.16': optional: true - '@rolldown/pluginutils@1.0.0-rc.15': {} + '@rolldown/pluginutils@1.0.0-rc.16': {} '@rolldown/pluginutils@1.0.0-rc.7': {} @@ -3833,7 +3828,7 @@ snapshots: transitivePeerDependencies: - csstype - '@tanstack/devtools-vite@0.6.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0))': + '@tanstack/devtools-vite@0.6.0(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0))': dependencies: '@babel/core': 7.29.0 '@babel/generator': 7.29.1 @@ -3845,7 +3840,7 @@ snapshots: chalk: 5.6.2 launch-editor: 2.13.2 picomatch: 4.0.4 - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0) + vite: 8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0) transitivePeerDependencies: - bufferutil - supports-color @@ -3867,7 +3862,7 @@ snapshots: - csstype - utf-8-validate - '@tanstack/form-core@1.28.6': + '@tanstack/form-core@1.29.0': dependencies: '@tanstack/devtools-event-client': 0.4.3 '@tanstack/pacer-lite': 0.1.1 @@ -3877,9 +3872,9 @@ snapshots: '@tanstack/pacer-lite@0.1.1': {} - '@tanstack/query-core@5.97.0': {} + '@tanstack/query-core@5.99.2': {} - '@tanstack/query-devtools@5.97.0': {} + '@tanstack/query-devtools@5.99.2': {} '@tanstack/react-devtools@0.10.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(solid-js@1.9.9)': dependencies: @@ -3894,42 +3889,42 @@ snapshots: - solid-js - utf-8-validate - '@tanstack/react-form@1.28.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@tanstack/react-form@1.29.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@tanstack/form-core': 1.28.6 + '@tanstack/form-core': 1.29.0 '@tanstack/react-store': 0.9.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react: 19.2.5 transitivePeerDependencies: - react-dom - '@tanstack/react-query-devtools@5.97.0(@tanstack/react-query@5.97.0(react@19.2.5))(react@19.2.5)': + '@tanstack/react-query-devtools@5.99.2(@tanstack/react-query@5.99.2(react@19.2.5))(react@19.2.5)': dependencies: - '@tanstack/query-devtools': 5.97.0 - '@tanstack/react-query': 5.97.0(react@19.2.5) + '@tanstack/query-devtools': 5.99.2 + '@tanstack/react-query': 5.99.2(react@19.2.5) react: 19.2.5 - '@tanstack/react-query@5.97.0(react@19.2.5)': + '@tanstack/react-query@5.99.2(react@19.2.5)': dependencies: - '@tanstack/query-core': 5.97.0 + '@tanstack/query-core': 5.99.2 react: 19.2.5 - '@tanstack/react-router-devtools@1.166.11(@tanstack/react-router@1.168.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@tanstack/router-core@1.168.9)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@tanstack/react-router-devtools@1.166.13(@tanstack/react-router@1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@tanstack/router-core@1.168.15)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@tanstack/react-router': 1.168.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@tanstack/router-devtools-core': 1.167.1(@tanstack/router-core@1.168.9)(csstype@3.2.3) + '@tanstack/react-router': 1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@tanstack/router-devtools-core': 1.167.3(@tanstack/router-core@1.168.15)(csstype@3.2.3) react: 19.2.5 react-dom: 19.2.5(react@19.2.5) optionalDependencies: - '@tanstack/router-core': 1.168.9 + '@tanstack/router-core': 1.168.15 transitivePeerDependencies: - csstype - '@tanstack/react-router@1.168.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@tanstack/react-router@1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@tanstack/history': 1.161.6 '@tanstack/react-store': 0.9.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@tanstack/router-core': 1.168.9 - isbot: 5.1.37 + '@tanstack/router-core': 1.168.15 + isbot: 5.1.39 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -3946,41 +3941,41 @@ snapshots: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - '@tanstack/react-virtual@3.13.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@tanstack/react-virtual@3.13.24(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@tanstack/virtual-core': 3.13.23 + '@tanstack/virtual-core': 3.14.0 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - '@tanstack/router-core@1.168.9': + '@tanstack/router-core@1.168.15': dependencies: '@tanstack/history': 1.161.6 - cookie-es: 2.0.1 + cookie-es: 3.1.1 seroval: 1.5.2 seroval-plugins: 1.5.2(seroval@1.5.2) - '@tanstack/router-devtools-core@1.167.1(@tanstack/router-core@1.168.9)(csstype@3.2.3)': + '@tanstack/router-devtools-core@1.167.3(@tanstack/router-core@1.168.15)(csstype@3.2.3)': dependencies: - '@tanstack/router-core': 1.168.9 + '@tanstack/router-core': 1.168.15 clsx: 2.1.1 goober: 2.1.18(csstype@3.2.3) optionalDependencies: csstype: 3.2.3 - '@tanstack/router-generator@1.166.24': + '@tanstack/router-generator@1.166.32': dependencies: - '@tanstack/router-core': 1.168.9 + '@babel/types': 7.29.0 + '@tanstack/router-core': 1.168.15 '@tanstack/router-utils': 1.161.6 '@tanstack/virtual-file-routes': 1.161.7 - prettier: 3.8.2 - recast: 0.23.11 - source-map: 0.7.6 + magic-string: 0.30.21 + prettier: 3.8.3 tsx: 4.21.0 zod: 3.25.76 transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.167.12(@tanstack/react-router@1.168.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0))': + '@tanstack/router-plugin@1.167.22(@tanstack/react-router@1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) @@ -3988,16 +3983,16 @@ snapshots: '@babel/template': 7.28.6 '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 - '@tanstack/router-core': 1.168.9 - '@tanstack/router-generator': 1.166.24 + '@tanstack/router-core': 1.168.15 + '@tanstack/router-generator': 1.166.32 '@tanstack/router-utils': 1.161.6 '@tanstack/virtual-file-routes': 1.161.7 chokidar: 3.6.0 unplugin: 2.3.11 zod: 3.25.76 optionalDependencies: - '@tanstack/react-router': 1.168.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0) + '@tanstack/react-router': 1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + vite: 8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0) transitivePeerDependencies: - supports-color @@ -4019,7 +4014,7 @@ snapshots: '@tanstack/table-core@8.21.3': {} - '@tanstack/virtual-core@3.13.23': {} + '@tanstack/virtual-core@3.14.0': {} '@tanstack/virtual-file-routes@1.161.7': {} @@ -4116,10 +4111,10 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@6.0.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0))': + '@vitejs/plugin-react@6.0.1(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0) + vite: 8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0) '@vitest/expect@4.1.4': dependencies: @@ -4130,13 +4125,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0))': + '@vitest/mocker@4.1.4(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0))': dependencies: '@vitest/spy': 4.1.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0) + vite: 8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0) '@vitest/pretty-format@4.1.4': dependencies: @@ -4165,7 +4160,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vitest: 4.1.4(@types/node@25.6.0)(@vitest/ui@4.1.4)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)) + vitest: 4.1.4(@types/node@25.6.0)(@vitest/ui@4.1.4)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)) '@vitest/utils@4.1.4': dependencies: @@ -4205,26 +4200,22 @@ snapshots: assertion-error@2.0.1: {} - ast-types@0.16.1: - dependencies: - tslib: 2.8.1 - astral-regex@2.0.0: {} asynckit@0.4.0: {} - autoprefixer@10.4.27(postcss@8.5.9): + autoprefixer@10.5.0(postcss@8.5.10): dependencies: browserslist: 4.28.2 - caniuse-lite: 1.0.30001787 + caniuse-lite: 1.0.30001788 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.5.9 + postcss: 8.5.10 postcss-value-parser: 4.2.0 - axios@1.15.0: + axios@1.15.1: dependencies: - follow-redirects: 1.15.11 + follow-redirects: 1.16.0 form-data: 4.0.5 proxy-from-env: 2.1.0 transitivePeerDependencies: @@ -4241,7 +4232,7 @@ snapshots: bail@2.0.2: {} - baseline-browser-mapping@2.10.17: {} + baseline-browser-mapping@2.10.20: {} binary-extensions@2.3.0: {} @@ -4251,9 +4242,9 @@ snapshots: browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.10.17 - caniuse-lite: 1.0.30001787 - electron-to-chromium: 1.5.334 + baseline-browser-mapping: 2.10.20 + caniuse-lite: 1.0.30001788 + electron-to-chromium: 1.5.340 node-releases: 2.0.37 update-browserslist-db: 1.2.3(browserslist@4.28.2) @@ -4279,7 +4270,7 @@ snapshots: callsites@3.1.0: {} - caniuse-lite@1.0.30001787: {} + caniuse-lite@1.0.30001788: {} ccount@2.0.1: {} @@ -4338,7 +4329,7 @@ snapshots: convert-source-map@2.0.0: {} - cookie-es@2.0.1: {} + cookie-es@3.1.1: {} cosmiconfig@9.0.1(typescript@5.9.3): dependencies: @@ -4432,7 +4423,7 @@ snapshots: easy-file-picker@1.2.0: {} - electron-to-chromium@1.5.334: {} + electron-to-chromium@1.5.340: {} emoji-regex@8.0.0: {} @@ -4459,7 +4450,7 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 has-tostringtag: 1.0.2 - hasown: 2.0.2 + hasown: 2.0.3 es-toolkit@1.45.1: {} @@ -4548,14 +4539,14 @@ snapshots: flatted@3.4.2: {} - follow-redirects@1.15.11: {} + follow-redirects@1.16.0: {} form-data@4.0.5: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 - hasown: 2.0.2 + hasown: 2.0.3 mime-types: 2.1.35 fraction.js@5.3.4: {} @@ -4588,7 +4579,7 @@ snapshots: get-proto: 1.0.1 gopd: 1.2.0 has-symbols: 1.1.0 - hasown: 2.0.2 + hasown: 2.0.3 math-intrinsics: 1.1.0 get-proto@1.0.1: @@ -4596,7 +4587,7 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - get-tsconfig@4.13.7: + get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -4614,7 +4605,7 @@ snapshots: kind-of: 6.0.3 which: 1.3.1 - globals@17.4.0: {} + globals@17.5.0: {} globby@16.2.0: dependencies: @@ -4645,7 +4636,7 @@ snapshots: dependencies: hookified: 1.15.1 - hasown@2.0.2: + hasown@2.0.3: dependencies: function-bind: 1.1.2 @@ -4792,7 +4783,7 @@ snapshots: is-plain-object@5.0.0: {} - isbot@5.1.37: {} + isbot@5.1.39: {} isexe@2.0.0: {} @@ -4820,7 +4811,7 @@ snapshots: known-css-properties@0.37.0: {} - kysely@0.28.15: {} + kysely@0.28.16: {} launch-editor@2.13.2: dependencies: @@ -4987,6 +4978,8 @@ snapshots: mdn-data@2.27.1: {} + mdn-data@2.28.0: {} + meow@14.1.0: {} merge2@1.4.1: {} @@ -5203,13 +5196,13 @@ snapshots: postcss-resolve-nested-selector@0.1.6: {} - postcss-safe-parser@7.0.1(postcss@8.5.9): + postcss-safe-parser@7.0.1(postcss@8.5.10): dependencies: - postcss: 8.5.9 + postcss: 8.5.10 - postcss-scss@4.0.9(postcss@8.5.9): + postcss-scss@4.0.9(postcss@8.5.10): dependencies: - postcss: 8.5.9 + postcss: 8.5.10 postcss-selector-parser@7.1.1: dependencies: @@ -5218,13 +5211,13 @@ snapshots: postcss-value-parser@4.2.0: {} - postcss@8.5.9: + postcss@8.5.10: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 - prettier@3.8.2: {} + prettier@3.8.3: {} property-information@7.1.0: {} @@ -5298,14 +5291,6 @@ snapshots: readdirp@4.1.2: {} - recast@0.23.11: - dependencies: - ast-types: 0.16.1 - esprima: 4.0.1 - source-map: 0.6.1 - tiny-invariant: 1.3.3 - tslib: 2.8.1 - recharts@3.8.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react-is@19.2.0)(react@19.2.5)(redux@5.0.1): dependencies: '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1))(react@19.2.5) @@ -5365,26 +5350,26 @@ snapshots: reusify@1.1.0: {} - rolldown@1.0.0-rc.15: + rolldown@1.0.0-rc.16: dependencies: - '@oxc-project/types': 0.124.0 - '@rolldown/pluginutils': 1.0.0-rc.15 + '@oxc-project/types': 0.126.0 + '@rolldown/pluginutils': 1.0.0-rc.16 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.15 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.15 - '@rolldown/binding-darwin-x64': 1.0.0-rc.15 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.15 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.15 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.15 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.15 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 + '@rolldown/binding-android-arm64': 1.0.0-rc.16 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.16 + '@rolldown/binding-darwin-x64': 1.0.0-rc.16 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.16 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.16 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.16 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.16 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.16 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.16 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.16 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.16 run-parallel@1.2.0: dependencies: @@ -5507,20 +5492,16 @@ snapshots: source-map-js@1.2.1: {} - source-map@0.6.1: {} - - source-map@0.7.6: {} - space-separated-tokens@2.0.2: {} - sqlite-wasm-kysely@0.3.0(kysely@0.28.15): + sqlite-wasm-kysely@0.3.0(kysely@0.28.16): dependencies: '@sqlite.org/sqlite-wasm': 3.48.0-build4 - kysely: 0.28.15 + kysely: 0.28.16 stackback@0.0.2: {} - std-env@4.0.0: {} + std-env@4.1.0: {} string-width@4.2.3: dependencies: @@ -5554,49 +5535,49 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - stylelint-config-recommended-scss@17.0.1(postcss@8.5.9)(stylelint@17.6.0(typescript@5.9.3)): + stylelint-config-recommended-scss@17.0.1(postcss@8.5.10)(stylelint@17.8.0(typescript@5.9.3)): dependencies: - postcss-scss: 4.0.9(postcss@8.5.9) - stylelint: 17.6.0(typescript@5.9.3) - stylelint-config-recommended: 18.0.0(stylelint@17.6.0(typescript@5.9.3)) - stylelint-scss: 7.0.0(stylelint@17.6.0(typescript@5.9.3)) + postcss-scss: 4.0.9(postcss@8.5.10) + stylelint: 17.8.0(typescript@5.9.3) + stylelint-config-recommended: 18.0.0(stylelint@17.8.0(typescript@5.9.3)) + stylelint-scss: 7.0.0(stylelint@17.8.0(typescript@5.9.3)) optionalDependencies: - postcss: 8.5.9 + postcss: 8.5.10 - stylelint-config-recommended@18.0.0(stylelint@17.6.0(typescript@5.9.3)): + stylelint-config-recommended@18.0.0(stylelint@17.8.0(typescript@5.9.3)): dependencies: - stylelint: 17.6.0(typescript@5.9.3) + stylelint: 17.8.0(typescript@5.9.3) - stylelint-config-standard-scss@17.0.0(postcss@8.5.9)(stylelint@17.6.0(typescript@5.9.3)): + stylelint-config-standard-scss@17.0.0(postcss@8.5.10)(stylelint@17.8.0(typescript@5.9.3)): dependencies: - stylelint: 17.6.0(typescript@5.9.3) - stylelint-config-recommended-scss: 17.0.1(postcss@8.5.9)(stylelint@17.6.0(typescript@5.9.3)) - stylelint-config-standard: 40.0.0(stylelint@17.6.0(typescript@5.9.3)) + stylelint: 17.8.0(typescript@5.9.3) + stylelint-config-recommended-scss: 17.0.1(postcss@8.5.10)(stylelint@17.8.0(typescript@5.9.3)) + stylelint-config-standard: 40.0.0(stylelint@17.8.0(typescript@5.9.3)) optionalDependencies: - postcss: 8.5.9 + postcss: 8.5.10 - stylelint-config-standard@40.0.0(stylelint@17.6.0(typescript@5.9.3)): + stylelint-config-standard@40.0.0(stylelint@17.8.0(typescript@5.9.3)): dependencies: - stylelint: 17.6.0(typescript@5.9.3) - stylelint-config-recommended: 18.0.0(stylelint@17.6.0(typescript@5.9.3)) + stylelint: 17.8.0(typescript@5.9.3) + stylelint-config-recommended: 18.0.0(stylelint@17.8.0(typescript@5.9.3)) - stylelint-scss@7.0.0(stylelint@17.6.0(typescript@5.9.3)): + stylelint-scss@7.0.0(stylelint@17.8.0(typescript@5.9.3)): dependencies: css-tree: 3.2.1 is-plain-object: 5.0.0 known-css-properties: 0.37.0 - mdn-data: 2.27.1 + mdn-data: 2.28.0 postcss-media-query-parser: 0.2.3 postcss-resolve-nested-selector: 0.1.6 postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 - stylelint: 17.6.0(typescript@5.9.3) + stylelint: 17.8.0(typescript@5.9.3) - stylelint@17.6.0(typescript@5.9.3): + stylelint@17.8.0(typescript@5.9.3): dependencies: - '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) - '@csstools/css-syntax-patches-for-csstree': 1.1.2(css-tree@3.2.1) + '@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1) '@csstools/css-tokenizer': 4.0.0 '@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.1) @@ -5621,8 +5602,8 @@ snapshots: micromatch: 4.0.8 normalize-path: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.9 - postcss-safe-parser: 7.0.1(postcss@8.5.9) + postcss: 8.5.10 + postcss-safe-parser: 7.0.1(postcss@8.5.10) postcss-selector-parser: 7.1.1 postcss-value-parser: 4.2.0 string-width: 8.2.0 @@ -5774,7 +5755,7 @@ snapshots: tsx@4.21.0: dependencies: esbuild: 0.27.7 - get-tsconfig: 4.13.7 + get-tsconfig: 4.14.0 optionalDependencies: fsevents: 2.3.3 @@ -5874,20 +5855,20 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite-plugin-image-optimizer@2.0.3(sharp@0.34.5)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)): + vite-plugin-image-optimizer@2.0.3(sharp@0.34.5)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)): dependencies: ansi-colors: 4.1.3 pathe: 2.0.3 - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0) + vite: 8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0) optionalDependencies: sharp: 0.34.5 - vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0): + vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.9 - rolldown: 1.0.0-rc.15 + postcss: 8.5.10 + rolldown: 1.0.0-rc.16 tinyglobby: 0.2.16 optionalDependencies: '@types/node': 25.6.0 @@ -5896,10 +5877,10 @@ snapshots: sass: 1.99.0 tsx: 4.21.0 - vitest@4.1.4(@types/node@25.6.0)(@vitest/ui@4.1.4)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)): + vitest@4.1.4(@types/node@25.6.0)(@vitest/ui@4.1.4)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)): dependencies: '@vitest/expect': 4.1.4 - '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)) + '@vitest/mocker': 4.1.4(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)) '@vitest/pretty-format': 4.1.4 '@vitest/runner': 4.1.4 '@vitest/snapshot': 4.1.4 @@ -5911,12 +5892,12 @@ snapshots: obug: 2.1.1 pathe: 2.0.3 picomatch: 4.0.4 - std-env: 4.0.0 + std-env: 4.1.0 tinybench: 2.9.0 tinyexec: 1.1.1 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0) + vite: 8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.6.0 From 0b04f47096fc5b681e7218d01a132d8e24d848a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 20 Apr 2026 12:38:43 +0200 Subject: [PATCH 34/46] post-merge fixes --- crates/defguard_certs/src/lib.rs | 6 ++++-- crates/defguard_core/tests/integration/api/common/mod.rs | 9 +++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs index 58a582c30..b88f0de69 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -3,8 +3,8 @@ use std::str::FromStr; use base64::{Engine, prelude::BASE64_STANDARD}; use chrono::NaiveDateTime; use rcgen::{ - BasicConstraints, Certificate, CertificateParams, CertificateSigningRequestParams, - ExtendedKeyUsagePurpose, IsCa, Issuer, KeyPair, KeyUsagePurpose, SigningKey, string::Ia5String, + BasicConstraints, Certificate, CertificateParams, CertificateSigningRequestParams, IsCa, + Issuer, KeyPair, KeyUsagePurpose, SigningKey, string::Ia5String, }; use rustls_pki_types::{CertificateDer, CertificateSigningRequestDer, pem::PemObject}; use thiserror::Error; @@ -14,6 +14,8 @@ use x509_parser::{ parse_x509_certificate, }; +pub use rcgen::ExtendedKeyUsagePurpose; + const CA_NAME: &str = "Defguard CA"; const NOT_BEFORE_OFFSET_SECS: Duration = Duration::minutes(5); const DEFAULT_CERT_VALIDITY_DAYS: i64 = 1825; diff --git a/crates/defguard_core/tests/integration/api/common/mod.rs b/crates/defguard_core/tests/integration/api/common/mod.rs index 72bf02403..f514e313b 100644 --- a/crates/defguard_core/tests/integration/api/common/mod.rs +++ b/crates/defguard_core/tests/integration/api/common/mod.rs @@ -7,7 +7,10 @@ use std::{ }; use axum_extra::extract::cookie::Key; -use defguard_certs::{CertificateAuthority, Csr, DnType, PemLabel, der_to_pem, generate_key_pair}; +use defguard_certs::{ + CertificateAuthority, Csr, DnType, ExtendedKeyUsagePurpose, PemLabel, der_to_pem, + generate_key_pair, +}; pub use defguard_common::db::setup_pool; use defguard_common::{ VERSION, @@ -267,7 +270,9 @@ pub(crate) fn generate_expired_test_cert_pem(common_name: &str) -> (String, Stri let san = vec![common_name.to_string()]; let dn = vec![(DnType::CommonName, common_name)]; let csr = Csr::new(&key_pair, &san, dn).unwrap(); - let cert = ca.sign_csr_with_validity(&csr, 0).unwrap(); + let cert = ca + .sign_csr_with_validity(&csr, 0, &[ExtendedKeyUsagePurpose::ServerAuth]) + .unwrap(); let cert_pem = der_to_pem(cert.der(), PemLabel::Certificate).unwrap(); let key_pem = der_to_pem(key_pair.serialize_der().as_slice(), PemLabel::PrivateKey).unwrap(); (cert_pem, key_pem) From ceb26ceb751834cc14ae5d5c2d1e303c9cd1e531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 20 Apr 2026 12:38:48 +0200 Subject: [PATCH 35/46] update deps --- Cargo.lock | 6 ------ flake.lock | 5 ----- web/package.json | 3 --- web/pnpm-lock.yaml | 54 ---------------------------------------------- 4 files changed, 68 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 55005c907..3a9ebdb0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4063,9 +4063,6 @@ name = "openssl" version = "0.10.78" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" -version = "0.10.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe4646e360ec77dff7dde40ed3d6c5fee52d156ef4a62f53973d38294dad87f" dependencies = [ "bitflags 2.11.1", "cfg-if", @@ -4098,9 +4095,6 @@ name = "openssl-sys" version = "0.9.114" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" -version = "0.9.113" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad2f2c0eba47118757e4c6d2bff2838f3e0523380021356e7875e858372ce644" dependencies = [ "cc", "libc", diff --git a/flake.lock b/flake.lock index 4ded1bdec..924c8016e 100644 --- a/flake.lock +++ b/flake.lock @@ -79,11 +79,6 @@ "owner": "oxalica", "repo": "rust-overlay", "rev": "25d75be8139815a53560745fa060909777495105", - "lastModified": 1776395632, - "narHash": "sha256-Mi1uF5f2FsdBIvy+v7MtsqxD3Xjhd0ARJdwoqqqPtJo=", - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "8087ff1f47fff983a1fba70fa88b759f2fd8ae97", "type": "github" }, "original": { diff --git a/web/package.json b/web/package.json index 163a578c3..bb5f6e8d9 100644 --- a/web/package.json +++ b/web/package.json @@ -26,8 +26,6 @@ "@tanstack/react-form": "^1.29.0", "@tanstack/react-query": "^5.99.2", "@tanstack/react-router": "^1.168.23", - "@tanstack/react-query": "^5.99.0", - "@tanstack/react-router": "^1.168.22", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.24", "@uidotdev/usehooks": "^2.4.1", @@ -61,7 +59,6 @@ "@tanstack/devtools-vite": "^0.6.0", "@tanstack/react-devtools": "^0.10.2", "@tanstack/react-query-devtools": "^5.99.2", - "@tanstack/react-query-devtools": "^5.99.0", "@tanstack/react-router-devtools": "^1.166.13", "@tanstack/router-plugin": "^1.167.22", "@types/byte-size": "^8.1.2", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index d471a0f65..a9af77222 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -38,11 +38,6 @@ importers: '@tanstack/react-router': specifier: ^1.168.23 version: 1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - specifier: ^5.99.0 - version: 5.99.0(react@19.2.5) - '@tanstack/react-router': - specifier: ^1.168.22 - version: 1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tanstack/react-table': specifier: ^8.21.3 version: 8.21.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -140,14 +135,6 @@ importers: '@tanstack/router-plugin': specifier: ^1.167.22 version: 1.167.22(@tanstack/react-router@1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)) - specifier: ^5.99.0 - version: 5.99.0(@tanstack/react-query@5.99.0(react@19.2.5))(react@19.2.5) - '@tanstack/react-router-devtools': - specifier: ^1.166.13 - version: 1.166.13(@tanstack/react-router@1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@tanstack/router-core@1.168.15)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@tanstack/router-plugin': - specifier: ^1.167.22 - version: 1.167.22(@tanstack/react-router@1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0)) '@types/byte-size': specifier: ^8.1.2 version: 8.1.2 @@ -1157,11 +1144,6 @@ packages: '@tanstack/query-devtools@5.99.2': resolution: {integrity: sha512-TEF1d+RYO9l8oeCwgzmOHIgKwAzXQmw2s/ny2bW8qeg2OMkkLjALfVEivgCMR3OL/jVdMmeTPX56WrV+uvYJFg==} - '@tanstack/query-core@5.99.0': - resolution: {integrity: sha512-3Jv3WQG0BCcH7G+7lf/bP8QyBfJOXeY+T08Rin3GZ1bshvwlbPt7NrDHMEzGdKIOmOzvIQmxjk28YEQX60k7pQ==} - - '@tanstack/query-devtools@5.99.0': - resolution: {integrity: sha512-m4ufXaJ8FjWXw7xDtyzE/6fkZAyQFg9WrbMrUpt8ZecRJx58jiFOZ2lxZMphZdIpAnIeto/S8stbwLKLusyckQ==} '@tanstack/react-devtools@0.10.2': resolution: {integrity: sha512-1BmZyxOrI5SqmRJ5MgkYZNNdnlLsJxQRI2YgorrAvcF2MxK6x5RcuStvD8+YlXoMw3JtNukPxoITirKAnKYDQA==} @@ -1189,14 +1171,6 @@ packages: '@tanstack/react-query@5.99.2': resolution: {integrity: sha512-vM91UEe45QUS9ED6OklsVL15i8qKcRqNwpWzPTVWvRPRSEgDudDgHpvyTjcdlwHcrKNa80T+xXYcchT2noPnZA==} - '@tanstack/react-query-devtools@5.99.0': - resolution: {integrity: sha512-CqqX7LCU9yOfCY/vBURSx2YSD83ryfX+QkfkaKionTfg1s2Hdm572Ro99gW3QPoJjzvsj1HM4pnN4nbDy3MXKA==} - peerDependencies: - '@tanstack/react-query': ^5.99.0 - react: ^18 || ^19 - - '@tanstack/react-query@5.99.0': - resolution: {integrity: sha512-OY2bCqPemT1LlqJ8Y2CUau4KELnIhhG9Ol3ZndPbdnB095pRbPo1cHuXTndg8iIwtoHTgwZjyaDnQ0xD0mYwAw==} peerDependencies: react: ^18 || ^19 @@ -1214,8 +1188,6 @@ packages: '@tanstack/react-router@1.168.23': resolution: {integrity: sha512-+GblieDnutG6oipJJPNtRJjrWF8QTZEG/l0532+BngFkVK48oHNOcvIkSoAFYftK1egAwM7KBxXsb0Ou+X6/MQ==} - '@tanstack/react-router@1.168.22': - resolution: {integrity: sha512-W2LyfkfJtDCf//jOjZeUBWwOVl8iDRVTECpGHa2M28MT3T5/VVnjgicYNHR/ax0Filk1iU67MRjcjHheTYvK1Q==} engines: {node: '>=20.19'} peerDependencies: react: '>=18.0.0 || >=19.0.0' @@ -1513,8 +1485,6 @@ packages: baseline-browser-mapping@2.10.20: resolution: {integrity: sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==} - baseline-browser-mapping@2.10.19: - resolution: {integrity: sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==} engines: {node: '>=6.0.0'} hasBin: true @@ -3905,9 +3875,6 @@ snapshots: '@tanstack/query-core@5.99.2': {} '@tanstack/query-devtools@5.99.2': {} - '@tanstack/query-core@5.99.0': {} - - '@tanstack/query-devtools@5.99.0': {} '@tanstack/react-devtools@0.10.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(solid-js@1.9.9)': dependencies: @@ -3944,20 +3911,6 @@ snapshots: '@tanstack/react-router-devtools@1.166.13(@tanstack/react-router@1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@tanstack/router-core@1.168.15)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@tanstack/react-router': 1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@tanstack/react-query-devtools@5.99.0(@tanstack/react-query@5.99.0(react@19.2.5))(react@19.2.5)': - dependencies: - '@tanstack/query-devtools': 5.99.0 - '@tanstack/react-query': 5.99.0(react@19.2.5) - react: 19.2.5 - - '@tanstack/react-query@5.99.0(react@19.2.5)': - dependencies: - '@tanstack/query-core': 5.99.0 - react: 19.2.5 - - '@tanstack/react-router-devtools@1.166.13(@tanstack/react-router@1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@tanstack/router-core@1.168.15)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - dependencies: - '@tanstack/react-router': 1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tanstack/router-devtools-core': 1.167.3(@tanstack/router-core@1.168.15)(csstype@3.2.3) react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -3967,7 +3920,6 @@ snapshots: - csstype '@tanstack/react-router@1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': - '@tanstack/react-router@1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@tanstack/history': 1.161.6 '@tanstack/react-store': 0.9.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -4024,7 +3976,6 @@ snapshots: - supports-color '@tanstack/router-plugin@1.167.22(@tanstack/react-router@1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0))': - '@tanstack/router-plugin@1.167.22(@tanstack/react-router@1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) @@ -4042,8 +3993,6 @@ snapshots: optionalDependencies: '@tanstack/react-router': 1.168.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5) vite: 8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0) - '@tanstack/react-router': 1.168.22(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(sass@1.99.0)(tsx@4.21.0) transitivePeerDependencies: - supports-color @@ -4284,7 +4233,6 @@ snapshots: bail@2.0.2: {} baseline-browser-mapping@2.10.20: {} - baseline-browser-mapping@2.10.19: {} binary-extensions@2.3.0: {} @@ -4295,7 +4243,6 @@ snapshots: browserslist@4.28.2: dependencies: baseline-browser-mapping: 2.10.20 - baseline-browser-mapping: 2.10.19 caniuse-lite: 1.0.30001788 electron-to-chromium: 1.5.340 node-releases: 2.0.37 @@ -5922,7 +5869,6 @@ snapshots: picomatch: 4.0.4 postcss: 8.5.10 rolldown: 1.0.0-rc.16 - rolldown: 1.0.0-rc.15 tinyglobby: 0.2.16 optionalDependencies: '@types/node': 25.6.0 From acc55c02962407f245d951685afcaf55d222cc59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 20 Apr 2026 15:54:11 +0200 Subject: [PATCH 36/46] change postgres notifications to send just the object ID --- crates/defguard_common/src/db/mod.rs | 5 ++--- ...20260414120000_[2.0.0]_core_grpc_cert.down.sql | 10 ++++++++++ .../20260414120000_[2.0.0]_core_grpc_cert.up.sql | 15 +++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/crates/defguard_common/src/db/mod.rs b/crates/defguard_common/src/db/mod.rs index 2827b9707..c767be6b8 100644 --- a/crates/defguard_common/src/db/mod.rs +++ b/crates/defguard_common/src/db/mod.rs @@ -59,8 +59,7 @@ pub enum TriggerOperation { } #[derive(Deserialize)] -pub struct ChangeNotification { +pub struct ChangeNotification { pub operation: TriggerOperation, - pub old: Option, - pub new: Option, + pub id: Id, } diff --git a/migrations/20260414120000_[2.0.0]_core_grpc_cert.down.sql b/migrations/20260414120000_[2.0.0]_core_grpc_cert.down.sql index ff2b6383a..03174799b 100644 --- a/migrations/20260414120000_[2.0.0]_core_grpc_cert.down.sql +++ b/migrations/20260414120000_[2.0.0]_core_grpc_cert.down.sql @@ -10,3 +10,13 @@ ALTER TABLE proxy DROP COLUMN core_client_cert_der, DROP COLUMN core_client_cert_key_der, DROP COLUMN core_client_cert_expiry; + +-- Restore the full row_change() function. +CREATE OR REPLACE FUNCTION row_change() RETURNS trigger AS $$ +BEGIN + PERFORM pg_notify(TG_TABLE_NAME || '_change', + json_build_object('operation', TG_OP, 'old', row_to_json(OLD), 'new', row_to_json(NEW))::text + ); + RETURN NULL; +END; +$$ LANGUAGE plpgsql; diff --git a/migrations/20260414120000_[2.0.0]_core_grpc_cert.up.sql b/migrations/20260414120000_[2.0.0]_core_grpc_cert.up.sql index b9d1e0a6b..32968154f 100644 --- a/migrations/20260414120000_[2.0.0]_core_grpc_cert.up.sql +++ b/migrations/20260414120000_[2.0.0]_core_grpc_cert.up.sql @@ -10,3 +10,18 @@ ALTER TABLE proxy ADD COLUMN core_client_cert_der bytea DEFAULT NULL, ADD COLUMN core_client_cert_key_der bytea DEFAULT NULL, ADD COLUMN core_client_cert_expiry timestamp without time zone NULL; + +-- Switch to a lightweight notification payload (id + operation only) to avoid +-- exceeding PostgreSQL's 8000-byte pg_notify limit when bytea cert columns are populated. +CREATE OR REPLACE FUNCTION row_change() RETURNS trigger AS $$ +BEGIN + PERFORM pg_notify( + TG_TABLE_NAME || '_change', + json_build_object( + 'operation', TG_OP, + 'id', COALESCE(NEW.id, OLD.id) + )::text + ); + RETURN NULL; +END; +$$ LANGUAGE plpgsql; From cf331b9151db8268151f51166ab93232c0ce1d48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 20 Apr 2026 21:15:44 +0200 Subject: [PATCH 37/46] avoid infinite update loops --- crates/defguard_gateway_manager/src/lib.rs | 262 +++++++++++++++------ 1 file changed, 196 insertions(+), 66 deletions(-) diff --git a/crates/defguard_gateway_manager/src/lib.rs b/crates/defguard_gateway_manager/src/lib.rs index 5f005303c..0fe411463 100644 --- a/crates/defguard_gateway_manager/src/lib.rs +++ b/crates/defguard_gateway_manager/src/lib.rs @@ -279,10 +279,8 @@ impl GatewayManager { } #[cfg(test)] - fn note_gateway_notification_for_tests(&self, maybe_gateway_id: Option) { - if let Some(gateway_id) = maybe_gateway_id { - self.test_support.note_gateway_notification(gateway_id); - } + fn note_gateway_notification_for_tests(&self, gateway_id: Id) { + self.test_support.note_gateway_notification(gateway_id); } fn manager_reconnect_delay(&self) -> Duration { @@ -351,7 +349,9 @@ impl GatewayManager { sleep(TEN_SECS).await; } })); - let mut abort_handles = HashMap::new(); + // Stores the abort handle and a snapshot of the gateway at the time the handler was last + // started. The snapshot is used by the Update arm to detect connection-relevant changes. + let mut abort_handles: HashMap)> = HashMap::new(); for gateway in Gateway::all(&self.pool).await? { if !gateway.enabled { debug!("Existing Gateway is disabled, so it won't be handled"); @@ -359,9 +359,10 @@ impl GatewayManager { } let id = gateway.id; + let snapshot = gateway.clone(); let abort_handle = self.run_handler(gateway, Arc::clone(&self.clients), certs_rx.clone())?; - abort_handles.insert(id, abort_handle); + abort_handles.insert(id, (abort_handle, snapshot)); } // Observe gateway changes. @@ -373,116 +374,158 @@ impl GatewayManager { while let Ok(notification) = listener.recv().await { let payload = notification.payload(); - match serde_json::from_str::>>(payload) { + match serde_json::from_str::(payload) { Ok(gateway_notification) => { - let _maybe_gateway_id = match gateway_notification.operation { + let gateway_id = gateway_notification.id; + + match gateway_notification.operation { TriggerOperation::Insert => { - let Some(new) = gateway_notification.new else { - continue; + let gateway = match Gateway::find_by_id(&self.pool, gateway_id).await { + Ok(Some(gateway)) => gateway, + Ok(None) => { + warn!( + "Received Insert notification for Gateway \ + id={gateway_id} but it was not found in the database" + ); + #[cfg(test)] + self.note_gateway_notification_for_tests(gateway_id); + continue; + } + Err(err) => { + error!("Failed to fetch Gateway id={gateway_id}: {err}"); + continue; + } }; - let id = new.id; - if new.enabled { + if gateway.enabled { + let snapshot = gateway.clone(); let abort_handle = self.run_handler( - new, + gateway, Arc::clone(&self.clients), certs_rx.clone(), )?; - abort_handles.insert(id, abort_handle); + abort_handles.insert(gateway_id, (abort_handle, snapshot)); } else { - debug!("New Gateway is disabled, so it won't be handled"); + debug!( + "New Gateway id={gateway_id} is disabled, so it won't be \ + handled" + ); } - Some(id) + #[cfg(test)] + self.note_gateway_notification_for_tests(gateway_id); } TriggerOperation::Update => { - let (Some(mut old), Some(new)) = - (gateway_notification.old, gateway_notification.new) - else { - continue; - }; - - let id = new.id; - if old.address == new.address - && old.port == new.port - && old.enabled == new.enabled - { - debug!("Gateway address/port/state didn't change"); - } else { - self.remove_client(old.id); - if let Some(abort_handle) = abort_handles.remove(&old.id) { - if let Err(err) = old.touch_disconnected(&self.pool).await { - error!( - "Failed to update disconnection time for Gateway {old} \ - after database change: {err}" + let mut gateway = + match Gateway::find_by_id(&self.pool, gateway_id).await { + Ok(Some(gateway)) => gateway, + Ok(None) => { + warn!( + "Received Update notification for Gateway \ + id={gateway_id} but it was not found in the database" ); + #[cfg(test)] + self.note_gateway_notification_for_tests(gateway_id); + continue; + } + Err(err) => { + error!("Failed to fetch Gateway id={gateway_id}: {err}"); + continue; } + }; + + // Only restart the handler when connection-relevant fields have actually changed + let should_restart = match abort_handles.get(&gateway_id) { + Some((_, snapshot)) => needs_restart(snapshot, &gateway), + // Gateway not currently handled - treat as needing a (re)start. + None => true, + }; + + if should_restart { + self.remove_client(gateway_id); + if let Some((abort_handle, _)) = abort_handles.remove(&gateway_id) { info!( - "Aborting connection to Gateway {old}, it has changed in \ - the database" + "Aborting connection to Gateway id={gateway_id}, \ + connection-relevant fields have changed" ); abort_handle.abort(); - } else if old.enabled { - warn!( - "Cannot find Gateway {old} on the list of connected \ - gateways" - ); } - if new.enabled { + + // Only mark disconnected if the gateway was actually connected + if gateway.is_connected() { + if let Err(err) = gateway.touch_disconnected(&self.pool).await { + error!( + "Failed to update disconnection time for Gateway \ + id={gateway_id} after database change: {err}" + ); + } + } + + if gateway.enabled { + let snapshot = gateway.clone(); let abort_handle = self.run_handler( - new, + gateway, Arc::clone(&self.clients), certs_rx.clone(), )?; - abort_handles.insert(id, abort_handle); + abort_handles.insert(gateway_id, (abort_handle, snapshot)); } else { - debug!("Updated Gateway is disabled, so it won't be handled"); + debug!( + "Updated Gateway id={gateway_id} is disabled, so it \ + won't be handled" + ); + } + } else { + // Non-connection-relevant update (e.g. version bump from handler + // save). Refresh the stored snapshot so future comparisons use + // up-to-date baseline values. + if let Some((_, snapshot)) = abort_handles.get_mut(&gateway_id) { + *snapshot = gateway; } } - Some(id) + #[cfg(test)] + self.note_gateway_notification_for_tests(gateway_id); } TriggerOperation::Delete => { - let Some(old) = gateway_notification.old else { - continue; - }; - // Send purge request to Gateway. - let maybe_client = self.remove_client(old.id); + let maybe_client = self.remove_client(gateway_id); if let Some(mut client) = maybe_client { - debug!("Sending purge request to Gateway {old}"); + debug!("Sending purge request to Gateway id={gateway_id}"); if let Err(err) = client.purge(Request::new(())).await { - error!("Error sending purge request to Gateway {old}: {err}"); + error!( + "Error sending purge request to Gateway id={gateway_id}: \ + {err}" + ); } else { - info!("Sent purge request to Gateway {old}"); + info!("Sent purge request to Gateway id={gateway_id}"); } } else { warn!( - "Cannot find gRPC client for Gateway {old}; skipping purge \ - request" + "Cannot find gRPC client for Gateway id={gateway_id}; \ + skipping purge request" ); } // Kill the `GatewayHandler` and the connection. - if let Some(abort_handle) = abort_handles.remove(&old.id) { + if let Some((abort_handle, _)) = abort_handles.remove(&gateway_id) { info!( - "Aborting connection to Gateway {old}, it has disappeared from \ - the database" + "Aborting connection to Gateway id={gateway_id}, it has \ + disappeared from the database" ); abort_handle.abort(); - } else if old.enabled { + } else { warn!( - "Cannot find Gateway {old} on the list of connected gateways" + "Cannot find Gateway id={gateway_id} on the list of \ + connected gateways" ); } - Some(old.id) + #[cfg(test)] + self.note_gateway_notification_for_tests(gateway_id); } }; - - #[cfg(test)] - self.note_gateway_notification_for_tests(_maybe_gateway_id); } Err(err) => error!("Failed to de-serialize database notification object: {err}"), } @@ -519,6 +562,93 @@ impl GatewayManager { } } +/// Returns true if the change from `old` to `new` requires the gateway handler to be +/// restarted - i.e. if any field that directly affects the gRPC connection or TLS identity +/// has changed. +/// +/// Fields that do NOT trigger a restart (version, timestamps, audit fields, cert expiry) +/// are intentionally excluded so that the handler-internal `gateway.save()` call, which +/// bumps those fields, does not cause an infinite restart loop. +fn needs_restart(old: &Gateway, new: &Gateway) -> bool { + old.address != new.address + || old.port != new.port + || old.enabled != new.enabled + || old.core_client_cert_der != new.core_client_cert_der + || old.core_client_cert_key_der != new.core_client_cert_key_der +} + +#[cfg(test)] +mod unit_tests { + use chrono::Utc; + use defguard_common::db::{Id, models::gateway::Gateway}; + + use super::needs_restart; + + fn base_gateway() -> Gateway { + Gateway { + id: 1, + location_id: 1, + name: "test".to_string(), + address: "127.0.0.1".to_string(), + port: 50051, + connected_at: None, + disconnected_at: None, + certificate_serial: None, + certificate_expiry: None, + version: None, + enabled: true, + modified_at: Utc::now().naive_utc(), + modified_by: "test".to_string(), + core_client_cert_der: None, + core_client_cert_key_der: None, + core_client_cert_expiry: None, + } + } + + #[test] + fn test_needs_restart_detects_connection_relevant_field_changes() { + let base = base_gateway(); + + // Identical gateways - no restart needed. + assert!(!needs_restart(&base, &base.clone())); + + // Non-connection-relevant fields - no restart. + let mut no_restart = base.clone(); + no_restart.version = Some("2.0.0".to_string()); + no_restart.modified_by = "someone-else".to_string(); + no_restart.connected_at = Some(Utc::now().naive_utc()); + no_restart.disconnected_at = Some(Utc::now().naive_utc()); + no_restart.certificate_serial = Some("abc".to_string()); + no_restart.core_client_cert_expiry = Some(Utc::now().naive_utc()); + assert!(!needs_restart(&base, &no_restart)); + + // address change - restart required. + let mut changed = base.clone(); + changed.address = "10.0.0.1".to_string(); + assert!(needs_restart(&base, &changed)); + + // port change - restart required. + let mut changed = base.clone(); + changed.port = 9999; + assert!(needs_restart(&base, &changed)); + + // enabled change - restart required. + let mut changed = base.clone(); + changed.enabled = false; + assert!(needs_restart(&base, &changed)); + + // core_client_cert_der change - restart required. + let mut changed = base.clone(); + changed.core_client_cert_der = Some(vec![1, 2, 3]); + assert!(needs_restart(&base, &changed)); + + // core_client_cert_key_der change - restart required. + let mut changed = base.clone(); + changed.core_client_cert_key_der = Some(vec![4, 5, 6]); + assert!(needs_restart(&base, &changed)); + } +} + /// Shared set of outbound channels that gateway instances use to forward /// events, notifications, and side effects to Core components. #[derive(Clone)] From 880d4b26650f81761cfecd7e4a46c94059341aea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 20 Apr 2026 21:15:55 +0200 Subject: [PATCH 38/46] update tests --- .../src/tests/gateway_manager/manager.rs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/crates/defguard_gateway_manager/src/tests/gateway_manager/manager.rs b/crates/defguard_gateway_manager/src/tests/gateway_manager/manager.rs index fa82343b0..27cfdd3d1 100644 --- a/crates/defguard_gateway_manager/src/tests/gateway_manager/manager.rs +++ b/crates/defguard_gateway_manager/src/tests/gateway_manager/manager.rs @@ -86,6 +86,9 @@ async fn test_noop_gateway_update_does_not_restart_handler( _: PgPoolOptions, options: PgConnectOptions, ) { + // A DB update that changes only non-connection-relevant fields (e.g. modified_by) + // should NOT cause the handler to be restarted. The Update notification is still + // received and counted, but the existing handler must remain connected. let mut context = ManagerTestContext::new(options).await; let network = create_network(&context.pool).await; let mut gateway = create_gateway(&context.pool, network.id).await; @@ -98,30 +101,24 @@ async fn test_noop_gateway_update_does_not_restart_handler( gateway = reload_gateway(&context.pool, gateway.id).await; let initial_spawn_attempts = context.handler_spawn_attempt_count(gateway.id); let initial_notification_count = context.gateway_notification_count(gateway.id); - let initial_connection_count = mock_gateway.connection_count(); gateway.modified_by = "manager-noop-update".to_string(); gateway .save(&context.pool) .await - .expect("failed to save no-op gateway update"); + .expect("failed to save gateway noop update"); + // The Update notification must be received and counted. context .wait_for_gateway_notification_count(gateway.id, initial_notification_count + 1) .await; + + // But no new handler spawn should have occurred. assert_eq!( context.handler_spawn_attempt_count(gateway.id), initial_spawn_attempts, - "no-op gateway update should not restart the handler" + "a non-connection-relevant update should not restart the handler" ); - assert_eq!( - mock_gateway.connection_count(), - initial_connection_count, - "no-op gateway update should not reconnect the handler" - ); - - let gateway_after = reload_gateway(&context.pool, gateway.id).await; - assert!(gateway_after.is_connected()); context.finish().await; } From 57719abb87d978aa1de09a86b4769a8b50607fb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 21 Apr 2026 07:47:48 +0200 Subject: [PATCH 39/46] add docstring --- crates/defguard_gateway_manager/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/defguard_gateway_manager/src/lib.rs b/crates/defguard_gateway_manager/src/lib.rs index 0fe411463..fa383116d 100644 --- a/crates/defguard_gateway_manager/src/lib.rs +++ b/crates/defguard_gateway_manager/src/lib.rs @@ -278,6 +278,9 @@ impl GatewayManager { } } + /// Records that the manager finished processing a pg_notify for the given gateway. + /// Tests block on `wait_for_gateway_notification_count` to synchronize against the + /// manager's async loop before asserting side effects. #[cfg(test)] fn note_gateway_notification_for_tests(&self, gateway_id: Id) { self.test_support.note_gateway_notification(gateway_id); From 90365e1e5bdbb17d82c673871d669c2c7f4a051c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 21 Apr 2026 09:04:25 +0200 Subject: [PATCH 40/46] post-merge fixes --- crates/defguard_common/src/db/models/proxy.rs | 19 +-- crates/defguard_common/src/types/proxy.rs | 21 ++- .../src/handlers/component_setup.rs | 144 +----------------- crates/defguard_core/src/handlers/proxy.rs | 1 + crates/defguard_core/src/letsencrypt.rs | 72 ++++----- crates/defguard_mail/src/tests.rs | 4 +- 6 files changed, 68 insertions(+), 193 deletions(-) diff --git a/crates/defguard_common/src/db/models/proxy.rs b/crates/defguard_common/src/db/models/proxy.rs index 6d005bc38..536d159a3 100644 --- a/crates/defguard_common/src/db/models/proxy.rs +++ b/crates/defguard_common/src/db/models/proxy.rs @@ -6,10 +6,7 @@ use serde::{Deserialize, Serialize}; use sqlx::{PgExecutor, PgPool, query, query_as}; use utoipa::ToSchema; -use crate::{ - db::{Id, NoId}, - types::proxy::ProxyInfo, -}; +use crate::db::{Id, NoId}; #[derive(Clone, Deserialize, Model, Serialize, ToSchema, PartialEq)] pub struct Proxy { @@ -149,16 +146,10 @@ impl Proxy { .await } - pub async fn list(pool: &PgPool) -> sqlx::Result> { - query_as!( - ProxyInfo, - "SELECT id, name, address, port, connected_at, disconnected_at, \ - version, enabled, certificate_serial, certificate_expiry, \ - modified_at, modified_by \ - FROM proxy", - ) - .fetch_all(pool) - .await + pub async fn list(pool: &PgPool) -> sqlx::Result> { + query_as!(Self, "SELECT * FROM proxy",) + .fetch_all(pool) + .await } pub async fn mark_connected(&mut self, pool: &PgPool, version: String) -> sqlx::Result<()> { diff --git a/crates/defguard_common/src/types/proxy.rs b/crates/defguard_common/src/types/proxy.rs index 2d60d1a43..560d2172b 100644 --- a/crates/defguard_common/src/types/proxy.rs +++ b/crates/defguard_common/src/types/proxy.rs @@ -2,7 +2,7 @@ use chrono::NaiveDateTime; use serde::Serialize; use utoipa::ToSchema; -use crate::db::Id; +use crate::db::{Id, models::proxy::Proxy}; // Used by the proxy manager to control proxies (start/shutdown). pub enum ProxyControlMessage { @@ -32,3 +32,22 @@ pub struct ProxyInfo { pub modified_at: NaiveDateTime, pub modified_by: String, } + +impl From> for ProxyInfo { + fn from(value: Proxy) -> Self { + Self { + id: value.id, + name: value.name, + address: value.address, + port: value.port, + connected_at: value.connected_at, + disconnected_at: value.disconnected_at, + version: value.version, + enabled: value.enabled, + certificate_serial: value.certificate_serial, + certificate_expiry: value.certificate_expiry, + modified_at: value.modified_at, + modified_by: value.modified_by, + } + } +} diff --git a/crates/defguard_core/src/handlers/component_setup.rs b/crates/defguard_core/src/handlers/component_setup.rs index 5ac6c0515..c189e60da 100644 --- a/crates/defguard_core/src/handlers/component_setup.rs +++ b/crates/defguard_core/src/handlers/component_setup.rs @@ -1,5 +1,5 @@ use std::{ - collections::{HashMap, VecDeque}, + collections::VecDeque, convert::Infallible, sync::{Arc, Mutex, PoisonError}, time::Duration, @@ -28,7 +28,6 @@ use defguard_common::{ types::proxy::ProxyControlMessage, utils::strip_scheme, }; -use defguard_grpc_tls::certs::proxy_mtls_channel; use defguard_proto::{ common::{CertBundle, CertificateInfo}, gateway::gateway_setup_client::GatewaySetupClient, @@ -41,8 +40,8 @@ use serde::{Deserialize, Serialize}; use sqlx::PgPool; use tokio::{ sync::{ - mpsc::{Sender, UnboundedReceiver, UnboundedSender, unbounded_channel}, - oneshot, watch, + mpsc::{Sender, UnboundedReceiver, unbounded_channel}, + oneshot, }, time::{Instant, sleep_until, timeout}, }; @@ -1138,143 +1137,6 @@ fn acme_error_event(step: &'static str, message: String, logs: Option &'static str { - match step { - AcmeStep::Unspecified | AcmeStep::Connecting => "Connecting", - AcmeStep::CheckingDomain => "CheckingDomain", - AcmeStep::ValidatingDomain => "ValidatingDomain", - AcmeStep::IssuingCertificate => "IssuingCertificate", - } -} - -fn parse_cert_expiry(cert_pem: &str) -> Option { - let der = defguard_certs::parse_pem_certificate(cert_pem) - .map_err(|e| warn!("Failed to parse ACME cert PEM for expiry: {e}")) - .ok()?; - defguard_certs::CertificateInfo::from_der(&der) - .map(|info| info.not_after) - .map_err(|e| warn!("Failed to extract expiry from ACME cert: {e}")) - .ok() -} - -fn public_proxy_hostname() -> Result { - let public_proxy_url = Settings::get_current_settings().public_proxy_url; - let url = public_proxy_url.trim(); - - if url.is_empty() { - return Err( - "Public Edge URL is not configured. Please re-submit the external URL settings \ - with a Let's Encrypt domain." - .to_string(), - ); - } - - Url::parse(url) - .ok() - .and_then(|u| u.host_str().map(ToString::to_string)) - .filter(|host| !host.is_empty()) - .ok_or_else(|| { - "Public Edge URL is not configured with a valid hostname. Please re-submit the \ - external URL settings with a valid domain." - .to_string() - }) -} - -/// Connects to the proxy's permanent `Proxy` gRPC service and calls `TriggerAcme`. -/// -/// Returns `(cert_pem, key_pem, account_credentials_json)` on success, or -/// `(error_message, log_lines)` on failure where `log_lines` are the proxy log entries -/// collected during the ACME run (sent by the proxy via an [`AcmeLogs`] event). -async fn call_proxy_trigger_acme( - pool: &PgPool, - proxy: &Proxy, - domain: String, - account_credentials_json: String, - progress_tx: UnboundedSender, -) -> Result<(String, String, String), (String, Vec)> { - let certs = Certificates::get_or_default(pool) - .await - .map_err(|e| (format!("Failed to load certificates: {e}"), Vec::new()))?; - let ca_cert_der = certs.ca_cert_der.ok_or_else(|| { - ( - "CA certificate not found in settings".to_string(), - Vec::new(), - ) - })?; - - let cert_serial = proxy.certificate_serial.as_deref().ok_or_else(|| { - ( - "Edge certificate serial not provisioned".to_string(), - Vec::new(), - ) - })?; - - // Seed a one-shot serial map so the rustls verifier validates the server cert serial. - let (_, certs_rx) = watch::channel(Arc::new(HashMap::from([( - proxy.id, - cert_serial.to_string(), - )]))); - - let channel = proxy_mtls_channel(proxy, &ca_cert_der, certs_rx) - .map_err(|e| (format!("Failed to build mTLS channel: {e}"), Vec::new()))?; - - let version = Version::parse(VERSION) - .map_err(|e| (format!("Failed to parse core version: {e}"), Vec::new()))?; - let version_interceptor = ClientVersionInterceptor::new(version); - - let mut client = ProxyClient::with_interceptor(channel, move |req: Request<()>| { - version_interceptor.clone().call(req) - }); - - let mut stream = client - .trigger_acme(AcmeChallenge { - domain: domain.clone(), - account_credentials_json, - }) - .await - .map_err(|e| (format!("TriggerAcme RPC failed: {e}"), Vec::new()))? - .into_inner(); - - let mut collected_logs: Vec = Vec::new(); - - loop { - match stream.message().await { - Ok(Some(event)) => match event.payload { - Some(acme_issue_event::Payload::Progress(p)) => { - if let Ok(step) = AcmeStep::try_from(p.step) { - let _ = progress_tx.send(step); - } - } - Some(acme_issue_event::Payload::Certificate(cert)) => { - return Ok((cert.cert_pem, cert.key_pem, cert.account_credentials_json)); - } - Some(acme_issue_event::Payload::Logs(AcmeLogs { lines })) => { - collected_logs = lines; - } - None => { - return Err(( - "TriggerAcme stream sent an event with no payload".to_string(), - collected_logs, - )); - } - }, - Ok(None) => { - return Err(( - "TriggerAcme stream ended without delivering a certificate".to_string(), - collected_logs, - )); - } - Err(e) => { - return Err(( - format!("Failed to read TriggerAcme response: {e}"), - collected_logs, - )); - } - } - } -} - /// Streams Let's Encrypt certificate issuance progress as Server-Sent Events. /// /// Delegates the ACME HTTP-01 process to the proxy component via the `TriggerAcme` diff --git a/crates/defguard_core/src/handlers/proxy.rs b/crates/defguard_core/src/handlers/proxy.rs index 3f1769dd2..f6b76d1df 100644 --- a/crates/defguard_core/src/handlers/proxy.rs +++ b/crates/defguard_core/src/handlers/proxy.rs @@ -45,6 +45,7 @@ pub(crate) async fn proxy_list( ) -> ApiResult { debug!("User {} displaying proxy list", session.user.username); let proxies = Proxy::list(&appstate.pool).await?; + let proxies: Vec = proxies.into_iter().map(Into::into).collect(); info!("User {} displayed proxy list", session.user.username); Ok(ApiResponse::json(proxies, StatusCode::OK)) diff --git a/crates/defguard_core/src/letsencrypt.rs b/crates/defguard_core/src/letsencrypt.rs index 601cc3ff5..acaaee92c 100644 --- a/crates/defguard_core/src/letsencrypt.rs +++ b/crates/defguard_core/src/letsencrypt.rs @@ -1,12 +1,15 @@ -use std::time::Duration; +use std::{collections::HashMap, sync::Arc, time::Duration}; use chrono::{NaiveDateTime, TimeDelta, Utc}; -use defguard_certs::der_to_pem; use defguard_common::{ VERSION, - db::models::{Certificates, ProxyCertSource, Settings, User, proxy::Proxy}, + db::{ + Id, + models::{Certificates, ProxyCertSource, Settings, User, proxy::Proxy}, + }, types::proxy::ProxyControlMessage, }; +use defguard_grpc_tls::certs::proxy_mtls_channel; use defguard_mail::templates; use defguard_proto::proxy::{ AcmeChallenge, AcmeLogs, AcmeStep, acme_issue_event, proxy_client::ProxyClient, @@ -14,12 +17,14 @@ use defguard_proto::proxy::{ use defguard_version::{Version, client::ClientVersionInterceptor}; use sqlx::PgPool; use thiserror::Error; -use tokio::sync::mpsc::{self, UnboundedSender, unbounded_channel}; -use tonic::{ - Request, - service::Interceptor, - transport::{Certificate, ClientTlsConfig, Endpoint}, +use tokio::{ + sync::{ + mpsc::{self, UnboundedSender, unbounded_channel}, + watch, + }, + time::timeout, }; +use tonic::{Request, service::Interceptor}; /// Maximum time (seconds) allowed for the ACME flow to complete end-to-end. #[cfg(not(test))] @@ -117,12 +122,11 @@ pub(crate) async fn do_letsencrypt_refresh( let (progress_tx, _progress_rx) = unbounded_channel::(); - match tokio::time::timeout( + match timeout( ACME_TIMEOUT, call_proxy_trigger_acme( pool, - &proxy_host, - proxy_port, + &proxy.into(), domain.clone(), account_credentials_json, progress_tx, @@ -237,8 +241,7 @@ pub(crate) fn acme_step_name(step: AcmeStep) -> &'static str { /// collected during the ACME run (sent by the proxy via an [`AcmeLogs`] event). pub(crate) async fn call_proxy_trigger_acme( pool: &PgPool, - proxy_host: &str, - proxy_port: u16, + proxy: &Proxy, domain: String, account_credentials_json: String, progress_tx: UnboundedSender, @@ -253,32 +256,29 @@ pub(crate) async fn call_proxy_trigger_acme( ) })?; - let cert_pem = der_to_pem(&ca_cert_der, defguard_certs::PemLabel::Certificate) - .map_err(|e| (format!("Failed to convert CA cert to PEM: {e}"), Vec::new()))?; - - let endpoint_str = format!("https://{proxy_host}:{proxy_port}"); - let endpoint = Endpoint::from_shared(endpoint_str) - .map_err(|e| (format!("Failed to build Edge endpoint: {e}"), Vec::new()))? - .http2_keep_alive_interval(Duration::from_secs(5)) - .tcp_keepalive(Some(Duration::from_secs(5))) - .keep_alive_while_idle(true); - - let tls = ClientTlsConfig::new().ca_certificate(Certificate::from_pem(cert_pem)); - let endpoint = endpoint.tls_config(tls).map_err(|e| { + let cert_serial = proxy.certificate_serial.as_deref().ok_or_else(|| { ( - format!("Failed to configure TLS for Edge endpoint: {e}"), + "Edge certificate serial not provisioned".to_string(), Vec::new(), ) })?; + // Seed a one-shot serial map so the rustls verifier validates the server cert serial. + let (_, certs_rx) = watch::channel(Arc::new(HashMap::from([( + proxy.id, + cert_serial.to_string(), + )]))); + + let channel = proxy_mtls_channel(proxy, &ca_cert_der, certs_rx) + .map_err(|e| (format!("Failed to build mTLS channel: {e}"), Vec::new()))?; + let version = Version::parse(VERSION) .map_err(|e| (format!("Failed to parse core version: {e}"), Vec::new()))?; let version_interceptor = ClientVersionInterceptor::new(version); - let mut client = - ProxyClient::with_interceptor(endpoint.connect_lazy(), move |req: Request<()>| { - version_interceptor.clone().call(req) - }); + let mut client = ProxyClient::with_interceptor(channel, move |req: Request<()>| { + version_interceptor.clone().call(req) + }); let mut stream = client .trigger_acme(AcmeChallenge { @@ -338,7 +338,9 @@ mod tests { time::Duration, }; - use defguard_certs::{CertificateAuthority, Csr, DnType, PemLabel, generate_key_pair}; + use defguard_certs::{ + CertificateAuthority, Csr, DnType, ExtendedKeyUsagePurpose, PemLabel, generate_key_pair, + }; use defguard_common::{ db::{ models::{Certificates, ProxyCertSource, Settings, User, proxy::Proxy}, @@ -502,7 +504,9 @@ mod tests { let san = vec![common_name.to_string()]; let dn = vec![(DnType::CommonName, common_name)]; let csr = Csr::new(&key_pair, &san, dn).expect("failed to create CSR"); - let cert = ca.sign_csr(&csr).expect("failed to sign server cert"); + let cert = ca + .sign_server_cert(&csr) + .expect("failed to sign server cert"); let cert_pem = defguard_certs::der_to_pem(cert.der(), PemLabel::Certificate).expect("cert PEM"); let key_pem = @@ -568,7 +572,7 @@ mod tests { let dn = vec![(DnType::CommonName, common_name)]; let csr = Csr::new(&key_pair, &san, dn).expect("failed to create CSR"); let cert = ca - .sign_csr_with_validity(&csr, valid_for_days) + .sign_csr_with_validity(&csr, valid_for_days, &[ExtendedKeyUsagePurpose::ServerAuth]) .expect("failed to sign cert"); let cert_pem = defguard_certs::der_to_pem(cert.der(), PemLabel::Certificate).expect("cert PEM"); @@ -674,7 +678,7 @@ mod tests { let san = vec!["localhost".to_string()]; let dn = vec![(DnType::CommonName, "localhost")]; let csr = Csr::new(&key_pair, &san, dn).expect("failed to create CSR"); - let cert = ca.sign_csr(&csr).expect("failed to sign cert"); + let cert = ca.sign_server_cert(&csr).expect("failed to sign cert"); ( defguard_certs::der_to_pem(cert.der(), PemLabel::Certificate).expect("cert PEM"), defguard_certs::der_to_pem( diff --git a/crates/defguard_mail/src/tests.rs b/crates/defguard_mail/src/tests.rs index 0694d0fa0..f5c92b1ae 100644 --- a/crates/defguard_mail/src/tests.rs +++ b/crates/defguard_mail/src/tests.rs @@ -22,8 +22,6 @@ use tokio::time::sleep; use super::{Attachment, mail::MailMessage, templates}; -const SEND_DELAY: Duration = Duration::from_secs(2); - #[test] fn dg25_8_server_side_template_injection() { let mut tera = templates::safe_tera(); @@ -34,7 +32,7 @@ fn dg25_8_server_side_template_injection() { /// Delay, so send_and_forget() can process the message. async fn delay() { - tokio::time::sleep(Duration::from_secs(2)).await; + sleep(Duration::from_secs(2)).await; } /// Set SMTP settings from environment variables. From 85adecf77d8d9e207bd98eee8fbaa430756e97da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 21 Apr 2026 09:44:56 +0200 Subject: [PATCH 41/46] update existing tests --- crates/defguard_core/src/letsencrypt.rs | 69 +++++++++++++++++++++---- 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/crates/defguard_core/src/letsencrypt.rs b/crates/defguard_core/src/letsencrypt.rs index acaaee92c..4830cae57 100644 --- a/crates/defguard_core/src/letsencrypt.rs +++ b/crates/defguard_core/src/letsencrypt.rs @@ -126,7 +126,7 @@ pub(crate) async fn do_letsencrypt_refresh( ACME_TIMEOUT, call_proxy_trigger_acme( pool, - &proxy.into(), + &proxy, domain.clone(), account_credentials_json, progress_tx, @@ -458,6 +458,7 @@ mod tests { struct MockAcmeServer { port: u16, + server_cert_serial: String, task: JoinHandle<()>, } @@ -468,7 +469,7 @@ mod tests { behavior: MockAcmeBehavior, ) -> Self { init_rustls_crypto_provider(); - let identity = make_server_identity(ca, common_name); + let (identity, server_cert_serial) = make_server_identity(ca, common_name); let listener = TcpListener::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)) .await .expect("failed to bind mock ACME server"); @@ -489,7 +490,11 @@ mod tests { tokio::task::yield_now().await; - Self { port, task } + Self { + port, + server_cert_serial, + task, + } } } @@ -499,7 +504,10 @@ mod tests { } } - fn make_server_identity(ca: &CertificateAuthority<'_>, common_name: &str) -> Identity { + fn make_server_identity( + ca: &CertificateAuthority<'_>, + common_name: &str, + ) -> (Identity, String) { let key_pair = generate_key_pair().expect("failed to generate key pair"); let san = vec![common_name.to_string()]; let dn = vec![(DnType::CommonName, common_name)]; @@ -507,12 +515,15 @@ mod tests { let cert = ca .sign_server_cert(&csr) .expect("failed to sign server cert"); + let serial = defguard_certs::CertificateInfo::from_der(cert.der()) + .expect("failed to parse server cert info") + .serial; let cert_pem = defguard_certs::der_to_pem(cert.der(), PemLabel::Certificate).expect("cert PEM"); let key_pem = defguard_certs::der_to_pem(key_pair.serialize_der().as_slice(), PemLabel::PrivateKey) .expect("key PEM"); - Identity::from_pem(cert_pem, key_pem) + (Identity::from_pem(cert_pem, key_pem), serial) } fn init_rustls_crypto_provider() { @@ -592,9 +603,19 @@ mod tests { certs.save(pool).await.expect("failed to save LE certs"); } - async fn create_proxy(pool: &sqlx::PgPool, address: &str, port: u16) { + async fn create_proxy( + pool: &sqlx::PgPool, + address: &str, + port: u16, + certificate_serial: &str, + core_client_cert: &defguard_certs::CoreClientCert, + ) { let mut proxy = Proxy::new("test-proxy", address, i32::from(port), "tester"); proxy.enabled = true; + proxy.certificate_serial = Some(certificate_serial.to_string()); + proxy.core_client_cert_der = Some(core_client_cert.cert_der.clone()); + proxy.core_client_cert_key_der = Some(core_client_cert.key_der.clone()); + proxy.core_client_cert_expiry = Some(core_client_cert.expiry); proxy.save(pool).await.expect("failed to save proxy"); } @@ -701,7 +722,17 @@ mod tests { }, ) .await; - create_proxy(&pool, "localhost", mock_server.port).await; + let core_client_cert = ca + .issue_core_client_cert("localhost") + .expect("failed to issue core client cert"); + create_proxy( + &pool, + "localhost", + mock_server.port, + &mock_server.server_cert_serial, + &core_client_cert, + ) + .await; let (proxy_control_tx, mut proxy_control_rx) = mpsc::channel(8); let result = do_letsencrypt_refresh(&pool, proxy_control_tx).await; @@ -754,7 +785,17 @@ mod tests { MockAcmeBehavior::RpcError(Status::unavailable("rpc unavailable")), ) .await; - create_proxy(&pool, "localhost", mock_server.port).await; + let core_client_cert = ca + .issue_core_client_cert("localhost") + .expect("failed to issue core client cert"); + create_proxy( + &pool, + "localhost", + mock_server.port, + &mock_server.server_cert_serial, + &core_client_cert, + ) + .await; let (proxy_control_tx, _proxy_control_rx) = mpsc::channel(8); let result = do_letsencrypt_refresh(&pool, proxy_control_tx).await; @@ -778,7 +819,17 @@ mod tests { seed_letsencrypt_cert(&pool, &ca, "localhost", 1).await; let mock_server = MockAcmeServer::start(&ca, "localhost", MockAcmeBehavior::Hang).await; - create_proxy(&pool, "localhost", mock_server.port).await; + let core_client_cert = ca + .issue_core_client_cert("localhost") + .expect("failed to issue core client cert"); + create_proxy( + &pool, + "localhost", + mock_server.port, + &mock_server.server_cert_serial, + &core_client_cert, + ) + .await; let (proxy_control_tx, _proxy_control_rx) = mpsc::channel(8); let result = timeout( From fcdd33414add284f1b1736cba044d49868ed1ee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 21 Apr 2026 11:01:22 +0200 Subject: [PATCH 42/46] update query data --- ...5a53edc5b492a6bb12eac3d97f3ebc5f8506.json} | 50 +++++++++++++------ 1 file changed, 34 insertions(+), 16 deletions(-) rename .sqlx/{query-8a9afd0b7e2f96be85230b35190fab658d841589e7670a4b3fcbc9d53cd1c250.json => query-472e3903cf3df3c5938527c5584a5a53edc5b492a6bb12eac3d97f3ebc5f8506.json} (74%) diff --git a/.sqlx/query-8a9afd0b7e2f96be85230b35190fab658d841589e7670a4b3fcbc9d53cd1c250.json b/.sqlx/query-472e3903cf3df3c5938527c5584a5a53edc5b492a6bb12eac3d97f3ebc5f8506.json similarity index 74% rename from .sqlx/query-8a9afd0b7e2f96be85230b35190fab658d841589e7670a4b3fcbc9d53cd1c250.json rename to .sqlx/query-472e3903cf3df3c5938527c5584a5a53edc5b492a6bb12eac3d97f3ebc5f8506.json index eed19b980..313b1c5a7 100644 --- a/.sqlx/query-8a9afd0b7e2f96be85230b35190fab658d841589e7670a4b3fcbc9d53cd1c250.json +++ b/.sqlx/query-472e3903cf3df3c5938527c5584a5a53edc5b492a6bb12eac3d97f3ebc5f8506.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, name, address, port, connected_at, disconnected_at, version, enabled, certificate_serial, certificate_expiry, modified_at, modified_by FROM proxy", + "query": "SELECT * FROM proxy", "describe": { "columns": [ { @@ -35,33 +35,48 @@ }, { "ordinal": 6, - "name": "version", - "type_info": "Text" + "name": "certificate_expiry", + "type_info": "Timestamp" }, { "ordinal": 7, - "name": "enabled", - "type_info": "Bool" + "name": "version", + "type_info": "Text" }, { "ordinal": 8, - "name": "certificate_serial", - "type_info": "Text" + "name": "modified_at", + "type_info": "Timestamp" }, { "ordinal": 9, - "name": "certificate_expiry", - "type_info": "Timestamp" + "name": "certificate_serial", + "type_info": "Text" }, { "ordinal": 10, - "name": "modified_at", - "type_info": "Timestamp" + "name": "modified_by", + "type_info": "Text" }, { "ordinal": 11, - "name": "modified_by", - "type_info": "Text" + "name": "enabled", + "type_info": "Bool" + }, + { + "ordinal": 12, + "name": "core_client_cert_der", + "type_info": "Bytea" + }, + { + "ordinal": 13, + "name": "core_client_cert_key_der", + "type_info": "Bytea" + }, + { + "ordinal": 14, + "name": "core_client_cert_expiry", + "type_info": "Timestamp" } ], "parameters": { @@ -75,12 +90,15 @@ true, true, true, - false, true, + false, true, false, - false + false, + true, + true, + true ] }, - "hash": "8a9afd0b7e2f96be85230b35190fab658d841589e7670a4b3fcbc9d53cd1c250" + "hash": "472e3903cf3df3c5938527c5584a5a53edc5b492a6bb12eac3d97f3ebc5f8506" } From 2a68d502dc09315e841c1f61ee70d598106c13c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 21 Apr 2026 11:23:26 +0200 Subject: [PATCH 43/46] fix naming --- crates/defguard_certs/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs index 13b990eeb..30654229c 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -147,7 +147,7 @@ impl CertificateAuthority<'_> { Ok(cert) } - /// Issue a Core gRPC client certificate for a specific Gateway or Proxy. + /// Issue a Core gRPC client certificate for a specific Gateway or Edge. /// /// Generates a fresh key pair, creates a CSR with `common_name` as both /// the Subject CN and the SAN DNS name, signs it with `ClientAuth` EKU, @@ -192,7 +192,7 @@ impl CertificateAuthority<'_> { } } -/// A Core gRPC client certificate issued for a specific Gateway or Proxy component. +/// A Core gRPC client certificate issued for a specific Gateway or Edge component. /// /// The DER bytes are stored in the database; the key bytes never leave Core. pub struct CoreClientCert { From 817dd483c6ffdba2bf67a1c754118cac580c9bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 21 Apr 2026 11:48:48 +0200 Subject: [PATCH 44/46] update protos --- proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto b/proto index 7e1b82934..37bed3af7 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 7e1b829343832d261fcfefc4587a795a975dec7e +Subproject commit 37bed3af781d157e7ff808686273f261ec546dac From 9c6c317639953d7e0f53e9ff6261acbc79bfd185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 21 Apr 2026 11:48:59 +0200 Subject: [PATCH 45/46] remove unnecessary sleep --- crates/defguard_core/tests/integration/api/acl/aliases.rs | 8 -------- .../tests/integration/api/acl/destinations.rs | 8 -------- crates/defguard_core/tests/integration/api/acl/rules.rs | 8 -------- 3 files changed, 24 deletions(-) diff --git a/crates/defguard_core/tests/integration/api/acl/aliases.rs b/crates/defguard_core/tests/integration/api/acl/aliases.rs index 8cdcefe09..3ca7cdeca 100644 --- a/crates/defguard_core/tests/integration/api/acl/aliases.rs +++ b/crates/defguard_core/tests/integration/api/acl/aliases.rs @@ -1,7 +1,3 @@ -use std::time::Duration; - -use tokio::time::sleep; - use super::*; #[sqlx::test] @@ -403,8 +399,6 @@ async fn test_alias_audit_fields_track_acting_user_across_mutations( assert_ne!(created_alias_row.modified_by, "admin"); let created_modified_at = created_alias_row.modified_at; - sleep(Duration::from_millis(2)).await; - let mut alias_update = created_alias.clone(); alias_update.name = "alias updated by hpotter".to_string(); let response = client @@ -425,8 +419,6 @@ async fn test_alias_audit_fields_track_acting_user_across_mutations( assert!(updated_alias_row.modified_at > created_modified_at); let updated_modified_at = updated_alias_row.modified_at; - sleep(Duration::from_millis(2)).await; - let response = client .put("/api/v1/acl/alias/apply") .json(&json!({ "aliases": [updated_alias.id] })) diff --git a/crates/defguard_core/tests/integration/api/acl/destinations.rs b/crates/defguard_core/tests/integration/api/acl/destinations.rs index 5771e15df..a170c941a 100644 --- a/crates/defguard_core/tests/integration/api/acl/destinations.rs +++ b/crates/defguard_core/tests/integration/api/acl/destinations.rs @@ -1,7 +1,3 @@ -use std::time::Duration; - -use tokio::time::sleep; - use super::*; #[sqlx::test] @@ -555,8 +551,6 @@ async fn test_destination_audit_fields_track_acting_user_across_mutations( assert_ne!(created_destination_row.modified_by, "admin"); let created_modified_at = created_destination_row.modified_at; - sleep(Duration::from_millis(2)).await; - let mut destination_update = created_destination.clone(); destination_update.name = "destination updated by hpotter".to_string(); let response = client @@ -584,8 +578,6 @@ async fn test_destination_audit_fields_track_acting_user_across_mutations( assert!(updated_destination_row.modified_at > created_modified_at); let updated_modified_at = updated_destination_row.modified_at; - sleep(Duration::from_millis(2)).await; - let response = client .put("/api/v1/acl/destination/apply") .json(&json!({ "destinations": [updated_destination.id] })) diff --git a/crates/defguard_core/tests/integration/api/acl/rules.rs b/crates/defguard_core/tests/integration/api/acl/rules.rs index 7667160cb..188531978 100644 --- a/crates/defguard_core/tests/integration/api/acl/rules.rs +++ b/crates/defguard_core/tests/integration/api/acl/rules.rs @@ -1,7 +1,3 @@ -use std::time::Duration; - -use tokio::time::sleep; - use super::*; use crate::api::PaginatedApiResponse; @@ -1413,8 +1409,6 @@ async fn test_rule_audit_fields_track_acting_user_across_mutations( assert_ne!(created_rule_row.modified_by, "admin"); let created_modified_at = created_rule_row.modified_at; - sleep(Duration::from_millis(2)).await; - let mut updated_rule = created_rule.clone(); updated_rule.name = "rule updated by hpotter".to_string(); let response = client @@ -1433,8 +1427,6 @@ async fn test_rule_audit_fields_track_acting_user_across_mutations( assert!(updated_rule_row.modified_at > created_modified_at); let updated_modified_at = updated_rule_row.modified_at; - sleep(Duration::from_millis(2)).await; - let response = client .put("/api/v1/acl/rule/apply") .json(&json!({ "rules": [created_rule.id] })) From dbd3d407f35c769d0b3c7f31b1df7b3e38915147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Tue, 21 Apr 2026 11:52:46 +0200 Subject: [PATCH 46/46] review fixes --- crates/defguard_certs/src/lib.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/defguard_certs/src/lib.rs b/crates/defguard_certs/src/lib.rs index 30654229c..99d64667e 100644 --- a/crates/defguard_certs/src/lib.rs +++ b/crates/defguard_certs/src/lib.rs @@ -1,4 +1,4 @@ -use std::str::FromStr; +use std::{net::IpAddr, str::FromStr}; use base64::{Engine, prelude::BASE64_STANDARD}; use chrono::NaiveDateTime; @@ -315,7 +315,7 @@ impl Csr<'_> { ))); } - let expected_ip: Option = expected_hostname.parse().ok(); + let expected_ip: Option = expected_hostname.parse().ok(); for san in sans { let matches = match san { @@ -327,7 +327,7 @@ impl Csr<'_> { }; if !matches { return Err(CertificateError::HostnameMismatch(format!( - "CSR SAN does not match expected hostname {expected_hostname:?}" + "CSR SAN does not match expected hostname {expected_hostname}" ))); } } @@ -435,7 +435,7 @@ mod tests { ], ) .unwrap(); - let signed_cert: Certificate = ca.sign_server_cert(&csr).unwrap(); + let signed_cert = ca.sign_server_cert(&csr).unwrap(); assert!(signed_cert.pem().contains("BEGIN CERTIFICATE")); } @@ -451,7 +451,7 @@ mod tests { vec![(rcgen::DnType::CommonName, "example.com")], ) .unwrap(); - let signed_cert: Certificate = ca + let signed_cert = ca .sign_csr_with_validity(&csr, 90, &[ExtendedKeyUsagePurpose::ServerAuth]) .unwrap(); let der = signed_cert.der();