v0.3.2-pre.005

This commit is contained in:
2026-08-29 19:33:43 +02:00
parent de3cec6a23
commit d93184d41d
20 changed files with 945 additions and 120 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 336 # version: 337
[workspace] [workspace]
resolver = "3" 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"] 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] [workspace.package]
version = "0.3.2-pre.4.fix.1" version = "0.3.2-pre.5"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
@@ -18,6 +18,7 @@ argon2 = { version = "^0.5", default-features = false }
base64 = { version = "^0.23" } base64 = { version = "^0.23" }
chacha20poly1305 = { version = "^0.11", default-features = false } chacha20poly1305 = { version = "^0.11", default-features = false }
chrono = { version = "^0.4", default-features = false } chrono = { version = "^0.4", default-features = false }
deadpool-postgres = { version = "^0.14", default-features = false }
directories = { version = "^6.0" } directories = { version = "^6.0" }
ed25519-dalek = { version = "^3.0", default-features = false } ed25519-dalek = { version = "^3.0", default-features = false }
fs2 = { version = "^0.4" } fs2 = { version = "^0.4" }
@@ -26,6 +27,8 @@ getrandom = { version = "^0.4", default-features = false }
http = { version = "^1.5", default-features = false } http = { version = "^1.5", default-features = false }
jsonschema = { version = "^0.51", default-features = false } jsonschema = { version = "^0.51", default-features = false }
reqwest = { version = "^0.13", 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 = { version = "^1.0" }
serde_json = { version = "^1.0" } serde_json = { version = "^1.0" }
solana-keypair = { version = "^3.1", default-features = false } 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-subscriber = { version = "^0.3", default-features = false }
tracing-appender = { version = "^0.2", default-features = false } tracing-appender = { version = "^0.2", default-features = false }
tokio = { version = "^1.53", 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 } tokio-tungstenite = { version = "^0.30", default-features = false }
tonic = { version = "^0.14", default-features = false } tonic = { version = "^0.14", default-features = false }
tonic-prost = { version = "^0.14", default-features = false } tonic-prost = { version = "^0.14", default-features = false }

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-store-lib/Cargo.toml # file: crates/ksp-store-lib/Cargo.toml
# version: 1 # version: 2
[package] [package]
name = "ksp-store-lib" name = "ksp-store-lib"
@@ -12,6 +12,7 @@ default = ["postgres"]
postgres = ["dep:ksp-store-postgres-lib"] postgres = ["dep:ksp-store-postgres-lib"]
[dependencies] [dependencies]
ksp-logging-lib = { path = "../ksp-logging-lib" }
ksp-store-api = { path = "../ksp-store-api" } ksp-store-api = { path = "../ksp-store-api" }
ksp-store-postgres-lib = { path = "../ksp-store-postgres-lib", optional = true } ksp-store-postgres-lib = { path = "../ksp-store-postgres-lib", optional = true }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/src/error.rs // 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. /// 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"); 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"); 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. /// 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"); 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. /// 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"); 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. /// Error code used when a Store cannot complete its explicit shutdown inside the configured bound.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/src/lib.rs // file: crates/ksp-store-lib/src/lib.rs
// version: 3 // version: 4
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -7,10 +7,9 @@
//! Common backend-neutral Store runtime facade for KSP. //! Common backend-neutral Store runtime facade for KSP.
//! //!
//! `0.3.2-pre.003` materializes Config-independent settings, stable backend //! `0.3.2-pre.005` materializes the first physical PostgreSQL runtime path:
//! identity, bounded validation and the opaque async Store lifecycle contract. //! one bounded Deadpool pool, one startup connection proof and explicit TLS
//! Physical PostgreSQL connection, pool, TLS and migrations remain private //! policy. SQL migrations remain private future slices of this release.
//! future slices of this release.
//! //!
//! The default `postgres` feature compiles the official PostgreSQL backend as //! The default `postgres` feature compiles the official PostgreSQL backend as
//! an optional implementation dependency. No backend implementation type is //! 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; pub use self::error::ERROR_CODE_BACKEND_NOT_COMPILED;
/// Error code used when a compiled Store backend cannot complete opening. /// Error code used when a compiled Store backend cannot complete opening.
pub use self::error::ERROR_CODE_BACKEND_OPEN_FAILED; 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. /// Error code used when Store settings violate backend-neutral bounds or invariants.
pub use self::error::ERROR_CODE_SETTINGS_INVALID; pub use self::error::ERROR_CODE_SETTINGS_INVALID;
/// Error code used when explicit Store shutdown exceeds its configured deadline. /// Error code used when explicit Store shutdown exceeds its configured deadline.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/src/settings.rs // file: crates/ksp-store-lib/src/settings.rs
// version: 2 // version: 3
const DEFAULT_CONNECT_TIMEOUT_MS: u64 = 10_000; const DEFAULT_CONNECT_TIMEOUT_MS: u64 = 10_000;
const DEFAULT_MAX_CONNECTIONS: u32 = 8; const DEFAULT_MAX_CONNECTIONS: u32 = 8;
@@ -239,6 +239,13 @@ impl PostgresStoreSettings {
return self.bootstrap; 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. /// Returns the PostgreSQL pool settings without exposing the sensitive connection URI.
#[must_use] #[must_use]
pub const fn pool(&self) -> PostgresPoolSettings { pub const fn pool(&self) -> PostgresPoolSettings {

View File

@@ -1,54 +1,155 @@
// file: crates/ksp-store-lib/src/store.rs // file: crates/ksp-store-lib/src/store.rs
// version: 1 // version: 2
/// Opaque common Store runtime facade. /// 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 /// A successful value is returned only after the selected compiled backend has completed its bounded physical opening path. PostgreSQL pool, client, TLS and
/// contract but intentionally has no successful opening path until the PostgreSQL runtime foundation is materialized by later prereleases. /// driver types remain private to the backend crate.
#[derive(Debug)] pub struct Store {
#[non_exhaustive] backend_kind: crate::StoreBackendKind,
pub struct Store; network: ksp_store_api::RawNetworkId,
#[cfg(feature = "postgres")]
runtime: StoreRuntime,
shutdown_timeout: std::time::Duration,
}
impl Store { 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 /// 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
/// with [`crate::ERROR_CODE_BACKEND_OPEN_FAILED`] because physical connection ownership is introduced in `pre.005`. /// been established under the typed TLS and timeout policy.
pub async fn open(settings: crate::StoreSettings) -> ksp_store_api::Result<Self> { pub async fn open(settings: crate::StoreSettings) -> ksp_store_api::Result<Self> {
let validation = settings.validate(); let validation = settings.validate();
if let std::result::Result::Err(error) = validation { if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error); return std::result::Result::Err(error);
} }
let backend_kind = settings.backend_kind(); let backend_kind = settings.backend_kind();
return match backend_kind { let network = settings.network().clone();
crate::StoreBackendKind::Postgres => open_postgres_contract(backend_kind).await, 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. /// Explicitly closes the Store runtime, consumes its facade handle and applies the configured bounded shutdown deadline.
///
/// `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.
pub async fn close(self) -> ksp_store_api::Result<()> { 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")] #[cfg(feature = "postgres")]
async fn open_postgres_contract(backend_kind: crate::StoreBackendKind) -> ksp_store_api::Result<Store> { enum StoreRuntime {
return std::result::Result::Err( Postgres(ksp_store_postgres_lib::PostgresBackend),
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"), #[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<Store> {
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"))] #[cfg(not(feature = "postgres"))]
async fn open_postgres_contract(backend_kind: crate::StoreBackendKind) -> ksp_store_api::Result<Store> { async fn open_postgres(
return std::result::Result::Err( backend_kind: crate::StoreBackendKind,
ksp_store_api::Error::new(crate::ERROR_CODE_BACKEND_NOT_COMPILED, "Selected Store backend is not compiled") _network: ksp_store_api::RawNetworkId,
.with_context("backend", backend_kind.code()), _shutdown_timeout: std::time::Duration,
); _settings: &crate::PostgresStoreSettings,
) -> ksp_store_api::Result<Store> {
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)] #[cfg(test)]

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/tests/dependency_boundary.rs // file: crates/ksp-store-lib/tests/dependency_boundary.rs
// version: 4 // version: 5
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -8,10 +8,11 @@
//! Cargo-feature and dependency-boundary canaries for the common Store runtime facade. //! Cargo-feature and dependency-boundary canaries for the common Store runtime facade.
#[test] #[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"); let manifest = include_str!("../Cargo.toml");
assert!(manifest.contains("default = [\"postgres\"]")); assert!(manifest.contains("default = [\"postgres\"]"));
assert!(manifest.contains("postgres = [\"dep:ksp-store-postgres-lib\"]")); 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-api = { path = \"../ksp-store-api\" }"));
assert!(manifest.contains("ksp-store-postgres-lib = { path = \"../ksp-store-postgres-lib\", optional = true }")); assert!(manifest.contains("ksp-store-postgres-lib = { path = \"../ksp-store-postgres-lib\", optional = true }"));
for forbidden in [ for forbidden in [
@@ -30,11 +31,8 @@ fn pre_002_manifest_owns_default_postgres_feature_and_optional_backend_edge() {
} }
#[test] #[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"); 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::settings::StoreSettings;"));
assert!(crate_root.contains("pub use self::store::Store;")); assert!(crate_root.contains("pub use self::store::Store;"));
assert!(crate_root.contains("pub use ksp_store_api::RawTransaction;")); assert!(crate_root.contains("pub use ksp_store_api::RawTransaction;"));

View File

@@ -1,11 +1,11 @@
// file: crates/ksp-store-lib/tests/feature_mismatch.rs // file: crates/ksp-store-lib/tests/feature_mismatch.rs
// version: 3 // version: 4
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
#![forbid(unsafe_code)] #![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<T>(future: impl std::future::Future<Output = T>) -> T { fn poll_ready<T>(future: impl std::future::Future<Output = T>) -> T {
let mut future = std::boxed::Box::pin(future); let mut future = std::boxed::Box::pin(future);
@@ -13,7 +13,7 @@ fn poll_ready<T>(future: impl std::future::Future<Output = T>) -> T {
let mut context = std::task::Context::from_waker(waker); let mut context = std::task::Context::from_waker(waker);
return match std::future::Future::poll(future.as_mut(), &mut context) { return match std::future::Future::poll(future.as_mut(), &mut context) {
std::task::Poll::Ready(value) => value, 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( let postgres = ksp_store_lib::PostgresStoreSettings::new(
"postgresql://operator-supplied-sensitive-value", connection_uri,
ksp_store_lib::PostgresPoolSettings::default(), ksp_store_lib::PostgresPoolSettings::default(),
ksp_store_lib::PostgresTlsMode::Disabled, ksp_store_lib::PostgresTlsMode::Disabled,
ksp_store_lib::PostgresBootstrapSettings::default(), ksp_store_lib::PostgresBootstrapSettings::default(),
@@ -36,8 +36,8 @@ fn settings() -> ksp_store_lib::StoreSettings {
#[cfg(not(feature = "postgres"))] #[cfg(not(feature = "postgres"))]
#[test] #[test]
fn pre_003_known_postgres_without_feature_returns_stable_error_before_io() { fn pre_005_known_postgres_without_feature_returns_stable_error_before_io() {
let result = poll_ready(ksp_store_lib::Store::open(settings())); let result = poll_ready(ksp_store_lib::Store::open(settings("postgresql://operator-supplied-sensitive-value@localhost/ksp")));
let error = result.err(); let error = result.err();
assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(ksp_store_lib::ERROR_CODE_BACKEND_NOT_COMPILED)); assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(ksp_store_lib::ERROR_CODE_BACKEND_NOT_COMPILED));
return; return;
@@ -45,9 +45,11 @@ fn pre_003_known_postgres_without_feature_returns_stable_error_before_io() {
#[cfg(feature = "postgres")] #[cfg(feature = "postgres")]
#[test] #[test]
fn pre_003_compiled_postgres_path_refuses_to_fake_readiness_before_pre_005() { fn pre_005_compiled_postgres_rejects_malformed_uri_without_secret_leak_before_io() {
let result = poll_ready(ksp_store_lib::Store::open(settings())); 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(); 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; return;
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/tests/public_api.rs // file: crates/ksp-store-lib/tests/public_api.rs
// version: 2 // version: 3
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -29,11 +29,15 @@ fn pre_003_settings_and_lifecycle_contract_are_available_from_crate_root() {
} }
#[test] #[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.domain(), "store");
assert_eq!(ksp_store_lib::ERROR_CODE_SETTINGS_INVALID.code(), "settings_invalid"); 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_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_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_BACKEND_CLOSED.code(), "backend_closed");
assert_eq!(ksp_store_lib::ERROR_CODE_SHUTDOWN_TIMEOUT.code(), "shutdown_timeout"); assert_eq!(ksp_store_lib::ERROR_CODE_SHUTDOWN_TIMEOUT.code(), "shutdown_timeout");
return; return;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-store-lib/unit_tests/store.rs // file: crates/ksp-store-lib/unit_tests/store.rs
// version: 3 // version: 4
fn poll_ready<T>(future: impl std::future::Future<Output = T>) -> T { fn poll_ready<T>(future: impl std::future::Future<Output = T>) -> T {
let mut future = std::boxed::Box::pin(future); let mut future = std::boxed::Box::pin(future);
@@ -7,7 +7,7 @@ fn poll_ready<T>(future: impl std::future::Future<Output = T>) -> T {
let mut context = std::task::Context::from_waker(waker); let mut context = std::task::Context::from_waker(waker);
return match std::future::Future::poll(future.as_mut(), &mut context) { return match std::future::Future::poll(future.as_mut(), &mut context) {
std::task::Poll::Ready(value) => value, 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( let postgres = crate::PostgresStoreSettings::new(
"postgresql://secret-user:secret-password@db.internal/ksp", connection_uri,
crate::PostgresPoolSettings::default(), crate::PostgresPoolSettings::default(),
crate::PostgresTlsMode::VerifyFull, crate::PostgresTlsMode::Disabled,
crate::PostgresBootstrapSettings::default(), crate::PostgresBootstrapSettings::default(),
); );
return crate::StoreSettings::with_default_shutdown(valid_network(), crate::StoreBackendSettings::Postgres(postgres)); return crate::StoreSettings::with_default_shutdown(valid_network(), crate::StoreBackendSettings::Postgres(postgres));
@@ -30,14 +30,7 @@ fn valid_store_settings() -> crate::StoreSettings {
#[test] #[test]
fn invalid_settings_are_rejected_before_backend_dispatch() { fn invalid_settings_are_rejected_before_backend_dispatch() {
let postgres = crate::PostgresStoreSettings::new( let result = poll_ready(crate::Store::open(store_settings("")));
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 error = result.err(); let error = result.err();
assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_SETTINGS_INVALID)); assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_SETTINGS_INVALID));
return; return;
@@ -45,17 +38,19 @@ fn invalid_settings_are_rejected_before_backend_dispatch() {
#[cfg(feature = "postgres")] #[cfg(feature = "postgres")]
#[test] #[test]
fn compiled_postgres_dispatch_does_not_fake_readiness_before_connection_materialization() { fn compiled_postgres_rejects_malformed_physical_configuration_before_io() {
let result = poll_ready(crate::Store::open(valid_store_settings())); let secret_canary = "not-a-postgresql-uri-secret-canary";
let result = poll_ready(crate::Store::open(store_settings(secret_canary)));
let error = result.err(); 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; return;
} }
#[cfg(not(feature = "postgres"))] #[cfg(not(feature = "postgres"))]
#[test] #[test]
fn known_postgres_without_feature_is_rejected_before_io() { 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(); let error = result.err();
assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_BACKEND_NOT_COMPILED)); assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_BACKEND_NOT_COMPILED));
return; return;

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-store-postgres-lib/Cargo.toml # file: crates/ksp-store-postgres-lib/Cargo.toml
# version: 1 # version: 2
[package] [package]
name = "ksp-store-postgres-lib" name = "ksp-store-postgres-lib"
@@ -8,7 +8,14 @@ edition.workspace = true
repository.workspace = true repository.workspace = true
[dependencies] [dependencies]
deadpool-postgres = { workspace = true, features = ["rt_tokio_1"] }
ksp-logging-lib = { path = "../ksp-logging-lib" }
ksp-store-api = { path = "../ksp-store-api" } 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] [lints]
workspace = true workspace = true

View File

@@ -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;
}
}

View File

@@ -1,25 +1,36 @@
// file: crates/ksp-store-postgres-lib/src/lib.rs // file: crates/ksp-store-postgres-lib/src/lib.rs
// version: 2 // version: 3
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
#![forbid(unsafe_code)] #![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 //! `0.3.2-pre.005` owns the physical `tokio-postgres` connection, bounded
//! driver, pooling, TLS, SQL, migrations and health behavior are intentionally //! Deadpool pool and explicit Rustls TLS policy. SQL migrations and business
//! absent until their dedicated prereleases. //! persistence remain absent until their dedicated prereleases.
//! //!
//! This crate depends on `ksp-store-api` and never on `ksp-store-lib`, which //! This crate depends on `ksp-store-api` and never on `ksp-store-lib`. The
//! keeps backend implementation ownership acyclic and reusable behind the //! common facade consumes only this crate's narrow backend bridge and never
//! common facade. //! exposes PostgreSQL pool, client, row or statement types.
mod constants; 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; pub(crate) use self::constants::TRACING_TARGET;
// Keep the mandatory crate-owned tracing target part of the compiled scaffold const _: &str = crate::TRACING_TARGET;
// without inventing runtime logging before the first behavioral tranche.
const _: &str = TRACING_TARGET;

View File

@@ -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<std::string::String>,
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", &"<redacted>")
.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<Self, crate::PostgresBackendError> {
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<tokio_postgres::Config, crate::PostgresBackendError> {
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::<tokio_postgres::Config>();
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<T>(
pg_config: tokio_postgres::Config,
tls: T,
settings: &PostgresBackendSettings,
) -> std::result::Result<deadpool_postgres::Pool, crate::PostgresBackendError>
where
T: tokio_postgres::tls::MakeTlsConnect<tokio_postgres::Socket> + 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,
<T::TlsConnect as tokio_postgres::tls::TlsConnect<tokio_postgres::Socket>>::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<tokio_postgres_rustls::MakeRustlsConnect, crate::PostgresBackendError> {
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;

View File

@@ -1,36 +1,60 @@
// file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs // file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
// version: 2 // version: 3
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
//! Dependency-boundary canaries for the PostgreSQL Store backend scaffold. //! Dependency and ownership canaries for the physical PostgreSQL Store backend.
#[test] #[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"); let manifest = include_str!("../Cargo.toml");
assert!(manifest.contains("ksp-store-api = { path = \"../ksp-store-api\" }")); for required in ["deadpool-postgres", "ksp-logging-lib", "ksp-store-api", "rustls", "rustls-native-certs", "tokio-postgres", "tokio-postgres-rustls"] {
for forbidden in ["ksp-store-lib", "ksp-config-lib", "ksp-materializer", "ksp-program", "ksp-onchain-transport-lib", "ksp-offchain-transport-lib"] { 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}"); assert!(!manifest.contains(forbidden), "forbidden PostgreSQL backend dependency detected: {forbidden}");
} }
return; return;
} }
#[test] #[test]
fn pre_002_backend_does_not_advance_connection_pool_tls_or_migrations() { fn pre_005_backend_keeps_environment_sql_migrations_and_physical_types_private() {
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}");
}
let crate_root = include_str!("../src/lib.rs"); let crate_root = include_str!("../src/lib.rs");
assert!(crate_root.contains("mod constants;")); assert!(crate_root.contains("mod error;"));
assert!(crate_root.contains("pub(crate) use self::constants::TRACING_TARGET;")); assert!(crate_root.contains("mod runtime;"));
assert!(crate_root.contains("const _: &str = TRACING_TARGET;")); assert!(crate_root.contains("const _: &str = crate::TRACING_TARGET;"));
for forbidden in ["pub mod ", "pub struct", "pub enum", "pub trait", "tokio_postgres", "deadpool_postgres", "mod migration", "SELECT ", "CREATE TABLE"] { for forbidden in [
assert!(!crate_root.contains(forbidden), "premature PostgreSQL backend surface detected: {forbidden}"); "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; return;
} }

View File

@@ -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;
}

View File

@@ -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("<redacted>"));
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;
}

135
deltas/0.3.2/pre.005.md Normal file
View File

@@ -0,0 +1,135 @@
<!-- file: deltas/0.3.2/pre.005.md -->
<!-- version: 1 -->
# 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
```

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md --> <!-- file: docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md -->
<!-- version: 6 --> <!-- version: 7 -->
# Plan `0.3.2` — Store/PostgreSQL runtime foundation # 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 ### `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 ### `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 : Les détails suivants sont réservés à leur tranche sans rouvrir les décisions du gate :
```text ```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 DDL précis de ksp_store_schema_migrations en pre.006
forme finale des snapshots health en pre.007 forme finale des snapshots health en pre.007
``` ```

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md --> <!-- file: docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md -->
<!-- version: 6 --> <!-- version: 7 -->
# Validation `0.3.2` — Store/PostgreSQL runtime foundation # 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.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 ## 3. Frontières Cargo
### V32-DEP-001 — Façade -> API ### V32-DEP-001 — Façade -> API
@@ -134,7 +136,9 @@ Store/backend -> Materializer
consumer ordinaire -> ksp-store-postgres-lib 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é ### V32-DEP-006 — `ksp-store-api` non régressé
@@ -199,9 +203,9 @@ close borné
Drop best-effort seulement 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 ## 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. 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 ### 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. 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 ### 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`. 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 ### 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. `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 ### 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. 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 ## 6. Pool et lifecycle PostgreSQL
@@ -292,19 +296,25 @@ aucune taille zéro
aucune valeur pathologique 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 ### 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. 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 ### 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. 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 ### V32-POOL-004 — Close
@@ -317,7 +327,9 @@ shutdown respecte timeout
aucune tâche volontairement laissée orpheline 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 ## 7. TLS
@@ -330,7 +342,9 @@ Disabled
VerifyFull 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 ### V32-TLS-002 — VerifyFull
@@ -344,7 +358,9 @@ nom serveur vérifié
aucun fallback plaintext 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 ### V32-TLS-003 — Pas de fichier TLS implicite