v0.2.11-pre.003-fix.001

This commit is contained in:
2026-08-25 21:24:30 +02:00
parent be2a06bc80
commit f4413ebbb0
11 changed files with 343 additions and 70 deletions

View File

@@ -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"

View File

@@ -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.

View File

@@ -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<u32>) -> ksp_core_lib::Result<Self> {
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<std::option::Option<std::time::Instant>>,
fallback_cooldown: std::time::Duration,
policy: HttpAdmissionPolicy,
policy: crate::HttpAdmissionPolicy,
token_bucket: std::sync::Mutex<std::option::Option<HttpTokenBucketState>>,
}
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<std::time::Duration>) -> ksp_core_lib::Result<Self> {
pub(crate) fn new(policy: crate::HttpAdmissionPolicy, fallback_cooldown: std::option::Option<std::time::Duration>) -> ksp_core_lib::Result<Self> {
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>) -> 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,
};
}

View File

@@ -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<u8>,
}
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<Self> {
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(<redacted>)");
}
@@ -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<Self> {
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<HttpJsonDocument> {
pub(crate) async fn get_json(
&self,
provider: &'static str,
operation: &'static str,
request: crate::HttpGetRequest,
) -> ksp_core_lib::Result<crate::HttpJsonDocument> {
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<std::time::Duration>,
) -> ksp_core_lib::Result<HttpJsonDocument> {
) -> ksp_core_lib::Result<crate::HttpJsonDocument> {
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<HttpJsonDocument> {
fn response_too_large(provider: &'static str, operation: &'static str, limit: usize) -> ksp_core_lib::Result<crate::HttpJsonDocument> {
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)

View File

@@ -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<HttpClientSettings> {
fn invalid_http_settings(message: &str, field: &'static str) -> ksp_core_lib::Result<crate::HttpClientSettings> {
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));
}

View File

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

View File

@@ -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),

View File

@@ -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 =

View File

@@ -0,0 +1,220 @@
<!-- file: deltas/0.2.11/pre.003-fix.001.md -->
<!-- version: 1 -->
# 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.

View File

@@ -1,9 +1,9 @@
<!-- file: docs/plans/018-V0_2_11_OFFCHAIN_PRICE_TRANSPORT_PLAN.md -->
<!-- version: 7 -->
<!-- version: 8 -->
# 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é.**

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/014-V0_2_11_OFFCHAIN_PRICE_TRANSPORT.md -->
<!-- version: 6 -->
<!-- version: 7 -->
# 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 |