From d93184d41d9849c9f2f764a74567a3968d9a84e7 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Sat, 29 Aug 2026 19:33:43 +0200 Subject: [PATCH] v0.3.2-pre.005 --- Cargo.toml | 9 +- crates/ksp-store-lib/Cargo.toml | 3 +- crates/ksp-store-lib/src/error.rs | 10 +- crates/ksp-store-lib/src/lib.rs | 17 +- crates/ksp-store-lib/src/settings.rs | 9 +- crates/ksp-store-lib/src/store.rs | 153 +++++++-- .../tests/dependency_boundary.rs | 10 +- .../ksp-store-lib/tests/feature_mismatch.rs | 22 +- crates/ksp-store-lib/tests/public_api.rs | 8 +- crates/ksp-store-lib/unit_tests/store.rs | 29 +- crates/ksp-store-postgres-lib/Cargo.toml | 9 +- crates/ksp-store-postgres-lib/src/error.rs | 45 +++ crates/ksp-store-postgres-lib/src/lib.rs | 35 +- crates/ksp-store-postgres-lib/src/runtime.rs | 303 ++++++++++++++++++ .../tests/dependency_boundary.rs | 58 +++- .../tests/public_api.rs | 44 +++ .../unit_tests/runtime.rs | 99 ++++++ deltas/0.3.2/pre.005.md | 135 ++++++++ ...3-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md | 21 +- .../019-V0_3_2_STORE_POSTGRES_FOUNDATION.md | 46 ++- 20 files changed, 945 insertions(+), 120 deletions(-) create mode 100644 crates/ksp-store-postgres-lib/src/error.rs create mode 100644 crates/ksp-store-postgres-lib/src/runtime.rs create mode 100644 crates/ksp-store-postgres-lib/tests/public_api.rs create mode 100644 crates/ksp-store-postgres-lib/unit_tests/runtime.rs create mode 100644 deltas/0.3.2/pre.005.md diff --git a/Cargo.toml b/Cargo.toml index b49acf7..fd6a13c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 336 +# version: 337 [workspace] resolver = "3" members = ["crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"] [workspace.package] -version = "0.3.2-pre.4.fix.1" +version = "0.3.2-pre.5" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" @@ -18,6 +18,7 @@ argon2 = { version = "^0.5", default-features = false } base64 = { version = "^0.23" } chacha20poly1305 = { version = "^0.11", default-features = false } chrono = { version = "^0.4", default-features = false } +deadpool-postgres = { version = "^0.14", default-features = false } directories = { version = "^6.0" } ed25519-dalek = { version = "^3.0", default-features = false } fs2 = { version = "^0.4" } @@ -26,6 +27,8 @@ getrandom = { version = "^0.4", default-features = false } http = { version = "^1.5", default-features = false } jsonschema = { version = "^0.51", default-features = false } reqwest = { version = "^0.13", default-features = false } +rustls = { version = "^0.23", default-features = false } +rustls-native-certs = { version = "^0.8", default-features = false } serde = { version = "^1.0" } serde_json = { version = "^1.0" } solana-keypair = { version = "^3.1", default-features = false } @@ -39,6 +42,8 @@ tracing = { version = "^0.1", default-features = false } tracing-subscriber = { version = "^0.3", default-features = false } tracing-appender = { version = "^0.2", default-features = false } tokio = { version = "^1.53", default-features = false } +tokio-postgres = { version = "^0.7", default-features = false } +tokio-postgres-rustls = { version = "^0.14", default-features = false } tokio-tungstenite = { version = "^0.30", default-features = false } tonic = { version = "^0.14", default-features = false } tonic-prost = { version = "^0.14", default-features = false } diff --git a/crates/ksp-store-lib/Cargo.toml b/crates/ksp-store-lib/Cargo.toml index 5ee26aa..fde8e0a 100644 --- a/crates/ksp-store-lib/Cargo.toml +++ b/crates/ksp-store-lib/Cargo.toml @@ -1,5 +1,5 @@ # file: crates/ksp-store-lib/Cargo.toml -# version: 1 +# version: 2 [package] name = "ksp-store-lib" @@ -12,6 +12,7 @@ default = ["postgres"] postgres = ["dep:ksp-store-postgres-lib"] [dependencies] +ksp-logging-lib = { path = "../ksp-logging-lib" } ksp-store-api = { path = "../ksp-store-api" } ksp-store-postgres-lib = { path = "../ksp-store-postgres-lib", optional = true } diff --git a/crates/ksp-store-lib/src/error.rs b/crates/ksp-store-lib/src/error.rs index 2a50c05..605ad65 100644 --- a/crates/ksp-store-lib/src/error.rs +++ b/crates/ksp-store-lib/src/error.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/src/error.rs -// version: 1 +// version: 2 /// Error code reserved for operations attempted after a Store backend has entered its closed state. pub const ERROR_CODE_BACKEND_CLOSED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "backend_closed"); @@ -7,6 +7,14 @@ pub const ERROR_CODE_BACKEND_CLOSED: ksp_store_api::ErrorCode = ksp_store_api::E pub const ERROR_CODE_BACKEND_NOT_COMPILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "backend_not_compiled"); /// Error code used when a compiled Store backend cannot complete its bounded opening lifecycle. pub const ERROR_CODE_BACKEND_OPEN_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "backend_open_failed"); +/// Error code used when the PostgreSQL backend rejects or cannot normalize its physical connection configuration. +pub const ERROR_CODE_POSTGRES_CONFIG_INVALID: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_config_invalid"); +/// Error code used when PostgreSQL physical connection establishment fails without exposing remote or credential details. +pub const ERROR_CODE_POSTGRES_CONNECT_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_connect_failed"); +/// Error code used when a bounded PostgreSQL pool wait, create or recycle operation reaches its deadline. +pub const ERROR_CODE_POSTGRES_POOL_TIMEOUT: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_pool_timeout"); +/// Error code used when verified PostgreSQL TLS setup or negotiation cannot be completed safely. +pub const ERROR_CODE_POSTGRES_TLS_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_tls_failed"); /// Error code used when backend-neutral Store settings violate runtime bounds or invariants. pub const ERROR_CODE_SETTINGS_INVALID: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "settings_invalid"); /// Error code used when a Store cannot complete its explicit shutdown inside the configured bound. diff --git a/crates/ksp-store-lib/src/lib.rs b/crates/ksp-store-lib/src/lib.rs index 1712715..04b3f3d 100644 --- a/crates/ksp-store-lib/src/lib.rs +++ b/crates/ksp-store-lib/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/src/lib.rs -// version: 3 +// version: 4 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -7,10 +7,9 @@ //! Common backend-neutral Store runtime facade for KSP. //! -//! `0.3.2-pre.003` materializes Config-independent settings, stable backend -//! identity, bounded validation and the opaque async Store lifecycle contract. -//! Physical PostgreSQL connection, pool, TLS and migrations remain private -//! future slices of this release. +//! `0.3.2-pre.005` materializes the first physical PostgreSQL runtime path: +//! one bounded Deadpool pool, one startup connection proof and explicit TLS +//! policy. SQL migrations remain private future slices of this release. //! //! The default `postgres` feature compiles the official PostgreSQL backend as //! an optional implementation dependency. No backend implementation type is @@ -27,6 +26,14 @@ pub use self::error::ERROR_CODE_BACKEND_CLOSED; pub use self::error::ERROR_CODE_BACKEND_NOT_COMPILED; /// Error code used when a compiled Store backend cannot complete opening. pub use self::error::ERROR_CODE_BACKEND_OPEN_FAILED; +/// Error code used when PostgreSQL physical configuration is malformed or unsupported. +pub use self::error::ERROR_CODE_POSTGRES_CONFIG_INVALID; +/// Error code used when PostgreSQL physical connection establishment fails. +pub use self::error::ERROR_CODE_POSTGRES_CONNECT_FAILED; +/// Error code used when a bounded PostgreSQL pool operation reaches its deadline. +pub use self::error::ERROR_CODE_POSTGRES_POOL_TIMEOUT; +/// Error code used when PostgreSQL verified TLS setup or negotiation fails. +pub use self::error::ERROR_CODE_POSTGRES_TLS_FAILED; /// Error code used when Store settings violate backend-neutral bounds or invariants. pub use self::error::ERROR_CODE_SETTINGS_INVALID; /// Error code used when explicit Store shutdown exceeds its configured deadline. diff --git a/crates/ksp-store-lib/src/settings.rs b/crates/ksp-store-lib/src/settings.rs index d39530f..948e022 100644 --- a/crates/ksp-store-lib/src/settings.rs +++ b/crates/ksp-store-lib/src/settings.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/src/settings.rs -// version: 2 +// version: 3 const DEFAULT_CONNECT_TIMEOUT_MS: u64 = 10_000; const DEFAULT_MAX_CONNECTIONS: u32 = 8; @@ -239,6 +239,13 @@ impl PostgresStoreSettings { return self.bootstrap; } + /// Returns the sensitive PostgreSQL connection URI only to the compiled backend bridge. + #[cfg(feature = "postgres")] + #[must_use] + pub(crate) fn connection_uri(&self) -> &str { + return self.connection_uri.as_str(); + } + /// Returns the PostgreSQL pool settings without exposing the sensitive connection URI. #[must_use] pub const fn pool(&self) -> PostgresPoolSettings { diff --git a/crates/ksp-store-lib/src/store.rs b/crates/ksp-store-lib/src/store.rs index 652d216..8e5616e 100644 --- a/crates/ksp-store-lib/src/store.rs +++ b/crates/ksp-store-lib/src/store.rs @@ -1,54 +1,155 @@ // file: crates/ksp-store-lib/src/store.rs -// version: 1 +// version: 2 /// Opaque common Store runtime facade. /// -/// A successful value is returned only after the selected backend has completed its bounded readiness path. `0.3.2-pre.003` establishes the lifecycle -/// contract but intentionally has no successful opening path until the PostgreSQL runtime foundation is materialized by later prereleases. -#[derive(Debug)] -#[non_exhaustive] -pub struct Store; +/// A successful value is returned only after the selected compiled backend has completed its bounded physical opening path. PostgreSQL pool, client, TLS and +/// driver types remain private to the backend crate. +pub struct Store { + backend_kind: crate::StoreBackendKind, + network: ksp_store_api::RawNetworkId, + #[cfg(feature = "postgres")] + runtime: StoreRuntime, + shutdown_timeout: std::time::Duration, +} impl Store { - /// Validates settings, selects the requested backend and opens a ready Store runtime. + /// Validates settings, selects the requested backend and opens one ready Store instance for exactly one logical network. /// - /// A known backend whose Cargo feature is absent is rejected before any I/O. During `0.3.2-pre.003`, the compiled PostgreSQL path also stops before I/O - /// with [`crate::ERROR_CODE_BACKEND_OPEN_FAILED`] because physical connection ownership is introduced in `pre.005`. + /// A known backend whose Cargo feature is absent is rejected before any I/O. A successful PostgreSQL result proves that one physical pooled connection has + /// been established under the typed TLS and timeout policy. pub async fn open(settings: crate::StoreSettings) -> ksp_store_api::Result { let validation = settings.validate(); if let std::result::Result::Err(error) = validation { return std::result::Result::Err(error); } let backend_kind = settings.backend_kind(); - return match backend_kind { - crate::StoreBackendKind::Postgres => open_postgres_contract(backend_kind).await, + let network = settings.network().clone(); + let shutdown_timeout = settings.shutdown_timeout(); + ksp_logging_lib::debug!( + target: crate::TRACING_TARGET, + backend = backend_kind.code(), + network = network.as_str(), + "opening Store runtime" + ); + return match settings.backend() { + crate::StoreBackendSettings::Postgres(postgres) => open_postgres(backend_kind, network, shutdown_timeout, postgres).await, }; } - /// Explicitly closes the Store runtime and consumes its facade handle. - /// - /// `pre.003` cannot yet produce a successful Store instance, so the physical bounded shutdown path remains reserved for backend composition. The consuming - /// async signature is fixed here so no pool or backend handle needs to escape later. + /// Explicitly closes the Store runtime, consumes its facade handle and applies the configured bounded shutdown deadline. pub async fn close(self) -> ksp_store_api::Result<()> { - return std::result::Result::Ok(()); + let backend_kind = self.backend_kind; + #[cfg(feature = "postgres")] + { + let network = self.network; + let shutdown_timeout = self.shutdown_timeout; + let result = match self.runtime { + StoreRuntime::Postgres(backend) => backend.close(shutdown_timeout).await, + }; + return match result { + std::result::Result::Ok(()) => { + ksp_logging_lib::debug!( + target: crate::TRACING_TARGET, + backend = backend_kind.code(), + network = network.as_str(), + "Store runtime closed" + ); + std::result::Result::Ok(()) + }, + std::result::Result::Err(error) => std::result::Result::Err(map_postgres_error(error, backend_kind, network.as_str())), + }; + } + #[cfg(not(feature = "postgres"))] + { + return std::result::Result::Err(unavailable_runtime_error(backend_kind)); + } + } +} + +impl std::fmt::Debug for Store { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("Store") + .field("backend_kind", &self.backend_kind) + .field("network", &self.network) + .field("shutdown_timeout", &self.shutdown_timeout) + .finish_non_exhaustive(); } } #[cfg(feature = "postgres")] -async fn open_postgres_contract(backend_kind: crate::StoreBackendKind) -> ksp_store_api::Result { - return std::result::Result::Err( - ksp_store_api::Error::new(crate::ERROR_CODE_BACKEND_OPEN_FAILED, "PostgreSQL Store runtime opening is not materialized in this prerelease") - .with_context("backend", backend_kind.code()) - .with_context("stage", "runtime_foundation_pending"), +enum StoreRuntime { + Postgres(ksp_store_postgres_lib::PostgresBackend), +} + +#[cfg(feature = "postgres")] +async fn open_postgres( + backend_kind: crate::StoreBackendKind, + network: ksp_store_api::RawNetworkId, + shutdown_timeout: std::time::Duration, + settings: &crate::PostgresStoreSettings, +) -> ksp_store_api::Result { + let pool = settings.pool(); + let tls_mode = match settings.tls_mode() { + crate::PostgresTlsMode::Disabled => ksp_store_postgres_lib::PostgresBackendTlsMode::Disabled, + crate::PostgresTlsMode::VerifyFull => ksp_store_postgres_lib::PostgresBackendTlsMode::VerifyFull, + }; + let backend_settings = ksp_store_postgres_lib::PostgresBackendSettings::new( + network.clone(), + settings.connection_uri(), + pool.max_connections(), + pool.connect_timeout(), + pool.wait_timeout(), + pool.create_timeout(), + pool.recycle_timeout(), + tls_mode, ); + let opened = ksp_store_postgres_lib::PostgresBackend::open(backend_settings).await; + return match opened { + std::result::Result::Ok(backend) => { + ksp_logging_lib::debug!( + target: crate::TRACING_TARGET, + backend = backend_kind.code(), + network = network.as_str(), + "Store backend is physically ready" + ); + std::result::Result::Ok(Store { backend_kind, network, runtime: StoreRuntime::Postgres(backend), shutdown_timeout }) + }, + std::result::Result::Err(error) => std::result::Result::Err(map_postgres_error(error, backend_kind, network.as_str())), + }; } #[cfg(not(feature = "postgres"))] -async fn open_postgres_contract(backend_kind: crate::StoreBackendKind) -> ksp_store_api::Result { - return std::result::Result::Err( - ksp_store_api::Error::new(crate::ERROR_CODE_BACKEND_NOT_COMPILED, "Selected Store backend is not compiled") - .with_context("backend", backend_kind.code()), - ); +async fn open_postgres( + backend_kind: crate::StoreBackendKind, + _network: ksp_store_api::RawNetworkId, + _shutdown_timeout: std::time::Duration, + _settings: &crate::PostgresStoreSettings, +) -> ksp_store_api::Result { + return std::result::Result::Err(unavailable_runtime_error(backend_kind)); +} + +#[cfg(feature = "postgres")] +fn map_postgres_error(error: ksp_store_postgres_lib::PostgresBackendError, backend_kind: crate::StoreBackendKind, network: &str) -> ksp_store_api::Error { + let code = match error.kind() { + ksp_store_postgres_lib::PostgresBackendErrorKind::ConfigInvalid => crate::ERROR_CODE_POSTGRES_CONFIG_INVALID, + ksp_store_postgres_lib::PostgresBackendErrorKind::ConnectFailed => crate::ERROR_CODE_POSTGRES_CONNECT_FAILED, + ksp_store_postgres_lib::PostgresBackendErrorKind::PoolTimeout => crate::ERROR_CODE_POSTGRES_POOL_TIMEOUT, + ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout => crate::ERROR_CODE_SHUTDOWN_TIMEOUT, + ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed => crate::ERROR_CODE_POSTGRES_TLS_FAILED, + _ => crate::ERROR_CODE_BACKEND_OPEN_FAILED, + }; + return ksp_store_api::Error::new(code, "PostgreSQL Store backend lifecycle operation failed") + .with_context("backend", backend_kind.code()) + .with_context("network", network) + .with_context("phase", error.phase()); +} + +#[cfg(not(feature = "postgres"))] +fn unavailable_runtime_error(backend_kind: crate::StoreBackendKind) -> ksp_store_api::Error { + return ksp_store_api::Error::new(crate::ERROR_CODE_BACKEND_NOT_COMPILED, "Selected Store backend is not compiled") + .with_context("backend", backend_kind.code()); } #[cfg(test)] diff --git a/crates/ksp-store-lib/tests/dependency_boundary.rs b/crates/ksp-store-lib/tests/dependency_boundary.rs index e0beaf5..ddd0129 100644 --- a/crates/ksp-store-lib/tests/dependency_boundary.rs +++ b/crates/ksp-store-lib/tests/dependency_boundary.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/tests/dependency_boundary.rs -// version: 4 +// version: 5 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -8,10 +8,11 @@ //! Cargo-feature and dependency-boundary canaries for the common Store runtime facade. #[test] -fn pre_002_manifest_owns_default_postgres_feature_and_optional_backend_edge() { +fn pre_005_manifest_keeps_backend_physical_dependencies_out_of_facade() { let manifest = include_str!("../Cargo.toml"); assert!(manifest.contains("default = [\"postgres\"]")); assert!(manifest.contains("postgres = [\"dep:ksp-store-postgres-lib\"]")); + assert!(manifest.contains("ksp-logging-lib = { path = \"../ksp-logging-lib\" }")); assert!(manifest.contains("ksp-store-api = { path = \"../ksp-store-api\" }")); assert!(manifest.contains("ksp-store-postgres-lib = { path = \"../ksp-store-postgres-lib\", optional = true }")); for forbidden in [ @@ -30,11 +31,8 @@ fn pre_002_manifest_owns_default_postgres_feature_and_optional_backend_edge() { } #[test] -fn pre_003_facade_adds_only_backend_neutral_settings_lifecycle_and_api_reexports() { +fn pre_005_facade_exposes_no_physical_postgres_types_or_environment_bypass() { let crate_root = include_str!("../src/lib.rs"); - assert!(crate_root.contains("mod error;")); - assert!(crate_root.contains("mod settings;")); - assert!(crate_root.contains("mod store;")); assert!(crate_root.contains("pub use self::settings::StoreSettings;")); assert!(crate_root.contains("pub use self::store::Store;")); assert!(crate_root.contains("pub use ksp_store_api::RawTransaction;")); diff --git a/crates/ksp-store-lib/tests/feature_mismatch.rs b/crates/ksp-store-lib/tests/feature_mismatch.rs index 1b8b6f7..5f21ee9 100644 --- a/crates/ksp-store-lib/tests/feature_mismatch.rs +++ b/crates/ksp-store-lib/tests/feature_mismatch.rs @@ -1,11 +1,11 @@ // file: crates/ksp-store-lib/tests/feature_mismatch.rs -// version: 3 +// version: 4 #![warn(missing_docs)] #![deny(unreachable_pub)] #![forbid(unsafe_code)] -//! Feature-selection canaries for known Store backends. +//! Feature-selection and pre-I/O PostgreSQL failure canaries for the common Store facade. fn poll_ready(future: impl std::future::Future) -> T { let mut future = std::boxed::Box::pin(future); @@ -13,7 +13,7 @@ fn poll_ready(future: impl std::future::Future) -> T { let mut context = std::task::Context::from_waker(waker); return match std::future::Future::poll(future.as_mut(), &mut context) { std::task::Poll::Ready(value) => value, - std::task::Poll::Pending => panic!("pre.003 Store feature-dispatch future unexpectedly became pending before any I/O exists"), + std::task::Poll::Pending => panic!("Store pre-I/O feature/config rejection unexpectedly became pending"), }; } @@ -24,9 +24,9 @@ fn valid_network() -> ksp_store_lib::RawNetworkId { }; } -fn settings() -> ksp_store_lib::StoreSettings { +fn settings(connection_uri: &str) -> ksp_store_lib::StoreSettings { let postgres = ksp_store_lib::PostgresStoreSettings::new( - "postgresql://operator-supplied-sensitive-value", + connection_uri, ksp_store_lib::PostgresPoolSettings::default(), ksp_store_lib::PostgresTlsMode::Disabled, ksp_store_lib::PostgresBootstrapSettings::default(), @@ -36,8 +36,8 @@ fn settings() -> ksp_store_lib::StoreSettings { #[cfg(not(feature = "postgres"))] #[test] -fn pre_003_known_postgres_without_feature_returns_stable_error_before_io() { - let result = poll_ready(ksp_store_lib::Store::open(settings())); +fn pre_005_known_postgres_without_feature_returns_stable_error_before_io() { + let result = poll_ready(ksp_store_lib::Store::open(settings("postgresql://operator-supplied-sensitive-value@localhost/ksp"))); let error = result.err(); assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(ksp_store_lib::ERROR_CODE_BACKEND_NOT_COMPILED)); return; @@ -45,9 +45,11 @@ fn pre_003_known_postgres_without_feature_returns_stable_error_before_io() { #[cfg(feature = "postgres")] #[test] -fn pre_003_compiled_postgres_path_refuses_to_fake_readiness_before_pre_005() { - let result = poll_ready(ksp_store_lib::Store::open(settings())); +fn pre_005_compiled_postgres_rejects_malformed_uri_without_secret_leak_before_io() { + let secret_canary = "not-a-postgresql-uri-secret-canary"; + let result = poll_ready(ksp_store_lib::Store::open(settings(secret_canary))); let error = result.err(); - assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(ksp_store_lib::ERROR_CODE_BACKEND_OPEN_FAILED)); + assert_eq!(error.as_ref().map(|value| return value.code()), std::option::Option::Some(ksp_store_lib::ERROR_CODE_POSTGRES_CONFIG_INVALID)); + assert!(!format!("{error:?}").contains(secret_canary)); return; } diff --git a/crates/ksp-store-lib/tests/public_api.rs b/crates/ksp-store-lib/tests/public_api.rs index 86ac309..7d5a09b 100644 --- a/crates/ksp-store-lib/tests/public_api.rs +++ b/crates/ksp-store-lib/tests/public_api.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/tests/public_api.rs -// version: 2 +// version: 3 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -29,11 +29,15 @@ fn pre_003_settings_and_lifecycle_contract_are_available_from_crate_root() { } #[test] -fn pre_003_common_error_codes_are_stable_and_store_owned() { +fn pre_005_common_and_postgres_error_codes_are_stable_and_store_owned() { assert_eq!(ksp_store_lib::ERROR_CODE_SETTINGS_INVALID.domain(), "store"); assert_eq!(ksp_store_lib::ERROR_CODE_SETTINGS_INVALID.code(), "settings_invalid"); assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_NOT_COMPILED.code(), "backend_not_compiled"); assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_OPEN_FAILED.code(), "backend_open_failed"); + assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_CONFIG_INVALID.code(), "postgres_config_invalid"); + assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_CONNECT_FAILED.code(), "postgres_connect_failed"); + assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_POOL_TIMEOUT.code(), "postgres_pool_timeout"); + assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_TLS_FAILED.code(), "postgres_tls_failed"); assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_CLOSED.code(), "backend_closed"); assert_eq!(ksp_store_lib::ERROR_CODE_SHUTDOWN_TIMEOUT.code(), "shutdown_timeout"); return; diff --git a/crates/ksp-store-lib/unit_tests/store.rs b/crates/ksp-store-lib/unit_tests/store.rs index 01a186b..07c3891 100644 --- a/crates/ksp-store-lib/unit_tests/store.rs +++ b/crates/ksp-store-lib/unit_tests/store.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/unit_tests/store.rs -// version: 3 +// version: 4 fn poll_ready(future: impl std::future::Future) -> T { let mut future = std::boxed::Box::pin(future); @@ -7,7 +7,7 @@ fn poll_ready(future: impl std::future::Future) -> T { let mut context = std::task::Context::from_waker(waker); return match std::future::Future::poll(future.as_mut(), &mut context) { std::task::Poll::Ready(value) => value, - std::task::Poll::Pending => panic!("pre.003 Store contract future unexpectedly became pending before any I/O exists"), + std::task::Poll::Pending => panic!("Store pre-I/O rejection unexpectedly became pending"), }; } @@ -18,11 +18,11 @@ fn valid_network() -> crate::RawNetworkId { }; } -fn valid_store_settings() -> crate::StoreSettings { +fn store_settings(connection_uri: &str) -> crate::StoreSettings { let postgres = crate::PostgresStoreSettings::new( - "postgresql://secret-user:secret-password@db.internal/ksp", + connection_uri, crate::PostgresPoolSettings::default(), - crate::PostgresTlsMode::VerifyFull, + crate::PostgresTlsMode::Disabled, crate::PostgresBootstrapSettings::default(), ); return crate::StoreSettings::with_default_shutdown(valid_network(), crate::StoreBackendSettings::Postgres(postgres)); @@ -30,14 +30,7 @@ fn valid_store_settings() -> crate::StoreSettings { #[test] fn invalid_settings_are_rejected_before_backend_dispatch() { - let postgres = crate::PostgresStoreSettings::new( - std::string::String::new(), - crate::PostgresPoolSettings::default(), - crate::PostgresTlsMode::Disabled, - crate::PostgresBootstrapSettings::default(), - ); - let settings = crate::StoreSettings::with_default_shutdown(valid_network(), crate::StoreBackendSettings::Postgres(postgres)); - let result = poll_ready(crate::Store::open(settings)); + let result = poll_ready(crate::Store::open(store_settings(""))); let error = result.err(); assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_SETTINGS_INVALID)); return; @@ -45,17 +38,19 @@ fn invalid_settings_are_rejected_before_backend_dispatch() { #[cfg(feature = "postgres")] #[test] -fn compiled_postgres_dispatch_does_not_fake_readiness_before_connection_materialization() { - let result = poll_ready(crate::Store::open(valid_store_settings())); +fn compiled_postgres_rejects_malformed_physical_configuration_before_io() { + let secret_canary = "not-a-postgresql-uri-secret-canary"; + let result = poll_ready(crate::Store::open(store_settings(secret_canary))); let error = result.err(); - assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_BACKEND_OPEN_FAILED)); + assert_eq!(error.as_ref().map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_POSTGRES_CONFIG_INVALID)); + assert!(!format!("{error:?}").contains(secret_canary)); return; } #[cfg(not(feature = "postgres"))] #[test] fn known_postgres_without_feature_is_rejected_before_io() { - let result = poll_ready(crate::Store::open(valid_store_settings())); + let result = poll_ready(crate::Store::open(store_settings("postgresql://operator-supplied-sensitive-value@localhost/ksp"))); let error = result.err(); assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_BACKEND_NOT_COMPILED)); return; diff --git a/crates/ksp-store-postgres-lib/Cargo.toml b/crates/ksp-store-postgres-lib/Cargo.toml index 1122639..e48a602 100644 --- a/crates/ksp-store-postgres-lib/Cargo.toml +++ b/crates/ksp-store-postgres-lib/Cargo.toml @@ -1,5 +1,5 @@ # file: crates/ksp-store-postgres-lib/Cargo.toml -# version: 1 +# version: 2 [package] name = "ksp-store-postgres-lib" @@ -8,7 +8,14 @@ edition.workspace = true repository.workspace = true [dependencies] +deadpool-postgres = { workspace = true, features = ["rt_tokio_1"] } +ksp-logging-lib = { path = "../ksp-logging-lib" } ksp-store-api = { path = "../ksp-store-api" } +rustls = { workspace = true, features = ["aws_lc_rs", "std", "tls12"] } +rustls-native-certs.workspace = true +tokio = { workspace = true, features = ["rt", "time"] } +tokio-postgres = { workspace = true, features = ["runtime"] } +tokio-postgres-rustls = { workspace = true, features = ["aws-lc-rs"] } [lints] workspace = true diff --git a/crates/ksp-store-postgres-lib/src/error.rs b/crates/ksp-store-postgres-lib/src/error.rs new file mode 100644 index 0000000..13c6871 --- /dev/null +++ b/crates/ksp-store-postgres-lib/src/error.rs @@ -0,0 +1,45 @@ +// file: crates/ksp-store-postgres-lib/src/error.rs +// version: 1 + +/// Safe backend-local classification used by the Store facade for stable error mapping. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum PostgresBackendErrorKind { + /// The supplied physical PostgreSQL configuration is unsupported or malformed. + ConfigInvalid, + /// A physical PostgreSQL connection could not be established. + ConnectFailed, + /// A bounded pool wait, create or recycle operation reached its deadline. + PoolTimeout, + /// Explicit backend shutdown did not drain inside the supplied deadline. + ShutdownTimeout, + /// Verified TLS configuration or negotiation could not be established. + TlsFailed, +} + +/// Redacted PostgreSQL backend error carrying only a safe classification and static lifecycle phase. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct PostgresBackendError { + kind: PostgresBackendErrorKind, + phase: &'static str, +} + +impl PostgresBackendError { + /// Creates one backend error without retaining external error text or sensitive connection material. + #[must_use] + pub(crate) const fn new(kind: PostgresBackendErrorKind, phase: &'static str) -> Self { + return Self { kind, phase }; + } + + /// Returns the safe backend-local error classification. + #[must_use] + pub const fn kind(&self) -> PostgresBackendErrorKind { + return self.kind; + } + + /// Returns the static safe lifecycle phase associated with the failure. + #[must_use] + pub const fn phase(&self) -> &'static str { + return self.phase; + } +} diff --git a/crates/ksp-store-postgres-lib/src/lib.rs b/crates/ksp-store-postgres-lib/src/lib.rs index 462eb3a..f39bc8d 100644 --- a/crates/ksp-store-postgres-lib/src/lib.rs +++ b/crates/ksp-store-postgres-lib/src/lib.rs @@ -1,25 +1,36 @@ // file: crates/ksp-store-postgres-lib/src/lib.rs -// version: 2 +// version: 3 #![warn(missing_docs)] #![deny(unreachable_pub)] #![forbid(unsafe_code)] -//! Official PostgreSQL backend implementation scaffold for KSP Store. +//! Official PostgreSQL backend implementation for KSP Store. //! -//! `0.3.2-pre.002` establishes only the backend crate boundary. PostgreSQL -//! driver, pooling, TLS, SQL, migrations and health behavior are intentionally -//! absent until their dedicated prereleases. +//! `0.3.2-pre.005` owns the physical `tokio-postgres` connection, bounded +//! Deadpool pool and explicit Rustls TLS policy. SQL migrations and business +//! persistence remain absent until their dedicated prereleases. //! -//! This crate depends on `ksp-store-api` and never on `ksp-store-lib`, which -//! keeps backend implementation ownership acyclic and reusable behind the -//! common facade. +//! This crate depends on `ksp-store-api` and never on `ksp-store-lib`. The +//! common facade consumes only this crate's narrow backend bridge and never +//! exposes PostgreSQL pool, client, row or statement types. mod constants; +mod error; +mod runtime; -/// Crate-owned tracing target reserved for later PostgreSQL backend behavior. +/// Safe backend-local error returned to the common Store facade. +pub use self::error::PostgresBackendError; +/// Safe backend-local error classification used by the common Store facade. +pub use self::error::PostgresBackendErrorKind; +/// Opaque physical PostgreSQL backend owning its connection pool. +pub use self::runtime::PostgresBackend; +/// Physical PostgreSQL settings bridge consumed only by the backend crate. +pub use self::runtime::PostgresBackendSettings; +/// TLS mode accepted by the physical PostgreSQL settings bridge. +pub use self::runtime::PostgresBackendTlsMode; + +/// Crate-owned tracing target for PostgreSQL backend behavior. pub(crate) use self::constants::TRACING_TARGET; -// Keep the mandatory crate-owned tracing target part of the compiled scaffold -// without inventing runtime logging before the first behavioral tranche. -const _: &str = TRACING_TARGET; +const _: &str = crate::TRACING_TARGET; diff --git a/crates/ksp-store-postgres-lib/src/runtime.rs b/crates/ksp-store-postgres-lib/src/runtime.rs new file mode 100644 index 0000000..1106c52 --- /dev/null +++ b/crates/ksp-store-postgres-lib/src/runtime.rs @@ -0,0 +1,303 @@ +// file: crates/ksp-store-postgres-lib/src/runtime.rs +// version: 1 + +const APPLICATION_NAME: &str = "ksp-store"; +const MAX_CONNECTION_URI_BYTES: usize = 4_096; +const SHUTDOWN_POLL_INTERVAL_MS: u64 = 10; + +/// TLS mode accepted by the physical PostgreSQL backend bridge. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum PostgresBackendTlsMode { + /// Disable TLS for the selected PostgreSQL target. + Disabled, + /// Require TLS with system-root trust and server-identity verification. + VerifyFull, +} + +impl PostgresBackendTlsMode { + /// Returns the stable safe TLS mode code used only in diagnostics. + #[must_use] + pub const fn code(&self) -> &'static str { + return match self { + Self::Disabled => "disabled", + Self::VerifyFull => "verify_full", + }; + } +} + +/// Physical settings consumed only by the PostgreSQL backend crate. +pub struct PostgresBackendSettings { + connect_timeout: std::time::Duration, + connection_uri: std::string::String, + create_timeout: std::time::Duration, + max_connections: u32, + network: ksp_store_api::RawNetworkId, + recycle_timeout: std::time::Duration, + tls_mode: PostgresBackendTlsMode, + wait_timeout: std::time::Duration, +} + +impl PostgresBackendSettings { + /// Creates the physical PostgreSQL settings bridge from already validated facade-owned values. + #[must_use] + pub fn new( + network: ksp_store_api::RawNetworkId, + connection_uri: impl std::convert::Into, + max_connections: u32, + connect_timeout: std::time::Duration, + wait_timeout: std::time::Duration, + create_timeout: std::time::Duration, + recycle_timeout: std::time::Duration, + tls_mode: PostgresBackendTlsMode, + ) -> Self { + return Self { + connect_timeout, + connection_uri: connection_uri.into(), + create_timeout, + max_connections, + network, + recycle_timeout, + tls_mode, + wait_timeout, + }; + } + + /// Returns the logical network bound to this one backend instance. + #[must_use] + pub const fn network(&self) -> &ksp_store_api::RawNetworkId { + return &self.network; + } + + /// Returns the selected safe TLS mode. + #[must_use] + pub const fn tls_mode(&self) -> PostgresBackendTlsMode { + return self.tls_mode; + } +} + +impl std::fmt::Debug for PostgresBackendSettings { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter + .debug_struct("PostgresBackendSettings") + .field("network", &self.network) + .field("connection_uri", &"") + .field("max_connections", &self.max_connections) + .field("connect_timeout", &self.connect_timeout) + .field("wait_timeout", &self.wait_timeout) + .field("create_timeout", &self.create_timeout) + .field("recycle_timeout", &self.recycle_timeout) + .field("tls_mode", &self.tls_mode) + .finish(); + } +} + +/// Opaque physical PostgreSQL backend owning the bounded Deadpool connection pool. +pub struct PostgresBackend { + network: ksp_store_api::RawNetworkId, + pool: deadpool_postgres::Pool, +} + +impl PostgresBackend { + /// Parses and normalizes one supplied URI, builds a bounded pool and proves one physical connection before returning readiness. + pub async fn open(settings: PostgresBackendSettings) -> std::result::Result { + let normalized = normalized_config(&settings); + let pg_config = match normalized { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + ksp_logging_lib::debug!( + target: crate::TRACING_TARGET, + network = settings.network().as_str(), + tls_mode = settings.tls_mode().code(), + max_connections = settings.max_connections, + "opening PostgreSQL Store backend pool" + ); + let pool_result = match settings.tls_mode { + PostgresBackendTlsMode::Disabled => build_pool(pg_config, tokio_postgres::NoTls, &settings), + PostgresBackendTlsMode::VerifyFull => { + let tls_result = build_verified_tls(); + let tls = match tls_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + build_pool(pg_config, tls, &settings) + }, + }; + let pool = match pool_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let probe = pool.get().await; + match probe { + std::result::Result::Ok(client) => drop(client), + std::result::Result::Err(error) => return std::result::Result::Err(map_pool_error(error)), + } + ksp_logging_lib::debug!( + target: crate::TRACING_TARGET, + network = settings.network().as_str(), + tls_mode = settings.tls_mode().code(), + "PostgreSQL Store backend established initial physical connection" + ); + return std::result::Result::Ok(Self { network: settings.network, pool }); + } + + /// Explicitly closes the pool and waits for all owned pooled objects to drain inside the supplied bound. + pub async fn close(self, timeout: std::time::Duration) -> std::result::Result<(), crate::PostgresBackendError> { + self.pool.close(); + let drain = async { + loop { + if self.pool.status().size == 0 { + return; + } + tokio::time::sleep(std::time::Duration::from_millis(SHUTDOWN_POLL_INTERVAL_MS)).await; + } + }; + let result = tokio::time::timeout(timeout, drain).await; + return match result { + std::result::Result::Ok(()) => { + ksp_logging_lib::debug!(target: crate::TRACING_TARGET, network = self.network.as_str(), "PostgreSQL Store backend pool closed"); + std::result::Result::Ok(()) + }, + std::result::Result::Err(_) => { + std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ShutdownTimeout, "pool_drain")) + }, + }; + } +} + +impl std::fmt::Debug for PostgresBackend { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + return formatter.debug_struct("PostgresBackend").field("network", &self.network).field("state", &"open").finish(); + } +} + +impl std::ops::Drop for PostgresBackend { + fn drop(&mut self) { + self.pool.close(); + } +} + +fn normalized_config(settings: &PostgresBackendSettings) -> std::result::Result { + if settings.connection_uri.is_empty() || settings.connection_uri.len() > MAX_CONNECTION_URI_BYTES { + return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "connection_uri")); + } + let parsed = settings.connection_uri.parse::(); + let mut config = match parsed { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "connection_uri")); + }, + }; + if config.get_options().is_some() { + return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "server_options")); + } + if config.get_hosts().is_empty() && config.get_hostaddrs().is_empty() { + return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "host")); + } + if settings.tls_mode == PostgresBackendTlsMode::VerifyFull { + if config.get_hosts().is_empty() { + return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "tls_server_identity")); + } + for host in config.get_hosts() { + if !is_tcp_host(host) { + return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "tls_server_identity")); + } + } + } + config.application_name(APPLICATION_NAME); + config.connect_timeout(settings.connect_timeout); + config.ssl_negotiation(tokio_postgres::config::SslNegotiation::Postgres); + match settings.tls_mode { + PostgresBackendTlsMode::Disabled => { + config.ssl_mode(tokio_postgres::config::SslMode::Disable); + }, + PostgresBackendTlsMode::VerifyFull => { + config.ssl_mode(tokio_postgres::config::SslMode::Require); + }, + } + return std::result::Result::Ok(config); +} + +fn is_tcp_host(host: &tokio_postgres::config::Host) -> bool { + return match host { + tokio_postgres::config::Host::Tcp(_) => true, + #[cfg(unix)] + tokio_postgres::config::Host::Unix(_) => false, + }; +} + +fn build_pool( + pg_config: tokio_postgres::Config, + tls: T, + settings: &PostgresBackendSettings, +) -> std::result::Result +where + T: tokio_postgres::tls::MakeTlsConnect + std::clone::Clone + std::marker::Send + std::marker::Sync + 'static, + T::Stream: std::marker::Send + std::marker::Sync, + T::TlsConnect: std::marker::Send + std::marker::Sync, + >::Future: std::marker::Send, +{ + let manager_config = deadpool_postgres::ManagerConfig { recycling_method: deadpool_postgres::RecyclingMethod::Verified }; + let manager = deadpool_postgres::Manager::from_config(pg_config, tls, manager_config); + let built = deadpool_postgres::Pool::builder(manager) + .max_size(settings.max_connections as usize) + .wait_timeout(std::option::Option::Some(settings.wait_timeout)) + .create_timeout(std::option::Option::Some(settings.create_timeout)) + .recycle_timeout(std::option::Option::Some(settings.recycle_timeout)) + .runtime(deadpool_postgres::Runtime::Tokio1) + .build(); + return match built { + std::result::Result::Ok(pool) => std::result::Result::Ok(pool), + std::result::Result::Err(_) => std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "pool_build")), + }; +} + +fn build_verified_tls() -> std::result::Result { + let native = rustls_native_certs::load_native_certs(); + let native_error_count = native.errors.len(); + if native.certs.is_empty() { + ksp_logging_lib::warn!(target: crate::TRACING_TARGET, native_error_count, "no system TLS roots available for PostgreSQL verify_full"); + return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::TlsFailed, "native_roots")); + } + let mut roots = rustls::RootCertStore::empty(); + let (added, ignored) = roots.add_parsable_certificates(native.certs); + if added == 0 { + ksp_logging_lib::warn!(target: crate::TRACING_TARGET, native_error_count, ignored, "system TLS roots could not be admitted for PostgreSQL verify_full"); + return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::TlsFailed, "native_roots")); + } + if native_error_count > 0 || ignored > 0 { + ksp_logging_lib::debug!(target: crate::TRACING_TARGET, added, ignored, native_error_count, "loaded PostgreSQL system TLS roots with partial diagnostics"); + } + let provider = std::sync::Arc::new(rustls::crypto::aws_lc_rs::default_provider()); + let builder_result = rustls::ClientConfig::builder_with_provider(provider).with_safe_default_protocol_versions(); + let builder = match builder_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::TlsFailed, "protocol_versions")); + }, + }; + let client_config = builder.with_root_certificates(roots).with_no_client_auth(); + return std::result::Result::Ok(tokio_postgres_rustls::MakeRustlsConnect::new(client_config)); +} + +fn map_pool_error(error: deadpool_postgres::PoolError) -> crate::PostgresBackendError { + return match error { + deadpool_postgres::PoolError::Timeout(timeout_type) => crate::PostgresBackendError::new( + crate::PostgresBackendErrorKind::PoolTimeout, + match timeout_type { + deadpool_postgres::TimeoutType::Wait => "pool_wait", + deadpool_postgres::TimeoutType::Create => "pool_create", + deadpool_postgres::TimeoutType::Recycle => "pool_recycle", + }, + ), + deadpool_postgres::PoolError::Backend(_) => crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConnectFailed, "physical_connect"), + deadpool_postgres::PoolError::Closed => crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConnectFailed, "pool_closed"), + deadpool_postgres::PoolError::NoRuntimeSpecified => crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "pool_runtime"), + deadpool_postgres::PoolError::PostCreateHook(_) => crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConnectFailed, "pool_post_create"), + }; +} + +#[cfg(test)] +#[path = "../unit_tests/runtime.rs"] +mod tests; diff --git a/crates/ksp-store-postgres-lib/tests/dependency_boundary.rs b/crates/ksp-store-postgres-lib/tests/dependency_boundary.rs index df051c5..107d327 100644 --- a/crates/ksp-store-postgres-lib/tests/dependency_boundary.rs +++ b/crates/ksp-store-postgres-lib/tests/dependency_boundary.rs @@ -1,36 +1,60 @@ // file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs -// version: 2 +// version: 3 #![warn(missing_docs)] #![deny(unreachable_pub)] #![forbid(unsafe_code)] -//! Dependency-boundary canaries for the PostgreSQL Store backend scaffold. +//! Dependency and ownership canaries for the physical PostgreSQL Store backend. #[test] -fn pre_002_backend_depends_on_store_api_without_reverse_facade_edge() { +fn pre_005_backend_owns_exact_physical_runtime_dependencies_without_reverse_facade_edge() { let manifest = include_str!("../Cargo.toml"); - assert!(manifest.contains("ksp-store-api = { path = \"../ksp-store-api\" }")); - for forbidden in ["ksp-store-lib", "ksp-config-lib", "ksp-materializer", "ksp-program", "ksp-onchain-transport-lib", "ksp-offchain-transport-lib"] { + for required in ["deadpool-postgres", "ksp-logging-lib", "ksp-store-api", "rustls", "rustls-native-certs", "tokio-postgres", "tokio-postgres-rustls"] { + assert!(manifest.contains(required), "missing PostgreSQL backend dependency: {required}"); + } + for forbidden in ["ksp-store-lib", "ksp-config-lib", "ksp-materializer", "ksp-program", "ksp-onchain-transport-lib", "ksp-offchain-transport-lib", "sqlx"] { assert!(!manifest.contains(forbidden), "forbidden PostgreSQL backend dependency detected: {forbidden}"); } return; } #[test] -fn pre_002_backend_does_not_advance_connection_pool_tls_or_migrations() { - let manifest = include_str!("../Cargo.toml"); - for forbidden in ["tokio", "tokio-postgres", "deadpool-postgres", "tokio-postgres-rustls", "rustls", "sha2"] { - assert!(!manifest.contains(forbidden), "premature PostgreSQL runtime dependency detected: {forbidden}"); - } +fn pre_005_backend_keeps_environment_sql_migrations_and_physical_types_private() { let crate_root = include_str!("../src/lib.rs"); - assert!(crate_root.contains("mod constants;")); - assert!(crate_root.contains("pub(crate) use self::constants::TRACING_TARGET;")); - assert!(crate_root.contains("const _: &str = TRACING_TARGET;")); - for forbidden in ["pub mod ", "pub struct", "pub enum", "pub trait", "tokio_postgres", "deadpool_postgres", "mod migration", "SELECT ", "CREATE TABLE"] { - assert!(!crate_root.contains(forbidden), "premature PostgreSQL backend surface detected: {forbidden}"); + assert!(crate_root.contains("mod error;")); + assert!(crate_root.contains("mod runtime;")); + assert!(crate_root.contains("const _: &str = crate::TRACING_TARGET;")); + for forbidden in [ + "pub mod ", + "ksp_store_lib", + "ksp_config_lib", + "tokio_postgres::Client", + "tokio_postgres::Row", + "tokio_postgres::Statement", + "deadpool_postgres::Pool;", + ] { + assert!(!crate_root.contains(forbidden), "forbidden PostgreSQL crate-root surface detected: {forbidden}"); + } + let runtime = include_str!("../src/runtime.rs"); + for forbidden in [ + "std::env", + "dotenv", + "KSP_", + "KSPB_", + "PGHOST", + "PGPORT", + "PGUSER", + "PGPASSWORD", + ".pgpass", + "CREATE TABLE", + "INSERT INTO", + "UPDATE ", + "DELETE FROM", + "SELECT ", + "mod migration", + ] { + assert!(!runtime.contains(forbidden), "forbidden PostgreSQL backend ownership/scope content detected: {forbidden}"); } - let constants = include_str!("../src/constants.rs"); - assert!(constants.contains("pub(crate) const TRACING_TARGET: &str = \"ksp-store-postgres-lib\";")); return; } diff --git a/crates/ksp-store-postgres-lib/tests/public_api.rs b/crates/ksp-store-postgres-lib/tests/public_api.rs new file mode 100644 index 0000000..667fada --- /dev/null +++ b/crates/ksp-store-postgres-lib/tests/public_api.rs @@ -0,0 +1,44 @@ +// file: crates/ksp-store-postgres-lib/tests/public_api.rs +// version: 1 + +#![warn(missing_docs)] +#![deny(unreachable_pub)] +#![forbid(unsafe_code)] + +//! Narrow physical bridge canaries consumed by `ksp-store-lib` without exposing driver or pool types. + +#[test] +fn pre_005_backend_bridge_is_constructible_without_io() { + let network = match ksp_store_api::RawNetworkId::new("devnet") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("valid backend bridge network rejected: {error:?}"), + }; + let settings = ksp_store_postgres_lib::PostgresBackendSettings::new( + network, + "postgresql://operator:secret@localhost/ksp", + 8, + std::time::Duration::from_secs(10), + std::time::Duration::from_secs(5), + std::time::Duration::from_secs(10), + std::time::Duration::from_secs(5), + ksp_store_postgres_lib::PostgresBackendTlsMode::VerifyFull, + ); + assert_eq!(settings.network().as_str(), "devnet"); + assert_eq!(settings.tls_mode().code(), "verify_full"); + let _open = ksp_store_postgres_lib::PostgresBackend::open; + let _close = ksp_store_postgres_lib::PostgresBackend::close; + return; +} + +#[test] +fn pre_005_backend_error_projection_is_safe_and_static() { + let kinds = [ + ksp_store_postgres_lib::PostgresBackendErrorKind::ConfigInvalid, + ksp_store_postgres_lib::PostgresBackendErrorKind::ConnectFailed, + ksp_store_postgres_lib::PostgresBackendErrorKind::PoolTimeout, + ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout, + ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed, + ]; + assert_eq!(kinds.len(), 5); + return; +} diff --git a/crates/ksp-store-postgres-lib/unit_tests/runtime.rs b/crates/ksp-store-postgres-lib/unit_tests/runtime.rs new file mode 100644 index 0000000..51a4bbb --- /dev/null +++ b/crates/ksp-store-postgres-lib/unit_tests/runtime.rs @@ -0,0 +1,99 @@ +// file: crates/ksp-store-postgres-lib/unit_tests/runtime.rs +// version: 1 + +fn network() -> ksp_store_api::RawNetworkId { + return match ksp_store_api::RawNetworkId::new("devnet") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("valid backend test network rejected: {error:?}"), + }; +} + +fn settings(connection_uri: &str, tls_mode: crate::PostgresBackendTlsMode) -> crate::PostgresBackendSettings { + return crate::PostgresBackendSettings::new( + network(), + connection_uri, + 8, + std::time::Duration::from_secs(10), + std::time::Duration::from_secs(5), + std::time::Duration::from_secs(10), + std::time::Duration::from_secs(5), + tls_mode, + ); +} + +#[test] +fn physical_settings_debug_redacts_connection_uri() { + let secret = "postgresql://secret-user:secret-password@localhost/ksp"; + let value = settings(secret, crate::PostgresBackendTlsMode::VerifyFull); + let rendered = format!("{value:?}"); + assert!(!rendered.contains("secret-user")); + assert!(!rendered.contains("secret-password")); + assert!(rendered.contains("")); + return; +} + +#[test] +fn malformed_or_oversized_uri_is_rejected_without_retaining_input() { + let malformed = "not-a-postgresql-uri-secret-canary"; + let malformed_result = super::normalized_config(&settings(malformed, crate::PostgresBackendTlsMode::Disabled)); + let malformed_error = malformed_result.err(); + assert_eq!(malformed_error.as_ref().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid)); + assert!(!format!("{malformed_error:?}").contains("secret-canary")); + let oversized = "x".repeat(super::MAX_CONNECTION_URI_BYTES + 1); + let oversized_result = super::normalized_config(&settings(oversized.as_str(), crate::PostgresBackendTlsMode::Disabled)); + assert_eq!(oversized_result.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid)); + return; +} + +#[test] +fn typed_disabled_policy_overrides_uri_tls_and_connection_controls() { + let value = settings( + "postgresql://operator:secret@localhost/ksp?sslmode=require&application_name=hostile&connect_timeout=1&sslnegotiation=direct", + crate::PostgresBackendTlsMode::Disabled, + ); + let config = match super::normalized_config(&value) { + std::result::Result::Ok(config) => config, + std::result::Result::Err(error) => panic!("valid disabled config rejected: {error:?}"), + }; + assert_eq!(config.get_ssl_mode(), tokio_postgres::config::SslMode::Disable); + assert_eq!(config.get_ssl_negotiation(), tokio_postgres::config::SslNegotiation::Postgres); + assert_eq!(config.get_application_name(), std::option::Option::Some(super::APPLICATION_NAME)); + assert_eq!(config.get_connect_timeout(), std::option::Option::Some(std::time::Duration::from_secs(10))); + return; +} + +#[test] +fn typed_verify_full_policy_forces_tls_and_rejects_hostaddr_only_identity() { + let value = settings( + "postgresql://operator:secret@localhost/ksp?sslmode=disable&application_name=hostile&connect_timeout=1", + crate::PostgresBackendTlsMode::VerifyFull, + ); + let config = match super::normalized_config(&value) { + std::result::Result::Ok(config) => config, + std::result::Result::Err(error) => panic!("valid verify_full config rejected: {error:?}"), + }; + assert_eq!(config.get_ssl_mode(), tokio_postgres::config::SslMode::Require); + assert_eq!(config.get_ssl_negotiation(), tokio_postgres::config::SslNegotiation::Postgres); + assert_eq!(config.get_application_name(), std::option::Option::Some(super::APPLICATION_NAME)); + assert_eq!(config.get_connect_timeout(), std::option::Option::Some(std::time::Duration::from_secs(10))); + let hostaddr_only = settings("hostaddr=127.0.0.1 user=operator dbname=ksp", crate::PostgresBackendTlsMode::VerifyFull); + let rejected = super::normalized_config(&hostaddr_only); + assert_eq!(rejected.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid)); + #[cfg(unix)] + { + let unix_socket = settings("host=/var/run/postgresql user=operator dbname=ksp", crate::PostgresBackendTlsMode::VerifyFull); + let unix_rejected = super::normalized_config(&unix_socket); + assert_eq!(unix_rejected.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid)); + } + return; +} + +#[test] +fn libpq_server_options_are_rejected_in_foundation_runtime() { + let value = settings("host=localhost user=operator dbname=ksp options='-c statement_timeout=0'", crate::PostgresBackendTlsMode::Disabled); + let rejected = super::normalized_config(&value); + let error = rejected.err(); + assert_eq!(error.as_ref().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid)); + assert_eq!(error.map(|value| return value.phase()), std::option::Option::Some("server_options")); + return; +} diff --git a/deltas/0.3.2/pre.005.md b/deltas/0.3.2/pre.005.md new file mode 100644 index 0000000..29ad689 --- /dev/null +++ b/deltas/0.3.2/pre.005.md @@ -0,0 +1,135 @@ + + + +# Delta `0.3.2-pre.005` — PostgreSQL connection + pool + TLS + +## 1. Base + +Base exacte : `0.3.2-pre.004-fix.001`, incluant l'ajustement opérateur de `.env.example` version `13`. + +Le gate opérateur du 29 août 2026 est entièrement vert : audits Rust/Markdown, workspace check/Clippy, 126 tests Config, ownership, Store avec et sans feature PostgreSQL, compilation `--no-default-features` et Config Desk passent. + +## 2. Objet + +Matérialiser la première ouverture PostgreSQL physique sans ouvrir encore les migrations ou le SQL métier : + +```text +tokio-postgres 0.7.18+ +deadpool-postgres 0.14.x +tokio-postgres-rustls 0.14.x +rustls 0.23.x / AWS-LC +roots système +pool borné +Store::open physique +Store::close borné +``` + +## 3. Frontière façade/backend + +`ksp-store-lib` reste l'unique surface consumer et ne dépend directement d'aucun driver/pool/TLS. Il convertit ses settings vers un bridge `ksp-store-postgres-lib`, puis mappe les erreurs backend vers des `ErrorCode` Store stables. + +`ksp-store-postgres-lib` possède seul : + +```text +parse/normalisation tokio_postgres::Config +Deadpool Manager/Pool +connector Rustls +roots système +première acquisition physique +fermeture/drain physique +``` + +Les types physiques `Pool`, `Client`, `Row` et `Statement` ne sont jamais réexportés par la façade. + +## 4. Normalisation de l'URI + +Le backend consomme uniquement l'URI déjà résolue par Config. Après parsing, KSP impose : + +```text +application_name = ksp-store +connect_timeout = settings typés +sslnegotiation = postgres +Disabled -> sslmode=disable +VerifyFull -> sslmode=require +``` + +`options=` est rejeté dans cette fondation. `VerifyFull` exige un host TCP afin de disposer d'une identité serveur à vérifier : `hostaddr` seul et les sockets Unix sont rejetés. Les erreurs de parsing ne conservent jamais le texte de l'URI. + +## 5. Pool et lifecycle + +Le pool applique les limites validées en `pre.003` : taille maximale et deadlines `wait/create/recycle`. Le manager utilise `RecyclingMethod::Verified`; la requête de vérification éventuelle reste interne à Deadpool et aucun SQL KSP n'est ajouté. + +La création du pool est lazy, donc `Store::open` exécute explicitement une première `pool.get().await`. Le succès signifie ainsi qu'une connexion physique/auth/TLS a réellement été établie. + +`Store::close(self)` appelle `Pool::close()` puis attend un drain `size == 0` sous `shutdown_timeout`. `Drop` ne fait qu'un close best-effort. + +## 6. TLS + +Modes exacts : + +```text +Disabled +VerifyFull +``` + +`VerifyFull` charge les roots système dans `rustls::RootCertStore`, utilise explicitement le provider AWS-LC et conserve la vérification standard de certificat + identité serveur. Aucun mode `Prefer`, aucun verifier permissif et aucun fallback plaintext ne sont introduits. + +## 7. Erreurs sûres + +Le backend retourne uniquement une classification et une phase statique : + +```text +ConfigInvalid +ConnectFailed +PoolTimeout +ShutdownTimeout +TlsFailed +``` + +La façade mappe vers : + +```text +store.postgres_config_invalid +store.postgres_connect_failed +store.postgres_pool_timeout +store.postgres_tls_failed +store.shutdown_timeout +``` + +Aucune erreur `tokio-postgres`, Deadpool, Rustls ou native-certs n'est attachée comme source publique. + +## 8. Hors scope + +```text +migrations/bootstrap SQL +ksp_store_schema_migrations +health snapshot public +RAW transaction/account persistence +routing multi-target/multi-réseau dans Store +live PostgreSQL integration test +``` + +Ces éléments restent respectivement aux tranches `pre.006`, `pre.007`, `0.3.3/0.3.4` et `pre.008`. + +## 9. Version + +```text +workspace.package.version = 0.3.2-pre.5 +``` + +## 10. Gate opérateur requis + +```bash +cargo fmt --all +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2 +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-store-postgres-lib +cargo test -p ksp-store-lib +cargo test -p ksp-store-lib --no-default-features +cargo check -p ksp-store-lib --no-default-features +cargo tree -p ksp-store-postgres-lib --edges normal +cargo tree -p ksp-store-lib --edges normal +cargo tree --duplicates +``` diff --git a/docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md b/docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md index d6752ac..169f371 100644 --- a/docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md +++ b/docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md @@ -1,5 +1,5 @@ - + # Plan `0.3.2` — Store/PostgreSQL runtime foundation @@ -963,7 +963,22 @@ Document/schema/example/registry/adaptor, `.env.example`, provenance/sensitivity ### `pre.005` — PostgreSQL connection + deadpool + Rustls -Implémenter parse/normalisation URI, pool borné, connect/open/close, VerifyFull/Disabled, timeouts et erreurs redacted. Aucun SQL métier. +Matérialiser le bridge physique backend sans modifier la façade publique backend-neutral : + +```text +ksp-store-lib + -> PostgresBackendSettings privé à la composition + -> PostgresBackend::open/close + +ksp-store-postgres-lib + -> tokio-postgres >= 0.7.18 + -> deadpool-postgres ^0.14.2 + -> tokio-postgres-rustls 0.14.x + -> rustls 0.23.x + AWS-LC + -> roots système via rustls-native-certs +``` + +Après parsing, la policy KSP réécrit `application_name`, `connect_timeout`, `sslnegotiation` et `sslmode`; les `options=` serveur sont rejetées. `Disabled` force le plaintext, `VerifyFull` force TLS + roots système + identité serveur et interdit un target `hostaddr` sans `host` ainsi que les sockets Unix, sur lesquels PostgreSQL ne négocie pas TLS. Le pool applique `max_size` et deadlines `wait/create/recycle`, utilise le recycling `Verified`, et `Store::open` ne réussit qu'après une première acquisition physique. Les erreurs externes ne sont jamais conservées comme source/texte public. `Store::close` ferme et draine sous deadline; `Drop` reste best-effort. Aucun SQL KSP, migration ou table métier n'est introduit. ### `pre.006` — Migration/bootstrap foundation @@ -1051,8 +1066,6 @@ Aucune question architecturale ne bloque `pre.002`. Les détails suivants sont réservés à leur tranche sans rouvrir les décisions du gate : ```text -mapping exact deadpool timeouts en pre.005 -construction exacte du rustls RootCertStore en pre.005 DDL précis de ksp_store_schema_migrations en pre.006 forme finale des snapshots health en pre.007 ``` diff --git a/docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md b/docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md index af92d7a..a06f9c7 100644 --- a/docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md +++ b/docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md @@ -1,5 +1,5 @@ - + # Validation `0.3.2` — Store/PostgreSQL runtime foundation @@ -74,6 +74,8 @@ Le gate opérateur de `pre.002-fix.001`, fourni le 29 août 2026, est entièreme Le gate opérateur de `pre.003-fix.001`, fourni le 29 août 2026, est entièrement vert : audits Rust/Markdown, workspace check/Clippy, tests `ksp-store-lib` avec et sans feature par défaut, tests backend et compilation `--no-default-features` passent. Cette base est l'entrée effective de `pre.004`. +Le gate opérateur de `pre.004-fix.001`, après correction manuelle des commentaires `.env.example`, est entièrement vert : audits Rust/Markdown, workspace check/Clippy, 126 tests `ksp-config-lib`, ownership, `ksp-store-lib` avec et sans feature PostgreSQL, compilation `--no-default-features` et 63 tests Config Desk passent. Cette base est l'entrée effective de `pre.005`. + ## 3. Frontières Cargo ### V32-DEP-001 — Façade -> API @@ -134,7 +136,9 @@ Store/backend -> Materializer consumer ordinaire -> ksp-store-postgres-lib ``` -Statut : `TODO pre.009`. +Matérialisé par `pre.005` : les types physiques nécessaires sont publics uniquement dans la crate backend pour la frontière inter-crates et ne sont jamais réexportés par `ksp-store-lib`; `Pool/Client/Row/Statement` restent absents de sa crate-root. + +Statut : `TODO gate opérateur pre.005 / TODO pre.009`. ### V32-DEP-006 — `ksp-store-api` non régressé @@ -199,9 +203,9 @@ close borné Drop best-effort seulement ``` -`pre.003` fixe les signatures `Store::open(settings).await` et `Store::close(self).await` et garde `Store` opaque/non constructible par un consumer. Aucun succès d'ouverture n'est simulé avant la connexion réelle : le backend compilé s'arrête avec `store.backend_open_failed` et le contexte sûr `runtime_foundation_pending`. +`pre.003` fixe les signatures `Store::open(settings).await` et `Store::close(self).await`. `pre.005` remplace le stop de staging PostgreSQL par l'ouverture physique : un succès exige `pool.get().await` après construction du pool, puis `close(self)` ferme et draine ce pool sous la deadline configurée. Aucun pool/client n'est exposé par la façade. -Statut : `PASS pre.003-fix.001 opérateur` pour les signatures et le staging ; `TODO pre.007` pour le shutdown physique borné. +Statut : `PASS pre.003-fix.001 opérateur` pour les signatures / `TODO gate opérateur pre.005` pour l'ouverture et le shutdown physiques / `TODO pre.007` pour la composition health. ## 5. Config ownership @@ -220,7 +224,7 @@ backend postgres explicite Matérialisé par `pre.004` puis corrigé par `pre.004-fix.001` avec `cfg.std.store` / `schema.std.store`, trois targets committed `devnet`/`mainnet`/`testnet`, `default_profile = devnet`, un `network` explicite par target et des URI PostgreSQL séparées. -Statut : `TODO gate opérateur pre.004-fix.001`. +Statut : `PASS pre.004-fix.001 opérateur`. ### V32-CONFIG-002 — Secrets/provenance @@ -235,7 +239,7 @@ dotenv inventory à jour Matérialisé par `pre.004-fix.001` : les trois URI réseau-spécifiques sont inventoriées, chaque fallback reste `Secret`, la safe projection est redacted et la provenance n'embarque aucune valeur. -Statut : `TODO gate opérateur pre.004-fix.001`. +Statut : `PASS pre.004-fix.001 opérateur`. ### V32-CONFIG-002B — Target/réseau sans multiplexage @@ -252,7 +256,7 @@ future RAW mismatch Store.network != entity/query.network rejeté avant I/O Matérialisé contractuellement par `pre.004-fix.001`; l'enforcement sur opérations RAW sera exercé dans `0.3.3`/`0.3.4`. -Statut : `TODO gate opérateur pre.004-fix.001 / TODO 0.3.3-0.3.4`. +Statut : `PASS pre.004-fix.001 opérateur` pour la sélection target/réseau / `TODO 0.3.3-0.3.4` pour le mismatch RAW. ### V32-CONFIG-003 — No-env Store/backend @@ -269,7 +273,7 @@ PG* `pre.004` renforce aussi le canari d'ownership avec les nouveaux filenames Store ; `pre.004-fix.001` conserve cette frontière tout en ajoutant le réseau au contrat `StoreSettings`. Store/backend restent sans dépendance Config et sans lecture KSP/KSPB. -Statut : `TODO gate opérateur pre.004-fix.001 / TODO pre.009`. +Statut : `PASS pre.004-fix.001 opérateur / TODO pre.009`. ### V32-CONFIG-004 — Adapter Config -> Store @@ -277,7 +281,7 @@ Critère : `ksp-config-lib` seul transforme un profil/target résolu en `StoreSe Matérialisé par `pre.004` puis `pre.004-fix.001` : seul `ksp-config-lib` dépend de `ksp-store-lib` avec `default-features = false`; il mappe le `profile_id` sélectionné vers un target, construit son `RawNetworkId` et ses settings backend sans forcer la feature backend. -Statut : `TODO gate opérateur pre.004-fix.001`. +Statut : `PASS pre.004-fix.001 opérateur`. ## 6. Pool et lifecycle PostgreSQL @@ -292,19 +296,25 @@ aucune taille zéro aucune valeur pathologique ``` -Statut : `TODO pre.005`. +Matérialisé par `pre.005` : `deadpool-postgres` possède un pool `max_size` explicite et applique les deadlines `wait/create/recycle`; `tokio-postgres::Config` reçoit aussi le `connect_timeout` typé après parsing de l'URI. + +Statut : `TODO gate opérateur pre.005`. ### V32-POOL-002 — Connection task ownership Critère : chaque connection future tokio-postgres est pilotée par le manager retenu et son task handle reste possédé jusqu'au drop/close. -Statut : `TODO pre.005/pre.009`. +`pre.005` délègue la création/recycle des connexions au `Manager` Deadpool retenu ; aucun `tokio::spawn` KSP n'est introduit dans Store. La preuve de non-régression détaillée reste au hardening. + +Statut : `TODO gate opérateur pre.005 / TODO pre.009`. ### V32-POOL-003 — Open failure safe Critère : DNS/connect/auth/server errors ne copient ni URI ni texte remote arbitraire dans Display/Debug public. -Statut : `TODO pre.005`. +Matérialisé par `pre.005` : parsing/connexion/pool/TLS sont ramenés à une classification backend locale sans conserver le texte des erreurs externes ni l'URI. La façade mappe vers des codes `store.postgres_*` stables avec contexte statique sûr. + +Statut : `TODO gate opérateur pre.005`. ### V32-POOL-004 — Close @@ -317,7 +327,9 @@ shutdown respecte timeout aucune tâche volontairement laissée orpheline ``` -Statut : `TODO pre.007/pre.008`. +`pre.005` matérialise déjà `Pool::close()` et un drain borné par `shutdown_timeout`; `Drop` ne fait qu'un `close()` best-effort. La preuve end-to-end avec backend réel reste réservée à `pre.007/pre.008`. + +Statut : `TODO gate opérateur pre.005 / TODO pre.007/pre.008`. ## 7. TLS @@ -330,7 +342,9 @@ Disabled VerifyFull ``` -Statut : `TODO pre.005`. +Matérialisé par `pre.005` : la surface physique accepte exactement `Disabled` et `VerifyFull`; l'URI parsée est ensuite normalisée vers `SslMode::Disable` ou `SslMode::Require`, donc elle ne peut pas modifier la policy typée. + +Statut : `TODO gate opérateur pre.005`. ### V32-TLS-002 — VerifyFull @@ -344,7 +358,9 @@ nom serveur vérifié aucun fallback plaintext ``` -Statut : `TODO pre.005`. +Matérialisé par `pre.005` : `VerifyFull` construit un `rustls::RootCertStore` à partir des roots système, utilise explicitement le provider AWS-LC, requiert un host TCP pour l'identité serveur, rejette `hostaddr` seul et les sockets Unix, et ne permet aucun fallback plaintext. Les erreurs de roots/certificats sont réduites à des compteurs sûrs. + +Statut : `TODO gate opérateur pre.005`. ### V32-TLS-003 — Pas de fichier TLS implicite