diff --git a/Cargo.toml b/Cargo.toml index 0f25fab..65a9ca7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 272 +# version: 273 [workspace] resolver = "3" members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"] [workspace.package] -version = "0.2.11-pre.3" +version = "0.2.11-pre.3.fix.1" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-offchain-transport-lib/src/error.rs b/crates/ksp-offchain-transport-lib/src/error.rs index bc89973..5e0aec3 100644 --- a/crates/ksp-offchain-transport-lib/src/error.rs +++ b/crates/ksp-offchain-transport-lib/src/error.rs @@ -1,30 +1,30 @@ // file: crates/ksp-offchain-transport-lib/src/error.rs -// version: 4 +// version: 5 -/// Stable off-chain transport error for an invalid crate-wide HTTP runtime configuration. -pub const ERROR_CODE_HTTP_SETTINGS_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "http_settings_invalid"); -/// Stable off-chain transport error when the hardened reqwest client cannot be initialized. -pub const ERROR_CODE_HTTP_CLIENT_BUILD_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "http_client_build_failed"); -/// Stable off-chain transport error for an invalid crate-owned HTTP request definition. -pub const ERROR_CODE_HTTP_REQUEST_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "http_request_invalid"); -/// Stable off-chain transport error for a connection failure without exposing the provider URL. -pub const ERROR_CODE_HTTP_CONNECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "http_connection_failed"); -/// Stable off-chain transport error for an end-to-end HTTP timeout. -pub const ERROR_CODE_HTTP_TIMEOUT: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "http_timeout"); -/// Stable off-chain transport error for a generic unsuccessful or transport-level HTTP request. -pub const ERROR_CODE_HTTP_REQUEST_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "http_request_failed"); /// Stable off-chain transport error for HTTP 401/403 access denial. pub const ERROR_CODE_HTTP_ACCESS_DENIED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "http_access_denied"); -/// Stable off-chain transport error for HTTP 429 rate limiting. -pub const ERROR_CODE_HTTP_RATE_LIMITED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "http_rate_limited"); -/// Stable off-chain transport error for transient HTTP status failures such as 408 or 5xx. -pub const ERROR_CODE_HTTP_TEMPORARY_FAILURE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "http_temporary_failure"); -/// Stable off-chain transport error when a response exceeds the defensive body limit. -pub const ERROR_CODE_HTTP_RESPONSE_TOO_LARGE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "http_response_too_large"); +/// Stable off-chain transport error when the hardened reqwest client cannot be initialized. +pub const ERROR_CODE_HTTP_CLIENT_BUILD_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "http_client_build_failed"); +/// Stable off-chain transport error for a connection failure without exposing the provider URL. +pub const ERROR_CODE_HTTP_CONNECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "http_connection_failed"); /// Stable off-chain transport error when a successful HTTP response is not syntactically valid JSON. pub const ERROR_CODE_HTTP_INVALID_JSON: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "http_invalid_json"); +/// Stable off-chain transport error for HTTP 429 rate limiting. +pub const ERROR_CODE_HTTP_RATE_LIMITED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "http_rate_limited"); /// Stable off-chain transport error for an invalid local request-admission/rate-limit policy. pub const ERROR_CODE_HTTP_RATE_LIMIT_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "http_rate_limit_invalid"); +/// Stable off-chain transport error for a generic unsuccessful or transport-level HTTP request. +pub const ERROR_CODE_HTTP_REQUEST_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "http_request_failed"); +/// Stable off-chain transport error for an invalid crate-owned HTTP request definition. +pub const ERROR_CODE_HTTP_REQUEST_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "http_request_invalid"); +/// Stable off-chain transport error when a response exceeds the defensive body limit. +pub const ERROR_CODE_HTTP_RESPONSE_TOO_LARGE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "http_response_too_large"); +/// Stable off-chain transport error for an invalid crate-wide HTTP runtime configuration. +pub const ERROR_CODE_HTTP_SETTINGS_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "http_settings_invalid"); +/// Stable off-chain transport error for transient HTTP status failures such as 408 or 5xx. +pub const ERROR_CODE_HTTP_TEMPORARY_FAILURE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "http_temporary_failure"); +/// Stable off-chain transport error for an end-to-end HTTP timeout. +pub const ERROR_CODE_HTTP_TIMEOUT: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "http_timeout"); /// Stable off-chain transport error for an invalid exact market-price decimal. pub const ERROR_CODE_MARKET_PRICE_DECIMAL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "market_price_decimal_invalid"); /// Stable off-chain transport error for an invalid normalized market-price observation. diff --git a/crates/ksp-offchain-transport-lib/src/http_admission.rs b/crates/ksp-offchain-transport-lib/src/http_admission.rs index 1ad0ef4..f87039e 100644 --- a/crates/ksp-offchain-transport-lib/src/http_admission.rs +++ b/crates/ksp-offchain-transport-lib/src/http_admission.rs @@ -1,5 +1,5 @@ // file: crates/ksp-offchain-transport-lib/src/http_admission.rs -// version: 1 +// version: 2 //! Provider-neutral local request admission and rate-limit cooldown primitives. @@ -20,14 +20,14 @@ pub(crate) enum HttpAdmissionPolicy { Unlimited, } -impl HttpAdmissionPolicy { +impl crate::HttpAdmissionPolicy { /// Creates a validated fixed-window admission policy. pub(crate) fn fixed(requests: u32, window: std::time::Duration, burst: std::option::Option) -> ksp_core_lib::Result { let burst = match burst { std::option::Option::Some(value) => value, std::option::Option::None => 1, }; - if requests == 0 || window.is_zero() || window > MAX_RATE_LIMIT_WINDOW || burst == 0 || burst > requests { + if requests == 0 || window.is_zero() || window > MAX_RATE_LIMIT_WINDOW || burst == 0 { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_HTTP_RATE_LIMIT_INVALID, "HTTP request-admission policy is invalid") .with_context("field", "rate_limit"), @@ -49,28 +49,28 @@ pub(crate) enum HttpAdmissionDecision { pub(crate) struct HttpAdmissionController { cooldown_until: std::sync::Mutex>, fallback_cooldown: std::time::Duration, - policy: HttpAdmissionPolicy, + policy: crate::HttpAdmissionPolicy, token_bucket: std::sync::Mutex>, } -impl HttpAdmissionController { +impl crate::HttpAdmissionController { /// Creates one limiter from a provider-owned local admission policy. - pub(crate) fn new(policy: HttpAdmissionPolicy, fallback_cooldown: std::option::Option) -> ksp_core_lib::Result { + pub(crate) fn new(policy: crate::HttpAdmissionPolicy, fallback_cooldown: std::option::Option) -> ksp_core_lib::Result { let fallback_cooldown = match fallback_cooldown { std::option::Option::Some(value) => value, std::option::Option::None => DEFAULT_RATE_LIMIT_COOLDOWN, }; - if fallback_cooldown.is_zero() || fallback_cooldown > HTTP_MAX_RETRY_AFTER { + if fallback_cooldown.is_zero() || fallback_cooldown > crate::HTTP_MAX_RETRY_AFTER { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_HTTP_RATE_LIMIT_INVALID, "HTTP fallback cooldown is outside the supported bounds") .with_context("field", "fallback_cooldown"), ); } let token_bucket = match policy { - HttpAdmissionPolicy::Fixed { requests, window, burst } => { + crate::HttpAdmissionPolicy::Fixed { requests, window, burst } => { std::option::Option::Some(HttpTokenBucketState::new(requests, window, burst, std::time::Instant::now())) }, - HttpAdmissionPolicy::Dynamic | HttpAdmissionPolicy::Unlimited => std::option::Option::None, + crate::HttpAdmissionPolicy::Dynamic | crate::HttpAdmissionPolicy::Unlimited => std::option::Option::None, }; return std::result::Result::Ok(Self { cooldown_until: std::sync::Mutex::new(std::option::Option::None), @@ -82,19 +82,19 @@ impl HttpAdmissionController { /// Returns the configured local policy. #[must_use] - pub(crate) const fn policy(&self) -> HttpAdmissionPolicy { + pub(crate) const fn policy(&self) -> crate::HttpAdmissionPolicy { return self.policy; } /// Tries to admit one request immediately without sleeping. - pub(crate) fn try_admit(&self) -> HttpAdmissionDecision { + pub(crate) fn try_admit(&self) -> crate::HttpAdmissionDecision { return self.try_admit_at(std::time::Instant::now()); } /// Records a provider 429 and extends cooldown using a bounded `Retry-After` value when present. pub(crate) fn record_rate_limited(&self, provider_retry_after: std::option::Option) -> std::time::Duration { let provider_delay = match provider_retry_after { - std::option::Option::Some(value) => std::cmp::min(value, HTTP_MAX_RETRY_AFTER), + std::option::Option::Some(value) => std::cmp::min(value, crate::HTTP_MAX_RETRY_AFTER), std::option::Option::None => std::time::Duration::ZERO, }; let effective = std::cmp::max(self.fallback_cooldown, provider_delay); @@ -113,9 +113,9 @@ impl HttpAdmissionController { return self.cooldown_remaining_at(std::time::Instant::now()); } - fn try_admit_at(&self, now: std::time::Instant) -> HttpAdmissionDecision { + fn try_admit_at(&self, now: std::time::Instant) -> crate::HttpAdmissionDecision { if let std::option::Option::Some(remaining) = self.cooldown_remaining_at(now) { - return HttpAdmissionDecision::Deferred(remaining); + return crate::HttpAdmissionDecision::Deferred(remaining); } let lock_result = self.token_bucket.lock(); let mut token_bucket = match lock_result { @@ -124,11 +124,11 @@ impl HttpAdmissionController { }; let state = match token_bucket.as_mut() { std::option::Option::Some(value) => value, - std::option::Option::None => return HttpAdmissionDecision::Ready, + std::option::Option::None => return crate::HttpAdmissionDecision::Ready, }; return match state.try_consume_at(now) { - std::option::Option::Some(delay) => HttpAdmissionDecision::Deferred(delay), - std::option::Option::None => HttpAdmissionDecision::Ready, + std::option::Option::Some(delay) => crate::HttpAdmissionDecision::Deferred(delay), + std::option::Option::None => crate::HttpAdmissionDecision::Ready, }; } diff --git a/crates/ksp-offchain-transport-lib/src/http_client.rs b/crates/ksp-offchain-transport-lib/src/http_client.rs index f6b8cad..5e91559 100644 --- a/crates/ksp-offchain-transport-lib/src/http_client.rs +++ b/crates/ksp-offchain-transport-lib/src/http_client.rs @@ -1,5 +1,5 @@ // file: crates/ksp-offchain-transport-lib/src/http_client.rs -// version: 1 +// version: 2 //! Crate-wide hardened REST client used internally by off-chain capability adapters. @@ -8,7 +8,7 @@ pub(crate) struct HttpJsonDocument { bytes: std::vec::Vec, } -impl HttpJsonDocument { +impl crate::HttpJsonDocument { /// Returns the validated raw JSON bytes for provider-specific typed deserialization. #[must_use] pub(crate) fn as_bytes(&self) -> &[u8] { @@ -16,7 +16,7 @@ impl HttpJsonDocument { } } -impl std::fmt::Debug for HttpJsonDocument { +impl std::fmt::Debug for crate::HttpJsonDocument { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter.debug_struct("HttpJsonDocument").field("byte_len", &self.bytes.len()).finish(); } @@ -30,7 +30,7 @@ pub(crate) struct HttpGetRequest { url: reqwest::Url, } -impl HttpGetRequest { +impl crate::HttpGetRequest { /// Creates one HTTPS GET request from a crate-owned official provider URL. pub(crate) fn new_https(url: &'static str) -> ksp_core_lib::Result { return Self::parse(url, false); @@ -106,7 +106,7 @@ impl HttpGetRequest { } } -impl std::fmt::Debug for HttpGetRequest { +impl std::fmt::Debug for crate::HttpGetRequest { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter.write_str("HttpGetRequest()"); } @@ -119,7 +119,7 @@ pub(crate) struct HttpRestClient { settings: crate::HttpClientSettings, } -impl HttpRestClient { +impl crate::HttpRestClient { /// Builds one hardened client with redirects, system proxies and reqwest automatic retries disabled. pub(crate) fn new(settings: crate::HttpClientSettings) -> ksp_core_lib::Result { let client_result = reqwest::Client::builder() @@ -151,7 +151,12 @@ impl HttpRestClient { } /// Executes one GET request and returns only a bounded syntactically valid JSON document. - pub(crate) async fn get_json(&self, provider: &'static str, operation: &'static str, request: HttpGetRequest) -> ksp_core_lib::Result { + pub(crate) async fn get_json( + &self, + provider: &'static str, + operation: &'static str, + request: crate::HttpGetRequest, + ) -> ksp_core_lib::Result { let send_result = self.client.get(request.url).headers(request.headers).send().await; let mut response = match send_result { std::result::Result::Ok(value) => value, @@ -198,7 +203,7 @@ impl HttpRestClient { response_body_bytes = body.len(), "completed off-chain HTTP REST request" ); - return std::result::Result::Ok(HttpJsonDocument { bytes: body }); + return std::result::Result::Ok(crate::HttpJsonDocument { bytes: body }); } } @@ -207,7 +212,7 @@ fn classify_http_status( operation: &'static str, status: u16, retry_after: std::option::Option, -) -> ksp_core_lib::Result { +) -> ksp_core_lib::Result { if status == 401 || status == 403 { return std::result::Result::Err(http_status_error( crate::ERROR_CODE_HTTP_ACCESS_DENIED, @@ -303,7 +308,7 @@ fn parse_retry_after(headers: &reqwest::header::HeaderMap) -> std::option::Optio return std::option::Option::Some(std::cmp::min(std::time::Duration::from_secs(seconds), crate::HTTP_MAX_RETRY_AFTER)); } -fn response_too_large(provider: &'static str, operation: &'static str, limit: usize) -> ksp_core_lib::Result { +fn response_too_large(provider: &'static str, operation: &'static str, limit: usize) -> ksp_core_lib::Result { return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_HTTP_RESPONSE_TOO_LARGE, "Off-chain provider response exceeded the configured body limit") .with_context("provider", provider) diff --git a/crates/ksp-offchain-transport-lib/src/http_settings.rs b/crates/ksp-offchain-transport-lib/src/http_settings.rs index c8e196c..ddb82ee 100644 --- a/crates/ksp-offchain-transport-lib/src/http_settings.rs +++ b/crates/ksp-offchain-transport-lib/src/http_settings.rs @@ -1,5 +1,5 @@ // file: crates/ksp-offchain-transport-lib/src/http_settings.rs -// version: 1 +// version: 2 //! Crate-wide bounded HTTP runtime settings shared by off-chain capability families. @@ -18,7 +18,7 @@ pub(crate) struct HttpClientSettings { request_timeout: std::time::Duration, } -impl HttpClientSettings { +impl crate::HttpClientSettings { /// Creates one validated HTTP settings value. pub(crate) fn new( connect_timeout: std::time::Duration, @@ -59,7 +59,7 @@ impl HttpClientSettings { } } -impl std::default::Default for HttpClientSettings { +impl std::default::Default for crate::HttpClientSettings { fn default() -> Self { return Self { connect_timeout: DEFAULT_CONNECT_TIMEOUT, @@ -69,7 +69,7 @@ impl std::default::Default for HttpClientSettings { } } -fn invalid_http_settings(message: &str, field: &'static str) -> ksp_core_lib::Result { +fn invalid_http_settings(message: &str, field: &'static str) -> ksp_core_lib::Result { ksp_logging_lib::warn!(target: crate::TRACING_TARGET, field = field, "rejected invalid off-chain HTTP client settings"); return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_HTTP_SETTINGS_INVALID, message).with_context("field", field)); } diff --git a/crates/ksp-offchain-transport-lib/src/lib.rs b/crates/ksp-offchain-transport-lib/src/lib.rs index 08c7c84..62a62ef 100644 --- a/crates/ksp-offchain-transport-lib/src/lib.rs +++ b/crates/ksp-offchain-transport-lib/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-offchain-transport-lib/src/lib.rs -// version: 4 +// version: 5 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -7,18 +7,19 @@ //! KSP-owned off-chain transport foundation. //! -//! `0.2.11-pre.003` keeps the first capability family deliberately narrow (`market_price`, SOL/USD only) while materializing crate-wide hardened HTTP REST -//! primitives and non-blocking local admission/cooldown machinery. These `http_*` internals are shared transport mechanics for future off-chain capability -//! families such as `swap_quote_*`; they are intentionally not re-exported as a generic consumer HTTP client. Provider wire DTOs, provider adapters, Config -//! integration and refresh orchestration remain outside this tranche. +//! `0.2.11-pre.003` keeps the first capability family deliberately narrow (`market_price`, SOL/USD only) while staging crate-wide hardened HTTP REST +//! primitives and non-blocking local admission/cooldown machinery under `cfg(test)` until the first production adapter consumes them in `pre.004`. +//! These `http_*` internals are shared transport mechanics for future off-chain capability families such as `swap_quote_*`; they are intentionally not +//! re-exported as a generic consumer HTTP client. Provider wire DTOs, provider adapters, Config integration and refresh orchestration remain outside this +//! tranche. mod constants; mod error; -#[allow(dead_code)] // Staged in pre.003 and consumed by provider adapters starting in pre.004. +#[cfg(test)] // RUST-API-008: staged until the first production adapter consumes the HTTP admission path in pre.004. mod http_admission; -#[allow(dead_code)] // Staged in pre.003 and consumed by provider adapters starting in pre.004. +#[cfg(test)] // RUST-API-008: staged until the first production adapter consumes the HTTP client path in pre.004. mod http_client; -#[allow(dead_code)] // Staged in pre.003 and consumed by provider adapters starting in pre.004. +#[cfg(test)] // RUST-API-008: staged until the first production adapter consumes the HTTP settings path in pre.004. mod http_settings; mod market_price_decimal; mod market_price_observation; @@ -109,18 +110,26 @@ pub use self::market_price_settings::MarketPriceProviderCommonSettings; /// Owning tracing target for events emitted by Off-chain Transport. pub(crate) use self::constants::TRACING_TARGET; /// Maximum provider-directed cooldown accepted from a server `Retry-After` value. +#[cfg(test)] pub(crate) use self::http_admission::HTTP_MAX_RETRY_AFTER; /// Crate-internal non-blocking request-admission controller. +#[cfg(test)] pub(crate) use self::http_admission::HttpAdmissionController; /// Crate-internal result of one immediate request-admission attempt. +#[cfg(test)] pub(crate) use self::http_admission::HttpAdmissionDecision; /// Crate-internal provider-neutral local request-admission policy. +#[cfg(test)] pub(crate) use self::http_admission::HttpAdmissionPolicy; /// Crate-internal fixed-origin GET request with redacted diagnostics. +#[cfg(test)] pub(crate) use self::http_client::HttpGetRequest; /// Crate-internal bounded syntactically valid JSON response document. +#[cfg(test)] pub(crate) use self::http_client::HttpJsonDocument; /// Crate-internal hardened REST client shared by capability adapters. +#[cfg(test)] pub(crate) use self::http_client::HttpRestClient; /// Crate-internal bounded HTTP runtime settings. +#[cfg(test)] pub(crate) use self::http_settings::HttpClientSettings; diff --git a/crates/ksp-offchain-transport-lib/unit_tests/http_admission.rs b/crates/ksp-offchain-transport-lib/unit_tests/http_admission.rs index 64b73f4..04f3b03 100644 --- a/crates/ksp-offchain-transport-lib/unit_tests/http_admission.rs +++ b/crates/ksp-offchain-transport-lib/unit_tests/http_admission.rs @@ -1,5 +1,5 @@ // file: crates/ksp-offchain-transport-lib/unit_tests/http_admission.rs -// version: 1 +// version: 2 #[test] fn fixed_admission_smooths_undocumented_burst_and_refills_deterministically() -> ksp_core_lib::Result<()> { @@ -32,7 +32,7 @@ fn fixed_admission_smooths_undocumented_burst_and_refills_deterministically() -> } #[test] -fn documented_burst_is_consumed_atomically_before_refill() -> ksp_core_lib::Result<()> { +fn documented_burst_may_exceed_average_window_budget_and_is_consumed_atomically() -> ksp_core_lib::Result<()> { let policy = match crate::HttpAdmissionPolicy::fixed(1, std::time::Duration::from_secs(1), std::option::Option::Some(2)) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), diff --git a/crates/ksp-offchain-transport-lib/unit_tests/http_client.rs b/crates/ksp-offchain-transport-lib/unit_tests/http_client.rs index 55b36aa..2dd3941 100644 --- a/crates/ksp-offchain-transport-lib/unit_tests/http_client.rs +++ b/crates/ksp-offchain-transport-lib/unit_tests/http_client.rs @@ -1,5 +1,5 @@ // file: crates/ksp-offchain-transport-lib/unit_tests/http_client.rs -// version: 1 +// version: 2 #[tokio::test] async fn rest_client_accepts_bounded_json_and_never_exposes_request_debug() -> ksp_core_lib::Result<()> { @@ -36,6 +36,22 @@ async fn rest_client_accepts_bounded_json_and_never_exposes_request_debug() -> k return std::result::Result::Ok(()); } +#[test] +fn https_request_builder_accepts_official_style_url_without_exposing_it() -> ksp_core_lib::Result<()> { + let mut request = match crate::HttpGetRequest::new_https("https://example.com/price") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + request.append_query_pair("symbol", "SOL/USD"); + if let std::result::Result::Err(error) = request.insert_sensitive_header("x-api-key", "redaction-canary") { + return std::result::Result::Err(error); + } + let debug = format!("{request:?}"); + assert!(!debug.contains("example.com")); + assert!(!debug.contains("redaction-canary")); + return std::result::Result::Ok(()); +} + #[tokio::test] async fn rest_client_rejects_redirects_instead_of_following_them() -> ksp_core_lib::Result<()> { let server_result = diff --git a/deltas/0.2.11/pre.003-fix.001.md b/deltas/0.2.11/pre.003-fix.001.md new file mode 100644 index 0000000..cfa21c3 --- /dev/null +++ b/deltas/0.2.11/pre.003-fix.001.md @@ -0,0 +1,220 @@ + + + +# Delta `0.2.11-pre.003-fix.001` — conformité crate-root, staging HTTP et burst documenté + +## 1. Base requise + +Ce correctif s'applique exclusivement après : + +```text +v0.2.10 ++ 0.2.11-pre.001 ++ 0.2.11-pre.001-fix.001 ++ 0.2.11-pre.002 ++ 0.2.11-pre.002-fix.001 ++ 0.2.11-pre.002-fix.002 ++ 0.2.11-pre.002-fix.003 ++ 0.2.11-pre.003 +``` + +La version Cargo attendue à l'entrée est : + +```text +0.2.11-pre.3 +``` + +La version Cargo de sortie est : + +```text +0.2.11-pre.3.fix.1 +``` + +## 2. Motif du correctif + +Le gate opérateur de `pre.003` a révélé plusieurs écarts avant poursuite vers `pre.004` : + +```text +RUST-FMT-104 : 6 violations dans src/error.rs +cargo check : 6 warnings de réexports pub(crate) HTTP inutilisés +cargo clippy --workspace --all-targets : mêmes warnings de réexports +cargo test -p ksp-offchain-transport-lib : 20 PASS, 1 FAIL +``` + +Le test en échec était : + +```text +http_admission::tests::documented_burst_is_consumed_atomically_before_refill +``` + +La cause était une validation incorrecte refusant `burst > requests`. Cette contrainte contredit un cas déjà retenu dans le plan : Coinbase documente `10 req/s` avec `burst 15`. + +Une relecture de `docs/rules/RULES_RUST.md` confirme également deux règles applicables : + +```text +RUST-IMPORT-009 / RUST-API-004 + un item pub/pub(crate) crate-wide est consommé via la façade crate::Item, + y compris depuis son module de déclaration. + +RUST-API-008 + un helper préparatoire sans consumer de production reste sous #[cfg(test)] + jusqu'à sa première consommation réelle ; #[allow(dead_code)] n'est pas utilisé + pour conserver artificiellement du code mort. +``` + +## 3. Correction de façade crate-root + +Les items HTTP partagés continuent d'être réexportés au crate-root dans le build de test, et leurs implémentations utilisent désormais leur chemin de façade : + +```text +impl crate::HttpAdmissionPolicy +impl crate::HttpAdmissionController +impl crate::HttpJsonDocument +impl crate::HttpGetRequest +impl crate::HttpRestClient +impl crate::HttpClientSettings +``` + +Les signatures et références crate-wide correspondantes utilisent également `crate::Item` lorsqu'elles traversent un module. + +Les types strictement privés, tels que `HttpTokenBucketState`, restent locaux et ne sont pas réexportés. + +## 4. Staging `RUST-API-008` + +Aucun adapter de production ne consomme encore les primitives `http_*` en `pre.003`. + +Les trois modules préparatoires sont donc compilés sous : + +```text +#[cfg(test)] +``` + +jusqu'à `pre.004`, où les premiers adapters de production les consommeront réellement. + +Les anciens : + +```text +#[allow(dead_code)] +``` + +sont supprimés. + +Le staging concerne : + +```text +http_admission +http_client +http_settings +``` + +Les codes d'erreur HTTP publics restent disponibles dans le build normal ; aucune API HTTP générique n'est rendue publique aux consumers. + +## 5. Correction du burst + +La policy fixe conserve les invariants suivants : + +```text +requests > 0 +window > 0 +window <= borne défensive +burst > 0 lorsqu'il est explicite +burst absent -> capacité locale conservatrice de 1 +``` + +En revanche : + +```text +burst <= requests +``` + +n'est plus un invariant. + +Un burst documenté est une capacité instantanée distincte de la cadence moyenne et peut légitimement lui être supérieur. + +Le canari déterministe couvre explicitement : + +```text +1 req/s +burst 2 +2 admissions immédiates +3e admission différée +``` + +Cette propriété protège notamment la représentabilité future du cas Coinbase `10 req/s, burst 15`. + +## 6. Ordre des constantes + +Le bloc `const` de `src/error.rs` est réordonné alphabétiquement conformément à `RUST-FMT-005` / `RUST-FMT-104` sans modifier les valeurs des codes d'erreur. + +Aucun code d'erreur n'est ajouté, supprimé ou renommé. + +## 7. Test HTTP complémentaire + +Un canari supplémentaire construit une requête HTTPS officielle-style sans effectuer de réseau, ajoute query pair et header sensible, puis vérifie que la représentation `Debug` ne contient ni host ni credential. + +Ce canari consomme également le constructeur `HttpGetRequest::new_https` pendant le staging `cfg(test)`. + +## 8. Fichiers modifiés + +```text +Cargo.toml +crates/ksp-offchain-transport-lib/src/error.rs +crates/ksp-offchain-transport-lib/src/http_admission.rs +crates/ksp-offchain-transport-lib/src/http_client.rs +crates/ksp-offchain-transport-lib/src/http_settings.rs +crates/ksp-offchain-transport-lib/src/lib.rs +crates/ksp-offchain-transport-lib/unit_tests/http_admission.rs +crates/ksp-offchain-transport-lib/unit_tests/http_client.rs +docs/plans/018-V0_2_11_OFFCHAIN_PRICE_TRANSPORT_PLAN.md +docs/validation/014-V0_2_11_OFFCHAIN_PRICE_TRANSPORT.md +``` + +## 9. Fichier ajouté + +```text +deltas/0.2.11/pre.003-fix.001.md +``` + +## 10. Fichiers volontairement inchangés + +```text +crates/ksp-offchain-transport-lib/Cargo.toml +crates/ksp-offchain-transport-lib/tests/** +CHANGELOG.md +ROADMAP.md +README.md +.env.example +config/** +crates/ksp-config-lib/** +crates/ksp-onchain-transport-lib/** +prompts/** +``` + +Le manifeste de la crate hérite toujours de `workspace.package.version`; seul le `Cargo.toml` racine porte le bump technique. + +## 11. Validations sandbox + +Exécutées après le correctif : + +```text +python3 scripts/audit_rust_workspace_rules.py + General Rust rule audit: clean + Rust export completeness audit: 0 candidate(s) + KSP workspace Rust rule audit: clean +``` + +Le sandbox ne fournit pas Cargo ; les gates opérateur restent à rejouer sur l'état exact du correctif. + +## 12. Gate opérateur demandé + +```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.2.11 +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-offchain-transport-lib +cargo test --workspace +``` + +Le passage à `pre.004` est conditionné à un gate propre sans warning et au retour au vert du test de burst. diff --git a/docs/plans/018-V0_2_11_OFFCHAIN_PRICE_TRANSPORT_PLAN.md b/docs/plans/018-V0_2_11_OFFCHAIN_PRICE_TRANSPORT_PLAN.md index 3599336..83fcaa0 100644 --- a/docs/plans/018-V0_2_11_OFFCHAIN_PRICE_TRANSPORT_PLAN.md +++ b/docs/plans/018-V0_2_11_OFFCHAIN_PRICE_TRANSPORT_PLAN.md @@ -1,9 +1,9 @@ - + # Plan `0.2.11` — Off-chain price transport SOL/USD multi-provider -**Statut courant : `0.2.11-pre.003` matérialise le HTTP REST commun et le rate limiting provider-neutral sans encore ajouter d'adapter provider. Les primitives `http_*` restent internes à la crate et réutilisables par les futures familles off-chain ; `market_price_*` reste la seule famille métier actuelle et demeure limitée à SOL/USD.** +**Statut courant : `0.2.11-pre.003-fix.001` corrige la conformité crate-root/ordre Rust de la fondation HTTP, autorise les bursts documentés supérieurs à la cadence moyenne et stage les helpers `http_*` sous `cfg(test)` conformément à `RUST-API-008` jusqu'à leur première consommation de production en `pre.004`. `market_price_*` reste la seule famille métier actuelle et demeure limitée à SOL/USD.** ## 1. Base et autorité @@ -504,7 +504,7 @@ Il n'existe pas d'URL provider arbitraire dans la Config V1. Cela évite SSRF, r Les primitives `http_*` sont **crate-private** : elles ne créent pas un client HTTP générique public contournant les capacités métier. Les adapters `market_price_*`, puis de futures familles telles que `swap_quote_*`, les consomment derrière leur propre contrat. -Le limiter `pre.003` est non bloquant. Une cadence fixe est matérialisée par un token bucket lissé ; lorsqu'aucun burst n'est documenté, la capacité locale initiale reste volontairement `1`. Une limite dynamique n'invente aucune cadence locale et apprend seulement des réponses provider, notamment `429`. Un `Retry-After` serveur peut prolonger le cooldown mais ne peut pas dépasser une borne défensive d'une heure. Aucune primitive HTTP commune ne dort en attendant la disponibilité : elle expose un délai de defer que l'orchestrateur `pre.008` pourra projeter provider par provider. +Le limiter `pre.003` est non bloquant. Une cadence fixe est matérialisée par un token bucket lissé ; lorsqu'aucun burst n'est documenté, la capacité locale initiale reste volontairement `1`. Lorsqu'un burst est documenté, sa capacité est indépendante du nombre moyen de requêtes de la fenêtre et peut donc lui être supérieure. Une limite dynamique n'invente aucune cadence locale et apprend seulement des réponses provider, notamment `429`. Un `Retry-After` serveur peut prolonger le cooldown mais ne peut pas dépasser une borne défensive d'une heure. Aucune primitive HTTP commune ne dort en attendant la disponibilité : elle expose un délai de defer que l'orchestrateur `pre.008` pourra projeter provider par provider. ## 15. Config provider-capability-aware @@ -692,10 +692,16 @@ Préfixage de toute la surface publique spécifique à la famille `market_price` ### `pre.003` — HTTP REST commun et rate limiting -**Statut : réalisé.** +**Statut : réalisé avec correctif.** Client HTTP REST commun crate-private, classification d'erreurs HTTP/reqwest, bornes de body et timeouts, redaction, désactivation explicite des redirects/proxy/retries implicites, JSON syntaxiquement validé, limiter token-bucket générique non bloquant et cooldown provider-driven borné. Aucun adapter provider n'est avancé. +#### `pre.003-fix.001` — Façade crate-root, staging et burst documenté + +**Statut : réalisé, gate Cargo opérateur à rejouer.** + +Correction de l'ordre alphabétique des constantes d'erreur, utilisation systématique de la façade crate-root pour les items `http_*` partagés (`impl crate::...` et signatures crate-wide), suppression des `#[allow(dead_code)]` préparatoires au profit de `#[cfg(test)]` conformément à `RUST-API-008`, et correction du contrat de burst : un burst documenté peut dépasser la cadence moyenne de la fenêtre, comme le cas Coinbase `10 req/s, burst 15`. La version technique workspace devient `0.2.11-pre.3.fix.1`. + ### `pre.004` — CoinGecko, CoinMarketCap et CoinPaprika **Statut : planifié.** diff --git a/docs/validation/014-V0_2_11_OFFCHAIN_PRICE_TRANSPORT.md b/docs/validation/014-V0_2_11_OFFCHAIN_PRICE_TRANSPORT.md index 171d104..5146ff1 100644 --- a/docs/validation/014-V0_2_11_OFFCHAIN_PRICE_TRANSPORT.md +++ b/docs/validation/014-V0_2_11_OFFCHAIN_PRICE_TRANSPORT.md @@ -1,5 +1,5 @@ - + # Validation `0.2.11` — Off-chain price transport SOL/USD @@ -122,10 +122,27 @@ Cette correction reste dans la responsabilité de `pre.002` : elle stabilise la | limite dynamique n'invente pas de cadence locale | PASS | `HttpAdmissionPolicy::Dynamic` | | cooldown `Retry-After` borné et admission non bloquante | PASS | `record_rate_limited` + `try_admit` | | adapters CoinGecko/Jupiter/etc. ajoutés | N/A | explicitement réservés à `pre.004+` | -| gate Cargo complet sur l'état livré | PENDING | à exécuter par l'opérateur | +| gate Cargo complet sur `pre.003-fix.001` | PENDING | à rejouer par l'opérateur après le correctif | Les tests déterministes de `pre.003` couvrent en particulier le body chunked dépassant la limite, le refus des redirects, un `429` avec `Retry-After`, la non-propagation d'un body distant canari, le JSON invalide et le refill/cooldown du limiter. +## 3.5 Correctif `0.2.11-pre.003-fix.001` + +Le gate opérateur de `pre.003` a révélé quatre classes d'écarts avant poursuite vers `pre.004` : six violations `RUST-FMT-104` dans `src/error.rs`, six réexports `pub(crate)` HTTP inutilisés dans le build normal, un test de burst rejeté par la validation `burst > requests`, et la non-utilisation de la façade crate-root dans les `impl` des items HTTP partagés. `cargo check` et `cargo clippy` terminaient avec les warnings de réexports, tandis que `cargo test -p ksp-offchain-transport-lib` échouait sur `documented_burst_is_consumed_atomically_before_refill`. + +Le correctif applique les règles suivantes : + +```text +RUST-FMT-005 / RUST-FMT-104 : constantes homogènes triées alphabétiquement +RUST-IMPORT-009 / RUST-API-004 : items HTTP partagés consommés via crate::Item, y compris dans leurs impl +RUST-API-008 : helpers préparatoires sans consumer production sous #[cfg(test)], aucun #[allow(dead_code)] compensatoire +contrat limiter : burst documenté indépendant de la cadence moyenne et autorisé à la dépasser +``` + +Le cas `1 req/s` avec `burst 2` devient un canari déterministe ; il protège notamment la représentabilité du cas Coinbase documenté dans le plan à `10 req/s, burst 15`. Les modules `http_*` restent donc testés et prêts en `pre.003`, mais leur compilation normale sera activée en `pre.004` lorsque les premiers adapters de production les consommeront réellement. + +La version Cargo du correctif est `0.2.11-pre.3.fix.1`. Les audits statiques du sandbox sont propres ; le gate Cargo complet doit être rejoué par l'opérateur. + ## 4. Matrice provider prévue | Provider | SOL/USD V1 | Gratuit V1 | Mode auth prévu | Test déterministe | Smoke live | Statut courant | @@ -166,7 +183,7 @@ Avant la stable, les conditions d'usage et la persistance des offres gratuites d | Cas | Statut | |---------------------------------------------------------------|---------| | cadence locale fixe représentable | PASS | -| burst documenté représentable | PASS | +| burst documenté supérieur à la cadence moyenne représentable | PASS | | limite dynamique keyless représentable | PASS | | `429` classé | PLANNED | | `Retry-After` honoré lorsqu'exploitable | PLANNED |