23 Commits

Author SHA1 Message Date
8c65096f4c v0.2.5-rel.001 2026-08-20 12:31:08 +02:00
cb142b1cb8 v0.2.5-pre.010.fix.003 2026-08-20 12:15:36 +02:00
08c8046262 v0.2.5-pre.010.fix.002 2026-08-20 11:53:55 +02:00
c0b131bf6f v0.2.5-pre.010.fix.001 2026-08-20 11:17:12 +02:00
5bf9651038 v0.2.5-pre.010 2026-08-20 09:58:14 +02:00
e91bf36e9e v0.2.5-pre.009 2026-08-20 09:24:10 +02:00
1125ade4c1 v0.2.5-pre.008 2026-08-19 18:47:06 +02:00
518cc9257f v0.2.5-pre.007-fix.001 2026-08-19 18:12:31 +02:00
46afe10423 v0.2.5-pre.007 2026-08-19 18:08:01 +02:00
25edcc231e v0.2.5-pre.006-fix.001 2026-08-19 17:35:14 +02:00
6094d33120 v0.2.5-pre.006 2026-08-19 17:31:29 +02:00
dcd23abb92 v0.2.5-pre.005-fix.001 2026-08-19 13:15:09 +02:00
36b98e0abc v0.2.5-pre.005 2026-08-19 13:09:16 +02:00
3c069347b7 v0.2.5-pre.004-fix.002 2026-08-19 11:47:32 +02:00
a0a5595186 v0.2.5-pre.004-fix.001 2026-08-19 11:37:36 +02:00
6c1172efc3 v0.2.5-pre.004 2026-08-19 11:29:05 +02:00
d5536b0778 v0.2.5-pre.003-fix.001 2026-08-19 11:24:32 +02:00
c6794af05b v0.2.5-pre.003 2026-08-19 10:52:21 +02:00
bcda6db11f v0.2.5-pre.002-fix.002 2026-08-19 10:08:47 +02:00
51b5114ae2 v0.2.5-pre.002-fix.001 2026-08-19 10:03:37 +02:00
c97a8739ab v0.2.5-pre.002 2026-08-19 09:56:01 +02:00
c2ccef3849 v0.2.5-pre.001-fix.001 2026-08-19 09:37:07 +02:00
58134c81d1 v0.2.5-pre.001 2026-08-19 09:11:36 +02:00
170 changed files with 18719 additions and 1733 deletions

5
.pydevproject Normal file
View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?eclipse-pydev version="1.0"?><pydev_project>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python interpreter</pydev_property>
</pydev_project>

View File

@@ -1,8 +1,14 @@
<!-- file: CHANGELOG.md --> <!-- file: CHANGELOG.md -->
<!-- version: 8 --> <!-- version: 9 -->
# Changelog KSP # Changelog KSP
## 0.2.5 — Wallet foundation — 2026-08-20
`0.2.5` publie `ksp-wallet-lib` et le format natif interopérable `.kspwallet` V1. La release stabilise les capacités cryptographiquement indépendantes VIEW/OWNER, le masquage de la Pubkey/alias/notes à létat verrouillé, Argon2id v19 avec paramètres sérialisés et profil de création `65 536 KiB / 3 / 1`, XChaCha20-Poly1305, une autorité Ed25519 OWNER distincte de la keypair Solana, les compartiments `owner_control`/`metadata`/`secret`, la création et louverture mémoire/fichier, la persistence no-clobber, la signature Solana OWNER sans getter secret, ladministration alias/notes, les rotations OWNER/VIEW, la révocation forte VIEW avec rekey metadata, la détection de handles stale et les adapters Solana CLI JSON/Base58 pour inspection/import/export. Le wire V1 est documenté indépendamment de Rust dans `docs/formats/KSPWALLET_V1.md` et couvert par des vecteurs publics ainsi que des canaris adversariaux et une reproduction externe. La release conserve Wallet indépendant de Config/Transport/ExecutionPolicy/Store/Tauri et expose la Pubkey via `ksp-core-lib` tandis que `solana-keypair` reste encapsulée dans Wallet.
La clôture technique `pre.010-fix.001` à `fix.003` met la dépendance directe KSP à `ed25519-dalek 3.0.0`, accepte la génération `2.2.0` transitive encore imposée par `solana-keypair 3.1.2`, normalise les sources Rust du workspace et introduit `python3 scripts/audit_rust_workspace_rules.py` comme gate structurel complémentaire à rustfmt/Clippy. Le checkpoint final `pre.010-fix.003` est validé avec audit Python clean, `cargo check`, Clippy, tests Wallet et workspace complet verts. Le prompt `prompts/011-V0_2_6_START_PROMPT.md` ouvre ensuite `0.2.6 — Wallet Desk` par audit/sizing avant implémentation.
## 0.2.4 — HTTP Blocks + Economics + compliance HTTP finale — 2026-08-18 ## 0.2.4 — HTTP Blocks + Economics + compliance HTTP finale — 2026-08-18
`0.2.4` achève la surface HTTP Solana standard de `ksp-onchain-transport-lib` : les 10 wrappers Blocks et 5 wrappers Economics portent la couverture typée à **52/52 méthodes HTTP courantes**, tandis que les **14/14 méthodes historiques Deprecated/runtime Removed** restent conservées pour compliance. La release stabilise notamment `getBlock` moderne avec sa forme bare encoding legacy dépréciée, les quatre variantes `transactionDetails`, les versions transaction numériques génériques, `numRewardPartitions` (SIMD-0118), `commissionBps` (SIMD-0291), les overloads de ranges/performance, `getBlockProduction`, les valeurs Economics fournies par le runtime et `getInflationReward` positionnel sans plafond d'adresses inventé. Le réaudit final confirme l'égalité exacte entre l'inventaire HTTP officiel courant et le registre KSP, applique `KSP-TRANSPORT-007` aux **52/52 wrappers**, et ne matérialise aucune extension spéculative issue de la watchlist SIMD. Les canaries de frontières, le workspace complet et les deux smokes Devnet — Transport pur couvrant Accounts/Tokens/Cluster/Transactions/Blocks/Economics et composition Config -> Transport transitoire — sont validés avant publication. Le prompt `prompts/010-V0_2_5_START_PROMPT.md`, enrichi par `pre.009-fix.001`, ouvre `0.2.5 — Wallet foundation` avec threat model, interopérabilité `.kspwallet` et capacités indépendantes VIEW/OWNER. `0.2.4` achève la surface HTTP Solana standard de `ksp-onchain-transport-lib` : les 10 wrappers Blocks et 5 wrappers Economics portent la couverture typée à **52/52 méthodes HTTP courantes**, tandis que les **14/14 méthodes historiques Deprecated/runtime Removed** restent conservées pour compliance. La release stabilise notamment `getBlock` moderne avec sa forme bare encoding legacy dépréciée, les quatre variantes `transactionDetails`, les versions transaction numériques génériques, `numRewardPartitions` (SIMD-0118), `commissionBps` (SIMD-0291), les overloads de ranges/performance, `getBlockProduction`, les valeurs Economics fournies par le runtime et `getInflationReward` positionnel sans plafond d'adresses inventé. Le réaudit final confirme l'égalité exacte entre l'inventaire HTTP officiel courant et le registre KSP, applique `KSP-TRANSPORT-007` aux **52/52 wrappers**, et ne matérialise aucune extension spéculative issue de la watchlist SIMD. Les canaries de frontières, le workspace complet et les deux smokes Devnet — Transport pur couvrant Accounts/Tokens/Cluster/Transactions/Blocks/Economics et composition Config -> Transport transitoire — sont validés avant publication. Le prompt `prompts/010-V0_2_5_START_PROMPT.md`, enrichi par `pre.009-fix.001`, ouvre `0.2.5 — Wallet foundation` avec threat model, interopérabilité `.kspwallet` et capacités indépendantes VIEW/OWNER.

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 142 # version: 163
[workspace] [workspace]
resolver = "3" resolver = "3"
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib"] members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
[workspace.package] [workspace.package]
version = "0.2.4" version = "0.2.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"
@@ -14,21 +14,29 @@ authors = ["SinuS von SifriduS <sinus@sasedev.net>"]
publish = false publish = false
[workspace.dependencies] [workspace.dependencies]
argon2 = { version = "^0.5", default-features = false }
chacha20poly1305 = { version = "^0.11", default-features = false }
ed25519-dalek = { version = "^3.0", default-features = false }
getrandom = { version = "^0.4", default-features = false }
base64 = { version = "^0.23" }
fs2 = { version = "^0.4" } fs2 = { version = "^0.4" }
serde = { version = "^1.0" } serde = { version = "^1.0" }
serde_json = { version = "^1.0" } serde_json = { version = "^1.0" }
jsonschema = { version = "^0.49", default-features = false } jsonschema = { version = "^0.49", default-features = false }
reqwest = { version = "^0.13", default-features = false } reqwest = { version = "^0.13", default-features = false }
solana-keypair = { version = "^3.1", default-features = false }
solana-pubkey = { version = "^4.3", default-features = false } solana-pubkey = { version = "^4.3", default-features = false }
tracing = { version = "^0.1", default-features = false } 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 }
tempfile = { version = "^3.27" }
chrono = { version = "^0.4", default-features = false } chrono = { version = "^0.4", default-features = false }
tauri = { version = "^2.11" } tauri = { version = "^2.11" }
tauri-build = { version = "^2.6" } tauri-build = { version = "^2.6" }
tauri-plugin-tracing = { version = "^0.3" } tauri-plugin-tracing = { version = "^0.3" }
ts-rs = { version = "^12.0" } ts-rs = { version = "^12.0" }
zeroize = { version = "^1.9" }
[workspace.lints.rust] [workspace.lints.rust]
missing_docs = "warn" missing_docs = "warn"

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md --> <!-- file: ROADMAP.md -->
<!-- version: 43 --> <!-- version: 55 -->
# Roadmap KSP # Roadmap KSP
@@ -49,13 +49,13 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
- [X] `0.2.2` — HTTP Accounts + Tokens + Cluster : 22 wrappers typés (5 Accounts + 5 Tokens + 12 Cluster), canaries de complétude 52+14, smoke Devnet Transport pur et smoke historique Config -> Transport validés, documentation durable et prompt `0.2.3` publiés stables. - [X] `0.2.2` — HTTP Accounts + Tokens + Cluster : 22 wrappers typés (5 Accounts + 5 Tokens + 12 Cluster), canaries de complétude 52+14, smoke Devnet Transport pur et smoke historique Config -> Transport validés, documentation durable et prompt `0.2.3` publiés stables.
- [X] `0.2.3` — HTTP Transactions stable : 11/11 wrappers typés publiés, classification `8 Read / 2 WriteSubmission / 1 Simulation`, no-resend ambigu prouvé pour les write submissions, `KSP-TRANSPORT-007` réaudité conforme sur les 37 wrappers HTTP courants, graphes Cargo et deux smokes Devnet validés ; `0.2.4` reprend les 15 Blocks/Economics restants. - [X] `0.2.3` — HTTP Transactions stable : 11/11 wrappers typés publiés, classification `8 Read / 2 WriteSubmission / 1 Simulation`, no-resend ambigu prouvé pour les write submissions, `KSP-TRANSPORT-007` réaudité conforme sur les 37 wrappers HTTP courants, graphes Cargo et deux smokes Devnet validés ; `0.2.4` reprend les 15 Blocks/Economics restants.
- [X] `0.2.4` — HTTP Blocks + Economics stable : 15/15 wrappers `V0_2_4` publiés, surface typed complète à 52/52 méthodes courantes, 14/14 historiques conservées, réaudit SIMD/inventaire final et `KSP-TRANSPORT-007` global validés ; deux smokes Devnet passés avant publication. - [X] `0.2.4` — HTTP Blocks + Economics stable : 15/15 wrappers `V0_2_4` publiés, surface typed complète à 52/52 méthodes courantes, 14/14 historiques conservées, réaudit SIMD/inventaire final et `KSP-TRANSPORT-007` global validés ; deux smokes Devnet passés avant publication.
- [ ] `0.2.5`Introduire `ksp-wallet-lib` et le format interopérable `.kspwallet` avec threat model offline, capacités indépendantes VIEW/OWNER, metadata protégées, signature, rotations de key slots et import/export extensible ; exclure `WalletPolicy`. - [X] `0.2.5`Wallet foundation stable : `.kspwallet` V1, VIEW/OWNER indépendants, Argon2id/XChaCha20-Poly1305, autorité Ed25519 OWNER, persistence no-clobber, signature, administration/rotations/révocation VIEW forte, import/export Solana CLI JSON + Base58, canaris adversariaux, interop externe et documentation durable publiés. La clôture `pre.010-fix.001``fix.003` ajoute `ed25519-dalek 3.0.0` direct, normalise le Rust workspace et installe laudit structurel Python complémentaire à rustfmt/Clippy. `Pubkey` reste via `ksp-core-lib`, la keypair reste encapsulée dans Wallet et Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet.
- [ ] `0.2.6` — Introduire `ksp-app-wallet-desk` utilisant Config composite + Wallet + transport HTTP, notamment pour afficher l'identité et le solde d'un wallet. - [ ] `0.2.6` — Introduire `ksp-app-wallet-desk` utilisant Config composite + Wallet + transport HTTP, notamment pour afficher l'identité et le solde d'un wallet ; le prompt de démarrage est préparé par `0.2.5-pre.010`.
- [ ] `0.2.7` — Étendre `ksp-onchain-transport-lib` au WebSocket Solana standard complet ; permettre plusieurs sessions sur une même URL sans imposer encore un pool automatique complexe. - [ ] `0.2.7` — Étendre `ksp-onchain-transport-lib` au WebSocket Solana standard complet ; permettre plusieurs sessions sur une même URL sans imposer encore un pool automatique complexe.
- [ ] `0.2.8` — Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client. - [ ] `0.2.8` — Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client.
- [ ] `0.2.9` — Ajouter une première fondation Yellowstone gRPC standard/provider-neutral ; dimensionner la surface exacte à `pre.001` selon la documentation normative actuelle. - [ ] `0.2.9` — Ajouter une première fondation Yellowstone gRPC standard/provider-neutral ; dimensionner la surface exacte à `pre.001` selon la documentation normative actuelle.
- [ ] `0.2.10` — Introduire `ksp-offchain-transport-lib` avec un premier lecteur de prix, au minimum SOL/USD et SOL/EUR. - [ ] `0.2.10` — Introduire `ksp-offchain-transport-lib` avec un premier lecteur de prix, au minimum SOL/USD et SOL/EUR.
- [ ] `0.2.11` — Introduire une petite application desk de visualisation des prix. - [ ] `0.2.11` — Introduire une petite application desk de visualisation/validation des prix offchain, puis intégrer cette capacité dans `ksp-app-wallet-desk` sans dupliquer la logique de récupération/normalisation possédée par le composant spécialisé.
- [ ] `0.2.12` — Introduire la première surface de `ksp-interface-lib`, comprenant une API wire publique utilisable par les implémentations officielles et externes. - [ ] `0.2.12` — Introduire la première surface de `ksp-interface-lib`, comprenant une API wire publique utilisable par les implémentations officielles et externes.
- [ ] `0.2.13` — Introduire `ksp-program-api` comme premier contrat Program extensible, sans imposer encore `ksp-program-lib` complet. - [ ] `0.2.13` — Introduire `ksp-program-api` comme premier contrat Program extensible, sans imposer encore `ksp-program-lib` complet.

View File

@@ -1,17 +1,8 @@
// file: crates/ksp-app-config-desk/src/app_state.rs // file: crates/ksp-app-config-desk/src/app_state.rs
// version: 6 // version: 8
//! Shared backend state owned by the Tauri application. //! Shared backend state owned by the Tauri application.
struct LoggingRuntimeState {
guard: ksp_logging_lib::LoggingGuard,
active_profile_id: std::option::Option<String>,
selection_source: String,
generation: u32,
fallback_active: bool,
startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
}
/// Shared Config Desk application state managed by Tauri. /// Shared Config Desk application state managed by Tauri.
pub(crate) struct AppState { pub(crate) struct AppState {
config_management: ksp_config_lib::ConfigManagement, config_management: ksp_config_lib::ConfigManagement,
@@ -28,7 +19,7 @@ impl AppState {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
let runtime_identity = crate::logging_runtime::launch_identity(); let runtime_identity = crate::launch_identity();
let runtime_identity = match runtime_identity { let runtime_identity = match runtime_identity {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -175,7 +166,7 @@ impl AppState {
let dropped = runtime.guard.dropped_lines(); let dropped = runtime.guard.dropped_lines();
let mut files = std::vec::Vec::<crate::LoggingRuntimeFileDto>::new(); let mut files = std::vec::Vec::<crate::LoggingRuntimeFileDto>::new();
for metadata in runtime.guard.active_file_outputs() { for metadata in runtime.guard.active_file_outputs() {
files.push(crate::logging_runtime::project_file(&metadata)); files.push(crate::project_logging_file(&metadata));
} }
return std::result::Result::Ok(crate::LoggingRuntimeStatusDto { return std::result::Result::Ok(crate::LoggingRuntimeStatusDto {
active_profile: runtime.active_profile_id.clone(), active_profile: runtime.active_profile_id.clone(),
@@ -185,9 +176,9 @@ impl AppState {
application_id: identity.application_id().to_owned(), application_id: identity.application_id().to_owned(),
launch_timestamp: identity.launch_timestamp().to_owned(), launch_timestamp: identity.launch_timestamp().to_owned(),
console_enabled, console_enabled,
dropped_console_lines: crate::logging_runtime::count_to_u64(dropped.console()), dropped_console_lines: crate::count_to_u64(dropped.console()),
dropped_file_lines: crate::logging_runtime::count_to_u64(dropped.file()), dropped_file_lines: crate::count_to_u64(dropped.file()),
dropped_total_lines: crate::logging_runtime::count_to_u64(dropped.total()), dropped_total_lines: crate::count_to_u64(dropped.total()),
files, files,
}); });
} }
@@ -206,3 +197,12 @@ impl AppState {
.is_ok(); .is_ok();
} }
} }
struct LoggingRuntimeState {
guard: ksp_logging_lib::LoggingGuard,
active_profile_id: std::option::Option<String>,
selection_source: String,
generation: u32,
fallback_active: bool,
startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
}

View File

@@ -1,13 +1,19 @@
// file: crates/ksp-app-config-desk/src/bootstrap.rs // file: crates/ksp-app-config-desk/src/bootstrap.rs
// version: 3 // version: 6
//! Config and Logging bootstrap for the desktop application. //! Config and Logging bootstrap for the desktop application.
/// Crate-internal `LoggingStartup` state shared across the owning crate.
pub(crate) struct LoggingStartup { pub(crate) struct LoggingStartup {
/// Stores the guard value for this state.
pub(crate) guard: ksp_logging_lib::LoggingGuard, pub(crate) guard: ksp_logging_lib::LoggingGuard,
/// Stores the active profile id value for this state.
pub(crate) active_profile_id: std::option::Option<String>, pub(crate) active_profile_id: std::option::Option<String>,
/// Stores the selection source value for this state.
pub(crate) selection_source: String, pub(crate) selection_source: String,
/// Stores the fallback active value for this state.
pub(crate) fallback_active: bool, pub(crate) fallback_active: bool,
/// Stores the startup diagnostic value for this state.
pub(crate) startup_diagnostic: std::option::Option<crate::CommandErrorDto>, pub(crate) startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
} }
@@ -23,6 +29,7 @@ enum LoggingStartupPlan {
}, },
} }
/// Executes the crate-internal config management operation for the owning module.
pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<ksp_config_lib::ConfigManagement> { pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<ksp_config_lib::ConfigManagement> {
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_args(arguments); let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_args(arguments);
let bootstrap = match bootstrap { let bootstrap = match bootstrap {
@@ -38,10 +45,11 @@ pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_li
return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(engine)); return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(engine));
} }
/// Executes the crate-internal initialize logging operation for the owning module.
pub(crate) fn initialize_logging( pub(crate) fn initialize_logging(
management: &ksp_config_lib::ConfigManagement, management: &ksp_config_lib::ConfigManagement,
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity, runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
) -> ksp_core_lib::Result<LoggingStartup> { ) -> ksp_core_lib::Result<crate::LoggingStartup> {
let plan = resolve_logging_startup(management); let plan = resolve_logging_startup(management);
return match plan { return match plan {
LoggingStartupPlan::Managed { active_profile_id, settings } => { LoggingStartupPlan::Managed { active_profile_id, settings } => {
@@ -71,6 +79,15 @@ pub(crate) fn initialize_logging(
}; };
} }
fn fallback_logging_settings() -> ksp_logging_lib::LoggingSettings {
return ksp_logging_lib::LoggingSettings::new(
ksp_logging_lib::LogFilterLevel::Info,
ksp_logging_lib::SpanEvents::Off,
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
std::vec::Vec::new(),
);
}
fn resolve_logging_startup(management: &ksp_config_lib::ConfigManagement) -> LoggingStartupPlan { fn resolve_logging_startup(management: &ksp_config_lib::ConfigManagement) -> LoggingStartupPlan {
let environment = ksp_config_lib::ConfigEnvironment::load(); let environment = ksp_config_lib::ConfigEnvironment::load();
let environment = match environment { let environment = match environment {
@@ -93,7 +110,7 @@ fn fallback_startup_plan(initial_error: ksp_core_lib::Error) -> LoggingStartupPl
fn initialize_fallback_logging( fn initialize_fallback_logging(
initial_error: ksp_core_lib::Error, initial_error: ksp_core_lib::Error,
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity, runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
) -> ksp_core_lib::Result<LoggingStartup> { ) -> ksp_core_lib::Result<crate::LoggingStartup> {
let diagnostic = crate::CommandErrorDto::from_error(&initial_error); let diagnostic = crate::CommandErrorDto::from_error(&initial_error);
return initialize_planned_fallback_logging(initial_error, diagnostic, fallback_logging_settings(), runtime_identity); return initialize_planned_fallback_logging(initial_error, diagnostic, fallback_logging_settings(), runtime_identity);
} }
@@ -103,7 +120,7 @@ fn initialize_planned_fallback_logging(
diagnostic: crate::CommandErrorDto, diagnostic: crate::CommandErrorDto,
settings: ksp_logging_lib::LoggingSettings, settings: ksp_logging_lib::LoggingSettings,
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity, runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
) -> ksp_core_lib::Result<LoggingStartup> { ) -> ksp_core_lib::Result<crate::LoggingStartup> {
let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity); let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
let guard = match guard { let guard = match guard {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -132,15 +149,6 @@ fn initialize_planned_fallback_logging(
}); });
} }
pub(crate) fn fallback_logging_settings() -> ksp_logging_lib::LoggingSettings {
return ksp_logging_lib::LoggingSettings::new(
ksp_logging_lib::LogFilterLevel::Info,
ksp_logging_lib::SpanEvents::Off,
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
std::vec::Vec::new(),
);
}
#[cfg(test)] #[cfg(test)]
#[path = "../unit_tests/bootstrap.rs"] #[path = "../unit_tests/bootstrap.rs"]
mod tests; mod tests;

View File

@@ -1,8 +1,28 @@
// file: crates/ksp-app-config-desk/src/constants.rs // file: crates/ksp-app-config-desk/src/constants.rs
// version: 9 // version: 10
//! Application-owned tracing targets and domains. //! Application-owned tracing targets and domains.
/// Structured domain used while bootstrapping Config and Logging.
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "config.bootstrap";
/// Structured domain for Config document inventory, diagnostics and repair.
pub(crate) const TRACING_DOMAIN_DOCUMENTS: &str = "config.documents";
/// Structured domain for safe Config environment reports.
pub(crate) const TRACING_DOMAIN_ENVIRONMENT: &str = "config.environment";
/// Structured domain used by technical frontend events.
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
/// Structured domain for typed Logging editor inspection.
pub(crate) const TRACING_DOMAIN_LOGGING_EDITOR: &str = "config.logging_editor";
/// Structured domain for active Logging runtime metadata and explicit profile application.
pub(crate) const TRACING_DOMAIN_LOGGING_RUNTIME: &str = "config.logging_runtime";
/// Structured domain used by controlled Logging test events.
pub(crate) const TRACING_DOMAIN_LOGGING_TEST: &str = "config.logging_test";
/// Structured domain for Config profile selection and provenance inspection.
pub(crate) const TRACING_DOMAIN_PROFILES: &str = "config.profiles";
/// Structured domain for explicit privileged Secret reveal operations.
pub(crate) const TRACING_DOMAIN_SECRETS: &str = "config.secrets";
/// Structured domain used by Tauri window lifecycle operations.
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
/// Owning target for backend events emitted by Config Desk. /// Owning target for backend events emitted by Config Desk.
pub(crate) const TRACING_TARGET: &str = "ksp-app-config-desk"; pub(crate) const TRACING_TARGET: &str = "ksp-app-config-desk";
/// Owning target for generic frontend events emitted through the KSP bridge. /// Owning target for generic frontend events emitted through the KSP bridge.
@@ -13,23 +33,3 @@ pub(crate) const TRACING_TARGET_FRONTEND_MAIN: &str = "ksp-app-config-desk.front
pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-config-desk.frontend.splash"; pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-config-desk.frontend.splash";
/// Dedicated target used by the controlled Logging test panel. /// Dedicated target used by the controlled Logging test panel.
pub(crate) const TRACING_TARGET_LOGGING_TEST: &str = "ksp-app-config-desk.logging-test"; pub(crate) const TRACING_TARGET_LOGGING_TEST: &str = "ksp-app-config-desk.logging-test";
/// Structured domain used while bootstrapping Config and Logging.
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "config.bootstrap";
/// Structured domain used by technical frontend events.
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
/// Structured domain used by Tauri window lifecycle operations.
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
/// Structured domain for Config document inventory, diagnostics and repair.
pub(crate) const TRACING_DOMAIN_DOCUMENTS: &str = "config.documents";
/// Structured domain for Config profile selection and provenance inspection.
pub(crate) const TRACING_DOMAIN_PROFILES: &str = "config.profiles";
/// Structured domain for safe Config environment reports.
pub(crate) const TRACING_DOMAIN_ENVIRONMENT: &str = "config.environment";
/// Structured domain for explicit privileged Secret reveal operations.
pub(crate) const TRACING_DOMAIN_SECRETS: &str = "config.secrets";
/// Structured domain for typed Logging editor inspection.
pub(crate) const TRACING_DOMAIN_LOGGING_EDITOR: &str = "config.logging_editor";
/// Structured domain for active Logging runtime metadata and explicit profile application.
pub(crate) const TRACING_DOMAIN_LOGGING_RUNTIME: &str = "config.logging_runtime";
/// Structured domain used by controlled Logging test events.
pub(crate) const TRACING_DOMAIN_LOGGING_TEST: &str = "config.logging_test";

View File

@@ -1,19 +1,19 @@
// file: crates/ksp-app-config-desk/src/documents.rs // file: crates/ksp-app-config-desk/src/documents.rs
// version: 1 // version: 3
//! Generic Config document inventory, diagnostics and validated repair services. //! Generic Config document inventory, diagnostics and validated repair services.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
const STATUS_VALID: &str = "valid"; const STAGE_EFFECTIVE: &str = "effective";
const STATUS_INVALID: &str = "invalid";
const STAGE_VALID: &str = "valid";
const STAGE_READ: &str = "read";
const STAGE_JSON: &str = "json"; const STAGE_JSON: &str = "json";
const STAGE_OTHER: &str = "other";
const STAGE_READ: &str = "read";
const STAGE_SCHEMA: &str = "schema"; const STAGE_SCHEMA: &str = "schema";
const STAGE_SEMANTIC: &str = "semantic"; const STAGE_SEMANTIC: &str = "semantic";
const STAGE_EFFECTIVE: &str = "effective"; const STAGE_VALID: &str = "valid";
const STAGE_OTHER: &str = "other"; const STATUS_INVALID: &str = "invalid";
const STATUS_VALID: &str = "valid";
/// Safe inventory row for one Config document registered by `ksp-config-lib`. /// Safe inventory row for one Config document registered by `ksp-config-lib`.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Serialize, TS)]
@@ -78,7 +78,7 @@ impl ConfigDocumentErrorDto {
} }
/// Lists all Config-kind documents from the Config registry and evaluates their current backend validation state. /// Lists all Config-kind documents from the Config registry and evaluates their current backend validation state.
pub(crate) fn inventory(state: &crate::AppState) -> std::vec::Vec<ConfigDocumentSummaryDto> { pub(crate) fn document_inventory(state: &crate::AppState) -> std::vec::Vec<ConfigDocumentSummaryDto> {
let management = state.config_management(); let management = state.config_management();
let engine = management.engine(); let engine = management.engine();
let mut documents = std::vec::Vec::new(); let mut documents = std::vec::Vec::new();
@@ -98,7 +98,7 @@ pub(crate) fn inventory(state: &crate::AppState) -> std::vec::Vec<ConfigDocument
} }
/// Loads one Config document detail, including raw source when Config can read it. /// Loads one Config document detail, including raw source when Config can read it.
pub(crate) fn detail(state: &crate::AppState, file_id_text: &str) -> std::result::Result<ConfigDocumentDetailDto, ConfigDocumentErrorDto> { pub(crate) fn document_detail(state: &crate::AppState, file_id_text: &str) -> std::result::Result<ConfigDocumentDetailDto, ConfigDocumentErrorDto> {
let file_id = parse_config_file_id(file_id_text); let file_id = parse_config_file_id(file_id_text);
let file_id = match file_id { let file_id = match file_id {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -133,7 +133,7 @@ pub(crate) fn detail(state: &crate::AppState, file_id_text: &str) -> std::result
} }
/// Validates and atomically persists one raw Config source candidate, then reloads the document detail. /// Validates and atomically persists one raw Config source candidate, then reloads the document detail.
pub(crate) fn save_source( pub(crate) fn save_document_source(
state: &crate::AppState, state: &crate::AppState,
file_id_text: &str, file_id_text: &str,
source: &str, source: &str,
@@ -148,7 +148,7 @@ pub(crate) fn save_source(
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(ConfigDocumentErrorDto::from_error(&error)), std::result::Result::Err(error) => return std::result::Result::Err(ConfigDocumentErrorDto::from_error(&error)),
}; };
let document = detail(state, file_id.as_str()); let document = crate::document_detail(state, file_id.as_str());
let document = match document { let document = match document {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/dto_common.rs // file: crates/ksp-app-config-desk/src/dto_common.rs
// version: 1 // version: 2
//! Common Tauri DTOs shared by Config Desk commands. //! Common Tauri DTOs shared by Config Desk commands.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
/// Safe command error projection that never serializes arbitrary KSP error context or source values. /// Safe command error projection that never serializes arbitrary KSP error context or source values.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Serialize, TS)]

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/environment.rs // file: crates/ksp-app-config-desk/src/environment.rs
// version: 2 // version: 4
//! Safe Config environment reports and `.env` management projections for Config Desk. //! Safe Config environment reports and `.env` management projections for Config Desk.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
/// Safe desired/effective environment view exposed to the ordinary Config Desk frontend. /// Safe desired/effective environment view exposed to the ordinary Config Desk frontend.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Serialize, TS)]
@@ -63,17 +63,17 @@ impl EnvironmentMutationOperation {
} }
/// Returns the safe environment report owned by Config. /// Returns the safe environment report owned by Config.
pub(crate) fn report(state: &crate::AppState) -> ksp_core_lib::Result<std::vec::Vec<ConfigEnvironmentReportDto>> { pub(crate) fn environment_report(state: &crate::AppState) -> ksp_core_lib::Result<std::vec::Vec<ConfigEnvironmentReportDto>> {
return report_from_management(state.config_management()); return report_from_management(state.config_management());
} }
/// Creates or replaces one `.env` entry exclusively through ConfigManagement. /// Creates or replaces one `.env` entry exclusively through ConfigManagement.
pub(crate) fn set_value(state: &crate::AppState, variable_name: &str, value: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeDto> { pub(crate) fn set_environment_value(state: &crate::AppState, variable_name: &str, value: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeDto> {
return set_value_from_management(state.config_management(), variable_name, value); return set_value_from_management(state.config_management(), variable_name, value);
} }
/// Removes one `.env` entry exclusively through ConfigManagement. /// Removes one `.env` entry exclusively through ConfigManagement.
pub(crate) fn remove_value(state: &crate::AppState, variable_name: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeDto> { pub(crate) fn remove_environment_value(state: &crate::AppState, variable_name: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeDto> {
return remove_value_from_management(state.config_management(), variable_name); return remove_value_from_management(state.config_management(), variable_name);
} }

View File

@@ -1,44 +1,42 @@
// file: crates/ksp-app-config-desk/src/errors.rs // file: crates/ksp-app-config-desk/src/errors.rs
// version: 9 // version: 10
//! Application-local error codes for the configuration desktop shell. //! Application-local error codes for the configuration desktop shell.
/// Tauri runtime assembly or execution failed. /// Shared Config Desk application state is internally inconsistent.
pub(crate) const ERROR_CODE_TAURI_RUNTIME_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "tauri_runtime_failed"); pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "app_state_invalid");
/// Shared Config Desk runtime state cannot be locked safely.
pub(crate) const ERROR_CODE_APP_STATE_LOCK_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "app_state_lock_failed");
/// A Documents command requested a registered file that is not a Config-kind document.
pub(crate) const ERROR_CODE_DOCUMENT_KIND_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "document_kind_invalid");
/// Frontend logging requested an unsupported level.
pub(crate) const ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "frontend_log_level_invalid");
/// Frontend logging requested a target outside the application whitelist.
pub(crate) const ERROR_CODE_FRONTEND_LOG_TARGET_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "frontend_log_target_invalid");
/// Config Desk could not install the managed Logging runtime or its safe fallback. /// Config Desk could not install the managed Logging runtime or its safe fallback.
pub(crate) const ERROR_CODE_LOGGING_BOOTSTRAP_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "logging_bootstrap_failed"); pub(crate) const ERROR_CODE_LOGGING_BOOTSTRAP_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "logging_bootstrap_failed");
/// Config Desk persisted a Logging candidate but could not restore the previous source after runtime application failed. /// Config Desk persisted a Logging candidate but could not restore the previous source after runtime application failed.
pub(crate) const ERROR_CODE_LOGGING_SOURCE_ROLLBACK_FAILED: ksp_core_lib::ErrorCode = pub(crate) const ERROR_CODE_LOGGING_SOURCE_ROLLBACK_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("config_desk", "logging_source_rollback_failed"); ksp_core_lib::ErrorCode::new("config_desk", "logging_source_rollback_failed");
/// Shared Config Desk application state is internally inconsistent.
pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "app_state_invalid");
/// Shared Config Desk runtime state cannot be locked safely.
pub(crate) const ERROR_CODE_APP_STATE_LOCK_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "app_state_lock_failed");
/// Frontend logging requested an unsupported level.
pub(crate) const ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "frontend_log_level_invalid");
/// Frontend logging requested a target outside the application whitelist.
pub(crate) const ERROR_CODE_FRONTEND_LOG_TARGET_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "frontend_log_target_invalid");
/// Controlled Logging test request contains an unsupported or invalid selector. /// Controlled Logging test request contains an unsupported or invalid selector.
pub(crate) const ERROR_CODE_LOGGING_TEST_REQUEST_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "logging_test_request_invalid"); pub(crate) const ERROR_CODE_LOGGING_TEST_REQUEST_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "logging_test_request_invalid");
/// A validated Config document does not expose the standard profile contract expected by the Profiles panel. /// A validated Config document does not expose the standard profile contract expected by the Profiles panel.
pub(crate) const ERROR_CODE_PROFILE_CONTRACT_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "profile_contract_missing"); pub(crate) const ERROR_CODE_PROFILE_CONTRACT_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "profile_contract_missing");
/// Config profile data could not be projected safely for the frontend. /// Config profile data could not be projected safely for the frontend.
pub(crate) const ERROR_CODE_PROFILE_PROJECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "profile_projection_failed"); pub(crate) const ERROR_CODE_PROFILE_PROJECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "profile_projection_failed");
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
pub(crate) const ERROR_CODE_SPLASH_SETTING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "splash_setting_invalid");
/// Splash readiness was invoked from a window other than the splash window.
pub(crate) const ERROR_CODE_SPLASH_ORIGIN_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "splash_origin_invalid");
/// A required Tauri window is missing from the configured application runtime.
pub(crate) const ERROR_CODE_TAURI_WINDOW_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "tauri_window_missing");
/// A Tauri window show/focus/destroy/event operation failed.
pub(crate) const ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("config_desk", "tauri_window_operation_failed");
/// A Documents command requested a registered file that is not a Config-kind document.
pub(crate) const ERROR_CODE_DOCUMENT_KIND_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "document_kind_invalid");
/// Privileged reveal was requested for a non-Secret KSP/KSPB namespace. /// Privileged reveal was requested for a non-Secret KSP/KSPB namespace.
pub(crate) const ERROR_CODE_SECRET_REVEAL_REQUIRES_SECRET: ksp_core_lib::ErrorCode = pub(crate) const ERROR_CODE_SECRET_REVEAL_REQUIRES_SECRET: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("config_desk", "secret_reveal_requires_secret"); ksp_core_lib::ErrorCode::new("config_desk", "secret_reveal_requires_secret");
/// Privileged reveal requested an unsupported source selector. /// Privileged reveal requested an unsupported source selector.
pub(crate) const ERROR_CODE_SECRET_REVEAL_SOURCE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "secret_reveal_source_invalid"); pub(crate) const ERROR_CODE_SECRET_REVEAL_SOURCE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "secret_reveal_source_invalid");
/// Splash readiness was invoked from a window other than the splash window.
pub(crate) const ERROR_CODE_SPLASH_ORIGIN_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "splash_origin_invalid");
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
pub(crate) const ERROR_CODE_SPLASH_SETTING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "splash_setting_invalid");
/// Tauri runtime assembly or execution failed.
pub(crate) const ERROR_CODE_TAURI_RUNTIME_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "tauri_runtime_failed");
/// A required Tauri window is missing from the configured application runtime.
pub(crate) const ERROR_CODE_TAURI_WINDOW_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "tauri_window_missing");
/// A Tauri window show/focus/destroy/event operation failed.
pub(crate) const ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("config_desk", "tauri_window_operation_failed");

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/frontend_logging.rs // file: crates/ksp-app-config-desk/src/frontend_logging.rs
// version: 1 // version: 2
//! KSP-owned bridge for technical log events emitted by Config Desk frontend scripts. //! KSP-owned bridge for technical log events emitted by Config Desk frontend scripts.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
/// Log payload sent by Config Desk frontend scripts. /// Log payload sent by Config Desk frontend scripts.
#[derive(Clone, Debug, serde::Deserialize, TS)] #[derive(Clone, Debug, serde::Deserialize, TS)]

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/lib.rs // file: crates/ksp-app-config-desk/src/lib.rs
// version: 14 // version: 16
//! Tauri desktop application for managing and validating KSP configuration. //! Tauri desktop application for managing and validating KSP configuration.
@@ -28,61 +28,173 @@ mod tw_splash;
/// Runs the KSP configuration desktop application. /// Runs the KSP configuration desktop application.
pub use self::tauri::run; pub use self::tauri::run;
/// Shared Config Desk application state managed by Tauri.
pub(crate) use self::app_state::AppState; pub(crate) use self::app_state::AppState;
/// Crate-internal `LoggingStartup` state shared across the owning crate.
pub(crate) use self::bootstrap::LoggingStartup;
/// Executes the crate-internal config management operation for the owning module.
pub(crate) use self::bootstrap::config_management; pub(crate) use self::bootstrap::config_management;
/// Executes the crate-internal initialize logging operation for the owning module.
pub(crate) use self::bootstrap::initialize_logging; pub(crate) use self::bootstrap::initialize_logging;
/// Structured domain used while bootstrapping Config and Logging.
pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP; pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP;
/// Structured domain for Config document inventory, diagnostics and repair.
pub(crate) use self::constants::TRACING_DOMAIN_DOCUMENTS; pub(crate) use self::constants::TRACING_DOMAIN_DOCUMENTS;
/// Structured domain for safe Config environment reports.
pub(crate) use self::constants::TRACING_DOMAIN_ENVIRONMENT; pub(crate) use self::constants::TRACING_DOMAIN_ENVIRONMENT;
/// Structured domain used by technical frontend events.
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND; pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
/// Structured domain for typed Logging editor inspection.
pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_EDITOR; pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_EDITOR;
/// Structured domain for active Logging runtime metadata and explicit profile application.
pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_RUNTIME; pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_RUNTIME;
/// Structured domain used by controlled Logging test events.
pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_TEST; pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_TEST;
/// Structured domain for Config profile selection and provenance inspection.
pub(crate) use self::constants::TRACING_DOMAIN_PROFILES; pub(crate) use self::constants::TRACING_DOMAIN_PROFILES;
/// Structured domain for explicit privileged Secret reveal operations.
pub(crate) use self::constants::TRACING_DOMAIN_SECRETS; pub(crate) use self::constants::TRACING_DOMAIN_SECRETS;
/// Structured domain used by Tauri window lifecycle operations.
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS; pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
/// Owning target for backend events emitted by Config Desk.
pub(crate) use self::constants::TRACING_TARGET; pub(crate) use self::constants::TRACING_TARGET;
/// Owning target for generic frontend events emitted through the KSP bridge.
pub(crate) use self::constants::TRACING_TARGET_FRONTEND; pub(crate) use self::constants::TRACING_TARGET_FRONTEND;
/// Owning target for main-window frontend events.
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN; pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN;
/// Owning target for splash-window frontend events.
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH; pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH;
/// Dedicated target used by the controlled Logging test panel.
pub(crate) use self::constants::TRACING_TARGET_LOGGING_TEST; pub(crate) use self::constants::TRACING_TARGET_LOGGING_TEST;
/// Detailed management view for one registered Config document.
pub(crate) use self::documents::ConfigDocumentDetailDto; pub(crate) use self::documents::ConfigDocumentDetailDto;
/// Safe document-specific command error with backend-owned diagnostic classification.
pub(crate) use self::documents::ConfigDocumentErrorDto; pub(crate) use self::documents::ConfigDocumentErrorDto;
/// Result of one validated raw-source repair attempt that reached persistence successfully.
pub(crate) use self::documents::ConfigDocumentSaveResultDto; pub(crate) use self::documents::ConfigDocumentSaveResultDto;
/// Safe inventory row for one Config document registered by `ksp-config-lib`.
pub(crate) use self::documents::ConfigDocumentSummaryDto; pub(crate) use self::documents::ConfigDocumentSummaryDto;
/// Loads one Config document detail, including raw source when Config can read it.
pub(crate) use self::documents::document_detail;
/// Lists all Config-kind documents from the Config registry and evaluates their current backend validation state.
pub(crate) use self::documents::document_inventory;
/// Validates and atomically persists one raw Config source candidate, then reloads the document detail.
pub(crate) use self::documents::save_document_source;
/// Initial application/runtime snapshot exposed to the frontend.
pub(crate) use self::dto_common::AppSnapshotDto; pub(crate) use self::dto_common::AppSnapshotDto;
/// Safe command error projection that never serializes arbitrary KSP error context or source values.
pub(crate) use self::dto_common::CommandErrorDto; pub(crate) use self::dto_common::CommandErrorDto;
/// Safe result metadata for one `.env` mutation.
pub(crate) use self::environment::ConfigEnvironmentChangeDto; pub(crate) use self::environment::ConfigEnvironmentChangeDto;
/// Safe desired/effective environment view exposed to the ordinary Config Desk frontend.
pub(crate) use self::environment::ConfigEnvironmentReportDto; pub(crate) use self::environment::ConfigEnvironmentReportDto;
/// Returns the safe environment report owned by Config.
pub(crate) use self::environment::environment_report;
/// Removes one `.env` entry exclusively through ConfigManagement.
pub(crate) use self::environment::remove_environment_value;
/// Creates or replaces one `.env` entry exclusively through ConfigManagement.
pub(crate) use self::environment::set_environment_value;
/// Shared Config Desk application state is internally inconsistent.
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID; pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
/// Shared Config Desk runtime state cannot be locked safely.
pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED; pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
/// A Documents command requested a registered file that is not a Config-kind document.
pub(crate) use self::errors::ERROR_CODE_DOCUMENT_KIND_INVALID; pub(crate) use self::errors::ERROR_CODE_DOCUMENT_KIND_INVALID;
/// Frontend logging requested an unsupported level.
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID; pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID;
/// Frontend logging requested a target outside the application whitelist.
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID; pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
/// Config Desk could not install the managed Logging runtime or its safe fallback.
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED; pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
/// Config Desk persisted a Logging candidate but could not restore the previous source after runtime application failed.
pub(crate) use self::errors::ERROR_CODE_LOGGING_SOURCE_ROLLBACK_FAILED; pub(crate) use self::errors::ERROR_CODE_LOGGING_SOURCE_ROLLBACK_FAILED;
/// Controlled Logging test request contains an unsupported or invalid selector.
pub(crate) use self::errors::ERROR_CODE_LOGGING_TEST_REQUEST_INVALID; pub(crate) use self::errors::ERROR_CODE_LOGGING_TEST_REQUEST_INVALID;
/// A validated Config document does not expose the standard profile contract expected by the Profiles panel.
pub(crate) use self::errors::ERROR_CODE_PROFILE_CONTRACT_MISSING; pub(crate) use self::errors::ERROR_CODE_PROFILE_CONTRACT_MISSING;
/// Config profile data could not be projected safely for the frontend.
pub(crate) use self::errors::ERROR_CODE_PROFILE_PROJECTION_FAILED; pub(crate) use self::errors::ERROR_CODE_PROFILE_PROJECTION_FAILED;
/// Privileged reveal was requested for a non-Secret KSP/KSPB namespace.
pub(crate) use self::errors::ERROR_CODE_SECRET_REVEAL_REQUIRES_SECRET; pub(crate) use self::errors::ERROR_CODE_SECRET_REVEAL_REQUIRES_SECRET;
/// Privileged reveal requested an unsupported source selector.
pub(crate) use self::errors::ERROR_CODE_SECRET_REVEAL_SOURCE_INVALID; pub(crate) use self::errors::ERROR_CODE_SECRET_REVEAL_SOURCE_INVALID;
/// Splash readiness was invoked from a window other than the splash window.
pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID; pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID;
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
pub(crate) use self::errors::ERROR_CODE_SPLASH_SETTING_INVALID; pub(crate) use self::errors::ERROR_CODE_SPLASH_SETTING_INVALID;
/// Tauri runtime assembly or execution failed.
pub(crate) use self::errors::ERROR_CODE_TAURI_RUNTIME_FAILED; pub(crate) use self::errors::ERROR_CODE_TAURI_RUNTIME_FAILED;
/// A required Tauri window is missing from the configured application runtime.
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_MISSING; pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_MISSING;
/// A Tauri window show/focus/destroy/event operation failed.
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED; pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED;
/// Log payload sent by Config Desk frontend scripts.
pub(crate) use self::frontend_logging::FrontendLogPayloadDto; pub(crate) use self::frontend_logging::FrontendLogPayloadDto;
/// Emits one validated frontend event through the KSP Logging facade.
pub(crate) use self::frontend_logging::emit_frontend_log_event; pub(crate) use self::frontend_logging::emit_frontend_log_event;
/// Console sink configuration exposed to the editor.
pub(crate) use self::logging_editor::LoggingConsoleDto;
/// Complete editable Logging candidate accepted from the frontend.
pub(crate) use self::logging_editor::LoggingDocumentCandidateDto; pub(crate) use self::logging_editor::LoggingDocumentCandidateDto;
/// Complete typed Logging document exposed to the editor.
pub(crate) use self::logging_editor::LoggingDocumentDto; pub(crate) use self::logging_editor::LoggingDocumentDto;
/// Result of one validated typed Logging persistence operation.
pub(crate) use self::logging_editor::LoggingDocumentSaveResultDto; pub(crate) use self::logging_editor::LoggingDocumentSaveResultDto;
pub(crate) use self::logging_runtime::{LoggingRuntimeFileDto, LoggingRuntimeStatusDto}; /// One persistent file sink exposed to the editor.
pub(crate) use self::logging_test::{LoggingTestRequestDto, LoggingTestResultDto}; pub(crate) use self::logging_editor::LoggingFileDto;
/// One Logging sink selector/filter exposed to the editor.
pub(crate) use self::logging_editor::LoggingOutputFilterDto;
/// One typed Logging profile exposed to the editor.
pub(crate) use self::logging_editor::LoggingProfileDto;
/// One global Logging target override exposed to the editor.
pub(crate) use self::logging_editor::LoggingTargetFilterDto;
/// Loads the typed Logging source through ConfigManagement and projects it for the frontend.
pub(crate) use self::logging_editor::logging_document;
/// Validates, atomically persists and hot-reloads one typed Logging candidate through ConfigManagement and `ksp-logging-lib`.
pub(crate) use self::logging_editor::save_logging_document;
/// Metadata for one currently active persistent Logging file output.
pub(crate) use self::logging_runtime::LoggingRuntimeFileDto;
/// Safe observable state of the currently active KSP Logging runtime.
pub(crate) use self::logging_runtime::LoggingRuntimeStatusDto;
/// Applies one persisted Logging profile explicitly without changing `default_profile` or the source document.
pub(crate) use self::logging_runtime::apply_logging_profile;
/// Executes the crate-internal count to u64 operation for the owning module.
pub(crate) use self::logging_runtime::count_to_u64;
/// Creates the stable runtime identity for this Config Desk process launch.
pub(crate) use self::logging_runtime::launch_identity;
/// Returns safe metadata for the currently active Logging runtime.
pub(crate) use self::logging_runtime::logging_runtime_status;
/// Executes the crate-internal project logging file operation for the owning module.
pub(crate) use self::logging_runtime::project_logging_file;
/// Controlled request emitted through the backend KSP Logging facade.
pub(crate) use self::logging_test::LoggingTestRequestDto;
/// Safe result metadata for one controlled backend Logging test request.
pub(crate) use self::logging_test::LoggingTestResultDto;
/// Emits one controlled backend test request through `ksp-logging-lib` only.
pub(crate) use self::logging_test::emit_logging_test;
/// Safe detailed view of one resolved Config profile.
pub(crate) use self::profiles::ConfigProfileDetailDto; pub(crate) use self::profiles::ConfigProfileDetailDto;
/// One Config document that exposes the standard `default_profile` / `profiles` contract.
pub(crate) use self::profiles::ConfigProfileDocumentDto; pub(crate) use self::profiles::ConfigProfileDocumentDto;
/// Resolves one default or explicitly selected profile into a safe inspection DTO.
pub(crate) use self::profiles::profile_detail;
/// Lists validated Config documents that expose standard profiles.
pub(crate) use self::profiles::profile_inventory;
/// Privileged reveal request kept separate from ordinary environment-report DTOs.
pub(crate) use self::secrets::SecretRevealRequestDto; pub(crate) use self::secrets::SecretRevealRequestDto;
/// Privileged reveal response. This type deliberately does not derive `Clone` or `Debug`.
pub(crate) use self::secrets::SecretRevealResponseDto; pub(crate) use self::secrets::SecretRevealResponseDto;
/// Reveals one real Secret value only after the dedicated Tauri command has been explicitly invoked.
pub(crate) use self::secrets::reveal_secret;
/// Command emitted by Rust to the splash frontend.
pub(crate) use self::splash::SplashOrderDto; pub(crate) use self::splash::SplashOrderDto;
/// Runtime timings used by the common desk splash lifecycle.
pub(crate) use self::splash::SplashSettings; pub(crate) use self::splash::SplashSettings;
pub(crate) use self::tw_main::show_and_focus as show_main_window; /// Resolves the required main window or returns a typed error.
pub(crate) use self::tw_splash::frontend_ready as splash_frontend_ready_service; pub(crate) use self::tw_main::require_main_window;
/// Shows main window.
pub(crate) use self::tw_main::show_main_window;
/// Resolves the required splash window or returns a typed error.
pub(crate) use self::tw_splash::require_splash_window;
/// Executes the crate-internal splash frontend ready service operation for the owning module.
pub(crate) use self::tw_splash::splash_frontend_ready_service;

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/logging_editor.rs // file: crates/ksp-app-config-desk/src/logging_editor.rs
// version: 4 // version: 6
//! Typed Logging document projection and validated persistence for the Config Desk Logging editor. //! Typed Logging document projection and validated persistence for the Config Desk Logging editor.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
/// One Logging sink selector/filter exposed to the editor. /// One Logging sink selector/filter exposed to the editor.
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)]
@@ -32,7 +32,7 @@ pub(crate) struct LoggingConsoleDto {
/// Console format source text. /// Console format source text.
pub(crate) format: String, pub(crate) format: String,
/// Sink-local selectors. /// Sink-local selectors.
pub(crate) filter: LoggingOutputFilterDto, pub(crate) filter: crate::LoggingOutputFilterDto,
} }
/// One persistent file sink exposed to the editor. /// One persistent file sink exposed to the editor.
@@ -53,7 +53,7 @@ pub(crate) struct LoggingFileDto {
/// Whether ANSI output is requested. /// Whether ANSI output is requested.
pub(crate) ansi: bool, pub(crate) ansi: bool,
/// Sink-local selectors. /// Sink-local selectors.
pub(crate) filter: LoggingOutputFilterDto, pub(crate) filter: crate::LoggingOutputFilterDto,
} }
/// One global Logging target override exposed to the editor. /// One global Logging target override exposed to the editor.
@@ -79,11 +79,11 @@ pub(crate) struct LoggingProfileDto {
/// Span lifecycle source text. /// Span lifecycle source text.
pub(crate) span_events: String, pub(crate) span_events: String,
/// Console sink configuration. /// Console sink configuration.
pub(crate) console: LoggingConsoleDto, pub(crate) console: crate::LoggingConsoleDto,
/// Persistent file sinks in source order. /// Persistent file sinks in source order.
pub(crate) files: std::vec::Vec<LoggingFileDto>, pub(crate) files: std::vec::Vec<crate::LoggingFileDto>,
/// Global target overrides in source order. /// Global target overrides in source order.
pub(crate) target_filters: std::vec::Vec<LoggingTargetFilterDto>, pub(crate) target_filters: std::vec::Vec<crate::LoggingTargetFilterDto>,
} }
/// Complete typed Logging document exposed to the editor. /// Complete typed Logging document exposed to the editor.
@@ -102,7 +102,7 @@ pub(crate) struct LoggingDocumentDto {
/// Autonomous default profile identifier. /// Autonomous default profile identifier.
pub(crate) default_profile: String, pub(crate) default_profile: String,
/// Typed profiles in source order. /// Typed profiles in source order.
pub(crate) profiles: std::vec::Vec<LoggingProfileDto>, pub(crate) profiles: std::vec::Vec<crate::LoggingProfileDto>,
} }
/// Complete editable Logging candidate accepted from the frontend. /// Complete editable Logging candidate accepted from the frontend.
@@ -115,7 +115,7 @@ pub(crate) struct LoggingDocumentCandidateDto {
/// Autonomous default profile identifier. /// Autonomous default profile identifier.
pub(crate) default_profile: String, pub(crate) default_profile: String,
/// Typed profiles in source order. /// Typed profiles in source order.
pub(crate) profiles: std::vec::Vec<LoggingProfileDto>, pub(crate) profiles: std::vec::Vec<crate::LoggingProfileDto>,
} }
/// Result of one validated typed Logging persistence operation. /// Result of one validated typed Logging persistence operation.
@@ -138,12 +138,12 @@ pub(crate) struct LoggingDocumentSaveResultDto {
} }
/// Loads the typed Logging source through ConfigManagement and projects it for the frontend. /// Loads the typed Logging source through ConfigManagement and projects it for the frontend.
pub(crate) fn document(state: &crate::AppState) -> ksp_core_lib::Result<LoggingDocumentDto> { pub(crate) fn logging_document(state: &crate::AppState) -> ksp_core_lib::Result<LoggingDocumentDto> {
return document_from_management(state.config_management()); return document_from_management(state.config_management());
} }
/// Validates, atomically persists and hot-reloads one typed Logging candidate through ConfigManagement and `ksp-logging-lib`. /// Validates, atomically persists and hot-reloads one typed Logging candidate through ConfigManagement and `ksp-logging-lib`.
pub(crate) fn save(state: &crate::AppState, candidate: LoggingDocumentCandidateDto) -> ksp_core_lib::Result<LoggingDocumentSaveResultDto> { pub(crate) fn save_logging_document(state: &crate::AppState, candidate: LoggingDocumentCandidateDto) -> ksp_core_lib::Result<LoggingDocumentSaveResultDto> {
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_STD_LOGGING); let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_STD_LOGGING);
let file_id = match file_id { let file_id = match file_id {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -255,7 +255,7 @@ fn document_from_management(management: &ksp_config_lib::ConfigManagement) -> ks
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
let mut profiles = std::vec::Vec::<LoggingProfileDto>::with_capacity(source.profiles().len()); let mut profiles = std::vec::Vec::<crate::LoggingProfileDto>::with_capacity(source.profiles().len());
let mut file_count = 0usize; let mut file_count = 0usize;
let mut target_filter_count = 0usize; let mut target_filter_count = 0usize;
for profile in source.profiles() { for profile in source.profiles() {
@@ -291,7 +291,7 @@ fn config_candidate(candidate: LoggingDocumentCandidateDto) -> ksp_config_lib::L
return ksp_config_lib::LoggingConfigDocument::new(candidate.logs_directory, candidate.default_profile, profiles); return ksp_config_lib::LoggingConfigDocument::new(candidate.logs_directory, candidate.default_profile, profiles);
} }
fn config_profile(profile: LoggingProfileDto) -> ksp_config_lib::LoggingProfileConfig { fn config_profile(profile: crate::LoggingProfileDto) -> ksp_config_lib::LoggingProfileConfig {
let mut files = std::vec::Vec::<ksp_config_lib::LoggingFileConfig>::with_capacity(profile.files.len()); let mut files = std::vec::Vec::<ksp_config_lib::LoggingFileConfig>::with_capacity(profile.files.len());
for file in profile.files { for file in profile.files {
let mut config = let mut config =
@@ -319,14 +319,14 @@ fn config_profile(profile: LoggingProfileDto) -> ksp_config_lib::LoggingProfileC
); );
} }
fn config_output_filter(filter: LoggingOutputFilterDto) -> ksp_config_lib::LoggingOutputFilterConfig { fn config_output_filter(filter: crate::LoggingOutputFilterDto) -> ksp_config_lib::LoggingOutputFilterConfig {
return ksp_config_lib::LoggingOutputFilterConfig::new(filter.level, filter.targets, filter.domains); return ksp_config_lib::LoggingOutputFilterConfig::new(filter.level, filter.targets, filter.domains);
} }
fn project_profile(profile: &ksp_config_lib::LoggingProfileConfig) -> LoggingProfileDto { fn project_profile(profile: &ksp_config_lib::LoggingProfileConfig) -> crate::LoggingProfileDto {
let mut files = std::vec::Vec::<LoggingFileDto>::with_capacity(profile.files().len()); let mut files = std::vec::Vec::<crate::LoggingFileDto>::with_capacity(profile.files().len());
for file in profile.files() { for file in profile.files() {
files.push(LoggingFileDto { files.push(crate::LoggingFileDto {
output_id: file.output_id().to_owned(), output_id: file.output_id().to_owned(),
enabled: file.enabled(), enabled: file.enabled(),
path: file.path().to_owned(), path: file.path().to_owned(),
@@ -336,15 +336,15 @@ fn project_profile(profile: &ksp_config_lib::LoggingProfileConfig) -> LoggingPro
filter: project_output_filter(file.filter()), filter: project_output_filter(file.filter()),
}); });
} }
let mut target_filters = std::vec::Vec::<LoggingTargetFilterDto>::with_capacity(profile.target_filters().len()); let mut target_filters = std::vec::Vec::<crate::LoggingTargetFilterDto>::with_capacity(profile.target_filters().len());
for target_filter in profile.target_filters() { for target_filter in profile.target_filters() {
target_filters.push(LoggingTargetFilterDto { target_prefix: target_filter.target_prefix().to_owned(), level: target_filter.level().to_owned() }); target_filters.push(crate::LoggingTargetFilterDto { target_prefix: target_filter.target_prefix().to_owned(), level: target_filter.level().to_owned() });
} }
return LoggingProfileDto { return crate::LoggingProfileDto {
profile_id: profile.profile_id().to_owned(), profile_id: profile.profile_id().to_owned(),
default_filter: profile.default_filter().to_owned(), default_filter: profile.default_filter().to_owned(),
span_events: profile.span_events().to_owned(), span_events: profile.span_events().to_owned(),
console: LoggingConsoleDto { console: crate::LoggingConsoleDto {
enabled: profile.console().enabled(), enabled: profile.console().enabled(),
output: profile.console().output().to_owned(), output: profile.console().output().to_owned(),
ansi: profile.console().ansi(), ansi: profile.console().ansi(),
@@ -356,8 +356,8 @@ fn project_profile(profile: &ksp_config_lib::LoggingProfileConfig) -> LoggingPro
}; };
} }
fn project_output_filter(filter: &ksp_config_lib::LoggingOutputFilterConfig) -> LoggingOutputFilterDto { fn project_output_filter(filter: &ksp_config_lib::LoggingOutputFilterConfig) -> crate::LoggingOutputFilterDto {
return LoggingOutputFilterDto { return crate::LoggingOutputFilterDto {
level: filter.level().to_owned(), level: filter.level().to_owned(),
targets: filter.targets().to_vec(), targets: filter.targets().to_vec(),
domains: filter.domains().to_vec(), domains: filter.domains().to_vec(),

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/logging_runtime.rs // file: crates/ksp-app-config-desk/src/logging_runtime.rs
// version: 2 // version: 4
//! Runtime Logging metadata, launch identity and explicit profile application for Config Desk. //! Runtime Logging metadata, launch identity and explicit profile application for Config Desk.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
/// Metadata for one currently active persistent Logging file output. /// Metadata for one currently active persistent Logging file output.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Serialize, TS)]
@@ -56,12 +56,12 @@ pub(crate) fn launch_identity() -> ksp_core_lib::Result<ksp_logging_lib::Logging
} }
/// Returns safe metadata for the currently active Logging runtime. /// Returns safe metadata for the currently active Logging runtime.
pub(crate) fn status(state: &crate::AppState) -> ksp_core_lib::Result<LoggingRuntimeStatusDto> { pub(crate) fn logging_runtime_status(state: &crate::AppState) -> ksp_core_lib::Result<LoggingRuntimeStatusDto> {
return state.logging_runtime_status(); return state.logging_runtime_status();
} }
/// Applies one persisted Logging profile explicitly without changing `default_profile` or the source document. /// Applies one persisted Logging profile explicitly without changing `default_profile` or the source document.
pub(crate) fn apply_profile(state: &crate::AppState, profile_id: &str) -> ksp_core_lib::Result<LoggingRuntimeStatusDto> { pub(crate) fn apply_logging_profile(state: &crate::AppState, profile_id: &str) -> ksp_core_lib::Result<LoggingRuntimeStatusDto> {
let environment = ksp_config_lib::ConfigEnvironment::load(); let environment = ksp_config_lib::ConfigEnvironment::load();
let environment = match environment { let environment = match environment {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -88,7 +88,8 @@ pub(crate) fn apply_profile(state: &crate::AppState, profile_id: &str) -> ksp_co
return state.logging_runtime_status(); return state.logging_runtime_status();
} }
pub(crate) fn project_file(metadata: &ksp_logging_lib::RuntimeFileMetadata) -> LoggingRuntimeFileDto { /// Executes the crate-internal project logging file operation for the owning module.
pub(crate) fn project_logging_file(metadata: &ksp_logging_lib::RuntimeFileMetadata) -> LoggingRuntimeFileDto {
return LoggingRuntimeFileDto { return LoggingRuntimeFileDto {
output_id: metadata.output_id().to_owned(), output_id: metadata.output_id().to_owned(),
directory: metadata.directory().display().to_string(), directory: metadata.directory().display().to_string(),
@@ -97,7 +98,7 @@ pub(crate) fn project_file(metadata: &ksp_logging_lib::RuntimeFileMetadata) -> L
}; };
} }
pub(crate) const fn rotation_label(rotation: ksp_logging_lib::FileRotation) -> &'static str { const fn rotation_label(rotation: ksp_logging_lib::FileRotation) -> &'static str {
return match rotation { return match rotation {
ksp_logging_lib::FileRotation::Never => "never", ksp_logging_lib::FileRotation::Never => "never",
ksp_logging_lib::FileRotation::Hourly => "hourly", ksp_logging_lib::FileRotation::Hourly => "hourly",
@@ -105,6 +106,7 @@ pub(crate) const fn rotation_label(rotation: ksp_logging_lib::FileRotation) -> &
}; };
} }
/// Executes the crate-internal count to u64 operation for the owning module.
pub(crate) fn count_to_u64(value: usize) -> u64 { pub(crate) fn count_to_u64(value: usize) -> u64 {
let converted = u64::try_from(value); let converted = u64::try_from(value);
return match converted { return match converted {

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/logging_test.rs // file: crates/ksp-app-config-desk/src/logging_test.rs
// version: 1 // version: 2
//! Controlled Logging test events for validating KSP runtime routing from Config Desk. //! Controlled Logging test events for validating KSP runtime routing from Config Desk.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
/// Controlled request emitted through the backend KSP Logging facade. /// Controlled request emitted through the backend KSP Logging facade.
#[derive(Clone, Debug, serde::Deserialize, TS)] #[derive(Clone, Debug, serde::Deserialize, TS)]
@@ -56,7 +56,7 @@ enum LoggingTestTarget {
} }
/// Emits one controlled backend test request through `ksp-logging-lib` only. /// Emits one controlled backend test request through `ksp-logging-lib` only.
pub(crate) fn emit(state: &crate::AppState, request: LoggingTestRequestDto) -> ksp_core_lib::Result<LoggingTestResultDto> { pub(crate) fn emit_logging_test(state: &crate::AppState, request: LoggingTestRequestDto) -> ksp_core_lib::Result<LoggingTestResultDto> {
let level = parse_level(request.level.as_str()); let level = parse_level(request.level.as_str());
let level = match level { let level = match level {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/profiles.rs // file: crates/ksp-app-config-desk/src/profiles.rs
// version: 1 // version: 4
//! Safe Config profile inspection and provenance projections for Config Desk. //! Safe Config profile inspection and provenance projections for Config Desk.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
/// One Config document that exposes the standard `default_profile` / `profiles` contract. /// One Config document that exposes the standard `default_profile` / `profiles` contract.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Serialize, TS)]
@@ -77,13 +77,22 @@ pub(crate) struct ConfigProfileDetailDto {
pub(crate) environment_provenance: std::vec::Vec<ConfigProfileEnvironmentProvenanceDto>, pub(crate) environment_provenance: std::vec::Vec<ConfigProfileEnvironmentProvenanceDto>,
} }
struct ProfileContract {
default_profile: String,
profile_ids: std::vec::Vec<String>,
}
/// Lists validated Config documents that expose standard profiles. /// Lists validated Config documents that expose standard profiles.
pub(crate) fn inventory(state: &crate::AppState) -> ksp_core_lib::Result<std::vec::Vec<ConfigProfileDocumentDto>> { pub(crate) fn profile_inventory(state: &crate::AppState) -> ksp_core_lib::Result<std::vec::Vec<ConfigProfileDocumentDto>> {
return inventory_from_management(state.config_management()); return inventory_from_management(state.config_management());
} }
/// Resolves one default or explicitly selected profile into a safe inspection DTO. /// Resolves one default or explicitly selected profile into a safe inspection DTO.
pub(crate) fn detail(state: &crate::AppState, file_id: &str, requested_profile: std::option::Option<&str>) -> ksp_core_lib::Result<ConfigProfileDetailDto> { pub(crate) fn profile_detail(
state: &crate::AppState,
file_id: &str,
requested_profile: std::option::Option<&str>,
) -> ksp_core_lib::Result<ConfigProfileDetailDto> {
return detail_from_management(state.config_management(), file_id, requested_profile); return detail_from_management(state.config_management(), file_id, requested_profile);
} }
@@ -213,11 +222,6 @@ fn detail_from_management(
}); });
} }
struct ProfileContract {
default_profile: String,
profile_ids: std::vec::Vec<String>,
}
fn profile_contract(document: &ksp_config_lib::ConfigJsonDocument) -> ksp_core_lib::Result<std::option::Option<ProfileContract>> { fn profile_contract(document: &ksp_config_lib::ConfigJsonDocument) -> ksp_core_lib::Result<std::option::Option<ProfileContract>> {
let root = match document.value().as_object() { let root = match document.value().as_object() {
std::option::Option::Some(value) => value, std::option::Option::Some(value) => value,

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/secrets.rs // file: crates/ksp-app-config-desk/src/secrets.rs
// version: 1 // version: 2
//! Privileged, explicitly requested Config Secret reveal boundary for Config Desk. //! Privileged, explicitly requested Config Secret reveal boundary for Config Desk.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
/// Privileged reveal request kept separate from ordinary environment-report DTOs. /// Privileged reveal request kept separate from ordinary environment-report DTOs.
#[derive(serde::Deserialize, TS)] #[derive(serde::Deserialize, TS)]
@@ -56,7 +56,7 @@ impl SecretRevealSource {
} }
/// Reveals one real Secret value only after the dedicated Tauri command has been explicitly invoked. /// Reveals one real Secret value only after the dedicated Tauri command has been explicitly invoked.
pub(crate) fn reveal(state: &crate::AppState, request: SecretRevealRequestDto) -> ksp_core_lib::Result<SecretRevealResponseDto> { pub(crate) fn reveal_secret(state: &crate::AppState, request: SecretRevealRequestDto) -> ksp_core_lib::Result<SecretRevealResponseDto> {
return reveal_from_management(state.config_management(), request); return reveal_from_management(state.config_management(), request);
} }

View File

@@ -1,16 +1,16 @@
// file: crates/ksp-app-config-desk/src/splash.rs // file: crates/ksp-app-config-desk/src/splash.rs
// version: 3 // version: 5
//! Common splash settings and frontend event contracts for Config Desk. //! Common splash settings and frontend event contracts for Config Desk.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: trait-import
const ENV_SPLASH_MINIMUM_MS: &str = "KSP_DESK_SPLASH_MINIMUM_MS";
const ENV_SPLASH_FADE_MS: &str = "KSP_DESK_SPLASH_FADE_MS";
const DEFAULT_SPLASH_MINIMUM_MS: u64 = 1200;
const DEFAULT_SPLASH_FADE_MS: u32 = 300; const DEFAULT_SPLASH_FADE_MS: u32 = 300;
const MAX_SPLASH_MINIMUM_MS: u64 = 60_000; const DEFAULT_SPLASH_MINIMUM_MS: u64 = 1200;
const ENV_SPLASH_FADE_MS: &str = "KSP_DESK_SPLASH_FADE_MS";
const ENV_SPLASH_MINIMUM_MS: &str = "KSP_DESK_SPLASH_MINIMUM_MS";
const MAX_SPLASH_FADE_MS: u32 = 10_000; const MAX_SPLASH_FADE_MS: u32 = 10_000;
const MAX_SPLASH_MINIMUM_MS: u64 = 60_000;
/// Runtime timings used by the common desk splash lifecycle. /// Runtime timings used by the common desk splash lifecycle.
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -94,14 +94,6 @@ impl SplashSettings {
} }
} }
const fn environment_source_code(source: ksp_config_lib::ConfigEnvironmentSource) -> &'static str {
return match source {
ksp_config_lib::ConfigEnvironmentSource::Process => "process",
ksp_config_lib::ConfigEnvironmentSource::DotEnv => "dotenv",
ksp_config_lib::ConfigEnvironmentSource::Fallback => "fallback",
};
}
/// Command emitted by Rust to the splash frontend. /// Command emitted by Rust to the splash frontend.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@@ -116,12 +108,21 @@ pub(crate) struct SplashOrderDto {
} }
impl SplashOrderDto { impl SplashOrderDto {
/// Creates a new `SplashOrderDto` value.
#[must_use] #[must_use]
pub(crate) fn new(action: &str, message: std::option::Option<&str>, duration_ms: std::option::Option<u32>) -> Self { pub(crate) fn new(action: &str, message: std::option::Option<&str>, duration_ms: std::option::Option<u32>) -> Self {
return Self { action: action.to_owned(), message: message.map(std::string::ToString::to_string), duration_ms }; return Self { action: action.to_owned(), message: message.map(std::string::ToString::to_string), duration_ms };
} }
} }
const fn environment_source_code(source: ksp_config_lib::ConfigEnvironmentSource) -> &'static str {
return match source {
ksp_config_lib::ConfigEnvironmentSource::Process => "process",
ksp_config_lib::ConfigEnvironmentSource::DotEnv => "dotenv",
ksp_config_lib::ConfigEnvironmentSource::Fallback => "fallback",
};
}
fn parse_u64_setting(variable_name: &str, value: &str, maximum: u64) -> ksp_core_lib::Result<u64> { fn parse_u64_setting(variable_name: &str, value: &str, maximum: u64) -> ksp_core_lib::Result<u64> {
let parsed = value.parse::<u64>(); let parsed = value.parse::<u64>();
let parsed = match parsed { let parsed = match parsed {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/tauri.rs // file: crates/ksp-app-config-desk/src/tauri.rs
// version: 15 // version: 16
//! Tauri runtime assembly for the KSP configuration desktop application. //! Tauri runtime assembly for the KSP configuration desktop application.
@@ -60,11 +60,11 @@ fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tau
fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> { fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
return builder.setup(|app| { return builder.setup(|app| {
let splash = crate::tw_splash::require_window(app); let splash = crate::require_splash_window(app);
if let std::result::Result::Err(error) = splash { if let std::result::Result::Err(error) = splash {
return std::result::Result::Err(std::boxed::Box::new(error)); return std::result::Result::Err(std::boxed::Box::new(error));
} }
let main = crate::tw_main::require_window(app); let main = crate::require_main_window(app);
if let std::result::Result::Err(error) = main { if let std::result::Result::Err(error) = main {
return std::result::Result::Err(std::boxed::Box::new(error)); return std::result::Result::Err(std::boxed::Box::new(error));
} }
@@ -83,7 +83,7 @@ fn get_app_snapshot(state: tauri::State<'_, crate::AppState>) -> std::result::Re
#[tauri::command] #[tauri::command]
fn get_config_documents(state: tauri::State<'_, crate::AppState>) -> std::vec::Vec<crate::ConfigDocumentSummaryDto> { fn get_config_documents(state: tauri::State<'_, crate::AppState>) -> std::vec::Vec<crate::ConfigDocumentSummaryDto> {
return crate::documents::inventory(&state); return crate::document_inventory(&state);
} }
#[tauri::command] #[tauri::command]
@@ -91,7 +91,7 @@ fn get_config_document_detail(
file_id: String, file_id: String,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::ConfigDocumentDetailDto, crate::ConfigDocumentErrorDto> { ) -> std::result::Result<crate::ConfigDocumentDetailDto, crate::ConfigDocumentErrorDto> {
return crate::documents::detail(&state, file_id.as_str()); return crate::document_detail(&state, file_id.as_str());
} }
#[tauri::command] #[tauri::command]
@@ -100,14 +100,14 @@ fn save_config_document_source(
source: String, source: String,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::ConfigDocumentSaveResultDto, crate::ConfigDocumentErrorDto> { ) -> std::result::Result<crate::ConfigDocumentSaveResultDto, crate::ConfigDocumentErrorDto> {
return crate::documents::save_source(&state, file_id.as_str(), source.as_str()); return crate::save_document_source(&state, file_id.as_str(), source.as_str());
} }
#[tauri::command] #[tauri::command]
fn get_config_profile_documents( fn get_config_profile_documents(
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<std::vec::Vec<crate::ConfigProfileDocumentDto>, crate::CommandErrorDto> { ) -> std::result::Result<std::vec::Vec<crate::ConfigProfileDocumentDto>, crate::CommandErrorDto> {
let result = crate::profiles::inventory(&state); let result = crate::profile_inventory(&state);
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -120,7 +120,7 @@ fn get_config_profile_detail(
profile_id: std::option::Option<String>, profile_id: std::option::Option<String>,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::ConfigProfileDetailDto, crate::CommandErrorDto> { ) -> std::result::Result<crate::ConfigProfileDetailDto, crate::CommandErrorDto> {
let result = crate::profiles::detail(&state, file_id.as_str(), profile_id.as_deref()); let result = crate::profile_detail(&state, file_id.as_str(), profile_id.as_deref());
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -131,7 +131,7 @@ fn get_config_profile_detail(
fn get_environment_report( fn get_environment_report(
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<std::vec::Vec<crate::ConfigEnvironmentReportDto>, crate::CommandErrorDto> { ) -> std::result::Result<std::vec::Vec<crate::ConfigEnvironmentReportDto>, crate::CommandErrorDto> {
let result = crate::environment::report(&state); let result = crate::environment_report(&state);
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -144,7 +144,7 @@ fn set_environment_value(
value: String, value: String,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::ConfigEnvironmentChangeDto, crate::CommandErrorDto> { ) -> std::result::Result<crate::ConfigEnvironmentChangeDto, crate::CommandErrorDto> {
let result = crate::environment::set_value(&state, variable_name.as_str(), value.as_str()); let result = crate::set_environment_value(&state, variable_name.as_str(), value.as_str());
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -156,7 +156,7 @@ fn remove_environment_value(
variable_name: String, variable_name: String,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::ConfigEnvironmentChangeDto, crate::CommandErrorDto> { ) -> std::result::Result<crate::ConfigEnvironmentChangeDto, crate::CommandErrorDto> {
let result = crate::environment::remove_value(&state, variable_name.as_str()); let result = crate::remove_environment_value(&state, variable_name.as_str());
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -168,7 +168,7 @@ fn reveal_environment_value(
request: crate::SecretRevealRequestDto, request: crate::SecretRevealRequestDto,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::SecretRevealResponseDto, crate::CommandErrorDto> { ) -> std::result::Result<crate::SecretRevealResponseDto, crate::CommandErrorDto> {
let result = crate::secrets::reveal(&state, request); let result = crate::reveal_secret(&state, request);
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -177,7 +177,7 @@ fn reveal_environment_value(
#[tauri::command] #[tauri::command]
fn get_logging_document(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::LoggingDocumentDto, crate::CommandErrorDto> { fn get_logging_document(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::LoggingDocumentDto, crate::CommandErrorDto> {
let result = crate::logging_editor::document(&state); let result = crate::logging_document(&state);
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -186,7 +186,7 @@ fn get_logging_document(state: tauri::State<'_, crate::AppState>) -> std::result
#[tauri::command] #[tauri::command]
fn get_logging_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::LoggingRuntimeStatusDto, crate::CommandErrorDto> { fn get_logging_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::LoggingRuntimeStatusDto, crate::CommandErrorDto> {
let result = crate::logging_runtime::status(&state); let result = crate::logging_runtime_status(&state);
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -198,7 +198,7 @@ fn apply_logging_profile(
profile_id: String, profile_id: String,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::LoggingRuntimeStatusDto, crate::CommandErrorDto> { ) -> std::result::Result<crate::LoggingRuntimeStatusDto, crate::CommandErrorDto> {
let result = crate::logging_runtime::apply_profile(&state, profile_id.as_str()); let result = crate::apply_logging_profile(&state, profile_id.as_str());
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -210,7 +210,7 @@ fn save_logging_document(
candidate: crate::LoggingDocumentCandidateDto, candidate: crate::LoggingDocumentCandidateDto,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::LoggingDocumentSaveResultDto, crate::CommandErrorDto> { ) -> std::result::Result<crate::LoggingDocumentSaveResultDto, crate::CommandErrorDto> {
let result = crate::logging_editor::save(&state, candidate); let result = crate::save_logging_document(&state, candidate);
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -222,7 +222,7 @@ fn emit_logging_test(
request: crate::LoggingTestRequestDto, request: crate::LoggingTestRequestDto,
state: tauri::State<'_, crate::AppState>, state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::LoggingTestResultDto, crate::CommandErrorDto> { ) -> std::result::Result<crate::LoggingTestResultDto, crate::CommandErrorDto> {
let result = crate::logging_test::emit(&state, request); let result = crate::emit_logging_test(&state, request);
return match result { return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),

View File

@@ -1,13 +1,15 @@
// file: crates/ksp-app-config-desk/src/tw_main.rs // file: crates/ksp-app-config-desk/src/tw_main.rs
// version: 1 // version: 4
//! Tauri-window helpers for the Config Desk main window. //! Tauri-window helpers for the Config Desk main window.
use tauri::Manager; // rust-rules: trait-import use tauri::Manager; // rust-rules: trait-import
/// Crate-internal `WINDOW_LABEL_MAIN` constant.
pub(crate) const WINDOW_LABEL_MAIN: &str = "main"; pub(crate) const WINDOW_LABEL_MAIN: &str = "main";
pub(crate) fn require_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> { /// Resolves the required main window or returns a typed error.
pub(crate) fn require_main_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
let window = manager.get_webview_window(WINDOW_LABEL_MAIN); let window = manager.get_webview_window(WINDOW_LABEL_MAIN);
return match window { return match window {
std::option::Option::Some(value) => std::result::Result::Ok(value), std::option::Option::Some(value) => std::result::Result::Ok(value),
@@ -18,8 +20,9 @@ pub(crate) fn require_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib
}; };
} }
pub(crate) fn show_and_focus(app: &tauri::AppHandle) -> ksp_core_lib::Result<()> { /// Shows main window.
let window = require_window(app); pub(crate) fn show_main_window(app: &tauri::AppHandle) -> ksp_core_lib::Result<()> {
let window = crate::require_main_window(app);
let window = match window { let window = match window {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),

View File

@@ -1,15 +1,18 @@
// file: crates/ksp-app-config-desk/src/tw_splash.rs // file: crates/ksp-app-config-desk/src/tw_splash.rs
// version: 2 // version: 5
//! Tauri-window lifecycle for the Config Desk splash window. //! Tauri-window lifecycle for the Config Desk splash window.
use tauri::Emitter; // rust-rules: trait-import use tauri::Emitter; // rust-rules: trait-import
use tauri::Manager; // rust-rules: trait-import use tauri::Manager; // rust-rules: trait-import
/// Crate-internal `WINDOW_LABEL_SPLASH` constant.
pub(crate) const WINDOW_LABEL_SPLASH: &str = "splash"; pub(crate) const WINDOW_LABEL_SPLASH: &str = "splash";
const SPLASH_EVENT_NAME: &str = "ksp-splash-order"; const SPLASH_EVENT_NAME: &str = "ksp-splash-order";
pub(crate) fn require_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> { /// Resolves the required splash window or returns a typed error.
pub(crate) fn require_splash_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
let window = manager.get_webview_window(WINDOW_LABEL_SPLASH); let window = manager.get_webview_window(WINDOW_LABEL_SPLASH);
return match window { return match window {
std::option::Option::Some(value) => std::result::Result::Ok(value), std::option::Option::Some(value) => std::result::Result::Ok(value),
@@ -20,7 +23,12 @@ pub(crate) fn require_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib
}; };
} }
pub(crate) async fn frontend_ready(app: tauri::AppHandle, invoking_window: tauri::WebviewWindow, state: &crate::AppState) -> ksp_core_lib::Result<()> { /// Executes the crate-internal splash frontend ready service operation for the owning module.
pub(crate) async fn splash_frontend_ready_service(
app: tauri::AppHandle,
invoking_window: tauri::WebviewWindow,
state: &crate::AppState,
) -> ksp_core_lib::Result<()> {
if invoking_window.label() != WINDOW_LABEL_SPLASH { if invoking_window.label() != WINDOW_LABEL_SPLASH {
return std::result::Result::Err( return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_ORIGIN_INVALID, "Splash readiness may only originate from the splash window") ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_ORIGIN_INVALID, "Splash readiness may only originate from the splash window")

View File

@@ -1,31 +1,29 @@
// file: crates/ksp-app-config-desk/unit_tests/documents.rs // file: crates/ksp-app-config-desk/unit_tests/documents.rs
// version: 1 // version: 2
use super::*;
#[test] #[test]
fn diagnostic_classifier_distinguishes_json_schema_semantic_and_effective_errors() { fn diagnostic_classifier_distinguishes_json_schema_semantic_and_effective_errors() {
let json = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_JSON_SYNTAX_INVALID, "json"); let json = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_JSON_SYNTAX_INVALID, "json");
assert_eq!(classify_error(&json), STAGE_JSON); assert_eq!(super::classify_error(&json), super::STAGE_JSON);
let schema = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_SCHEMA_VALIDATION_FAILED, "schema"); let schema = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_SCHEMA_VALIDATION_FAILED, "schema");
assert_eq!(classify_error(&schema), STAGE_SCHEMA); assert_eq!(super::classify_error(&schema), super::STAGE_SCHEMA);
let semantic = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID, "semantic"); let semantic = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID, "semantic");
assert_eq!(classify_error(&semantic), STAGE_SEMANTIC); assert_eq!(super::classify_error(&semantic), super::STAGE_SEMANTIC);
let effective = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_EFFECTIVE_CONFIG_INVALID, "effective"); let effective = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_EFFECTIVE_CONFIG_INVALID, "effective");
assert_eq!(classify_error(&effective), STAGE_EFFECTIVE); assert_eq!(super::classify_error(&effective), super::STAGE_EFFECTIVE);
} }
#[test] #[test]
fn schema_source_read_errors_are_classified_as_schema_failures() { fn schema_source_read_errors_are_classified_as_schema_failures() {
let error = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_JSON_FILE_READ_FAILED, "missing schema").with_context("file_id", "schema.std.logging"); let error = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_JSON_FILE_READ_FAILED, "missing schema").with_context("file_id", "schema.std.logging");
assert_eq!(classify_error(&error), STAGE_SCHEMA); assert_eq!(super::classify_error(&error), super::STAGE_SCHEMA);
} }
#[test] #[test]
fn document_error_projection_does_not_expose_arbitrary_error_context() { fn document_error_projection_does_not_expose_arbitrary_error_context() {
let error = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_JSON_SYNTAX_INVALID, "invalid source").with_context("detail", "sensitive-canary"); let error = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_JSON_SYNTAX_INVALID, "invalid source").with_context("detail", "sensitive-canary");
let projection = ConfigDocumentErrorDto::from_error(&error); let projection = crate::ConfigDocumentErrorDto::from_error(&error);
assert_eq!(projection.diagnostic_stage, STAGE_JSON); assert_eq!(projection.diagnostic_stage, super::STAGE_JSON);
assert_eq!(projection.error.domain, "config"); assert_eq!(projection.error.domain, "config");
assert_eq!(projection.error.code, "json_syntax_invalid"); assert_eq!(projection.error.code, "json_syntax_invalid");
assert_eq!(projection.error.message, "invalid source"); assert_eq!(projection.error.message, "invalid source");

View File

@@ -1,10 +1,10 @@
// file: crates/ksp-app-config-desk/unit_tests/dto_common.rs // file: crates/ksp-app-config-desk/unit_tests/dto_common.rs
// version: 1 // version: 2
#[test] #[test]
fn command_error_projection_excludes_context_values() { fn command_error_projection_excludes_context_values() {
let error = ksp_core_lib::Error::new(ksp_core_lib::ErrorCode::new("test", "failure"), "safe message").with_context("secret_canary", "must-not-be-exported"); let error = ksp_core_lib::Error::new(ksp_core_lib::ErrorCode::new("test", "failure"), "safe message").with_context("secret_canary", "must-not-be-exported");
let dto = super::CommandErrorDto::from_error(&error); let dto = crate::CommandErrorDto::from_error(&error);
assert_eq!(dto.domain, "test"); assert_eq!(dto.domain, "test");
assert_eq!(dto.code, "failure"); assert_eq!(dto.code, "failure");
assert_eq!(dto.message, "safe message"); assert_eq!(dto.message, "safe message");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/unit_tests/environment.rs // file: crates/ksp-app-config-desk/unit_tests/environment.rs
// version: 2 // version: 4
#[test] #[test]
fn namespace_projection_distinguishes_ksp_and_kspb_sensitivity_families() { fn namespace_projection_distinguishes_ksp_and_kspb_sensitivity_families() {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/unit_tests/logging_editor.rs // file: crates/ksp-app-config-desk/unit_tests/logging_editor.rs
// version: 4 // version: 7
#[test] #[test]
fn committed_logging_document_maps_complete_read_only_editor_contract() { fn committed_logging_document_maps_complete_read_only_editor_contract() {
@@ -66,7 +66,7 @@ fn editor_candidate_round_trips_through_typed_config_contract() {
let document = super::document_from_management(&management); let document = super::document_from_management(&management);
assert!(document.is_ok(), "Logging editor document should load: {document:?}"); assert!(document.is_ok(), "Logging editor document should load: {document:?}");
if let std::result::Result::Ok(document) = document { if let std::result::Result::Ok(document) = document {
let candidate = super::LoggingDocumentCandidateDto { let candidate = crate::LoggingDocumentCandidateDto {
logs_directory: document.logs_directory.clone(), logs_directory: document.logs_directory.clone(),
default_profile: document.default_profile.clone(), default_profile: document.default_profile.clone(),
profiles: document.profiles.clone(), profiles: document.profiles.clone(),
@@ -89,42 +89,42 @@ fn editor_candidate_round_trips_through_typed_config_contract() {
#[test] #[test]
fn profile_candidate_preserves_multi_file_and_target_filter_shape() { fn profile_candidate_preserves_multi_file_and_target_filter_shape() {
let profile = super::LoggingProfileDto { let profile = crate::LoggingProfileDto {
profile_id: "clone_test".to_owned(), profile_id: "clone_test".to_owned(),
default_filter: "info".to_owned(), default_filter: "info".to_owned(),
span_events: "full".to_owned(), span_events: "full".to_owned(),
console: super::LoggingConsoleDto { console: crate::LoggingConsoleDto {
enabled: true, enabled: true,
output: "stdout".to_owned(), output: "stdout".to_owned(),
ansi: false, ansi: false,
format: "human".to_owned(), format: "human".to_owned(),
filter: super::LoggingOutputFilterDto { level: "info".to_owned(), targets: std::vec!["*".to_owned()], domains: std::vec!["*".to_owned()] }, filter: crate::LoggingOutputFilterDto { level: "info".to_owned(), targets: std::vec!["*".to_owned()], domains: std::vec!["*".to_owned()] },
}, },
files: std::vec![ files: std::vec![
super::LoggingFileDto { crate::LoggingFileDto {
output_id: "file.one".to_owned(), output_id: "file.one".to_owned(),
enabled: true, enabled: true,
path: "one.log".to_owned(), path: "one.log".to_owned(),
rotation: "daily".to_owned(), rotation: "daily".to_owned(),
format: "human".to_owned(), format: "human".to_owned(),
ansi: false, ansi: false,
filter: super::LoggingOutputFilterDto { level: "debug".to_owned(), targets: std::vec!["*".to_owned()], domains: std::vec!["*".to_owned()] }, filter: crate::LoggingOutputFilterDto { level: "debug".to_owned(), targets: std::vec!["*".to_owned()], domains: std::vec!["*".to_owned()] },
}, },
super::LoggingFileDto { crate::LoggingFileDto {
output_id: "file.two".to_owned(), output_id: "file.two".to_owned(),
enabled: false, enabled: false,
path: "two.jsonl".to_owned(), path: "two.jsonl".to_owned(),
rotation: "hourly".to_owned(), rotation: "hourly".to_owned(),
format: "json".to_owned(), format: "json".to_owned(),
ansi: false, ansi: false,
filter: super::LoggingOutputFilterDto { filter: crate::LoggingOutputFilterDto {
level: "error".to_owned(), level: "error".to_owned(),
targets: std::vec!["ksp-config-lib".to_owned()], targets: std::vec!["ksp-config-lib".to_owned()],
domains: std::vec!["config".to_owned()], domains: std::vec!["config".to_owned()],
}, },
}, },
], ],
target_filters: std::vec![super::LoggingTargetFilterDto { target_prefix: "ksp-app-config-desk".to_owned(), level: "trace".to_owned() }], target_filters: std::vec![crate::LoggingTargetFilterDto { target_prefix: "ksp-app-config-desk".to_owned(), level: "trace".to_owned() }],
}; };
let config = super::config_profile(profile); let config = super::config_profile(profile);
assert_eq!(config.profile_id(), "clone_test"); assert_eq!(config.profile_id(), "clone_test");

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/unit_tests/logging_runtime.rs // file: crates/ksp-app-config-desk/unit_tests/logging_runtime.rs
// version: 1 // version: 2
#[test] #[test]
fn launch_identity_is_safe_and_contains_application_and_process_identity() { fn launch_identity_is_safe_and_contains_application_and_process_identity() {
let identity = super::launch_identity(); let identity = crate::launch_identity();
assert!(identity.is_ok()); assert!(identity.is_ok());
if let std::result::Result::Ok(identity) = identity { if let std::result::Result::Ok(identity) = identity {
assert_eq!(identity.application_id(), crate::TRACING_TARGET); assert_eq!(identity.application_id(), crate::TRACING_TARGET);
@@ -21,6 +21,6 @@ fn runtime_rotation_labels_are_stable() {
#[test] #[test]
fn runtime_count_projection_is_non_lossy_for_normal_values() { fn runtime_count_projection_is_non_lossy_for_normal_values() {
assert_eq!(super::count_to_u64(0), 0); assert_eq!(crate::count_to_u64(0), 0);
assert_eq!(super::count_to_u64(42), 42); assert_eq!(crate::count_to_u64(42), 42);
} }

View File

@@ -1,25 +1,23 @@
// file: crates/ksp-app-config-desk/unit_tests/logging_test.rs // file: crates/ksp-app-config-desk/unit_tests/logging_test.rs
// version: 1 // version: 2
use super::{LoggingTestLevel, LoggingTestTarget};
#[test] #[test]
fn controlled_logging_test_levels_include_all_five_levels_and_batch_mode() { fn controlled_logging_test_levels_include_all_five_levels_and_batch_mode() {
assert!(matches!(super::parse_level("trace"), std::result::Result::Ok(LoggingTestLevel::Trace))); assert!(matches!(super::parse_level("trace"), std::result::Result::Ok(super::LoggingTestLevel::Trace)));
assert!(matches!(super::parse_level("debug"), std::result::Result::Ok(LoggingTestLevel::Debug))); assert!(matches!(super::parse_level("debug"), std::result::Result::Ok(super::LoggingTestLevel::Debug)));
assert!(matches!(super::parse_level("info"), std::result::Result::Ok(LoggingTestLevel::Info))); assert!(matches!(super::parse_level("info"), std::result::Result::Ok(super::LoggingTestLevel::Info)));
assert!(matches!(super::parse_level("warn"), std::result::Result::Ok(LoggingTestLevel::Warn))); assert!(matches!(super::parse_level("warn"), std::result::Result::Ok(super::LoggingTestLevel::Warn)));
assert!(matches!(super::parse_level("error"), std::result::Result::Ok(LoggingTestLevel::Error))); assert!(matches!(super::parse_level("error"), std::result::Result::Ok(super::LoggingTestLevel::Error)));
assert!(matches!(super::parse_level("all"), std::result::Result::Ok(LoggingTestLevel::All))); assert!(matches!(super::parse_level("all"), std::result::Result::Ok(super::LoggingTestLevel::All)));
assert!(super::parse_level("fatal").is_err()); assert!(super::parse_level("fatal").is_err());
} }
#[test] #[test]
fn controlled_logging_test_targets_are_static_ksp_targets() { fn controlled_logging_test_targets_are_static_ksp_targets() {
assert!(matches!(super::parse_target("app"), std::result::Result::Ok(LoggingTestTarget::App))); assert!(matches!(super::parse_target("app"), std::result::Result::Ok(super::LoggingTestTarget::App)));
assert!(matches!(super::parse_target("logging_test"), std::result::Result::Ok(LoggingTestTarget::Dedicated))); assert!(matches!(super::parse_target("logging_test"), std::result::Result::Ok(super::LoggingTestTarget::Dedicated)));
assert_eq!(super::target_value(LoggingTestTarget::App), crate::TRACING_TARGET); assert_eq!(super::target_value(super::LoggingTestTarget::App), crate::TRACING_TARGET);
assert_eq!(super::target_value(LoggingTestTarget::Dedicated), crate::TRACING_TARGET_LOGGING_TEST); assert_eq!(super::target_value(super::LoggingTestTarget::Dedicated), crate::TRACING_TARGET_LOGGING_TEST);
assert!(super::parse_target("external").is_err()); assert!(super::parse_target("external").is_err());
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/unit_tests/profiles.rs // file: crates/ksp-app-config-desk/unit_tests/profiles.rs
// version: 3 // version: 5
#[test] #[test]
fn profile_inventory_exposes_registered_standard_profile_documents() { fn profile_inventory_exposes_registered_standard_profile_documents() {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/unit_tests/secrets.rs // file: crates/ksp-app-config-desk/unit_tests/secrets.rs
// version: 1 // version: 2
#[test] #[test]
fn reveal_policy_accepts_only_secret_names_and_supported_sources() { fn reveal_policy_accepts_only_secret_names_and_supported_sources() {
@@ -21,7 +21,7 @@ fn reveal_policy_accepts_only_secret_names_and_supported_sources() {
#[test] #[test]
fn reveal_response_type_does_not_require_debug_or_clone_contracts() { fn reveal_response_type_does_not_require_debug_or_clone_contracts() {
let response = super::SecretRevealResponseDto { let response = crate::SecretRevealResponseDto {
variable_name: "KSP_SECRET_TEST_VALUE".to_owned(), variable_name: "KSP_SECRET_TEST_VALUE".to_owned(),
source: "dotenv".to_owned(), source: "dotenv".to_owned(),
value: std::option::Option::Some("secret-canary".to_owned()), value: std::option::Option::Some("secret-canary".to_owned()),

View File

@@ -1,14 +1,14 @@
// file: crates/ksp-config-lib/src/bootstrap.rs // file: crates/ksp-config-lib/src/bootstrap.rs
// version: 1 // version: 2
/// Default root containing KSP runtime configuration documents.
pub const DEFAULT_CFG_PATH: &str = "config";
/// Default root containing KSP JSON schemas.
pub const DEFAULT_SCHEMA_PATH: &str = "config/schemas";
/// Bootstrap argument used to replace the configuration document root. /// Bootstrap argument used to replace the configuration document root.
pub const ARG_CFG_PATH: &str = "--cfgpath"; pub const ARG_CFG_PATH: &str = "--cfgpath";
/// Bootstrap argument used to replace the schema root. /// Bootstrap argument used to replace the schema root.
pub const ARG_SCHEMA_PATH: &str = "--schemapath"; pub const ARG_SCHEMA_PATH: &str = "--schemapath";
/// Default root containing KSP runtime configuration documents.
pub const DEFAULT_CFG_PATH: &str = "config";
/// Default root containing KSP JSON schemas.
pub const DEFAULT_SCHEMA_PATH: &str = "config/schemas";
/// Non-recursive bootstrap options required before Config can resolve any managed document. /// Non-recursive bootstrap options required before Config can resolve any managed document.
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/composite.rs // file: crates/ksp-config-lib/src/composite.rs
// version: 1 // version: 2
/// One resolved document component selected by a composite profile. /// One resolved document component selected by a composite profile.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
@@ -118,6 +118,7 @@ struct CompositeDocumentReferenceSource {
profile_id: std::option::Option<String>, profile_id: std::option::Option<String>,
} }
/// Validates composite document contract.
pub(crate) fn validate_composite_document_contract(engine: &crate::ConfigDocumentEngine, document: &crate::ConfigJsonDocument) -> ksp_core_lib::Result<()> { pub(crate) fn validate_composite_document_contract(engine: &crate::ConfigDocumentEngine, document: &crate::ConfigJsonDocument) -> ksp_core_lib::Result<()> {
if !document.file_id().as_str().starts_with("cfg.composite.") { if !document.file_id().as_str().starts_with("cfg.composite.") {
return std::result::Result::Ok(()); return std::result::Result::Ok(());
@@ -198,12 +199,8 @@ fn resolve_composite_document(
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
let resolved = crate::profile::load_resolved_profile_with_source( let resolved =
engine, crate::load_resolved_profile_with_source(engine, &file_id, reference.profile_id.as_deref(), crate::ConfigProfileSelectionSource::Composite);
&file_id,
reference.profile_id.as_deref(),
crate::ConfigProfileSelectionSource::Composite,
);
let resolved = match resolved { let resolved = match resolved {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -262,8 +259,7 @@ fn validate_reference(
"referenced file_id is not a Config document", "referenced file_id is not a Config document",
)); ));
} }
let resolved = let resolved = crate::load_resolved_profile_with_source(engine, &file_id, reference.profile_id.as_deref(), crate::ConfigProfileSelectionSource::Composite);
crate::profile::load_resolved_profile_with_source(engine, &file_id, reference.profile_id.as_deref(), crate::ConfigProfileSelectionSource::Composite);
if let std::result::Result::Err(error) = resolved { if let std::result::Result::Err(error) = resolved {
return std::result::Result::Err(error); return std::result::Result::Err(error);
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/document.rs // file: crates/ksp-config-lib/src/document.rs
// version: 5 // version: 6
/// A Config-managed JSON document that has passed syntax, schema and current semantic validation. /// A Config-managed JSON document that has passed syntax, schema and current semantic validation.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
@@ -85,6 +85,7 @@ impl ConfigDocumentEngine {
return self.validate_document(document, &schema_file_id); return self.validate_document(document, &schema_file_id);
} }
/// Validates source candidate.
pub(crate) fn validate_source_candidate(&self, file_id: &crate::ConfigFileId, source: &str) -> ksp_core_lib::Result<ConfigJsonDocument> { pub(crate) fn validate_source_candidate(&self, file_id: &crate::ConfigFileId, source: &str) -> ksp_core_lib::Result<ConfigJsonDocument> {
let path = self.registry.resolve_path(&self.bootstrap, file_id); let path = self.registry.resolve_path(&self.bootstrap, file_id);
let path = match path { let path = match path {
@@ -99,6 +100,7 @@ impl ConfigDocumentEngine {
return self.validate_candidate(file_id, value); return self.validate_candidate(file_id, value);
} }
/// Validates candidate.
pub(crate) fn validate_candidate(&self, file_id: &crate::ConfigFileId, value: serde_json::Value) -> ksp_core_lib::Result<ConfigJsonDocument> { pub(crate) fn validate_candidate(&self, file_id: &crate::ConfigFileId, value: serde_json::Value) -> ksp_core_lib::Result<ConfigJsonDocument> {
let descriptor = self.registry.descriptor(file_id); let descriptor = self.registry.descriptor(file_id);
let descriptor = match descriptor { let descriptor = match descriptor {
@@ -256,11 +258,11 @@ fn validate_instance(document: &ConfigJsonDocument, schema: &ConfigJsonDocument)
} }
fn validate_document_semantics(engine: &ConfigDocumentEngine, document: &ConfigJsonDocument) -> ksp_core_lib::Result<()> { fn validate_document_semantics(engine: &ConfigDocumentEngine, document: &ConfigJsonDocument) -> ksp_core_lib::Result<()> {
let profile_validation = crate::profile::validate_document_profile_contract(document); let profile_validation = crate::validate_document_profile_contract(document);
if let std::result::Result::Err(error) = profile_validation { if let std::result::Result::Err(error) = profile_validation {
return std::result::Result::Err(error); return std::result::Result::Err(error);
} }
let composite_validation = crate::composite::validate_composite_document_contract(engine, document); let composite_validation = crate::validate_composite_document_contract(engine, document);
if let std::result::Result::Err(error) = composite_validation { if let std::result::Result::Err(error) = composite_validation {
return std::result::Result::Err(error); return std::result::Result::Err(error);
} }

View File

@@ -1,11 +1,10 @@
// file: crates/ksp-config-lib/src/environment.rs // file: crates/ksp-config-lib/src/environment.rs
// version: 5 // version: 8
/// Default local environment file read by Config from the process launch directory.
pub const DEFAULT_DOTENV_PATH: &str = ".env";
/// Versioned environment contract template expected at the repository/runtime root. /// Versioned environment contract template expected at the repository/runtime root.
pub const DEFAULT_DOTENV_EXAMPLE_PATH: &str = ".env.example"; pub const DEFAULT_DOTENV_EXAMPLE_PATH: &str = ".env.example";
/// Default local environment file read by Config from the process launch directory.
pub const DEFAULT_DOTENV_PATH: &str = ".env";
const LOGGING_DOMAIN: &str = "config.environment"; const LOGGING_DOMAIN: &str = "config.environment";
@@ -230,6 +229,7 @@ impl ConfigEnvironment {
}; };
} }
/// Loads from dotenv path.
pub(crate) fn load_from_dotenv_path(dotenv_path: &std::path::Path) -> ksp_core_lib::Result<Self> { pub(crate) fn load_from_dotenv_path(dotenv_path: &std::path::Path) -> ksp_core_lib::Result<Self> {
let process = collect_process_environment(std::env::vars_os()); let process = collect_process_environment(std::env::vars_os());
let process = match process { let process = match process {
@@ -244,57 +244,24 @@ impl ConfigEnvironment {
return std::result::Result::Ok(Self { process, dotenv, dotenv_path: dotenv_path.to_path_buf() }); return std::result::Result::Ok(Self { process, dotenv, dotenv_path: dotenv_path.to_path_buf() });
} }
/// Returns the current process values.
pub(crate) const fn process_values(&self) -> &std::collections::BTreeMap<String, String> { pub(crate) const fn process_values(&self) -> &std::collections::BTreeMap<String, String> {
return &self.process; return &self.process;
} }
/// Returns the current dotenv values.
pub(crate) const fn dotenv_values(&self) -> &std::collections::BTreeMap<String, String> { pub(crate) const fn dotenv_values(&self) -> &std::collections::BTreeMap<String, String> {
return &self.dotenv; return &self.dotenv;
} }
/// Builds `ConfigEnvironment` from maps.
#[cfg(test)] #[cfg(test)]
pub(crate) fn from_maps(process: std::collections::BTreeMap<String, String>, dotenv: std::collections::BTreeMap<String, String>) -> Self { pub(crate) fn from_maps(process: std::collections::BTreeMap<String, String>, dotenv: std::collections::BTreeMap<String, String>) -> Self {
return Self { process, dotenv, dotenv_path: std::path::PathBuf::from(DEFAULT_DOTENV_PATH) }; return Self { process, dotenv, dotenv_path: std::path::PathBuf::from(DEFAULT_DOTENV_PATH) };
} }
} }
fn collect_process_environment<I>(values: I) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>> /// Executes the crate-internal parse dotenv content operation for the owning module.
where
I: std::iter::IntoIterator<Item = (std::ffi::OsString, std::ffi::OsString)>,
{
let mut output = std::collections::BTreeMap::<String, String>::new();
for (name, value) in values {
let name = match name.to_str() {
std::option::Option::Some(value) => value,
std::option::Option::None => continue,
};
if !has_supported_namespace(name) {
continue;
}
let validation = validate_supported_variable_name(name);
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error);
}
let value = value.into_string();
let value = match value {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(invalid_environment_value_error(name)),
};
output.insert(name.to_owned(), value);
}
return std::result::Result::Ok(output);
}
fn load_dotenv_file(path: &std::path::Path) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>> {
let content = std::fs::read_to_string(path);
let content = match content {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => return std::result::Result::Ok(std::collections::BTreeMap::new()),
std::result::Result::Err(error) => return std::result::Result::Err(dotenv_read_error(path, error)),
};
return parse_dotenv_content(path, content.as_str());
}
pub(crate) fn parse_dotenv_content(path: &std::path::Path, content: &str) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>> { pub(crate) fn parse_dotenv_content(path: &std::path::Path, content: &str) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>> {
let mut output = std::collections::BTreeMap::<String, String>::new(); let mut output = std::collections::BTreeMap::<String, String>::new();
for (line_index, raw_line) in content.lines().enumerate() { for (line_index, raw_line) in content.lines().enumerate() {
@@ -342,6 +309,61 @@ pub(crate) fn parse_dotenv_content(path: &std::path::Path, content: &str) -> ksp
return std::result::Result::Ok(output); return std::result::Result::Ok(output);
} }
/// Validates supported variable name.
pub(crate) fn validate_supported_variable_name(variable_name: &str) -> ksp_core_lib::Result<()> {
if !has_supported_namespace(variable_name) {
return std::result::Result::Err(invalid_variable_error(variable_name, "variable must use the KSP_ or KSPB_ namespace"));
}
let prefix_length = if variable_name.starts_with("KSPB_") { 5 } else { 4 };
if variable_name.len() <= prefix_length {
return std::result::Result::Err(invalid_variable_error(variable_name, "variable namespace must be followed by a name"));
}
for byte in variable_name.bytes() {
let valid = byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_';
if !valid {
return std::result::Result::Err(invalid_variable_error(variable_name, "variable names use uppercase ASCII letters, digits and underscores"));
}
}
return std::result::Result::Ok(());
}
fn collect_process_environment<I>(values: I) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>>
where
I: std::iter::IntoIterator<Item = (std::ffi::OsString, std::ffi::OsString)>,
{
let mut output = std::collections::BTreeMap::<String, String>::new();
for (name, value) in values {
let name = match name.to_str() {
std::option::Option::Some(value) => value,
std::option::Option::None => continue,
};
if !has_supported_namespace(name) {
continue;
}
let validation = validate_supported_variable_name(name);
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error);
}
let value = value.into_string();
let value = match value {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(invalid_environment_value_error(name)),
};
output.insert(name.to_owned(), value);
}
return std::result::Result::Ok(output);
}
fn load_dotenv_file(path: &std::path::Path) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>> {
let content = std::fs::read_to_string(path);
let content = match content {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => return std::result::Result::Ok(std::collections::BTreeMap::new()),
std::result::Result::Err(error) => return std::result::Result::Err(dotenv_read_error(path, error)),
};
return crate::parse_dotenv_content(path, content.as_str());
}
fn parse_dotenv_value(path: &std::path::Path, line_number: usize, raw_value: &str) -> ksp_core_lib::Result<String> { fn parse_dotenv_value(path: &std::path::Path, line_number: usize, raw_value: &str) -> ksp_core_lib::Result<String> {
if raw_value.starts_with('\'') && (raw_value.len() < 2 || !raw_value.ends_with('\'')) { if raw_value.starts_with('\'') && (raw_value.len() < 2 || !raw_value.ends_with('\'')) {
return std::result::Result::Err(dotenv_syntax_error(path, line_number, "single-quoted value is not terminated")); return std::result::Result::Err(dotenv_syntax_error(path, line_number, "single-quoted value is not terminated"));
@@ -516,23 +538,6 @@ fn escape_json_pointer_token(value: &str) -> String {
return value.replace('~', "~0").replace('/', "~1"); return value.replace('~', "~0").replace('/', "~1");
} }
pub(crate) fn validate_supported_variable_name(variable_name: &str) -> ksp_core_lib::Result<()> {
if !has_supported_namespace(variable_name) {
return std::result::Result::Err(invalid_variable_error(variable_name, "variable must use the KSP_ or KSPB_ namespace"));
}
let prefix_length = if variable_name.starts_with("KSPB_") { 5 } else { 4 };
if variable_name.len() <= prefix_length {
return std::result::Result::Err(invalid_variable_error(variable_name, "variable namespace must be followed by a name"));
}
for byte in variable_name.bytes() {
let valid = byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_';
if !valid {
return std::result::Result::Err(invalid_variable_error(variable_name, "variable names use uppercase ASCII letters, digits and underscores"));
}
}
return std::result::Result::Ok(());
}
fn has_supported_namespace(variable_name: &str) -> bool { fn has_supported_namespace(variable_name: &str) -> bool {
return variable_name.starts_with("KSP_") || variable_name.starts_with("KSPB_"); return variable_name.starts_with("KSP_") || variable_name.starts_with("KSPB_");
} }

View File

@@ -1,68 +1,47 @@
// file: crates/ksp-config-lib/src/error.rs // file: crates/ksp-config-lib/src/error.rs
// version: 8 // version: 9
/// Error code used when a Config bootstrap argument is missing its value. /// Error code used when a Config bootstrap argument is missing its value.
pub const ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "bootstrap_argument_missing_value"); pub const ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "bootstrap_argument_missing_value");
/// Error code used when a Config bootstrap path is empty, inaccessible, or resolves to an existing non-directory path. /// Error code used when a Config bootstrap path is empty, inaccessible, or resolves to an existing non-directory path.
pub const ERROR_CODE_BOOTSTRAP_INVALID_PATH: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "bootstrap_invalid_path"); pub const ERROR_CODE_BOOTSTRAP_INVALID_PATH: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "bootstrap_invalid_path");
/// Error code used when a logical Config file identifier is malformed.
pub const ERROR_CODE_FILE_ID_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_id_invalid");
/// Error code used when a requested logical Config file identifier is not registered.
pub const ERROR_CODE_FILE_ID_UNKNOWN: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_id_unknown");
/// Error code used when the same logical Config file identifier is registered more than once.
pub const ERROR_CODE_FILE_ID_DUPLICATE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_id_duplicate");
/// Error code used when a Config filename mapping or descriptor relation is invalid.
pub const ERROR_CODE_FILE_MAPPING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_mapping_invalid");
/// Error code used when a Config-managed JSON document or schema cannot be read from its resolved path.
pub const ERROR_CODE_JSON_FILE_READ_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "json_file_read_failed");
/// Error code used when a Config-managed file contains invalid JSON syntax.
pub const ERROR_CODE_JSON_SYNTAX_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "json_syntax_invalid");
/// Error code used when a JSON Schema document is itself invalid for the selected JSON Schema draft.
pub const ERROR_CODE_SCHEMA_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "schema_invalid");
/// Error code used when a Config document does not satisfy its registered JSON Schema.
pub const ERROR_CODE_SCHEMA_VALIDATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "schema_validation_failed");
/// Error code used when a schema-valid Config document violates KSP semantic invariants for its document type.
pub const ERROR_CODE_DOCUMENT_SEMANTIC_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "document_semantic_invalid");
/// Error code used when an explicitly requested Config profile does not exist in a validated document.
pub const ERROR_CODE_PROFILE_NOT_FOUND: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "profile_not_found");
/// Error code used when a composite document references an invalid, unknown, or unsupported Config document. /// Error code used when a composite document references an invalid, unknown, or unsupported Config document.
pub const ERROR_CODE_COMPOSITE_REFERENCE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "composite_reference_invalid"); pub const ERROR_CODE_COMPOSITE_REFERENCE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "composite_reference_invalid");
/// Error code used when a schema-valid Config document violates KSP semantic invariants for its document type.
pub const ERROR_CODE_DOCUMENT_SEMANTIC_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "document_semantic_invalid");
/// Error code used when the local `.env` file cannot be read for a reason other than absence. /// Error code used when the local `.env` file cannot be read for a reason other than absence.
pub const ERROR_CODE_DOTENV_FILE_READ_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "dotenv_file_read_failed"); pub const ERROR_CODE_DOTENV_FILE_READ_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "dotenv_file_read_failed");
/// Error code used when the local `.env` file contains syntax Config cannot interpret safely. /// Error code used when the local `.env` file contains syntax Config cannot interpret safely.
pub const ERROR_CODE_DOTENV_SYNTAX_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "dotenv_syntax_invalid"); pub const ERROR_CODE_DOTENV_SYNTAX_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "dotenv_syntax_invalid");
/// Error code used when a Config environment variable name is malformed or outside the KSP/KSPB namespaces.
pub const ERROR_CODE_ENVIRONMENT_VARIABLE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_variable_invalid");
/// Error code used when a referenced Config environment variable is absent and has no fallback.
pub const ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_variable_missing");
/// Error code used when a supported process environment variable has a value that cannot become a JSON UTF-8 string.
pub const ERROR_CODE_ENVIRONMENT_VALUE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_value_invalid");
/// Error code used when a `${NAME}` / `${NAME:-fallback}` expression is malformed.
pub const ERROR_CODE_ENVIRONMENT_PLACEHOLDER_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_placeholder_invalid");
/// Error code used when an environment-resolved Config cannot be mapped safely to a runtime consumer contract. /// Error code used when an environment-resolved Config cannot be mapped safely to a runtime consumer contract.
pub const ERROR_CODE_EFFECTIVE_CONFIG_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "effective_config_invalid"); pub const ERROR_CODE_EFFECTIVE_CONFIG_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "effective_config_invalid");
/// Error code used when a `${NAME}` / `${NAME:-fallback}` expression is malformed.
pub const ERROR_CODE_ENVIRONMENT_PLACEHOLDER_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_placeholder_invalid");
/// Error code used when a supported process environment variable has a value that cannot become a JSON UTF-8 string.
pub const ERROR_CODE_ENVIRONMENT_VALUE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_value_invalid");
/// Error code used when a Config environment variable name is malformed or outside the KSP/KSPB namespaces.
pub const ERROR_CODE_ENVIRONMENT_VARIABLE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_variable_invalid");
/// Error code used when a referenced Config environment variable is absent and has no fallback.
pub const ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_variable_missing");
/// Error code used when the same logical Config file identifier is registered more than once.
pub const ERROR_CODE_FILE_ID_DUPLICATE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_id_duplicate");
/// Error code used when a logical Config file identifier is malformed.
pub const ERROR_CODE_FILE_ID_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_id_invalid");
/// Error code used when a requested logical Config file identifier is not registered.
pub const ERROR_CODE_FILE_ID_UNKNOWN: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_id_unknown");
/// Error code used when a Config filename mapping or descriptor relation is invalid.
pub const ERROR_CODE_FILE_MAPPING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_mapping_invalid");
/// Error code used when a Config-managed JSON document or schema cannot be read from its resolved path.
pub const ERROR_CODE_JSON_FILE_READ_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "json_file_read_failed");
/// Error code used when a Config-managed file contains invalid JSON syntax.
pub const ERROR_CODE_JSON_SYNTAX_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "json_syntax_invalid");
/// Error code used when an explicit Config management operation is unsupported or targets the wrong managed resource kind. /// Error code used when an explicit Config management operation is unsupported or targets the wrong managed resource kind.
pub const ERROR_CODE_MANAGEMENT_OPERATION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "management_operation_invalid"); pub const ERROR_CODE_MANAGEMENT_OPERATION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "management_operation_invalid");
/// Error code used when an atomic managed Config or `.env` persistence operation fails before commit. /// Error code used when an atomic managed Config or `.env` persistence operation fails before commit.
pub const ERROR_CODE_PERSISTENCE_WRITE_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "persistence_write_failed"); pub const ERROR_CODE_PERSISTENCE_WRITE_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "persistence_write_failed");
/// Error code used when an explicitly requested Config profile does not exist in a validated document.
pub const ERROR_CODE_PROFILE_NOT_FOUND: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "profile_not_found");
/// Error code used when a JSON Schema document is itself invalid for the selected JSON Schema draft.
pub const ERROR_CODE_SCHEMA_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "schema_invalid");
/// Error code used when a Config document does not satisfy its registered JSON Schema.
pub const ERROR_CODE_SCHEMA_VALIDATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "schema_validation_failed");

View File

@@ -1,5 +1,6 @@
// file: crates/ksp-config-lib/src/lib.rs // file: crates/ksp-config-lib/src/lib.rs
// version: 12 // version: 14
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
@@ -25,8 +26,6 @@ mod registry;
mod sensitivity; mod sensitivity;
mod transport; mod transport;
pub(crate) use self::constants::TRACING_TARGET;
/// Bootstrap argument used to replace the configuration document root. /// Bootstrap argument used to replace the configuration document root.
pub use self::bootstrap::ARG_CFG_PATH; pub use self::bootstrap::ARG_CFG_PATH;
/// Bootstrap argument used to replace the schema root. /// Bootstrap argument used to replace the schema root.
@@ -171,3 +170,22 @@ pub use self::sensitivity::ResolvedConfigJson;
pub use self::sensitivity::ResolvedConfigText; pub use self::sensitivity::ResolvedConfigText;
/// Effective standard HTTP Transport configuration mapped to `ksp_onchain_transport_lib::HttpTransportSettings`. /// Effective standard HTTP Transport configuration mapped to `ksp_onchain_transport_lib::HttpTransportSettings`.
pub use self::transport::ResolvedTransportConfig; pub use self::transport::ResolvedTransportConfig;
/// Validates composite document contract.
pub(crate) use self::composite::validate_composite_document_contract;
/// Owning tracing target for events emitted by the Config crate.
pub(crate) use self::constants::TRACING_TARGET;
/// Executes the crate-internal parse dotenv content operation for the owning module.
pub(crate) use self::environment::parse_dotenv_content;
/// Validates supported variable name.
pub(crate) use self::environment::validate_supported_variable_name;
/// Executes the crate-internal atomic write operation for the owning module.
pub(crate) use self::persistence::atomic_write;
/// Executes the crate-internal atomic write private operation for the owning module.
pub(crate) use self::persistence::atomic_write_private;
/// Loads resolved profile with source.
pub(crate) use self::profile::load_resolved_profile_with_source;
/// Validates document profile contract.
pub(crate) use self::profile::validate_document_profile_contract;
/// Executes the crate-internal build registry operation for the owning module.
pub(crate) use self::registry::build_registry;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/management.rs // file: crates/ksp-config-lib/src/management.rs
// version: 4 // version: 6
/// Raw source of one registered Config document read for explicit management/correction. /// Raw source of one registered Config document read for explicit management/correction.
#[derive(Clone, Eq, PartialEq)] #[derive(Clone, Eq, PartialEq)]
@@ -727,7 +727,7 @@ impl ConfigManagement {
/// Authentication/authorization of the human user belongs to the calling application. Calling this method is the explicit Config boundary that opts into /// Authentication/authorization of the human user belongs to the calling application. Calling this method is the explicit Config boundary that opts into
/// real-value access; the returned value must never be logged. /// real-value access; the returned value must never be logged.
pub fn reveal_effective_environment_value(&self, variable_name: &str) -> ksp_core_lib::Result<std::option::Option<String>> { pub fn reveal_effective_environment_value(&self, variable_name: &str) -> ksp_core_lib::Result<std::option::Option<String>> {
let validation = crate::environment::validate_supported_variable_name(variable_name); let validation = crate::validate_supported_variable_name(variable_name);
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);
} }
@@ -744,7 +744,7 @@ impl ConfigManagement {
/// Explicitly reveals the real persisted `.env` value for management display/editing. /// Explicitly reveals the real persisted `.env` value for management display/editing.
pub fn reveal_dotenv_value(&self, variable_name: &str) -> ksp_core_lib::Result<std::option::Option<String>> { pub fn reveal_dotenv_value(&self, variable_name: &str) -> ksp_core_lib::Result<std::option::Option<String>> {
let validation = crate::environment::validate_supported_variable_name(variable_name); let validation = crate::validate_supported_variable_name(variable_name);
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);
} }
@@ -758,7 +758,7 @@ impl ConfigManagement {
/// Creates or updates one supported KSP/KSPB `.env` entry atomically without mutating the inherited process environment. /// Creates or updates one supported KSP/KSPB `.env` entry atomically without mutating the inherited process environment.
pub fn set_dotenv_value(&self, variable_name: &str, value: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeReport> { pub fn set_dotenv_value(&self, variable_name: &str, value: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeReport> {
let validation = crate::environment::validate_supported_variable_name(variable_name); let validation = crate::validate_supported_variable_name(variable_name);
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);
} }
@@ -781,7 +781,7 @@ impl ConfigManagement {
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
if candidate != content { if candidate != content {
let write = crate::persistence::atomic_write_private(self.dotenv_path.as_path(), candidate.as_bytes()); let write = crate::atomic_write_private(self.dotenv_path.as_path(), candidate.as_bytes());
match write { match write {
std::result::Result::Ok(()) => {}, std::result::Result::Ok(()) => {},
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -797,7 +797,7 @@ impl ConfigManagement {
/// Removes one supported KSP/KSPB `.env` entry atomically without mutating the inherited process environment. /// Removes one supported KSP/KSPB `.env` entry atomically without mutating the inherited process environment.
pub fn remove_dotenv_value(&self, variable_name: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeReport> { pub fn remove_dotenv_value(&self, variable_name: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeReport> {
let validation = crate::environment::validate_supported_variable_name(variable_name); let validation = crate::validate_supported_variable_name(variable_name);
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);
} }
@@ -820,7 +820,7 @@ impl ConfigManagement {
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
if candidate != content { if candidate != content {
let write = crate::persistence::atomic_write_private(self.dotenv_path.as_path(), candidate.as_bytes()); let write = crate::atomic_write_private(self.dotenv_path.as_path(), candidate.as_bytes());
match write { match write {
std::result::Result::Ok(()) => {}, std::result::Result::Ok(()) => {},
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -838,6 +838,7 @@ impl ConfigManagement {
return crate::ConfigEnvironment::load_from_dotenv_path(self.dotenv_path.as_path()); return crate::ConfigEnvironment::load_from_dotenv_path(self.dotenv_path.as_path());
} }
/// Executes the crate-internal with dotenv path operation for `ConfigManagement`.
#[cfg(test)] #[cfg(test)]
pub(crate) fn with_dotenv_path(engine: crate::ConfigDocumentEngine, dotenv_path: std::path::PathBuf) -> Self { pub(crate) fn with_dotenv_path(engine: crate::ConfigDocumentEngine, dotenv_path: std::path::PathBuf) -> Self {
return Self { engine, dotenv_path }; return Self { engine, dotenv_path };
@@ -921,7 +922,7 @@ fn read_dotenv_source(path: &std::path::Path) -> ksp_core_lib::Result<String> {
let content = std::fs::read_to_string(path); let content = std::fs::read_to_string(path);
return match content { return match content {
std::result::Result::Ok(value) => { std::result::Result::Ok(value) => {
let validation = crate::environment::parse_dotenv_content(path, value.as_str()); let validation = crate::parse_dotenv_content(path, value.as_str());
match validation { match validation {
std::result::Result::Ok(_) => std::result::Result::Ok(value), std::result::Result::Ok(_) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(error), std::result::Result::Err(error) => std::result::Result::Err(error),
@@ -937,7 +938,7 @@ fn read_dotenv_source(path: &std::path::Path) -> ksp_core_lib::Result<String> {
} }
fn update_dotenv_source(path: &std::path::Path, source: &str, variable_name: &str, replacement: std::option::Option<&str>) -> ksp_core_lib::Result<String> { fn update_dotenv_source(path: &std::path::Path, source: &str, variable_name: &str, replacement: std::option::Option<&str>) -> ksp_core_lib::Result<String> {
let validation = crate::environment::parse_dotenv_content(path, source); let validation = crate::parse_dotenv_content(path, source);
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);
} }
@@ -967,7 +968,7 @@ fn update_dotenv_source(path: &std::path::Path, source: &str, variable_name: &st
if !candidate.is_empty() { if !candidate.is_empty() {
candidate.push('\n'); candidate.push('\n');
} }
let candidate_validation = crate::environment::parse_dotenv_content(path, candidate.as_str()); let candidate_validation = crate::parse_dotenv_content(path, candidate.as_str());
return match candidate_validation { return match candidate_validation {
std::result::Result::Ok(_) => std::result::Result::Ok(candidate), std::result::Result::Ok(_) => std::result::Result::Ok(candidate),
std::result::Result::Err(error) => std::result::Result::Err(error), std::result::Result::Err(error) => std::result::Result::Err(error),
@@ -1030,7 +1031,7 @@ fn persist_document_source(file_id: &crate::ConfigFileId, path: &std::path::Path
if existing.as_slice() == source { if existing.as_slice() == source {
return std::result::Result::Ok(ConfigDocumentChangeReport { source_changed: false, reload_required: false }); return std::result::Result::Ok(ConfigDocumentChangeReport { source_changed: false, reload_required: false });
} }
let write = crate::persistence::atomic_write(path, source); let write = crate::atomic_write(path, source);
if let std::result::Result::Err(error) = write { if let std::result::Result::Err(error) = write {
return std::result::Result::Err(error); return std::result::Result::Err(error);
} }

View File

@@ -1,12 +1,14 @@
// file: crates/ksp-config-lib/src/persistence.rs // file: crates/ksp-config-lib/src/persistence.rs
// version: 3 // version: 4
static NEXT_TEMPORARY_FILE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1); static NEXT_TEMPORARY_FILE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
/// Executes the crate-internal atomic write operation for the owning module.
pub(crate) fn atomic_write(path: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> { pub(crate) fn atomic_write(path: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
return atomic_write_with_policy(path, content, false); return atomic_write_with_policy(path, content, false);
} }
/// Executes the crate-internal atomic write private operation for the owning module.
pub(crate) fn atomic_write_private(path: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> { pub(crate) fn atomic_write_private(path: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
return atomic_write_with_policy(path, content, true); return atomic_write_with_policy(path, content, true);
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/profile.rs // file: crates/ksp-config-lib/src/profile.rs
// version: 4 // version: 5
/// Origin of one top-level value in a resolved standard Config profile. /// Origin of one top-level value in a resolved standard Config profile.
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -135,6 +135,7 @@ impl crate::ConfigDocumentEngine {
} }
} }
/// Loads resolved profile with source.
pub(crate) fn load_resolved_profile_with_source( pub(crate) fn load_resolved_profile_with_source(
engine: &crate::ConfigDocumentEngine, engine: &crate::ConfigDocumentEngine,
file_id: &crate::ConfigFileId, file_id: &crate::ConfigFileId,
@@ -153,6 +154,7 @@ pub(crate) fn load_resolved_profile_with_source(
return resolve_document_profile(&document, requested_profile, source); return resolve_document_profile(&document, requested_profile, source);
} }
/// Validates document profile contract.
pub(crate) fn validate_document_profile_contract(document: &crate::ConfigJsonDocument) -> ksp_core_lib::Result<()> { pub(crate) fn validate_document_profile_contract(document: &crate::ConfigJsonDocument) -> ksp_core_lib::Result<()> {
let root = match document.value().as_object() { let root = match document.value().as_object() {
std::option::Option::Some(value) => value, std::option::Option::Some(value) => value,

View File

@@ -1,18 +1,10 @@
// file: crates/ksp-config-lib/src/registry.rs // file: crates/ksp-config-lib/src/registry.rs
// version: 5 // version: 7
/// Bootstrap argument used to replace a known Config filename mapping. /// Bootstrap argument used to replace a known Config filename mapping.
pub const ARG_FILE_MAP: &str = "--filemap"; pub const ARG_FILE_MAP: &str = "--filemap";
/// Logical file identifier for the standard Logging configuration document. /// Default physical filename for the generic composite JSON Schema document.
pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging"; pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json";
/// Logical file identifier for the standard Logging JSON Schema document.
pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging";
/// Logical file identifier for the standard HTTP Transport configuration document.
pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport";
/// Logical file identifier for the standard HTTP Transport JSON Schema document.
pub const FILE_ID_SCHEMA_STD_TRANSPORT: &str = "schema.std.transport";
/// Logical file identifier for the generic composite JSON Schema document.
pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite";
/// Default physical filename for the standard Logging configuration document. /// Default physical filename for the standard Logging configuration document.
pub const DEFAULT_STD_LOGGING_FILENAME: &str = "std.logging.json"; pub const DEFAULT_STD_LOGGING_FILENAME: &str = "std.logging.json";
/// Default physical filename for the standard Logging JSON Schema document. /// Default physical filename for the standard Logging JSON Schema document.
@@ -21,8 +13,16 @@ pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json";
pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json"; pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json";
/// Default physical filename for the standard HTTP Transport JSON Schema document. /// Default physical filename for the standard HTTP Transport JSON Schema document.
pub const DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME: &str = "std.transport.schema.json"; pub const DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME: &str = "std.transport.schema.json";
/// Default physical filename for the generic composite JSON Schema document. /// Logical file identifier for the generic composite JSON Schema document.
pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json"; pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite";
/// Logical file identifier for the standard Logging JSON Schema document.
pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging";
/// Logical file identifier for the standard HTTP Transport JSON Schema document.
pub const FILE_ID_SCHEMA_STD_TRANSPORT: &str = "schema.std.transport";
/// Logical file identifier for the standard Logging configuration document.
pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging";
/// Logical file identifier for the standard HTTP Transport configuration document.
pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport";
/// Stable logical identifier for a Config-managed file. /// Stable logical identifier for a Config-managed file.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
@@ -89,6 +89,7 @@ impl ConfigFileDescriptor {
return self.schema_file_id.as_ref(); return self.schema_file_id.as_ref();
} }
/// Creates a new `ConfigFileDescriptor` value.
pub(crate) fn new( pub(crate) fn new(
file_id: &'static str, file_id: &'static str,
kind: ConfigFileKind, kind: ConfigFileKind,
@@ -165,7 +166,7 @@ impl ConfigFileRegistry {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
return build_registry([logging, logging_schema, transport, transport_schema, composite_schema]); return crate::build_registry([logging, logging_schema, transport, transport_schema, composite_schema]);
} }
/// Creates the default registry and applies repeatable `--filemap=<file_id>=<filename>` overrides from raw process arguments. /// Creates the default registry and applies repeatable `--filemap=<file_id>=<filename>` overrides from raw process arguments.
@@ -249,6 +250,7 @@ impl ConfigFileRegistry {
} }
} }
/// Executes the crate-internal build registry operation for the owning module.
pub(crate) fn build_registry<const N: usize>(descriptors: [ConfigFileDescriptor; N]) -> ksp_core_lib::Result<ConfigFileRegistry> { pub(crate) fn build_registry<const N: usize>(descriptors: [ConfigFileDescriptor; N]) -> ksp_core_lib::Result<ConfigFileRegistry> {
let mut registry = ConfigFileRegistry { descriptors: std::collections::BTreeMap::new() }; let mut registry = ConfigFileRegistry { descriptors: std::collections::BTreeMap::new() };
for descriptor in descriptors { for descriptor in descriptors {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/sensitivity.rs // file: crates/ksp-config-lib/src/sensitivity.rs
// version: 1 // version: 2
/// Replacement used for secret environment fragments in safe diagnostic representations. /// Replacement used for secret environment fragments in safe diagnostic representations.
pub const REDACTED_CONFIG_VALUE: &str = "********"; pub const REDACTED_CONFIG_VALUE: &str = "********";
@@ -18,7 +18,7 @@ pub enum ConfigSensitivity {
impl ConfigSensitivity { impl ConfigSensitivity {
/// Classifies one supported KSP/KSPB environment variable by its namespace. /// Classifies one supported KSP/KSPB environment variable by its namespace.
pub fn from_variable_name(variable_name: &str) -> ksp_core_lib::Result<Self> { pub fn from_variable_name(variable_name: &str) -> ksp_core_lib::Result<Self> {
let validation = crate::environment::validate_supported_variable_name(variable_name); let validation = crate::validate_supported_variable_name(variable_name);
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);
} }
@@ -103,6 +103,7 @@ pub struct ResolvedConfigText {
} }
impl ResolvedConfigText { impl ResolvedConfigText {
/// Creates a new `ResolvedConfigText` value.
pub(crate) fn new(value: String, safe_value: String, sensitivity: ConfigSensitivity, provenance: std::vec::Vec<ConfigValueProvenance>) -> Self { pub(crate) fn new(value: String, safe_value: String, sensitivity: ConfigSensitivity, provenance: std::vec::Vec<ConfigValueProvenance>) -> Self {
return Self { value, safe_value, sensitivity, provenance }; return Self { value, safe_value, sensitivity, provenance };
} }
@@ -153,6 +154,7 @@ pub struct ResolvedConfigJson {
} }
impl ResolvedConfigJson { impl ResolvedConfigJson {
/// Creates a new `ResolvedConfigJson` value.
pub(crate) fn new( pub(crate) fn new(
value: serde_json::Value, value: serde_json::Value,
safe_value: serde_json::Value, safe_value: serde_json::Value,

View File

@@ -1,39 +1,39 @@
// file: crates/ksp-config-lib/unit_tests/bootstrap.rs // file: crates/ksp-config-lib/unit_tests/bootstrap.rs
// version: 1 // version: 2
#[test] #[test]
fn defaults_use_hardcoded_ksp_roots() { fn defaults_use_hardcoded_ksp_roots() {
let result = super::ConfigBootstrapOptions::defaults(); let result = crate::ConfigBootstrapOptions::defaults();
assert!(result.is_ok(), "default bootstrap paths should be valid: {result:?}"); assert!(result.is_ok(), "default bootstrap paths should be valid: {result:?}");
if let std::result::Result::Ok(options) = result { if let std::result::Result::Ok(options) = result {
assert_eq!(options.cfg_path(), std::path::Path::new(super::DEFAULT_CFG_PATH)); assert_eq!(options.cfg_path(), std::path::Path::new(crate::DEFAULT_CFG_PATH));
assert_eq!(options.schema_path(), std::path::Path::new(super::DEFAULT_SCHEMA_PATH)); assert_eq!(options.schema_path(), std::path::Path::new(crate::DEFAULT_SCHEMA_PATH));
} }
} }
#[test] #[test]
fn cfg_path_override_keeps_schema_default() { fn cfg_path_override_keeps_schema_default() {
let defaults = super::ConfigBootstrapOptions::defaults(); let defaults = crate::ConfigBootstrapOptions::defaults();
assert!(defaults.is_ok(), "default bootstrap paths should be valid: {defaults:?}"); assert!(defaults.is_ok(), "default bootstrap paths should be valid: {defaults:?}");
if let std::result::Result::Ok(options) = defaults { if let std::result::Result::Ok(options) = defaults {
let result = options.with_cfg_path("custom-config"); let result = options.with_cfg_path("custom-config");
assert!(result.is_ok(), "cfg path override should be valid: {result:?}"); assert!(result.is_ok(), "cfg path override should be valid: {result:?}");
if let std::result::Result::Ok(options) = result { if let std::result::Result::Ok(options) = result {
assert_eq!(options.cfg_path(), std::path::Path::new("custom-config")); assert_eq!(options.cfg_path(), std::path::Path::new("custom-config"));
assert_eq!(options.schema_path(), std::path::Path::new(super::DEFAULT_SCHEMA_PATH)); assert_eq!(options.schema_path(), std::path::Path::new(crate::DEFAULT_SCHEMA_PATH));
} }
} }
} }
#[test] #[test]
fn schema_path_override_keeps_cfg_default() { fn schema_path_override_keeps_cfg_default() {
let defaults = super::ConfigBootstrapOptions::defaults(); let defaults = crate::ConfigBootstrapOptions::defaults();
assert!(defaults.is_ok(), "default bootstrap paths should be valid: {defaults:?}"); assert!(defaults.is_ok(), "default bootstrap paths should be valid: {defaults:?}");
if let std::result::Result::Ok(options) = defaults { if let std::result::Result::Ok(options) = defaults {
let result = options.with_schema_path("custom-schemas"); let result = options.with_schema_path("custom-schemas");
assert!(result.is_ok(), "schema path override should be valid: {result:?}"); assert!(result.is_ok(), "schema path override should be valid: {result:?}");
if let std::result::Result::Ok(options) = result { if let std::result::Result::Ok(options) = result {
assert_eq!(options.cfg_path(), std::path::Path::new(super::DEFAULT_CFG_PATH)); assert_eq!(options.cfg_path(), std::path::Path::new(crate::DEFAULT_CFG_PATH));
assert_eq!(options.schema_path(), std::path::Path::new("custom-schemas")); assert_eq!(options.schema_path(), std::path::Path::new("custom-schemas"));
} }
} }
@@ -42,21 +42,21 @@ fn schema_path_override_keeps_cfg_default() {
#[test] #[test]
fn cfg_cli_override_keeps_schema_default() { fn cfg_cli_override_keeps_schema_default() {
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--cfgpath=cli-config")]; let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--cfgpath=cli-config")];
let result = super::ConfigBootstrapOptions::from_args(&args); let result = crate::ConfigBootstrapOptions::from_args(&args);
assert!(result.is_ok(), "cfg CLI override should parse: {result:?}"); assert!(result.is_ok(), "cfg CLI override should parse: {result:?}");
if let std::result::Result::Ok(options) = result { if let std::result::Result::Ok(options) = result {
assert_eq!(options.cfg_path(), std::path::Path::new("cli-config")); assert_eq!(options.cfg_path(), std::path::Path::new("cli-config"));
assert_eq!(options.schema_path(), std::path::Path::new(super::DEFAULT_SCHEMA_PATH)); assert_eq!(options.schema_path(), std::path::Path::new(crate::DEFAULT_SCHEMA_PATH));
} }
} }
#[test] #[test]
fn schema_cli_override_keeps_cfg_default() { fn schema_cli_override_keeps_cfg_default() {
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--schemapath=cli-schemas")]; let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--schemapath=cli-schemas")];
let result = super::ConfigBootstrapOptions::from_args(&args); let result = crate::ConfigBootstrapOptions::from_args(&args);
assert!(result.is_ok(), "schema CLI override should parse: {result:?}"); assert!(result.is_ok(), "schema CLI override should parse: {result:?}");
if let std::result::Result::Ok(options) = result { if let std::result::Result::Ok(options) = result {
assert_eq!(options.cfg_path(), std::path::Path::new(super::DEFAULT_CFG_PATH)); assert_eq!(options.cfg_path(), std::path::Path::new(crate::DEFAULT_CFG_PATH));
assert_eq!(options.schema_path(), std::path::Path::new("cli-schemas")); assert_eq!(options.schema_path(), std::path::Path::new("cli-schemas"));
} }
} }
@@ -73,7 +73,7 @@ fn parser_accepts_inline_and_separate_forms_and_last_value_wins() {
std::ffi::OsString::from("--schemapath"), std::ffi::OsString::from("--schemapath"),
std::ffi::OsString::from("second-schemas"), std::ffi::OsString::from("second-schemas"),
]; ];
let result = super::ConfigBootstrapOptions::from_args(&args); let result = crate::ConfigBootstrapOptions::from_args(&args);
assert!(result.is_ok(), "bootstrap arguments should parse: {result:?}"); assert!(result.is_ok(), "bootstrap arguments should parse: {result:?}");
if let std::result::Result::Ok(options) = result { if let std::result::Result::Ok(options) = result {
assert_eq!(options.cfg_path(), std::path::Path::new("second-config")); assert_eq!(options.cfg_path(), std::path::Path::new("second-config"));
@@ -84,7 +84,7 @@ fn parser_accepts_inline_and_separate_forms_and_last_value_wins() {
#[test] #[test]
fn parser_reports_missing_separate_value() { fn parser_reports_missing_separate_value() {
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--cfgpath")]; let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--cfgpath")];
let result = super::ConfigBootstrapOptions::from_args(&args); let result = crate::ConfigBootstrapOptions::from_args(&args);
assert!(result.is_err(), "missing value must be rejected"); assert!(result.is_err(), "missing value must be rejected");
if let std::result::Result::Err(error) = result { if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE); assert_eq!(error.code(), crate::ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE);
@@ -94,7 +94,7 @@ fn parser_reports_missing_separate_value() {
#[test] #[test]
fn parser_reports_another_option_as_missing_separate_value() { fn parser_reports_another_option_as_missing_separate_value() {
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--cfgpath"), std::ffi::OsString::from("--other-option")]; let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--cfgpath"), std::ffi::OsString::from("--other-option")];
let result = super::ConfigBootstrapOptions::from_args(&args); let result = crate::ConfigBootstrapOptions::from_args(&args);
assert!(result.is_err(), "another option must not become a bootstrap path value"); assert!(result.is_err(), "another option must not become a bootstrap path value");
if let std::result::Result::Err(error) = result { if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE); assert_eq!(error.code(), crate::ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE);
@@ -104,7 +104,7 @@ fn parser_reports_another_option_as_missing_separate_value() {
#[test] #[test]
fn empty_inline_path_is_rejected() { fn empty_inline_path_is_rejected() {
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--schemapath=")]; let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--schemapath=")];
let result = super::ConfigBootstrapOptions::from_args(&args); let result = crate::ConfigBootstrapOptions::from_args(&args);
assert!(result.is_err(), "empty path must be rejected"); assert!(result.is_err(), "empty path must be rejected");
if let std::result::Result::Err(error) = result { if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_BOOTSTRAP_INVALID_PATH); assert_eq!(error.code(), crate::ERROR_CODE_BOOTSTRAP_INVALID_PATH);
@@ -113,7 +113,7 @@ fn empty_inline_path_is_rejected() {
#[test] #[test]
fn explicit_programmatic_paths_do_not_depend_on_default_roots() { fn explicit_programmatic_paths_do_not_depend_on_default_roots() {
let result = super::ConfigBootstrapOptions::from_paths("programmatic-config", "programmatic-schemas"); let result = crate::ConfigBootstrapOptions::from_paths("programmatic-config", "programmatic-schemas");
assert!(result.is_ok(), "explicit programmatic paths should be valid: {result:?}"); assert!(result.is_ok(), "explicit programmatic paths should be valid: {result:?}");
if let std::result::Result::Ok(options) = result { if let std::result::Result::Ok(options) = result {
assert_eq!(options.cfg_path(), std::path::Path::new("programmatic-config")); assert_eq!(options.cfg_path(), std::path::Path::new("programmatic-config"));
@@ -126,7 +126,7 @@ fn existing_non_directory_path_is_rejected() {
let fixture = unique_fixture_path("existing-file"); let fixture = unique_fixture_path("existing-file");
let create = std::fs::write(fixture.as_path(), b"fixture"); let create = std::fs::write(fixture.as_path(), b"fixture");
assert!(create.is_ok(), "fixture file should be creatable: {create:?}"); assert!(create.is_ok(), "fixture file should be creatable: {create:?}");
let result = super::ConfigBootstrapOptions::from_paths(fixture.as_path(), "programmatic-schemas"); let result = crate::ConfigBootstrapOptions::from_paths(fixture.as_path(), "programmatic-schemas");
let remove = std::fs::remove_file(fixture.as_path()); let remove = std::fs::remove_file(fixture.as_path());
assert!(remove.is_ok(), "fixture file should be removable: {remove:?}"); assert!(remove.is_ok(), "fixture file should be removable: {remove:?}");
assert!(result.is_err(), "existing file must not be accepted as a bootstrap directory"); assert!(result.is_err(), "existing file must not be accepted as a bootstrap directory");

View File

@@ -1,8 +1,8 @@
// file: crates/ksp-config-lib/unit_tests/composite.rs // file: crates/ksp-config-lib/unit_tests/composite.rs
// version: 3 // version: 4
const TEST_COMPOSITE_FILE_ID: &str = "cfg.composite.test";
const TEST_COMPOSITE_FILENAME: &str = "examples/composite.example.json"; const TEST_COMPOSITE_FILENAME: &str = "examples/composite.example.json";
const TEST_COMPOSITE_FILE_ID: &str = "cfg.composite.test";
#[test] #[test]
fn fixture_composite_example_resolves_default_document_profile() { fn fixture_composite_example_resolves_default_document_profile() {
@@ -197,7 +197,7 @@ fn test_registry(composite_filename: &'static str) -> ksp_core_lib::Result<crate
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
return crate::registry::build_registry([logging, logging_schema, composite_schema, composite]); return crate::build_registry([logging, logging_schema, composite_schema, composite]);
} }
fn prepare_fixture(fixture: &FixtureRoots, composite: &str) -> std::io::Result<()> { fn prepare_fixture(fixture: &FixtureRoots, composite: &str) -> std::io::Result<()> {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/unit_tests/document.rs // file: crates/ksp-config-lib/unit_tests/document.rs
// version: 3 // version: 4
#[test] #[test]
fn committed_logging_document_passes_registered_schema_and_semantic_validation() { fn committed_logging_document_passes_registered_schema_and_semantic_validation() {
@@ -11,7 +11,7 @@ fn committed_logging_document_passes_registered_schema_and_semantic_validation()
assert!(registry.is_ok(), "default registry should be valid: {registry:?}"); assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
assert!(file_id.is_ok(), "logging file_id should be valid: {file_id:?}"); assert!(file_id.is_ok(), "logging file_id should be valid: {file_id:?}");
if let (std::result::Result::Ok(bootstrap), std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (bootstrap, registry, file_id) { if let (std::result::Result::Ok(bootstrap), std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (bootstrap, registry, file_id) {
let engine = super::ConfigDocumentEngine::new(bootstrap, registry); let engine = crate::ConfigDocumentEngine::new(bootstrap, registry);
let document = engine.load_validated_document(&file_id); let document = engine.load_validated_document(&file_id);
assert!(document.is_ok(), "committed std.logging.json should validate: {document:?}"); assert!(document.is_ok(), "committed std.logging.json should validate: {document:?}");
if let std::result::Result::Ok(document) = document { if let std::result::Result::Ok(document) = document {
@@ -228,7 +228,7 @@ fn valid_logging_profile(profile_id: &str, output_id: &str) -> String {
); );
} }
fn load_fixture(fixture: &FixtureRoots) -> ksp_core_lib::Result<super::ConfigJsonDocument> { fn load_fixture(fixture: &FixtureRoots) -> ksp_core_lib::Result<crate::ConfigJsonDocument> {
let bootstrap = crate::ConfigBootstrapOptions::from_paths(fixture.config.as_path(), fixture.schemas.as_path()); let bootstrap = crate::ConfigBootstrapOptions::from_paths(fixture.config.as_path(), fixture.schemas.as_path());
let bootstrap = match bootstrap { let bootstrap = match bootstrap {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -244,7 +244,7 @@ fn load_fixture(fixture: &FixtureRoots) -> ksp_core_lib::Result<super::ConfigJso
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error), std::result::Result::Err(error) => return std::result::Result::Err(error),
}; };
let engine = super::ConfigDocumentEngine::new(bootstrap, registry); let engine = crate::ConfigDocumentEngine::new(bootstrap, registry);
return engine.load_validated_document(&file_id); return engine.load_validated_document(&file_id);
} }
@@ -278,7 +278,7 @@ fn valid_minimal_logging_schema() -> &'static str {
}"#; }"#;
} }
fn assert_error_code(result: ksp_core_lib::Result<super::ConfigJsonDocument>, expected: ksp_core_lib::ErrorCode) { fn assert_error_code(result: ksp_core_lib::Result<crate::ConfigJsonDocument>, expected: ksp_core_lib::ErrorCode) {
assert!(result.is_err(), "fixture should fail with {expected:?}: {result:?}"); assert!(result.is_err(), "fixture should fail with {expected:?}: {result:?}");
if let std::result::Result::Err(error) = result { if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), expected); assert_eq!(error.code(), expected);

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/unit_tests/environment.rs // file: crates/ksp-config-lib/unit_tests/environment.rs
// version: 5 // version: 6
#[test] #[test]
fn process_environment_wins_over_dotenv_and_fallback_even_when_empty() { fn process_environment_wins_over_dotenv_and_fallback_even_when_empty() {
@@ -7,7 +7,7 @@ fn process_environment_wins_over_dotenv_and_fallback_even_when_empty() {
process.insert("KSP_LOGS_DIRECTORY".to_owned(), String::new()); process.insert("KSP_LOGS_DIRECTORY".to_owned(), String::new());
let mut dotenv = std::collections::BTreeMap::<String, String>::new(); let mut dotenv = std::collections::BTreeMap::<String, String>::new();
dotenv.insert("KSP_LOGS_DIRECTORY".to_owned(), "dotenv-logs".to_owned()); dotenv.insert("KSP_LOGS_DIRECTORY".to_owned(), "dotenv-logs".to_owned());
let environment = super::ConfigEnvironment::from_maps(process, dotenv); let environment = crate::ConfigEnvironment::from_maps(process, dotenv);
let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::Some("fallback-logs")); let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::Some("fallback-logs"));
assert!(resolved.is_ok(), "process value should resolve"); assert!(resolved.is_ok(), "process value should resolve");
let resolved = match resolved { let resolved = match resolved {
@@ -15,7 +15,7 @@ fn process_environment_wins_over_dotenv_and_fallback_even_when_empty() {
std::result::Result::Err(_) => return, std::result::Result::Err(_) => return,
}; };
assert_eq!(resolved.value(), ""); assert_eq!(resolved.value(), "");
assert_eq!(resolved.source(), super::ConfigEnvironmentSource::Process); assert_eq!(resolved.source(), crate::ConfigEnvironmentSource::Process);
} }
#[test] #[test]
@@ -23,7 +23,7 @@ fn dotenv_wins_over_fallback_when_process_value_is_absent() {
let process = std::collections::BTreeMap::<String, String>::new(); let process = std::collections::BTreeMap::<String, String>::new();
let mut dotenv = std::collections::BTreeMap::<String, String>::new(); let mut dotenv = std::collections::BTreeMap::<String, String>::new();
dotenv.insert("KSP_LOGS_DIRECTORY".to_owned(), "dotenv-logs".to_owned()); dotenv.insert("KSP_LOGS_DIRECTORY".to_owned(), "dotenv-logs".to_owned());
let environment = super::ConfigEnvironment::from_maps(process, dotenv); let environment = crate::ConfigEnvironment::from_maps(process, dotenv);
let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::Some("fallback-logs")); let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::Some("fallback-logs"));
assert!(resolved.is_ok(), "dotenv value should resolve"); assert!(resolved.is_ok(), "dotenv value should resolve");
let resolved = match resolved { let resolved = match resolved {
@@ -31,25 +31,25 @@ fn dotenv_wins_over_fallback_when_process_value_is_absent() {
std::result::Result::Err(_) => return, std::result::Result::Err(_) => return,
}; };
assert_eq!(resolved.value(), "dotenv-logs"); assert_eq!(resolved.value(), "dotenv-logs");
assert_eq!(resolved.source(), super::ConfigEnvironmentSource::DotEnv); assert_eq!(resolved.source(), crate::ConfigEnvironmentSource::DotEnv);
} }
#[test] #[test]
fn empty_dotenv_value_is_defined_and_beats_fallback() { fn empty_dotenv_value_is_defined_and_beats_fallback() {
let mut dotenv = std::collections::BTreeMap::<String, String>::new(); let mut dotenv = std::collections::BTreeMap::<String, String>::new();
dotenv.insert("KSP_LOGS_DIRECTORY".to_owned(), String::new()); dotenv.insert("KSP_LOGS_DIRECTORY".to_owned(), String::new());
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), dotenv); let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), dotenv);
let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::Some("fallback-logs")); let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::Some("fallback-logs"));
assert!(resolved.is_ok(), "empty dotenv value should resolve"); assert!(resolved.is_ok(), "empty dotenv value should resolve");
if let std::result::Result::Ok(resolved) = resolved { if let std::result::Result::Ok(resolved) = resolved {
assert_eq!(resolved.value(), ""); assert_eq!(resolved.value(), "");
assert_eq!(resolved.source(), super::ConfigEnvironmentSource::DotEnv); assert_eq!(resolved.source(), crate::ConfigEnvironmentSource::DotEnv);
} }
} }
#[test] #[test]
fn fallback_is_used_only_when_external_sources_are_absent() { fn fallback_is_used_only_when_external_sources_are_absent() {
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::Some("fallback-logs")); let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::Some("fallback-logs"));
assert!(resolved.is_ok(), "fallback should resolve"); assert!(resolved.is_ok(), "fallback should resolve");
let resolved = match resolved { let resolved = match resolved {
@@ -57,12 +57,12 @@ fn fallback_is_used_only_when_external_sources_are_absent() {
std::result::Result::Err(_) => return, std::result::Result::Err(_) => return,
}; };
assert_eq!(resolved.value(), "fallback-logs"); assert_eq!(resolved.value(), "fallback-logs");
assert_eq!(resolved.source(), super::ConfigEnvironmentSource::Fallback); assert_eq!(resolved.source(), crate::ConfigEnvironmentSource::Fallback);
} }
#[test] #[test]
fn missing_variable_without_fallback_is_a_distinct_error() { fn missing_variable_without_fallback_is_a_distinct_error() {
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::None); let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::None);
let error = match resolved { let error = match resolved {
std::result::Result::Ok(_) => return, std::result::Result::Ok(_) => return,
@@ -78,7 +78,7 @@ fn ksp_and_kspb_namespaces_are_supported_but_external_names_are_rejected() {
process.insert("KSP_LOGS_DIRECTORY".to_owned(), "logs".to_owned()); process.insert("KSP_LOGS_DIRECTORY".to_owned(), "logs".to_owned());
let bot_variable = ["KSPB_", "TEST_KEY"].concat(); let bot_variable = ["KSPB_", "TEST_KEY"].concat();
process.insert(bot_variable.clone(), "hidden".to_owned()); process.insert(bot_variable.clone(), "hidden".to_owned());
let environment = super::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new()); let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
assert!(environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::None).is_ok()); assert!(environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::None).is_ok());
assert!(environment.resolve_variable(bot_variable.as_str(), std::option::Option::None).is_ok()); assert!(environment.resolve_variable(bot_variable.as_str(), std::option::Option::None).is_ok());
let external = environment.resolve_variable("OTHER_NETWORK", std::option::Option::None); let external = environment.resolve_variable("OTHER_NETWORK", std::option::Option::None);
@@ -91,7 +91,7 @@ fn ksp_and_kspb_namespaces_are_supported_but_external_names_are_rejected() {
#[test] #[test]
fn text_resolver_supports_multiple_placeholders_and_literal_fallbacks() { fn text_resolver_supports_multiple_placeholders_and_literal_fallbacks() {
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let resolved = environment.resolve_text("logs=${KSP_LOGS_DIRECTORY:-logs};second=${KSP_LOGS_DIRECTORY:-other}"); let resolved = environment.resolve_text("logs=${KSP_LOGS_DIRECTORY:-logs};second=${KSP_LOGS_DIRECTORY:-other}");
assert!(resolved.is_ok(), "multiple placeholders should resolve"); assert!(resolved.is_ok(), "multiple placeholders should resolve");
if let std::result::Result::Ok(resolved) = resolved { if let std::result::Result::Ok(resolved) = resolved {
@@ -101,7 +101,7 @@ fn text_resolver_supports_multiple_placeholders_and_literal_fallbacks() {
#[test] #[test]
fn malformed_or_nested_placeholders_are_rejected() { fn malformed_or_nested_placeholders_are_rejected() {
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let unclosed = environment.resolve_text("${KSP_LOGS_DIRECTORY"); let unclosed = environment.resolve_text("${KSP_LOGS_DIRECTORY");
let unclosed = match unclosed { let unclosed = match unclosed {
std::result::Result::Ok(_) => return, std::result::Result::Ok(_) => return,
@@ -120,7 +120,7 @@ fn malformed_or_nested_placeholders_are_rejected() {
fn json_resolver_walks_objects_and_arrays_without_changing_keys() { fn json_resolver_walks_objects_and_arrays_without_changing_keys() {
let mut dotenv = std::collections::BTreeMap::<String, String>::new(); let mut dotenv = std::collections::BTreeMap::<String, String>::new();
dotenv.insert("KSP_LOGS_DIRECTORY".to_owned(), "runtime-logs".to_owned()); dotenv.insert("KSP_LOGS_DIRECTORY".to_owned(), "runtime-logs".to_owned());
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), dotenv); let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), dotenv);
let source = serde_json::json!({"path": "${KSP_LOGS_DIRECTORY}", "items": [1, "${KSP_LOGS_DIRECTORY}"], "enabled": true}); let source = serde_json::json!({"path": "${KSP_LOGS_DIRECTORY}", "items": [1, "${KSP_LOGS_DIRECTORY}"], "enabled": true});
let resolved = environment.resolve_json(&source); let resolved = environment.resolve_json(&source);
assert!(resolved.is_ok(), "recursive JSON resolution should succeed"); assert!(resolved.is_ok(), "recursive JSON resolution should succeed");
@@ -137,7 +137,7 @@ fn json_resolver_walks_objects_and_arrays_without_changing_keys() {
fn dotenv_parser_supports_comments_export_quotes_empty_values_and_ignores_external_keys() { fn dotenv_parser_supports_comments_export_quotes_empty_values_and_ignores_external_keys() {
let path = std::path::Path::new("fixture.env"); let path = std::path::Path::new("fixture.env");
let content = "# comment\nexport KSP_LOGS_DIRECTORY = 'quoted logs'\nOTHER_TOOL=value\n"; let content = "# comment\nexport KSP_LOGS_DIRECTORY = 'quoted logs'\nOTHER_TOOL=value\n";
let parsed = super::parse_dotenv_content(path, content); let parsed = crate::parse_dotenv_content(path, content);
assert!(parsed.is_ok(), "dotenv fixture should parse"); assert!(parsed.is_ok(), "dotenv fixture should parse");
let parsed = match parsed { let parsed = match parsed {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -149,7 +149,7 @@ fn dotenv_parser_supports_comments_export_quotes_empty_values_and_ignores_extern
#[test] #[test]
fn dotenv_duplicate_ksp_key_is_rejected() { fn dotenv_duplicate_ksp_key_is_rejected() {
let parsed = super::parse_dotenv_content(std::path::Path::new("fixture.env"), "KSP_LOGS_DIRECTORY=one\nKSP_LOGS_DIRECTORY=two\n"); let parsed = crate::parse_dotenv_content(std::path::Path::new("fixture.env"), "KSP_LOGS_DIRECTORY=one\nKSP_LOGS_DIRECTORY=two\n");
let error = match parsed { let error = match parsed {
std::result::Result::Ok(_) => return, std::result::Result::Ok(_) => return,
std::result::Result::Err(error) => error, std::result::Result::Err(error) => error,
@@ -199,7 +199,7 @@ fn logging_fixture_profile_resolves_environment_fallback_without_changing_source
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return, std::result::Result::Err(_) => return,
}; };
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let effective = profile.resolve_effective_environment(&environment); let effective = profile.resolve_effective_environment(&environment);
assert!(effective.is_ok(), "committed Logging environment fallback should resolve"); assert!(effective.is_ok(), "committed Logging environment fallback should resolve");
let effective = match effective { let effective = match effective {
@@ -245,7 +245,7 @@ fn secret_environment_value_keeps_real_value_but_redacts_safe_and_debug_views()
let canary = "KSP_SECRET_CANARY_91b7c6"; let canary = "KSP_SECRET_CANARY_91b7c6";
let mut process = std::collections::BTreeMap::<String, String>::new(); let mut process = std::collections::BTreeMap::<String, String>::new();
process.insert("KSP_SECRET_TEST_TOKEN".to_owned(), canary.to_owned()); process.insert("KSP_SECRET_TEST_TOKEN".to_owned(), canary.to_owned());
let environment = super::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new()); let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
let resolved = environment.resolve_variable("KSP_SECRET_TEST_TOKEN", std::option::Option::None); let resolved = environment.resolve_variable("KSP_SECRET_TEST_TOKEN", std::option::Option::None);
assert!(resolved.is_ok(), "secret process value should resolve"); assert!(resolved.is_ok(), "secret process value should resolve");
let resolved = match resolved { let resolved = match resolved {
@@ -255,7 +255,7 @@ fn secret_environment_value_keeps_real_value_but_redacts_safe_and_debug_views()
assert_eq!(resolved.value(), canary); assert_eq!(resolved.value(), canary);
assert_eq!(resolved.safe_value(), crate::REDACTED_CONFIG_VALUE); assert_eq!(resolved.safe_value(), crate::REDACTED_CONFIG_VALUE);
assert_eq!(resolved.sensitivity(), crate::ConfigSensitivity::Secret); assert_eq!(resolved.sensitivity(), crate::ConfigSensitivity::Secret);
assert_eq!(resolved.source(), super::ConfigEnvironmentSource::Process); assert_eq!(resolved.source(), crate::ConfigEnvironmentSource::Process);
let debug = format!("{resolved:?}"); let debug = format!("{resolved:?}");
assert!(!debug.contains(canary), "Debug must not reveal the secret canary"); assert!(!debug.contains(canary), "Debug must not reveal the secret canary");
assert!(debug.contains(crate::REDACTED_CONFIG_VALUE)); assert!(debug.contains(crate::REDACTED_CONFIG_VALUE));
@@ -267,7 +267,7 @@ fn detailed_text_redacts_only_secret_segments_and_keeps_ordered_provenance() {
let mut process = std::collections::BTreeMap::<String, String>::new(); let mut process = std::collections::BTreeMap::<String, String>::new();
process.insert("KSP_PUBLIC_HOST".to_owned(), "rpc.example.test".to_owned()); process.insert("KSP_PUBLIC_HOST".to_owned(), "rpc.example.test".to_owned());
process.insert("KSP_SECRET_TOKEN".to_owned(), secret.to_owned()); process.insert("KSP_SECRET_TOKEN".to_owned(), secret.to_owned());
let environment = super::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new()); let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
let resolved = environment.resolve_text_detailed("https://${KSP_PUBLIC_HOST}/?token=${KSP_SECRET_TOKEN}"); let resolved = environment.resolve_text_detailed("https://${KSP_PUBLIC_HOST}/?token=${KSP_SECRET_TOKEN}");
assert!(resolved.is_ok(), "composed secret URL should resolve"); assert!(resolved.is_ok(), "composed secret URL should resolve");
let resolved = match resolved { let resolved = match resolved {
@@ -288,7 +288,7 @@ fn detailed_text_redacts_only_secret_segments_and_keeps_ordered_provenance() {
#[test] #[test]
fn secret_fallback_inherits_secret_sensitivity_and_is_redacted() { fn secret_fallback_inherits_secret_sensitivity_and_is_redacted() {
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let resolved = environment.resolve_text_detailed("token=${KSP_SECRET_TOKEN:-false-secret}"); let resolved = environment.resolve_text_detailed("token=${KSP_SECRET_TOKEN:-false-secret}");
assert!(resolved.is_ok(), "secret fallback should resolve"); assert!(resolved.is_ok(), "secret fallback should resolve");
let resolved = match resolved { let resolved = match resolved {
@@ -298,7 +298,7 @@ fn secret_fallback_inherits_secret_sensitivity_and_is_redacted() {
assert_eq!(resolved.value(), "token=false-secret"); assert_eq!(resolved.value(), "token=false-secret");
assert_eq!(resolved.safe_value(), "token=********"); assert_eq!(resolved.safe_value(), "token=********");
assert_eq!(resolved.sensitivity(), crate::ConfigSensitivity::Secret); assert_eq!(resolved.sensitivity(), crate::ConfigSensitivity::Secret);
assert_eq!(resolved.provenance()[1].environment_source(), std::option::Option::Some(super::ConfigEnvironmentSource::Fallback)); assert_eq!(resolved.provenance()[1].environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::Fallback));
} }
#[test] #[test]
@@ -306,7 +306,7 @@ fn detailed_json_preserves_safe_tree_sensitivity_and_pointer_provenance() {
let secret = "nested-secret-canary-2d11"; let secret = "nested-secret-canary-2d11";
let mut dotenv = std::collections::BTreeMap::<String, String>::new(); let mut dotenv = std::collections::BTreeMap::<String, String>::new();
dotenv.insert("KSP_SECRET_TOKEN".to_owned(), secret.to_owned()); dotenv.insert("KSP_SECRET_TOKEN".to_owned(), secret.to_owned());
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), dotenv); let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), dotenv);
let source = serde_json::json!({"transport": {"url": "https://host/?token=${KSP_SECRET_TOKEN}"}, "items": ["plain", 7]}); let source = serde_json::json!({"transport": {"url": "https://host/?token=${KSP_SECRET_TOKEN}"}, "items": ["plain", 7]});
let resolved = environment.resolve_json_detailed(&source); let resolved = environment.resolve_json_detailed(&source);
assert!(resolved.is_ok(), "detailed JSON should resolve"); assert!(resolved.is_ok(), "detailed JSON should resolve");
@@ -349,7 +349,7 @@ fn detailed_fixture_profile_environment_keeps_global_origin_and_adds_environment
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return, std::result::Result::Err(_) => return,
}; };
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let effective = profile.resolve_effective_environment_detailed(&environment); let effective = profile.resolve_effective_environment_detailed(&environment);
assert!(effective.is_ok(), "detailed committed Logging profile should resolve"); assert!(effective.is_ok(), "detailed committed Logging profile should resolve");
let effective = match effective { let effective = match effective {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/unit_tests/profile.rs // file: crates/ksp-config-lib/unit_tests/profile.rs
// version: 2 // version: 3
#[test] #[test]
fn fixture_default_profile_resolves_globals_profile_and_provenance() { fn fixture_default_profile_resolves_globals_profile_and_provenance() {
@@ -15,9 +15,9 @@ fn fixture_default_profile_resolves_globals_profile_and_provenance() {
if let (std::result::Result::Ok(document), std::result::Result::Ok(resolved)) = (document, resolved) { if let (std::result::Result::Ok(document), std::result::Result::Ok(resolved)) = (document, resolved) {
let default_profile = document.value().get("default_profile").and_then(serde_json::Value::as_str); let default_profile = document.value().get("default_profile").and_then(serde_json::Value::as_str);
assert_eq!(default_profile, std::option::Option::Some(resolved.profile_id())); assert_eq!(default_profile, std::option::Option::Some(resolved.profile_id()));
assert_eq!(resolved.selection_source(), super::ConfigProfileSelectionSource::DefaultProfile); assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::DefaultProfile);
assert_eq!(resolved.origin("logs_directory"), std::option::Option::Some(super::ConfigValueOrigin::Global)); assert_eq!(resolved.origin("logs_directory"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
assert_eq!(resolved.origin("default_filter"), std::option::Option::Some(super::ConfigValueOrigin::Profile)); assert_eq!(resolved.origin("default_filter"), std::option::Option::Some(crate::ConfigValueOrigin::Profile));
assert_eq!( assert_eq!(
resolved.profile().get("default_filter").and_then(serde_json::Value::as_str), resolved.profile().get("default_filter").and_then(serde_json::Value::as_str),
resolved.effective().get("default_filter").and_then(serde_json::Value::as_str), resolved.effective().get("default_filter").and_then(serde_json::Value::as_str),
@@ -45,7 +45,7 @@ fn explicit_profile_selection_is_distinct_from_default_selection() {
assert!(resolved.is_ok(), "explicit committed profile should resolve: {resolved:?}"); assert!(resolved.is_ok(), "explicit committed profile should resolve: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved { if let std::result::Result::Ok(resolved) = resolved {
assert_eq!(resolved.profile_id(), profile_id); assert_eq!(resolved.profile_id(), profile_id);
assert_eq!(resolved.selection_source(), super::ConfigProfileSelectionSource::Explicit); assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::Explicit);
} }
} }
} }

View File

@@ -1,49 +1,49 @@
// file: crates/ksp-config-lib/unit_tests/registry.rs // file: crates/ksp-config-lib/unit_tests/registry.rs
// version: 5 // version: 6
#[test] #[test]
fn descriptors_expose_complete_registry_in_deterministic_file_id_order() { fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
let registry = super::ConfigFileRegistry::defaults(); let registry = crate::ConfigFileRegistry::defaults();
assert!(registry.is_ok(), "default registry should be valid: {registry:?}"); assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
if let std::result::Result::Ok(registry) = registry { if let std::result::Result::Ok(registry) = registry {
let descriptors: std::vec::Vec<&super::ConfigFileDescriptor> = registry.descriptors().collect(); let descriptors: std::vec::Vec<&crate::ConfigFileDescriptor> = registry.descriptors().collect();
assert_eq!(descriptors.len(), 5); assert_eq!(descriptors.len(), 5);
assert_eq!(descriptors[0].file_id().as_str(), super::FILE_ID_STD_LOGGING); assert_eq!(descriptors[0].file_id().as_str(), crate::FILE_ID_STD_LOGGING);
assert_eq!(descriptors[0].kind(), super::ConfigFileKind::Config); assert_eq!(descriptors[0].kind(), crate::ConfigFileKind::Config);
assert_eq!(descriptors[0].filename(), std::path::Path::new(super::DEFAULT_STD_LOGGING_FILENAME)); assert_eq!(descriptors[0].filename(), std::path::Path::new(crate::DEFAULT_STD_LOGGING_FILENAME));
let logging_schema_file_id = descriptors[0].schema_file_id(); let logging_schema_file_id = descriptors[0].schema_file_id();
assert!(logging_schema_file_id.is_some(), "logging descriptor should expose its validation schema"); assert!(logging_schema_file_id.is_some(), "logging descriptor should expose its validation schema");
if let std::option::Option::Some(schema_file_id) = logging_schema_file_id { if let std::option::Option::Some(schema_file_id) = logging_schema_file_id {
assert_eq!(schema_file_id.as_str(), super::FILE_ID_SCHEMA_STD_LOGGING); assert_eq!(schema_file_id.as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
} }
assert_eq!(descriptors[1].file_id().as_str(), super::FILE_ID_STD_TRANSPORT); assert_eq!(descriptors[1].file_id().as_str(), crate::FILE_ID_STD_TRANSPORT);
assert_eq!(descriptors[1].kind(), super::ConfigFileKind::Config); assert_eq!(descriptors[1].kind(), crate::ConfigFileKind::Config);
assert_eq!(descriptors[1].filename(), std::path::Path::new(super::DEFAULT_STD_TRANSPORT_FILENAME)); assert_eq!(descriptors[1].filename(), std::path::Path::new(crate::DEFAULT_STD_TRANSPORT_FILENAME));
let transport_schema_file_id = descriptors[1].schema_file_id(); let transport_schema_file_id = descriptors[1].schema_file_id();
assert!(transport_schema_file_id.is_some(), "transport descriptor should expose its validation schema"); assert!(transport_schema_file_id.is_some(), "transport descriptor should expose its validation schema");
if let std::option::Option::Some(schema_file_id) = transport_schema_file_id { if let std::option::Option::Some(schema_file_id) = transport_schema_file_id {
assert_eq!(schema_file_id.as_str(), super::FILE_ID_SCHEMA_STD_TRANSPORT); assert_eq!(schema_file_id.as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT);
} }
assert_eq!(descriptors[2].file_id().as_str(), super::FILE_ID_SCHEMA_COMPOSITE); assert_eq!(descriptors[2].file_id().as_str(), crate::FILE_ID_SCHEMA_COMPOSITE);
assert_eq!(descriptors[2].kind(), super::ConfigFileKind::Schema); assert_eq!(descriptors[2].kind(), crate::ConfigFileKind::Schema);
assert_eq!(descriptors[3].file_id().as_str(), super::FILE_ID_SCHEMA_STD_LOGGING); assert_eq!(descriptors[3].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
assert_eq!(descriptors[3].kind(), super::ConfigFileKind::Schema); assert_eq!(descriptors[3].kind(), crate::ConfigFileKind::Schema);
assert_eq!(descriptors[4].file_id().as_str(), super::FILE_ID_SCHEMA_STD_TRANSPORT); assert_eq!(descriptors[4].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT);
assert_eq!(descriptors[4].kind(), super::ConfigFileKind::Schema); assert_eq!(descriptors[4].kind(), crate::ConfigFileKind::Schema);
} }
} }
#[test] #[test]
fn descriptors_reflect_filename_overrides_without_changing_logical_metadata() { fn descriptors_reflect_filename_overrides_without_changing_logical_metadata() {
let registry = super::ConfigFileRegistry::defaults(); let registry = crate::ConfigFileRegistry::defaults();
let file_id = super::ConfigFileId::new(super::FILE_ID_STD_LOGGING); let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
assert!(registry.is_ok(), "default registry should be valid: {registry:?}"); assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
assert!(file_id.is_ok(), "logging file_id should be valid: {file_id:?}"); assert!(file_id.is_ok(), "logging file_id should be valid: {file_id:?}");
if let (std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (registry, file_id) { if let (std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (registry, file_id) {
let overridden = registry.with_filename_override(&file_id, "profiles/desktop.logging.json"); let overridden = registry.with_filename_override(&file_id, "profiles/desktop.logging.json");
assert!(overridden.is_ok(), "filename override should remain valid: {overridden:?}"); assert!(overridden.is_ok(), "filename override should remain valid: {overridden:?}");
if let std::result::Result::Ok(overridden) = overridden { if let std::result::Result::Ok(overridden) = overridden {
let mut found: std::option::Option<&super::ConfigFileDescriptor> = std::option::Option::None; let mut found: std::option::Option<&crate::ConfigFileDescriptor> = std::option::Option::None;
for descriptor in overridden.descriptors() { for descriptor in overridden.descriptors() {
if descriptor.file_id() == &file_id { if descriptor.file_id() == &file_id {
found = std::option::Option::Some(descriptor); found = std::option::Option::Some(descriptor);
@@ -52,12 +52,12 @@ fn descriptors_reflect_filename_overrides_without_changing_logical_metadata() {
assert!(found.is_some(), "public descriptor inventory should retain the overridden logging descriptor"); assert!(found.is_some(), "public descriptor inventory should retain the overridden logging descriptor");
if let std::option::Option::Some(descriptor) = found { if let std::option::Option::Some(descriptor) = found {
assert_eq!(descriptor.file_id(), &file_id); assert_eq!(descriptor.file_id(), &file_id);
assert_eq!(descriptor.kind(), super::ConfigFileKind::Config); assert_eq!(descriptor.kind(), crate::ConfigFileKind::Config);
assert_eq!(descriptor.filename(), std::path::Path::new("profiles/desktop.logging.json")); assert_eq!(descriptor.filename(), std::path::Path::new("profiles/desktop.logging.json"));
let schema_file_id = descriptor.schema_file_id(); let schema_file_id = descriptor.schema_file_id();
assert!(schema_file_id.is_some(), "filename override should preserve schema association"); assert!(schema_file_id.is_some(), "filename override should preserve schema association");
if let std::option::Option::Some(schema_file_id) = schema_file_id { if let std::option::Option::Some(schema_file_id) = schema_file_id {
assert_eq!(schema_file_id.as_str(), super::FILE_ID_SCHEMA_STD_LOGGING); assert_eq!(schema_file_id.as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
} }
} }
} }
@@ -66,11 +66,11 @@ fn descriptors_reflect_filename_overrides_without_changing_logical_metadata() {
#[test] #[test]
fn defaults_register_logging_document_and_schema_with_distinct_roots() { fn defaults_register_logging_document_and_schema_with_distinct_roots() {
let registry = super::ConfigFileRegistry::defaults(); let registry = crate::ConfigFileRegistry::defaults();
assert!(registry.is_ok(), "default registry should be valid: {registry:?}"); assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
if let std::result::Result::Ok(registry) = registry { if let std::result::Result::Ok(registry) = registry {
let logging_id = super::ConfigFileId::new(super::FILE_ID_STD_LOGGING); let logging_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
let schema_id = super::ConfigFileId::new(super::FILE_ID_SCHEMA_STD_LOGGING); let schema_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_STD_LOGGING);
assert!(logging_id.is_ok(), "logging file_id should be valid: {logging_id:?}"); assert!(logging_id.is_ok(), "logging file_id should be valid: {logging_id:?}");
assert!(schema_id.is_ok(), "logging schema file_id should be valid: {schema_id:?}"); assert!(schema_id.is_ok(), "logging schema file_id should be valid: {schema_id:?}");
if let (std::result::Result::Ok(logging_id), std::result::Result::Ok(schema_id)) = (logging_id, schema_id) { if let (std::result::Result::Ok(logging_id), std::result::Result::Ok(schema_id)) = (logging_id, schema_id) {
@@ -79,15 +79,15 @@ fn defaults_register_logging_document_and_schema_with_distinct_roots() {
assert!(logging.is_ok(), "logging descriptor should exist: {logging:?}"); assert!(logging.is_ok(), "logging descriptor should exist: {logging:?}");
assert!(schema.is_ok(), "logging schema descriptor should exist: {schema:?}"); assert!(schema.is_ok(), "logging schema descriptor should exist: {schema:?}");
if let (std::result::Result::Ok(logging), std::result::Result::Ok(schema)) = (logging, schema) { if let (std::result::Result::Ok(logging), std::result::Result::Ok(schema)) = (logging, schema) {
assert_eq!(logging.kind(), super::ConfigFileKind::Config); assert_eq!(logging.kind(), crate::ConfigFileKind::Config);
assert_eq!(logging.filename(), std::path::Path::new(super::DEFAULT_STD_LOGGING_FILENAME)); assert_eq!(logging.filename(), std::path::Path::new(crate::DEFAULT_STD_LOGGING_FILENAME));
let logging_schema = logging.schema_file_id(); let logging_schema = logging.schema_file_id();
assert!(logging_schema.is_some(), "logging document should declare its validation schema"); assert!(logging_schema.is_some(), "logging document should declare its validation schema");
if let std::option::Option::Some(logging_schema) = logging_schema { if let std::option::Option::Some(logging_schema) = logging_schema {
assert_eq!(logging_schema, &schema_id); assert_eq!(logging_schema, &schema_id);
} }
assert_eq!(schema.kind(), super::ConfigFileKind::Schema); assert_eq!(schema.kind(), crate::ConfigFileKind::Schema);
assert_eq!(schema.filename(), std::path::Path::new(super::DEFAULT_STD_LOGGING_SCHEMA_FILENAME)); assert_eq!(schema.filename(), std::path::Path::new(crate::DEFAULT_STD_LOGGING_SCHEMA_FILENAME));
} }
} }
} }
@@ -95,11 +95,11 @@ fn defaults_register_logging_document_and_schema_with_distinct_roots() {
#[test] #[test]
fn defaults_register_transport_document_and_schema_with_distinct_roots() { fn defaults_register_transport_document_and_schema_with_distinct_roots() {
let registry = super::ConfigFileRegistry::defaults(); let registry = crate::ConfigFileRegistry::defaults();
assert!(registry.is_ok(), "default registry should be valid: {registry:?}"); assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
if let std::result::Result::Ok(registry) = registry { if let std::result::Result::Ok(registry) = registry {
let transport_id = super::ConfigFileId::new(super::FILE_ID_STD_TRANSPORT); let transport_id = crate::ConfigFileId::new(crate::FILE_ID_STD_TRANSPORT);
let schema_id = super::ConfigFileId::new(super::FILE_ID_SCHEMA_STD_TRANSPORT); let schema_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_STD_TRANSPORT);
assert!(transport_id.is_ok(), "transport file_id should be valid: {transport_id:?}"); assert!(transport_id.is_ok(), "transport file_id should be valid: {transport_id:?}");
assert!(schema_id.is_ok(), "transport schema file_id should be valid: {schema_id:?}"); assert!(schema_id.is_ok(), "transport schema file_id should be valid: {schema_id:?}");
if let (std::result::Result::Ok(transport_id), std::result::Result::Ok(schema_id)) = (transport_id, schema_id) { if let (std::result::Result::Ok(transport_id), std::result::Result::Ok(schema_id)) = (transport_id, schema_id) {
@@ -108,11 +108,11 @@ fn defaults_register_transport_document_and_schema_with_distinct_roots() {
assert!(transport.is_ok(), "transport descriptor should exist: {transport:?}"); assert!(transport.is_ok(), "transport descriptor should exist: {transport:?}");
assert!(schema.is_ok(), "transport schema descriptor should exist: {schema:?}"); assert!(schema.is_ok(), "transport schema descriptor should exist: {schema:?}");
if let (std::result::Result::Ok(transport), std::result::Result::Ok(schema)) = (transport, schema) { if let (std::result::Result::Ok(transport), std::result::Result::Ok(schema)) = (transport, schema) {
assert_eq!(transport.kind(), super::ConfigFileKind::Config); assert_eq!(transport.kind(), crate::ConfigFileKind::Config);
assert_eq!(transport.filename(), std::path::Path::new(super::DEFAULT_STD_TRANSPORT_FILENAME)); assert_eq!(transport.filename(), std::path::Path::new(crate::DEFAULT_STD_TRANSPORT_FILENAME));
assert_eq!(transport.schema_file_id(), std::option::Option::Some(&schema_id)); assert_eq!(transport.schema_file_id(), std::option::Option::Some(&schema_id));
assert_eq!(schema.kind(), super::ConfigFileKind::Schema); assert_eq!(schema.kind(), crate::ConfigFileKind::Schema);
assert_eq!(schema.filename(), std::path::Path::new(super::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME)); assert_eq!(schema.filename(), std::path::Path::new(crate::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME));
} }
} }
} }
@@ -120,13 +120,13 @@ fn defaults_register_transport_document_and_schema_with_distinct_roots() {
#[test] #[test]
fn resolve_path_uses_descriptor_kind_to_select_bootstrap_root() { fn resolve_path_uses_descriptor_kind_to_select_bootstrap_root() {
let registry = super::ConfigFileRegistry::defaults(); let registry = crate::ConfigFileRegistry::defaults();
let bootstrap = crate::ConfigBootstrapOptions::from_paths("runtime-config", "runtime-schemas"); let bootstrap = crate::ConfigBootstrapOptions::from_paths("runtime-config", "runtime-schemas");
assert!(registry.is_ok(), "default registry should be valid: {registry:?}"); assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
assert!(bootstrap.is_ok(), "bootstrap paths should be valid: {bootstrap:?}"); assert!(bootstrap.is_ok(), "bootstrap paths should be valid: {bootstrap:?}");
if let (std::result::Result::Ok(registry), std::result::Result::Ok(bootstrap)) = (registry, bootstrap) { if let (std::result::Result::Ok(registry), std::result::Result::Ok(bootstrap)) = (registry, bootstrap) {
let logging_id = super::ConfigFileId::new(super::FILE_ID_STD_LOGGING); let logging_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
let schema_id = super::ConfigFileId::new(super::FILE_ID_SCHEMA_STD_LOGGING); let schema_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_STD_LOGGING);
if let (std::result::Result::Ok(logging_id), std::result::Result::Ok(schema_id)) = (logging_id, schema_id) { if let (std::result::Result::Ok(logging_id), std::result::Result::Ok(schema_id)) = (logging_id, schema_id) {
let logging = registry.resolve_path(&bootstrap, &logging_id); let logging = registry.resolve_path(&bootstrap, &logging_id);
let schema = registry.resolve_path(&bootstrap, &schema_id); let schema = registry.resolve_path(&bootstrap, &schema_id);
@@ -150,16 +150,16 @@ fn cli_filemap_override_replaces_filename_and_last_value_wins() {
std::ffi::OsString::from("--other-option"), std::ffi::OsString::from("--other-option"),
std::ffi::OsString::from("--filemap=cfg.std.logging=profiles/custom.logging.json"), std::ffi::OsString::from("--filemap=cfg.std.logging=profiles/custom.logging.json"),
]; ];
let registry = super::ConfigFileRegistry::from_args(&args); let registry = crate::ConfigFileRegistry::from_args(&args);
assert!(registry.is_ok(), "filemap overrides should parse: {registry:?}"); assert!(registry.is_ok(), "filemap overrides should parse: {registry:?}");
if let std::result::Result::Ok(registry) = registry { if let std::result::Result::Ok(registry) = registry {
let file_id = super::ConfigFileId::new(super::FILE_ID_STD_LOGGING); let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
if let std::result::Result::Ok(file_id) = file_id { if let std::result::Result::Ok(file_id) = file_id {
let descriptor = registry.descriptor(&file_id); let descriptor = registry.descriptor(&file_id);
assert!(descriptor.is_ok(), "logging descriptor should remain registered: {descriptor:?}"); assert!(descriptor.is_ok(), "logging descriptor should remain registered: {descriptor:?}");
if let std::result::Result::Ok(descriptor) = descriptor { if let std::result::Result::Ok(descriptor) = descriptor {
assert_eq!(descriptor.filename(), std::path::Path::new("profiles/custom.logging.json")); assert_eq!(descriptor.filename(), std::path::Path::new("profiles/custom.logging.json"));
assert_eq!(descriptor.kind(), super::ConfigFileKind::Config); assert_eq!(descriptor.kind(), crate::ConfigFileKind::Config);
} }
} }
} }
@@ -167,8 +167,8 @@ fn cli_filemap_override_replaces_filename_and_last_value_wins() {
#[test] #[test]
fn programmatic_override_preserves_file_id_and_kind() { fn programmatic_override_preserves_file_id_and_kind() {
let registry = super::ConfigFileRegistry::defaults(); let registry = crate::ConfigFileRegistry::defaults();
let file_id = super::ConfigFileId::new(super::FILE_ID_SCHEMA_STD_LOGGING); let file_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_STD_LOGGING);
assert!(registry.is_ok(), "default registry should be valid: {registry:?}"); assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
assert!(file_id.is_ok(), "schema file_id should be valid: {file_id:?}"); assert!(file_id.is_ok(), "schema file_id should be valid: {file_id:?}");
if let (std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (registry, file_id) { if let (std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (registry, file_id) {
@@ -178,7 +178,7 @@ fn programmatic_override_preserves_file_id_and_kind() {
let descriptor = overridden.descriptor(&file_id); let descriptor = overridden.descriptor(&file_id);
if let std::result::Result::Ok(descriptor) = descriptor { if let std::result::Result::Ok(descriptor) = descriptor {
assert_eq!(descriptor.file_id(), &file_id); assert_eq!(descriptor.file_id(), &file_id);
assert_eq!(descriptor.kind(), super::ConfigFileKind::Schema); assert_eq!(descriptor.kind(), crate::ConfigFileKind::Schema);
assert_eq!(descriptor.filename(), std::path::Path::new("alternate/logging.schema.json")); assert_eq!(descriptor.filename(), std::path::Path::new("alternate/logging.schema.json"));
} }
} }
@@ -188,7 +188,7 @@ fn programmatic_override_preserves_file_id_and_kind() {
#[test] #[test]
fn unknown_file_id_override_is_rejected() { fn unknown_file_id_override_is_rejected() {
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--filemap=cfg.unknown=unknown.json")]; let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--filemap=cfg.unknown=unknown.json")];
let result = super::ConfigFileRegistry::from_args(&args); let result = crate::ConfigFileRegistry::from_args(&args);
assert!(result.is_err(), "unknown logical files must not be introduced by CLI override"); assert!(result.is_err(), "unknown logical files must not be introduced by CLI override");
if let std::result::Result::Err(error) = result { if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_UNKNOWN); assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_UNKNOWN);
@@ -198,7 +198,7 @@ fn unknown_file_id_override_is_rejected() {
#[test] #[test]
fn invalid_file_ids_are_rejected() { fn invalid_file_ids_are_rejected() {
for value in ["", ".cfg", "cfg.", "cfg..logging", "CFG.logging", "cfg/logging"] { for value in ["", ".cfg", "cfg.", "cfg..logging", "CFG.logging", "cfg/logging"] {
let result = super::ConfigFileId::new(value); let result = crate::ConfigFileId::new(value);
assert!(result.is_err(), "invalid file_id must be rejected: {value}"); assert!(result.is_err(), "invalid file_id must be rejected: {value}");
if let std::result::Result::Err(error) = result { if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_INVALID); assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_INVALID);
@@ -208,8 +208,8 @@ fn invalid_file_ids_are_rejected() {
#[test] #[test]
fn absolute_and_traversing_filenames_are_rejected() { fn absolute_and_traversing_filenames_are_rejected() {
let registry = super::ConfigFileRegistry::defaults(); let registry = crate::ConfigFileRegistry::defaults();
let file_id = super::ConfigFileId::new(super::FILE_ID_STD_LOGGING); let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
assert!(registry.is_ok(), "default registry should be valid: {registry:?}"); assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
assert!(file_id.is_ok(), "logging file_id should be valid: {file_id:?}"); assert!(file_id.is_ok(), "logging file_id should be valid: {file_id:?}");
if let (std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (registry, file_id) { if let (std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (registry, file_id) {
@@ -226,7 +226,7 @@ fn absolute_and_traversing_filenames_are_rejected() {
fn malformed_filemap_arguments_are_rejected() { fn malformed_filemap_arguments_are_rejected() {
for argument in ["--filemap", "--filemap=cfg.std.logging", "--filemap==logging.json", "--filemap=cfg.std.logging="] { for argument in ["--filemap", "--filemap=cfg.std.logging", "--filemap==logging.json", "--filemap=cfg.std.logging="] {
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from(argument)]; let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from(argument)];
let result = super::ConfigFileRegistry::from_args(&args); let result = crate::ConfigFileRegistry::from_args(&args);
assert!(result.is_err(), "malformed filemap argument must be rejected: {argument}"); assert!(result.is_err(), "malformed filemap argument must be rejected: {argument}");
if let std::result::Result::Err(error) = result { if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_FILE_MAPPING_INVALID); assert_eq!(error.code(), crate::ERROR_CODE_FILE_MAPPING_INVALID);
@@ -236,12 +236,12 @@ fn malformed_filemap_arguments_are_rejected() {
#[test] #[test]
fn duplicate_registry_ids_are_rejected() { fn duplicate_registry_ids_are_rejected() {
let first = super::ConfigFileDescriptor::new("cfg.duplicate", super::ConfigFileKind::Config, "first.json", std::option::Option::None); let first = crate::ConfigFileDescriptor::new("cfg.duplicate", crate::ConfigFileKind::Config, "first.json", std::option::Option::None);
let second = super::ConfigFileDescriptor::new("cfg.duplicate", super::ConfigFileKind::Config, "second.json", std::option::Option::None); let second = crate::ConfigFileDescriptor::new("cfg.duplicate", crate::ConfigFileKind::Config, "second.json", std::option::Option::None);
assert!(first.is_ok(), "first descriptor should be valid: {first:?}"); assert!(first.is_ok(), "first descriptor should be valid: {first:?}");
assert!(second.is_ok(), "second descriptor should be valid: {second:?}"); assert!(second.is_ok(), "second descriptor should be valid: {second:?}");
if let (std::result::Result::Ok(first), std::result::Result::Ok(second)) = (first, second) { if let (std::result::Result::Ok(first), std::result::Result::Ok(second)) = (first, second) {
let result = super::build_registry([first, second]); let result = crate::build_registry([first, second]);
assert!(result.is_err(), "duplicate file_ids must be rejected"); assert!(result.is_err(), "duplicate file_ids must be rejected");
if let std::result::Result::Err(error) = result { if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_DUPLICATE); assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_DUPLICATE);
@@ -251,7 +251,7 @@ fn duplicate_registry_ids_are_rejected() {
#[test] #[test]
fn descriptor_kind_must_match_file_id_namespace() { fn descriptor_kind_must_match_file_id_namespace() {
let result = super::ConfigFileDescriptor::new("schema.invalid-kind", super::ConfigFileKind::Config, "invalid.json", std::option::Option::None); let result = crate::ConfigFileDescriptor::new("schema.invalid-kind", crate::ConfigFileKind::Config, "invalid.json", std::option::Option::None);
assert!(result.is_err(), "descriptor kind mismatch must be rejected"); assert!(result.is_err(), "descriptor kind mismatch must be rejected");
if let std::result::Result::Err(error) = result { if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_FILE_MAPPING_INVALID); assert_eq!(error.code(), crate::ERROR_CODE_FILE_MAPPING_INVALID);
@@ -260,10 +260,10 @@ fn descriptor_kind_must_match_file_id_namespace() {
#[test] #[test]
fn config_schema_association_must_reference_registered_schema_descriptor() { fn config_schema_association_must_reference_registered_schema_descriptor() {
let config = super::ConfigFileDescriptor::new("cfg.test", super::ConfigFileKind::Config, "test.json", std::option::Option::Some("schema.test")); let config = crate::ConfigFileDescriptor::new("cfg.test", crate::ConfigFileKind::Config, "test.json", std::option::Option::Some("schema.test"));
assert!(config.is_ok(), "config descriptor should be valid before registry association validation: {config:?}"); assert!(config.is_ok(), "config descriptor should be valid before registry association validation: {config:?}");
if let std::result::Result::Ok(config) = config { if let std::result::Result::Ok(config) = config {
let result = super::build_registry([config]); let result = crate::build_registry([config]);
assert!(result.is_err(), "registry must reject a missing schema association"); assert!(result.is_err(), "registry must reject a missing schema association");
if let std::result::Result::Err(error) = result { if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_FILE_MAPPING_INVALID); assert_eq!(error.code(), crate::ERROR_CODE_FILE_MAPPING_INVALID);
@@ -286,9 +286,9 @@ fn absolute_fixture_path() -> std::path::PathBuf {
#[test] #[test]
fn defaults_register_generic_composite_schema_without_runtime_composite() { fn defaults_register_generic_composite_schema_without_runtime_composite() {
let registry = super::ConfigFileRegistry::defaults(); let registry = crate::ConfigFileRegistry::defaults();
let schema_id = super::ConfigFileId::new(super::FILE_ID_SCHEMA_COMPOSITE); let schema_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_COMPOSITE);
let runtime_id = super::ConfigFileId::new("cfg.composite.ksp-app-wallet-desk"); let runtime_id = crate::ConfigFileId::new("cfg.composite.ksp-app-wallet-desk");
assert!(registry.is_ok(), "default registry should be valid: {registry:?}"); assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
assert!(schema_id.is_ok(), "composite schema file_id should be valid: {schema_id:?}"); assert!(schema_id.is_ok(), "composite schema file_id should be valid: {schema_id:?}");
assert!(runtime_id.is_ok(), "future composite runtime file_id syntax should be valid: {runtime_id:?}"); assert!(runtime_id.is_ok(), "future composite runtime file_id syntax should be valid: {runtime_id:?}");
@@ -298,8 +298,8 @@ fn defaults_register_generic_composite_schema_without_runtime_composite() {
assert!(schema.is_ok(), "generic composite schema should be registered: {schema:?}"); assert!(schema.is_ok(), "generic composite schema should be registered: {schema:?}");
assert!(runtime.is_err(), "no fictitious runtime composite should be registered"); assert!(runtime.is_err(), "no fictitious runtime composite should be registered");
if let std::result::Result::Ok(schema) = schema { if let std::result::Result::Ok(schema) = schema {
assert_eq!(schema.kind(), super::ConfigFileKind::Schema); assert_eq!(schema.kind(), crate::ConfigFileKind::Schema);
assert_eq!(schema.filename(), std::path::Path::new(super::DEFAULT_COMPOSITE_SCHEMA_FILENAME)); assert_eq!(schema.filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_SCHEMA_FILENAME));
} }
if let std::result::Result::Err(error) = runtime { if let std::result::Result::Err(error) = runtime {
assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_UNKNOWN); assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_UNKNOWN);

View File

@@ -1,31 +1,31 @@
// file: crates/ksp-config-lib/unit_tests/sensitivity.rs // file: crates/ksp-config-lib/unit_tests/sensitivity.rs
// version: 1 // version: 2
#[test] #[test]
fn environment_names_map_to_expected_sensitivity() { fn environment_names_map_to_expected_sensitivity() {
assert_eq!(super::ConfigSensitivity::from_variable_name("KSP_PUBLIC_ENDPOINT").ok(), std::option::Option::Some(super::ConfigSensitivity::Public)); assert_eq!(crate::ConfigSensitivity::from_variable_name("KSP_PUBLIC_ENDPOINT").ok(), std::option::Option::Some(crate::ConfigSensitivity::Public));
assert_eq!(super::ConfigSensitivity::from_variable_name("KSP_MODE").ok(), std::option::Option::Some(super::ConfigSensitivity::Internal)); assert_eq!(crate::ConfigSensitivity::from_variable_name("KSP_MODE").ok(), std::option::Option::Some(crate::ConfigSensitivity::Internal));
assert_eq!(super::ConfigSensitivity::from_variable_name("KSP_SECRET_PASSWORD").ok(), std::option::Option::Some(super::ConfigSensitivity::Secret)); assert_eq!(crate::ConfigSensitivity::from_variable_name("KSP_SECRET_PASSWORD").ok(), std::option::Option::Some(crate::ConfigSensitivity::Secret));
assert_eq!(super::ConfigSensitivity::from_variable_name("KSPB_PUBLIC_ENDPOINT").ok(), std::option::Option::Some(super::ConfigSensitivity::Public)); assert_eq!(crate::ConfigSensitivity::from_variable_name("KSPB_PUBLIC_ENDPOINT").ok(), std::option::Option::Some(crate::ConfigSensitivity::Public));
assert_eq!(super::ConfigSensitivity::from_variable_name("KSPB_MODE").ok(), std::option::Option::Some(super::ConfigSensitivity::Internal)); assert_eq!(crate::ConfigSensitivity::from_variable_name("KSPB_MODE").ok(), std::option::Option::Some(crate::ConfigSensitivity::Internal));
assert_eq!(super::ConfigSensitivity::from_variable_name("KSPB_SECRET_PASSWORD").ok(), std::option::Option::Some(super::ConfigSensitivity::Secret)); assert_eq!(crate::ConfigSensitivity::from_variable_name("KSPB_SECRET_PASSWORD").ok(), std::option::Option::Some(crate::ConfigSensitivity::Secret));
} }
#[test] #[test]
fn strongest_sensitivity_follows_secret_internal_public_order() { fn strongest_sensitivity_follows_secret_internal_public_order() {
assert_eq!(super::ConfigSensitivity::Public.strongest(super::ConfigSensitivity::Internal), super::ConfigSensitivity::Internal); assert_eq!(crate::ConfigSensitivity::Public.strongest(crate::ConfigSensitivity::Internal), crate::ConfigSensitivity::Internal);
assert_eq!(super::ConfigSensitivity::Internal.strongest(super::ConfigSensitivity::Secret), super::ConfigSensitivity::Secret); assert_eq!(crate::ConfigSensitivity::Internal.strongest(crate::ConfigSensitivity::Secret), crate::ConfigSensitivity::Secret);
assert_eq!(super::ConfigSensitivity::Secret.strongest(super::ConfigSensitivity::Public), super::ConfigSensitivity::Secret); assert_eq!(crate::ConfigSensitivity::Secret.strongest(crate::ConfigSensitivity::Public), crate::ConfigSensitivity::Secret);
} }
#[test] #[test]
fn provenance_exposes_names_and_sources_without_values() { fn provenance_exposes_names_and_sources_without_values() {
let process = super::ConfigValueProvenance::EnvironmentProcess { variable_name: "KSP_SECRET_TOKEN".to_owned() }; let process = crate::ConfigValueProvenance::EnvironmentProcess { variable_name: "KSP_SECRET_TOKEN".to_owned() };
let dotenv = super::ConfigValueProvenance::EnvironmentDotEnv { variable_name: "KSP_MODE".to_owned() }; let dotenv = crate::ConfigValueProvenance::EnvironmentDotEnv { variable_name: "KSP_MODE".to_owned() };
let fallback = super::ConfigValueProvenance::EnvironmentFallback { variable_name: "KSP_PUBLIC_HOST".to_owned() }; let fallback = crate::ConfigValueProvenance::EnvironmentFallback { variable_name: "KSP_PUBLIC_HOST".to_owned() };
assert_eq!(process.variable_name(), std::option::Option::Some("KSP_SECRET_TOKEN")); assert_eq!(process.variable_name(), std::option::Option::Some("KSP_SECRET_TOKEN"));
assert_eq!(process.environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::Process)); assert_eq!(process.environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::Process));
assert_eq!(dotenv.environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::DotEnv)); assert_eq!(dotenv.environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::DotEnv));
assert_eq!(fallback.environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::Fallback)); assert_eq!(fallback.environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::Fallback));
assert_eq!(super::ConfigValueProvenance::DocumentLiteral.variable_name(), std::option::Option::None); assert_eq!(crate::ConfigValueProvenance::DocumentLiteral.variable_name(), std::option::Option::None);
} }

View File

@@ -1,5 +1,6 @@
// file: crates/ksp-core-lib/src/lib.rs // file: crates/ksp-core-lib/src/lib.rs
// version: 6 // version: 8
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-core-lib/src/program_ids.rs // file: crates/ksp-core-lib/src/program_ids.rs
// version: 2 // version: 4
const DOMAIN_SOLANA: &str = "solana"; const DOMAIN_SOLANA: &str = "solana";
const FAMILY_CONSENSUS: &str = "consensus"; const FAMILY_CONSENSUS: &str = "consensus";
@@ -266,6 +266,12 @@ impl<'a> ProgramIdFilter<'a> {
} }
} }
/// Returns the canonical KSP Program ID registry.
#[must_use]
pub const fn entries() -> &'static [crate::ProgramIdEntry] {
return PROGRAM_ID_ENTRIES;
}
const PROGRAM_ID_ENTRIES: &[crate::ProgramIdEntry] = &[ const PROGRAM_ID_ENTRIES: &[crate::ProgramIdEntry] = &[
crate::ProgramIdEntry::new( crate::ProgramIdEntry::new(
"solana.address_lookup_table", "solana.address_lookup_table",
@@ -438,12 +444,6 @@ const PROGRAM_ID_ENTRIES: &[crate::ProgramIdEntry] = &[
), ),
]; ];
/// Returns the canonical KSP Program ID registry.
#[must_use]
pub const fn entries() -> &'static [crate::ProgramIdEntry] {
return PROGRAM_ID_ENTRIES;
}
/// Returns a lazy view of Program IDs matching all configured filter axes. /// Returns a lazy view of Program IDs matching all configured filter axes.
pub fn program_ids<'a>(filter: crate::ProgramIdFilter<'a>) -> impl std::iter::Iterator<Item = &'static crate::ProgramIdEntry> + 'a { pub fn program_ids<'a>(filter: crate::ProgramIdFilter<'a>) -> impl std::iter::Iterator<Item = &'static crate::ProgramIdEntry> + 'a {
return PROGRAM_ID_ENTRIES.iter().filter(move |entry| { return PROGRAM_ID_ENTRIES.iter().filter(move |entry| {

View File

@@ -1,37 +1,15 @@
// file: crates/ksp-logging-lib/src/domain.rs // file: crates/ksp-logging-lib/src/domain.rs
// version: 1 // version: 3
std::thread_local! { std::thread_local! {
static CURRENT_DOMAIN: std::cell::RefCell<std::option::Option<std::string::String>> = const { std::cell::RefCell::new(std::option::Option::None) }; static CURRENT_DOMAIN: std::cell::RefCell<std::option::Option<std::string::String>> = const { std::cell::RefCell::new(std::option::Option::None) };
} }
#[derive(Clone, Debug, Default, Eq, PartialEq)] /// Crate-internal `DomainContextLayer` state shared across the owning crate.
struct SpanDomain {
value: std::option::Option<std::string::String>,
}
#[derive(Default)]
struct DomainVisitor {
value: std::option::Option<std::string::String>,
}
impl tracing::field::Visit for DomainVisitor {
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
if field.name() == "domain" {
self.value = std::option::Option::Some(value.to_string());
}
}
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
if field.name() == "domain" {
self.value = std::option::Option::Some(format!("{value:?}"));
}
}
}
pub(crate) struct DomainContextLayer; pub(crate) struct DomainContextLayer;
impl DomainContextLayer { impl DomainContextLayer {
/// Creates a new `DomainContextLayer` value.
pub(crate) const fn new() -> Self { pub(crate) const fn new() -> Self {
return Self; return Self;
} }
@@ -99,6 +77,31 @@ where
} }
} }
#[derive(Clone, Debug, Default, Eq, PartialEq)]
struct SpanDomain {
value: std::option::Option<std::string::String>,
}
#[derive(Default)]
struct DomainVisitor {
value: std::option::Option<std::string::String>,
}
impl tracing::field::Visit for DomainVisitor {
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
if field.name() == "domain" {
self.value = std::option::Option::Some(value.to_string());
}
}
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
if field.name() == "domain" {
self.value = std::option::Option::Some(format!("{value:?}"));
}
}
}
/// Executes the crate-internal current domain matches operation for the owning module.
pub(crate) fn current_domain_matches(selectors: &[std::string::String]) -> bool { pub(crate) fn current_domain_matches(selectors: &[std::string::String]) -> bool {
if let [selector] = selectors if let [selector] = selectors
&& selector == "*" && selector == "*"

View File

@@ -1,13 +1,13 @@
// file: crates/ksp-logging-lib/src/error.rs // file: crates/ksp-logging-lib/src/error.rs
// version: 4 // version: 5
/// Error code used when runtime logging settings are invalid.
pub const ERROR_CODE_INVALID_SETTINGS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "invalid_settings");
/// Error code used when a global logging subscriber is already installed. /// Error code used when a global logging subscriber is already installed.
pub const ERROR_CODE_ALREADY_INITIALIZED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "already_initialized"); pub const ERROR_CODE_ALREADY_INITIALIZED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "already_initialized");
/// Error code used when a hot reload cannot replace the active runtime layers.
pub const ERROR_CODE_RELOAD_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "reload_failed");
/// Error code used when the rolling file output cannot be initialized. /// Error code used when the rolling file output cannot be initialized.
pub const ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "file_output_initialization_failed"); pub const ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "file_output_initialization_failed");
/// Error code used when an application Logging runtime identity is invalid. /// Error code used when an application Logging runtime identity is invalid.
pub const ERROR_CODE_INVALID_RUNTIME_IDENTITY: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "invalid_runtime_identity"); pub const ERROR_CODE_INVALID_RUNTIME_IDENTITY: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "invalid_runtime_identity");
/// Error code used when runtime logging settings are invalid.
pub const ERROR_CODE_INVALID_SETTINGS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "invalid_settings");
/// Error code used when a hot reload cannot replace the active runtime layers.
pub const ERROR_CODE_RELOAD_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "reload_failed");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/src/identity.rs // file: crates/ksp-logging-lib/src/identity.rs
// version: 1 // version: 2
//! Stable runtime identity used to separate persistent file outputs between application launches. //! Stable runtime identity used to separate persistent file outputs between application launches.
@@ -41,6 +41,7 @@ impl LoggingRuntimeIdentity {
return self.launch_timestamp.as_str(); return self.launch_timestamp.as_str();
} }
/// Executes the crate-internal file name prefix operation for `LoggingRuntimeIdentity`.
pub(crate) fn file_name_prefix(&self, configured_prefix: &str) -> std::string::String { pub(crate) fn file_name_prefix(&self, configured_prefix: &str) -> std::string::String {
return format!("{}.{}.{}", self.application_id, self.launch_timestamp, configured_prefix); return format!("{}.{}.{}", self.application_id, self.launch_timestamp, configured_prefix);
} }

View File

@@ -1,5 +1,6 @@
// file: crates/ksp-logging-lib/src/lib.rs // file: crates/ksp-logging-lib/src/lib.rs
// version: 8 // version: 10
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
@@ -69,6 +70,17 @@ pub use self::span::Span;
/// Instruments an asynchronous future with a KSP span. /// Instruments an asynchronous future with a KSP span.
pub use self::span::instrument; pub use self::span::instrument;
/// Crate-internal `DomainContextLayer` state shared across the owning crate.
pub(crate) use self::domain::DomainContextLayer;
/// Executes the crate-internal current domain matches operation for the owning module.
pub(crate) use self::domain::current_domain_matches;
/// Crate-internal `RouteMakeWriter` state shared across the owning crate.
pub(crate) use self::writer::RouteMakeWriter;
/// Crate-internal `RoutedWriter` variants used by the owning crate.
pub(crate) use self::writer::RoutedWriter;
/// Crate-internal `StripAnsiWriter` state shared across the owning crate.
pub(crate) use self::writer::StripAnsiWriter;
/// Hidden tracing crate bridge used exclusively by exported KSP logging macros.
#[doc(hidden)] #[doc(hidden)]
/// Internal macro bridge. KSP consumers must not use this reexport directly. pub extern crate tracing;
pub extern crate tracing as __private_tracing;

View File

@@ -1,11 +1,11 @@
// file: crates/ksp-logging-lib/src/macros.rs // file: crates/ksp-logging-lib/src/macros.rs
// version: 1 // version: 2
/// Emits a KSP error event with an explicit owning target. /// Emits a KSP error event with an explicit owning target.
#[macro_export] #[macro_export]
macro_rules! error { macro_rules! error {
(target: $target:expr, $($argument:tt)+) => {{ (target: $target:expr, $($argument:tt)+) => {{
$crate::__private_tracing::error!(target: $target, $($argument)+); $crate::tracing::error!(target: $target, $($argument)+);
}}; }};
} }
@@ -13,7 +13,7 @@ macro_rules! error {
#[macro_export] #[macro_export]
macro_rules! warn { macro_rules! warn {
(target: $target:expr, $($argument:tt)+) => {{ (target: $target:expr, $($argument:tt)+) => {{
$crate::__private_tracing::warn!(target: $target, $($argument)+); $crate::tracing::warn!(target: $target, $($argument)+);
}}; }};
} }
@@ -21,7 +21,7 @@ macro_rules! warn {
#[macro_export] #[macro_export]
macro_rules! info { macro_rules! info {
(target: $target:expr, $($argument:tt)+) => {{ (target: $target:expr, $($argument:tt)+) => {{
$crate::__private_tracing::info!(target: $target, $($argument)+); $crate::tracing::info!(target: $target, $($argument)+);
}}; }};
} }
@@ -29,7 +29,7 @@ macro_rules! info {
#[macro_export] #[macro_export]
macro_rules! debug { macro_rules! debug {
(target: $target:expr, $($argument:tt)+) => {{ (target: $target:expr, $($argument:tt)+) => {{
$crate::__private_tracing::debug!(target: $target, $($argument)+); $crate::tracing::debug!(target: $target, $($argument)+);
}}; }};
} }
@@ -37,7 +37,7 @@ macro_rules! debug {
#[macro_export] #[macro_export]
macro_rules! trace { macro_rules! trace {
(target: $target:expr, $($argument:tt)+) => {{ (target: $target:expr, $($argument:tt)+) => {{
$crate::__private_tracing::trace!(target: $target, $($argument)+); $crate::tracing::trace!(target: $target, $($argument)+);
}}; }};
} }
@@ -45,10 +45,10 @@ macro_rules! trace {
#[macro_export] #[macro_export]
macro_rules! error_span { macro_rules! error_span {
(target: $target:expr, $name:expr) => {{ (target: $target:expr, $name:expr) => {{
$crate::Span::__from_tracing($crate::__private_tracing::error_span!(target: $target, $name)) $crate::Span::__from_tracing($crate::tracing::error_span!(target: $target, $name))
}}; }};
(target: $target:expr, $name:expr, $($field:tt)+) => {{ (target: $target:expr, $name:expr, $($field:tt)+) => {{
$crate::Span::__from_tracing($crate::__private_tracing::error_span!(target: $target, $name, $($field)+)) $crate::Span::__from_tracing($crate::tracing::error_span!(target: $target, $name, $($field)+))
}}; }};
} }
@@ -56,10 +56,10 @@ macro_rules! error_span {
#[macro_export] #[macro_export]
macro_rules! warn_span { macro_rules! warn_span {
(target: $target:expr, $name:expr) => {{ (target: $target:expr, $name:expr) => {{
$crate::Span::__from_tracing($crate::__private_tracing::warn_span!(target: $target, $name)) $crate::Span::__from_tracing($crate::tracing::warn_span!(target: $target, $name))
}}; }};
(target: $target:expr, $name:expr, $($field:tt)+) => {{ (target: $target:expr, $name:expr, $($field:tt)+) => {{
$crate::Span::__from_tracing($crate::__private_tracing::warn_span!(target: $target, $name, $($field)+)) $crate::Span::__from_tracing($crate::tracing::warn_span!(target: $target, $name, $($field)+))
}}; }};
} }
@@ -67,10 +67,10 @@ macro_rules! warn_span {
#[macro_export] #[macro_export]
macro_rules! info_span { macro_rules! info_span {
(target: $target:expr, $name:expr) => {{ (target: $target:expr, $name:expr) => {{
$crate::Span::__from_tracing($crate::__private_tracing::info_span!(target: $target, $name)) $crate::Span::__from_tracing($crate::tracing::info_span!(target: $target, $name))
}}; }};
(target: $target:expr, $name:expr, $($field:tt)+) => {{ (target: $target:expr, $name:expr, $($field:tt)+) => {{
$crate::Span::__from_tracing($crate::__private_tracing::info_span!(target: $target, $name, $($field)+)) $crate::Span::__from_tracing($crate::tracing::info_span!(target: $target, $name, $($field)+))
}}; }};
} }
@@ -78,10 +78,10 @@ macro_rules! info_span {
#[macro_export] #[macro_export]
macro_rules! debug_span { macro_rules! debug_span {
(target: $target:expr, $name:expr) => {{ (target: $target:expr, $name:expr) => {{
$crate::Span::__from_tracing($crate::__private_tracing::debug_span!(target: $target, $name)) $crate::Span::__from_tracing($crate::tracing::debug_span!(target: $target, $name))
}}; }};
(target: $target:expr, $name:expr, $($field:tt)+) => {{ (target: $target:expr, $name:expr, $($field:tt)+) => {{
$crate::Span::__from_tracing($crate::__private_tracing::debug_span!(target: $target, $name, $($field)+)) $crate::Span::__from_tracing($crate::tracing::debug_span!(target: $target, $name, $($field)+))
}}; }};
} }
@@ -89,9 +89,9 @@ macro_rules! debug_span {
#[macro_export] #[macro_export]
macro_rules! trace_span { macro_rules! trace_span {
(target: $target:expr, $name:expr) => {{ (target: $target:expr, $name:expr) => {{
$crate::Span::__from_tracing($crate::__private_tracing::trace_span!(target: $target, $name)) $crate::Span::__from_tracing($crate::tracing::trace_span!(target: $target, $name))
}}; }};
(target: $target:expr, $name:expr, $($field:tt)+) => {{ (target: $target:expr, $name:expr, $($field:tt)+) => {{
$crate::Span::__from_tracing($crate::__private_tracing::trace_span!(target: $target, $name, $($field)+)) $crate::Span::__from_tracing($crate::tracing::trace_span!(target: $target, $name, $($field)+))
}}; }};
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/src/runtime.rs // file: crates/ksp-logging-lib/src/runtime.rs
// version: 13 // version: 16
use tracing_subscriber::Layer; // rust-rules: trait-import use tracing_subscriber::Layer; // rust-rules: trait-import
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
@@ -228,31 +228,6 @@ pub fn initialize_with_identity(settings: &crate::LoggingSettings, identity: &cr
return initialize_runtime(settings, std::option::Option::Some(identity.clone())); return initialize_runtime(settings, std::option::Option::Some(identity.clone()));
} }
fn initialize_runtime(
settings: &crate::LoggingSettings,
runtime_identity: std::option::Option<crate::LoggingRuntimeIdentity>,
) -> ksp_core_lib::Result<crate::LoggingGuard> {
return prepare_runtime_with_identity(settings, runtime_identity.as_ref()).and_then(|prepared| -> ksp_core_lib::Result<crate::LoggingGuard> {
let PreparedRuntime { layers, outputs } = prepared;
let (reload_layer, reload_handle) = tracing_subscriber::reload::Layer::new(layers);
let subscriber = tracing_subscriber::registry().with(reload_layer);
let install_result = tracing::subscriber::set_global_default(subscriber);
return match install_result {
std::result::Result::Ok(()) => std::result::Result::Ok(crate::LoggingGuard {
reload_handle,
settings: settings.clone(),
runtime_identity,
outputs,
retired_dropped_lines: crate::DroppedLines::zero(),
retired_file_dropped_lines: std::collections::HashMap::new(),
}),
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_ALREADY_INITIALIZED, "the global KSP tracing subscriber is already installed").with_source(error),
),
};
});
}
/// Replaces the active KSP logging settings and non-blocking outputs without reinstalling the global subscriber. /// Replaces the active KSP logging settings and non-blocking outputs without reinstalling the global subscriber.
/// ///
/// New runtime layers, writers and guards are fully prepared before the reload is attempted. If validation or preparation fails, the currently active /// New runtime layers, writers and guards are fully prepared before the reload is attempted. If validation or preparation fails, the currently active
@@ -282,6 +257,31 @@ pub fn reinitialize(guard: &mut crate::LoggingGuard, settings: &crate::LoggingSe
}); });
} }
fn initialize_runtime(
settings: &crate::LoggingSettings,
runtime_identity: std::option::Option<crate::LoggingRuntimeIdentity>,
) -> ksp_core_lib::Result<crate::LoggingGuard> {
return prepare_runtime_with_identity(settings, runtime_identity.as_ref()).and_then(|prepared| -> ksp_core_lib::Result<crate::LoggingGuard> {
let PreparedRuntime { layers, outputs } = prepared;
let (reload_layer, reload_handle) = tracing_subscriber::reload::Layer::new(layers);
let subscriber = tracing_subscriber::registry().with(reload_layer);
let install_result = tracing::subscriber::set_global_default(subscriber);
return match install_result {
std::result::Result::Ok(()) => std::result::Result::Ok(crate::LoggingGuard {
reload_handle,
settings: settings.clone(),
runtime_identity,
outputs,
retired_dropped_lines: crate::DroppedLines::zero(),
retired_file_dropped_lines: std::collections::HashMap::new(),
}),
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_ALREADY_INITIALIZED, "the global KSP tracing subscriber is already installed").with_source(error),
),
};
});
}
fn prepare_runtime_with_identity( fn prepare_runtime_with_identity(
settings: &crate::LoggingSettings, settings: &crate::LoggingSettings,
runtime_identity: std::option::Option<&crate::LoggingRuntimeIdentity>, runtime_identity: std::option::Option<&crate::LoggingRuntimeIdentity>,
@@ -315,7 +315,7 @@ fn prepare_runtime_with_identity(
if output_layers.is_empty() { if output_layers.is_empty() {
return std::result::Result::Ok(PreparedRuntime { layers: RuntimeLayers::new(), outputs }); return std::result::Result::Ok(PreparedRuntime { layers: RuntimeLayers::new(), outputs });
} }
output_layers.insert(0, crate::domain::DomainContextLayer::new().boxed()); output_layers.insert(0, crate::DomainContextLayer::new().boxed());
let takeover_layer = build_target_filter(settings).and_then(output_layers).boxed(); let takeover_layer = build_target_filter(settings).and_then(output_layers).boxed();
return std::result::Result::Ok(PreparedRuntime { layers: vec![takeover_layer], outputs }); return std::result::Result::Ok(PreparedRuntime { layers: vec![takeover_layer], outputs });
} }
@@ -368,7 +368,7 @@ fn build_file_output(
); );
}, },
}; };
let stripped_writer = crate::writer::StripAnsiWriter::new(appender); let stripped_writer = crate::StripAnsiWriter::new(appender);
let thread_name = format!("ksp-logging-{}", file.output_id()); let thread_name = format!("ksp-logging-{}", file.output_id());
let prepared = build_non_blocking_output(stripped_writer, thread_name.as_str(), settings.span_events(), false, false, file.format(), file.filter()); let prepared = build_non_blocking_output(stripped_writer, thread_name.as_str(), settings.span_events(), false, false, file.format(), file.filter());
let metadata = crate::RuntimeFileMetadata { let metadata = crate::RuntimeFileMetadata {
@@ -413,7 +413,7 @@ fn build_format_layer(
let span_events = map_span_events(span_events); let span_events = map_span_events(span_events);
return match format { return match format {
crate::LogFormat::Human => tracing_subscriber::fmt::layer() crate::LogFormat::Human => tracing_subscriber::fmt::layer()
.with_writer(crate::writer::RouteMakeWriter::new(writer, filter.clone())) .with_writer(crate::RouteMakeWriter::new(writer, filter.clone()))
.with_ansi(ansi) .with_ansi(ansi)
.with_ansi_sanitization(ansi_sanitization) .with_ansi_sanitization(ansi_sanitization)
.with_target(true) .with_target(true)
@@ -423,7 +423,7 @@ fn build_format_layer(
.boxed(), .boxed(),
crate::LogFormat::Compact => tracing_subscriber::fmt::layer() crate::LogFormat::Compact => tracing_subscriber::fmt::layer()
.compact() .compact()
.with_writer(crate::writer::RouteMakeWriter::new(writer, filter.clone())) .with_writer(crate::RouteMakeWriter::new(writer, filter.clone()))
.with_ansi(ansi) .with_ansi(ansi)
.with_ansi_sanitization(ansi_sanitization) .with_ansi_sanitization(ansi_sanitization)
.with_target(true) .with_target(true)
@@ -433,7 +433,7 @@ fn build_format_layer(
.boxed(), .boxed(),
crate::LogFormat::Pretty => tracing_subscriber::fmt::layer() crate::LogFormat::Pretty => tracing_subscriber::fmt::layer()
.pretty() .pretty()
.with_writer(crate::writer::RouteMakeWriter::new(writer, filter.clone())) .with_writer(crate::RouteMakeWriter::new(writer, filter.clone()))
.with_ansi(ansi) .with_ansi(ansi)
.with_ansi_sanitization(ansi_sanitization) .with_ansi_sanitization(ansi_sanitization)
.with_target(true) .with_target(true)
@@ -443,7 +443,7 @@ fn build_format_layer(
.boxed(), .boxed(),
crate::LogFormat::Json => tracing_subscriber::fmt::layer() crate::LogFormat::Json => tracing_subscriber::fmt::layer()
.json() .json()
.with_writer(crate::writer::RouteMakeWriter::new(writer, filter.clone())) .with_writer(crate::RouteMakeWriter::new(writer, filter.clone()))
.with_ansi(false) .with_ansi(false)
.with_target(true) .with_target(true)
.with_file(true) .with_file(true)

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/src/writer.rs // file: crates/ksp-logging-lib/src/writer.rs
// version: 3 // version: 5
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum StripAnsiState { enum StripAnsiState {
@@ -12,12 +12,14 @@ enum StripAnsiState {
StringEscape, StringEscape,
} }
/// Crate-internal `StripAnsiWriter` state shared across the owning crate.
pub(crate) struct StripAnsiWriter<W> { pub(crate) struct StripAnsiWriter<W> {
inner: W, inner: W,
state: StripAnsiState, state: StripAnsiState,
} }
impl<W> StripAnsiWriter<W> { impl<W> StripAnsiWriter<W> {
/// Creates a new `StripAnsiWriter` value.
pub(crate) const fn new(inner: W) -> Self { pub(crate) const fn new(inner: W) -> Self {
return Self { inner, state: StripAnsiState::Text }; return Self { inner, state: StripAnsiState::Text };
} }
@@ -100,6 +102,7 @@ impl<W> StripAnsiWriter<W> {
} }
} }
/// Crate-internal `RouteMakeWriter` state shared across the owning crate.
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct RouteMakeWriter<W> { pub(crate) struct RouteMakeWriter<W> {
inner: W, inner: W,
@@ -107,17 +110,19 @@ pub(crate) struct RouteMakeWriter<W> {
} }
impl<W> RouteMakeWriter<W> { impl<W> RouteMakeWriter<W> {
/// Creates a new `RouteMakeWriter` value.
pub(crate) fn new(inner: W, filter: crate::OutputFilter) -> Self { pub(crate) fn new(inner: W, filter: crate::OutputFilter) -> Self {
return Self { inner, filter }; return Self { inner, filter };
} }
} }
/// Crate-internal `RoutedWriter` variants used by the owning crate.
pub(crate) enum RoutedWriter<W> { pub(crate) enum RoutedWriter<W> {
Enabled(W), Enabled(W),
Disabled, Disabled,
} }
impl<W> std::io::Write for RoutedWriter<W> impl<W> std::io::Write for crate::RoutedWriter<W>
where where
W: std::io::Write, W: std::io::Write,
{ {
@@ -140,17 +145,17 @@ impl<'writer, W> tracing_subscriber::fmt::MakeWriter<'writer> for RouteMakeWrite
where where
W: tracing_subscriber::fmt::MakeWriter<'writer>, W: tracing_subscriber::fmt::MakeWriter<'writer>,
{ {
type Writer = RoutedWriter<W::Writer>; type Writer = crate::RoutedWriter<W::Writer>;
fn make_writer(&'writer self) -> Self::Writer { fn make_writer(&'writer self) -> Self::Writer {
return RoutedWriter::Enabled(tracing_subscriber::fmt::MakeWriter::make_writer(&self.inner)); return crate::RoutedWriter::Enabled(tracing_subscriber::fmt::MakeWriter::make_writer(&self.inner));
} }
fn make_writer_for(&'writer self, metadata: &tracing::Metadata<'_>) -> Self::Writer { fn make_writer_for(&'writer self, metadata: &tracing::Metadata<'_>) -> Self::Writer {
if metadata_matches_filter(metadata, &self.filter) { if metadata_matches_filter(metadata, &self.filter) {
return RoutedWriter::Enabled(tracing_subscriber::fmt::MakeWriter::make_writer_for(&self.inner, metadata)); return crate::RoutedWriter::Enabled(tracing_subscriber::fmt::MakeWriter::make_writer_for(&self.inner, metadata));
} }
return RoutedWriter::Disabled; return crate::RoutedWriter::Disabled;
} }
} }
@@ -163,7 +168,7 @@ fn metadata_matches_filter(metadata: &tracing::Metadata<'_>, filter: &crate::Out
}) { }) {
return false; return false;
} }
return crate::domain::current_domain_matches(filter.domains()); return crate::current_domain_matches(filter.domains());
} }
fn level_is_enabled(level: &tracing::Level, filter: crate::LogFilterLevel) -> bool { fn level_is_enabled(level: &tracing::Level, filter: crate::LogFilterLevel) -> bool {

View File

@@ -1,12 +1,12 @@
// file: crates/ksp-logging-lib/tests/overhead.rs // file: crates/ksp-logging-lib/tests/overhead.rs
// version: 1 // version: 2
//! Diagnostic gross-overhead probe for the reload layer used by KSP Logging. //! Diagnostic gross-overhead probe for the reload layer used by KSP Logging.
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
const TEST_TARGET: &str = "ksp-logging-lib";
const ITERATIONS: u64 = 200_000; const ITERATIONS: u64 = 200_000;
const TEST_TARGET: &str = "ksp-logging-lib";
fn emit_probe_events() { fn emit_probe_events() {
for sequence in 0..ITERATIONS { for sequence in 0..ITERATIONS {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/tests/ownership.rs // file: crates/ksp-logging-lib/tests/ownership.rs
// version: 2 // version: 3
//! Integration audit ensuring KSP crates do not bypass the logging facade. //! Integration audit ensuring KSP crates do not bypass the logging facade.
@@ -157,6 +157,11 @@ fn workspace_crates_do_not_bypass_ksp_logging_facade() {
std::result::Result::Err(_) => continue, std::result::Result::Err(_) => continue,
}; };
assert!(!source_uses_direct_path(source.as_str(), "tracing::"), "{} bypasses ksp-logging-lib via tracing", rust_file.display()); assert!(!source_uses_direct_path(source.as_str(), "tracing::"), "{} bypasses ksp-logging-lib via tracing", rust_file.display());
assert!(
!source_uses_direct_path(source.as_str(), "ksp_logging_lib::tracing::"),
"{} bypasses ksp-logging-lib via its hidden tracing bridge",
rust_file.display(),
);
assert!( assert!(
!source_uses_direct_path(source.as_str(), "tracing_subscriber::"), !source_uses_direct_path(source.as_str(), "tracing_subscriber::"),
"{} bypasses ksp-logging-lib via tracing-subscriber", "{} bypasses ksp-logging-lib via tracing-subscriber",

View File

@@ -1,12 +1,12 @@
// file: crates/ksp-logging-lib/tests/runtime.rs // file: crates/ksp-logging-lib/tests/runtime.rs
// version: 9 // version: 10
//! Integration tests for global initialization, takeover filtering, non-blocking outputs and hot reload. //! Integration tests for global initialization, takeover filtering, non-blocking outputs and hot reload.
const EXTERNAL_TARGET: &str = "sqlx";
const JSON_KSP_TARGET: &str = "ksp-logging-json-test";
const LOGGING_TARGET: &str = "ksp-logging-lib"; const LOGGING_TARGET: &str = "ksp-logging-lib";
const OTHER_KSP_TARGET: &str = "ksp-store-lib"; const OTHER_KSP_TARGET: &str = "ksp-store-lib";
const JSON_KSP_TARGET: &str = "ksp-logging-json-test";
const EXTERNAL_TARGET: &str = "sqlx";
fn logging_trace_enabled() -> bool { fn logging_trace_enabled() -> bool {
return tracing::enabled!(target: LOGGING_TARGET, tracing::Level::TRACE); return tracing::enabled!(target: LOGGING_TARGET, tracing::Level::TRACE);

View File

@@ -1,20 +1,20 @@
// file: crates/ksp-logging-lib/unit_tests/domain.rs // file: crates/ksp-logging-lib/unit_tests/domain.rs
// version: 1 // version: 2
#[test] #[test]
fn wildcard_domain_matches_with_or_without_current_domain() { fn wildcard_domain_matches_with_or_without_current_domain() {
super::set_current_domain(std::option::Option::None); super::set_current_domain(std::option::Option::None);
assert!(super::current_domain_matches(&["*".to_string()])); assert!(crate::current_domain_matches(&["*".to_string()]));
super::set_current_domain(std::option::Option::Some("logging")); super::set_current_domain(std::option::Option::Some("logging"));
assert!(super::current_domain_matches(&["*".to_string()])); assert!(crate::current_domain_matches(&["*".to_string()]));
} }
#[test] #[test]
fn named_domain_requires_current_matching_prefix() { fn named_domain_requires_current_matching_prefix() {
super::set_current_domain(std::option::Option::None); super::set_current_domain(std::option::Option::None);
assert!(!super::current_domain_matches(&["logging".to_string()])); assert!(!crate::current_domain_matches(&["logging".to_string()]));
super::set_current_domain(std::option::Option::Some("logging.runtime")); super::set_current_domain(std::option::Option::Some("logging.runtime"));
assert!(super::current_domain_matches(&["logging".to_string()])); assert!(crate::current_domain_matches(&["logging".to_string()]));
assert!(super::current_domain_matches(&["store".to_string(), "logging.runtime".to_string()])); assert!(crate::current_domain_matches(&["store".to_string(), "logging.runtime".to_string()]));
assert!(!super::current_domain_matches(&["store".to_string()])); assert!(!crate::current_domain_matches(&["store".to_string()]));
} }

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-logging-lib/unit_tests/writer.rs // file: crates/ksp-logging-lib/unit_tests/writer.rs
// version: 2 // version: 3
#[test] #[test]
fn ansi_writer_strips_csi_sequences() { fn ansi_writer_strips_csi_sequences() {
let mut writer = super::StripAnsiWriter::new(std::vec::Vec::<u8>::new()); let mut writer = crate::StripAnsiWriter::new(std::vec::Vec::<u8>::new());
let write_result = std::io::Write::write_all(&mut writer, b"before\x1b[31mred\x1b[0mafter"); let write_result = std::io::Write::write_all(&mut writer, b"before\x1b[31mred\x1b[0mafter");
assert!(write_result.is_ok()); assert!(write_result.is_ok());
assert_eq!(writer.into_inner(), b"beforeredafter"); assert_eq!(writer.into_inner(), b"beforeredafter");
@@ -11,7 +11,7 @@ fn ansi_writer_strips_csi_sequences() {
#[test] #[test]
fn ansi_writer_preserves_state_across_split_writes() { fn ansi_writer_preserves_state_across_split_writes() {
let mut writer = super::StripAnsiWriter::new(std::vec::Vec::<u8>::new()); let mut writer = crate::StripAnsiWriter::new(std::vec::Vec::<u8>::new());
let first = std::io::Write::write_all(&mut writer, b"a\x1b["); let first = std::io::Write::write_all(&mut writer, b"a\x1b[");
let second = std::io::Write::write_all(&mut writer, b"32mb"); let second = std::io::Write::write_all(&mut writer, b"32mb");
assert!(first.is_ok()); assert!(first.is_ok());
@@ -21,7 +21,7 @@ fn ansi_writer_preserves_state_across_split_writes() {
#[test] #[test]
fn ansi_writer_strips_osc_sequences_terminated_by_bell_or_st() { fn ansi_writer_strips_osc_sequences_terminated_by_bell_or_st() {
let mut writer = super::StripAnsiWriter::new(std::vec::Vec::<u8>::new()); let mut writer = crate::StripAnsiWriter::new(std::vec::Vec::<u8>::new());
let first = std::io::Write::write_all(&mut writer, b"a\x1b]0;title\x07b"); let first = std::io::Write::write_all(&mut writer, b"a\x1b]0;title\x07b");
let second = std::io::Write::write_all(&mut writer, b"c\x1b]8;;https://example.invalid\x1b\\d"); let second = std::io::Write::write_all(&mut writer, b"c\x1b]8;;https://example.invalid\x1b\\d");
assert!(first.is_ok()); assert!(first.is_ok());
@@ -43,7 +43,7 @@ fn output_level_routing_covers_all_filter_levels() {
#[test] #[test]
fn disabled_routed_writer_discards_bytes_without_error() { fn disabled_routed_writer_discards_bytes_without_error() {
let mut writer = super::RoutedWriter::<std::vec::Vec<u8>>::Disabled; let mut writer = crate::RoutedWriter::<std::vec::Vec<u8>>::Disabled;
let write_result = std::io::Write::write_all(&mut writer, b"discarded"); let write_result = std::io::Write::write_all(&mut writer, b"discarded");
let flush_result = std::io::Write::flush(&mut writer); let flush_result = std::io::Write::flush(&mut writer);
assert!(write_result.is_ok()); assert!(write_result.is_ok());

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/client.rs // file: crates/ksp-onchain-transport-lib/src/client.rs
// version: 5 // version: 7
/// Passive runtime availability reported for one logical HTTP endpoint or role. /// Passive runtime availability reported for one logical HTTP endpoint or role.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -163,26 +163,6 @@ impl HttpEndpointSnapshot {
} }
} }
pub(crate) struct HttpEndpointHttpResponse {
status: u16,
retry_after: std::option::Option<std::time::Duration>,
body: std::vec::Vec<u8>,
}
impl HttpEndpointHttpResponse {
pub(crate) const fn status(&self) -> u16 {
return self.status;
}
pub(crate) const fn retry_after(&self) -> std::option::Option<std::time::Duration> {
return self.retry_after;
}
pub(crate) fn body(&self) -> &[u8] {
return self.body.as_slice();
}
}
/// Shareable logical HTTP endpoint client owned by KSP Transport. /// Shareable logical HTTP endpoint client owned by KSP Transport.
/// ///
/// The underlying `reqwest::Client` owns socket pooling. KSP keeps the configured URL private from diagnostics and exposes only safe routing metadata. /// The underlying `reqwest::Client` owns socket pooling. KSP keeps the configured URL private from diagnostics and exposes only safe routing metadata.
@@ -191,20 +171,15 @@ pub struct HttpEndpointClient {
inner: std::sync::Arc<HttpEndpointClientInner>, inner: std::sync::Arc<HttpEndpointClientInner>,
} }
struct HttpEndpointClientInner {
settings: crate::HttpEndpointSettings,
client: reqwest::Client,
role_runtimes: std::vec::Vec<std::sync::Arc<crate::resilience::HttpRoleRuntime>>,
}
impl HttpEndpointClient { impl HttpEndpointClient {
/// Builds one logical endpoint client from KSP-owned runtime settings. /// Builds one logical endpoint client from KSP-owned runtime settings.
pub fn new(settings: crate::HttpEndpointSettings) -> ksp_core_lib::Result<Self> { pub fn new(settings: crate::HttpEndpointSettings) -> ksp_core_lib::Result<Self> {
return Self::new_with_notify(settings, std::sync::Arc::new(tokio::sync::Notify::new())); return Self::new_with_notify(settings, std::sync::Arc::new(tokio::sync::Notify::new()));
} }
/// Creates a new with notify value for `HttpEndpointClient`.
pub(crate) fn new_with_notify(settings: crate::HttpEndpointSettings, notify: std::sync::Arc<tokio::sync::Notify>) -> ksp_core_lib::Result<Self> { pub(crate) fn new_with_notify(settings: crate::HttpEndpointSettings, notify: std::sync::Arc<tokio::sync::Notify>) -> ksp_core_lib::Result<Self> {
let validation = crate::settings::validate_endpoint_settings(&settings); let validation = crate::validate_endpoint_settings(&settings);
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);
} }
@@ -221,7 +196,7 @@ impl HttpEndpointClient {
}; };
let mut role_runtimes = std::vec::Vec::with_capacity(settings.roles().len()); let mut role_runtimes = std::vec::Vec::with_capacity(settings.roles().len());
for role in settings.roles() { for role in settings.roles() {
role_runtimes.push(std::sync::Arc::new(crate::resilience::HttpRoleRuntime::new(role, std::sync::Arc::clone(&notify)))); role_runtimes.push(std::sync::Arc::new(crate::HttpRoleRuntime::new(role, std::sync::Arc::clone(&notify))));
} }
ksp_logging_lib::debug!( ksp_logging_lib::debug!(
target: crate::TRACING_TARGET, target: crate::TRACING_TARGET,
@@ -265,6 +240,7 @@ impl HttpEndpointClient {
return self.inner.settings.request_timeout(); return self.inner.settings.request_timeout();
} }
/// Executes the crate-internal post json rpc operation for `HttpEndpointClient`.
pub(crate) async fn post_json_rpc(&self, payload: &str, timeout: std::time::Duration) -> ksp_core_lib::Result<HttpEndpointHttpResponse> { pub(crate) async fn post_json_rpc(&self, payload: &str, timeout: std::time::Duration) -> ksp_core_lib::Result<HttpEndpointHttpResponse> {
if timeout.is_zero() { if timeout.is_zero() {
return std::result::Result::Err( return std::result::Result::Err(
@@ -327,6 +303,7 @@ impl HttpEndpointClient {
}; };
} }
/// Executes the crate-internal matching role operation for `HttpEndpointClient`.
pub(crate) fn matching_role<'a>( pub(crate) fn matching_role<'a>(
&'a self, &'a self,
role: &crate::HttpRoleName, role: &crate::HttpRoleName,
@@ -348,11 +325,12 @@ impl HttpEndpointClient {
return std::option::Option::None; return std::option::Option::None;
} }
/// Executes the crate-internal matching role runtime operation for `HttpEndpointClient`.
pub(crate) fn matching_role_runtime( pub(crate) fn matching_role_runtime(
&self, &self,
role: &crate::HttpRoleName, role: &crate::HttpRoleName,
request_kind: &crate::HttpRequestKind, request_kind: &crate::HttpRequestKind,
) -> std::option::Option<(u32, std::sync::Arc<crate::resilience::HttpRoleRuntime>)> { ) -> std::option::Option<(u32, std::sync::Arc<crate::HttpRoleRuntime>)> {
if !self.enabled() { if !self.enabled() {
return std::option::Option::None; return std::option::Option::None;
} }
@@ -378,6 +356,7 @@ impl HttpEndpointClient {
return std::option::Option::None; return std::option::Option::None;
} }
/// Returns the current availability.
pub(crate) fn availability(&self) -> crate::HttpEndpointAvailability { pub(crate) fn availability(&self) -> crate::HttpEndpointAvailability {
if !self.enabled() { if !self.enabled() {
return crate::HttpEndpointAvailability::Disabled; return crate::HttpEndpointAvailability::Disabled;
@@ -419,10 +398,40 @@ impl std::fmt::Debug for HttpEndpointClient {
} }
} }
/// Crate-internal `HttpEndpointHttpResponse` state shared across the owning crate.
pub(crate) struct HttpEndpointHttpResponse {
status: u16,
retry_after: std::option::Option<std::time::Duration>,
body: std::vec::Vec<u8>,
}
impl HttpEndpointHttpResponse {
/// Returns the current status.
pub(crate) const fn status(&self) -> u16 {
return self.status;
}
/// Returns the current retry after.
pub(crate) const fn retry_after(&self) -> std::option::Option<std::time::Duration> {
return self.retry_after;
}
/// Returns the current body.
pub(crate) fn body(&self) -> &[u8] {
return self.body.as_slice();
}
}
struct HttpEndpointClientInner {
settings: crate::HttpEndpointSettings,
client: reqwest::Client,
role_runtimes: std::vec::Vec<std::sync::Arc<crate::HttpRoleRuntime>>,
}
fn role_snapshot( fn role_snapshot(
role: &crate::HttpEndpointRoleSettings, role: &crate::HttpEndpointRoleSettings,
request_kinds: std::vec::Vec<std::string::String>, request_kinds: std::vec::Vec<std::string::String>,
runtime: &crate::resilience::HttpRoleRuntime, runtime: &crate::HttpRoleRuntime,
) -> crate::HttpEndpointRoleSnapshot { ) -> crate::HttpEndpointRoleSnapshot {
let availability = if role.enabled() { runtime.availability(std::time::Instant::now()) } else { crate::HttpEndpointAvailability::Disabled }; let availability = if role.enabled() { runtime.availability(std::time::Instant::now()) } else { crate::HttpEndpointAvailability::Disabled };
return crate::HttpEndpointRoleSnapshot { return crate::HttpEndpointRoleSnapshot {

View File

@@ -1,29 +1,29 @@
// file: crates/ksp-onchain-transport-lib/src/error.rs // file: crates/ksp-onchain-transport-lib/src/error.rs
// version: 2 // version: 3
/// Error code used when HTTP transport runtime settings are invalid.
pub const ERROR_CODE_INVALID_SETTINGS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "invalid_settings");
/// Error code used when no logical endpoint can satisfy a request. /// Error code used when no logical endpoint can satisfy a request.
pub const ERROR_CODE_ENDPOINT_SELECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "endpoint_selection_failed"); pub const ERROR_CODE_ENDPOINT_SELECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "endpoint_selection_failed");
/// Error code used when an HTTP connection cannot be established. /// Error code used when an HTTP connection cannot be established.
pub const ERROR_CODE_HTTP_CONNECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "http_connection_failed"); pub const ERROR_CODE_HTTP_CONNECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "http_connection_failed");
/// Error code used when an HTTP request fails after a connection exists. /// Error code used when an HTTP request fails after a connection exists.
pub const ERROR_CODE_HTTP_REQUEST_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "http_request_failed"); pub const ERROR_CODE_HTTP_REQUEST_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "http_request_failed");
/// Error code used when a transport deadline expires.
pub const ERROR_CODE_TIMEOUT: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "timeout");
/// Error code used when an endpoint or provider rate-limits a request.
pub const ERROR_CODE_RATE_LIMITED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "rate_limited");
/// Error code used when a JSON-RPC request cannot be encoded.
pub const ERROR_CODE_JSON_ENCODE_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "json_encode_failed");
/// Error code used when an HTTP JSON-RPC payload cannot be decoded as JSON.
pub const ERROR_CODE_JSON_DECODE_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "json_decode_failed");
/// Error code used when a decoded JSON-RPC envelope violates protocol invariants.
pub const ERROR_CODE_JSON_RPC_PROTOCOL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "json_rpc_protocol_invalid");
/// Error code used when a remote JSON-RPC endpoint returns an application-level RPC error.
pub const ERROR_CODE_RPC_APPLICATION_ERROR: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "rpc_application_error");
/// Error code used when a historically documented RPC method is no longer supported by the targeted runtime.
pub const ERROR_CODE_METHOD_REMOVED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "method_removed");
/// Error code used when a decoded response cannot satisfy the KSP transport contract expected by the caller. /// Error code used when a decoded response cannot satisfy the KSP transport contract expected by the caller.
pub const ERROR_CODE_INVALID_RESPONSE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "invalid_response"); pub const ERROR_CODE_INVALID_RESPONSE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "invalid_response");
/// Error code used when typed Solana RPC parameters violate a locally enforceable method contract. /// Error code used when typed Solana RPC parameters violate a locally enforceable method contract.
pub const ERROR_CODE_INVALID_RPC_PARAMETERS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "invalid_rpc_parameters"); pub const ERROR_CODE_INVALID_RPC_PARAMETERS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "invalid_rpc_parameters");
/// Error code used when HTTP transport runtime settings are invalid.
pub const ERROR_CODE_INVALID_SETTINGS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "invalid_settings");
/// Error code used when an HTTP JSON-RPC payload cannot be decoded as JSON.
pub const ERROR_CODE_JSON_DECODE_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "json_decode_failed");
/// Error code used when a JSON-RPC request cannot be encoded.
pub const ERROR_CODE_JSON_ENCODE_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "json_encode_failed");
/// Error code used when a decoded JSON-RPC envelope violates protocol invariants.
pub const ERROR_CODE_JSON_RPC_PROTOCOL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "json_rpc_protocol_invalid");
/// Error code used when a historically documented RPC method is no longer supported by the targeted runtime.
pub const ERROR_CODE_METHOD_REMOVED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "method_removed");
/// Error code used when an endpoint or provider rate-limits a request.
pub const ERROR_CODE_RATE_LIMITED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "rate_limited");
/// Error code used when a remote JSON-RPC endpoint returns an application-level RPC error.
pub const ERROR_CODE_RPC_APPLICATION_ERROR: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "rpc_application_error");
/// Error code used when a transport deadline expires.
pub const ERROR_CODE_TIMEOUT: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "timeout");

View File

@@ -1,12 +1,12 @@
// file: crates/ksp-onchain-transport-lib/src/executor.rs // file: crates/ksp-onchain-transport-lib/src/executor.rs
// version: 2 // version: 3
const HTTP_REQUEST_TIMEOUT: u16 = 408;
const HTTP_TOO_MANY_REQUESTS: u16 = 429;
const HTTP_INTERNAL_SERVER_ERROR: u16 = 500;
const HTTP_BAD_GATEWAY: u16 = 502; const HTTP_BAD_GATEWAY: u16 = 502;
const HTTP_SERVICE_UNAVAILABLE: u16 = 503;
const HTTP_GATEWAY_TIMEOUT: u16 = 504; const HTTP_GATEWAY_TIMEOUT: u16 = 504;
const HTTP_INTERNAL_SERVER_ERROR: u16 = 500;
const HTTP_REQUEST_TIMEOUT: u16 = 408;
const HTTP_SERVICE_UNAVAILABLE: u16 = 503;
const HTTP_TOO_MANY_REQUESTS: u16 = 429;
impl crate::HttpTransportPool { impl crate::HttpTransportPool {
/// Executes one audited standard Solana HTTP JSON-RPC method through KSP routing, admission and bounded retry policy. /// Executes one audited standard Solana HTTP JSON-RPC method through KSP routing, admission and bounded retry policy.

View File

@@ -1,5 +1,6 @@
// file: crates/ksp-onchain-transport-lib/src/lib.rs // file: crates/ksp-onchain-transport-lib/src/lib.rs
// version: 19 // version: 21
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
@@ -34,8 +35,6 @@ mod rpc_tokens;
mod rpc_transactions; mod rpc_transactions;
mod settings; mod settings;
pub(crate) use self::constants::TRACING_TARGET;
/// Passive runtime availability reported for one logical HTTP endpoint. /// Passive runtime availability reported for one logical HTTP endpoint.
pub use self::client::HttpEndpointAvailability; pub use self::client::HttpEndpointAvailability;
/// Shareable logical HTTP endpoint client owned by KSP Transport. /// Shareable logical HTTP endpoint client owned by KSP Transport.
@@ -194,10 +193,6 @@ pub use self::rpc_common::SolanaContextConfig;
pub use self::rpc_common::SolanaRpcContext; pub use self::rpc_common::SolanaRpcContext;
/// Generic contextual result returned by typed Solana HTTP RPC adapters. /// Generic contextual result returned by typed Solana HTTP RPC adapters.
pub use self::rpc_common::SolanaRpcResponse; pub use self::rpc_common::SolanaRpcResponse;
/// Decodes one private serde wire type into the shared Transport error domain for typed RPC adapters.
pub(crate) use self::rpc_common::decode_wire_json;
/// Parses a base58 public key without echoing its wire value into diagnostics for typed RPC adapters.
pub(crate) use self::rpc_common::parse_wire_pubkey;
/// Inflation-governor values returned by `getInflationGovernor`. /// Inflation-governor values returned by `getInflationGovernor`.
pub use self::rpc_economics::SolanaInflationGovernor; pub use self::rpc_economics::SolanaInflationGovernor;
/// Current inflation-rate values returned by `getInflationRate`. /// Current inflation-rate values returned by `getInflationRate`.
@@ -296,3 +291,18 @@ pub use self::settings::HttpRoleLimits;
pub use self::settings::HttpRoleName; pub use self::settings::HttpRoleName;
/// Complete runtime settings consumed by the Solana HTTP transport foundation. /// Complete runtime settings consumed by the Solana HTTP transport foundation.
pub use self::settings::HttpTransportSettings; pub use self::settings::HttpTransportSettings;
/// Owning tracing target for events emitted by the on-chain transport crate.
pub(crate) use self::constants::TRACING_TARGET;
/// Crate-internal `HttpConcurrencyPermit` state shared across the owning crate.
pub(crate) use self::resilience::HttpConcurrencyPermit;
/// Crate-internal `HttpRoleRuntime` state shared across the owning crate.
pub(crate) use self::resilience::HttpRoleRuntime;
/// Crate-internal `RoleAdmissionAttempt` variants used by the owning crate.
pub(crate) use self::resilience::RoleAdmissionAttempt;
/// Decodes one private serde wire type into the shared Transport error domain for typed RPC adapters.
pub(crate) use self::rpc_common::decode_wire_json;
/// Parses a base58 public key without echoing its wire value into diagnostics for typed RPC adapters.
pub(crate) use self::rpc_common::parse_wire_pubkey;
/// Validates endpoint settings.
pub(crate) use self::settings::validate_endpoint_settings;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/pool.rs // file: crates/ksp-onchain-transport-lib/src/pool.rs
// version: 5 // version: 7
/// Safe snapshot of the logical HTTP endpoint pool. /// Safe snapshot of the logical HTTP endpoint pool.
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@@ -74,8 +74,8 @@ impl HttpEndpointSelection {
pub struct HttpRequestPermit { pub struct HttpRequestPermit {
selection: crate::HttpEndpointSelection, selection: crate::HttpEndpointSelection,
deadline: std::time::Instant, deadline: std::time::Instant,
role_runtime: std::sync::Arc<crate::resilience::HttpRoleRuntime>, role_runtime: std::sync::Arc<crate::HttpRoleRuntime>,
_concurrency_permit: crate::resilience::HttpConcurrencyPermit, _concurrency_permit: crate::HttpConcurrencyPermit,
} }
impl HttpRequestPermit { impl HttpRequestPermit {
@@ -165,14 +165,6 @@ pub struct HttpTransportPool {
inner: std::sync::Arc<HttpTransportPoolInner>, inner: std::sync::Arc<HttpTransportPoolInner>,
} }
struct HttpTransportPoolInner {
clients: std::vec::Vec<crate::HttpEndpointClient>,
retry: crate::HttpRetrySettings,
notify: std::sync::Arc<tokio::sync::Notify>,
request_ids: std::sync::atomic::AtomicU64,
cursors: std::sync::Mutex<std::collections::BTreeMap<(std::string::String, std::string::String, u32), usize>>,
}
impl HttpTransportPool { impl HttpTransportPool {
/// Builds a logical endpoint pool after validating all Transport-owned runtime settings. /// Builds a logical endpoint pool after validating all Transport-owned runtime settings.
pub fn new(settings: crate::HttpTransportSettings) -> ksp_core_lib::Result<Self> { pub fn new(settings: crate::HttpTransportSettings) -> ksp_core_lib::Result<Self> {
@@ -215,6 +207,7 @@ impl HttpTransportPool {
return &self.inner.retry; return &self.inner.retry;
} }
/// Executes the crate-internal next request id operation for `HttpTransportPool`.
pub(crate) fn next_request_id(&self) -> u64 { pub(crate) fn next_request_id(&self) -> u64 {
let id = self.inner.request_ids.fetch_add(1, std::sync::atomic::Ordering::Relaxed); let id = self.inner.request_ids.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if id == 0 { if id == 0 {
@@ -369,7 +362,7 @@ impl HttpTransportPool {
let candidate = &candidates[position]; let candidate = &candidates[position];
let admission = candidate.runtime.try_acquire(now); let admission = candidate.runtime.try_acquire(now);
match admission { match admission {
crate::resilience::RoleAdmissionAttempt::Ready(concurrency_permit) => { crate::RoleAdmissionAttempt::Ready(concurrency_permit) => {
let selection_result = self.selection_from_runtime_candidate(role, request_kind, candidate); let selection_result = self.selection_from_runtime_candidate(role, request_kind, candidate);
let selection = match selection_result { let selection = match selection_result {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -391,13 +384,13 @@ impl HttpTransportPool {
_concurrency_permit: concurrency_permit, _concurrency_permit: concurrency_permit,
}); });
}, },
crate::resilience::RoleAdmissionAttempt::BlockedUntil(ready_at) => { crate::RoleAdmissionAttempt::BlockedUntil(ready_at) => {
earliest_ready = earlier_instant(earliest_ready, ready_at); earliest_ready = earlier_instant(earliest_ready, ready_at);
}, },
crate::resilience::RoleAdmissionAttempt::ConcurrencySaturated => { crate::RoleAdmissionAttempt::ConcurrencySaturated => {
concurrency_saturated = true; concurrency_saturated = true;
}, },
crate::resilience::RoleAdmissionAttempt::Unavailable => {}, crate::RoleAdmissionAttempt::Unavailable => {},
} }
offset = offset.saturating_add(1); offset = offset.saturating_add(1);
} }
@@ -434,6 +427,7 @@ impl HttpTransportPool {
return std::time::Instant::now() < deadline; return std::time::Instant::now() < deadline;
} }
/// Executes the crate-internal common request timeout operation for `HttpTransportPool`.
pub(crate) fn common_request_timeout( pub(crate) fn common_request_timeout(
&self, &self,
role: &crate::HttpRoleName, role: &crate::HttpRoleName,
@@ -548,6 +542,14 @@ impl std::fmt::Debug for HttpTransportPool {
} }
} }
struct HttpTransportPoolInner {
clients: std::vec::Vec<crate::HttpEndpointClient>,
retry: crate::HttpRetrySettings,
notify: std::sync::Arc<tokio::sync::Notify>,
request_ids: std::sync::atomic::AtomicU64,
cursors: std::sync::Mutex<std::collections::BTreeMap<(std::string::String, std::string::String, u32), usize>>,
}
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
struct PoolCandidate { struct PoolCandidate {
client_index: usize, client_index: usize,
@@ -557,7 +559,7 @@ struct PoolCandidate {
struct RuntimePoolCandidate { struct RuntimePoolCandidate {
client_index: usize, client_index: usize,
priority: u32, priority: u32,
runtime: std::sync::Arc<crate::resilience::HttpRoleRuntime>, runtime: std::sync::Arc<crate::HttpRoleRuntime>,
} }
enum RuntimeSelectionAttempt { enum RuntimeSelectionAttempt {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/resilience.rs // file: crates/ksp-onchain-transport-lib/src/resilience.rs
// version: 1 // version: 3
const DEFAULT_RATE_LIMIT_COOLDOWN: std::time::Duration = std::time::Duration::from_secs(1); const DEFAULT_RATE_LIMIT_COOLDOWN: std::time::Duration = std::time::Duration::from_secs(1);
const MAX_PROVIDER_RETRY_AFTER: std::time::Duration = std::time::Duration::from_secs(60); const MAX_PROVIDER_RETRY_AFTER: std::time::Duration = std::time::Duration::from_secs(60);
@@ -71,61 +71,7 @@ impl HttpRetryDecision {
} }
} }
/// Evaluates the centralized bounded HTTP retry policy for one audited RPC method. /// Crate-internal `HttpRoleRuntime` state shared across the owning crate.
///
/// `completed_retries` counts retries already performed after the initial attempt. Provider `Retry-After` values are defensively bounded to sixty seconds
/// before they can extend the local exponential backoff. RPC application errors are never converted into transport retries.
#[must_use]
pub fn evaluate_transport_retry(
method: &crate::HttpRpcMethodDescriptor,
settings: &crate::HttpRetrySettings,
cause: crate::HttpRetryCause,
dispatch_state: crate::HttpDispatchState,
completed_retries: u32,
provider_retry_after: std::option::Option<std::time::Duration>,
) -> crate::HttpRetryDecision {
if completed_retries >= settings.max_retries() || !cause.is_retryable() {
return crate::HttpRetryDecision::Stop;
}
if method.transport_retry_class() == crate::TransportRetryClass::NotApplicable {
return crate::HttpRetryDecision::Stop;
}
if method.transport_retry_class() == crate::TransportRetryClass::NeverAfterDispatch && dispatch_state == crate::HttpDispatchState::DispatchedAmbiguous {
return crate::HttpRetryDecision::Stop;
}
let retry_number = completed_retries.saturating_add(1);
let mut delay = retry_backoff(settings, retry_number);
if cause == crate::HttpRetryCause::RateLimited
&& let std::option::Option::Some(provider_delay) = provider_retry_after
{
let bounded_provider_delay = std::cmp::min(provider_delay, MAX_PROVIDER_RETRY_AFTER);
if bounded_provider_delay > delay {
delay = bounded_provider_delay;
}
}
return crate::HttpRetryDecision::RetryAfter(delay);
}
pub(crate) fn retry_backoff(settings: &crate::HttpRetrySettings, retry_number: u32) -> std::time::Duration {
let mut delay = settings.initial_backoff();
if retry_number <= 1 {
return std::cmp::min(delay, settings.max_backoff());
}
let mut step = 1_u32;
while step < retry_number {
let doubled = match delay.checked_mul(2) {
std::option::Option::Some(value) => value,
std::option::Option::None => settings.max_backoff(),
};
delay = std::cmp::min(doubled, settings.max_backoff());
if delay >= settings.max_backoff() {
return settings.max_backoff();
}
step = step.saturating_add(1);
}
return delay;
}
pub(crate) struct HttpRoleRuntime { pub(crate) struct HttpRoleRuntime {
limits: crate::HttpRoleLimits, limits: crate::HttpRoleLimits,
bucket: std::sync::Mutex<std::option::Option<HttpTokenBucketState>>, bucket: std::sync::Mutex<std::option::Option<HttpTokenBucketState>>,
@@ -139,6 +85,7 @@ pub(crate) struct HttpRoleRuntime {
} }
impl HttpRoleRuntime { impl HttpRoleRuntime {
/// Creates a new `HttpRoleRuntime` value.
pub(crate) fn new(settings: &crate::HttpEndpointRoleSettings, notify: std::sync::Arc<tokio::sync::Notify>) -> Self { pub(crate) fn new(settings: &crate::HttpEndpointRoleSettings, notify: std::sync::Arc<tokio::sync::Notify>) -> Self {
let bucket = match settings.limits().requests_per_second() { let bucket = match settings.limits().requests_per_second() {
std::option::Option::Some(requests_per_second) => { std::option::Option::Some(requests_per_second) => {
@@ -169,6 +116,7 @@ impl HttpRoleRuntime {
}; };
} }
/// Returns the current availability.
pub(crate) fn availability(&self, now: std::time::Instant) -> crate::HttpEndpointAvailability { pub(crate) fn availability(&self, now: std::time::Instant) -> crate::HttpEndpointAvailability {
if self.cooldown_remaining_at(now).is_some() { if self.cooldown_remaining_at(now).is_some() {
return crate::HttpEndpointAvailability::RateLimited; return crate::HttpEndpointAvailability::RateLimited;
@@ -179,14 +127,17 @@ impl HttpRoleRuntime {
return crate::HttpEndpointAvailability::Available; return crate::HttpEndpointAvailability::Available;
} }
/// Returns the current cooldown remaining.
pub(crate) fn cooldown_remaining(&self) -> std::option::Option<std::time::Duration> { pub(crate) fn cooldown_remaining(&self) -> std::option::Option<std::time::Duration> {
return self.cooldown_remaining_at(std::time::Instant::now()); return self.cooldown_remaining_at(std::time::Instant::now());
} }
/// Returns the current max concurrent requests.
pub(crate) fn max_concurrent_requests(&self) -> std::option::Option<u32> { pub(crate) fn max_concurrent_requests(&self) -> std::option::Option<u32> {
return self.limits.max_concurrent_requests().map(|value| return value.get()); return self.limits.max_concurrent_requests().map(|value| return value.get());
} }
/// Returns the current in flight requests.
pub(crate) fn in_flight_requests(&self) -> std::option::Option<u32> { pub(crate) fn in_flight_requests(&self) -> std::option::Option<u32> {
let semaphore = match &self.semaphore { let semaphore = match &self.semaphore {
std::option::Option::Some(value) => value, std::option::Option::Some(value) => value,
@@ -204,33 +155,37 @@ impl HttpRoleRuntime {
return std::option::Option::Some(maximum.saturating_sub(available_u32)); return std::option::Option::Some(maximum.saturating_sub(available_u32));
} }
/// Returns the current success count.
pub(crate) fn success_count(&self) -> u64 { pub(crate) fn success_count(&self) -> u64 {
return self.success_count.load(std::sync::atomic::Ordering::Relaxed); return self.success_count.load(std::sync::atomic::Ordering::Relaxed);
} }
/// Returns the current failure count.
pub(crate) fn failure_count(&self) -> u64 { pub(crate) fn failure_count(&self) -> u64 {
return self.failure_count.load(std::sync::atomic::Ordering::Relaxed); return self.failure_count.load(std::sync::atomic::Ordering::Relaxed);
} }
/// Returns the current rate limit count.
pub(crate) fn rate_limit_count(&self) -> u64 { pub(crate) fn rate_limit_count(&self) -> u64 {
return self.rate_limit_count.load(std::sync::atomic::Ordering::Relaxed); return self.rate_limit_count.load(std::sync::atomic::Ordering::Relaxed);
} }
pub(crate) fn try_acquire(self: &std::sync::Arc<Self>, now: std::time::Instant) -> RoleAdmissionAttempt { /// Attempts to acquire.
pub(crate) fn try_acquire(self: &std::sync::Arc<Self>, now: std::time::Instant) -> crate::RoleAdmissionAttempt {
if let std::option::Option::Some(remaining) = self.cooldown_remaining_at(now) { if let std::option::Option::Some(remaining) = self.cooldown_remaining_at(now) {
let ready_at = match now.checked_add(remaining) { let ready_at = match now.checked_add(remaining) {
std::option::Option::Some(value) => value, std::option::Option::Some(value) => value,
std::option::Option::None => now, std::option::Option::None => now,
}; };
return RoleAdmissionAttempt::BlockedUntil(ready_at); return crate::RoleAdmissionAttempt::BlockedUntil(ready_at);
} }
let semaphore_permit = match &self.semaphore { let semaphore_permit = match &self.semaphore {
std::option::Option::Some(semaphore) => { std::option::Option::Some(semaphore) => {
let permit_result = std::sync::Arc::clone(semaphore).try_acquire_owned(); let permit_result = std::sync::Arc::clone(semaphore).try_acquire_owned();
match permit_result { match permit_result {
std::result::Result::Ok(permit) => std::option::Option::Some(permit), std::result::Result::Ok(permit) => std::option::Option::Some(permit),
std::result::Result::Err(tokio::sync::TryAcquireError::NoPermits) => return RoleAdmissionAttempt::ConcurrencySaturated, std::result::Result::Err(tokio::sync::TryAcquireError::NoPermits) => return crate::RoleAdmissionAttempt::ConcurrencySaturated,
std::result::Result::Err(tokio::sync::TryAcquireError::Closed) => return RoleAdmissionAttempt::Unavailable, std::result::Result::Err(tokio::sync::TryAcquireError::Closed) => return crate::RoleAdmissionAttempt::Unavailable,
} }
}, },
std::option::Option::None => std::option::Option::None, std::option::Option::None => std::option::Option::None,
@@ -239,11 +194,12 @@ impl HttpRoleRuntime {
if let std::option::Option::Some(ready_at) = token_result { if let std::option::Option::Some(ready_at) = token_result {
drop(semaphore_permit); drop(semaphore_permit);
self.notify.notify_one(); self.notify.notify_one();
return RoleAdmissionAttempt::BlockedUntil(ready_at); return crate::RoleAdmissionAttempt::BlockedUntil(ready_at);
} }
return RoleAdmissionAttempt::Ready(HttpConcurrencyPermit { semaphore_permit, notify: std::sync::Arc::clone(&self.notify) }); return crate::RoleAdmissionAttempt::Ready(HttpConcurrencyPermit { semaphore_permit, notify: std::sync::Arc::clone(&self.notify) });
} }
/// Records success.
pub(crate) fn record_success(&self) { pub(crate) fn record_success(&self) {
self.success_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed); self.success_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
self.degraded.store(false, std::sync::atomic::Ordering::Relaxed); self.degraded.store(false, std::sync::atomic::Ordering::Relaxed);
@@ -251,6 +207,7 @@ impl HttpRoleRuntime {
return; return;
} }
/// Records failure.
pub(crate) fn record_failure(&self) { pub(crate) fn record_failure(&self) {
self.failure_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed); self.failure_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
self.degraded.store(true, std::sync::atomic::Ordering::Relaxed); self.degraded.store(true, std::sync::atomic::Ordering::Relaxed);
@@ -258,6 +215,7 @@ impl HttpRoleRuntime {
return; return;
} }
/// Records rate limited.
pub(crate) fn record_rate_limited(&self, provider_retry_after: std::option::Option<std::time::Duration>) -> std::time::Duration { pub(crate) fn record_rate_limited(&self, provider_retry_after: std::option::Option<std::time::Duration>) -> std::time::Duration {
self.failure_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed); self.failure_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
self.rate_limit_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed); self.rate_limit_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
@@ -324,6 +282,7 @@ impl HttpRoleRuntime {
} }
} }
/// Crate-internal `RoleAdmissionAttempt` variants used by the owning crate.
pub(crate) enum RoleAdmissionAttempt { pub(crate) enum RoleAdmissionAttempt {
Ready(HttpConcurrencyPermit), Ready(HttpConcurrencyPermit),
BlockedUntil(std::time::Instant), BlockedUntil(std::time::Instant),
@@ -331,6 +290,7 @@ pub(crate) enum RoleAdmissionAttempt {
Unavailable, Unavailable,
} }
/// Crate-internal `HttpConcurrencyPermit` state shared across the owning crate.
pub(crate) struct HttpConcurrencyPermit { pub(crate) struct HttpConcurrencyPermit {
semaphore_permit: std::option::Option<tokio::sync::OwnedSemaphorePermit>, semaphore_permit: std::option::Option<tokio::sync::OwnedSemaphorePermit>,
notify: std::sync::Arc<tokio::sync::Notify>, notify: std::sync::Arc<tokio::sync::Notify>,
@@ -385,6 +345,61 @@ impl HttpTokenBucketState {
} }
} }
/// Evaluates the centralized bounded HTTP retry policy for one audited RPC method.
///
/// `completed_retries` counts retries already performed after the initial attempt. Provider `Retry-After` values are defensively bounded to sixty seconds
/// before they can extend the local exponential backoff. RPC application errors are never converted into transport retries.
#[must_use]
pub fn evaluate_transport_retry(
method: &crate::HttpRpcMethodDescriptor,
settings: &crate::HttpRetrySettings,
cause: crate::HttpRetryCause,
dispatch_state: crate::HttpDispatchState,
completed_retries: u32,
provider_retry_after: std::option::Option<std::time::Duration>,
) -> crate::HttpRetryDecision {
if completed_retries >= settings.max_retries() || !cause.is_retryable() {
return crate::HttpRetryDecision::Stop;
}
if method.transport_retry_class() == crate::TransportRetryClass::NotApplicable {
return crate::HttpRetryDecision::Stop;
}
if method.transport_retry_class() == crate::TransportRetryClass::NeverAfterDispatch && dispatch_state == crate::HttpDispatchState::DispatchedAmbiguous {
return crate::HttpRetryDecision::Stop;
}
let retry_number = completed_retries.saturating_add(1);
let mut delay = retry_backoff(settings, retry_number);
if cause == crate::HttpRetryCause::RateLimited
&& let std::option::Option::Some(provider_delay) = provider_retry_after
{
let bounded_provider_delay = std::cmp::min(provider_delay, MAX_PROVIDER_RETRY_AFTER);
if bounded_provider_delay > delay {
delay = bounded_provider_delay;
}
}
return crate::HttpRetryDecision::RetryAfter(delay);
}
fn retry_backoff(settings: &crate::HttpRetrySettings, retry_number: u32) -> std::time::Duration {
let mut delay = settings.initial_backoff();
if retry_number <= 1 {
return std::cmp::min(delay, settings.max_backoff());
}
let mut step = 1_u32;
while step < retry_number {
let doubled = match delay.checked_mul(2) {
std::option::Option::Some(value) => value,
std::option::Option::None => settings.max_backoff(),
};
delay = std::cmp::min(doubled, settings.max_backoff());
if delay >= settings.max_backoff() {
return settings.max_backoff();
}
step = step.saturating_add(1);
}
return delay;
}
#[cfg(test)] #[cfg(test)]
#[path = "../unit_tests/resilience.rs"] #[path = "../unit_tests/resilience.rs"]
mod tests; mod tests;

View File

@@ -1,5 +1,9 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_accounts.rs // file: crates/ksp-onchain-transport-lib/src/rpc_accounts.rs
// version: 4 // version: 6
const MAX_MEMCMP_BYTES: usize = 128;
const MAX_MULTIPLE_ACCOUNTS: usize = 100;
const MAX_PROGRAM_ACCOUNT_FILTERS: usize = 4;
/// Account-data encoding accepted by Solana HTTP account methods. /// Account-data encoding accepted by Solana HTTP account methods.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -116,6 +120,7 @@ impl SolanaAccountInfoConfig {
return self.context.min_context_slot(); return self.context.min_context_slot();
} }
/// Returns whether empty.
pub(crate) fn is_empty(&self) -> bool { pub(crate) fn is_empty(&self) -> bool {
return self.encoding.is_none() && self.data_slice.is_none() && self.commitment().is_none() && self.min_context_slot().is_none(); return self.encoding.is_none() && self.data_slice.is_none() && self.commitment().is_none() && self.min_context_slot().is_none();
} }
@@ -592,10 +597,6 @@ pub enum SolanaProgramAccountsResult {
Context(crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaKeyedAccount>>), Context(crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaKeyedAccount>>),
} }
const MAX_MULTIPLE_ACCOUNTS: usize = 100;
const MAX_PROGRAM_ACCOUNT_FILTERS: usize = 4;
const MAX_MEMCMP_BYTES: usize = 128;
impl crate::HttpTransportPool { impl crate::HttpTransportPool {
/// Executes typed `getAccountInfo` through the common KSP HTTP transport path. /// Executes typed `getAccountInfo` through the common KSP HTTP transport path.
pub async fn get_account_info( pub async fn get_account_info(
@@ -752,6 +753,58 @@ impl crate::HttpTransportPool {
} }
} }
#[derive(serde::Deserialize)]
struct WireRpcResponse<T> {
context: serde_json::Value,
value: T,
}
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum WireProgramAccountsResult {
Context(WireRpcResponse<std::vec::Vec<serde_json::Value>>),
Accounts(std::vec::Vec<serde_json::Value>),
}
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum WireAccountData {
LegacyBinary(std::string::String),
JsonParsed(WireParsedAccountData),
Encoded((std::string::String, std::string::String)),
}
#[derive(serde::Deserialize)]
struct WireParsedAccountData {
program: std::string::String,
parsed: serde_json::Value,
space: u64,
}
#[derive(serde::Deserialize)]
struct WireAccount {
lamports: u64,
data: serde_json::Value,
owner: std::string::String,
executable: bool,
#[serde(rename = "rentEpoch")]
rent_epoch: u64,
#[serde(default)]
space: std::option::Option<u64>,
}
#[derive(serde::Deserialize)]
struct WireKeyedAccount {
pubkey: std::string::String,
account: serde_json::Value,
}
#[derive(serde::Deserialize)]
struct WireAccountBalance {
address: std::string::String,
lamports: u64,
}
fn account_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> { fn account_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> {
let descriptor = crate::find_http_rpc_method(method); let descriptor = crate::find_http_rpc_method(method);
return match descriptor { return match descriptor {
@@ -934,58 +987,6 @@ fn decode_keyed_accounts(method: &str, values: std::vec::Vec<serde_json::Value>)
return std::result::Result::Ok(decoded_values); return std::result::Result::Ok(decoded_values);
} }
#[derive(serde::Deserialize)]
struct WireRpcResponse<T> {
context: serde_json::Value,
value: T,
}
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum WireProgramAccountsResult {
Context(WireRpcResponse<std::vec::Vec<serde_json::Value>>),
Accounts(std::vec::Vec<serde_json::Value>),
}
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum WireAccountData {
LegacyBinary(std::string::String),
JsonParsed(WireParsedAccountData),
Encoded((std::string::String, std::string::String)),
}
#[derive(serde::Deserialize)]
struct WireParsedAccountData {
program: std::string::String,
parsed: serde_json::Value,
space: u64,
}
#[derive(serde::Deserialize)]
struct WireAccount {
lamports: u64,
data: serde_json::Value,
owner: std::string::String,
executable: bool,
#[serde(rename = "rentEpoch")]
rent_epoch: u64,
#[serde(default)]
space: std::option::Option<u64>,
}
#[derive(serde::Deserialize)]
struct WireKeyedAccount {
pubkey: std::string::String,
account: serde_json::Value,
}
#[derive(serde::Deserialize)]
struct WireAccountBalance {
address: std::string::String,
lamports: u64,
}
#[cfg(test)] #[cfg(test)]
#[path = "../unit_tests/rpc_accounts.rs"] #[path = "../unit_tests/rpc_accounts.rs"]
mod tests; mod tests;

View File

@@ -1,5 +1,8 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_blocks.rs // file: crates/ksp-onchain-transport-lib/src/rpc_blocks.rs
// version: 6 // version: 7
const MAX_GET_BLOCKS_RANGE: u64 = 500_000;
const MAX_GET_RECENT_PERFORMANCE_SAMPLES: u64 = 720;
/// Transaction detail level accepted by modern `getBlock` requests. /// Transaction detail level accepted by modern `getBlock` requests.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
@@ -828,8 +831,85 @@ impl crate::HttpTransportPool {
} }
} }
const MAX_GET_BLOCKS_RANGE: u64 = 500_000; #[derive(serde::Deserialize)]
const MAX_GET_RECENT_PERFORMANCE_SAMPLES: u64 = 720; #[serde(rename_all = "camelCase")]
struct WireBlockCommitment {
commitment: std::option::Option<std::vec::Vec<u64>>,
total_stake: u64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockProductionRange {
first_slot: u64,
last_slot: u64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockProduction {
by_identity: std::collections::BTreeMap<std::string::String, (usize, usize)>,
range: WireBlockProductionRange,
}
#[derive(serde::Deserialize)]
struct WireBlockProductionRpcResponse {
context: serde_json::Value,
value: serde_json::Value,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockReward {
pubkey: std::string::String,
lamports: i64,
post_balance: u64,
#[serde(default)]
reward_type: std::option::Option<std::string::String>,
#[serde(default)]
commission: std::option::Option<u8>,
#[serde(default)]
commission_bps: crate::SolanaWireField<u16>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockTransaction {
transaction: serde_json::Value,
#[serde(default)]
meta: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
version: crate::SolanaWireField<serde_json::Value>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireConfirmedBlock {
previous_blockhash: std::string::String,
blockhash: std::string::String,
parent_slot: u64,
#[serde(default)]
transactions: crate::SolanaWireField<std::vec::Vec<serde_json::Value>>,
#[serde(default)]
signatures: crate::SolanaWireField<std::vec::Vec<std::string::String>>,
#[serde(default)]
rewards: crate::SolanaWireField<std::vec::Vec<serde_json::Value>>,
#[serde(default)]
num_reward_partitions: crate::SolanaWireField<u64>,
block_time: std::option::Option<i64>,
block_height: std::option::Option<u64>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WirePerformanceSample {
slot: u64,
num_transactions: u64,
#[serde(default)]
num_non_vote_transactions: std::option::Option<u64>,
num_slots: u64,
sample_period_secs: u16,
}
fn validate_blocks_context_commitment(method: &'static str, config: std::option::Option<&crate::SolanaContextConfig>) -> ksp_core_lib::Result<()> { fn validate_blocks_context_commitment(method: &'static str, config: std::option::Option<&crate::SolanaContextConfig>) -> ksp_core_lib::Result<()> {
if let std::option::Option::Some(config) = config if let std::option::Option::Some(config) = config
@@ -978,86 +1058,6 @@ fn decode_block_rewards(
return std::result::Result::Ok(crate::SolanaWireField::Value(rewards)); return std::result::Result::Ok(crate::SolanaWireField::Value(rewards));
} }
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockCommitment {
commitment: std::option::Option<std::vec::Vec<u64>>,
total_stake: u64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockProductionRange {
first_slot: u64,
last_slot: u64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockProduction {
by_identity: std::collections::BTreeMap<std::string::String, (usize, usize)>,
range: WireBlockProductionRange,
}
#[derive(serde::Deserialize)]
struct WireBlockProductionRpcResponse {
context: serde_json::Value,
value: serde_json::Value,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockReward {
pubkey: std::string::String,
lamports: i64,
post_balance: u64,
#[serde(default)]
reward_type: std::option::Option<std::string::String>,
#[serde(default)]
commission: std::option::Option<u8>,
#[serde(default)]
commission_bps: crate::SolanaWireField<u16>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireBlockTransaction {
transaction: serde_json::Value,
#[serde(default)]
meta: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
version: crate::SolanaWireField<serde_json::Value>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireConfirmedBlock {
previous_blockhash: std::string::String,
blockhash: std::string::String,
parent_slot: u64,
#[serde(default)]
transactions: crate::SolanaWireField<std::vec::Vec<serde_json::Value>>,
#[serde(default)]
signatures: crate::SolanaWireField<std::vec::Vec<std::string::String>>,
#[serde(default)]
rewards: crate::SolanaWireField<std::vec::Vec<serde_json::Value>>,
#[serde(default)]
num_reward_partitions: crate::SolanaWireField<u64>,
block_time: std::option::Option<i64>,
block_height: std::option::Option<u64>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WirePerformanceSample {
slot: u64,
num_transactions: u64,
#[serde(default)]
num_non_vote_transactions: std::option::Option<u64>,
num_slots: u64,
sample_period_secs: u16,
}
#[cfg(test)] #[cfg(test)]
#[path = "../unit_tests/rpc_blocks.rs"] #[path = "../unit_tests/rpc_blocks.rs"]
mod tests; mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_cluster.rs // file: crates/ksp-onchain-transport-lib/src/rpc_cluster.rs
// version: 4 // version: 7
const MAX_GET_SLOT_LEADERS: u64 = 5_000; const MAX_GET_SLOT_LEADERS: u64 = 5_000;
@@ -29,71 +29,85 @@ impl SolanaClusterNode {
pub const fn pubkey(&self) -> &ksp_core_lib::Pubkey { pub const fn pubkey(&self) -> &ksp_core_lib::Pubkey {
return &self.pubkey; return &self.pubkey;
} }
/// Returns the optional feature-set identifier. /// Returns the optional feature-set identifier.
#[must_use] #[must_use]
pub const fn feature_set(&self) -> std::option::Option<u32> { pub const fn feature_set(&self) -> std::option::Option<u32> {
return self.feature_set; return self.feature_set;
} }
/// Returns the optional gossip endpoint. /// Returns the optional gossip endpoint.
#[must_use] #[must_use]
pub fn gossip(&self) -> std::option::Option<&str> { pub fn gossip(&self) -> std::option::Option<&str> {
return self.gossip.as_deref(); return self.gossip.as_deref();
} }
/// Returns the optional PubSub endpoint. /// Returns the optional PubSub endpoint.
#[must_use] #[must_use]
pub fn pubsub(&self) -> std::option::Option<&str> { pub fn pubsub(&self) -> std::option::Option<&str> {
return self.pubsub.as_deref(); return self.pubsub.as_deref();
} }
/// Returns the optional JSON-RPC endpoint. /// Returns the optional JSON-RPC endpoint.
#[must_use] #[must_use]
pub fn rpc(&self) -> std::option::Option<&str> { pub fn rpc(&self) -> std::option::Option<&str> {
return self.rpc.as_deref(); return self.rpc.as_deref();
} }
/// Returns the optional repair endpoint. /// Returns the optional repair endpoint.
#[must_use] #[must_use]
pub fn serve_repair(&self) -> std::option::Option<&str> { pub fn serve_repair(&self) -> std::option::Option<&str> {
return self.serve_repair.as_deref(); return self.serve_repair.as_deref();
} }
/// Returns the optional shred version. /// Returns the optional shred version.
#[must_use] #[must_use]
pub const fn shred_version(&self) -> std::option::Option<u16> { pub const fn shred_version(&self) -> std::option::Option<u16> {
return self.shred_version; return self.shred_version;
} }
/// Returns the optional TPU endpoint. /// Returns the optional TPU endpoint.
#[must_use] #[must_use]
pub fn tpu(&self) -> std::option::Option<&str> { pub fn tpu(&self) -> std::option::Option<&str> {
return self.tpu.as_deref(); return self.tpu.as_deref();
} }
/// Returns the optional TPU forwards endpoint. /// Returns the optional TPU forwards endpoint.
#[must_use] #[must_use]
pub fn tpu_forwards(&self) -> std::option::Option<&str> { pub fn tpu_forwards(&self) -> std::option::Option<&str> {
return self.tpu_forwards.as_deref(); return self.tpu_forwards.as_deref();
} }
/// Returns the optional TPU forwards QUIC endpoint. /// Returns the optional TPU forwards QUIC endpoint.
#[must_use] #[must_use]
pub fn tpu_forwards_quic(&self) -> std::option::Option<&str> { pub fn tpu_forwards_quic(&self) -> std::option::Option<&str> {
return self.tpu_forwards_quic.as_deref(); return self.tpu_forwards_quic.as_deref();
} }
/// Returns the optional TPU QUIC endpoint. /// Returns the optional TPU QUIC endpoint.
#[must_use] #[must_use]
pub fn tpu_quic(&self) -> std::option::Option<&str> { pub fn tpu_quic(&self) -> std::option::Option<&str> {
return self.tpu_quic.as_deref(); return self.tpu_quic.as_deref();
} }
/// Returns the optional TPU vote endpoint. /// Returns the optional TPU vote endpoint.
#[must_use] #[must_use]
pub fn tpu_vote(&self) -> std::option::Option<&str> { pub fn tpu_vote(&self) -> std::option::Option<&str> {
return self.tpu_vote.as_deref(); return self.tpu_vote.as_deref();
} }
/// Returns the optional TVU endpoint. /// Returns the optional TVU endpoint.
#[must_use] #[must_use]
pub fn tvu(&self) -> std::option::Option<&str> { pub fn tvu(&self) -> std::option::Option<&str> {
return self.tvu.as_deref(); return self.tvu.as_deref();
} }
/// Returns the optional software-version string. /// Returns the optional software-version string.
#[must_use] #[must_use]
pub fn version(&self) -> std::option::Option<&str> { pub fn version(&self) -> std::option::Option<&str> {
return self.version.as_deref(); return self.version.as_deref();
} }
/// Returns the optional Agave client identifier extension. /// Returns the optional Agave client identifier extension.
#[must_use] #[must_use]
pub fn client_id(&self) -> std::option::Option<&str> { pub fn client_id(&self) -> std::option::Option<&str> {
@@ -149,26 +163,31 @@ impl SolanaEpochInfo {
pub const fn absolute_slot(&self) -> u64 { pub const fn absolute_slot(&self) -> u64 {
return self.absolute_slot; return self.absolute_slot;
} }
/// Returns the block height. /// Returns the block height.
#[must_use] #[must_use]
pub const fn block_height(&self) -> u64 { pub const fn block_height(&self) -> u64 {
return self.block_height; return self.block_height;
} }
/// Returns the epoch number. /// Returns the epoch number.
#[must_use] #[must_use]
pub const fn epoch(&self) -> u64 { pub const fn epoch(&self) -> u64 {
return self.epoch; return self.epoch;
} }
/// Returns the slot index within the epoch. /// Returns the slot index within the epoch.
#[must_use] #[must_use]
pub const fn slot_index(&self) -> u64 { pub const fn slot_index(&self) -> u64 {
return self.slot_index; return self.slot_index;
} }
/// Returns the number of slots in the epoch. /// Returns the number of slots in the epoch.
#[must_use] #[must_use]
pub const fn slots_in_epoch(&self) -> u64 { pub const fn slots_in_epoch(&self) -> u64 {
return self.slots_in_epoch; return self.slots_in_epoch;
} }
/// Returns the nullable transaction count. /// Returns the nullable transaction count.
#[must_use] #[must_use]
pub const fn transaction_count(&self) -> std::option::Option<u64> { pub const fn transaction_count(&self) -> std::option::Option<u64> {
@@ -208,21 +227,25 @@ impl SolanaEpochSchedule {
pub const fn first_normal_epoch(&self) -> u64 { pub const fn first_normal_epoch(&self) -> u64 {
return self.first_normal_epoch; return self.first_normal_epoch;
} }
/// Returns the first normal slot. /// Returns the first normal slot.
#[must_use] #[must_use]
pub const fn first_normal_slot(&self) -> u64 { pub const fn first_normal_slot(&self) -> u64 {
return self.first_normal_slot; return self.first_normal_slot;
} }
/// Returns the leader-schedule slot offset. /// Returns the leader-schedule slot offset.
#[must_use] #[must_use]
pub const fn leader_schedule_slot_offset(&self) -> u64 { pub const fn leader_schedule_slot_offset(&self) -> u64 {
return self.leader_schedule_slot_offset; return self.leader_schedule_slot_offset;
} }
/// Returns the number of slots per epoch. /// Returns the number of slots per epoch.
#[must_use] #[must_use]
pub const fn slots_per_epoch(&self) -> u64 { pub const fn slots_per_epoch(&self) -> u64 {
return self.slots_per_epoch; return self.slots_per_epoch;
} }
/// Returns whether epoch warmup is enabled. /// Returns whether epoch warmup is enabled.
#[must_use] #[must_use]
pub const fn warmup(&self) -> bool { pub const fn warmup(&self) -> bool {
@@ -258,6 +281,7 @@ impl SolanaSnapshotSlotInfo {
pub const fn full(&self) -> u64 { pub const fn full(&self) -> u64 {
return self.full; return self.full;
} }
/// Returns the optional highest incremental snapshot slot. /// Returns the optional highest incremental snapshot slot.
#[must_use] #[must_use]
pub const fn incremental(&self) -> std::option::Option<u64> { pub const fn incremental(&self) -> std::option::Option<u64> {
@@ -287,19 +311,25 @@ impl SolanaLeaderScheduleConfig {
pub const fn new(identity: std::option::Option<ksp_core_lib::Pubkey>, commitment: std::option::Option<crate::SolanaCommitment>) -> Self { pub const fn new(identity: std::option::Option<ksp_core_lib::Pubkey>, commitment: std::option::Option<crate::SolanaCommitment>) -> Self {
return Self { identity, commitment }; return Self { identity, commitment };
} }
/// Returns the optional validator identity filter. /// Returns the optional validator identity filter.
#[must_use] #[must_use]
pub const fn identity(&self) -> std::option::Option<&ksp_core_lib::Pubkey> { pub const fn identity(&self) -> std::option::Option<&ksp_core_lib::Pubkey> {
return self.identity.as_ref(); return self.identity.as_ref();
} }
/// Returns the optional commitment level. /// Returns the optional commitment level.
#[must_use] #[must_use]
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> { pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
return self.commitment; return self.commitment;
} }
/// Returns whether empty.
pub(crate) const fn is_empty(&self) -> bool { pub(crate) const fn is_empty(&self) -> bool {
return self.identity.is_none() && self.commitment.is_none(); return self.identity.is_none() && self.commitment.is_none();
} }
/// Executes the crate-internal to json value operation for `SolanaLeaderScheduleConfig`.
pub(crate) fn to_json_value(&self) -> serde_json::Value { pub(crate) fn to_json_value(&self) -> serde_json::Value {
let mut object = serde_json::Map::new(); let mut object = serde_json::Map::new();
if let std::option::Option::Some(identity) = self.identity.as_ref() { if let std::option::Option::Some(identity) = self.identity.as_ref() {
@@ -400,29 +430,36 @@ impl SolanaVoteAccountsConfig {
) -> Self { ) -> Self {
return Self { commitment, vote_pubkey, keep_unstaked_delinquents, delinquent_slot_distance }; return Self { commitment, vote_pubkey, keep_unstaked_delinquents, delinquent_slot_distance };
} }
/// Returns the optional commitment. /// Returns the optional commitment.
#[must_use] #[must_use]
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> { pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
return self.commitment; return self.commitment;
} }
/// Returns the optional vote-account public key filter. /// Returns the optional vote-account public key filter.
#[must_use] #[must_use]
pub const fn vote_pubkey(&self) -> std::option::Option<&ksp_core_lib::Pubkey> { pub const fn vote_pubkey(&self) -> std::option::Option<&ksp_core_lib::Pubkey> {
return self.vote_pubkey.as_ref(); return self.vote_pubkey.as_ref();
} }
/// Returns whether unstaked delinquent validators should be kept. /// Returns whether unstaked delinquent validators should be kept.
#[must_use] #[must_use]
pub const fn keep_unstaked_delinquents(&self) -> std::option::Option<bool> { pub const fn keep_unstaked_delinquents(&self) -> std::option::Option<bool> {
return self.keep_unstaked_delinquents; return self.keep_unstaked_delinquents;
} }
/// Returns the optional delinquent slot distance. /// Returns the optional delinquent slot distance.
#[must_use] #[must_use]
pub const fn delinquent_slot_distance(&self) -> std::option::Option<u64> { pub const fn delinquent_slot_distance(&self) -> std::option::Option<u64> {
return self.delinquent_slot_distance; return self.delinquent_slot_distance;
} }
/// Returns whether empty.
pub(crate) const fn is_empty(&self) -> bool { pub(crate) const fn is_empty(&self) -> bool {
return self.commitment.is_none() && self.vote_pubkey.is_none() && self.keep_unstaked_delinquents.is_none() && self.delinquent_slot_distance.is_none(); return self.commitment.is_none() && self.vote_pubkey.is_none() && self.keep_unstaked_delinquents.is_none() && self.delinquent_slot_distance.is_none();
} }
/// Serializes this config to the Solana JSON-RPC wire object. /// Serializes this config to the Solana JSON-RPC wire object.
#[must_use] #[must_use]
pub(crate) fn to_json_value(&self) -> serde_json::Value { pub(crate) fn to_json_value(&self) -> serde_json::Value {
@@ -457,11 +494,13 @@ impl SolanaEpochCredits {
pub const fn epoch(&self) -> u64 { pub const fn epoch(&self) -> u64 {
return self.epoch; return self.epoch;
} }
/// Returns cumulative credits at the end of the epoch. /// Returns cumulative credits at the end of the epoch.
#[must_use] #[must_use]
pub const fn credits(&self) -> u64 { pub const fn credits(&self) -> u64 {
return self.credits; return self.credits;
} }
/// Returns cumulative credits before the epoch. /// Returns cumulative credits before the epoch.
#[must_use] #[must_use]
pub const fn previous_credits(&self) -> u64 { pub const fn previous_credits(&self) -> u64 {
@@ -489,41 +528,49 @@ impl SolanaVoteAccountInfo {
pub const fn vote_pubkey(&self) -> &ksp_core_lib::Pubkey { pub const fn vote_pubkey(&self) -> &ksp_core_lib::Pubkey {
return &self.vote_pubkey; return &self.vote_pubkey;
} }
/// Returns the validator identity public key. /// Returns the validator identity public key.
#[must_use] #[must_use]
pub const fn node_pubkey(&self) -> &ksp_core_lib::Pubkey { pub const fn node_pubkey(&self) -> &ksp_core_lib::Pubkey {
return &self.node_pubkey; return &self.node_pubkey;
} }
/// Returns the activated stake in lamports. /// Returns the activated stake in lamports.
#[must_use] #[must_use]
pub const fn activated_stake(&self) -> u64 { pub const fn activated_stake(&self) -> u64 {
return self.activated_stake; return self.activated_stake;
} }
/// Returns the legacy/effective percentage commission field. /// Returns the legacy/effective percentage commission field.
#[must_use] #[must_use]
pub const fn commission(&self) -> u8 { pub const fn commission(&self) -> u8 {
return self.commission; return self.commission;
} }
/// Returns the optional raw inflation-rewards commission in basis points. /// Returns the optional raw inflation-rewards commission in basis points.
#[must_use] #[must_use]
pub const fn inflation_rewards_commission_bps(&self) -> std::option::Option<u16> { pub const fn inflation_rewards_commission_bps(&self) -> std::option::Option<u16> {
return self.inflation_rewards_commission_bps; return self.inflation_rewards_commission_bps;
} }
/// Returns whether the vote account is staked for the current epoch. /// Returns whether the vote account is staked for the current epoch.
#[must_use] #[must_use]
pub const fn epoch_vote_account(&self) -> bool { pub const fn epoch_vote_account(&self) -> bool {
return self.epoch_vote_account; return self.epoch_vote_account;
} }
/// Returns the bounded RPC epoch-credit history. /// Returns the bounded RPC epoch-credit history.
#[must_use] #[must_use]
pub fn epoch_credits(&self) -> &[crate::SolanaEpochCredits] { pub fn epoch_credits(&self) -> &[crate::SolanaEpochCredits] {
return self.epoch_credits.as_slice(); return self.epoch_credits.as_slice();
} }
/// Returns the latest voted slot or zero when no vote exists. /// Returns the latest voted slot or zero when no vote exists.
#[must_use] #[must_use]
pub const fn last_vote(&self) -> u64 { pub const fn last_vote(&self) -> u64 {
return self.last_vote; return self.last_vote;
} }
/// Returns the current root slot or zero when no root exists. /// Returns the current root slot or zero when no root exists.
#[must_use] #[must_use]
pub const fn root_slot(&self) -> u64 { pub const fn root_slot(&self) -> u64 {
@@ -578,6 +625,7 @@ impl SolanaVoteAccountStatus {
pub fn current(&self) -> &[crate::SolanaVoteAccountInfo] { pub fn current(&self) -> &[crate::SolanaVoteAccountInfo] {
return self.current.as_slice(); return self.current.as_slice();
} }
/// Returns delinquent vote accounts. /// Returns delinquent vote accounts.
#[must_use] #[must_use]
pub fn delinquent(&self) -> &[crate::SolanaVoteAccountInfo] { pub fn delinquent(&self) -> &[crate::SolanaVoteAccountInfo] {
@@ -807,55 +855,6 @@ impl crate::HttpTransportPool {
} }
} }
fn push_context_config(params: &mut std::vec::Vec<serde_json::Value>, config: std::option::Option<&crate::SolanaContextConfig>) {
if let std::option::Option::Some(config) = config
&& (config.commitment().is_some() || config.min_context_slot().is_some())
{
params.push((*config).to_json_value());
}
return;
}
fn decode_pubkey_list(method: &str, field: &'static str, value: serde_json::Value) -> ksp_core_lib::Result<std::vec::Vec<ksp_core_lib::Pubkey>> {
let decoded = crate::decode_wire_json::<std::vec::Vec<std::string::String>>(method, value);
let values = match decoded {
std::result::Result::Ok(values) => values,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut pubkeys = std::vec::Vec::with_capacity(values.len());
for value in values {
let pubkey = crate::parse_wire_pubkey(method, field, value.as_str());
match pubkey {
std::result::Result::Ok(pubkey) => pubkeys.push(pubkey),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
}
return std::result::Result::Ok(pubkeys);
}
fn invalid_cluster_parameters<T>(method: &str, message: &str, field: &'static str, value: u64) -> ksp_core_lib::Result<T> {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, message)
.with_context("rpc_method", method)
.with_context(field, value.to_string()),
);
}
fn cluster_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> {
let descriptor = crate::find_http_rpc_method(method);
return match descriptor {
std::option::Option::Some(descriptor)
if descriptor.category() == crate::HttpRpcCategory::Cluster && descriptor.coverage_release() == crate::HttpRpcCoverageRelease::V0_2_2 =>
{
std::result::Result::Ok(descriptor)
},
_ => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Cluster descriptor is missing from the audited 0.2.2 registry")
.with_context("rpc_method", method),
),
};
}
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct WireClusterNode { struct WireClusterNode {
@@ -938,6 +937,55 @@ struct WireVoteAccountStatus {
delinquent: std::vec::Vec<serde_json::Value>, delinquent: std::vec::Vec<serde_json::Value>,
} }
fn push_context_config(params: &mut std::vec::Vec<serde_json::Value>, config: std::option::Option<&crate::SolanaContextConfig>) {
if let std::option::Option::Some(config) = config
&& (config.commitment().is_some() || config.min_context_slot().is_some())
{
params.push((*config).to_json_value());
}
return;
}
fn decode_pubkey_list(method: &str, field: &'static str, value: serde_json::Value) -> ksp_core_lib::Result<std::vec::Vec<ksp_core_lib::Pubkey>> {
let decoded = crate::decode_wire_json::<std::vec::Vec<std::string::String>>(method, value);
let values = match decoded {
std::result::Result::Ok(values) => values,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut pubkeys = std::vec::Vec::with_capacity(values.len());
for value in values {
let pubkey = crate::parse_wire_pubkey(method, field, value.as_str());
match pubkey {
std::result::Result::Ok(pubkey) => pubkeys.push(pubkey),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
}
return std::result::Result::Ok(pubkeys);
}
fn invalid_cluster_parameters<T>(method: &str, message: &str, field: &'static str, value: u64) -> ksp_core_lib::Result<T> {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, message)
.with_context("rpc_method", method)
.with_context(field, value.to_string()),
);
}
fn cluster_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> {
let descriptor = crate::find_http_rpc_method(method);
return match descriptor {
std::option::Option::Some(descriptor)
if descriptor.category() == crate::HttpRpcCategory::Cluster && descriptor.coverage_release() == crate::HttpRpcCoverageRelease::V0_2_2 =>
{
std::result::Result::Ok(descriptor)
},
_ => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Cluster descriptor is missing from the audited 0.2.2 registry")
.with_context("rpc_method", method),
),
};
}
#[cfg(test)] #[cfg(test)]
#[path = "../unit_tests/rpc_cluster.rs"] #[path = "../unit_tests/rpc_cluster.rs"]
mod tests; mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_common.rs // file: crates/ksp-onchain-transport-lib/src/rpc_common.rs
// version: 4 // version: 5
/// Commitment level accepted by typed Solana HTTP RPC adapters. /// Commitment level accepted by typed Solana HTTP RPC adapters.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -155,6 +155,13 @@ impl<T> SolanaRpcResponse<T> {
} }
} }
#[derive(serde::Deserialize)]
struct WireRpcContext {
slot: u64,
#[serde(rename = "apiVersion", default)]
api_version: std::option::Option<std::string::String>,
}
/// Decodes one private serde wire type and maps shape failures to the shared Transport error domain. /// Decodes one private serde wire type and maps shape failures to the shared Transport error domain.
pub(crate) fn decode_wire_json<T: serde::de::DeserializeOwned>(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<T> { pub(crate) fn decode_wire_json<T: serde::de::DeserializeOwned>(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<T> {
let decoded = serde_json::from_value::<T>(value); let decoded = serde_json::from_value::<T>(value);
@@ -181,13 +188,6 @@ pub(crate) fn parse_wire_pubkey(method: &str, field: &str, value: &str) -> ksp_c
}; };
} }
#[derive(serde::Deserialize)]
struct WireRpcContext {
slot: u64,
#[serde(rename = "apiVersion", default)]
api_version: std::option::Option<std::string::String>,
}
#[cfg(test)] #[cfg(test)]
#[path = "../unit_tests/rpc_common.rs"] #[path = "../unit_tests/rpc_common.rs"]
mod tests; mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_economics.rs // file: crates/ksp-onchain-transport-lib/src/rpc_economics.rs
// version: 5 // version: 6
/// Inflation-governor values returned by `getInflationGovernor`. /// Inflation-governor values returned by `getInflationGovernor`.
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
@@ -441,6 +441,53 @@ impl crate::HttpTransportPool {
} }
} }
#[derive(serde::Deserialize)]
struct WireEconomicsRpcResponse<T> {
context: serde_json::Value,
value: T,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireInflationGovernor {
initial: f64,
terminal: f64,
taper: f64,
foundation: f64,
foundation_term: f64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireInflationRate {
total: f64,
validator: f64,
foundation: f64,
epoch: u64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireInflationReward {
epoch: u64,
effective_slot: u64,
amount: u64,
post_balance: u64,
#[serde(default)]
commission: std::option::Option<u8>,
#[serde(default)]
commission_bps: crate::SolanaWireField<u16>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireSupply {
total: u64,
circulating: u64,
non_circulating: u64,
non_circulating_accounts: std::vec::Vec<std::string::String>,
}
fn push_economics_commitment_config(params: &mut std::vec::Vec<serde_json::Value>, config: std::option::Option<&crate::SolanaCommitmentConfig>) { fn push_economics_commitment_config(params: &mut std::vec::Vec<serde_json::Value>, config: std::option::Option<&crate::SolanaCommitmentConfig>) {
if let std::option::Option::Some(config) = config if let std::option::Option::Some(config) = config
&& config.commitment().is_some() && config.commitment().is_some()
@@ -539,53 +586,6 @@ fn economics_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::Ht
}; };
} }
#[derive(serde::Deserialize)]
struct WireEconomicsRpcResponse<T> {
context: serde_json::Value,
value: T,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireInflationGovernor {
initial: f64,
terminal: f64,
taper: f64,
foundation: f64,
foundation_term: f64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireInflationRate {
total: f64,
validator: f64,
foundation: f64,
epoch: u64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireInflationReward {
epoch: u64,
effective_slot: u64,
amount: u64,
post_balance: u64,
#[serde(default)]
commission: std::option::Option<u8>,
#[serde(default)]
commission_bps: crate::SolanaWireField<u16>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireSupply {
total: u64,
circulating: u64,
non_circulating: u64,
non_circulating_accounts: std::vec::Vec<std::string::String>,
}
#[cfg(test)] #[cfg(test)]
#[path = "../unit_tests/rpc_economics.rs"] #[path = "../unit_tests/rpc_economics.rs"]
mod tests; mod tests;

View File

@@ -1,281 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_method.rs // file: crates/ksp-onchain-transport-lib/src/rpc_method.rs
// version: 2 // version: 5
/// Functional category used by the audited Solana HTTP JSON-RPC registry.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum HttpRpcCategory {
/// Account state and rent queries.
Accounts,
/// SPL token-oriented RPC queries exposed by the standard Solana HTTP surface.
Tokens,
/// Transaction, signature, fee, simulation and submission methods.
Transactions,
/// Block, slot-history and performance-sample methods.
Blocks,
/// Cluster identity, epoch, leader, health and validator methods.
Cluster,
/// Supply, inflation and stake-economics methods.
Economics,
/// Historically documented methods removed from the targeted Agave runtime generation.
Historical,
}
/// Documentation lifecycle status of one audited RPC method.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum RpcDocumentationStatus {
/// The method is documented as stable.
Stable,
/// The method is documented as deprecated or obsolete.
Deprecated,
/// The method is documented as unstable or experimental.
Unstable,
}
impl RpcDocumentationStatus {
/// Returns the stable machine-readable status code.
#[must_use]
pub const fn code(self) -> &'static str {
return match self {
Self::Stable => "stable",
Self::Deprecated => "deprecated",
Self::Unstable => "unstable",
};
}
}
/// Runtime availability status of one audited RPC method.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum RpcRuntimeStatus {
/// The targeted runtime generation still supports the method.
Supported,
/// The method is historically documented but removed from the targeted runtime generation.
Removed,
}
impl RpcRuntimeStatus {
/// Returns the stable machine-readable runtime status code.
#[must_use]
pub const fn code(self) -> &'static str {
return match self {
Self::Supported => "supported",
Self::Removed => "removed",
};
}
}
/// Request-form policy attached to a stable RPC method.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum RpcRequestFormStatus {
/// Only the currently documented stable request form is tracked by KSP.
Stable,
/// The method is stable but also has a documented deprecated legacy request form that must warn when explicitly used.
StableWithDeprecatedLegacy,
}
impl RpcRequestFormStatus {
/// Returns whether the method has a documented deprecated legacy request form.
#[must_use]
pub const fn has_deprecated_legacy(self) -> bool {
return match self {
Self::Stable => false,
Self::StableWithDeprecatedLegacy => true,
};
}
}
/// Technical operation kind used to separate reads, simulations and submissions.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum RpcOperationKind {
/// Read-only RPC operation.
Read,
/// Simulation operation that does not submit a transaction for execution.
Simulation,
/// Technical write/submission operation whose ambiguous post-dispatch outcome must not be resent automatically.
WriteSubmission,
}
/// HTTP transport retry classification attached to an RPC method descriptor.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum TransportRetryClass {
/// The identical transport request may be retried when the transport failure is classified as retryable.
RetrySafe,
/// The request must not be resent automatically after an ambiguous dispatch.
NeverAfterDispatch,
/// Retry classification does not apply because the method is not callable on the targeted runtime.
NotApplicable,
}
/// Release that owns the typed KSP coverage for one audited current HTTP method.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum HttpRpcCoverageRelease {
/// `0.2.1` HTTP foundation and four canary methods.
V0_2_1,
/// `0.2.2` Accounts + Tokens + remaining Cluster methods.
V0_2_2,
/// `0.2.3` Transactions methods.
V0_2_3,
/// `0.2.4` Blocks + Economics methods and final HTTP compliance.
V0_2_4,
/// Historical registry entry with no callable typed release.
Historical,
}
/// Immutable audited descriptor for one Solana HTTP JSON-RPC method.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct HttpRpcMethodDescriptor {
method: &'static str,
category: crate::HttpRpcCategory,
request_kind: &'static str,
documentation_status: crate::RpcDocumentationStatus,
runtime_status: crate::RpcRuntimeStatus,
request_form_status: crate::RpcRequestFormStatus,
operation_kind: crate::RpcOperationKind,
transport_retry_class: crate::TransportRetryClass,
replacement: std::option::Option<&'static str>,
coverage_release: crate::HttpRpcCoverageRelease,
}
impl HttpRpcMethodDescriptor {
const fn new(
method: &'static str,
category: crate::HttpRpcCategory,
request_kind: &'static str,
documentation_status: crate::RpcDocumentationStatus,
runtime_status: crate::RpcRuntimeStatus,
request_form_status: crate::RpcRequestFormStatus,
operation_kind: crate::RpcOperationKind,
transport_retry_class: crate::TransportRetryClass,
replacement: std::option::Option<&'static str>,
coverage_release: crate::HttpRpcCoverageRelease,
) -> Self {
return Self {
method,
category,
request_kind,
documentation_status,
runtime_status,
request_form_status,
operation_kind,
transport_retry_class,
replacement,
coverage_release,
};
}
/// Returns the exact JSON-RPC method name.
#[must_use]
pub const fn method(&self) -> &'static str {
return self.method;
}
/// Returns the audited functional category.
#[must_use]
pub const fn category(&self) -> crate::HttpRpcCategory {
return self.category;
}
/// Returns the stable open request-kind descriptor used by future endpoint role matching.
#[must_use]
pub const fn request_kind(&self) -> &'static str {
return self.request_kind;
}
/// Returns the method documentation lifecycle status.
#[must_use]
pub const fn documentation_status(&self) -> crate::RpcDocumentationStatus {
return self.documentation_status;
}
/// Returns runtime availability on the targeted Agave generation.
#[must_use]
pub const fn runtime_status(&self) -> crate::RpcRuntimeStatus {
return self.runtime_status;
}
/// Returns the request-form policy, including whether a documented deprecated legacy form exists.
#[must_use]
pub const fn request_form_status(&self) -> crate::RpcRequestFormStatus {
return self.request_form_status;
}
/// Returns the technical operation kind.
#[must_use]
pub const fn operation_kind(&self) -> crate::RpcOperationKind {
return self.operation_kind;
}
/// Returns the transport retry classification.
#[must_use]
pub const fn transport_retry_class(&self) -> crate::TransportRetryClass {
return self.transport_retry_class;
}
/// Returns the documented replacement or migration direction when one exists.
#[must_use]
pub const fn replacement(&self) -> std::option::Option<&'static str> {
return self.replacement;
}
/// Returns the release assigned to typed KSP coverage.
#[must_use]
pub const fn coverage_release(&self) -> crate::HttpRpcCoverageRelease {
return self.coverage_release;
}
/// Returns whether calling the method itself must emit a lifecycle warning when runtime support exists.
#[must_use]
pub const fn requires_method_usage_warning(&self) -> bool {
return match self.runtime_status {
crate::RpcRuntimeStatus::Removed => false,
crate::RpcRuntimeStatus::Supported => match self.documentation_status {
crate::RpcDocumentationStatus::Stable => false,
crate::RpcDocumentationStatus::Deprecated | crate::RpcDocumentationStatus::Unstable => true,
},
};
}
/// Checks runtime support and centrally emits the KSP warning required for deprecated, unstable or removed methods.
///
/// Removed methods return [`crate::ERROR_CODE_METHOD_REMOVED`] and are never presented as callable standard RPC operations.
pub fn ensure_runtime_supported(&self) -> ksp_core_lib::Result<()> {
if self.runtime_status == crate::RpcRuntimeStatus::Removed {
let replacement = match self.replacement {
std::option::Option::Some(replacement) => replacement,
std::option::Option::None => "none",
};
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
rpc_method = self.method,
documentation_status = self.documentation_status.code(),
runtime_status = self.runtime_status.code(),
replacement,
"removed Solana HTTP RPC method requested"
);
let mut error = ksp_core_lib::Error::new(crate::ERROR_CODE_METHOD_REMOVED, "Solana HTTP RPC method is removed from the targeted runtime")
.with_context("rpc_method", self.method)
.with_context("documentation_status", self.documentation_status.code());
if let std::option::Option::Some(replacement) = self.replacement {
error = error.with_context("replacement", replacement);
}
return std::result::Result::Err(error);
}
if self.requires_method_usage_warning() {
let replacement = match self.replacement {
std::option::Option::Some(replacement) => replacement,
std::option::Option::None => "none",
};
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
rpc_method = self.method,
documentation_status = self.documentation_status.code(),
runtime_status = self.runtime_status.code(),
replacement,
"non-stable Solana HTTP RPC method requested"
);
}
return std::result::Result::Ok(());
}
}
const CURRENT_HTTP_RPC_METHODS: [crate::HttpRpcMethodDescriptor; 52] = [ const CURRENT_HTTP_RPC_METHODS: [crate::HttpRpcMethodDescriptor; 52] = [
crate::HttpRpcMethodDescriptor::new( crate::HttpRpcMethodDescriptor::new(
@@ -1075,6 +799,282 @@ const HISTORICAL_HTTP_RPC_METHODS: [crate::HttpRpcMethodDescriptor; 14] = [
), ),
]; ];
/// Functional category used by the audited Solana HTTP JSON-RPC registry.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum HttpRpcCategory {
/// Account state and rent queries.
Accounts,
/// SPL token-oriented RPC queries exposed by the standard Solana HTTP surface.
Tokens,
/// Transaction, signature, fee, simulation and submission methods.
Transactions,
/// Block, slot-history and performance-sample methods.
Blocks,
/// Cluster identity, epoch, leader, health and validator methods.
Cluster,
/// Supply, inflation and stake-economics methods.
Economics,
/// Historically documented methods removed from the targeted Agave runtime generation.
Historical,
}
/// Documentation lifecycle status of one audited RPC method.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum RpcDocumentationStatus {
/// The method is documented as stable.
Stable,
/// The method is documented as deprecated or obsolete.
Deprecated,
/// The method is documented as unstable or experimental.
Unstable,
}
impl RpcDocumentationStatus {
/// Returns the stable machine-readable status code.
#[must_use]
pub const fn code(self) -> &'static str {
return match self {
Self::Stable => "stable",
Self::Deprecated => "deprecated",
Self::Unstable => "unstable",
};
}
}
/// Runtime availability status of one audited RPC method.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum RpcRuntimeStatus {
/// The targeted runtime generation still supports the method.
Supported,
/// The method is historically documented but removed from the targeted runtime generation.
Removed,
}
impl RpcRuntimeStatus {
/// Returns the stable machine-readable runtime status code.
#[must_use]
pub const fn code(self) -> &'static str {
return match self {
Self::Supported => "supported",
Self::Removed => "removed",
};
}
}
/// Request-form policy attached to a stable RPC method.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum RpcRequestFormStatus {
/// Only the currently documented stable request form is tracked by KSP.
Stable,
/// The method is stable but also has a documented deprecated legacy request form that must warn when explicitly used.
StableWithDeprecatedLegacy,
}
impl RpcRequestFormStatus {
/// Returns whether the method has a documented deprecated legacy request form.
#[must_use]
pub const fn has_deprecated_legacy(self) -> bool {
return match self {
Self::Stable => false,
Self::StableWithDeprecatedLegacy => true,
};
}
}
/// Technical operation kind used to separate reads, simulations and submissions.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum RpcOperationKind {
/// Read-only RPC operation.
Read,
/// Simulation operation that does not submit a transaction for execution.
Simulation,
/// Technical write/submission operation whose ambiguous post-dispatch outcome must not be resent automatically.
WriteSubmission,
}
/// HTTP transport retry classification attached to an RPC method descriptor.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum TransportRetryClass {
/// The identical transport request may be retried when the transport failure is classified as retryable.
RetrySafe,
/// The request must not be resent automatically after an ambiguous dispatch.
NeverAfterDispatch,
/// Retry classification does not apply because the method is not callable on the targeted runtime.
NotApplicable,
}
/// Release that owns the typed KSP coverage for one audited current HTTP method.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum HttpRpcCoverageRelease {
/// `0.2.1` HTTP foundation and four canary methods.
V0_2_1,
/// `0.2.2` Accounts + Tokens + remaining Cluster methods.
V0_2_2,
/// `0.2.3` Transactions methods.
V0_2_3,
/// `0.2.4` Blocks + Economics methods and final HTTP compliance.
V0_2_4,
/// Historical registry entry with no callable typed release.
Historical,
}
/// Immutable audited descriptor for one Solana HTTP JSON-RPC method.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct HttpRpcMethodDescriptor {
method: &'static str,
category: crate::HttpRpcCategory,
request_kind: &'static str,
documentation_status: crate::RpcDocumentationStatus,
runtime_status: crate::RpcRuntimeStatus,
request_form_status: crate::RpcRequestFormStatus,
operation_kind: crate::RpcOperationKind,
transport_retry_class: crate::TransportRetryClass,
replacement: std::option::Option<&'static str>,
coverage_release: crate::HttpRpcCoverageRelease,
}
impl HttpRpcMethodDescriptor {
const fn new(
method: &'static str,
category: crate::HttpRpcCategory,
request_kind: &'static str,
documentation_status: crate::RpcDocumentationStatus,
runtime_status: crate::RpcRuntimeStatus,
request_form_status: crate::RpcRequestFormStatus,
operation_kind: crate::RpcOperationKind,
transport_retry_class: crate::TransportRetryClass,
replacement: std::option::Option<&'static str>,
coverage_release: crate::HttpRpcCoverageRelease,
) -> Self {
return Self {
method,
category,
request_kind,
documentation_status,
runtime_status,
request_form_status,
operation_kind,
transport_retry_class,
replacement,
coverage_release,
};
}
/// Returns the exact JSON-RPC method name.
#[must_use]
pub const fn method(&self) -> &'static str {
return self.method;
}
/// Returns the audited functional category.
#[must_use]
pub const fn category(&self) -> crate::HttpRpcCategory {
return self.category;
}
/// Returns the stable open request-kind descriptor used by future endpoint role matching.
#[must_use]
pub const fn request_kind(&self) -> &'static str {
return self.request_kind;
}
/// Returns the method documentation lifecycle status.
#[must_use]
pub const fn documentation_status(&self) -> crate::RpcDocumentationStatus {
return self.documentation_status;
}
/// Returns runtime availability on the targeted Agave generation.
#[must_use]
pub const fn runtime_status(&self) -> crate::RpcRuntimeStatus {
return self.runtime_status;
}
/// Returns the request-form policy, including whether a documented deprecated legacy form exists.
#[must_use]
pub const fn request_form_status(&self) -> crate::RpcRequestFormStatus {
return self.request_form_status;
}
/// Returns the technical operation kind.
#[must_use]
pub const fn operation_kind(&self) -> crate::RpcOperationKind {
return self.operation_kind;
}
/// Returns the transport retry classification.
#[must_use]
pub const fn transport_retry_class(&self) -> crate::TransportRetryClass {
return self.transport_retry_class;
}
/// Returns the documented replacement or migration direction when one exists.
#[must_use]
pub const fn replacement(&self) -> std::option::Option<&'static str> {
return self.replacement;
}
/// Returns the release assigned to typed KSP coverage.
#[must_use]
pub const fn coverage_release(&self) -> crate::HttpRpcCoverageRelease {
return self.coverage_release;
}
/// Returns whether calling the method itself must emit a lifecycle warning when runtime support exists.
#[must_use]
pub const fn requires_method_usage_warning(&self) -> bool {
return match self.runtime_status {
crate::RpcRuntimeStatus::Removed => false,
crate::RpcRuntimeStatus::Supported => match self.documentation_status {
crate::RpcDocumentationStatus::Stable => false,
crate::RpcDocumentationStatus::Deprecated | crate::RpcDocumentationStatus::Unstable => true,
},
};
}
/// Checks runtime support and centrally emits the KSP warning required for deprecated, unstable or removed methods.
///
/// Removed methods return [`crate::ERROR_CODE_METHOD_REMOVED`] and are never presented as callable standard RPC operations.
pub fn ensure_runtime_supported(&self) -> ksp_core_lib::Result<()> {
if self.runtime_status == crate::RpcRuntimeStatus::Removed {
let replacement = match self.replacement {
std::option::Option::Some(replacement) => replacement,
std::option::Option::None => "none",
};
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
rpc_method = self.method,
documentation_status = self.documentation_status.code(),
runtime_status = self.runtime_status.code(),
replacement,
"removed Solana HTTP RPC method requested"
);
let mut error = ksp_core_lib::Error::new(crate::ERROR_CODE_METHOD_REMOVED, "Solana HTTP RPC method is removed from the targeted runtime")
.with_context("rpc_method", self.method)
.with_context("documentation_status", self.documentation_status.code());
if let std::option::Option::Some(replacement) = self.replacement {
error = error.with_context("replacement", replacement);
}
return std::result::Result::Err(error);
}
if self.requires_method_usage_warning() {
let replacement = match self.replacement {
std::option::Option::Some(replacement) => replacement,
std::option::Option::None => "none",
};
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
rpc_method = self.method,
documentation_status = self.documentation_status.code(),
runtime_status = self.runtime_status.code(),
replacement,
"non-stable Solana HTTP RPC method requested"
);
}
return std::result::Result::Ok(());
}
}
/// Returns all 52 current Solana HTTP RPC method descriptors audited for the `0.2.1``0.2.4` coverage sequence. /// Returns all 52 current Solana HTTP RPC method descriptors audited for the `0.2.1``0.2.4` coverage sequence.
#[must_use] #[must_use]
pub const fn current_http_rpc_methods() -> &'static [crate::HttpRpcMethodDescriptor] { pub const fn current_http_rpc_methods() -> &'static [crate::HttpRpcMethodDescriptor] {

View File

@@ -1,5 +1,9 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_transactions.rs // file: crates/ksp-onchain-transport-lib/src/rpc_transactions.rs
// version: 8 // version: 10
const MAX_RECENT_PRIORITIZATION_FEE_ACCOUNTS: usize = 128;
const MAX_SIGNATURES_FOR_ADDRESS_LIMIT: usize = 1_000;
const MAX_SIGNATURE_STATUSES: usize = 256;
/// Binary encoding accepted for serialized transaction input payloads. /// Binary encoding accepted for serialized transaction input payloads.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -1083,10 +1087,6 @@ impl SolanaSimulateTransactionResult {
} }
} }
const MAX_RECENT_PRIORITIZATION_FEE_ACCOUNTS: usize = 128;
const MAX_SIGNATURES_FOR_ADDRESS_LIMIT: usize = 1_000;
const MAX_SIGNATURE_STATUSES: usize = 256;
impl crate::HttpTransportPool { impl crate::HttpTransportPool {
/// Executes typed `getFeeForMessage` through the common KSP HTTP transport path. /// Executes typed `getFeeForMessage` through the common KSP HTTP transport path.
pub async fn get_fee_for_message( pub async fn get_fee_for_message(
@@ -1389,7 +1389,6 @@ impl crate::HttpTransportPool {
); );
} }
} }
let mut params = std::vec![serde_json::Value::String(transaction.to_owned())]; let mut params = std::vec![serde_json::Value::String(transaction.to_owned())];
if let std::option::Option::Some(config) = config if let std::option::Option::Some(config) = config
&& !config.is_empty() && !config.is_empty()
@@ -1469,6 +1468,104 @@ impl crate::HttpTransportPool {
} }
} }
#[derive(serde::Deserialize)]
struct WireTransactionRpcResponse {
context: serde_json::Value,
value: serde_json::Value,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireLatestBlockhash {
blockhash: std::string::String,
last_valid_block_height: u64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WirePrioritizationFee {
slot: u64,
prioritization_fee: u64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireSignatureInfo {
signature: std::string::String,
slot: u64,
#[serde(default)]
err: std::option::Option<serde_json::Value>,
#[serde(default)]
memo: std::option::Option<std::string::String>,
#[serde(default)]
block_time: std::option::Option<i64>,
#[serde(default)]
confirmation_status: std::option::Option<std::string::String>,
#[serde(default)]
transaction_index: std::option::Option<u32>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireSignatureStatus {
slot: u64,
#[serde(default)]
confirmations: std::option::Option<u64>,
status: serde_json::Value,
#[serde(default)]
err: std::option::Option<serde_json::Value>,
#[serde(default)]
confirmation_status: std::option::Option<std::string::String>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireConfirmedTransaction {
slot: u64,
transaction: serde_json::Value,
#[serde(default)]
meta: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
block_time: std::option::Option<i64>,
#[serde(default)]
version: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
transaction_index: crate::SolanaWireField<u32>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireSimulateTransactionResult {
#[serde(default)]
err: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
logs: crate::SolanaWireField<std::vec::Vec<std::string::String>>,
#[serde(default)]
accounts: crate::SolanaWireField<std::vec::Vec<std::option::Option<serde_json::Value>>>,
#[serde(default)]
units_consumed: crate::SolanaWireField<u64>,
#[serde(default)]
loaded_accounts_data_size: crate::SolanaWireField<u32>,
#[serde(default)]
return_data: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
inner_instructions: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
replacement_blockhash: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
fee: crate::SolanaWireField<u64>,
#[serde(default)]
pre_balances: crate::SolanaWireField<std::vec::Vec<u64>>,
#[serde(default)]
post_balances: crate::SolanaWireField<std::vec::Vec<u64>>,
#[serde(default)]
pre_token_balances: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
post_token_balances: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
loaded_addresses: crate::SolanaWireField<serde_json::Value>,
}
fn push_transaction_context_config(params: &mut std::vec::Vec<serde_json::Value>, config: std::option::Option<&crate::SolanaContextConfig>) { fn push_transaction_context_config(params: &mut std::vec::Vec<serde_json::Value>, config: std::option::Option<&crate::SolanaContextConfig>) {
if let std::option::Option::Some(config) = config if let std::option::Option::Some(config) = config
&& (config.commitment().is_some() || config.min_context_slot().is_some()) && (config.commitment().is_some() || config.min_context_slot().is_some())
@@ -1584,12 +1681,6 @@ fn transaction_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::
}; };
} }
#[derive(serde::Deserialize)]
struct WireTransactionRpcResponse {
context: serde_json::Value,
value: serde_json::Value,
}
fn invalid_transaction_wire(method: &str, field: &str, message: &'static str) -> ksp_core_lib::Error { fn invalid_transaction_wire(method: &str, field: &str, message: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, message).with_context("rpc_method", method).with_context("field", field); return ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, message).with_context("rpc_method", method).with_context("field", field);
} }
@@ -1711,98 +1802,6 @@ fn decode_replacement_blockhash_field(
}; };
} }
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireLatestBlockhash {
blockhash: std::string::String,
last_valid_block_height: u64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WirePrioritizationFee {
slot: u64,
prioritization_fee: u64,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireSignatureInfo {
signature: std::string::String,
slot: u64,
#[serde(default)]
err: std::option::Option<serde_json::Value>,
#[serde(default)]
memo: std::option::Option<std::string::String>,
#[serde(default)]
block_time: std::option::Option<i64>,
#[serde(default)]
confirmation_status: std::option::Option<std::string::String>,
#[serde(default)]
transaction_index: std::option::Option<u32>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireSignatureStatus {
slot: u64,
#[serde(default)]
confirmations: std::option::Option<u64>,
status: serde_json::Value,
#[serde(default)]
err: std::option::Option<serde_json::Value>,
#[serde(default)]
confirmation_status: std::option::Option<std::string::String>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireConfirmedTransaction {
slot: u64,
transaction: serde_json::Value,
#[serde(default)]
meta: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
block_time: std::option::Option<i64>,
#[serde(default)]
version: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
transaction_index: crate::SolanaWireField<u32>,
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireSimulateTransactionResult {
#[serde(default)]
err: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
logs: crate::SolanaWireField<std::vec::Vec<std::string::String>>,
#[serde(default)]
accounts: crate::SolanaWireField<std::vec::Vec<std::option::Option<serde_json::Value>>>,
#[serde(default)]
units_consumed: crate::SolanaWireField<u64>,
#[serde(default)]
loaded_accounts_data_size: crate::SolanaWireField<u32>,
#[serde(default)]
return_data: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
inner_instructions: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
replacement_blockhash: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
fee: crate::SolanaWireField<u64>,
#[serde(default)]
pre_balances: crate::SolanaWireField<std::vec::Vec<u64>>,
#[serde(default)]
post_balances: crate::SolanaWireField<std::vec::Vec<u64>>,
#[serde(default)]
pre_token_balances: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
post_token_balances: crate::SolanaWireField<serde_json::Value>,
#[serde(default)]
loaded_addresses: crate::SolanaWireField<serde_json::Value>,
}
#[cfg(test)] #[cfg(test)]
#[path = "../unit_tests/rpc_transactions.rs"] #[path = "../unit_tests/rpc_transactions.rs"]
mod tests; mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/settings.rs // file: crates/ksp-onchain-transport-lib/src/settings.rs
// version: 5 // version: 6
/// Runtime HTTP endpoint URL owned by Transport. /// Runtime HTTP endpoint URL owned by Transport.
/// ///
@@ -446,6 +446,7 @@ impl HttpTransportSettings {
} }
} }
/// Validates endpoint settings.
pub(crate) fn validate_endpoint_settings(endpoint: &crate::HttpEndpointSettings) -> ksp_core_lib::Result<()> { pub(crate) fn validate_endpoint_settings(endpoint: &crate::HttpEndpointSettings) -> ksp_core_lib::Result<()> {
return validate_endpoint(endpoint, 0); return validate_endpoint(endpoint, 0);
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs // file: crates/ksp-onchain-transport-lib/tests/public_api.rs
// version: 23 // version: 24
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract. //! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
@@ -442,7 +442,6 @@ fn public_v0_2_4_pre_007_simple_economics_wrappers_are_available_from_crate_root
let _get_inflation_rate = ksp_onchain_transport_lib::HttpTransportPool::get_inflation_rate; let _get_inflation_rate = ksp_onchain_transport_lib::HttpTransportPool::get_inflation_rate;
let _get_stake_minimum_delegation = ksp_onchain_transport_lib::HttpTransportPool::get_stake_minimum_delegation; let _get_stake_minimum_delegation = ksp_onchain_transport_lib::HttpTransportPool::get_stake_minimum_delegation;
let _get_supply = ksp_onchain_transport_lib::HttpTransportPool::get_supply; let _get_supply = ksp_onchain_transport_lib::HttpTransportPool::get_supply;
let supply_config = ksp_onchain_transport_lib::SolanaSupplyConfig::new( let supply_config = ksp_onchain_transport_lib::SolanaSupplyConfig::new(
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Processed), std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Processed),
std::option::Option::Some(false), std::option::Option::Some(false),

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs // file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
// version: 21 // version: 22
//! Release-level completeness canaries for the staged HTTP wrapper sequence. //! Release-level completeness canaries for the staged HTTP wrapper sequence.
@@ -681,7 +681,6 @@ fn release_v0_2_4_pre_009_final_http_inventory_and_coverage_partition_are_exact(
"getSnapshotSlot", "getSnapshotSlot",
"getStakeActivation", "getStakeActivation",
]; ];
let current = ksp_onchain_transport_lib::current_http_rpc_methods(); let current = ksp_onchain_transport_lib::current_http_rpc_methods();
let historical = ksp_onchain_transport_lib::historical_http_rpc_methods(); let historical = ksp_onchain_transport_lib::historical_http_rpc_methods();
let mut actual_current = std::vec::Vec::with_capacity(current.len()); let mut actual_current = std::vec::Vec::with_capacity(current.len());
@@ -691,7 +690,6 @@ fn release_v0_2_4_pre_009_final_http_inventory_and_coverage_partition_are_exact(
let mut v0_2_3 = 0_usize; let mut v0_2_3 = 0_usize;
let mut v0_2_4 = 0_usize; let mut v0_2_4 = 0_usize;
let mut historical_current = 0_usize; let mut historical_current = 0_usize;
for descriptor in current { for descriptor in current {
actual_current.push(descriptor.method()); actual_current.push(descriptor.method());
assert_eq!(descriptor.runtime_status(), ksp_onchain_transport_lib::RpcRuntimeStatus::Supported); assert_eq!(descriptor.runtime_status(), ksp_onchain_transport_lib::RpcRuntimeStatus::Supported);
@@ -719,7 +717,6 @@ fn release_v0_2_4_pre_009_final_http_inventory_and_coverage_partition_are_exact(
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::Historical); assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::Historical);
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::NotApplicable); assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::NotApplicable);
} }
actual_current.sort_unstable(); actual_current.sort_unstable();
actual_historical.sort_unstable(); actual_historical.sort_unstable();
let mut expected_current = expected_current; let mut expected_current = expected_current;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/transport_devnet_smoke.rs // file: crates/ksp-onchain-transport-lib/tests/transport_devnet_smoke.rs
// version: 5 // version: 6
//! Opt-in live Devnet smoke for representative pure Transport reads across the complete HTTP typed surface. //! Opt-in live Devnet smoke for representative pure Transport reads across the complete HTTP typed surface.
@@ -45,7 +45,6 @@ fn devnet_pool() -> ksp_core_lib::Result<ksp_onchain_transport_lib::HttpTranspor
async fn programmatic_devnet_transport_reaches_accounts_tokens_cluster_transactions_blocks_and_economics_reads() { async fn programmatic_devnet_transport_reaches_accounts_tokens_cluster_transactions_blocks_and_economics_reads() {
let pool = devnet_pool().expect("programmatic Devnet Transport settings must construct a pool"); let pool = devnet_pool().expect("programmatic Devnet Transport settings must construct a pool");
let role = ksp_onchain_transport_lib::HttpRoleName::new("default"); let role = ksp_onchain_transport_lib::HttpRoleName::new("default");
let account_config = ksp_onchain_transport_lib::SolanaAccountInfoConfig::new( let account_config = ksp_onchain_transport_lib::SolanaAccountInfoConfig::new(
std::option::Option::Some(ksp_onchain_transport_lib::SolanaAccountEncoding::Base64), std::option::Option::Some(ksp_onchain_transport_lib::SolanaAccountEncoding::Base64),
std::option::Option::None, std::option::Option::None,
@@ -58,7 +57,6 @@ async fn programmatic_devnet_transport_reaches_accounts_tokens_cluster_transacti
.expect("Devnet getAccountInfo smoke must succeed"); .expect("Devnet getAccountInfo smoke must succeed");
assert!(account.context().slot() > 0); assert!(account.context().slot() > 0);
assert!(account.value().is_some()); assert!(account.value().is_some());
// Follow the current official Devnet example shape with an ordinary owner, the canonical SPL Token program selector, // Follow the current official Devnet example shape with an ordinary owner, the canonical SPL Token program selector,
// and an explicit finalized/jsonParsed config. The owner need not retain any token account; an empty list remains valid. // and an explicit finalized/jsonParsed config. The owner need not retain any token account; an empty list remains valid.
let token_owner = "A1TMhSGzQxMr1TboBKtgixKz1sS6REASMxPo1qsyTSJd" let token_owner = "A1TMhSGzQxMr1TboBKtgixKz1sS6REASMxPo1qsyTSJd"
@@ -79,7 +77,6 @@ async fn programmatic_devnet_transport_reaches_accounts_tokens_cluster_transacti
.await .await
.expect("Devnet getTokenAccountsByOwner smoke must succeed with the documented finalized/jsonParsed request shape"); .expect("Devnet getTokenAccountsByOwner smoke must succeed with the documented finalized/jsonParsed request shape");
assert!(token_accounts.context().slot() > 0); assert!(token_accounts.context().slot() > 0);
let epoch = pool let epoch = pool
.get_epoch_info( .get_epoch_info(
&role, &role,
@@ -91,10 +88,8 @@ async fn programmatic_devnet_transport_reaches_accounts_tokens_cluster_transacti
.await .await
.expect("Devnet getEpochInfo smoke must succeed"); .expect("Devnet getEpochInfo smoke must succeed");
assert!(epoch.absolute_slot() > 0); assert!(epoch.absolute_slot() > 0);
let vote_accounts = pool.get_vote_accounts(&role, std::option::Option::None).await.expect("Devnet getVoteAccounts smoke must succeed"); let vote_accounts = pool.get_vote_accounts(&role, std::option::Option::None).await.expect("Devnet getVoteAccounts smoke must succeed");
assert!(!vote_accounts.current().is_empty() || !vote_accounts.delinquent().is_empty()); assert!(!vote_accounts.current().is_empty() || !vote_accounts.delinquent().is_empty());
let transaction_context = ksp_onchain_transport_lib::SolanaContextConfig::new( let transaction_context = ksp_onchain_transport_lib::SolanaContextConfig::new(
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized), std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized),
std::option::Option::None, std::option::Option::None,
@@ -105,27 +100,22 @@ async fn programmatic_devnet_transport_reaches_accounts_tokens_cluster_transacti
.expect("Devnet getLatestBlockhash smoke must succeed"); .expect("Devnet getLatestBlockhash smoke must succeed");
assert!(!latest_blockhash.value().blockhash().is_empty()); assert!(!latest_blockhash.value().blockhash().is_empty());
assert!(latest_blockhash.value().last_valid_block_height() > 0); assert!(latest_blockhash.value().last_valid_block_height() > 0);
let blockhash_valid = pool let blockhash_valid = pool
.is_blockhash_valid(&role, latest_blockhash.value().blockhash(), std::option::Option::Some(&transaction_context)) .is_blockhash_valid(&role, latest_blockhash.value().blockhash(), std::option::Option::Some(&transaction_context))
.await .await
.expect("Devnet isBlockhashValid smoke must succeed"); .expect("Devnet isBlockhashValid smoke must succeed");
assert_eq!(blockhash_valid.value(), &true); assert_eq!(blockhash_valid.value(), &true);
let transaction_count = pool let transaction_count = pool
.get_transaction_count(&role, std::option::Option::Some(&transaction_context)) .get_transaction_count(&role, std::option::Option::Some(&transaction_context))
.await .await
.expect("Devnet getTransactionCount smoke must succeed"); .expect("Devnet getTransactionCount smoke must succeed");
assert!(transaction_count > 0); assert!(transaction_count > 0);
let block_height = pool.get_block_height(&role, std::option::Option::Some(&transaction_context)).await.expect("Devnet getBlockHeight smoke must succeed"); let block_height = pool.get_block_height(&role, std::option::Option::Some(&transaction_context)).await.expect("Devnet getBlockHeight smoke must succeed");
assert!(block_height > 0); assert!(block_height > 0);
let inflation_rate = pool.get_inflation_rate(&role).await.expect("Devnet getInflationRate smoke must succeed"); let inflation_rate = pool.get_inflation_rate(&role).await.expect("Devnet getInflationRate smoke must succeed");
assert!(inflation_rate.total().is_finite()); assert!(inflation_rate.total().is_finite());
assert!(inflation_rate.validator().is_finite()); assert!(inflation_rate.validator().is_finite());
assert!(inflation_rate.foundation().is_finite()); assert!(inflation_rate.foundation().is_finite());
let stake_minimum_delegation = pool let stake_minimum_delegation = pool
.get_stake_minimum_delegation(&role, std::option::Option::Some(&transaction_context)) .get_stake_minimum_delegation(&role, std::option::Option::Some(&transaction_context))
.await .await

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/client.rs // file: crates/ksp-onchain-transport-lib/unit_tests/client.rs
// version: 2 // version: 3
fn endpoint(enabled: bool, url_text: &str) -> crate::HttpEndpointSettings { fn endpoint(enabled: bool, url_text: &str) -> crate::HttpEndpointSettings {
let url = crate::HttpEndpointUrl::parse(url_text).expect("test endpoint URL must parse"); let url = crate::HttpEndpointUrl::parse(url_text).expect("test endpoint URL must parse");
@@ -25,7 +25,7 @@ fn endpoint(enabled: bool, url_text: &str) -> crate::HttpEndpointSettings {
#[test] #[test]
fn endpoint_client_snapshot_never_contains_url_or_secret_material() { fn endpoint_client_snapshot_never_contains_url_or_secret_material() {
let client = super::HttpEndpointClient::new(endpoint(true, "https://provider.invalid/rpc?api-key=SECRET-CANARY")).expect("client must build"); let client = crate::HttpEndpointClient::new(endpoint(true, "https://provider.invalid/rpc?api-key=SECRET-CANARY")).expect("client must build");
let snapshot = client.snapshot(); let snapshot = client.snapshot();
let rendered = format!("{snapshot:?} {client:?}"); let rendered = format!("{snapshot:?} {client:?}");
assert_eq!(snapshot.availability(), crate::HttpEndpointAvailability::Available); assert_eq!(snapshot.availability(), crate::HttpEndpointAvailability::Available);
@@ -36,21 +36,21 @@ fn endpoint_client_snapshot_never_contains_url_or_secret_material() {
#[test] #[test]
fn disabled_endpoint_client_is_visible_but_not_selectable() { fn disabled_endpoint_client_is_visible_but_not_selectable() {
let client = super::HttpEndpointClient::new(endpoint(false, "https://api.devnet.solana.com")).expect("disabled client must still build"); let client = crate::HttpEndpointClient::new(endpoint(false, "https://api.devnet.solana.com")).expect("disabled client must still build");
assert_eq!(client.snapshot().availability(), crate::HttpEndpointAvailability::Disabled); assert_eq!(client.snapshot().availability(), crate::HttpEndpointAvailability::Disabled);
assert!(!client.supports(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("get_balance"))); assert!(!client.supports(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("get_balance")));
} }
#[test] #[test]
fn endpoint_client_matches_exact_and_wildcard_capabilities() { fn endpoint_client_matches_exact_and_wildcard_capabilities() {
let client = super::HttpEndpointClient::new(endpoint(true, "https://api.devnet.solana.com")).expect("client must build"); let client = crate::HttpEndpointClient::new(endpoint(true, "https://api.devnet.solana.com")).expect("client must build");
assert!(client.supports(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("get_balance"))); assert!(client.supports(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("get_balance")));
assert!(!client.supports(&crate::HttpRoleName::new("write"), &crate::HttpRequestKind::new("get_balance"))); assert!(!client.supports(&crate::HttpRoleName::new("write"), &crate::HttpRequestKind::new("get_balance")));
} }
#[test] #[test]
fn endpoint_role_snapshot_exposes_safe_resilience_state() { fn endpoint_role_snapshot_exposes_safe_resilience_state() {
let client = super::HttpEndpointClient::new(endpoint(true, "https://api.devnet.solana.com")).expect("client must build"); let client = crate::HttpEndpointClient::new(endpoint(true, "https://api.devnet.solana.com")).expect("client must build");
let snapshot = client.snapshot(); let snapshot = client.snapshot();
let role = &snapshot.roles()[0]; let role = &snapshot.roles()[0];
assert_eq!(role.availability(), crate::HttpEndpointAvailability::Available); assert_eq!(role.availability(), crate::HttpEndpointAvailability::Available);

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/json_rpc.rs // file: crates/ksp-onchain-transport-lib/unit_tests/json_rpc.rs
// version: 2 // version: 3
#[test] #[test]
fn request_serialization_matches_json_rpc_2_0_shape() { fn request_serialization_matches_json_rpc_2_0_shape() {
let request = super::JsonRpcRequest::new(7, "getBalance", std::vec![serde_json::json!("Address111"), serde_json::json!({"commitment":"confirmed"})]) let request = crate::JsonRpcRequest::new(7, "getBalance", std::vec![serde_json::json!("Address111"), serde_json::json!({"commitment":"confirmed"})])
.expect("valid request must construct"); .expect("valid request must construct");
let encoded = request.to_json_string().expect("serializable request must encode"); let encoded = request.to_json_string().expect("serializable request must encode");
let value: serde_json::Value = serde_json::from_str(encoded.as_str()).expect("encoded request must remain JSON"); let value: serde_json::Value = serde_json::from_str(encoded.as_str()).expect("encoded request must remain JSON");
@@ -15,28 +15,28 @@ fn request_serialization_matches_json_rpc_2_0_shape() {
#[test] #[test]
fn request_rejects_empty_or_untrimmed_method() { fn request_rejects_empty_or_untrimmed_method() {
assert!(super::JsonRpcRequest::new(1, "", std::vec![]).is_err()); assert!(crate::JsonRpcRequest::new(1, "", std::vec![]).is_err());
assert!(super::JsonRpcRequest::new(1, " getHealth", std::vec![]).is_err()); assert!(crate::JsonRpcRequest::new(1, " getHealth", std::vec![]).is_err());
} }
#[test] #[test]
fn response_parser_preserves_null_success_result() { fn response_parser_preserves_null_success_result() {
let response = super::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":null,"id":9}"#, 9).expect("null result is a valid success payload"); let response = crate::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":null,"id":9}"#, 9).expect("null result is a valid success payload");
assert!(matches!(&response, super::JsonRpcResponse::Success(_)), "success response must not parse as error"); assert!(matches!(&response, crate::JsonRpcResponse::Success(_)), "success response must not parse as error");
if let super::JsonRpcResponse::Success(success) = response { if let crate::JsonRpcResponse::Success(success) = response {
assert!(success.result().is_null()); assert!(success.result().is_null());
} }
} }
#[test] #[test]
fn response_parser_preserves_rpc_error_payload() { fn response_parser_preserves_rpc_error_payload() {
let response = super::parse_json_rpc_response_text( let response = crate::parse_json_rpc_response_text(
r#"{"jsonrpc":"2.0","error":{"code":-32005,"message":"Node is unhealthy","data":{"numSlotsBehind":12}},"id":4}"#, r#"{"jsonrpc":"2.0","error":{"code":-32005,"message":"Node is unhealthy","data":{"numSlotsBehind":12}},"id":4}"#,
4, 4,
) )
.expect("valid JSON-RPC error envelope must parse"); .expect("valid JSON-RPC error envelope must parse");
assert!(matches!(&response, super::JsonRpcResponse::Error(_)), "RPC error response must not parse as success"); assert!(matches!(&response, crate::JsonRpcResponse::Error(_)), "RPC error response must not parse as success");
if let super::JsonRpcResponse::Error(error_response) = response { if let crate::JsonRpcResponse::Error(error_response) = response {
assert_eq!(error_response.error().code(), -32005); assert_eq!(error_response.error().code(), -32005);
assert_eq!(error_response.error().message(), "Node is unhealthy"); assert_eq!(error_response.error().message(), "Node is unhealthy");
assert_eq!(error_response.error().data(), std::option::Option::Some(&serde_json::json!({"numSlotsBehind":12}))); assert_eq!(error_response.error().data(), std::option::Option::Some(&serde_json::json!({"numSlotsBehind":12})));
@@ -45,38 +45,38 @@ fn response_parser_preserves_rpc_error_payload() {
#[test] #[test]
fn response_parser_rejects_id_mismatch() { fn response_parser_rejects_id_mismatch() {
let error = super::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":"ok","id":2}"#, 1).expect_err("mismatched id must fail"); let error = crate::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":"ok","id":2}"#, 1).expect_err("mismatched id must fail");
assert_eq!(error.code(), crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID); assert_eq!(error.code(), crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID);
} }
#[test] #[test]
fn response_parser_rejects_wrong_protocol_version() { fn response_parser_rejects_wrong_protocol_version() {
let error = super::parse_json_rpc_response_text(r#"{"jsonrpc":"1.0","result":"ok","id":1}"#, 1).expect_err("wrong version must fail"); let error = crate::parse_json_rpc_response_text(r#"{"jsonrpc":"1.0","result":"ok","id":1}"#, 1).expect_err("wrong version must fail");
assert_eq!(error.code(), crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID); assert_eq!(error.code(), crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID);
} }
#[test] #[test]
fn response_parser_rejects_both_result_and_error() { fn response_parser_rejects_both_result_and_error() {
let error = super::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":"ok","error":{"code":-1,"message":"bad"},"id":1}"#, 1) let error = crate::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":"ok","error":{"code":-1,"message":"bad"},"id":1}"#, 1)
.expect_err("mutually exclusive fields must fail"); .expect_err("mutually exclusive fields must fail");
assert_eq!(error.code(), crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID); assert_eq!(error.code(), crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID);
} }
#[test] #[test]
fn response_parser_rejects_missing_result_and_error() { fn response_parser_rejects_missing_result_and_error() {
let error = super::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","id":1}"#, 1).expect_err("missing outcome must fail"); let error = crate::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","id":1}"#, 1).expect_err("missing outcome must fail");
assert_eq!(error.code(), crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID); assert_eq!(error.code(), crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID);
} }
#[test] #[test]
fn response_parser_distinguishes_invalid_json_from_protocol_error() { fn response_parser_distinguishes_invalid_json_from_protocol_error() {
let error = super::parse_json_rpc_response_text("not-json", 1).expect_err("invalid JSON must fail decoding"); let error = crate::parse_json_rpc_response_text("not-json", 1).expect_err("invalid JSON must fail decoding");
assert_eq!(error.code(), crate::ERROR_CODE_JSON_DECODE_FAILED); assert_eq!(error.code(), crate::ERROR_CODE_JSON_DECODE_FAILED);
} }
#[test] #[test]
fn rpc_error_maps_to_shared_ksp_error_without_copying_remote_payload_into_context() { fn rpc_error_maps_to_shared_ksp_error_without_copying_remote_payload_into_context() {
let response = super::parse_json_rpc_response_text( let response = crate::parse_json_rpc_response_text(
r#"{"jsonrpc":"2.0","error":{"code":-32000,"message":"SECRET-CANARY","data":{"payload":"SECRET-DATA"}},"id":1}"#, r#"{"jsonrpc":"2.0","error":{"code":-32000,"message":"SECRET-CANARY","data":{"payload":"SECRET-DATA"}},"id":1}"#,
1, 1,
) )
@@ -90,7 +90,7 @@ fn rpc_error_maps_to_shared_ksp_error_without_copying_remote_payload_into_contex
#[test] #[test]
fn request_debug_omits_parameter_payloads() { fn request_debug_omits_parameter_payloads() {
let request = super::JsonRpcRequest::new(1, "sendTransaction", std::vec![serde_json::json!("SIGNED-TRANSACTION-SECRET-CANARY")]) let request = crate::JsonRpcRequest::new(1, "sendTransaction", std::vec![serde_json::json!("SIGNED-TRANSACTION-SECRET-CANARY")])
.expect("test request must construct"); .expect("test request must construct");
let rendered = format!("{request:?}"); let rendered = format!("{request:?}");
assert!(rendered.contains("sendTransaction")); assert!(rendered.contains("sendTransaction"));
@@ -100,11 +100,11 @@ fn request_debug_omits_parameter_payloads() {
#[test] #[test]
fn response_debug_omits_result_and_remote_error_payloads() { fn response_debug_omits_result_and_remote_error_payloads() {
let success = super::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":{"secret":"RESULT-SECRET-CANARY"},"id":1}"#, 1) let success = crate::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":{"secret":"RESULT-SECRET-CANARY"},"id":1}"#, 1)
.expect("test success response must parse"); .expect("test success response must parse");
let success_rendered = format!("{success:?}"); let success_rendered = format!("{success:?}");
assert!(!success_rendered.contains("RESULT-SECRET-CANARY")); assert!(!success_rendered.contains("RESULT-SECRET-CANARY"));
let failure = super::parse_json_rpc_response_text( let failure = crate::parse_json_rpc_response_text(
r#"{"jsonrpc":"2.0","error":{"code":-32000,"message":"MESSAGE-SECRET-CANARY","data":{"secret":"DATA-SECRET-CANARY"}},"id":2}"#, r#"{"jsonrpc":"2.0","error":{"code":-32000,"message":"MESSAGE-SECRET-CANARY","data":{"secret":"DATA-SECRET-CANARY"}},"id":2}"#,
2, 2,
) )

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/pool.rs // file: crates/ksp-onchain-transport-lib/unit_tests/pool.rs
// version: 3 // version: 4
fn role(name: &str, priority: u32, request_kinds: std::vec::Vec<crate::HttpRequestKind>) -> crate::HttpEndpointRoleSettings { fn role(name: &str, priority: u32, request_kinds: std::vec::Vec<crate::HttpRequestKind>) -> crate::HttpEndpointRoleSettings {
return crate::HttpEndpointRoleSettings::new( return crate::HttpEndpointRoleSettings::new(
@@ -66,7 +66,7 @@ fn settings(endpoints: std::vec::Vec<crate::HttpEndpointSettings>) -> crate::Htt
#[test] #[test]
fn pool_prefers_lowest_priority_tier() { fn pool_prefers_lowest_priority_tier() {
let pool = super::HttpTransportPool::new(settings(std::vec![ let pool = crate::HttpTransportPool::new(settings(std::vec![
endpoint("secondary", true, 20, std::vec![crate::HttpRequestKind::wildcard()]), endpoint("secondary", true, 20, std::vec![crate::HttpRequestKind::wildcard()]),
endpoint("primary", true, 10, std::vec![crate::HttpRequestKind::wildcard()]), endpoint("primary", true, 10, std::vec![crate::HttpRequestKind::wildcard()]),
])) ]))
@@ -80,7 +80,7 @@ fn pool_prefers_lowest_priority_tier() {
#[test] #[test]
fn pool_round_robins_fairly_inside_best_priority_tier() { fn pool_round_robins_fairly_inside_best_priority_tier() {
let pool = super::HttpTransportPool::new(settings(std::vec![ let pool = crate::HttpTransportPool::new(settings(std::vec![
endpoint("one", true, 10, std::vec![crate::HttpRequestKind::wildcard()]), endpoint("one", true, 10, std::vec![crate::HttpRequestKind::wildcard()]),
endpoint("two", true, 10, std::vec![crate::HttpRequestKind::wildcard()]), endpoint("two", true, 10, std::vec![crate::HttpRequestKind::wildcard()]),
endpoint("fallback", true, 20, std::vec![crate::HttpRequestKind::wildcard()]), endpoint("fallback", true, 20, std::vec![crate::HttpRequestKind::wildcard()]),
@@ -98,7 +98,7 @@ fn pool_round_robins_fairly_inside_best_priority_tier() {
#[test] #[test]
fn disabled_best_priority_endpoint_falls_back_to_next_tier() { fn disabled_best_priority_endpoint_falls_back_to_next_tier() {
let pool = super::HttpTransportPool::new(settings(std::vec![ let pool = crate::HttpTransportPool::new(settings(std::vec![
endpoint("disabled-primary", false, 1, std::vec![crate::HttpRequestKind::wildcard()]), endpoint("disabled-primary", false, 1, std::vec![crate::HttpRequestKind::wildcard()]),
endpoint("fallback", true, 20, std::vec![crate::HttpRequestKind::wildcard()]), endpoint("fallback", true, 20, std::vec![crate::HttpRequestKind::wildcard()]),
])) ]))
@@ -111,7 +111,7 @@ fn disabled_best_priority_endpoint_falls_back_to_next_tier() {
#[test] #[test]
fn pool_filters_role_and_capability_before_priority() { fn pool_filters_role_and_capability_before_priority() {
let pool = super::HttpTransportPool::new(settings(std::vec![ let pool = crate::HttpTransportPool::new(settings(std::vec![
endpoint("wrong-capability", true, 1, std::vec![crate::HttpRequestKind::new("send_transaction")]), endpoint("wrong-capability", true, 1, std::vec![crate::HttpRequestKind::new("send_transaction")]),
endpoint("matching", true, 50, std::vec![crate::HttpRequestKind::new("get_balance")]), endpoint("matching", true, 50, std::vec![crate::HttpRequestKind::new("get_balance")]),
])) ]))
@@ -124,7 +124,7 @@ fn pool_filters_role_and_capability_before_priority() {
#[test] #[test]
fn pool_returns_structured_error_when_no_endpoint_matches() { fn pool_returns_structured_error_when_no_endpoint_matches() {
let pool = super::HttpTransportPool::new(settings(std::vec![endpoint("read-only", true, 10, std::vec![crate::HttpRequestKind::new("get_balance")],)])) let pool = crate::HttpTransportPool::new(settings(std::vec![endpoint("read-only", true, 10, std::vec![crate::HttpRequestKind::new("get_balance")],)]))
.expect("pool must build"); .expect("pool must build");
let error = pool let error = pool
.select_for_request_kind(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("send_transaction")) .select_for_request_kind(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("send_transaction"))
@@ -135,7 +135,7 @@ fn pool_returns_structured_error_when_no_endpoint_matches() {
#[test] #[test]
fn standard_method_selection_uses_registry_request_kind() { fn standard_method_selection_uses_registry_request_kind() {
let pool = super::HttpTransportPool::new(settings(std::vec![endpoint("balance", true, 10, std::vec![crate::HttpRequestKind::new("get_balance")],)])) let pool = crate::HttpTransportPool::new(settings(std::vec![endpoint("balance", true, 10, std::vec![crate::HttpRequestKind::new("get_balance")],)]))
.expect("pool must build"); .expect("pool must build");
let method = crate::find_http_rpc_method("getBalance").expect("audited method must exist"); let method = crate::find_http_rpc_method("getBalance").expect("audited method must exist");
let selection = pool.select_for_method(&crate::HttpRoleName::new("default"), method).expect("standard method must route"); let selection = pool.select_for_method(&crate::HttpRoleName::new("default"), method).expect("standard method must route");
@@ -144,7 +144,7 @@ fn standard_method_selection_uses_registry_request_kind() {
#[test] #[test]
fn pool_snapshot_is_safe_and_preserves_disabled_endpoints() { fn pool_snapshot_is_safe_and_preserves_disabled_endpoints() {
let pool = super::HttpTransportPool::new(settings(std::vec![ let pool = crate::HttpTransportPool::new(settings(std::vec![
endpoint("enabled", true, 10, std::vec![crate::HttpRequestKind::wildcard()]), endpoint("enabled", true, 10, std::vec![crate::HttpRequestKind::wildcard()]),
endpoint("disabled", false, 10, std::vec![crate::HttpRequestKind::wildcard()]), endpoint("disabled", false, 10, std::vec![crate::HttpRequestKind::wildcard()]),
])) ]))
@@ -185,7 +185,7 @@ fn disabled_role_is_excluded_before_priority_selection() {
base.max_idle_connections_per_host(), base.max_idle_connections_per_host(),
std::vec![disabled_role, enabled_non_matching_role], std::vec![disabled_role, enabled_non_matching_role],
); );
let pool = super::HttpTransportPool::new(settings(std::vec![ let pool = crate::HttpTransportPool::new(settings(std::vec![
disabled_role_endpoint, disabled_role_endpoint,
endpoint("fallback", true, 20, std::vec![crate::HttpRequestKind::wildcard()]), endpoint("fallback", true, 20, std::vec![crate::HttpRequestKind::wildcard()]),
])) ]))
@@ -198,7 +198,7 @@ fn disabled_role_is_excluded_before_priority_selection() {
#[test] #[test]
fn removed_standard_method_is_rejected_before_endpoint_routing() { fn removed_standard_method_is_rejected_before_endpoint_routing() {
let pool = super::HttpTransportPool::new(settings(std::vec![endpoint("wildcard", true, 10, std::vec![crate::HttpRequestKind::wildcard()],)])) let pool = crate::HttpTransportPool::new(settings(std::vec![endpoint("wildcard", true, 10, std::vec![crate::HttpRequestKind::wildcard()],)]))
.expect("pool must build"); .expect("pool must build");
let method = crate::find_http_rpc_method("confirmTransaction").expect("historical method must exist"); let method = crate::find_http_rpc_method("confirmTransaction").expect("historical method must exist");
let error = pool.select_for_method(&crate::HttpRoleName::new("default"), method).expect_err("removed standard method must be rejected before routing"); let error = pool.select_for_method(&crate::HttpRoleName::new("default"), method).expect_err("removed standard method must be rejected before routing");
@@ -207,7 +207,7 @@ fn removed_standard_method_is_rejected_before_endpoint_routing() {
#[tokio::test] #[tokio::test]
async fn runtime_concurrency_saturation_falls_back_to_lower_priority_tier() { async fn runtime_concurrency_saturation_falls_back_to_lower_priority_tier() {
let pool = super::HttpTransportPool::new(settings(std::vec![ let pool = crate::HttpTransportPool::new(settings(std::vec![
limited_endpoint("primary", 1, std::option::Option::None, std::option::Option::None, std::option::Option::Some(1), std::option::Option::None), limited_endpoint("primary", 1, std::option::Option::None, std::option::Option::None, std::option::Option::Some(1), std::option::Option::None),
limited_endpoint("fallback", 20, std::option::Option::None, std::option::Option::None, std::option::Option::Some(1), std::option::Option::None), limited_endpoint("fallback", 20, std::option::Option::None, std::option::Option::None, std::option::Option::Some(1), std::option::Option::None),
])) ]))
@@ -222,7 +222,7 @@ async fn runtime_concurrency_saturation_falls_back_to_lower_priority_tier() {
#[tokio::test] #[tokio::test]
async fn runtime_token_bucket_exhaustion_falls_back_without_busy_waiting() { async fn runtime_token_bucket_exhaustion_falls_back_without_busy_waiting() {
let pool = super::HttpTransportPool::new(settings(std::vec![ let pool = crate::HttpTransportPool::new(settings(std::vec![
limited_endpoint("primary", 1, std::option::Option::Some(1), std::option::Option::Some(1), std::option::Option::None, std::option::Option::None), limited_endpoint("primary", 1, std::option::Option::Some(1), std::option::Option::Some(1), std::option::Option::None, std::option::Option::None),
limited_endpoint("fallback", 20, std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None), limited_endpoint("fallback", 20, std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
])) ]))
@@ -238,7 +238,7 @@ async fn runtime_token_bucket_exhaustion_falls_back_without_busy_waiting() {
#[tokio::test] #[tokio::test]
async fn provider_cooldown_excludes_rate_limited_role_and_uses_fallback() { async fn provider_cooldown_excludes_rate_limited_role_and_uses_fallback() {
let pool = super::HttpTransportPool::new(settings(std::vec![ let pool = crate::HttpTransportPool::new(settings(std::vec![
limited_endpoint( limited_endpoint(
"primary", "primary",
1, 1,
@@ -263,7 +263,7 @@ async fn provider_cooldown_excludes_rate_limited_role_and_uses_fallback() {
#[tokio::test] #[tokio::test]
async fn admission_waits_for_released_concurrency_without_holding_a_sync_mutex_across_await() { async fn admission_waits_for_released_concurrency_without_holding_a_sync_mutex_across_await() {
let pool = super::HttpTransportPool::new(settings(std::vec![limited_endpoint( let pool = crate::HttpTransportPool::new(settings(std::vec![limited_endpoint(
"primary", "primary",
1, 1,
std::option::Option::None, std::option::Option::None,
@@ -289,7 +289,7 @@ async fn admission_waits_for_released_concurrency_without_holding_a_sync_mutex_a
#[tokio::test] #[tokio::test]
async fn admission_timeout_is_bounded_when_concurrency_never_becomes_available() { async fn admission_timeout_is_bounded_when_concurrency_never_becomes_available() {
let pool = super::HttpTransportPool::new(settings(std::vec![limited_endpoint( let pool = crate::HttpTransportPool::new(settings(std::vec![limited_endpoint(
"primary", "primary",
1, 1,
std::option::Option::None, std::option::Option::None,
@@ -310,7 +310,7 @@ async fn admission_timeout_is_bounded_when_concurrency_never_becomes_available()
#[tokio::test] #[tokio::test]
async fn passive_health_snapshot_moves_from_degraded_back_to_available_after_success() { async fn passive_health_snapshot_moves_from_degraded_back_to_available_after_success() {
let pool = super::HttpTransportPool::new(settings(std::vec![limited_endpoint( let pool = crate::HttpTransportPool::new(settings(std::vec![limited_endpoint(
"primary", "primary",
1, 1,
std::option::Option::None, std::option::Option::None,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/resilience.rs // file: crates/ksp-onchain-transport-lib/unit_tests/resilience.rs
// version: 1 // version: 2
fn non_zero(value: u32) -> std::num::NonZeroU32 { fn non_zero(value: u32) -> std::num::NonZeroU32 {
return std::num::NonZeroU32::new(value).expect("test limit must be non-zero"); return std::num::NonZeroU32::new(value).expect("test limit must be non-zero");
@@ -46,46 +46,46 @@ fn retry_backoff_is_exponential_and_bounded() {
#[test] #[test]
fn retry_safe_timeout_is_retried_until_budget_is_exhausted() { fn retry_safe_timeout_is_retried_until_budget_is_exhausted() {
let settings = retry_settings(); let settings = retry_settings();
let first = super::evaluate_transport_retry( let first = crate::evaluate_transport_retry(
method("getBalance"), method("getBalance"),
&settings, &settings,
super::HttpRetryCause::Timeout, crate::HttpRetryCause::Timeout,
super::HttpDispatchState::DispatchedAmbiguous, crate::HttpDispatchState::DispatchedAmbiguous,
0, 0,
std::option::Option::None, std::option::Option::None,
); );
assert_eq!(first, super::HttpRetryDecision::RetryAfter(std::time::Duration::from_millis(100))); assert_eq!(first, crate::HttpRetryDecision::RetryAfter(std::time::Duration::from_millis(100)));
let exhausted = super::evaluate_transport_retry( let exhausted = crate::evaluate_transport_retry(
method("getBalance"), method("getBalance"),
&settings, &settings,
super::HttpRetryCause::Timeout, crate::HttpRetryCause::Timeout,
super::HttpDispatchState::DispatchedAmbiguous, crate::HttpDispatchState::DispatchedAmbiguous,
settings.max_retries(), settings.max_retries(),
std::option::Option::None, std::option::Option::None,
); );
assert_eq!(exhausted, super::HttpRetryDecision::Stop); assert_eq!(exhausted, crate::HttpRetryDecision::Stop);
} }
#[test] #[test]
fn write_submission_never_retries_after_ambiguous_dispatch() { fn write_submission_never_retries_after_ambiguous_dispatch() {
let decision = super::evaluate_transport_retry( let decision = crate::evaluate_transport_retry(
method("sendTransaction"), method("sendTransaction"),
&retry_settings(), &retry_settings(),
super::HttpRetryCause::Connection, crate::HttpRetryCause::Connection,
super::HttpDispatchState::DispatchedAmbiguous, crate::HttpDispatchState::DispatchedAmbiguous,
0, 0,
std::option::Option::None, std::option::Option::None,
); );
assert_eq!(decision, super::HttpRetryDecision::Stop); assert_eq!(decision, crate::HttpRetryDecision::Stop);
} }
#[test] #[test]
fn write_submission_can_retry_when_transport_proves_no_dispatch() { fn write_submission_can_retry_when_transport_proves_no_dispatch() {
let decision = super::evaluate_transport_retry( let decision = crate::evaluate_transport_retry(
method("sendTransaction"), method("sendTransaction"),
&retry_settings(), &retry_settings(),
super::HttpRetryCause::Connection, crate::HttpRetryCause::Connection,
super::HttpDispatchState::NotDispatched, crate::HttpDispatchState::NotDispatched,
0, 0,
std::option::Option::None, std::option::Option::None,
); );
@@ -94,36 +94,36 @@ fn write_submission_can_retry_when_transport_proves_no_dispatch() {
#[test] #[test]
fn rpc_application_and_invalid_response_are_not_transport_retries() { fn rpc_application_and_invalid_response_are_not_transport_retries() {
for cause in [super::HttpRetryCause::RpcApplication, super::HttpRetryCause::InvalidResponse, super::HttpRetryCause::Request] { for cause in [crate::HttpRetryCause::RpcApplication, crate::HttpRetryCause::InvalidResponse, crate::HttpRetryCause::Request] {
let decision = super::evaluate_transport_retry( let decision = crate::evaluate_transport_retry(
method("getBalance"), method("getBalance"),
&retry_settings(), &retry_settings(),
cause, cause,
super::HttpDispatchState::NotDispatched, crate::HttpDispatchState::NotDispatched,
0, 0,
std::option::Option::None, std::option::Option::None,
); );
assert_eq!(decision, super::HttpRetryDecision::Stop); assert_eq!(decision, crate::HttpRetryDecision::Stop);
} }
} }
#[test] #[test]
fn provider_retry_after_can_extend_backoff_but_is_defensively_bounded() { fn provider_retry_after_can_extend_backoff_but_is_defensively_bounded() {
let settings = retry_settings(); let settings = retry_settings();
let extended = super::evaluate_transport_retry( let extended = crate::evaluate_transport_retry(
method("getBalance"), method("getBalance"),
&settings, &settings,
super::HttpRetryCause::RateLimited, crate::HttpRetryCause::RateLimited,
super::HttpDispatchState::DispatchedAmbiguous, crate::HttpDispatchState::DispatchedAmbiguous,
0, 0,
std::option::Option::Some(std::time::Duration::from_secs(3)), std::option::Option::Some(std::time::Duration::from_secs(3)),
); );
assert_eq!(extended.delay(), std::option::Option::Some(std::time::Duration::from_secs(3))); assert_eq!(extended.delay(), std::option::Option::Some(std::time::Duration::from_secs(3)));
let bounded = super::evaluate_transport_retry( let bounded = crate::evaluate_transport_retry(
method("getBalance"), method("getBalance"),
&settings, &settings,
super::HttpRetryCause::RateLimited, crate::HttpRetryCause::RateLimited,
super::HttpDispatchState::DispatchedAmbiguous, crate::HttpDispatchState::DispatchedAmbiguous,
0, 0,
std::option::Option::Some(std::time::Duration::from_secs(600)), std::option::Option::Some(std::time::Duration::from_secs(600)),
); );
@@ -144,29 +144,29 @@ fn token_bucket_consumes_burst_then_refills_from_elapsed_time() {
#[test] #[test]
fn absent_burst_capacity_defaults_to_one_second_of_rps_capacity() { fn absent_burst_capacity_defaults_to_one_second_of_rps_capacity() {
let role = role_limits(std::option::Option::Some(2), std::option::Option::None, std::option::Option::None, std::option::Option::None); let role = role_limits(std::option::Option::Some(2), std::option::Option::None, std::option::Option::None, std::option::Option::None);
let runtime = std::sync::Arc::new(super::HttpRoleRuntime::new(&role, std::sync::Arc::new(tokio::sync::Notify::new()))); let runtime = std::sync::Arc::new(crate::HttpRoleRuntime::new(&role, std::sync::Arc::new(tokio::sync::Notify::new())));
let now = std::time::Instant::now(); let now = std::time::Instant::now();
let first = runtime.try_acquire(now); let first = runtime.try_acquire(now);
let second = runtime.try_acquire(now); let second = runtime.try_acquire(now);
let third = runtime.try_acquire(now); let third = runtime.try_acquire(now);
assert!(matches!(first, super::RoleAdmissionAttempt::Ready(_))); assert!(matches!(first, crate::RoleAdmissionAttempt::Ready(_)));
assert!(matches!(second, super::RoleAdmissionAttempt::Ready(_))); assert!(matches!(second, crate::RoleAdmissionAttempt::Ready(_)));
assert!(matches!(third, super::RoleAdmissionAttempt::BlockedUntil(_))); assert!(matches!(third, crate::RoleAdmissionAttempt::BlockedUntil(_)));
} }
#[test] #[test]
fn concurrency_semaphore_releases_capacity_when_permit_is_dropped() { fn concurrency_semaphore_releases_capacity_when_permit_is_dropped() {
let role = role_limits(std::option::Option::None, std::option::Option::None, std::option::Option::Some(1), std::option::Option::None); let role = role_limits(std::option::Option::None, std::option::Option::None, std::option::Option::Some(1), std::option::Option::None);
let runtime = std::sync::Arc::new(super::HttpRoleRuntime::new(&role, std::sync::Arc::new(tokio::sync::Notify::new()))); let runtime = std::sync::Arc::new(crate::HttpRoleRuntime::new(&role, std::sync::Arc::new(tokio::sync::Notify::new())));
let now = std::time::Instant::now(); let now = std::time::Instant::now();
let first = runtime.try_acquire(now); let first = runtime.try_acquire(now);
let held = match first { let held = match first {
super::RoleAdmissionAttempt::Ready(permit) => permit, crate::RoleAdmissionAttempt::Ready(permit) => permit,
_ => panic!("first concurrency permit must be available"), _ => panic!("first concurrency permit must be available"),
}; };
assert!(matches!(runtime.try_acquire(now), super::RoleAdmissionAttempt::ConcurrencySaturated)); assert!(matches!(runtime.try_acquire(now), crate::RoleAdmissionAttempt::ConcurrencySaturated));
drop(held); drop(held);
assert!(matches!(runtime.try_acquire(now), super::RoleAdmissionAttempt::Ready(_))); assert!(matches!(runtime.try_acquire(now), crate::RoleAdmissionAttempt::Ready(_)));
} }
#[test] #[test]
@@ -177,7 +177,7 @@ fn rate_limit_cooldown_marks_role_and_caps_provider_delay() {
std::option::Option::None, std::option::Option::None,
std::option::Option::Some(std::time::Duration::from_millis(10)), std::option::Option::Some(std::time::Duration::from_millis(10)),
); );
let runtime = super::HttpRoleRuntime::new(&role, std::sync::Arc::new(tokio::sync::Notify::new())); let runtime = crate::HttpRoleRuntime::new(&role, std::sync::Arc::new(tokio::sync::Notify::new()));
let pause = runtime.record_rate_limited(std::option::Option::Some(std::time::Duration::from_secs(600))); let pause = runtime.record_rate_limited(std::option::Option::Some(std::time::Duration::from_secs(600)));
assert_eq!(pause, std::time::Duration::from_secs(60)); assert_eq!(pause, std::time::Duration::from_secs(60));
assert_eq!(runtime.rate_limit_count(), 1); assert_eq!(runtime.rate_limit_count(), 1);

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_accounts.rs // file: crates/ksp-onchain-transport-lib/unit_tests/rpc_accounts.rs
// version: 3 // version: 4
#[test] #[test]
fn account_config_serializes_all_common_fields() { fn account_config_serializes_all_common_fields() {
@@ -64,7 +64,6 @@ fn staged_largest_and_keyed_account_helpers_match_wire_shapes() {
std::option::Option::Some(true), std::option::Option::Some(true),
); );
assert_eq!(config.to_json_value(), serde_json::json!({"commitment":"finalized","filter":"nonCirculating","sortResults":true})); assert_eq!(config.to_json_value(), serde_json::json!({"commitment":"finalized","filter":"nonCirculating","sortResults":true}));
let keyed = crate::SolanaKeyedAccount::decode_wire( let keyed = crate::SolanaKeyedAccount::decode_wire(
"fixture", "fixture",
serde_json::json!({ serde_json::json!({
@@ -82,7 +81,6 @@ fn staged_largest_and_keyed_account_helpers_match_wire_shapes() {
.expect("keyed account must decode"); .expect("keyed account must decode");
assert_eq!(keyed.pubkey().to_string(), "11111111111111111111111111111111"); assert_eq!(keyed.pubkey().to_string(), "11111111111111111111111111111111");
assert_eq!(keyed.account().lamports(), 42); assert_eq!(keyed.account().lamports(), 42);
let balance = crate::SolanaAccountBalance::decode_wire("fixture", serde_json::json!({"address":"11111111111111111111111111111111","lamports":99})) let balance = crate::SolanaAccountBalance::decode_wire("fixture", serde_json::json!({"address":"11111111111111111111111111111111","lamports":99}))
.expect("account balance must decode"); .expect("account balance must decode");
assert_eq!(balance.address().to_string(), "11111111111111111111111111111111"); assert_eq!(balance.address().to_string(), "11111111111111111111111111111111");
@@ -417,7 +415,6 @@ async fn typed_get_program_accounts_rejects_filter_cardinality_and_oversized_raw
let error = result.expect_err("more than four program-account filters must be rejected locally"); let error = result.expect_err("more than four program-account filters must be rejected locally");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS); assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
assert_eq!(error.context()[1].value(), "5"); assert_eq!(error.context()[1].value(), "5");
let oversized = crate::SolanaProgramAccountsConfig::new( let oversized = crate::SolanaProgramAccountsConfig::new(
crate::SolanaAccountInfoConfig::default(), crate::SolanaAccountInfoConfig::default(),
std::vec![crate::SolanaProgramAccountFilter::Memcmp(crate::SolanaMemcmpFilter::new(0, crate::SolanaMemcmpBytes::Bytes(std::vec![0_u8; 129]),))], std::vec![crate::SolanaProgramAccountFilter::Memcmp(crate::SolanaMemcmpFilter::new(0, crate::SolanaMemcmpBytes::Bytes(std::vec![0_u8; 129]),))],

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs // file: crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs
// version: 5 // version: 6
#[test] #[test]
fn transaction_details_and_get_block_config_preserve_all_modern_options() { fn transaction_details_and_get_block_config_preserve_all_modern_options() {
@@ -280,7 +280,6 @@ async fn typed_get_block_time_preserves_timestamp_and_null() {
assert_eq!(timestamp, std::option::Option::Some(1_787_072_400)); assert_eq!(timestamp, std::option::Option::Some(1_787_072_400));
let request = handle.join().expect("fixture server must join"); let request = handle.join().expect("fixture server must join");
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([430000123])); assert_eq!(request_body(request.as_str())["params"], serde_json::json!([430000123]));
let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_time.null.json")); let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_time.null.json"));
let pool = pool_for_url(url.as_str()); let pool = pool_for_url(url.as_str());
let timestamp = pool.get_block_time(&crate::HttpRoleName::new("default"), 430_000_124).await.expect("null block time fixture must succeed"); let timestamp = pool.get_block_time(&crate::HttpRoleName::new("default"), 430_000_124).await.expect("null block time fixture must succeed");
@@ -356,7 +355,6 @@ async fn typed_get_blocks_allows_reversed_and_boundary_ranges_but_rejects_oversi
assert!(blocks.is_empty()); assert!(blocks.is_empty());
let request = handle.join().expect("fixture server must join"); let request = handle.join().expect("fixture server must join");
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([430000100, 430000099])); assert_eq!(request_body(request.as_str())["params"], serde_json::json!([430000100, 430000099]));
let (url, handle) = serve_once(include_str!("../fixtures/http/get_blocks.success.json")); let (url, handle) = serve_once(include_str!("../fixtures/http/get_blocks.success.json"));
let pool = pool_for_url(url.as_str()); let pool = pool_for_url(url.as_str());
let blocks = pool let blocks = pool
@@ -366,7 +364,6 @@ async fn typed_get_blocks_allows_reversed_and_boundary_ranges_but_rejects_oversi
assert_eq!(blocks, std::vec![430_000_100, 430_000_103, 430_000_109]); assert_eq!(blocks, std::vec![430_000_100, 430_000_103, 430_000_109]);
let request = handle.join().expect("fixture server must join"); let request = handle.join().expect("fixture server must join");
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([10, 500010])); assert_eq!(request_body(request.as_str())["params"], serde_json::json!([10, 500010]));
let pool = pool_for_url("http://127.0.0.1:9"); let pool = pool_for_url("http://127.0.0.1:9");
let result = pool.get_blocks(&role, 10, std::option::Option::Some(500_011), std::option::Option::None).await; let result = pool.get_blocks(&role, 10, std::option::Option::Some(500_011), std::option::Option::None).await;
let error = result.expect_err("range above 500000 must reject before I/O"); let error = result.expect_err("range above 500000 must reject before I/O");
@@ -387,7 +384,6 @@ async fn typed_block_range_wrappers_reject_processed_commitment_before_io() {
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS); assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
assert_eq!(error.context()[1].key(), "commitment"); assert_eq!(error.context()[1].key(), "commitment");
assert_eq!(error.context()[1].value(), "processed"); assert_eq!(error.context()[1].value(), "processed");
let get_blocks_with_limit = pool.get_blocks_with_limit(&role, 1, 1, std::option::Option::Some(&config)).await; let get_blocks_with_limit = pool.get_blocks_with_limit(&role, 1, 1, std::option::Option::Some(&config)).await;
let error = get_blocks_with_limit.expect_err("getBlocksWithLimit processed commitment must reject before I/O"); let error = get_blocks_with_limit.expect_err("getBlocksWithLimit processed commitment must reject before I/O");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS); assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
@@ -404,7 +400,6 @@ async fn typed_get_blocks_with_limit_accepts_zero_and_maximum_and_rejects_above_
assert!(blocks.is_empty()); assert!(blocks.is_empty());
let request = handle.join().expect("fixture server must join"); let request = handle.join().expect("fixture server must join");
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([430000200, 0])); assert_eq!(request_body(request.as_str())["params"], serde_json::json!([430000200, 0]));
let config = crate::SolanaContextConfig::new(std::option::Option::Some(crate::SolanaCommitment::Finalized), std::option::Option::Some(430_000_000)); let config = crate::SolanaContextConfig::new(std::option::Option::Some(crate::SolanaCommitment::Finalized), std::option::Option::Some(430_000_000));
let (url, handle) = serve_once(include_str!("../fixtures/http/get_blocks_with_limit.success.json")); let (url, handle) = serve_once(include_str!("../fixtures/http/get_blocks_with_limit.success.json"));
let pool = pool_for_url(url.as_str()); let pool = pool_for_url(url.as_str());
@@ -415,7 +410,6 @@ async fn typed_get_blocks_with_limit_accepts_zero_and_maximum_and_rejects_above_
assert_eq!(blocks, std::vec![430_000_200, 430_000_201, 430_000_205]); assert_eq!(blocks, std::vec![430_000_200, 430_000_201, 430_000_205]);
let request = handle.join().expect("fixture server must join"); let request = handle.join().expect("fixture server must join");
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([430000200,500000,{"commitment":"finalized","minContextSlot":430000000}])); assert_eq!(request_body(request.as_str())["params"], serde_json::json!([430000200,500000,{"commitment":"finalized","minContextSlot":430000000}]));
let pool = pool_for_url("http://127.0.0.1:9"); let pool = pool_for_url("http://127.0.0.1:9");
let result = pool.get_blocks_with_limit(&role, 1, 500_001, std::option::Option::None).await; let result = pool.get_blocks_with_limit(&role, 1, 500_001, std::option::Option::None).await;
let error = result.expect_err("getBlocksWithLimit above 500000 must reject before I/O"); let error = result.expect_err("getBlocksWithLimit above 500000 must reject before I/O");
@@ -497,7 +491,6 @@ async fn typed_get_block_production_omits_empty_config_and_supports_open_range_w
assert_eq!(response.value().range().last_slot(), 430_000_099); assert_eq!(response.value().range().last_slot(), 430_000_099);
let request = handle.join().expect("fixture server must join"); let request = handle.join().expect("fixture server must join");
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([])); assert_eq!(request_body(request.as_str())["params"], serde_json::json!([]));
let range = crate::SolanaBlockProductionRange::new(430_000_000, std::option::Option::None); let range = crate::SolanaBlockProductionRange::new(430_000_000, std::option::Option::None);
let config = crate::SolanaBlockProductionConfig::new( let config = crate::SolanaBlockProductionConfig::new(
std::option::Option::Some(crate::SolanaCommitment::Processed), std::option::Option::Some(crate::SolanaCommitment::Processed),

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs // file: crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs
// version: 4 // version: 5
#[test] #[test]
fn cluster_node_fixture_preserves_v4_client_id_and_optional_fields() { fn cluster_node_fixture_preserves_v4_client_id_and_optional_fields() {
@@ -59,19 +59,16 @@ fn epoch_snapshot_and_leader_helpers_preserve_wire_shapes() {
) )
.expect("epoch info must decode"); .expect("epoch info must decode");
assert_eq!(epoch.transaction_count(), std::option::Option::None); assert_eq!(epoch.transaction_count(), std::option::Option::None);
let schedule = crate::SolanaEpochSchedule::decode_wire( let schedule = crate::SolanaEpochSchedule::decode_wire(
"getEpochSchedule", "getEpochSchedule",
serde_json::json!({"firstNormalEpoch":1,"firstNormalSlot":32,"leaderScheduleSlotOffset":32,"slotsPerEpoch":64,"warmup":false}), serde_json::json!({"firstNormalEpoch":1,"firstNormalSlot":32,"leaderScheduleSlotOffset":32,"slotsPerEpoch":64,"warmup":false}),
) )
.expect("epoch schedule must decode"); .expect("epoch schedule must decode");
assert_eq!(schedule.slots_per_epoch(), 64); assert_eq!(schedule.slots_per_epoch(), 64);
let snapshot = crate::SolanaSnapshotSlotInfo::decode_wire("getHighestSnapshotSlot", serde_json::json!({"full":100,"incremental":null})) let snapshot = crate::SolanaSnapshotSlotInfo::decode_wire("getHighestSnapshotSlot", serde_json::json!({"full":100,"incremental":null}))
.expect("snapshot info must decode"); .expect("snapshot info must decode");
assert_eq!(snapshot.full(), 100); assert_eq!(snapshot.full(), 100);
assert_eq!(snapshot.incremental(), std::option::Option::None); assert_eq!(snapshot.incremental(), std::option::Option::None);
let leader = crate::SolanaLeaderSchedule::decode_wire("getLeaderSchedule", serde_json::json!({"11111111111111111111111111111111":[0,2,4]})) let leader = crate::SolanaLeaderSchedule::decode_wire("getLeaderSchedule", serde_json::json!({"11111111111111111111111111111111":[0,2,4]}))
.expect("leader schedule must decode"); .expect("leader schedule must decode");
assert_eq!(leader.entries().len(), 1); assert_eq!(leader.entries().len(), 1);
@@ -90,7 +87,6 @@ fn vote_status_and_config_helpers_preserve_wire_shapes() {
config.to_json_value(), config.to_json_value(),
serde_json::json!({"commitment":"finalized","votePubkey":"11111111111111111111111111111111","keepUnstakedDelinquents":true,"delinquentSlotDistance":128}) serde_json::json!({"commitment":"finalized","votePubkey":"11111111111111111111111111111111","keepUnstakedDelinquents":true,"delinquentSlotDistance":128})
); );
let status = crate::SolanaVoteAccountStatus::decode_wire( let status = crate::SolanaVoteAccountStatus::decode_wire(
"getVoteAccounts", "getVoteAccounts",
serde_json::json!({ serde_json::json!({
@@ -363,7 +359,6 @@ async fn typed_get_slot_serializes_context_config_and_omits_empty_config() {
assert_eq!(slot, 430_000_020); assert_eq!(slot, 430_000_020);
let request = configured_handle.join().expect("fixture server must join"); let request = configured_handle.join().expect("fixture server must join");
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([{"commitment":"confirmed","minContextSlot":429999999}])); assert_eq!(request_body(request.as_str())["params"], serde_json::json!([{"commitment":"confirmed","minContextSlot":429999999}]));
let (empty_url, empty_handle) = serve_once(include_str!("../fixtures/http/get_slot.success.json")); let (empty_url, empty_handle) = serve_once(include_str!("../fixtures/http/get_slot.success.json"));
let empty_pool = pool_for_url(empty_url.as_str()); let empty_pool = pool_for_url(empty_url.as_str());
let empty = crate::SolanaContextConfig::default(); let empty = crate::SolanaContextConfig::default();

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_economics.rs // file: crates/ksp-onchain-transport-lib/unit_tests/rpc_economics.rs
// version: 3 // version: 4
#[test] #[test]
fn inflation_reward_config_preserves_epoch_commitment_and_min_context_slot() { fn inflation_reward_config_preserves_epoch_commitment_and_min_context_slot() {
@@ -346,7 +346,6 @@ async fn typed_get_inflation_reward_serializes_full_config_and_preserves_positio
let fourth = rewards[3].as_ref().expect("fourth reward must be present"); let fourth = rewards[3].as_ref().expect("fourth reward must be present");
assert_eq!(fourth.commission(), std::option::Option::Some(3)); assert_eq!(fourth.commission(), std::option::Option::Some(3));
assert!(fourth.commission_bps().is_null()); assert!(fourth.commission_bps().is_null());
let request = handle.join().expect("fixture server must join"); let request = handle.join().expect("fixture server must join");
let body = request_body(request.as_str()); let body = request_body(request.as_str());
assert_eq!(body["method"], serde_json::json!("getInflationReward")); assert_eq!(body["method"], serde_json::json!("getInflationReward"));

View File

@@ -1,19 +1,19 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_method.rs // file: crates/ksp-onchain-transport-lib/unit_tests/rpc_method.rs
// version: 2 // version: 3
#[test] #[test]
fn audited_registry_has_expected_current_and_historical_counts() { fn audited_registry_has_expected_current_and_historical_counts() {
assert_eq!(super::current_http_rpc_methods().len(), 52); assert_eq!(crate::current_http_rpc_methods().len(), 52);
assert_eq!(super::historical_http_rpc_methods().len(), 14); assert_eq!(crate::historical_http_rpc_methods().len(), 14);
} }
#[test] #[test]
fn audited_registry_method_names_are_unique() { fn audited_registry_method_names_are_unique() {
let mut names = std::collections::BTreeSet::<&str>::new(); let mut names = std::collections::BTreeSet::<&str>::new();
for descriptor in super::current_http_rpc_methods() { for descriptor in crate::current_http_rpc_methods() {
assert!(names.insert(descriptor.method()), "duplicate current method {}", descriptor.method()); assert!(names.insert(descriptor.method()), "duplicate current method {}", descriptor.method());
} }
for descriptor in super::historical_http_rpc_methods() { for descriptor in crate::historical_http_rpc_methods() {
assert!(names.insert(descriptor.method()), "duplicate historical method {}", descriptor.method()); assert!(names.insert(descriptor.method()), "duplicate historical method {}", descriptor.method());
} }
assert_eq!(names.len(), 66); assert_eq!(names.len(), 66);
@@ -26,13 +26,13 @@ fn coverage_release_counts_match_recalibrated_matrix() {
let mut transactions = 0_usize; let mut transactions = 0_usize;
let mut blocks_economics = 0_usize; let mut blocks_economics = 0_usize;
let mut historical = 0_usize; let mut historical = 0_usize;
for descriptor in super::current_http_rpc_methods() { for descriptor in crate::current_http_rpc_methods() {
match descriptor.coverage_release() { match descriptor.coverage_release() {
super::HttpRpcCoverageRelease::V0_2_1 => foundation += 1, crate::HttpRpcCoverageRelease::V0_2_1 => foundation += 1,
super::HttpRpcCoverageRelease::V0_2_2 => accounts_tokens_cluster += 1, crate::HttpRpcCoverageRelease::V0_2_2 => accounts_tokens_cluster += 1,
super::HttpRpcCoverageRelease::V0_2_3 => transactions += 1, crate::HttpRpcCoverageRelease::V0_2_3 => transactions += 1,
super::HttpRpcCoverageRelease::V0_2_4 => blocks_economics += 1, crate::HttpRpcCoverageRelease::V0_2_4 => blocks_economics += 1,
super::HttpRpcCoverageRelease::Historical => historical += 1, crate::HttpRpcCoverageRelease::Historical => historical += 1,
} }
} }
assert_eq!(historical, 0); assert_eq!(historical, 0);
@@ -45,8 +45,8 @@ fn coverage_release_counts_match_recalibrated_matrix() {
#[test] #[test]
fn foundation_canary_assignment_is_exact() { fn foundation_canary_assignment_is_exact() {
let mut names = std::vec::Vec::<&str>::new(); let mut names = std::vec::Vec::<&str>::new();
for descriptor in super::current_http_rpc_methods() { for descriptor in crate::current_http_rpc_methods() {
if descriptor.coverage_release() == super::HttpRpcCoverageRelease::V0_2_1 { if descriptor.coverage_release() == crate::HttpRpcCoverageRelease::V0_2_1 {
names.push(descriptor.method()); names.push(descriptor.method());
} }
} }
@@ -56,62 +56,62 @@ fn foundation_canary_assignment_is_exact() {
#[test] #[test]
fn historical_methods_are_deprecated_removed_and_not_retryable() { fn historical_methods_are_deprecated_removed_and_not_retryable() {
for descriptor in super::historical_http_rpc_methods() { for descriptor in crate::historical_http_rpc_methods() {
assert_eq!(descriptor.documentation_status(), super::RpcDocumentationStatus::Deprecated); assert_eq!(descriptor.documentation_status(), crate::RpcDocumentationStatus::Deprecated);
assert_eq!(descriptor.runtime_status(), super::RpcRuntimeStatus::Removed); assert_eq!(descriptor.runtime_status(), crate::RpcRuntimeStatus::Removed);
assert_eq!(descriptor.transport_retry_class(), super::TransportRetryClass::NotApplicable); assert_eq!(descriptor.transport_retry_class(), crate::TransportRetryClass::NotApplicable);
assert_eq!(descriptor.coverage_release(), super::HttpRpcCoverageRelease::Historical); assert_eq!(descriptor.coverage_release(), crate::HttpRpcCoverageRelease::Historical);
} }
} }
#[test] #[test]
fn get_transaction_and_get_block_track_deprecated_legacy_request_form() { fn get_transaction_and_get_block_track_deprecated_legacy_request_form() {
let get_transaction = super::find_http_rpc_method("getTransaction").expect("getTransaction descriptor must exist"); let get_transaction = crate::find_http_rpc_method("getTransaction").expect("getTransaction descriptor must exist");
let get_block = super::find_http_rpc_method("getBlock").expect("getBlock descriptor must exist"); let get_block = crate::find_http_rpc_method("getBlock").expect("getBlock descriptor must exist");
assert!(get_transaction.request_form_status().has_deprecated_legacy()); assert!(get_transaction.request_form_status().has_deprecated_legacy());
assert!(get_block.request_form_status().has_deprecated_legacy()); assert!(get_block.request_form_status().has_deprecated_legacy());
let get_balance = super::find_http_rpc_method("getBalance").expect("getBalance descriptor must exist"); let get_balance = crate::find_http_rpc_method("getBalance").expect("getBalance descriptor must exist");
assert!(!get_balance.request_form_status().has_deprecated_legacy()); assert!(!get_balance.request_form_status().has_deprecated_legacy());
} }
#[test] #[test]
fn write_submission_methods_are_never_retry_after_ambiguous_dispatch() { fn write_submission_methods_are_never_retry_after_ambiguous_dispatch() {
for method in ["sendTransaction", "requestAirdrop"] { for method in ["sendTransaction", "requestAirdrop"] {
let descriptor = super::find_http_rpc_method(method).expect("write descriptor must exist"); let descriptor = crate::find_http_rpc_method(method).expect("write descriptor must exist");
assert_eq!(descriptor.operation_kind(), super::RpcOperationKind::WriteSubmission); assert_eq!(descriptor.operation_kind(), crate::RpcOperationKind::WriteSubmission);
assert_eq!(descriptor.transport_retry_class(), super::TransportRetryClass::NeverAfterDispatch); assert_eq!(descriptor.transport_retry_class(), crate::TransportRetryClass::NeverAfterDispatch);
} }
let simulation = super::find_http_rpc_method("simulateTransaction").expect("simulation descriptor must exist"); let simulation = crate::find_http_rpc_method("simulateTransaction").expect("simulation descriptor must exist");
assert_eq!(simulation.operation_kind(), super::RpcOperationKind::Simulation); assert_eq!(simulation.operation_kind(), crate::RpcOperationKind::Simulation);
assert_eq!(simulation.transport_retry_class(), super::TransportRetryClass::RetrySafe); assert_eq!(simulation.transport_retry_class(), crate::TransportRetryClass::RetrySafe);
} }
#[test] #[test]
fn removed_method_support_check_returns_method_removed_error() { fn removed_method_support_check_returns_method_removed_error() {
let descriptor = super::find_http_rpc_method("confirmTransaction").expect("historical descriptor must exist"); let descriptor = crate::find_http_rpc_method("confirmTransaction").expect("historical descriptor must exist");
let error = descriptor.ensure_runtime_supported().expect_err("removed method must not be callable"); let error = descriptor.ensure_runtime_supported().expect_err("removed method must not be callable");
assert_eq!(error.code(), crate::ERROR_CODE_METHOD_REMOVED); assert_eq!(error.code(), crate::ERROR_CODE_METHOD_REMOVED);
} }
#[test] #[test]
fn stable_supported_method_passes_runtime_support_check() { fn stable_supported_method_passes_runtime_support_check() {
let descriptor = super::find_http_rpc_method("getHealth").expect("current descriptor must exist"); let descriptor = crate::find_http_rpc_method("getHealth").expect("current descriptor must exist");
assert!(descriptor.ensure_runtime_supported().is_ok()); assert!(descriptor.ensure_runtime_supported().is_ok());
} }
#[test] #[test]
fn supported_unstable_descriptor_executes_central_warning_path() { fn supported_unstable_descriptor_executes_central_warning_path() {
let descriptor = super::HttpRpcMethodDescriptor::new( let descriptor = crate::HttpRpcMethodDescriptor::new(
"experimentalMethod", "experimentalMethod",
super::HttpRpcCategory::Cluster, crate::HttpRpcCategory::Cluster,
"experimental_method", "experimental_method",
super::RpcDocumentationStatus::Unstable, crate::RpcDocumentationStatus::Unstable,
super::RpcRuntimeStatus::Supported, crate::RpcRuntimeStatus::Supported,
super::RpcRequestFormStatus::Stable, crate::RpcRequestFormStatus::Stable,
super::RpcOperationKind::Read, crate::RpcOperationKind::Read,
super::TransportRetryClass::RetrySafe, crate::TransportRetryClass::RetrySafe,
std::option::Option::None, std::option::Option::None,
super::HttpRpcCoverageRelease::V0_2_1, crate::HttpRpcCoverageRelease::V0_2_1,
); );
assert!(descriptor.requires_method_usage_warning()); assert!(descriptor.requires_method_usage_warning());
assert!(descriptor.ensure_runtime_supported().is_ok()); assert!(descriptor.ensure_runtime_supported().is_ok());
@@ -119,5 +119,5 @@ fn supported_unstable_descriptor_executes_central_warning_path() {
#[test] #[test]
fn lookup_rejects_unknown_method_without_affecting_raw_provider_extensions() { fn lookup_rejects_unknown_method_without_affecting_raw_provider_extensions() {
assert!(super::find_http_rpc_method("providerCustomMethod").is_none()); assert!(crate::find_http_rpc_method("providerCustomMethod").is_none());
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs // file: crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs
// version: 8 // version: 9
#[test] #[test]
fn transaction_encoding_strings_match_current_and_legacy_wire_labels() { fn transaction_encoding_strings_match_current_and_legacy_wire_labels() {
@@ -19,7 +19,6 @@ fn wire_field_preserves_omitted_null_and_value_states() {
#[serde(default)] #[serde(default)]
field: crate::SolanaWireField<u64>, field: crate::SolanaWireField<u64>,
} }
let omitted = serde_json::from_value::<Holder>(serde_json::json!({})).expect("omitted fixture must decode"); let omitted = serde_json::from_value::<Holder>(serde_json::json!({})).expect("omitted fixture must decode");
let null = serde_json::from_value::<Holder>(serde_json::json!({"field": null})).expect("null fixture must decode"); let null = serde_json::from_value::<Holder>(serde_json::json!({"field": null})).expect("null fixture must decode");
let value = serde_json::from_value::<Holder>(serde_json::json!({"field": 7})).expect("value fixture must decode"); let value = serde_json::from_value::<Holder>(serde_json::json!({"field": 7})).expect("value fixture must decode");
@@ -402,7 +401,6 @@ fn serve_transaction_status_then_body(
let first_request = read_transaction_request(&mut first_stream); let first_request = read_transaction_request(&mut first_stream);
let first_response = format!("HTTP/1.1 {status_line}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"); let first_response = format!("HTTP/1.1 {status_line}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
std::io::Write::write_all(&mut first_stream, first_response.as_bytes()).expect("fixture first response must write"); std::io::Write::write_all(&mut first_stream, first_response.as_bytes()).expect("fixture first response must write");
listener.set_nonblocking(true).expect("fixture listener must become nonblocking"); listener.set_nonblocking(true).expect("fixture listener must become nonblocking");
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(500); let deadline = std::time::Instant::now() + std::time::Duration::from_millis(500);
return loop { return loop {
@@ -612,7 +610,6 @@ async fn typed_get_recent_prioritization_fees_omits_optional_accounts_and_reject
assert_eq!(fees.len(), 2); assert_eq!(fees.len(), 2);
let request = handle.join().expect("fixture server must join"); let request = handle.join().expect("fixture server must join");
assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!([])); assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!([]));
let account = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse"); let account = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
let max_accounts = std::vec![account; 128]; let max_accounts = std::vec![account; 128];
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_recent_prioritization_fees.success.json")); let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_recent_prioritization_fees.success.json"));
@@ -625,7 +622,6 @@ async fn typed_get_recent_prioritization_fees_omits_optional_accounts_and_reject
let request = handle.join().expect("fixture server must join"); let request = handle.join().expect("fixture server must join");
let body = transaction_request_body(request.as_str()); let body = transaction_request_body(request.as_str());
assert_eq!(body["params"][0].as_array().expect("address parameter must be an array").len(), 128); assert_eq!(body["params"][0].as_array().expect("address parameter must be an array").len(), 128);
let invalid_pool = transaction_pool_for_url("http://127.0.0.1:9"); let invalid_pool = transaction_pool_for_url("http://127.0.0.1:9");
let accounts = std::vec![account; 129]; let accounts = std::vec![account; 129];
let result = invalid_pool.get_recent_prioritization_fees(&crate::HttpRoleName::new("default"), std::option::Option::Some(accounts.as_slice())).await; let result = invalid_pool.get_recent_prioritization_fees(&crate::HttpRoleName::new("default"), std::option::Option::Some(accounts.as_slice())).await;
@@ -768,7 +764,6 @@ async fn typed_get_signature_statuses_accepts_256_and_rejects_more_than_256_befo
let request = handle.join().expect("fixture server must join"); let request = handle.join().expect("fixture server must join");
let body = transaction_request_body(request.as_str()); let body = transaction_request_body(request.as_str());
assert_eq!(body["params"][0].as_array().expect("signature parameter must be an array").len(), 256); assert_eq!(body["params"][0].as_array().expect("signature parameter must be an array").len(), 256);
let invalid_pool = transaction_pool_for_url("http://127.0.0.1:9"); let invalid_pool = transaction_pool_for_url("http://127.0.0.1:9");
let signatures = std::vec!["signature".to_owned(); 257]; let signatures = std::vec!["signature".to_owned(); 257];
let result = invalid_pool.get_signature_statuses(&crate::HttpRoleName::new("default"), signatures.as_slice(), std::option::Option::None).await; let result = invalid_pool.get_signature_statuses(&crate::HttpRoleName::new("default"), signatures.as_slice(), std::option::Option::None).await;
@@ -804,7 +799,6 @@ async fn typed_get_transaction_modern_supports_omitted_and_explicit_empty_config
assert!(result.is_none()); assert!(result.is_none());
let request = handle.join().expect("fixture server must join"); let request = handle.join().expect("fixture server must join");
assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!(["fixture-signature"])); assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!(["fixture-signature"]));
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_transaction.null.json")); let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_transaction.null.json"));
let pool = transaction_pool_for_url(url.as_str()); let pool = transaction_pool_for_url(url.as_str());
let config = crate::SolanaGetTransactionConfig::default(); let config = crate::SolanaGetTransactionConfig::default();
@@ -1006,7 +1000,6 @@ async fn typed_request_airdrop_exposes_runtime_config_and_canonicalizes_empty_co
{"recentBlockhash":"recent-blockhash-fixture","commitment":"finalized"} {"recentBlockhash":"recent-blockhash-fixture","commitment":"finalized"}
]) ])
); );
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/request_airdrop.success.json")); let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/request_airdrop.success.json"));
let pool = transaction_pool_for_url(url.as_str()); let pool = transaction_pool_for_url(url.as_str());
let empty = crate::SolanaRequestAirdropConfig::default(); let empty = crate::SolanaRequestAirdropConfig::default();
@@ -1050,7 +1043,6 @@ async fn typed_send_transaction_exposes_all_current_options_and_both_binary_enco
} }
]) ])
); );
let base58 = crate::SolanaSendTransactionConfig::new( let base58 = crate::SolanaSendTransactionConfig::new(
std::option::Option::None, std::option::Option::None,
std::option::Option::None, std::option::Option::None,
@@ -1066,7 +1058,6 @@ async fn typed_send_transaction_exposes_all_current_options_and_both_binary_enco
.expect("sendTransaction base58 fixture must succeed"); .expect("sendTransaction base58 fixture must succeed");
let request = handle.join().expect("fixture server must join"); let request = handle.join().expect("fixture server must join");
assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!(["opaque-base58-transaction", {"encoding":"base58"}])); assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!(["opaque-base58-transaction", {"encoding":"base58"}]));
let empty = crate::SolanaSendTransactionConfig::default(); let empty = crate::SolanaSendTransactionConfig::default();
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/send_transaction.success.json")); let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/send_transaction.success.json"));
let pool = transaction_pool_for_url(url.as_str()); let pool = transaction_pool_for_url(url.as_str());
@@ -1086,7 +1077,6 @@ async fn typed_write_wrappers_preserve_rpc_application_errors_without_transport_
let result = pool.request_airdrop(&crate::HttpRoleName::new("default"), &recipient, 1, std::option::Option::None).await; let result = pool.request_airdrop(&crate::HttpRoleName::new("default"), &recipient, 1, std::option::Option::None).await;
assert_eq!(result.expect_err("requestAirdrop RPC error must propagate").code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR); assert_eq!(result.expect_err("requestAirdrop RPC error must propagate").code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR);
handle.join().expect("fixture server must join"); handle.join().expect("fixture server must join");
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/send_transaction.preflight_error.json")); let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/send_transaction.preflight_error.json"));
let pool = transaction_pool_for_urls(&[(url.as_str(), 10)], std::time::Duration::from_millis(500), 3); let pool = transaction_pool_for_urls(&[(url.as_str(), 10)], std::time::Duration::from_millis(500), 3);
let result = pool.send_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::None).await; let result = pool.send_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::None).await;
@@ -1104,7 +1094,6 @@ async fn typed_write_wrappers_never_resend_after_http_429_dispatch() {
let (count, request) = handle.join().expect("fixture server must join"); let (count, request) = handle.join().expect("fixture server must join");
assert_eq!(count, 1); assert_eq!(count, 1);
assert_eq!(transaction_request_body(request.as_str())["method"], serde_json::json!("requestAirdrop")); assert_eq!(transaction_request_body(request.as_str())["method"], serde_json::json!("requestAirdrop"));
let (url, handle) = serve_transaction_status_and_count("429 Too Many Requests"); let (url, handle) = serve_transaction_status_and_count("429 Too Many Requests");
let pool = transaction_pool_for_urls(&[(url.as_str(), 10)], std::time::Duration::from_millis(500), 3); let pool = transaction_pool_for_urls(&[(url.as_str(), 10)], std::time::Duration::from_millis(500), 3);
let result = pool.send_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::None).await; let result = pool.send_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::None).await;
@@ -1123,7 +1112,6 @@ async fn typed_write_wrappers_never_resend_after_temporary_http_dispatch() {
assert_eq!(result.expect_err("airdrop 503 must stop after dispatch").code(), crate::ERROR_CODE_HTTP_REQUEST_FAILED); assert_eq!(result.expect_err("airdrop 503 must stop after dispatch").code(), crate::ERROR_CODE_HTTP_REQUEST_FAILED);
let (count, _) = handle.join().expect("fixture server must join"); let (count, _) = handle.join().expect("fixture server must join");
assert_eq!(count, 1); assert_eq!(count, 1);
let (url, handle) = serve_transaction_status_and_count("503 Service Unavailable"); let (url, handle) = serve_transaction_status_and_count("503 Service Unavailable");
let pool = transaction_pool_for_urls(&[(url.as_str(), 10)], std::time::Duration::from_millis(500), 3); let pool = transaction_pool_for_urls(&[(url.as_str(), 10)], std::time::Duration::from_millis(500), 3);
let result = pool.send_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::None).await; let result = pool.send_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::None).await;
@@ -1141,7 +1129,6 @@ async fn typed_write_wrappers_never_resend_after_ambiguous_timeout_dispatch() {
assert_eq!(result.expect_err("airdrop timeout must stop after ambiguous dispatch").code(), crate::ERROR_CODE_TIMEOUT); assert_eq!(result.expect_err("airdrop timeout must stop after ambiguous dispatch").code(), crate::ERROR_CODE_TIMEOUT);
let (count, _) = handle.join().expect("fixture server must join"); let (count, _) = handle.join().expect("fixture server must join");
assert_eq!(count, 1); assert_eq!(count, 1);
let (url, handle) = serve_transaction_timeout_and_count(std::time::Duration::from_millis(80)); let (url, handle) = serve_transaction_timeout_and_count(std::time::Duration::from_millis(80));
let pool = transaction_pool_for_urls(&[(url.as_str(), 10)], std::time::Duration::from_millis(20), 3); let pool = transaction_pool_for_urls(&[(url.as_str(), 10)], std::time::Duration::from_millis(20), 3);
let result = pool.send_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::None).await; let result = pool.send_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::None).await;
@@ -1163,7 +1150,6 @@ async fn typed_write_wrappers_can_retry_when_connection_failure_proves_not_dispa
assert_eq!(result, "airdrop-fixture-signature-111111111111111111111111111111111111111111111111"); assert_eq!(result, "airdrop-fixture-signature-111111111111111111111111111111111111111111111111");
let request = handle.join().expect("secondary fixture server must join"); let request = handle.join().expect("secondary fixture server must join");
assert_eq!(transaction_request_body(request.as_str())["method"], serde_json::json!("requestAirdrop")); assert_eq!(transaction_request_body(request.as_str())["method"], serde_json::json!("requestAirdrop"));
let closed = unused_local_url(); let closed = unused_local_url();
let (secondary, handle) = serve_transaction_once(include_str!("../fixtures/http/send_transaction.success.json")); let (secondary, handle) = serve_transaction_once(include_str!("../fixtures/http/send_transaction.success.json"));
let pool = transaction_pool_for_urls(&[(closed.as_str(), 10), (secondary.as_str(), 10)], std::time::Duration::from_millis(500), 1); let pool = transaction_pool_for_urls(&[(closed.as_str(), 10), (secondary.as_str(), 10)], std::time::Duration::from_millis(500), 1);
@@ -1210,7 +1196,6 @@ async fn typed_simulate_transaction_serializes_complete_config_and_preserves_ric
let replacement = response.value().replacement_blockhash().value().expect("replacement blockhash must be present"); let replacement = response.value().replacement_blockhash().value().expect("replacement blockhash must be present");
assert_eq!(replacement.blockhash(), "ComputeBudget111111111111111111111111111111"); assert_eq!(replacement.blockhash(), "ComputeBudget111111111111111111111111111111");
assert_eq!(replacement.last_valid_block_height(), 430_123_999); assert_eq!(replacement.last_valid_block_height(), 430_123_999);
let request = handle.join().expect("fixture server must join"); let request = handle.join().expect("fixture server must join");
let body = transaction_request_body(request.as_str()); let body = transaction_request_body(request.as_str());
assert_eq!(body["method"], serde_json::json!("simulateTransaction")); assert_eq!(body["method"], serde_json::json!("simulateTransaction"));
@@ -1250,7 +1235,6 @@ async fn typed_simulate_transaction_supports_base58_and_omits_empty_config() {
.expect("simulateTransaction base58 fixture must succeed"); .expect("simulateTransaction base58 fixture must succeed");
let request = handle.join().expect("fixture server must join"); let request = handle.join().expect("fixture server must join");
assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!(["opaque-base58-transaction", {"encoding":"base58"}])); assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!(["opaque-base58-transaction", {"encoding":"base58"}]));
let empty = crate::SolanaSimulateTransactionConfig::default(); let empty = crate::SolanaSimulateTransactionConfig::default();
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/simulate_transaction.partial.json")); let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/simulate_transaction.partial.json"));
let pool = transaction_pool_for_url(url.as_str()); let pool = transaction_pool_for_url(url.as_str());
@@ -1303,7 +1287,6 @@ async fn typed_simulate_transaction_rejects_deterministic_invalid_configs_before
); );
let result = pool.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::Some(&conflicting)).await; let result = pool.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::Some(&conflicting)).await;
assert_eq!(result.expect_err("conflicting simulation flags must fail before I/O").code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS); assert_eq!(result.expect_err("conflicting simulation flags must fail before I/O").code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
let address = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse"); let address = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
let rejected_encodings = [crate::SolanaAccountEncoding::Binary, crate::SolanaAccountEncoding::Base58]; let rejected_encodings = [crate::SolanaAccountEncoding::Binary, crate::SolanaAccountEncoding::Base58];
for encoding in rejected_encodings { for encoding in rejected_encodings {

View File

@@ -1,52 +1,52 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/settings.rs // file: crates/ksp-onchain-transport-lib/unit_tests/settings.rs
// version: 1 // version: 2
fn non_zero(value: u32) -> std::num::NonZeroU32 { fn non_zero(value: u32) -> std::num::NonZeroU32 {
return std::num::NonZeroU32::new(value).expect("test non-zero value must remain non-zero"); return std::num::NonZeroU32::new(value).expect("test non-zero value must remain non-zero");
} }
fn valid_settings(url_text: &str) -> super::HttpTransportSettings { fn valid_settings(url_text: &str) -> crate::HttpTransportSettings {
let url = super::HttpEndpointUrl::parse(url_text).expect("test URL must be valid"); let url = crate::HttpEndpointUrl::parse(url_text).expect("test URL must be valid");
let limits = super::HttpRoleLimits::new( let limits = crate::HttpRoleLimits::new(
std::option::Option::Some(non_zero(10)), std::option::Option::Some(non_zero(10)),
std::option::Option::Some(non_zero(20)), std::option::Option::Some(non_zero(20)),
std::option::Option::Some(non_zero(4)), std::option::Option::Some(non_zero(4)),
std::option::Option::Some(std::time::Duration::from_millis(500)), std::option::Option::Some(std::time::Duration::from_millis(500)),
); );
let role = super::HttpEndpointRoleSettings::new(super::HttpRoleName::new("default"), true, std::vec![super::HttpRequestKind::wildcard()], 100, limits); let role = crate::HttpEndpointRoleSettings::new(crate::HttpRoleName::new("default"), true, std::vec![crate::HttpRequestKind::wildcard()], 100, limits);
let endpoint = super::HttpEndpointSettings::new( let endpoint = crate::HttpEndpointSettings::new(
"devnet_public", "devnet_public",
true, true,
super::HttpProviderName::new("solana-public"), crate::HttpProviderName::new("solana-public"),
super::HttpClusterName::new("devnet"), crate::HttpClusterName::new("devnet"),
url, url,
std::time::Duration::from_secs(5), std::time::Duration::from_secs(5),
std::time::Duration::from_secs(15), std::time::Duration::from_secs(15),
std::option::Option::Some(8), std::option::Option::Some(8),
std::vec![role], std::vec![role],
); );
return super::HttpTransportSettings::new( return crate::HttpTransportSettings::new(
std::vec![endpoint], std::vec![endpoint],
super::HttpRetrySettings::new(2, std::time::Duration::from_millis(100), std::time::Duration::from_secs(2)), crate::HttpRetrySettings::new(2, std::time::Duration::from_millis(100), std::time::Duration::from_secs(2)),
); );
} }
#[test] #[test]
fn endpoint_url_accepts_http_and_https() { fn endpoint_url_accepts_http_and_https() {
assert!(super::HttpEndpointUrl::parse("https://api.devnet.solana.com").is_ok()); assert!(crate::HttpEndpointUrl::parse("https://api.devnet.solana.com").is_ok());
assert!(super::HttpEndpointUrl::parse("http://127.0.0.1:8899").is_ok()); assert!(crate::HttpEndpointUrl::parse("http://127.0.0.1:8899").is_ok());
} }
#[test] #[test]
fn endpoint_url_rejects_non_http_schemes() { fn endpoint_url_rejects_non_http_schemes() {
let result = super::HttpEndpointUrl::parse("ws://api.devnet.solana.com"); let result = crate::HttpEndpointUrl::parse("ws://api.devnet.solana.com");
let error = result.expect_err("WebSocket URL must not be accepted by HTTP settings"); let error = result.expect_err("WebSocket URL must not be accepted by HTTP settings");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS); assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
} }
#[test] #[test]
fn endpoint_url_debug_redacts_secret_material() { fn endpoint_url_debug_redacts_secret_material() {
let url = super::HttpEndpointUrl::parse("https://provider.invalid/rpc?api-key=SECRET-CANARY").expect("test URL must parse"); let url = crate::HttpEndpointUrl::parse("https://provider.invalid/rpc?api-key=SECRET-CANARY").expect("test URL must parse");
let rendered = format!("{url:?}"); let rendered = format!("{url:?}");
assert!(rendered.contains("<redacted>")); assert!(rendered.contains("<redacted>"));
assert!(!rendered.contains("SECRET-CANARY")); assert!(!rendered.contains("SECRET-CANARY"));
@@ -70,28 +70,28 @@ fn transport_settings_debug_does_not_leak_endpoint_url() {
#[test] #[test]
fn transport_settings_require_one_enabled_endpoint() { fn transport_settings_require_one_enabled_endpoint() {
let url = super::HttpEndpointUrl::parse("https://api.devnet.solana.com").expect("test URL must parse"); let url = crate::HttpEndpointUrl::parse("https://api.devnet.solana.com").expect("test URL must parse");
let role = super::HttpEndpointRoleSettings::new( let role = crate::HttpEndpointRoleSettings::new(
super::HttpRoleName::new("default"), crate::HttpRoleName::new("default"),
true, true,
std::vec![super::HttpRequestKind::wildcard()], std::vec![crate::HttpRequestKind::wildcard()],
100, 100,
super::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None), crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
); );
let endpoint = super::HttpEndpointSettings::new( let endpoint = crate::HttpEndpointSettings::new(
"disabled", "disabled",
false, false,
super::HttpProviderName::new("provider"), crate::HttpProviderName::new("provider"),
super::HttpClusterName::new("devnet"), crate::HttpClusterName::new("devnet"),
url, url,
std::time::Duration::from_secs(1), std::time::Duration::from_secs(1),
std::time::Duration::from_secs(1), std::time::Duration::from_secs(1),
std::option::Option::None, std::option::Option::None,
std::vec![role], std::vec![role],
); );
let settings = super::HttpTransportSettings::new( let settings = crate::HttpTransportSettings::new(
std::vec![endpoint], std::vec![endpoint],
super::HttpRetrySettings::new(1, std::time::Duration::from_millis(1), std::time::Duration::from_millis(2)), crate::HttpRetrySettings::new(1, std::time::Duration::from_millis(1), std::time::Duration::from_millis(2)),
); );
let error = settings.validate().expect_err("all-disabled settings must fail"); let error = settings.validate().expect_err("all-disabled settings must fail");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS); assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
@@ -101,7 +101,7 @@ fn transport_settings_require_one_enabled_endpoint() {
fn transport_settings_reject_duplicate_endpoint_names() { fn transport_settings_reject_duplicate_endpoint_names() {
let first = valid_settings("https://one.invalid"); let first = valid_settings("https://one.invalid");
let second = valid_settings("https://two.invalid"); let second = valid_settings("https://two.invalid");
let settings = super::HttpTransportSettings::new(std::vec![first.endpoints()[0].clone(), second.endpoints()[0].clone()], first.retry().clone()); let settings = crate::HttpTransportSettings::new(std::vec![first.endpoints()[0].clone(), second.endpoints()[0].clone()], first.retry().clone());
let error = settings.validate().expect_err("duplicate endpoint names must fail"); let error = settings.validate().expect_err("duplicate endpoint names must fail");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS); assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
} }
@@ -110,7 +110,7 @@ fn transport_settings_reject_duplicate_endpoint_names() {
fn transport_settings_reject_duplicate_roles() { fn transport_settings_reject_duplicate_roles() {
let base = valid_settings("https://api.devnet.solana.com"); let base = valid_settings("https://api.devnet.solana.com");
let endpoint = &base.endpoints()[0]; let endpoint = &base.endpoints()[0];
let duplicated_endpoint = super::HttpEndpointSettings::new( let duplicated_endpoint = crate::HttpEndpointSettings::new(
endpoint.name(), endpoint.name(),
true, true,
endpoint.provider().clone(), endpoint.provider().clone(),
@@ -121,7 +121,7 @@ fn transport_settings_reject_duplicate_roles() {
endpoint.max_idle_connections_per_host(), endpoint.max_idle_connections_per_host(),
std::vec![endpoint.roles()[0].clone(), endpoint.roles()[0].clone()], std::vec![endpoint.roles()[0].clone(), endpoint.roles()[0].clone()],
); );
let settings = super::HttpTransportSettings::new(std::vec![duplicated_endpoint], base.retry().clone()); let settings = crate::HttpTransportSettings::new(std::vec![duplicated_endpoint], base.retry().clone());
assert!(settings.validate().is_err()); assert!(settings.validate().is_err());
} }
@@ -129,14 +129,14 @@ fn transport_settings_reject_duplicate_roles() {
fn transport_settings_reject_wildcard_mixed_with_specific_kind() { fn transport_settings_reject_wildcard_mixed_with_specific_kind() {
let base = valid_settings("https://api.devnet.solana.com"); let base = valid_settings("https://api.devnet.solana.com");
let endpoint = &base.endpoints()[0]; let endpoint = &base.endpoints()[0];
let role = super::HttpEndpointRoleSettings::new( let role = crate::HttpEndpointRoleSettings::new(
super::HttpRoleName::new("default"), crate::HttpRoleName::new("default"),
true, true,
std::vec![super::HttpRequestKind::wildcard(), super::HttpRequestKind::new("get_balance")], std::vec![crate::HttpRequestKind::wildcard(), crate::HttpRequestKind::new("get_balance")],
100, 100,
endpoint.roles()[0].limits().clone(), endpoint.roles()[0].limits().clone(),
); );
let modified_endpoint = super::HttpEndpointSettings::new( let modified_endpoint = crate::HttpEndpointSettings::new(
endpoint.name(), endpoint.name(),
true, true,
endpoint.provider().clone(), endpoint.provider().clone(),
@@ -147,7 +147,7 @@ fn transport_settings_reject_wildcard_mixed_with_specific_kind() {
endpoint.max_idle_connections_per_host(), endpoint.max_idle_connections_per_host(),
std::vec![role], std::vec![role],
); );
let settings = super::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone()); let settings = crate::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone());
assert!(settings.validate().is_err()); assert!(settings.validate().is_err());
} }
@@ -155,14 +155,14 @@ fn transport_settings_reject_wildcard_mixed_with_specific_kind() {
fn transport_settings_reject_burst_without_rps() { fn transport_settings_reject_burst_without_rps() {
let base = valid_settings("https://api.devnet.solana.com"); let base = valid_settings("https://api.devnet.solana.com");
let endpoint = &base.endpoints()[0]; let endpoint = &base.endpoints()[0];
let role = super::HttpEndpointRoleSettings::new( let role = crate::HttpEndpointRoleSettings::new(
super::HttpRoleName::new("default"), crate::HttpRoleName::new("default"),
true, true,
std::vec![super::HttpRequestKind::wildcard()], std::vec![crate::HttpRequestKind::wildcard()],
100, 100,
super::HttpRoleLimits::new(std::option::Option::None, std::option::Option::Some(non_zero(2)), std::option::Option::None, std::option::Option::None), crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::Some(non_zero(2)), std::option::Option::None, std::option::Option::None),
); );
let modified_endpoint = super::HttpEndpointSettings::new( let modified_endpoint = crate::HttpEndpointSettings::new(
endpoint.name(), endpoint.name(),
true, true,
endpoint.provider().clone(), endpoint.provider().clone(),
@@ -173,16 +173,16 @@ fn transport_settings_reject_burst_without_rps() {
endpoint.max_idle_connections_per_host(), endpoint.max_idle_connections_per_host(),
std::vec![role], std::vec![role],
); );
let settings = super::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone()); let settings = crate::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone());
assert!(settings.validate().is_err()); assert!(settings.validate().is_err());
} }
#[test] #[test]
fn transport_settings_reject_reversed_retry_backoff() { fn transport_settings_reject_reversed_retry_backoff() {
let base = valid_settings("https://api.devnet.solana.com"); let base = valid_settings("https://api.devnet.solana.com");
let settings = super::HttpTransportSettings::new( let settings = crate::HttpTransportSettings::new(
base.endpoints().to_vec(), base.endpoints().to_vec(),
super::HttpRetrySettings::new(2, std::time::Duration::from_secs(2), std::time::Duration::from_secs(1)), crate::HttpRetrySettings::new(2, std::time::Duration::from_secs(2), std::time::Duration::from_secs(1)),
); );
assert!(settings.validate().is_err()); assert!(settings.validate().is_err());
} }
@@ -191,7 +191,7 @@ fn transport_settings_reject_reversed_retry_backoff() {
fn transport_settings_reject_zero_request_timeout() { fn transport_settings_reject_zero_request_timeout() {
let base = valid_settings("https://api.devnet.solana.com"); let base = valid_settings("https://api.devnet.solana.com");
let endpoint = &base.endpoints()[0]; let endpoint = &base.endpoints()[0];
let modified_endpoint = super::HttpEndpointSettings::new( let modified_endpoint = crate::HttpEndpointSettings::new(
endpoint.name(), endpoint.name(),
true, true,
endpoint.provider().clone(), endpoint.provider().clone(),
@@ -202,6 +202,6 @@ fn transport_settings_reject_zero_request_timeout() {
endpoint.max_idle_connections_per_host(), endpoint.max_idle_connections_per_host(),
endpoint.roles().to_vec(), endpoint.roles().to_vec(),
); );
let settings = super::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone()); let settings = crate::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone());
assert!(settings.validate().is_err()); assert!(settings.validate().is_err());
} }

View File

@@ -0,0 +1,26 @@
# file: crates/ksp-wallet-lib/Cargo.toml
# version: 6
[package]
name = "ksp-wallet-lib"
version.workspace = true
edition.workspace = true
repository.workspace = true
[dependencies]
argon2 = { workspace = true, features = ["alloc", "zeroize"] }
chacha20poly1305 = { workspace = true, features = ["alloc", "zeroize"] }
ed25519-dalek = { workspace = true, features = ["signature", "zeroize"] }
getrandom.workspace = true
ksp-core-lib = { path = "../ksp-core-lib" }
ksp-logging-lib = { path = "../ksp-logging-lib" }
base64.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
solana-keypair.workspace = true
tokio = { workspace = true, features = ["rt"] }
tempfile.workspace = true
zeroize.workspace = true
[lints]
workspace = true

View File

@@ -0,0 +1,194 @@
<!-- file: crates/ksp-wallet-lib/README.md -->
<!-- version: 2 -->
# `ksp-wallet-lib`
Statut : **stable depuis KSP `0.2.5`**.
`ksp-wallet-lib` est la bibliothèque KSP propriétaire du Wallet Solana natif. Elle possède le format autonome `.kspwallet` V1, les capacités indépendantes VIEW/OWNER, la protection du secret Solana, la signature, l'administration des metadata, les rotations de credentials, la persistence native et les adapters d'import/export explicitement supportés.
La crate est volontairement indépendante de Config, du réseau et de Tauri. Un consumer fournit les chemins, passwords et metadata ; Wallet ouvre, protège, signe et persiste sans décider d'une policy de dépense ni contacter un RPC.
## Responsabilités
La crate possède :
- le format natif `.kspwallet` V1 et son parser JSON strict ;
- les key slots OWNER/VIEW indépendants ;
- Argon2id pour les KDF de passwords ;
- XChaCha20-Poly1305 pour le wrapping et les compartiments ;
- une autorité Ed25519 d'administration distincte de la keypair Solana ;
- les compartiments `owner_control`, `metadata` et `secret` ;
- la création et l'ouverture in-memory ;
- la création et l'ouverture fichier async-first ;
- la projection verrouillée minimale ;
- la signature Solana via OWNER sans getter secret ;
- l'alias et les notes protégés ;
- les rotations OWNER/VIEW ;
- la révocation forte VIEW par OWNER ;
- la persistence create/import no-clobber ;
- le remplacement administratif capability-bound avec détection de handle stale ;
- l'inspection/import/export Solana CLI JSON et Base58 de keypair complète ;
- les diagnostics et erreurs Wallet sans exposition de secrets.
La crate ne possède pas :
```text
Config
Transport HTTP/WebSocket/gRPC
balance/réseau
WalletPolicy / execution policy
construction ou envoi de transaction
Store
Tauri/UI
hardware wallet / remote signer
anti-rollback externe
```
## Frontières de dépendances
La direction de production est :
```text
ksp-wallet-lib
-> ksp-core-lib
-> ksp-logging-lib
-> argon2 / chacha20poly1305 / getrandom
-> ed25519-dalek / solana-keypair
-> serde / serde_json
-> tempfile / tokio
-> zeroize
```
Les dépendances suivantes sont interdites :
```text
ksp-wallet-lib -X-> ksp-config-lib
ksp-wallet-lib -X-> ksp-onchain-transport-lib
ksp-wallet-lib -X-> execution policy
ksp-wallet-lib -X-> Store
ksp-wallet-lib -X-> Tauri
ksp-wallet-lib -X-> tracing direct
ksp-wallet-lib -X-> std::env
ksp-wallet-lib -X-> solana-pubkey direct
ksp-wallet-lib -X-> solana-signer direct
ksp-wallet-lib -X-> solana-signature direct
```
La Pubkey publique est toujours `ksp_core_lib::Pubkey`. `solana-keypair` reste un détail secret/signing interne à Wallet et n'est pas réexportée.
## Modèle de capacités
### Verrouillé
Sans password, `inspect_locked_wallet_v1` et `inspect_locked_wallet_file_v1` exposent uniquement :
```text
format_version
view_enabled
```
La Pubkey, l'alias et les notes restent chiffrés.
### VIEW
`WalletView` expose :
```text
Pubkey
alias
notes
rotation de son propre password VIEW
```
VIEW ne possède aucune API de signature, d'export secret, de mutation metadata, de rotation OWNER ou de désactivation/recréation VIEW.
### OWNER
`WalletOwner` expose toutes les metadata autorisées et ajoute :
```text
signature Solana
export du secret via adapters explicites
alias/notes administration
rotation OWNER
rotation VIEW sans ancien password VIEW
disable VIEW avec rekey metadata fort
recreate VIEW avec nouveau slot et nouveau K_metadata
```
OWNER ne dépend jamais du password VIEW.
## Format `.kspwallet` V1
Le format est publiquement spécifié dans [`../../docs/formats/KSPWALLET_V1.md`](../../docs/formats/KSPWALLET_V1.md). Il est autonome : un fichier valide et le password de la capacité concernée suffisent à l'ouverture ; aucun pepper KSP, keychain, OTP, service distant, réseau ou secret externe n'est requis.
Les principales primitives sont :
```text
KDF Argon2id v19
profil création 65 536 KiB / 3 iterations / 1 lane
AEAD XChaCha20-Poly1305
CSPRNG OS via getrandom
state signature Ed25519
wire binary Base64url sans padding
format JSON UTF-8 strict
```
Les paramètres KDF sont sérialisés dans chaque slot afin que de futurs defaults puissent évoluer sans rendre les wallets existants illisibles.
## Persistence
Toute création/import reçoit un chemin explicite du caller et applique le no-clobber. Wallet ne découvre ni ne crée un répertoire configuré par lui-même.
Les mutations OWNER/VIEW persistées utilisent un remplacement capability-bound : le fichier courant doit encore correspondre à l'enveloppe authentifiée attendue par le handle. Un handle stale ou une mauvaise cible retourne `wallet.state_conflict`.
Cette protection ne constitue pas un CAS filesystem linéarisable et ne fournit pas d'anti-rollback externe. Les garanties d'atomicité/crash-durability dépendent également de l'OS et du filesystem.
## Import/export
Les formats built-in V1 sont :
```text
WalletTransferFormat::SolanaCliJson
WalletTransferFormat::SolanaKeypairBase58
```
L'inspection d'un transfert retourne seulement sa Pubkey et son format. L'import crée toujours un nouveau `.kspwallet` no-clobber autour de la keypair validée. L'export secret appartient uniquement à `WalletOwner`.
`WalletTransferFormat` est `#[non_exhaustive]` afin de permettre des formats built-in supplémentaires sans prétendre fournir un plugin public arbitraire de codec secret.
## Sécurité et limites
Le threat model V1 considère notamment un attaquant possédant une copie complète du fichier et capable d'essais de password offline sans limite serveur. Argon2id augmente le coût de chaque essai ; il ne compense pas un password faible.
Limites explicitement assumées :
- remplacement total par un autre wallet valide non détectable depuis le nouveau fichier seul ;
- rollback vers une ancienne copie valide non détectable sans état/ancre externe ;
- absence de second facteur, keychain, hardware wallet ou remote signer en V1 ;
- `zeroize` réduit les copies possédées mais ne prouve pas l'effacement physique de toute copie potentielle ;
- les permissions filesystem sont une hygiène externe, pas une garantie cryptographique.
La matrice durable [`../../docs/validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md`](../../docs/validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md) enregistre les canaris adversariaux, l'interop indépendante et l'audit Cargo.
## Tests et vecteurs
Les fixtures publiques test-only sont sous `tests/fixtures/` :
```text
kspwallet_v1_wire_only.json
kspwallet_v1_crypto_vectors.json
kspwallet_v1_full_vector.json
kspwallet_v1_full_vector_meta.json
```
Elles couvrent le wire, Argon2id/XChaCha20-Poly1305, l'ouverture VIEW/OWNER, la signature d'état et l'interop transfer. Les tests adversariaux couvrent tampering, wrong passwords, frontières VIEW/OWNER, persistence, concurrence et diagnostics secrets.
## Documentation
- [`USAGE.md`](USAGE.md) — exemples des principales surfaces publiques ;
- [`../../docs/formats/KSPWALLET_V1.md`](../../docs/formats/KSPWALLET_V1.md) — spécification normative indépendante de Rust ;
- [`../../docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md`](../../docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md) — plan historique et threat model de `0.2.5` ;
- [`../../docs/validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md`](../../docs/validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md) — matrice de sécurité/interoperabilité/compliance ;
- [`../../prompts/011-V0_2_6_START_PROMPT.md`](../../prompts/011-V0_2_6_START_PROMPT.md) — reprise vers Wallet Desk après publication stable de `0.2.5`.

View File

@@ -0,0 +1,279 @@
<!-- file: crates/ksp-wallet-lib/USAGE.md -->
<!-- version: 2 -->
# Utilisation de `ksp-wallet-lib`
Ce guide présente les principales surfaces publiques de Wallet V1. La spécification cryptographique du fichier reste [`../../docs/formats/KSPWALLET_V1.md`](../../docs/formats/KSPWALLET_V1.md).
Les exemples utilisent des chemins explicites : Wallet ne lit ni Config ni environnement pour découvrir un répertoire.
## 1. Créer un nouveau `.kspwallet`
```rust
async fn create_example() -> ksp_core_lib::Result<()> {
let metadata = ksp_wallet_lib::WalletCreateMetadataV1::new(
Some(std::string::String::from("devnet-main")),
vec![std::string::String::from("wallet de test")],
);
let owner_password = ksp_wallet_lib::OwnerPassword::new(std::string::String::from("OWNER-PASSWORD"));
let view_password = ksp_wallet_lib::ViewPassword::new(std::string::String::from("VIEW-PASSWORD"));
let created = ksp_wallet_lib::create_wallet_file_v1(
"wallets/devnet-main.kspwallet",
owner_password,
Some(view_password),
metadata,
)
.await;
let owner = match created {
Ok(value) => value,
Err(error) => return Err(error),
};
println!("{}", owner.pubkey());
return Ok(());
}
```
La destination doit avoir un parent existant. Une destination existante n'est jamais remplacée par une création.
Pour créer uniquement en mémoire, utiliser `create_wallet_v1` puis `WalletOwner::to_json_bytes()` si le caller possède lui-même une autre boundary de stockage.
## 2. Inspecter un wallet verrouillé
```rust
async fn inspect_example() -> ksp_core_lib::Result<()> {
let inspected = ksp_wallet_lib::inspect_locked_wallet_file_v1("wallets/devnet-main.kspwallet").await;
let locked = match inspected {
Ok(value) => value,
Err(error) => return Err(error),
};
println!("format={}", locked.format_version());
println!("view_enabled={}", locked.view_enabled());
return Ok(());
}
```
Cette projection ne contient volontairement ni Pubkey, ni alias, ni notes.
## 3. Ouvrir avec VIEW
```rust
async fn open_view_example() -> ksp_core_lib::Result<()> {
let password = ksp_wallet_lib::ViewPassword::new(std::string::String::from("VIEW-PASSWORD"));
let opened = ksp_wallet_lib::open_wallet_view_file_v1("wallets/devnet-main.kspwallet", password).await;
let mut view = match opened {
Ok(value) => value,
Err(error) => return Err(error),
};
println!("pubkey={}", view.pubkey());
println!("alias={:?}", view.alias());
for note in view.notes() {
println!("note={} text={}", note.id(), note.text());
}
let rotated = view
.rotate_view_password(
"wallets/devnet-main.kspwallet",
ksp_wallet_lib::ViewPassword::new(std::string::String::from("NEW-VIEW-PASSWORD")),
)
.await;
if let Err(error) = rotated {
return Err(error);
}
return Ok(());
}
```
VIEW ne possède aucune API `sign`, `export_transfer`, `update_alias`, `add_note`, `rotate_owner_password`, `disable_view` ou `recreate_view`.
## 4. Ouvrir avec OWNER et signer
```rust
async fn sign_example(message: &[u8]) -> ksp_core_lib::Result<[u8; ksp_wallet_lib::KSPWALLET_SOLANA_SIGNATURE_BYTES]> {
let password = ksp_wallet_lib::OwnerPassword::new(std::string::String::from("OWNER-PASSWORD"));
let opened = ksp_wallet_lib::open_wallet_owner_file_v1("wallets/devnet-main.kspwallet", password).await;
let owner = match opened {
Ok(value) => value,
Err(error) => return Err(error),
};
return owner.sign(message);
}
```
La signature retournée contient 64 octets Ed25519. Aucun getter public ne retourne la keypair Solana.
## 5. Administrer alias et notes
```rust
async fn metadata_example() -> ksp_core_lib::Result<()> {
let password = ksp_wallet_lib::OwnerPassword::new(std::string::String::from("OWNER-PASSWORD"));
let opened = ksp_wallet_lib::open_wallet_owner_file_v1("wallets/devnet-main.kspwallet", password).await;
let mut owner = match opened {
Ok(value) => value,
Err(error) => return Err(error),
};
let alias_result = owner
.update_alias("wallets/devnet-main.kspwallet", Some(std::string::String::from("primary-devnet")))
.await;
if let Err(error) = alias_result {
return Err(error);
}
let added = owner
.add_note("wallets/devnet-main.kspwallet", std::string::String::from("rotation trimestrielle"))
.await;
let note_id = match added {
Ok(value) => value,
Err(error) => return Err(error),
};
let updated = owner
.update_note(
"wallets/devnet-main.kspwallet",
note_id.as_str(),
std::string::String::from("rotation contrôlée"),
)
.await;
if let Err(error) = updated {
return Err(error);
}
return Ok(());
}
```
Chaque mutation vérifie que le fichier courant correspond encore à l'état authentifié attendu par le handle. Un handle stale reçoit `wallet.state_conflict`.
## 6. Rotations de credentials
OWNER peut changer son password :
```rust
let result = owner
.rotate_owner_password(
"wallets/devnet-main.kspwallet",
ksp_wallet_lib::OwnerPassword::new(std::string::String::from("NEW-OWNER-PASSWORD")),
)
.await;
```
OWNER peut aussi changer le password VIEW sans connaître l'ancien :
```rust
let result = owner
.rotate_view_password(
"wallets/devnet-main.kspwallet",
ksp_wallet_lib::ViewPassword::new(std::string::String::from("NEW-VIEW-PASSWORD")),
)
.await;
```
Ces rotations de credentials ne changent pas la keypair Solana.
## 7. Révocation forte VIEW
La simple rotation VIEW remplace le credential courant mais ne peut pas retirer des metadata déjà connues d'un ancien détenteur VIEW. OWNER peut effectuer une révocation forte pour les metadata futures :
```rust
let disabled = owner.disable_view("wallets/devnet-main.kspwallet").await;
if let Err(error) = disabled {
return Err(error);
}
let recreated = owner
.recreate_view(
"wallets/devnet-main.kspwallet",
ksp_wallet_lib::ViewPassword::new(std::string::String::from("FRESH-VIEW-PASSWORD")),
)
.await;
```
`disable_view` rekey les metadata. `recreate_view` crée ensuite un nouveau slot VIEW autour du nouveau `K_metadata`. La keypair Solana reste inchangée.
## 8. Inspecter et importer une keypair externe
Le format doit être choisi explicitement ; Wallet ne fait pas d'auto-détection heuristique.
```rust
async fn inspect_transfer_example(source: &[u8]) -> ksp_core_lib::Result<()> {
let inspected = ksp_wallet_lib::inspect_wallet_transfer(
source,
ksp_wallet_lib::WalletTransferFormat::SolanaCliJson,
);
let info = match inspected {
Ok(value) => value,
Err(error) => return Err(error),
};
println!("{}", info.pubkey());
return Ok(());
}
```
Import fichier vers un nouveau `.kspwallet` :
```rust
let imported = ksp_wallet_lib::import_wallet_transfer_file_v1(
"wallets/imported.kspwallet",
"wallets/legacy-id.json",
ksp_wallet_lib::WalletTransferFormat::SolanaCliJson,
ksp_wallet_lib::OwnerPassword::new(std::string::String::from("OWNER-PASSWORD")),
None,
ksp_wallet_lib::WalletCreateMetadataV1::new(Some(std::string::String::from("imported")), vec![]),
)
.await;
```
L'import valide la cohérence secret/public, conserve exactement la keypair Solana et génère un nouvel environnement cryptographique KSP. Une destination native existante n'est jamais écrasée.
## 9. Exporter avec OWNER
En mémoire :
```rust
let exported = owner.export_transfer(ksp_wallet_lib::WalletTransferFormat::SolanaKeypairBase58);
```
Vers un fichier no-clobber :
```rust
let exported = owner
.export_transfer_file(
"exports/devnet-main.keypair",
ksp_wallet_lib::WalletTransferFormat::SolanaCliJson,
)
.await;
```
Les octets retournés par `export_transfer` contiennent volontairement le secret. Le caller doit limiter leur durée de vie et les zeroize lorsqu'approprié. Sur Unix, l'export fichier tente `0600`, qui reste une hygiène filesystem et non une garantie cryptographique.
## 10. Surfaces in-memory
Les équivalents sans I/O filesystem sont :
```text
create_wallet_v1
open_wallet_view_v1
open_wallet_owner_v1
inspect_locked_wallet_v1
inspect_wallet_transfer
```
Les handles `WalletOwner` et `WalletView` peuvent être sérialisés vers le document natif courant avec `to_json_bytes()`. Ces bytes restent un `.kspwallet` chiffré, pas un export de la keypair.
## 11. Erreurs et diagnostics
Les erreurs Wallet sont des `ksp_core_lib::Error` avec codes `wallet.*`. Les erreurs de password restent génériques (`owner_unlock_failed`, `view_unlock_failed`) et ne doivent jamais être transformées en oracle détaillant KDF/wrap/ciphertext.
Les `Debug` des passwords et metadata protégées sont redacted. Les logs Wallet ne doivent contenir ni password, ni keypair, ni payload exporté.
## 12. Intégration avec Config et Transport
Wallet lui-même ne dépend ni de Config ni de Transport. Une application compose explicitement les couches :
```text
ksp-config-lib
-> fournit chemins/profils applicatifs
ksp-wallet-lib
-> ouvre le wallet et fournit la Pubkey autorisée
ksp-onchain-transport-lib
-> utilise cette Pubkey pour getBalance et autres lectures réseau
```
Cette composition est le rôle de `0.2.6 — ksp-app-wallet-desk`, pas de `ksp-wallet-lib`.

View File

@@ -0,0 +1,15 @@
// file: crates/ksp-wallet-lib/src/capability.rs
// version: 1
/// Authorized capability represented by an unlocked Wallet handle.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum WalletCapability {
/// Metadata access plus rotation of the current VIEW password only.
View,
/// Full Wallet ownership including signing and administration.
Owner,
}
#[cfg(test)]
#[path = "../unit_tests/capability.rs"]
mod tests;

View File

@@ -0,0 +1,90 @@
// file: crates/ksp-wallet-lib/src/constants.rs
// version: 7
//! Wallet-owned constants.
/// Native Wallet format version implemented by the V1 codec.
pub const KSPWALLET_FORMAT_VERSION_V1: u32 = 1;
/// Exact magic string required by every native `.kspwallet` document.
pub const KSPWALLET_MAGIC: &str = "KSPWALLET";
/// Maximum accepted `.kspwallet` document size before JSON parsing.
pub const KSPWALLET_MAX_FILE_BYTES: usize = 1024 * 1024;
/// Byte length of every Solana Ed25519 message signature returned by Wallet OWNER.
pub const KSPWALLET_SOLANA_SIGNATURE_BYTES: usize = 64;
/// Maximum accepted canonical Base58 keypair transfer size before decoding.
pub const KSPWALLET_TRANSFER_MAX_BASE58_BYTES: usize = 128;
/// Maximum accepted Solana CLI keypair JSON transfer size before parsing.
pub const KSPWALLET_TRANSFER_MAX_SOLANA_CLI_JSON_BYTES: usize = 1024;
/// Byte length of the Poly1305 authentication tag appended to each ciphertext.
pub const KSPWALLET_V1_AEAD_TAG_BYTES: usize = 16;
/// Argon2 version serialized by V1 key slots.
pub const KSPWALLET_V1_ARGON2_VERSION: u32 = 19;
/// Default Argon2id iteration count for newly created V1 key slots.
pub const KSPWALLET_V1_DEFAULT_ARGON2_ITERATIONS: u32 = 3;
/// Default Argon2id memory cost for newly created V1 key slots, calibrated on the 2026-08-19 operator benchmark.
pub const KSPWALLET_V1_DEFAULT_ARGON2_MEMORY_KIB: u32 = 65_536;
/// Default Argon2id parallelism for newly created V1 key slots.
pub const KSPWALLET_V1_DEFAULT_ARGON2_PARALLELISM: u32 = 1;
/// Salt size generated independently for every newly created V1 key slot.
pub const KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES: usize = 32;
/// Byte length of an Ed25519 public key used as Wallet format authority.
pub const KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES: usize = 32;
/// Byte length of an Ed25519 detached state signature.
pub const KSPWALLET_V1_ED25519_SIGNATURE_BYTES: usize = 64;
/// Initial protected payload version used independently by control, metadata and secret compartments.
pub const KSPWALLET_V1_INITIAL_PAYLOAD_VERSION: u32 = 1;
/// Maximum protected alias size in UTF-8 bytes for metadata V1.
pub const KSPWALLET_V1_MAX_ALIAS_BYTES: usize = 256;
/// Structural V1 ceiling for serialized Argon2 iteration cost; creation defaults are benchmarked separately.
pub const KSPWALLET_V1_MAX_ARGON2_ITERATIONS: u32 = 64;
/// Structural V1 ceiling for serialized Argon2 memory cost; creation defaults are benchmarked separately.
pub const KSPWALLET_V1_MAX_ARGON2_MEMORY_KIB: u32 = 1024 * 1024;
/// Structural V1 ceiling for serialized Argon2 parallelism; creation defaults are benchmarked separately.
pub const KSPWALLET_V1_MAX_ARGON2_PARALLELISM: u32 = 64;
/// Maximum accepted Argon2 salt size in bytes.
pub const KSPWALLET_V1_MAX_KDF_SALT_BYTES: usize = 64;
/// Maximum number of key slots understood by format V1: exactly one OWNER plus optional VIEW.
pub const KSPWALLET_V1_MAX_KEY_SLOTS: usize = 2;
/// Maximum ciphertext size of one wrapped capability payload.
pub const KSPWALLET_V1_MAX_KEY_WRAP_CIPHERTEXT_BYTES: usize = 4096;
/// Maximum metadata ciphertext size, including the AEAD tag over the bounded 64-KiB plaintext.
pub const KSPWALLET_V1_MAX_METADATA_CIPHERTEXT_BYTES: usize = KSPWALLET_V1_MAX_METADATA_PLAINTEXT_BYTES + KSPWALLET_V1_AEAD_TAG_BYTES;
/// Maximum metadata plaintext size before V1 encryption.
pub const KSPWALLET_V1_MAX_METADATA_PLAINTEXT_BYTES: usize = 64 * 1024;
/// Maximum number of protected notes in metadata V1.
pub const KSPWALLET_V1_MAX_NOTES: usize = 64;
/// Maximum protected note text size in UTF-8 bytes for metadata V1.
pub const KSPWALLET_V1_MAX_NOTE_TEXT_BYTES: usize = 8 * 1024;
/// Maximum OWNER-control ciphertext size accepted by envelope V1.
pub const KSPWALLET_V1_MAX_OWNER_CONTROL_CIPHERTEXT_BYTES: usize = 4096;
/// Maximum password size in exact UTF-8 input bytes.
pub const KSPWALLET_V1_MAX_PASSWORD_BYTES: usize = 1024;
/// Maximum OWNER-only secret ciphertext size accepted by envelope V1.
pub const KSPWALLET_V1_MAX_SECRET_CIPHERTEXT_BYTES: usize = 4096;
/// Domain separator for metadata compartment AAD.
pub const KSPWALLET_V1_METADATA_AAD_DOMAIN: &[u8] = b"KSPWALLET-V1-AAD-METADATA";
/// Minimum accepted Argon2 salt size in bytes.
pub const KSPWALLET_V1_MIN_KDF_SALT_BYTES: usize = 16;
/// Byte length of every protected metadata note identifier.
pub const KSPWALLET_V1_NOTE_ID_BYTES: usize = 16;
/// Domain separator for OWNER-control compartment AAD.
pub const KSPWALLET_V1_OWNER_CONTROL_AAD_DOMAIN: &[u8] = b"KSPWALLET-V1-AAD-OWNER-CONTROL";
/// Exact plaintext size of the V1 OWNER-control compartment.
pub const KSPWALLET_V1_OWNER_CONTROL_PLAINTEXT_BYTES: usize = 96;
/// Domain separator for OWNER key-slot wrapping AAD.
pub const KSPWALLET_V1_OWNER_SLOT_AAD_DOMAIN: &[u8] = b"KSPWALLET-V1-AAD-OWNER-SLOT";
/// Domain separator for OWNER-only secret compartment AAD.
pub const KSPWALLET_V1_SECRET_AAD_DOMAIN: &[u8] = b"KSPWALLET-V1-AAD-SECRET";
/// Exact plaintext size of the V1 Solana secret compartment.
pub const KSPWALLET_V1_SECRET_PLAINTEXT_BYTES: usize = 64;
/// Byte length of every V1 key-slot identifier.
pub const KSPWALLET_V1_SLOT_ID_BYTES: usize = 16;
/// Domain separator for the OWNER state-signature transcript.
pub const KSPWALLET_V1_STATE_TRANSCRIPT_DOMAIN: &[u8] = b"KSPWALLET-V1-STATE";
/// Domain separator for VIEW key-slot wrapping AAD.
pub const KSPWALLET_V1_VIEW_SLOT_AAD_DOMAIN: &[u8] = b"KSPWALLET-V1-AAD-VIEW-SLOT";
/// Byte length of an XChaCha20-Poly1305 nonce.
pub const KSPWALLET_V1_XCHACHA_NONCE_BYTES: usize = 24;
/// Owning tracing target for events emitted by the Wallet crate.
pub(crate) const TRACING_TARGET: &str = "ksp-wallet-lib";

View File

@@ -0,0 +1,197 @@
// file: crates/ksp-wallet-lib/src/crypto.rs
// version: 4
//! In-memory cryptographic primitives for native `.kspwallet` V1.
use chacha20poly1305::KeyInit; // rust-rules: trait-import
use chacha20poly1305::aead::Aead; // rust-rules: trait-import
/// Exact V1 content-key and password-derived-key size in bytes.
pub(crate) const SECRET_KEY_BYTES: usize = 32;
/// Owned 32-byte secret key with redacted diagnostics and drop-time zeroization.
pub(crate) struct SecretKeyV1 {
bytes: [u8; SECRET_KEY_BYTES],
}
impl crate::SecretKeyV1 {
/// Takes ownership of exact 32-byte secret material.
pub(crate) const fn from_bytes(bytes: [u8; SECRET_KEY_BYTES]) -> Self {
return Self { bytes };
}
/// Generates a fresh secret key from the operating-system CSPRNG.
pub(crate) fn random() -> ksp_core_lib::Result<Self> {
let bytes = match random_bytes::<SECRET_KEY_BYTES>() {
std::result::Result::Ok(bytes) => bytes,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(Self { bytes });
}
/// Borrows secret bytes internally without allocating or cloning.
pub(crate) const fn as_bytes(&self) -> &[u8; SECRET_KEY_BYTES] {
return &self.bytes;
}
}
impl std::fmt::Debug for crate::SecretKeyV1 {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.write_str("SecretKeyV1(<redacted>)");
}
}
impl std::ops::Drop for crate::SecretKeyV1 {
fn drop(&mut self) {
zeroize::Zeroize::zeroize(&mut self.bytes);
}
}
/// Generates a fresh fixed-size byte array from the operating-system CSPRNG.
pub(crate) fn random_bytes<const LENGTH: usize>() -> ksp_core_lib::Result<[u8; LENGTH]> {
let mut bytes = [0_u8; LENGTH];
let fill_result = getrandom::fill(bytes.as_mut_slice());
if fill_result.is_err() {
zeroize::Zeroize::zeroize(&mut bytes);
return std::result::Result::Err(randomness_error());
}
return std::result::Result::Ok(bytes);
}
/// Generates a fresh XChaCha20-Poly1305 nonce from the operating-system CSPRNG.
pub(crate) fn random_nonce() -> ksp_core_lib::Result<[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES]> {
return random_bytes::<{ crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES }>();
}
/// Derives one V1 password wrapping key from serialized Argon2id parameters.
pub(crate) fn derive_password_key(password: &[u8], kdf: &crate::WalletKdfParametersV1) -> ksp_core_lib::Result<crate::SecretKeyV1> {
return derive_argon2id(password, kdf.salt(), kdf.memory_kib(), kdf.iterations(), kdf.parallelism());
}
/// Wraps one 32-byte content key with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
pub(crate) fn wrap_key(
wrapping_key: &crate::SecretKeyV1,
key_to_wrap: &crate::SecretKeyV1,
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
aad: &[u8],
) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return crate::encrypt_bytes(wrapping_key, nonce, aad, key_to_wrap.as_bytes());
}
/// Unwraps one 32-byte content key and maps every AEAD authentication failure to the generic Wallet authentication error.
pub(crate) fn unwrap_key(
wrapping_key: &crate::SecretKeyV1,
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
aad: &[u8],
ciphertext: &[u8],
) -> ksp_core_lib::Result<crate::SecretKeyV1> {
let plaintext_result = crate::decrypt_bytes(wrapping_key, nonce, aad, ciphertext);
let mut plaintext = match plaintext_result {
std::result::Result::Ok(plaintext) => plaintext,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if plaintext.len() != SECRET_KEY_BYTES {
zeroize::Zeroize::zeroize(plaintext.as_mut_slice());
return std::result::Result::Err(authentication_error());
}
let converted = <[u8; SECRET_KEY_BYTES]>::try_from(plaintext.as_slice());
let key = match converted {
std::result::Result::Ok(key) => crate::SecretKeyV1::from_bytes(key),
std::result::Result::Err(_) => {
zeroize::Zeroize::zeroize(plaintext.as_mut_slice());
return std::result::Result::Err(authentication_error());
},
};
zeroize::Zeroize::zeroize(plaintext.as_mut_slice());
return std::result::Result::Ok(key);
}
/// Encrypts bounded plaintext bytes with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
pub(crate) fn encrypt_bytes(
key: &crate::SecretKeyV1,
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
aad: &[u8],
plaintext: &[u8],
) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
let cipher_result = chacha20poly1305::XChaCha20Poly1305::new_from_slice(key.as_bytes());
let cipher = match cipher_result {
std::result::Result::Ok(cipher) => cipher,
std::result::Result::Err(_) => return std::result::Result::Err(crypto_parameter_error()),
};
let nonce_value = chacha20poly1305::XNonce::from(*nonce);
let payload = chacha20poly1305::aead::Payload { msg: plaintext, aad };
let encrypted = cipher.encrypt(&nonce_value, payload);
return match encrypted {
std::result::Result::Ok(ciphertext) => std::result::Result::Ok(ciphertext),
std::result::Result::Err(_) => std::result::Result::Err(crypto_parameter_error()),
};
}
/// Decrypts authenticated ciphertext bytes and returns a generic authentication error on tag failure.
pub(crate) fn decrypt_bytes(
key: &crate::SecretKeyV1,
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
aad: &[u8],
ciphertext: &[u8],
) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
let cipher_result = chacha20poly1305::XChaCha20Poly1305::new_from_slice(key.as_bytes());
let cipher = match cipher_result {
std::result::Result::Ok(cipher) => cipher,
std::result::Result::Err(_) => return std::result::Result::Err(crypto_parameter_error()),
};
let nonce_value = chacha20poly1305::XNonce::from(*nonce);
let payload = chacha20poly1305::aead::Payload { msg: ciphertext, aad };
let decrypted = cipher.decrypt(&nonce_value, payload);
return match decrypted {
std::result::Result::Ok(plaintext) => std::result::Result::Ok(plaintext),
std::result::Result::Err(_) => std::result::Result::Err(authentication_error()),
};
}
fn derive_argon2id(password: &[u8], salt: &[u8], memory_kib: u32, iterations: u32, parallelism: u32) -> ksp_core_lib::Result<crate::SecretKeyV1> {
if password.is_empty() || password.len() > crate::KSPWALLET_V1_MAX_PASSWORD_BYTES {
return std::result::Result::Err(crypto_parameter_error());
}
if salt.len() < crate::KSPWALLET_V1_MIN_KDF_SALT_BYTES || salt.len() > crate::KSPWALLET_V1_MAX_KDF_SALT_BYTES {
return std::result::Result::Err(crypto_parameter_error());
}
if memory_kib == 0
|| memory_kib > crate::KSPWALLET_V1_MAX_ARGON2_MEMORY_KIB
|| iterations == 0
|| iterations > crate::KSPWALLET_V1_MAX_ARGON2_ITERATIONS
|| parallelism == 0
|| parallelism > crate::KSPWALLET_V1_MAX_ARGON2_PARALLELISM
|| memory_kib < parallelism * 8
{
return std::result::Result::Err(crypto_parameter_error());
}
let params_result = argon2::Params::new(memory_kib, iterations, parallelism, std::option::Option::Some(SECRET_KEY_BYTES));
let params = match params_result {
std::result::Result::Ok(params) => params,
std::result::Result::Err(_) => return std::result::Result::Err(crypto_parameter_error()),
};
let argon2 = argon2::Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
let mut output = [0_u8; SECRET_KEY_BYTES];
let derive_result = argon2.hash_password_into(password, salt, output.as_mut_slice());
if derive_result.is_err() {
zeroize::Zeroize::zeroize(&mut output);
return std::result::Result::Err(crypto_parameter_error());
}
return std::result::Result::Ok(crate::SecretKeyV1::from_bytes(output));
}
fn randomness_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_RANDOMNESS_FAILED, "Wallet cryptographic randomness could not be obtained");
}
fn crypto_parameter_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_CRYPTO_PARAMETERS_INVALID, "Wallet cryptographic parameters are invalid or unsupported");
}
fn authentication_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_AUTHENTICATION_FAILED, "Wallet cryptographic authentication failed");
}
#[cfg(test)]
#[path = "../unit_tests/crypto.rs"]
mod tests;

View File

@@ -0,0 +1,37 @@
// file: crates/ksp-wallet-lib/src/error.rs
// version: 5
/// Error code used when an atomic Wallet persistence operation cannot publish a valid replacement.
pub const ERROR_CODE_ATOMIC_PERSISTENCE_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "atomic_persistence_failed");
/// Error code used when an authenticated Wallet structure cannot be verified.
pub const ERROR_CODE_AUTHENTICATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "authentication_failed");
/// Error code used when an operation requires a capability that the caller does not own.
pub const ERROR_CODE_CAPABILITY_INSUFFICIENT: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "capability_insufficient");
/// Error code used when an internal blocking cryptographic operation cannot complete.
pub const ERROR_CODE_CRYPTO_OPERATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "crypto_operation_failed");
/// Error code used when serialized cryptographic parameters are invalid or unsupported.
pub const ERROR_CODE_CRYPTO_PARAMETERS_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "crypto_parameters_invalid");
/// Error code used when a no-clobber create or import destination already exists.
pub const ERROR_CODE_DESTINATION_EXISTS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "destination_exists");
/// Error code used when a native Wallet structure is invalid.
pub const ERROR_CODE_FORMAT_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "format_invalid");
/// Error code used when a native Wallet format version is unsupported.
pub const ERROR_CODE_FORMAT_VERSION_UNSUPPORTED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "format_version_unsupported");
/// Error code used when Wallet filesystem I/O fails.
pub const ERROR_CODE_IO_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "io_failed");
/// Error code used when imported or decoded key material is invalid.
pub const ERROR_CODE_KEY_MATERIAL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "key_material_invalid");
/// Error code used when a protected Wallet note identifier is not present.
pub const ERROR_CODE_NOTE_NOT_FOUND: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "note_not_found");
/// Error code used when an OWNER unlock attempt fails without exposing a finer cryptographic oracle.
pub const ERROR_CODE_OWNER_UNLOCK_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "owner_unlock_failed");
/// Error code used when the operating-system cryptographic random source cannot provide bytes.
pub const ERROR_CODE_RANDOMNESS_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "randomness_failed");
/// Error code used when a Wallet signing operation fails.
pub const ERROR_CODE_SIGNATURE_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "signature_failed");
/// Error code used when an administrative replacement targets a different or stale Wallet state.
pub const ERROR_CODE_STATE_CONFLICT: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "state_conflict");
/// Error code used when an import/export transfer format is unsupported.
pub const ERROR_CODE_TRANSFER_FORMAT_UNSUPPORTED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "transfer_format_unsupported");
/// Error code used when a VIEW unlock attempt fails without exposing a finer cryptographic oracle.
pub const ERROR_CODE_VIEW_UNLOCK_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "view_unlock_failed");

View File

@@ -0,0 +1,289 @@
// file: crates/ksp-wallet-lib/src/lib.rs
// version: 11
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! Autonomous KSP Wallet foundation.
//!
//! `ksp-wallet-lib` owns the native `.kspwallet` domain, VIEW/OWNER capability model, protected metadata projection, password-secret wrappers and Wallet
//! error contract. `0.2.5-pre.003` freezes the strict V1 JSON envelope, canonical Base64url decoding, structural limits and deterministic
//! state-transcript/AEAD-AAD byte codecs. `0.2.5-pre.004` adds the in-memory Argon2id/XChaCha20-Poly1305/CSPRNG primitives and deterministic crypto
//! vectors. `0.2.5-pre.005` adds exact protected payloads, OWNER Ed25519 state authentication and async in-memory create/open flows for VIEW and OWNER.
//! `0.2.5-pre.006` adds bounded async-first filesystem reads plus same-directory synchronized no-clobber publication for new native files. `0.2.5-pre.007`
//! adds Solana message signing, protected metadata administration, password rotation and strong VIEW disable/recreate with capability-bound atomic
//! replacement. `0.2.5-pre.008` adds bounded Solana CLI JSON and canonical full-keypair Base58 import/export adapters with safe inspection and no-clobber
//! native import/export publication. `0.2.5-pre.009` adds adversarial security/compliance canaries and records the final dependency/interop audit before
//! documentation closure. Public keys are consumed exclusively through the [`ksp_core_lib::Pubkey`] re-export owned by KSP Core, and behavioral
//! observability uses only
//! `ksp-logging-lib` with the explicit crate target defined in `src/constants.rs`.
mod capability;
mod constants;
mod crypto;
mod error;
mod metadata;
mod owner;
mod password;
mod payload;
mod persistence;
mod transcript;
mod transfer;
mod view;
mod wallet;
mod wire;
#[cfg(test)]
#[path = "../unit_tests/security.rs"]
mod security_tests;
/// Authorized capability represented by an unlocked Wallet handle.
pub use self::capability::WalletCapability;
/// Native `.kspwallet` V1 format version.
pub use self::constants::KSPWALLET_FORMAT_VERSION_V1;
/// Exact magic string required by every native `.kspwallet` document.
pub use self::constants::KSPWALLET_MAGIC;
/// Maximum accepted `.kspwallet` document size before parsing.
pub use self::constants::KSPWALLET_MAX_FILE_BYTES;
/// Byte length of every Solana Ed25519 signature returned by OWNER.
pub use self::constants::KSPWALLET_SOLANA_SIGNATURE_BYTES;
/// Maximum accepted Base58 keypair transfer size before decoding.
pub use self::constants::KSPWALLET_TRANSFER_MAX_BASE58_BYTES;
/// Maximum accepted Solana CLI JSON transfer size before parsing.
pub use self::constants::KSPWALLET_TRANSFER_MAX_SOLANA_CLI_JSON_BYTES;
/// Byte length of the AEAD authentication tag appended to V1 ciphertexts.
pub use self::constants::KSPWALLET_V1_AEAD_TAG_BYTES;
/// Argon2 version serialized by `.kspwallet` V1 key slots.
pub use self::constants::KSPWALLET_V1_ARGON2_VERSION;
/// Default Argon2id iteration count for newly created V1 slots.
pub use self::constants::KSPWALLET_V1_DEFAULT_ARGON2_ITERATIONS;
/// Default Argon2id memory cost for newly created V1 slots.
pub use self::constants::KSPWALLET_V1_DEFAULT_ARGON2_MEMORY_KIB;
/// Default Argon2id parallelism for newly created V1 slots.
pub use self::constants::KSPWALLET_V1_DEFAULT_ARGON2_PARALLELISM;
/// Default KDF salt size generated for newly created V1 slots.
pub use self::constants::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES;
/// Byte length of the Ed25519 format-authority public key.
pub use self::constants::KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES;
/// Byte length of the Ed25519 detached state signature.
pub use self::constants::KSPWALLET_V1_ED25519_SIGNATURE_BYTES;
/// Initial per-compartment protected payload version.
pub use self::constants::KSPWALLET_V1_INITIAL_PAYLOAD_VERSION;
/// Maximum alias size in protected metadata V1.
pub use self::constants::KSPWALLET_V1_MAX_ALIAS_BYTES;
/// Maximum structural Argon2 iteration count accepted by the V1 parser.
pub use self::constants::KSPWALLET_V1_MAX_ARGON2_ITERATIONS;
/// Maximum structural Argon2 memory cost accepted by the V1 parser.
pub use self::constants::KSPWALLET_V1_MAX_ARGON2_MEMORY_KIB;
/// Maximum structural Argon2 parallelism accepted by the V1 parser.
pub use self::constants::KSPWALLET_V1_MAX_ARGON2_PARALLELISM;
/// Maximum Argon2 salt size accepted by V1.
pub use self::constants::KSPWALLET_V1_MAX_KDF_SALT_BYTES;
/// Maximum number of OWNER/VIEW slots accepted by format V1.
pub use self::constants::KSPWALLET_V1_MAX_KEY_SLOTS;
/// Maximum wrapped-key ciphertext size accepted by V1.
pub use self::constants::KSPWALLET_V1_MAX_KEY_WRAP_CIPHERTEXT_BYTES;
/// Maximum protected metadata ciphertext size accepted by V1.
pub use self::constants::KSPWALLET_V1_MAX_METADATA_CIPHERTEXT_BYTES;
/// Maximum protected metadata plaintext size defined by V1.
pub use self::constants::KSPWALLET_V1_MAX_METADATA_PLAINTEXT_BYTES;
/// Maximum protected note text size defined by metadata V1.
pub use self::constants::KSPWALLET_V1_MAX_NOTE_TEXT_BYTES;
/// Maximum number of protected notes defined by metadata V1.
pub use self::constants::KSPWALLET_V1_MAX_NOTES;
/// Maximum OWNER-control ciphertext size accepted by V1.
pub use self::constants::KSPWALLET_V1_MAX_OWNER_CONTROL_CIPHERTEXT_BYTES;
/// Maximum password size in exact UTF-8 bytes defined by V1.
pub use self::constants::KSPWALLET_V1_MAX_PASSWORD_BYTES;
/// Maximum OWNER-only secret ciphertext size accepted by V1.
pub use self::constants::KSPWALLET_V1_MAX_SECRET_CIPHERTEXT_BYTES;
/// Domain separator for metadata-compartment AEAD AAD.
pub use self::constants::KSPWALLET_V1_METADATA_AAD_DOMAIN;
/// Minimum Argon2 salt size accepted by V1.
pub use self::constants::KSPWALLET_V1_MIN_KDF_SALT_BYTES;
/// Byte length of every protected metadata note identifier.
pub use self::constants::KSPWALLET_V1_NOTE_ID_BYTES;
/// Domain separator for OWNER-control compartment AEAD AAD.
pub use self::constants::KSPWALLET_V1_OWNER_CONTROL_AAD_DOMAIN;
/// Exact OWNER-control plaintext size fixed by V1.
pub use self::constants::KSPWALLET_V1_OWNER_CONTROL_PLAINTEXT_BYTES;
/// Domain separator for OWNER key-slot wrapping AAD.
pub use self::constants::KSPWALLET_V1_OWNER_SLOT_AAD_DOMAIN;
/// Domain separator for OWNER-only secret compartment AEAD AAD.
pub use self::constants::KSPWALLET_V1_SECRET_AAD_DOMAIN;
/// Exact Solana keypair plaintext size fixed by V1.
pub use self::constants::KSPWALLET_V1_SECRET_PLAINTEXT_BYTES;
/// Byte length of every V1 key-slot identifier.
pub use self::constants::KSPWALLET_V1_SLOT_ID_BYTES;
/// Domain separator for the OWNER state-signature transcript.
pub use self::constants::KSPWALLET_V1_STATE_TRANSCRIPT_DOMAIN;
/// Domain separator for VIEW key-slot wrapping AAD.
pub use self::constants::KSPWALLET_V1_VIEW_SLOT_AAD_DOMAIN;
/// Byte length of an XChaCha20-Poly1305 nonce.
pub use self::constants::KSPWALLET_V1_XCHACHA_NONCE_BYTES;
/// Error code used when an atomic Wallet persistence operation cannot publish a valid replacement.
pub use self::error::ERROR_CODE_ATOMIC_PERSISTENCE_FAILED;
/// Error code used when an authenticated Wallet structure cannot be verified.
pub use self::error::ERROR_CODE_AUTHENTICATION_FAILED;
/// Error code used when an operation requires a capability that the caller does not own.
pub use self::error::ERROR_CODE_CAPABILITY_INSUFFICIENT;
/// Error code used when a blocking cryptographic task cannot complete.
pub use self::error::ERROR_CODE_CRYPTO_OPERATION_FAILED;
/// Error code used when serialized cryptographic parameters are invalid or unsupported.
pub use self::error::ERROR_CODE_CRYPTO_PARAMETERS_INVALID;
/// Error code used when a no-clobber create or import destination already exists.
pub use self::error::ERROR_CODE_DESTINATION_EXISTS;
/// Error code used when a native Wallet structure is invalid.
pub use self::error::ERROR_CODE_FORMAT_INVALID;
/// Error code used when a native Wallet format version is unsupported.
pub use self::error::ERROR_CODE_FORMAT_VERSION_UNSUPPORTED;
/// Error code used when Wallet filesystem I/O fails.
pub use self::error::ERROR_CODE_IO_FAILED;
/// Error code used when imported or decoded key material is invalid.
pub use self::error::ERROR_CODE_KEY_MATERIAL_INVALID;
/// Error code used when a protected Wallet note identifier is absent.
pub use self::error::ERROR_CODE_NOTE_NOT_FOUND;
/// Error code used when an OWNER unlock attempt fails without exposing a finer cryptographic oracle.
pub use self::error::ERROR_CODE_OWNER_UNLOCK_FAILED;
/// Error code used when the operating-system cryptographic random source cannot provide bytes.
pub use self::error::ERROR_CODE_RANDOMNESS_FAILED;
/// Error code used when a Wallet signing operation fails.
pub use self::error::ERROR_CODE_SIGNATURE_FAILED;
/// Error code used when an administrative replacement sees a different or stale Wallet state.
pub use self::error::ERROR_CODE_STATE_CONFLICT;
/// Error code used when an import/export transfer format is unsupported.
pub use self::error::ERROR_CODE_TRANSFER_FORMAT_UNSUPPORTED;
/// Error code used when a VIEW unlock attempt fails without exposing a finer cryptographic oracle.
pub use self::error::ERROR_CODE_VIEW_UNLOCK_FAILED;
/// Minimal non-secret information available while a native Wallet remains locked.
pub use self::metadata::LockedWalletInfo;
/// Protected initial metadata supplied to native Wallet creation.
pub use self::metadata::WalletCreateMetadataV1;
/// Safe metadata projection produced after VIEW or OWNER authorization.
pub use self::metadata::WalletInfo;
/// One protected Wallet note exposed only after authorization.
pub use self::metadata::WalletNote;
/// Authorized OWNER capability handle.
pub use self::owner::WalletOwner;
/// Owned OWNER password material with redacted diagnostics and drop-time zeroization.
pub use self::password::OwnerPassword;
/// Owned VIEW password material with redacted diagnostics and drop-time zeroization.
pub use self::password::ViewPassword;
/// Creates and no-clobber persists a new native `.kspwallet` V1 file.
pub use self::persistence::create_wallet_file_v1;
/// Reads and verifies a locked native `.kspwallet` V1 file.
pub use self::persistence::inspect_locked_wallet_file_v1;
/// Opens a native `.kspwallet` V1 file with OWNER capability.
pub use self::persistence::open_wallet_owner_file_v1;
/// Opens a native `.kspwallet` V1 file with VIEW capability.
pub use self::persistence::open_wallet_view_file_v1;
/// Explicit secret-transfer format supported by Wallet.
pub use self::transfer::WalletTransferFormat;
/// Safe public identity projection of one validated secret-transfer source.
pub use self::transfer::WalletTransferInspection;
/// Imports one bounded external transfer file into a new no-clobber native Wallet V1.
pub use self::transfer::import_wallet_transfer_file_v1;
/// Imports one in-memory transfer payload into a new no-clobber native Wallet V1.
pub use self::transfer::import_wallet_transfer_v1;
/// Validates one in-memory transfer payload and exposes only its derived public identity.
pub use self::transfer::inspect_wallet_transfer;
/// Validates one bounded external transfer file and exposes only its derived public identity.
pub use self::transfer::inspect_wallet_transfer_file;
/// Authorized VIEW capability handle.
pub use self::view::WalletView;
/// Creates a new in-memory native Wallet V1.
pub use self::wallet::create_wallet_v1;
/// Parses and verifies locked native Wallet state without unlocking protected metadata.
pub use self::wallet::inspect_locked_wallet_v1;
/// Opens the OWNER capability from a native Wallet V1 document.
pub use self::wallet::open_wallet_owner_v1;
/// Opens the VIEW capability from a native Wallet V1 document.
pub use self::wallet::open_wallet_view_v1;
/// Strict semantic representation of one parsed native `.kspwallet` V1 envelope.
pub use self::wire::KspWalletEnvelopeV1;
/// Authenticated-encryption algorithm fixed by native Wallet V1.
pub use self::wire::WalletAeadAlgorithmV1;
/// Protected compartment kind fixed by native Wallet V1.
pub use self::wire::WalletCompartmentKindV1;
/// One validated encrypted V1 compartment.
pub use self::wire::WalletEncryptedCompartmentV1;
/// Password KDF fixed by native Wallet V1.
pub use self::wire::WalletKdfAlgorithmV1;
/// Parsed Argon2id parameters from one V1 key slot.
pub use self::wire::WalletKdfParametersV1;
/// Role of one native V1 key slot.
pub use self::wire::WalletKeySlotRoleV1;
/// One validated OWNER or VIEW V1 key slot.
pub use self::wire::WalletKeySlotV1;
/// Parsed AEAD wrapped-key payload from one V1 key slot.
pub use self::wire::WalletKeyWrapV1;
/// State-signature algorithm fixed by native Wallet V1.
pub use self::wire::WalletStateSignatureAlgorithmV1;
/// Detached OWNER state signature embedded in a V1 envelope.
pub use self::wire::WalletStateSignatureV1;
/// OWNER-signed stable descriptor of the optional VIEW slot.
pub use self::wire::WalletViewDescriptorV1;
/// Wallet-owned tracing target used by the KSP logging facade.
pub(crate) use self::constants::TRACING_TARGET;
/// Exact V1 content-key and password-derived-key size in bytes.
pub(crate) use self::crypto::SECRET_KEY_BYTES;
/// Owned 32-byte secret key with redacted diagnostics and drop-time zeroization.
pub(crate) use self::crypto::SecretKeyV1;
/// Decrypts authenticated ciphertext bytes and returns a generic authentication error on tag failure.
pub(crate) use self::crypto::decrypt_bytes;
/// Derives one V1 password wrapping key from serialized Argon2id parameters.
pub(crate) use self::crypto::derive_password_key;
/// Encrypts bounded plaintext bytes with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
pub(crate) use self::crypto::encrypt_bytes;
/// Generates a fresh fixed-size byte array from the operating-system CSPRNG.
pub(crate) use self::crypto::random_bytes;
/// Generates a fresh XChaCha20-Poly1305 nonce from the operating-system CSPRNG.
pub(crate) use self::crypto::random_nonce;
/// Unwraps one 32-byte content key and maps every AEAD authentication failure to the generic Wallet authentication error.
pub(crate) use self::crypto::unwrap_key;
/// Wraps one 32-byte content key with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
pub(crate) use self::crypto::wrap_key;
/// Crate-internal `MetadataPayloadV1` state shared across the owning crate.
pub(crate) use self::payload::MetadataPayloadV1;
/// Decodes metadata payload.
pub(crate) use self::payload::decode_metadata_payload;
/// Decodes owner control payload.
pub(crate) use self::payload::decode_owner_control_payload;
/// Encodes initial metadata payload.
pub(crate) use self::payload::encode_initial_metadata_payload;
/// Encodes owner control payload.
pub(crate) use self::payload::encode_owner_control_payload;
/// Internal no-clobber native persistence path shared by transfer adapters.
pub(crate) use self::persistence::persist_new_wallet_content_v1;
/// Persists new wallet fault before publish.
#[cfg(test)]
pub(crate) use self::persistence::persist_new_wallet_fault_before_publish;
/// Persists new wallet for test.
#[cfg(test)]
pub(crate) use self::persistence::persist_new_wallet_for_test;
/// Replaces wallet fault before publish.
#[cfg(test)]
pub(crate) use self::persistence::replace_wallet_fault_before_publish;
/// Replaces wallet file v1.
pub(crate) use self::persistence::replace_wallet_file_v1;
/// Replaces wallet for test.
#[cfg(test)]
pub(crate) use self::persistence::replace_wallet_for_test;
/// Internal deterministic compartment-AAD codec shared by Wallet crypto layers.
pub(crate) use self::transcript::compartment_aad;
/// Internal deterministic key-slot-AAD codec shared by Wallet crypto layers.
pub(crate) use self::transcript::slot_aad;
/// Internal deterministic OWNER-state transcript codec shared by Wallet crypto layers.
pub(crate) use self::transcript::state_transcript;
/// Internal no-clobber transfer-file writer used only by OWNER export.
pub(crate) use self::transfer::write_wallet_transfer_file_v1;
/// Crate-internal `OwnerStateV1` state shared across the owning crate.
pub(crate) use self::wallet::OwnerStateV1;
/// Crate-internal `ViewStateV1` state shared across the owning crate.
pub(crate) use self::wallet::ViewStateV1;
/// Internal imported-keypair creation path shared by transfer adapters.
pub(crate) use self::wallet::create_wallet_v1_from_keypair;
/// Verifies state signature.
pub(crate) use self::wallet::verify_state_signature;

View File

@@ -0,0 +1,161 @@
// file: crates/ksp-wallet-lib/src/metadata.rs
// version: 4
/// Protected metadata requested when creating a new native Wallet.
///
/// Alias and note texts are never rendered by `Debug`. Wallet generates stable random note identifiers when the metadata is sealed into the V1 payload.
#[derive(Default, Eq, PartialEq)]
pub struct WalletCreateMetadataV1 {
alias: std::option::Option<std::string::String>,
note_texts: std::vec::Vec<std::string::String>,
}
impl WalletCreateMetadataV1 {
/// Creates protected initial Wallet metadata.
#[must_use]
pub fn new(alias: std::option::Option<std::string::String>, note_texts: std::vec::Vec<std::string::String>) -> Self {
return Self { alias, note_texts };
}
/// Consumes this value and returns parts.
pub(crate) fn into_parts(self) -> (std::option::Option<std::string::String>, std::vec::Vec<std::string::String>) {
return (self.alias, self.note_texts);
}
}
impl std::fmt::Debug for WalletCreateMetadataV1 {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("WalletCreateMetadataV1")
.field("alias", &self.alias.as_ref().map(|_| return "<redacted>"))
.field("note_count", &self.note_texts.len())
.finish();
}
}
/// One protected Wallet note exposed only after VIEW or OWNER authorization.
#[derive(Clone, Eq, PartialEq)]
pub struct WalletNote {
id: std::string::String,
text: std::string::String,
}
impl WalletNote {
/// Creates a new `WalletNote` value.
pub(crate) fn new(id: std::string::String, text: std::string::String) -> Self {
return Self { id, text };
}
/// Returns the stable note identifier used by Wallet administration operations.
#[must_use]
pub fn id(&self) -> &str {
return self.id.as_str();
}
/// Returns the protected note text after authorization.
#[must_use]
pub fn text(&self) -> &str {
return self.text.as_str();
}
}
impl std::fmt::Debug for WalletNote {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.debug_struct("WalletNote").field("id", &"<redacted>").field("text", &"<redacted>").finish();
}
}
/// Safe metadata projection produced after VIEW or OWNER authorization.
#[derive(Clone, Eq, PartialEq)]
pub struct WalletInfo {
format_version: u32,
capability: crate::WalletCapability,
pubkey: ksp_core_lib::Pubkey,
alias: std::option::Option<std::string::String>,
notes: std::vec::Vec<crate::WalletNote>,
}
impl WalletInfo {
/// Creates a new `WalletInfo` value.
pub(crate) fn new(
capability: crate::WalletCapability,
pubkey: ksp_core_lib::Pubkey,
alias: std::option::Option<std::string::String>,
notes: std::vec::Vec<crate::WalletNote>,
) -> Self {
return Self { format_version: crate::KSPWALLET_FORMAT_VERSION_V1, capability, pubkey, alias, notes };
}
/// Returns the native Wallet format version parsed for this projection.
#[must_use]
pub const fn format_version(&self) -> u32 {
return self.format_version;
}
/// Returns the authorization capability that produced this projection.
#[must_use]
pub const fn capability(&self) -> crate::WalletCapability {
return self.capability;
}
/// Returns the authorized Solana public key using the KSP Core re-export.
#[must_use]
pub const fn pubkey(&self) -> &ksp_core_lib::Pubkey {
return &self.pubkey;
}
/// Returns the protected internal alias after authorization, when present.
#[must_use]
pub fn alias(&self) -> std::option::Option<&str> {
return self.alias.as_deref();
}
/// Returns the protected Wallet notes after authorization.
#[must_use]
pub fn notes(&self) -> &[crate::WalletNote] {
return self.notes.as_slice();
}
}
impl std::fmt::Debug for WalletInfo {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("WalletInfo")
.field("format_version", &self.format_version)
.field("capability", &self.capability)
.field("pubkey", &self.pubkey)
.field("alias", &self.alias.as_ref().map(|_| return "<redacted>"))
.field("note_count", &self.notes.len())
.finish();
}
}
/// Minimal non-secret information available while a native Wallet remains locked.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct LockedWalletInfo {
format_version: u32,
view_enabled: bool,
}
impl LockedWalletInfo {
/// Creates a new `LockedWalletInfo` value.
pub(crate) const fn new(view_enabled: bool) -> Self {
return Self { format_version: crate::KSPWALLET_FORMAT_VERSION_V1, view_enabled };
}
/// Returns the native Wallet format version.
#[must_use]
pub const fn format_version(&self) -> u32 {
return self.format_version;
}
/// Reports whether a VIEW capability slot exists without exposing its protected contents.
#[must_use]
pub const fn view_enabled(&self) -> bool {
return self.view_enabled;
}
}
#[cfg(test)]
#[path = "../unit_tests/metadata.rs"]
mod tests;

View File

@@ -0,0 +1,316 @@
// file: crates/ksp-wallet-lib/src/owner.rs
// version: 6
/// Authorized OWNER capability handle.
///
/// OWNER exposes protected metadata, Solana message signing and the authenticated administration operations defined by native Wallet V1. Secret key
/// material remains encapsulated and is never exposed through a general-purpose getter.
pub struct WalletOwner {
info: crate::WalletInfo,
state: crate::OwnerStateV1,
}
impl WalletOwner {
/// Builds `WalletOwner` from unlocked.
pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::OwnerStateV1) -> Self {
return Self { info, state };
}
/// Returns the authorization capability represented by this handle.
#[must_use]
pub const fn capability(&self) -> crate::WalletCapability {
return crate::WalletCapability::Owner;
}
/// Returns the authorized Solana public key.
#[must_use]
pub const fn pubkey(&self) -> &ksp_core_lib::Pubkey {
return self.info.pubkey();
}
/// Returns the protected internal alias, when present.
#[must_use]
pub fn alias(&self) -> std::option::Option<&str> {
return self.info.alias();
}
/// Returns the protected notes.
#[must_use]
pub fn notes(&self) -> &[crate::WalletNote] {
return self.info.notes();
}
/// Returns the complete safe metadata projection for this OWNER capability.
#[must_use]
pub fn info(&self) -> &crate::WalletInfo {
ksp_logging_lib::trace!(
target: crate::TRACING_TARGET,
operation = "wallet_info_projection",
capability = "owner",
"wallet metadata projection requested"
);
return &self.info;
}
/// Exports the immutable Solana keypair through one explicitly selected transfer adapter.
///
/// The returned bytes contain secret key material and are owned by the caller. Callers should minimize their lifetime and zeroize the buffer after use.
/// VIEW has no corresponding export API.
pub fn export_transfer(&self, format: crate::WalletTransferFormat) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
let encoded = self.state.export_transfer(format);
if let std::result::Result::Ok(value) = encoded.as_ref() {
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_export_transfer",
capability = "owner",
transfer_format = format.code(),
transfer_bytes = value.len(),
"wallet secret encoded through explicit OWNER transfer adapter"
);
}
return encoded;
}
/// Exports the immutable Solana keypair to a new no-clobber transfer file.
///
/// The destination parent must already exist. On Unix, Wallet attempts `0600` permissions as filesystem hygiene, but OS ACLs are not part of the
/// `.kspwallet` cryptographic security guarantee. Existing destinations are never overwritten.
pub async fn export_transfer_file(
&self,
destination: impl std::convert::AsRef<std::path::Path>,
format: crate::WalletTransferFormat,
) -> ksp_core_lib::Result<()> {
let encoded = match self.state.export_transfer(format) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let result = crate::write_wallet_transfer_file_v1(destination.as_ref().to_path_buf(), encoded, format).await;
if result.is_ok() {
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_export_transfer_file",
capability = "owner",
transfer_format = format.code(),
"wallet secret exported through explicit OWNER transfer adapter"
);
}
return result;
}
/// Signs one caller-provided message with the Wallet's immutable Solana keypair.
///
/// The returned bytes are the 64-byte Ed25519 signature. The Solana secret key is never returned or logged.
pub fn sign(&self, message: &[u8]) -> ksp_core_lib::Result<[u8; crate::KSPWALLET_SOLANA_SIGNATURE_BYTES]> {
let signature = self.state.sign_message(message);
if signature.is_ok() {
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_sign",
capability = "owner",
message_bytes = message.len(),
"wallet message signed"
);
}
return signature;
}
/// Updates or clears the protected internal alias and atomically replaces the existing wallet file.
pub async fn update_alias(
&mut self,
destination: impl std::convert::AsRef<std::path::Path>,
alias: std::option::Option<std::string::String>,
) -> ksp_core_lib::Result<()> {
let mut payload = crate::MetadataPayloadV1::from_info(&self.info);
let mutation_result = payload.set_alias(alias);
if let std::result::Result::Err(error) = mutation_result {
return std::result::Result::Err(error);
}
return self.persist_metadata_payload(destination.as_ref().to_path_buf(), payload, "wallet_update_alias").await;
}
/// Adds one protected note and returns its generated stable note identifier after successful persistence.
pub async fn add_note(
&mut self,
destination: impl std::convert::AsRef<std::path::Path>,
text: std::string::String,
) -> ksp_core_lib::Result<std::string::String> {
let mut payload = crate::MetadataPayloadV1::from_info(&self.info);
let note_id = match payload.add_note(text) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = self.persist_metadata_payload(destination.as_ref().to_path_buf(), payload, "wallet_add_note").await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(note_id);
}
/// Updates one protected note selected by its stable identifier.
pub async fn update_note(
&mut self,
destination: impl std::convert::AsRef<std::path::Path>,
note_id: &str,
text: std::string::String,
) -> ksp_core_lib::Result<()> {
let mut payload = crate::MetadataPayloadV1::from_info(&self.info);
let mutation_result = payload.update_note(note_id, text);
if let std::result::Result::Err(error) = mutation_result {
return std::result::Result::Err(error);
}
return self.persist_metadata_payload(destination.as_ref().to_path_buf(), payload, "wallet_update_note").await;
}
/// Deletes one protected note selected by its stable identifier.
pub async fn delete_note(&mut self, destination: impl std::convert::AsRef<std::path::Path>, note_id: &str) -> ksp_core_lib::Result<()> {
let mut payload = crate::MetadataPayloadV1::from_info(&self.info);
let mutation_result = payload.delete_note(note_id);
if let std::result::Result::Err(error) = mutation_result {
return std::result::Result::Err(error);
}
return self.persist_metadata_payload(destination.as_ref().to_path_buf(), payload, "wallet_delete_note").await;
}
/// Rotates the OWNER password while preserving the Wallet identity, metadata content key and Solana keypair.
pub async fn rotate_owner_password(
&mut self,
destination: impl std::convert::AsRef<std::path::Path>,
new_password: crate::OwnerPassword,
) -> ksp_core_lib::Result<()> {
let envelope = match self.state.stage_owner_password_rotation(new_password).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
self.state.apply_envelope(envelope);
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_rotate_owner_password",
capability = "owner",
"wallet OWNER password rotated"
);
return std::result::Result::Ok(());
}
/// Rotates the existing VIEW password without knowing the previous VIEW password.
///
/// This is a credential rotation only: it rewraps the same metadata content key in the same OWNER-signed VIEW slot descriptor.
pub async fn rotate_view_password(
&mut self,
destination: impl std::convert::AsRef<std::path::Path>,
new_password: crate::ViewPassword,
) -> ksp_core_lib::Result<()> {
let envelope = match self.state.stage_view_password_rotation(new_password).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
self.state.apply_envelope(envelope);
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_rotate_view_password",
capability = "owner",
"wallet VIEW password rotated by OWNER"
);
return std::result::Result::Ok(());
}
/// Strongly disables VIEW by generating a new metadata content key and removing the VIEW slot/descriptor.
///
/// Existing historical copies remain outside the revocation guarantee, but the old VIEW metadata key cannot decrypt future metadata states.
pub async fn disable_view(&mut self, destination: impl std::convert::AsRef<std::path::Path>) -> ksp_core_lib::Result<()> {
let (envelope, metadata_key) = match self.state.stage_disable_view() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
self.state.apply_strong_view_state(envelope, metadata_key);
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_disable_view",
capability = "owner",
"wallet VIEW capability strongly disabled"
);
return std::result::Result::Ok(());
}
/// Strongly recreates VIEW with a fresh metadata content key, fresh slot identifier and new VIEW password.
pub async fn recreate_view(
&mut self,
destination: impl std::convert::AsRef<std::path::Path>,
new_password: crate::ViewPassword,
) -> ksp_core_lib::Result<()> {
let (envelope, metadata_key) = match self.state.stage_recreate_view(new_password).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
self.state.apply_strong_view_state(envelope, metadata_key);
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_recreate_view",
capability = "owner",
"wallet VIEW capability strongly recreated"
);
return std::result::Result::Ok(());
}
/// Serializes the complete locked `.kspwallet` V1 document without exposing any unlocked secret material.
pub fn to_json_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return self.state.envelope().to_json_bytes();
}
async fn persist_metadata_payload(
&mut self,
destination: std::path::PathBuf,
payload: crate::MetadataPayloadV1,
operation: &'static str,
) -> ksp_core_lib::Result<()> {
let (envelope, info) = match self.state.stage_metadata_payload(payload) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = persist_staged(destination, self.state.envelope(), &envelope).await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
self.state.apply_envelope(envelope);
self.info = info;
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, operation = operation, capability = "owner", "wallet protected metadata updated");
return std::result::Result::Ok(());
}
}
impl std::fmt::Debug for WalletOwner {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.debug_struct("WalletOwner").field("info", &self.info).field("unlocked_state", &"<redacted>").finish();
}
}
async fn persist_staged(
destination: std::path::PathBuf,
expected_current: &crate::KspWalletEnvelopeV1,
envelope: &crate::KspWalletEnvelopeV1,
) -> ksp_core_lib::Result<()> {
let serialized = match envelope.to_json_bytes() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::replace_wallet_file_v1(destination, expected_current.clone(), serialized).await;
}
#[cfg(test)]
#[path = "../unit_tests/administration.rs"]
mod tests;

Some files were not shown because too many files have changed in this diff Show More