v0.2.5-pre.010.fix.001

This commit is contained in:
2026-08-20 11:17:12 +02:00
parent 5bf9651038
commit c0b131bf6f
97 changed files with 2277 additions and 655 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 159
# version: 160
[workspace]
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", "crates/ksp-wallet-lib"]
[workspace.package]
version = "0.2.5-pre.9"
version = "0.2.5-pre.10.fix.1"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md -->
<!-- version: 53 -->
<!-- version: 54 -->
# Roadmap KSP
@@ -49,7 +49,7 @@ 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.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.
- [/] `0.2.5` — Wallet foundation : `pre.001``pre.009` ont livré puis audité `.kspwallet` V1, VIEW/OWNER, crypto, persistence, administration, signature et transfer ; `pre.010` finalise maintenant README/USAGE, spec, graphes, matrice de clôture et prompt Wallet Desk sans changement de code/Cargo. `Pubkey` reste via `ksp-core-lib`, la keypair reste encapsulée dans Wallet et Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet. La candidate est prête pour `rel.001` après validation documentaire/opérateur finale.
- [/] `0.2.5` — Wallet foundation : `pre.001``pre.009` ont livré puis audité `.kspwallet` V1, VIEW/OWNER, crypto, persistence, administration, signature et transfer ; `pre.010` a finalisé README/USAGE, spec, graphes et prompt Wallet Desk. `pre.010-fix.001` est maintenant le gate technique avant release : dépendance directe `ed25519-dalek ^3.0`, normalisation Rust workspace-wide, audit `scripts/audit_rust_workspace_rules.py`, règles Rust/prompt renforcés. `Pubkey` reste via `ksp-core-lib`, la keypair reste encapsulée dans Wallet et Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet. `rel.001` ne sera ouverte qu'après validation opérateur complète du fix.
- [ ] `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.8` — Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/app_state.rs
// version: 7
// version: 8
//! Shared backend state owned by the Tauri application.
@@ -166,7 +166,7 @@ impl AppState {
let dropped = runtime.guard.dropped_lines();
let mut files = std::vec::Vec::<crate::LoggingRuntimeFileDto>::new();
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 {
active_profile: runtime.active_profile_id.clone(),

View File

@@ -1,13 +1,19 @@
// file: crates/ksp-app-config-desk/src/bootstrap.rs
// version: 4
// version: 5
//! Config and Logging bootstrap for the desktop application.
/// Crate-internal `LoggingStartup` state shared across the owning crate.
pub(crate) struct LoggingStartup {
/// Stores the guard value for this state.
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>,
/// Stores the selection source value for this state.
pub(crate) selection_source: String,
/// Stores the fallback active value for this state.
pub(crate) fallback_active: bool,
/// Stores the startup diagnostic value for this state.
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> {
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_args(arguments);
let bootstrap = match bootstrap {
@@ -38,6 +45,7 @@ pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_li
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(
management: &ksp_config_lib::ConfigManagement,
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,

View File

@@ -1,19 +1,19 @@
// file: crates/ksp-app-config-desk/src/documents.rs
// version: 1
// version: 2
//! 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 STATUS_INVALID: &str = "invalid";
const STAGE_VALID: &str = "valid";
const STAGE_READ: &str = "read";
const STAGE_EFFECTIVE: &str = "effective";
const STAGE_JSON: &str = "json";
const STAGE_OTHER: &str = "other";
const STAGE_READ: &str = "read";
const STAGE_SCHEMA: &str = "schema";
const STAGE_SEMANTIC: &str = "semantic";
const STAGE_EFFECTIVE: &str = "effective";
const STAGE_OTHER: &str = "other";
const STAGE_VALID: &str = "valid";
const STATUS_INVALID: &str = "invalid";
const STATUS_VALID: &str = "valid";
/// Safe inventory row for one Config document registered by `ksp-config-lib`.
#[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.
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 engine = management.engine();
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.
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 = match file_id {
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.
pub(crate) fn save_source(
pub(crate) fn save_document_source(
state: &crate::AppState,
file_id_text: &str,
source: &str,

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/dto_common.rs
// version: 1
// version: 2
//! 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.
#[derive(Clone, Debug, serde::Serialize, TS)]

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/environment.rs
// version: 2
// version: 3
//! 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.
#[derive(Clone, Debug, serde::Serialize, TS)]
@@ -63,17 +63,17 @@ impl EnvironmentMutationOperation {
}
/// 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());
}
/// 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);
}
/// 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);
}

View File

@@ -1,9 +1,9 @@
// 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.
use ts_rs::TS; // rust-rules: derive-import
use ts_rs::TS; // rust-rules: trait-import
/// Log payload sent by Config Desk frontend scripts.
#[derive(Clone, Debug, serde::Deserialize, TS)]

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/lib.rs
// version: 15
// version: 16
//! Tauri desktop application for managing and validating KSP configuration.
@@ -28,70 +28,173 @@ mod tw_splash;
/// Runs the KSP configuration desktop application.
pub use self::tauri::run;
/// Shared Config Desk application state managed by Tauri.
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;
/// Executes the crate-internal initialize logging operation for the owning module.
pub(crate) use self::bootstrap::initialize_logging;
/// Structured domain used while bootstrapping Config and Logging.
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;
/// Structured domain for safe Config environment reports.
pub(crate) use self::constants::TRACING_DOMAIN_ENVIRONMENT;
/// Structured domain used by technical frontend events.
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
/// Structured domain for typed Logging editor inspection.
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;
/// Structured domain used by controlled Logging test events.
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;
/// Structured domain for explicit privileged Secret reveal operations.
pub(crate) use self::constants::TRACING_DOMAIN_SECRETS;
/// Structured domain used by Tauri window lifecycle operations.
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
/// Owning target for backend events emitted by Config Desk.
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;
/// Owning target for main-window frontend events.
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;
/// Dedicated target used by the controlled Logging test panel.
pub(crate) use self::constants::TRACING_TARGET_LOGGING_TEST;
/// Detailed management view for one registered Config document.
pub(crate) use self::documents::ConfigDocumentDetailDto;
/// Safe document-specific command error with backend-owned diagnostic classification.
pub(crate) use self::documents::ConfigDocumentErrorDto;
/// Result of one validated raw-source repair attempt that reached persistence successfully.
pub(crate) use self::documents::ConfigDocumentSaveResultDto;
/// Safe inventory row for one Config document registered by `ksp-config-lib`.
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;
/// Safe command error projection that never serializes arbitrary KSP error context or source values.
pub(crate) use self::dto_common::CommandErrorDto;
/// Safe result metadata for one `.env` mutation.
pub(crate) use self::environment::ConfigEnvironmentChangeDto;
/// Safe desired/effective environment view exposed to the ordinary Config Desk frontend.
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;
/// Shared Config Desk runtime state cannot be locked safely.
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;
/// Frontend logging requested an unsupported level.
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;
/// Config Desk could not install the managed Logging runtime or its safe fallback.
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;
/// Controlled Logging test request contains an unsupported or invalid selector.
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;
/// Config profile data could not be projected safely for the frontend.
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;
/// Privileged reveal requested an unsupported source selector.
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;
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
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;
/// A required Tauri window is missing from the configured application runtime.
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;
/// Log payload sent by Config Desk frontend scripts.
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;
/// 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;
/// Complete typed Logging document exposed to the editor.
pub(crate) use self::logging_editor::LoggingDocumentDto;
/// Result of one validated typed Logging persistence operation.
pub(crate) use self::logging_editor::LoggingDocumentSaveResultDto;
/// One persistent file sink exposed to the editor.
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;
/// One Config document that exposes the standard `default_profile` / `profiles` contract.
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;
/// Privileged reveal response. This type deliberately does not derive `Clone` or `Debug`.
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;
/// Runtime timings used by the common desk splash lifecycle.
pub(crate) use self::splash::SplashSettings;
/// Resolves the required main window or returns a typed error.
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
// version: 5
// version: 6
//! 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.
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)]
@@ -138,12 +138,12 @@ pub(crate) struct LoggingDocumentSaveResultDto {
}
/// 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());
}
/// 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 = match file_id {
std::result::Result::Ok(value) => value,

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/logging_runtime.rs
// version: 3
// version: 4
//! 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.
#[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.
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();
}
/// 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 = match environment {
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();
}
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 {
output_id: metadata.output_id().to_owned(),
directory: metadata.directory().display().to_string(),
@@ -105,6 +106,7 @@ const fn rotation_label(rotation: ksp_logging_lib::FileRotation) -> &'static str
};
}
/// Executes the crate-internal count to u64 operation for the owning module.
pub(crate) fn count_to_u64(value: usize) -> u64 {
let converted = u64::try_from(value);
return match converted {

View File

@@ -1,9 +1,9 @@
// 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.
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.
#[derive(Clone, Debug, serde::Deserialize, TS)]
@@ -56,7 +56,7 @@ enum LoggingTestTarget {
}
/// 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 = match level {
std::result::Result::Ok(value) => value,

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/profiles.rs
// version: 2
// version: 3
//! 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.
#[derive(Clone, Debug, serde::Serialize, TS)]
@@ -83,12 +83,16 @@ struct ProfileContract {
}
/// 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());
}
/// 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);
}

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-app-config-desk/src/secrets.rs
// version: 1
// version: 2
//! 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.
#[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.
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);
}

View File

@@ -1,16 +1,16 @@
// file: crates/ksp-app-config-desk/src/splash.rs
// version: 4
// version: 5
//! 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 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_MINIMUM_MS: u64 = 60_000;
/// Runtime timings used by the common desk splash lifecycle.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -108,6 +108,7 @@ pub(crate) struct SplashOrderDto {
}
impl SplashOrderDto {
/// Creates a new `SplashOrderDto` value.
#[must_use]
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 };

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/tauri.rs
// version: 15
// version: 16
//! 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> {
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 {
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 {
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]
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]
@@ -91,7 +91,7 @@ fn get_config_document_detail(
file_id: String,
state: tauri::State<'_, crate::AppState>,
) -> 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]
@@ -100,14 +100,14 @@ fn save_config_document_source(
source: String,
state: tauri::State<'_, crate::AppState>,
) -> 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]
fn get_config_profile_documents(
state: tauri::State<'_, crate::AppState>,
) -> 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 {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
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>,
state: tauri::State<'_, crate::AppState>,
) -> 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 {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
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(
state: tauri::State<'_, crate::AppState>,
) -> 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 {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -144,7 +144,7 @@ fn set_environment_value(
value: String,
state: tauri::State<'_, crate::AppState>,
) -> 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 {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
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,
state: tauri::State<'_, crate::AppState>,
) -> 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 {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
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,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::SecretRevealResponseDto, crate::CommandErrorDto> {
let result = crate::secrets::reveal(&state, request);
let result = crate::reveal_secret(&state, request);
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
@@ -177,7 +177,7 @@ fn reveal_environment_value(
#[tauri::command]
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 {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
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]
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 {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
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,
state: tauri::State<'_, crate::AppState>,
) -> 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 {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
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,
state: tauri::State<'_, crate::AppState>,
) -> 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 {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
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,
state: tauri::State<'_, crate::AppState>,
) -> 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 {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
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
// version: 2
// version: 3
//! Tauri-window helpers for the Config Desk main window.
use tauri::Manager; // rust-rules: trait-import
/// Crate-internal `WINDOW_LABEL_MAIN` constant.
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);
return match window {
std::option::Option::Some(value) => std::result::Result::Ok(value),
@@ -18,6 +20,7 @@ pub(crate) fn require_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib
};
}
/// Shows main window.
pub(crate) fn show_main_window(app: &tauri::AppHandle) -> ksp_core_lib::Result<()> {
let window = require_window(app);
let window = match window {

View File

@@ -1,15 +1,17 @@
// file: crates/ksp-app-config-desk/src/tw_splash.rs
// version: 3
// version: 4
//! Tauri-window lifecycle for the Config Desk splash window.
use tauri::Emitter; // 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";
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);
return match window {
std::option::Option::Some(value) => std::result::Result::Ok(value),
@@ -20,6 +22,7 @@ pub(crate) fn require_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib
};
}
/// 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,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/unit_tests/environment.rs
// version: 2
// version: 3
#[test]
fn namespace_projection_distinguishes_ksp_and_kspb_sensitivity_families() {
@@ -53,7 +53,7 @@ fn committed_environment_report_is_safe_and_deterministic_when_present() {
let management = committed_management();
assert!(management.is_ok(), "committed management should construct: {management:?}");
if let std::result::Result::Ok(management) = management {
let report = super::report_from_management(&management);
let report = crate::environment_report_from_management(&management);
assert!(report.is_ok(), "environment report should project safely: {report:?}");
if let std::result::Result::Ok(report) = report {
let mut previous = std::option::Option::<&str>::None;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/unit_tests/logging_editor.rs
// version: 5
// version: 6
#[test]
fn committed_logging_document_maps_complete_read_only_editor_contract() {
@@ -8,7 +8,7 @@ fn committed_logging_document_maps_complete_read_only_editor_contract() {
if let std::result::Result::Ok(management) = management {
let source = management.load_logging_document();
assert!(source.is_ok(), "typed Logging source should load: {source:?}");
let document = super::document_from_management(&management);
let document = crate::logging_document_from_management(&management);
assert!(document.is_ok(), "Logging editor document should load: {document:?}");
if let (std::result::Result::Ok(source), std::result::Result::Ok(document)) = (source, document) {
assert_eq!(document.file_id, ksp_config_lib::FILE_ID_STD_LOGGING);
@@ -63,7 +63,7 @@ fn editor_candidate_round_trips_through_typed_config_contract() {
let management = committed_management();
assert!(management.is_ok(), "committed management should construct: {management:?}");
if let std::result::Result::Ok(management) = management {
let document = super::document_from_management(&management);
let document = crate::logging_document_from_management(&management);
assert!(document.is_ok(), "Logging editor document should load: {document:?}");
if let std::result::Result::Ok(document) = document {
let candidate = crate::LoggingDocumentCandidateDto {

View File

@@ -1,12 +1,12 @@
// file: crates/ksp-app-config-desk/unit_tests/profiles.rs
// version: 3
// version: 4
#[test]
fn profile_inventory_exposes_registered_standard_profile_documents() {
let management = fixture_management();
assert!(management.is_ok(), "fixture management should construct: {management:?}");
if let std::result::Result::Ok(management) = management {
let inventory = super::inventory_from_management(&management);
let inventory = crate::profile_inventory_from_management(&management);
assert!(inventory.is_ok(), "profile inventory should resolve: {inventory:?}");
if let std::result::Result::Ok(inventory) = inventory {
assert!(inventory.iter().any(|document| -> bool {
@@ -32,7 +32,7 @@ fn default_profile_detail_uses_safe_effective_value_and_dotenv_provenance() {
if let std::result::Result::Ok(management) = management {
let source = management.load_logging_document();
assert!(source.is_ok(), "typed Logging source should load: {source:?}");
let detail = super::detail_from_management(&management, ksp_config_lib::FILE_ID_STD_LOGGING, std::option::Option::None);
let detail = crate::profile_detail_from_management(&management, ksp_config_lib::FILE_ID_STD_LOGGING, std::option::Option::None);
assert!(detail.is_ok(), "default profile should inspect safely: {detail:?}");
if let (std::result::Result::Ok(source), std::result::Result::Ok(detail)) = (source, detail) {
assert_eq!(detail.selected_profile, source.default_profile());
@@ -60,7 +60,8 @@ fn explicit_profile_inspection_reports_explicit_selection_source() {
assert!(source.is_ok(), "typed Logging source should load: {source:?}");
if let std::result::Result::Ok(source) = source {
let profile_id = source.default_profile().to_owned();
let detail = super::detail_from_management(&management, ksp_config_lib::FILE_ID_STD_LOGGING, std::option::Option::Some(profile_id.as_str()));
let detail =
crate::profile_detail_from_management(&management, ksp_config_lib::FILE_ID_STD_LOGGING, std::option::Option::Some(profile_id.as_str()));
assert!(detail.is_ok(), "explicit profile should inspect: {detail:?}");
if let std::result::Result::Ok(detail) = detail {
assert_eq!(detail.selected_profile, profile_id);

View File

@@ -1,14 +1,14 @@
// 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.
pub const ARG_CFG_PATH: &str = "--cfgpath";
/// Bootstrap argument used to replace the schema root.
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.
#[derive(Clone, Debug, Eq, PartialEq)]

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/composite.rs
// version: 1
// version: 2
/// One resolved document component selected by a composite profile.
#[derive(Clone, Debug, PartialEq)]
@@ -118,6 +118,7 @@ struct CompositeDocumentReferenceSource {
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<()> {
if !document.file_id().as_str().starts_with("cfg.composite.") {
return std::result::Result::Ok(());
@@ -198,12 +199,8 @@ fn resolve_composite_document(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let resolved = crate::profile::load_resolved_profile_with_source(
engine,
&file_id,
reference.profile_id.as_deref(),
crate::ConfigProfileSelectionSource::Composite,
);
let resolved =
crate::load_resolved_profile_with_source(engine, &file_id, reference.profile_id.as_deref(), crate::ConfigProfileSelectionSource::Composite);
let resolved = match resolved {
std::result::Result::Ok(value) => value,
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",
));
}
let resolved =
crate::profile::load_resolved_profile_with_source(engine, &file_id, reference.profile_id.as_deref(), crate::ConfigProfileSelectionSource::Composite);
let resolved = crate::load_resolved_profile_with_source(engine, &file_id, reference.profile_id.as_deref(), crate::ConfigProfileSelectionSource::Composite);
if let std::result::Result::Err(error) = resolved {
return std::result::Result::Err(error);
}

View File

@@ -1,5 +1,5 @@
// 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.
#[derive(Clone, Debug, PartialEq)]
@@ -85,6 +85,7 @@ impl ConfigDocumentEngine {
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> {
let path = self.registry.resolve_path(&self.bootstrap, file_id);
let path = match path {
@@ -99,6 +100,7 @@ impl ConfigDocumentEngine {
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> {
let descriptor = self.registry.descriptor(file_id);
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<()> {
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 {
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 {
return std::result::Result::Err(error);
}

View File

@@ -1,12 +1,10 @@
// file: crates/ksp-config-lib/src/environment.rs
// version: 6
/// Default local environment file read by Config from the process launch directory.
pub const DEFAULT_DOTENV_PATH: &str = ".env";
// version: 7
/// Versioned environment contract template expected at the repository/runtime root.
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";
/// Source that supplied one resolved Config environment variable.
@@ -230,6 +228,7 @@ impl ConfigEnvironment {
};
}
/// Loads from dotenv path.
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 = match process {
@@ -244,20 +243,24 @@ impl ConfigEnvironment {
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> {
return &self.process;
}
/// Returns the current dotenv values.
pub(crate) const fn dotenv_values(&self) -> &std::collections::BTreeMap<String, String> {
return &self.dotenv;
}
/// Builds `ConfigEnvironment` from maps.
#[cfg(test)]
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) };
}
}
/// Executes the crate-internal parse dotenv content operation for the owning module.
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();
for (line_index, raw_line) in content.lines().enumerate() {
@@ -305,6 +308,7 @@ pub(crate) fn parse_dotenv_content(path: &std::path::Path, content: &str) -> ksp
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"));

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/lib.rs
// version: 13
// version: 14
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -171,6 +171,21 @@ pub use self::sensitivity::ResolvedConfigText;
/// Effective standard HTTP Transport configuration mapped to `ksp_onchain_transport_lib::HttpTransportSettings`.
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
// version: 5
// version: 6
/// Raw source of one registered Config document read for explicit management/correction.
#[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
/// 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>> {
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 {
return std::result::Result::Err(error);
}
@@ -744,7 +744,7 @@ impl ConfigManagement {
/// 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>> {
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 {
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.
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 {
return std::result::Result::Err(error);
}
@@ -781,7 +781,7 @@ impl ConfigManagement {
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
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 {
std::result::Result::Ok(()) => {},
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.
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 {
return std::result::Result::Err(error);
}
@@ -820,7 +820,7 @@ impl ConfigManagement {
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
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 {
std::result::Result::Ok(()) => {},
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());
}
/// Executes the crate-internal with dotenv path operation for `ConfigManagement`.
#[cfg(test)]
pub(crate) fn with_dotenv_path(engine: crate::ConfigDocumentEngine, dotenv_path: std::path::PathBuf) -> Self {
return Self { engine, dotenv_path };
@@ -1030,7 +1031,7 @@ fn persist_document_source(file_id: &crate::ConfigFileId, path: &std::path::Path
if existing.as_slice() == source {
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 {
return std::result::Result::Err(error);
}

View File

@@ -1,12 +1,14 @@
// 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);
/// 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<()> {
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<()> {
return atomic_write_with_policy(path, content, true);
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/profile.rs
// version: 4
// version: 5
/// Origin of one top-level value in a resolved standard Config profile.
#[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(
engine: &crate::ConfigDocumentEngine,
file_id: &crate::ConfigFileId,
@@ -153,6 +154,7 @@ pub(crate) fn load_resolved_profile_with_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<()> {
let root = match document.value().as_object() {
std::option::Option::Some(value) => value,

View File

@@ -1,18 +1,10 @@
// file: crates/ksp-config-lib/src/registry.rs
// version: 6
// version: 7
/// Bootstrap argument used to replace a known Config filename mapping.
pub const ARG_FILE_MAP: &str = "--filemap";
/// 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 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 generic composite JSON Schema document.
pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json";
/// Default physical filename for the standard Logging configuration document.
pub const DEFAULT_STD_LOGGING_FILENAME: &str = "std.logging.json";
/// 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";
/// Default physical filename for the standard HTTP Transport JSON Schema document.
pub const DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME: &str = "std.transport.schema.json";
/// Default physical filename for the generic composite JSON Schema document.
pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json";
/// Logical file identifier for the generic composite JSON Schema document.
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.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
@@ -89,6 +89,7 @@ impl ConfigFileDescriptor {
return self.schema_file_id.as_ref();
}
/// Creates a new `ConfigFileDescriptor` value.
pub(crate) fn new(
file_id: &'static str,
kind: ConfigFileKind,
@@ -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> {
let mut registry = ConfigFileRegistry { descriptors: std::collections::BTreeMap::new() };
for descriptor in descriptors {

View File

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

View File

@@ -1,8 +1,8 @@
// 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_FILE_ID: &str = "cfg.composite.test";
#[test]
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::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<()> {

View File

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

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-core-lib/src/program_ids.rs
// version: 2
// version: 3
const DOMAIN_SOLANA: &str = "solana";
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] = &[
crate::ProgramIdEntry::new(
"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.
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| {

View File

@@ -1,13 +1,15 @@
// file: crates/ksp-logging-lib/src/domain.rs
// version: 2
// version: 3
std::thread_local! {
static CURRENT_DOMAIN: std::cell::RefCell<std::option::Option<std::string::String>> = const { std::cell::RefCell::new(std::option::Option::None) };
}
/// Crate-internal `DomainContextLayer` state shared across the owning crate.
pub(crate) struct DomainContextLayer;
impl DomainContextLayer {
/// Creates a new `DomainContextLayer` value.
pub(crate) const fn new() -> Self {
return Self;
}
@@ -99,6 +101,7 @@ impl tracing::field::Visit for DomainVisitor {
}
}
/// Executes the crate-internal current domain matches operation for the owning module.
pub(crate) fn current_domain_matches(selectors: &[std::string::String]) -> bool {
if let [selector] = selectors
&& selector == "*"

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/src/identity.rs
// version: 1
// version: 2
//! Stable runtime identity used to separate persistent file outputs between application launches.
@@ -41,6 +41,7 @@ impl LoggingRuntimeIdentity {
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 {
return format!("{}.{}.{}", self.application_id, self.launch_timestamp, configured_prefix);
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/src/lib.rs
// version: 9
// version: 10
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -70,10 +70,17 @@ pub use self::span::Span;
/// Instruments an asynchronous future with a KSP span.
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)]
/// Internal macro bridge. KSP consumers must not use this reexport directly.
pub extern crate tracing as __private_tracing;
pub extern crate tracing;

View File

@@ -1,11 +1,11 @@
// file: crates/ksp-logging-lib/src/macros.rs
// version: 1
// version: 2
/// Emits a KSP error event with an explicit owning target.
#[macro_export]
macro_rules! error {
(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_rules! warn {
(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_rules! info {
(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_rules! debug {
(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_rules! trace {
(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_rules! error_span {
(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)+) => {{
$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_rules! warn_span {
(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)+) => {{
$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_rules! info_span {
(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)+) => {{
$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_rules! debug_span {
(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)+) => {{
$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_rules! trace_span {
(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)+) => {{
$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
// version: 14
// version: 15
use tracing_subscriber::Layer; // rust-rules: trait-import
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
@@ -315,7 +315,7 @@ fn prepare_runtime_with_identity(
if output_layers.is_empty() {
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();
return std::result::Result::Ok(PreparedRuntime { layers: vec![takeover_layer], outputs });
}
@@ -413,7 +413,7 @@ fn build_format_layer(
let span_events = map_span_events(span_events);
return match format {
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_sanitization(ansi_sanitization)
.with_target(true)
@@ -423,7 +423,7 @@ fn build_format_layer(
.boxed(),
crate::LogFormat::Compact => tracing_subscriber::fmt::layer()
.compact()
.with_writer(crate::writer::RouteMakeWriter::new(writer, filter.clone()))
.with_writer(crate::RouteMakeWriter::new(writer, filter.clone()))
.with_ansi(ansi)
.with_ansi_sanitization(ansi_sanitization)
.with_target(true)
@@ -433,7 +433,7 @@ fn build_format_layer(
.boxed(),
crate::LogFormat::Pretty => tracing_subscriber::fmt::layer()
.pretty()
.with_writer(crate::writer::RouteMakeWriter::new(writer, filter.clone()))
.with_writer(crate::RouteMakeWriter::new(writer, filter.clone()))
.with_ansi(ansi)
.with_ansi_sanitization(ansi_sanitization)
.with_target(true)
@@ -443,7 +443,7 @@ fn build_format_layer(
.boxed(),
crate::LogFormat::Json => tracing_subscriber::fmt::layer()
.json()
.with_writer(crate::writer::RouteMakeWriter::new(writer, filter.clone()))
.with_writer(crate::RouteMakeWriter::new(writer, filter.clone()))
.with_ansi(false)
.with_target(true)
.with_file(true)
@@ -473,7 +473,6 @@ const fn map_filter_level(level: crate::LogFilterLevel) -> tracing_subscriber::f
crate::LogFilterLevel::Trace => tracing_subscriber::filter::LevelFilter::TRACE,
};
}
const fn map_file_rotation(rotation: crate::FileRotation) -> tracing_appender::rolling::Rotation {
return match rotation {
crate::FileRotation::Never => tracing_appender::rolling::Rotation::NEVER,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/src/writer.rs
// version: 4
// version: 5
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum StripAnsiState {
@@ -12,12 +12,14 @@ enum StripAnsiState {
StringEscape,
}
/// Crate-internal `StripAnsiWriter` state shared across the owning crate.
pub(crate) struct StripAnsiWriter<W> {
inner: W,
state: StripAnsiState,
}
impl<W> StripAnsiWriter<W> {
/// Creates a new `StripAnsiWriter` value.
pub(crate) const fn new(inner: W) -> Self {
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)]
pub(crate) struct RouteMakeWriter<W> {
inner: W,
@@ -107,11 +110,13 @@ pub(crate) struct RouteMakeWriter<W> {
}
impl<W> RouteMakeWriter<W> {
/// Creates a new `RouteMakeWriter` value.
pub(crate) fn new(inner: W, filter: crate::OutputFilter) -> Self {
return Self { inner, filter };
}
}
/// Crate-internal `RoutedWriter` variants used by the owning crate.
pub(crate) enum RoutedWriter<W> {
Enabled(W),
Disabled,

View File

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

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/tests/ownership.rs
// version: 2
// version: 3
//! 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,
};
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!(
!source_uses_direct_path(source.as_str(), "tracing_subscriber::"),
"{} bypasses ksp-logging-lib via tracing-subscriber",

View File

@@ -1,12 +1,12 @@
// 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.
const EXTERNAL_TARGET: &str = "sqlx";
const JSON_KSP_TARGET: &str = "ksp-logging-json-test";
const LOGGING_TARGET: &str = "ksp-logging-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 {
return tracing::enabled!(target: LOGGING_TARGET, tracing::Level::TRACE);

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/client.rs
// version: 6
// version: 7
/// Passive runtime availability reported for one logical HTTP endpoint or role.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -177,8 +177,9 @@ impl HttpEndpointClient {
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> {
let validation = crate::settings::validate_endpoint_settings(&settings);
let validation = crate::validate_endpoint_settings(&settings);
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error);
}
@@ -239,6 +240,7 @@ impl HttpEndpointClient {
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> {
if timeout.is_zero() {
return std::result::Result::Err(
@@ -301,6 +303,7 @@ impl HttpEndpointClient {
};
}
/// Executes the crate-internal matching role operation for `HttpEndpointClient`.
pub(crate) fn matching_role<'a>(
&'a self,
role: &crate::HttpRoleName,
@@ -322,6 +325,7 @@ impl HttpEndpointClient {
return std::option::Option::None;
}
/// Executes the crate-internal matching role runtime operation for `HttpEndpointClient`.
pub(crate) fn matching_role_runtime(
&self,
role: &crate::HttpRoleName,
@@ -352,6 +356,7 @@ impl HttpEndpointClient {
return std::option::Option::None;
}
/// Returns the current availability.
pub(crate) fn availability(&self) -> crate::HttpEndpointAvailability {
if !self.enabled() {
return crate::HttpEndpointAvailability::Disabled;
@@ -393,6 +398,7 @@ 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>,
@@ -400,14 +406,17 @@ pub(crate) struct HttpEndpointHttpResponse {
}
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();
}

View File

@@ -1,12 +1,12 @@
// 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_SERVICE_UNAVAILABLE: u16 = 503;
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 {
/// Executes one audited standard Solana HTTP JSON-RPC method through KSP routing, admission and bounded retry policy.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/lib.rs
// version: 20
// version: 21
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -193,10 +193,6 @@ pub use self::rpc_common::SolanaContextConfig;
pub use self::rpc_common::SolanaRpcContext;
/// Generic contextual result returned by typed Solana HTTP RPC adapters.
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`.
pub use self::rpc_economics::SolanaInflationGovernor;
/// Current inflation-rate values returned by `getInflationRate`.
@@ -296,7 +292,17 @@ pub use self::settings::HttpRoleName;
/// Complete runtime settings consumed by the Solana HTTP transport foundation.
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
// version: 6
// version: 7
/// Safe snapshot of the logical HTTP endpoint pool.
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -207,6 +207,7 @@ impl HttpTransportPool {
return &self.inner.retry;
}
/// Executes the crate-internal next request id operation for `HttpTransportPool`.
pub(crate) fn next_request_id(&self) -> u64 {
let id = self.inner.request_ids.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if id == 0 {
@@ -426,6 +427,7 @@ impl HttpTransportPool {
return std::time::Instant::now() < deadline;
}
/// Executes the crate-internal common request timeout operation for `HttpTransportPool`.
pub(crate) fn common_request_timeout(
&self,
role: &crate::HttpRoleName,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/resilience.rs
// version: 2
// version: 3
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);
@@ -71,6 +71,7 @@ impl HttpRetryDecision {
}
}
/// Crate-internal `HttpRoleRuntime` state shared across the owning crate.
pub(crate) struct HttpRoleRuntime {
limits: crate::HttpRoleLimits,
bucket: std::sync::Mutex<std::option::Option<HttpTokenBucketState>>,
@@ -84,6 +85,7 @@ pub(crate) struct HttpRoleRuntime {
}
impl HttpRoleRuntime {
/// Creates a new `HttpRoleRuntime` value.
pub(crate) fn new(settings: &crate::HttpEndpointRoleSettings, notify: std::sync::Arc<tokio::sync::Notify>) -> Self {
let bucket = match settings.limits().requests_per_second() {
std::option::Option::Some(requests_per_second) => {
@@ -114,6 +116,7 @@ impl HttpRoleRuntime {
};
}
/// Returns the current availability.
pub(crate) fn availability(&self, now: std::time::Instant) -> crate::HttpEndpointAvailability {
if self.cooldown_remaining_at(now).is_some() {
return crate::HttpEndpointAvailability::RateLimited;
@@ -124,14 +127,17 @@ impl HttpRoleRuntime {
return crate::HttpEndpointAvailability::Available;
}
/// Returns the current cooldown remaining.
pub(crate) fn cooldown_remaining(&self) -> std::option::Option<std::time::Duration> {
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> {
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> {
let semaphore = match &self.semaphore {
std::option::Option::Some(value) => value,
@@ -149,18 +155,22 @@ impl HttpRoleRuntime {
return std::option::Option::Some(maximum.saturating_sub(available_u32));
}
/// Returns the current success count.
pub(crate) fn success_count(&self) -> u64 {
return self.success_count.load(std::sync::atomic::Ordering::Relaxed);
}
/// Returns the current failure count.
pub(crate) fn failure_count(&self) -> u64 {
return self.failure_count.load(std::sync::atomic::Ordering::Relaxed);
}
/// Returns the current rate limit count.
pub(crate) fn rate_limit_count(&self) -> u64 {
return self.rate_limit_count.load(std::sync::atomic::Ordering::Relaxed);
}
/// 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) {
let ready_at = match now.checked_add(remaining) {
@@ -189,6 +199,7 @@ impl HttpRoleRuntime {
return crate::RoleAdmissionAttempt::Ready(HttpConcurrencyPermit { semaphore_permit, notify: std::sync::Arc::clone(&self.notify) });
}
/// Records success.
pub(crate) fn record_success(&self) {
self.success_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
self.degraded.store(false, std::sync::atomic::Ordering::Relaxed);
@@ -196,6 +207,7 @@ impl HttpRoleRuntime {
return;
}
/// Records failure.
pub(crate) fn record_failure(&self) {
self.failure_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
self.degraded.store(true, std::sync::atomic::Ordering::Relaxed);
@@ -203,6 +215,7 @@ impl HttpRoleRuntime {
return;
}
/// Records rate limited.
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.rate_limit_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
@@ -269,6 +282,7 @@ impl HttpRoleRuntime {
}
}
/// Crate-internal `RoleAdmissionAttempt` variants used by the owning crate.
pub(crate) enum RoleAdmissionAttempt {
Ready(HttpConcurrencyPermit),
BlockedUntil(std::time::Instant),
@@ -276,6 +290,7 @@ pub(crate) enum RoleAdmissionAttempt {
Unavailable,
}
/// Crate-internal `HttpConcurrencyPermit` state shared across the owning crate.
pub(crate) struct HttpConcurrencyPermit {
semaphore_permit: std::option::Option<tokio::sync::OwnedSemaphorePermit>,
notify: std::sync::Arc<tokio::sync::Notify>,

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_accounts.rs
// version: 5
// version: 6
const MAX_MEMCMP_BYTES: usize = 128;
const MAX_MULTIPLE_ACCOUNTS: usize = 100;
const MAX_PROGRAM_ACCOUNT_FILTERS: usize = 4;
const MAX_MEMCMP_BYTES: usize = 128;
/// Account-data encoding accepted by Solana HTTP account methods.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -120,6 +120,7 @@ impl SolanaAccountInfoConfig {
return self.context.min_context_slot();
}
/// Returns whether empty.
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();
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_cluster.rs
// version: 5
// version: 6
const MAX_GET_SLOT_LEADERS: u64 = 5_000;
@@ -297,9 +297,11 @@ impl SolanaLeaderScheduleConfig {
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
return self.commitment;
}
/// Returns whether empty.
pub(crate) const fn is_empty(&self) -> bool {
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 {
let mut object = serde_json::Map::new();
if let std::option::Option::Some(identity) = self.identity.as_ref() {
@@ -420,6 +422,7 @@ impl SolanaVoteAccountsConfig {
pub const fn delinquent_slot_distance(&self) -> std::option::Option<u64> {
return self.delinquent_slot_distance;
}
/// Returns whether empty.
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();
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_method.rs
// version: 3
// version: 4
const CURRENT_HTTP_RPC_METHODS: [crate::HttpRpcMethodDescriptor; 52] = [
crate::HttpRpcMethodDescriptor::new(
@@ -1080,7 +1080,6 @@ impl HttpRpcMethodDescriptor {
pub const fn current_http_rpc_methods() -> &'static [crate::HttpRpcMethodDescriptor] {
return &CURRENT_HTTP_RPC_METHODS;
}
/// Returns the 14 historically documented deprecated HTTP RPC descriptors retained for compliance history.
#[must_use]
pub const fn historical_http_rpc_methods() -> &'static [crate::HttpRpcMethodDescriptor] {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_transactions.rs
// version: 9
// version: 10
const MAX_RECENT_PRIORITIZATION_FEE_ACCOUNTS: usize = 128;
const MAX_SIGNATURES_FOR_ADDRESS_LIMIT: usize = 1_000;
@@ -1389,7 +1389,6 @@ impl crate::HttpTransportPool {
);
}
}
let mut params = std::vec![serde_json::Value::String(transaction.to_owned())];
if let std::option::Option::Some(config) = config
&& !config.is_empty()

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/settings.rs
// version: 5
// version: 6
/// 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<()> {
return validate_endpoint(endpoint, 0);
}

View File

@@ -1,5 +1,5 @@
// 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.
@@ -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_stake_minimum_delegation = ksp_onchain_transport_lib::HttpTransportPool::get_stake_minimum_delegation;
let _get_supply = ksp_onchain_transport_lib::HttpTransportPool::get_supply;
let supply_config = ksp_onchain_transport_lib::SolanaSupplyConfig::new(
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Processed),
std::option::Option::Some(false),

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
// version: 21
// version: 22
//! 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",
"getStakeActivation",
];
let current = ksp_onchain_transport_lib::current_http_rpc_methods();
let historical = ksp_onchain_transport_lib::historical_http_rpc_methods();
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_4 = 0_usize;
let mut historical_current = 0_usize;
for descriptor in current {
actual_current.push(descriptor.method());
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.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::NotApplicable);
}
actual_current.sort_unstable();
actual_historical.sort_unstable();
let mut expected_current = expected_current;

View File

@@ -1,5 +1,5 @@
// 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.
@@ -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() {
let pool = devnet_pool().expect("programmatic Devnet Transport settings must construct a pool");
let role = ksp_onchain_transport_lib::HttpRoleName::new("default");
let account_config = ksp_onchain_transport_lib::SolanaAccountInfoConfig::new(
std::option::Option::Some(ksp_onchain_transport_lib::SolanaAccountEncoding::Base64),
std::option::Option::None,
@@ -58,7 +57,6 @@ async fn programmatic_devnet_transport_reaches_accounts_tokens_cluster_transacti
.expect("Devnet getAccountInfo smoke must succeed");
assert!(account.context().slot() > 0);
assert!(account.value().is_some());
// 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.
let token_owner = "A1TMhSGzQxMr1TboBKtgixKz1sS6REASMxPo1qsyTSJd"
@@ -79,7 +77,6 @@ async fn programmatic_devnet_transport_reaches_accounts_tokens_cluster_transacti
.await
.expect("Devnet getTokenAccountsByOwner smoke must succeed with the documented finalized/jsonParsed request shape");
assert!(token_accounts.context().slot() > 0);
let epoch = pool
.get_epoch_info(
&role,
@@ -91,10 +88,8 @@ async fn programmatic_devnet_transport_reaches_accounts_tokens_cluster_transacti
.await
.expect("Devnet getEpochInfo smoke must succeed");
assert!(epoch.absolute_slot() > 0);
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());
let transaction_context = ksp_onchain_transport_lib::SolanaContextConfig::new(
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized),
std::option::Option::None,
@@ -105,27 +100,22 @@ async fn programmatic_devnet_transport_reaches_accounts_tokens_cluster_transacti
.expect("Devnet getLatestBlockhash smoke must succeed");
assert!(!latest_blockhash.value().blockhash().is_empty());
assert!(latest_blockhash.value().last_valid_block_height() > 0);
let blockhash_valid = pool
.is_blockhash_valid(&role, latest_blockhash.value().blockhash(), std::option::Option::Some(&transaction_context))
.await
.expect("Devnet isBlockhashValid smoke must succeed");
assert_eq!(blockhash_valid.value(), &true);
let transaction_count = pool
.get_transaction_count(&role, std::option::Option::Some(&transaction_context))
.await
.expect("Devnet getTransactionCount smoke must succeed");
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");
assert!(block_height > 0);
let inflation_rate = pool.get_inflation_rate(&role).await.expect("Devnet getInflationRate smoke must succeed");
assert!(inflation_rate.total().is_finite());
assert!(inflation_rate.validator().is_finite());
assert!(inflation_rate.foundation().is_finite());
let stake_minimum_delegation = pool
.get_stake_minimum_delegation(&role, std::option::Option::Some(&transaction_context))
.await

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_accounts.rs
// version: 3
// version: 4
#[test]
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),
);
assert_eq!(config.to_json_value(), serde_json::json!({"commitment":"finalized","filter":"nonCirculating","sortResults":true}));
let keyed = crate::SolanaKeyedAccount::decode_wire(
"fixture",
serde_json::json!({
@@ -82,7 +81,6 @@ fn staged_largest_and_keyed_account_helpers_match_wire_shapes() {
.expect("keyed account must decode");
assert_eq!(keyed.pubkey().to_string(), "11111111111111111111111111111111");
assert_eq!(keyed.account().lamports(), 42);
let balance = crate::SolanaAccountBalance::decode_wire("fixture", serde_json::json!({"address":"11111111111111111111111111111111","lamports":99}))
.expect("account balance must decode");
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");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
assert_eq!(error.context()[1].value(), "5");
let oversized = crate::SolanaProgramAccountsConfig::new(
crate::SolanaAccountInfoConfig::default(),
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
// version: 5
// version: 6
#[test]
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));
let request = handle.join().expect("fixture server must join");
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 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");
@@ -356,7 +355,6 @@ async fn typed_get_blocks_allows_reversed_and_boundary_ranges_but_rejects_oversi
assert!(blocks.is_empty());
let request = handle.join().expect("fixture server must join");
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 pool = pool_for_url(url.as_str());
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]);
let request = handle.join().expect("fixture server must join");
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 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");
@@ -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.context()[1].key(), "commitment");
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 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);
@@ -404,7 +400,6 @@ async fn typed_get_blocks_with_limit_accepts_zero_and_maximum_and_rejects_above_
assert!(blocks.is_empty());
let request = handle.join().expect("fixture server must join");
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 (url, handle) = serve_once(include_str!("../fixtures/http/get_blocks_with_limit.success.json"));
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]);
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}]));
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 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);
let request = handle.join().expect("fixture server must join");
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([]));
let range = crate::SolanaBlockProductionRange::new(430_000_000, std::option::Option::None);
let config = crate::SolanaBlockProductionConfig::new(
std::option::Option::Some(crate::SolanaCommitment::Processed),

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs
// version: 4
// version: 5
#[test]
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");
assert_eq!(epoch.transaction_count(), std::option::Option::None);
let schedule = crate::SolanaEpochSchedule::decode_wire(
"getEpochSchedule",
serde_json::json!({"firstNormalEpoch":1,"firstNormalSlot":32,"leaderScheduleSlotOffset":32,"slotsPerEpoch":64,"warmup":false}),
)
.expect("epoch schedule must decode");
assert_eq!(schedule.slots_per_epoch(), 64);
let snapshot = crate::SolanaSnapshotSlotInfo::decode_wire("getHighestSnapshotSlot", serde_json::json!({"full":100,"incremental":null}))
.expect("snapshot info must decode");
assert_eq!(snapshot.full(), 100);
assert_eq!(snapshot.incremental(), std::option::Option::None);
let leader = crate::SolanaLeaderSchedule::decode_wire("getLeaderSchedule", serde_json::json!({"11111111111111111111111111111111":[0,2,4]}))
.expect("leader schedule must decode");
assert_eq!(leader.entries().len(), 1);
@@ -90,7 +87,6 @@ fn vote_status_and_config_helpers_preserve_wire_shapes() {
config.to_json_value(),
serde_json::json!({"commitment":"finalized","votePubkey":"11111111111111111111111111111111","keepUnstakedDelinquents":true,"delinquentSlotDistance":128})
);
let status = crate::SolanaVoteAccountStatus::decode_wire(
"getVoteAccounts",
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);
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}]));
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 = crate::SolanaContextConfig::default();

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_economics.rs
// version: 3
// version: 4
#[test]
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");
assert_eq!(fourth.commission(), std::option::Option::Some(3));
assert!(fourth.commission_bps().is_null());
let request = handle.join().expect("fixture server must join");
let body = request_body(request.as_str());
assert_eq!(body["method"], serde_json::json!("getInflationReward"));

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs
// version: 8
// version: 9
#[test]
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)]
field: crate::SolanaWireField<u64>,
}
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 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_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");
listener.set_nonblocking(true).expect("fixture listener must become nonblocking");
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(500);
return loop {
@@ -612,7 +610,6 @@ async fn typed_get_recent_prioritization_fees_omits_optional_accounts_and_reject
assert_eq!(fees.len(), 2);
let request = handle.join().expect("fixture server must join");
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 max_accounts = std::vec![account; 128];
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 body = transaction_request_body(request.as_str());
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 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;
@@ -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 body = transaction_request_body(request.as_str());
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 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;
@@ -804,7 +799,6 @@ async fn typed_get_transaction_modern_supports_omitted_and_explicit_empty_config
assert!(result.is_none());
let request = handle.join().expect("fixture server must join");
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 pool = transaction_pool_for_url(url.as_str());
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"}
])
);
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/request_airdrop.success.json"));
let pool = transaction_pool_for_url(url.as_str());
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(
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");
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"}]));
let empty = crate::SolanaSendTransactionConfig::default();
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/send_transaction.success.json"));
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;
assert_eq!(result.expect_err("requestAirdrop RPC error must propagate").code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR);
handle.join().expect("fixture server must join");
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 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");
assert_eq!(count, 1);
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 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;
@@ -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);
let (count, _) = handle.join().expect("fixture server must join");
assert_eq!(count, 1);
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 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);
let (count, _) = handle.join().expect("fixture server must join");
assert_eq!(count, 1);
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 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");
let request = handle.join().expect("secondary fixture server must join");
assert_eq!(transaction_request_body(request.as_str())["method"], serde_json::json!("requestAirdrop"));
let closed = unused_local_url();
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);
@@ -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");
assert_eq!(replacement.blockhash(), "ComputeBudget111111111111111111111111111111");
assert_eq!(replacement.last_valid_block_height(), 430_123_999);
let request = handle.join().expect("fixture server must join");
let body = transaction_request_body(request.as_str());
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");
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"}]));
let empty = crate::SolanaSimulateTransactionConfig::default();
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/simulate_transaction.partial.json"));
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;
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 rejected_encodings = [crate::SolanaAccountEncoding::Binary, crate::SolanaAccountEncoding::Base58];
for encoding in rejected_encodings {

View File

@@ -1,90 +1,89 @@
// file: crates/ksp-wallet-lib/src/constants.rs
// version: 5
// version: 6
//! Wallet-owned constants.
/// Exact magic string required by every native `.kspwallet` document.
pub const KSPWALLET_MAGIC: &str = "KSPWALLET";
/// 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;
/// Maximum accepted Solana CLI keypair JSON transfer size before parsing.
pub const KSPWALLET_TRANSFER_MAX_SOLANA_CLI_JSON_BYTES: usize = 1024;
/// Maximum accepted canonical Base58 keypair transfer size before decoding.
pub const KSPWALLET_TRANSFER_MAX_BASE58_BYTES: usize = 128;
/// Maximum protected alias size in UTF-8 bytes for metadata V1.
pub const KSPWALLET_V1_MAX_ALIAS_BYTES: usize = 256;
/// 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 metadata plaintext size before V1 encryption.
pub const KSPWALLET_V1_MAX_METADATA_PLAINTEXT_BYTES: usize = 64 * 1024;
/// Maximum password size in exact UTF-8 input bytes.
pub const KSPWALLET_V1_MAX_PASSWORD_BYTES: usize = 1024;
/// Byte length of every V1 key-slot identifier.
pub const KSPWALLET_V1_SLOT_ID_BYTES: usize = 16;
/// Byte length of every protected metadata note identifier.
pub const KSPWALLET_V1_NOTE_ID_BYTES: usize = 16;
/// 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;
/// Byte length of every Solana Ed25519 message signature returned by Wallet OWNER.
pub const KSPWALLET_SOLANA_SIGNATURE_BYTES: usize = 64;
/// Byte length of an XChaCha20-Poly1305 nonce.
pub const KSPWALLET_V1_XCHACHA_NONCE_BYTES: usize = 24;
/// 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 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 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;
/// Exact plaintext size of the V1 OWNER-control compartment.
pub const KSPWALLET_V1_OWNER_CONTROL_PLAINTEXT_BYTES: usize = 96;
/// Exact plaintext size of the V1 Solana secret compartment.
pub const KSPWALLET_V1_SECRET_PLAINTEXT_BYTES: usize = 64;
/// Minimum accepted Argon2 salt size in bytes.
pub const KSPWALLET_V1_MIN_KDF_SALT_BYTES: usize = 16;
/// Maximum accepted Argon2 salt size in bytes.
pub const KSPWALLET_V1_MAX_KDF_SALT_BYTES: usize = 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;
/// 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 OWNER-control ciphertext size accepted by envelope V1.
pub const KSPWALLET_V1_MAX_OWNER_CONTROL_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;
/// Initial protected payload version used independently by control, metadata and secret compartments.
pub const KSPWALLET_V1_INITIAL_PAYLOAD_VERSION: u32 = 1;
/// Domain separator for the OWNER state-signature transcript.
pub const KSPWALLET_V1_STATE_TRANSCRIPT_DOMAIN: &[u8] = b"KSPWALLET-V1-STATE";
/// 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 VIEW key-slot wrapping AAD.
pub const KSPWALLET_V1_VIEW_SLOT_AAD_DOMAIN: &[u8] = b"KSPWALLET-V1-AAD-VIEW-SLOT";
/// Domain separator for OWNER-control compartment AAD.
pub const KSPWALLET_V1_OWNER_CONTROL_AAD_DOMAIN: &[u8] = b"KSPWALLET-V1-AAD-OWNER-CONTROL";
/// 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

@@ -1,10 +1,10 @@
// file: crates/ksp-wallet-lib/src/crypto.rs
// version: 3
// version: 4
//! In-memory cryptographic primitives for native `.kspwallet` V1.
use chacha20poly1305::KeyInit; // rust-rules: derive-import
use chacha20poly1305::aead::Aead; // rust-rules: derive-import
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;
@@ -165,7 +165,6 @@ fn derive_argon2id(password: &[u8], salt: &[u8], memory_kib: u32, iterations: u3
{
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,

View File

@@ -1,37 +1,37 @@
// file: crates/ksp-wallet-lib/src/error.rs
// version: 4
// 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 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 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 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 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 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");
/// 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 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 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 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 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 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 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 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 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

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/lib.rs
// version: 10
// version: 11
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -245,10 +245,32 @@ pub(crate) use self::crypto::random_nonce;
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.
@@ -257,5 +279,11 @@ pub(crate) use self::transcript::slot_aad;
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

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/metadata.rs
// version: 3
// version: 4
/// Protected metadata requested when creating a new native Wallet.
///
@@ -17,6 +17,7 @@ impl WalletCreateMetadataV1 {
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);
}
@@ -40,6 +41,7 @@ pub struct WalletNote {
}
impl WalletNote {
/// Creates a new `WalletNote` value.
pub(crate) fn new(id: std::string::String, text: std::string::String) -> Self {
return Self { id, text };
}
@@ -74,6 +76,7 @@ pub struct WalletInfo {
}
impl WalletInfo {
/// Creates a new `WalletInfo` value.
pub(crate) fn new(
capability: crate::WalletCapability,
pubkey: ksp_core_lib::Pubkey,
@@ -135,6 +138,7 @@ pub struct LockedWalletInfo {
}
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 };
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/owner.rs
// version: 5
// version: 6
/// Authorized OWNER capability handle.
///
@@ -7,11 +7,12 @@
/// material remains encapsulated and is never exposed through a general-purpose getter.
pub struct WalletOwner {
info: crate::WalletInfo,
state: crate::wallet::OwnerStateV1,
state: crate::OwnerStateV1,
}
impl WalletOwner {
pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::wallet::OwnerStateV1) -> Self {
/// Builds `WalletOwner` from unlocked.
pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::OwnerStateV1) -> Self {
return Self { info, state };
}
@@ -307,7 +308,7 @@ async fn persist_staged(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::persistence::replace_wallet_file_v1(destination, expected_current.clone(), serialized).await;
return crate::replace_wallet_file_v1(destination, expected_current.clone(), serialized).await;
}
#[cfg(test)]

View File

@@ -1,11 +1,12 @@
// file: crates/ksp-wallet-lib/src/payload.rs
// version: 4
// version: 5
//! Plaintext payload codecs protected inside native `.kspwallet` V1 compartments.
use base64::Engine; // rust-rules: derive-import
use std::str::FromStr; // rust-rules: derive-import
use base64::Engine; // rust-rules: trait-import
use std::str::FromStr; // rust-rules: trait-import
/// Crate-internal `MetadataPayloadV1` state shared across the owning crate.
pub(crate) struct MetadataPayloadV1 {
pubkey: ksp_core_lib::Pubkey,
alias: std::option::Option<std::string::String>,
@@ -13,16 +14,19 @@ pub(crate) struct MetadataPayloadV1 {
}
impl crate::MetadataPayloadV1 {
/// Returns the current pubkey.
pub(crate) const fn pubkey(&self) -> &ksp_core_lib::Pubkey {
return &self.pubkey;
}
/// Builds `MetadataPayloadV1` from info.
pub(crate) fn from_info(info: &crate::WalletInfo) -> Self {
let alias = info.alias().map(std::string::String::from);
let notes = info.notes().to_vec();
return Self { pubkey: *info.pubkey(), alias, notes };
}
/// Updates alias.
pub(crate) fn set_alias(&mut self, alias: std::option::Option<std::string::String>) -> ksp_core_lib::Result<()> {
let validation_result = validate_alias(alias.as_deref());
if let std::result::Result::Err(error) = validation_result {
@@ -32,6 +36,7 @@ impl crate::MetadataPayloadV1 {
return std::result::Result::Ok(());
}
/// Adds note.
pub(crate) fn add_note(&mut self, text: std::string::String) -> ksp_core_lib::Result<std::string::String> {
if self.notes.len() >= crate::KSPWALLET_V1_MAX_NOTES {
return std::result::Result::Err(metadata_error("Wallet note count exceeds the V1 limit", "metadata.notes"));
@@ -40,7 +45,7 @@ impl crate::MetadataPayloadV1 {
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let note_id_bytes = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_NOTE_ID_BYTES }>() {
let note_id_bytes = match crate::random_bytes::<{ crate::KSPWALLET_V1_NOTE_ID_BYTES }>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -54,6 +59,7 @@ impl crate::MetadataPayloadV1 {
return std::result::Result::Ok(note_id);
}
/// Updates note.
pub(crate) fn update_note(&mut self, note_id: &str, text: std::string::String) -> ksp_core_lib::Result<()> {
let validation_result = validate_note_text(text.as_str());
if let std::result::Result::Err(error) = validation_result {
@@ -70,6 +76,7 @@ impl crate::MetadataPayloadV1 {
return std::result::Result::Err(note_not_found_error());
}
/// Deletes note.
pub(crate) fn delete_note(&mut self, note_id: &str) -> ksp_core_lib::Result<()> {
let mut index = 0_usize;
while index < self.notes.len() {
@@ -82,6 +89,7 @@ impl crate::MetadataPayloadV1 {
return std::result::Result::Err(note_not_found_error());
}
/// Executes the crate-internal encode operation for `MetadataPayloadV1`.
pub(crate) fn encode(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
let validation_result = validate_alias(self.alias.as_deref());
if let std::result::Result::Err(error) = validation_result {
@@ -110,23 +118,26 @@ impl crate::MetadataPayloadV1 {
return std::result::Result::Ok(serialized);
}
/// Consumes this value and returns info.
pub(crate) fn into_info(self, capability: crate::WalletCapability) -> crate::WalletInfo {
return crate::WalletInfo::new(capability, self.pubkey, self.alias, self.notes);
}
}
/// Crate-internal `OwnerControlMaterialV1` state shared across the owning crate.
pub(crate) struct OwnerControlMaterialV1 {
admin_signing_secret: [u8; crate::crypto::SECRET_KEY_BYTES],
admin_signing_secret: [u8; crate::SECRET_KEY_BYTES],
metadata_key: crate::SecretKeyV1,
secret_key: crate::SecretKeyV1,
}
impl OwnerControlMaterialV1 {
pub(crate) fn into_parts(mut self) -> ([u8; crate::crypto::SECRET_KEY_BYTES], crate::SecretKeyV1, crate::SecretKeyV1) {
let mut admin_signing_secret = [0_u8; crate::crypto::SECRET_KEY_BYTES];
/// Consumes this value and returns parts.
pub(crate) fn into_parts(mut self) -> ([u8; crate::SECRET_KEY_BYTES], crate::SecretKeyV1, crate::SecretKeyV1) {
let mut admin_signing_secret = [0_u8; crate::SECRET_KEY_BYTES];
std::mem::swap(&mut admin_signing_secret, &mut self.admin_signing_secret);
let metadata_key = std::mem::replace(&mut self.metadata_key, crate::SecretKeyV1::from_bytes([0_u8; crate::crypto::SECRET_KEY_BYTES]));
let secret_key = std::mem::replace(&mut self.secret_key, crate::SecretKeyV1::from_bytes([0_u8; crate::crypto::SECRET_KEY_BYTES]));
let metadata_key = std::mem::replace(&mut self.metadata_key, crate::SecretKeyV1::from_bytes([0_u8; crate::SECRET_KEY_BYTES]));
let secret_key = std::mem::replace(&mut self.secret_key, crate::SecretKeyV1::from_bytes([0_u8; crate::SECRET_KEY_BYTES]));
return (admin_signing_secret, metadata_key, secret_key);
}
}
@@ -152,6 +163,7 @@ struct RawMetadataNoteV1 {
text: std::string::String,
}
/// Encodes initial metadata payload.
pub(crate) fn encode_initial_metadata_payload(
pubkey: ksp_core_lib::Pubkey,
metadata: crate::WalletCreateMetadataV1,
@@ -164,7 +176,6 @@ pub(crate) fn encode_initial_metadata_payload(
if note_texts.len() > crate::KSPWALLET_V1_MAX_NOTES {
return std::result::Result::Err(metadata_error("Wallet note count exceeds the V1 limit", "metadata.notes"));
}
let mut notes = std::vec::Vec::with_capacity(note_texts.len());
let mut note_ids = std::collections::BTreeSet::new();
for text in note_texts {
@@ -172,7 +183,7 @@ pub(crate) fn encode_initial_metadata_payload(
if let std::result::Result::Err(error) = validation_result {
return std::result::Result::Err(error);
}
let note_id_bytes = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_NOTE_ID_BYTES }>() {
let note_id_bytes = match crate::random_bytes::<{ crate::KSPWALLET_V1_NOTE_ID_BYTES }>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -182,7 +193,6 @@ pub(crate) fn encode_initial_metadata_payload(
}
notes.push(crate::WalletNote::new(note_id, text));
}
let payload = crate::MetadataPayloadV1 { pubkey, alias, notes };
let serialized = match payload.encode() {
std::result::Result::Ok(value) => value,
@@ -191,6 +201,7 @@ pub(crate) fn encode_initial_metadata_payload(
return std::result::Result::Ok((serialized, payload));
}
/// Decodes metadata payload.
pub(crate) fn decode_metadata_payload(source: &[u8]) -> ksp_core_lib::Result<crate::MetadataPayloadV1> {
if source.len() > crate::KSPWALLET_V1_MAX_METADATA_PLAINTEXT_BYTES {
return std::result::Result::Err(metadata_error("Wallet metadata payload exceeds the V1 plaintext limit", "metadata"));
@@ -207,7 +218,6 @@ pub(crate) fn decode_metadata_payload(source: &[u8]) -> ksp_core_lib::Result<cra
if raw.notes.len() > crate::KSPWALLET_V1_MAX_NOTES {
return std::result::Result::Err(metadata_error("Wallet note count exceeds the V1 limit", "metadata.notes"));
}
let pubkey_result = ksp_core_lib::Pubkey::from_str(raw.pubkey.as_str());
let pubkey = match pubkey_result {
std::result::Result::Ok(value) => value,
@@ -216,7 +226,6 @@ pub(crate) fn decode_metadata_payload(source: &[u8]) -> ksp_core_lib::Result<cra
if pubkey.to_string() != raw.pubkey {
return std::result::Result::Err(metadata_error("Wallet metadata Pubkey is not canonical Base58", "metadata.pubkey"));
}
let mut notes = std::vec::Vec::with_capacity(raw.notes.len());
let mut note_ids = std::collections::BTreeSet::new();
for raw_note in raw.notes {
@@ -239,8 +248,9 @@ pub(crate) fn decode_metadata_payload(source: &[u8]) -> ksp_core_lib::Result<cra
return std::result::Result::Ok(crate::MetadataPayloadV1 { pubkey, alias: raw.alias, notes });
}
/// Encodes owner control payload.
pub(crate) fn encode_owner_control_payload(
admin_signing_secret: &[u8; crate::crypto::SECRET_KEY_BYTES],
admin_signing_secret: &[u8; crate::SECRET_KEY_BYTES],
metadata_key: &crate::SecretKeyV1,
secret_key: &crate::SecretKeyV1,
) -> [u8; crate::KSPWALLET_V1_OWNER_CONTROL_PLAINTEXT_BYTES] {
@@ -251,15 +261,16 @@ pub(crate) fn encode_owner_control_payload(
return output;
}
/// Decodes owner control payload.
pub(crate) fn decode_owner_control_payload(source: &[u8]) -> ksp_core_lib::Result<OwnerControlMaterialV1> {
if source.len() != crate::KSPWALLET_V1_OWNER_CONTROL_PLAINTEXT_BYTES {
return std::result::Result::Err(metadata_error("Wallet OWNER-control payload length is invalid", "owner_control"));
}
let mut admin_signing_secret = [0_u8; crate::crypto::SECRET_KEY_BYTES];
let mut admin_signing_secret = [0_u8; crate::SECRET_KEY_BYTES];
admin_signing_secret.copy_from_slice(&source[0..32]);
let mut metadata_key = [0_u8; crate::crypto::SECRET_KEY_BYTES];
let mut metadata_key = [0_u8; crate::SECRET_KEY_BYTES];
metadata_key.copy_from_slice(&source[32..64]);
let mut secret_key = [0_u8; crate::crypto::SECRET_KEY_BYTES];
let mut secret_key = [0_u8; crate::SECRET_KEY_BYTES];
secret_key.copy_from_slice(&source[64..96]);
return std::result::Result::Ok(OwnerControlMaterialV1 {
admin_signing_secret,

View File

@@ -1,10 +1,10 @@
// file: crates/ksp-wallet-lib/src/persistence.rs
// version: 6
// version: 7
//! Async-first native Wallet V1 filesystem persistence.
use std::io::Read; // rust-rules: derive-import
use std::io::Write; // rust-rules: derive-import
use std::io::Read; // rust-rules: trait-import
use std::io::Write; // rust-rules: trait-import
/// Creates a new native `.kspwallet` V1 at `destination` without overwriting an existing path.
///
@@ -83,10 +83,12 @@ pub async fn inspect_locked_wallet_file_v1(source: impl std::convert::AsRef<std:
return crate::inspect_locked_wallet_v1(bytes.as_slice());
}
/// Persists new wallet content v1.
pub(crate) async fn persist_new_wallet_content_v1(destination: std::path::PathBuf, content: std::vec::Vec<u8>) -> ksp_core_lib::Result<()> {
return persist_new_wallet_async(destination, content).await;
}
/// Replaces wallet file v1.
pub(crate) async fn replace_wallet_file_v1(
destination: std::path::PathBuf,
expected_current: crate::KspWalletEnvelopeV1,
@@ -131,7 +133,6 @@ fn read_wallet_file_blocking(source: &std::path::Path) -> ksp_core_lib::Result<s
if metadata.len() > crate::KSPWALLET_MAX_FILE_BYTES as u64 {
return std::result::Result::Err(oversized_document_error());
}
let capacity = std::cmp::min(metadata.len(), crate::KSPWALLET_MAX_FILE_BYTES as u64) as usize;
let mut bytes = std::vec::Vec::with_capacity(capacity);
let mut bounded = file.take((crate::KSPWALLET_MAX_FILE_BYTES + 1) as u64);
@@ -175,7 +176,7 @@ fn verify_expected_wallet_state(destination: &std::path::Path, expected_current:
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let verify_result = crate::wallet::verify_state_signature(&current);
let verify_result = crate::verify_state_signature(&current);
if let std::result::Result::Err(error) = verify_result {
return std::result::Result::Err(error);
}
@@ -207,7 +208,6 @@ where
if !existing_metadata.is_file() {
return std::result::Result::Err(atomic_error("replace_destination", "Wallet replacement destination is not a regular file"));
}
let temporary_result = tempfile::Builder::new().prefix(".kspwallet-replace-").suffix(".tmp").tempfile_in(parent);
let mut temporary = match temporary_result {
std::result::Result::Ok(value) => value,
@@ -225,7 +225,6 @@ where
if let std::result::Result::Err(error) = hook_result {
return std::result::Result::Err(error);
}
let persist_result = temporary.persist(destination);
let persisted = match persist_result {
std::result::Result::Ok(value) => value,
@@ -254,7 +253,6 @@ where
if destination.file_name().is_none() {
return std::result::Result::Err(atomic_error("destination", "Wallet destination has no file name"));
}
let temporary_result = tempfile::Builder::new().prefix(".kspwallet-write-").suffix(".tmp").tempfile_in(parent);
let mut temporary = match temporary_result {
std::result::Result::Ok(value) => value,
@@ -268,12 +266,10 @@ where
if let std::result::Result::Err(error) = sync_result {
return std::result::Result::Err(atomic_io_error("temporary_sync", error));
}
let hook_result = before_publish();
if let std::result::Result::Err(error) = hook_result {
return std::result::Result::Err(error);
}
let persist_result = temporary.persist_noclobber(destination);
let persisted = match persist_result {
std::result::Result::Ok(value) => value,
@@ -371,6 +367,7 @@ fn blocking_atomic_error(operation: &'static str, source: tokio::task::JoinError
.with_source(source);
}
/// Persists new wallet fault before publish.
#[cfg(test)]
pub(crate) fn persist_new_wallet_fault_before_publish(destination: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
return persist_new_wallet_with_hook(destination, content, || {
@@ -378,11 +375,13 @@ pub(crate) fn persist_new_wallet_fault_before_publish(destination: &std::path::P
});
}
/// Persists new wallet for test.
#[cfg(test)]
pub(crate) fn persist_new_wallet_for_test(destination: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
return persist_new_wallet_blocking(destination, content);
}
/// Replaces wallet fault before publish.
#[cfg(test)]
pub(crate) fn replace_wallet_fault_before_publish(destination: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
return replace_wallet_file_with_hook(destination, content, || {
@@ -390,6 +389,7 @@ pub(crate) fn replace_wallet_fault_before_publish(destination: &std::path::Path,
});
}
/// Replaces wallet for test.
#[cfg(test)]
pub(crate) fn replace_wallet_for_test(destination: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
return replace_wallet_file_blocking(destination, content);

View File

@@ -1,31 +1,31 @@
// file: crates/ksp-wallet-lib/src/transcript.rs
// version: 1
// version: 2
//! Deterministic `.kspwallet` V1 state-transcript and AEAD-AAD encoding.
const TAG_MAGIC: u16 = 0x0001;
const TAG_COMPARTMENT_ALGORITHM: u16 = 0x0202;
const TAG_COMPARTMENT_CIPHERTEXT: u16 = 0x0204;
const TAG_COMPARTMENT_KIND: u16 = 0x0200;
const TAG_COMPARTMENT_NONCE: u16 = 0x0203;
const TAG_COMPARTMENT_VERSION: u16 = 0x0201;
const TAG_FORMAT_VERSION: u16 = 0x0002;
const TAG_KDF_ALGORITHM: u16 = 0x0102;
const TAG_KDF_ITERATIONS: u16 = 0x0105;
const TAG_KDF_MEMORY_KIB: u16 = 0x0104;
const TAG_KDF_PARALLELISM: u16 = 0x0106;
const TAG_KDF_SALT: u16 = 0x0107;
const TAG_KDF_VERSION: u16 = 0x0103;
const TAG_MAGIC: u16 = 0x0001;
const TAG_OWNER_AUTH_PUBLIC_KEY: u16 = 0x0003;
const TAG_SLOT_ID: u16 = 0x0100;
const TAG_SLOT_ROLE: u16 = 0x0101;
const TAG_STATE_SIGNATURE_ALGORITHM: u16 = 0x0500;
const TAG_VIEW_ENABLED: u16 = 0x0010;
const TAG_VIEW_ROLE: u16 = 0x0011;
const TAG_VIEW_SLOT_ID: u16 = 0x0012;
const TAG_SLOT_ID: u16 = 0x0100;
const TAG_SLOT_ROLE: u16 = 0x0101;
const TAG_KDF_ALGORITHM: u16 = 0x0102;
const TAG_KDF_VERSION: u16 = 0x0103;
const TAG_KDF_MEMORY_KIB: u16 = 0x0104;
const TAG_KDF_ITERATIONS: u16 = 0x0105;
const TAG_KDF_PARALLELISM: u16 = 0x0106;
const TAG_KDF_SALT: u16 = 0x0107;
const TAG_WRAP_ALGORITHM: u16 = 0x0108;
const TAG_WRAP_NONCE: u16 = 0x0109;
const TAG_WRAP_CIPHERTEXT: u16 = 0x010A;
const TAG_COMPARTMENT_KIND: u16 = 0x0200;
const TAG_COMPARTMENT_VERSION: u16 = 0x0201;
const TAG_COMPARTMENT_ALGORITHM: u16 = 0x0202;
const TAG_COMPARTMENT_NONCE: u16 = 0x0203;
const TAG_COMPARTMENT_CIPHERTEXT: u16 = 0x0204;
const TAG_STATE_SIGNATURE_ALGORITHM: u16 = 0x0500;
const TAG_WRAP_NONCE: u16 = 0x0109;
/// Builds the normative OWNER state-signature transcript for one validated V1 envelope.
pub(crate) fn state_transcript(envelope: &crate::KspWalletEnvelopeV1) -> std::vec::Vec<u8> {
@@ -37,7 +37,6 @@ pub(crate) fn state_transcript(envelope: &crate::KspWalletEnvelopeV1) -> std::ve
std::option::Option::Some(slot_id) => push_bytes(&mut output, TAG_VIEW_SLOT_ID, slot_id),
std::option::Option::None => push_bytes(&mut output, TAG_VIEW_SLOT_ID, &[]),
}
push_slot(&mut output, envelope.owner_slot(), true);
push_compartment(&mut output, envelope.owner_control(), true);
push_compartment(&mut output, envelope.metadata(), true);

View File

@@ -1,13 +1,13 @@
// file: crates/ksp-wallet-lib/src/transfer.rs
// version: 2
// version: 3
//! Explicit OWNER-only Solana keypair import/export adapters.
use std::io::Read; // rust-rules: derive-import
use std::io::Write; // rust-rules: derive-import
use std::io::Read; // rust-rules: trait-import
use std::io::Write; // rust-rules: trait-import
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt; // rust-rules: derive-import
use zeroize::Zeroize; // rust-rules: derive-import
use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import
use zeroize::Zeroize; // rust-rules: trait-import
/// Explicit secret-transfer formats supported by Wallet `0.2.5`.
#[non_exhaustive]
@@ -161,6 +161,7 @@ pub async fn import_wallet_transfer_file_v1(
return import_wallet_transfer_v1(destination, bytes.as_slice(), format, owner_password, view_password, metadata).await;
}
/// Executes the crate-internal write wallet transfer file v1 operation for the owning module.
pub(crate) async fn write_wallet_transfer_file_v1(
destination: std::path::PathBuf,
content: std::vec::Vec<u8>,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/view.rs
// version: 3
// version: 4
/// Authorized VIEW capability handle.
///
@@ -7,11 +7,12 @@
/// Solana secret, OWNER administration material or metadata-write authority.
pub struct WalletView {
info: crate::WalletInfo,
state: crate::wallet::ViewStateV1,
state: crate::ViewStateV1,
}
impl WalletView {
pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::wallet::ViewStateV1) -> Self {
/// Builds `WalletView` from unlocked.
pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::ViewStateV1) -> Self {
return Self { info, state };
}
@@ -68,7 +69,7 @@ impl WalletView {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = crate::persistence::replace_wallet_file_v1(destination.as_ref().to_path_buf(), self.state.envelope().clone(), serialized).await;
let persist_result = crate::replace_wallet_file_v1(destination.as_ref().to_path_buf(), self.state.envelope().clone(), serialized).await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}

View File

@@ -1,11 +1,12 @@
// file: crates/ksp-wallet-lib/src/wallet.rs
// version: 7
// version: 8
//! In-memory native Wallet V1 create/open orchestration.
use ed25519_dalek::Signer; // rust-rules: derive-import
use zeroize::Zeroize; // rust-rules: derive-import
use ed25519_dalek::Signer; // rust-rules: trait-import
use zeroize::Zeroize; // rust-rules: trait-import
/// Crate-internal `OwnerStateV1` state shared across the owning crate.
pub(crate) struct OwnerStateV1 {
envelope: crate::KspWalletEnvelopeV1,
owner_root: std::option::Option<crate::SecretKeyV1>,
@@ -16,6 +17,7 @@ pub(crate) struct OwnerStateV1 {
}
impl OwnerStateV1 {
/// Creates a new `OwnerStateV1` value.
pub(crate) fn new(
envelope: crate::KspWalletEnvelopeV1,
owner_root: crate::SecretKeyV1,
@@ -34,15 +36,18 @@ impl OwnerStateV1 {
};
}
/// Returns the current envelope.
pub(crate) const fn envelope(&self) -> &crate::KspWalletEnvelopeV1 {
return &self.envelope;
}
/// Applies envelope.
pub(crate) fn apply_envelope(&mut self, envelope: crate::KspWalletEnvelopeV1) {
self.envelope = envelope;
return;
}
/// Applies strong view state.
pub(crate) fn apply_strong_view_state(&mut self, envelope: crate::KspWalletEnvelopeV1, metadata_key: crate::SecretKeyV1) {
self.envelope = envelope;
let previous = self.metadata_key.replace(metadata_key);
@@ -50,6 +55,7 @@ impl OwnerStateV1 {
return;
}
/// Executes the crate-internal export transfer operation for `OwnerStateV1`.
pub(crate) fn export_transfer(&self, format: crate::WalletTransferFormat) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
let keypair = match self.solana_keypair.as_ref() {
std::option::Option::Some(value) => value,
@@ -74,6 +80,7 @@ impl OwnerStateV1 {
};
}
/// Executes the crate-internal sign message operation for `OwnerStateV1`.
pub(crate) fn sign_message(&self, message: &[u8]) -> ksp_core_lib::Result<[u8; crate::KSPWALLET_SOLANA_SIGNATURE_BYTES]> {
let keypair = match self.solana_keypair.as_ref() {
std::option::Option::Some(value) => value,
@@ -89,6 +96,7 @@ impl OwnerStateV1 {
return std::result::Result::Ok(signature);
}
/// Executes the crate-internal stage metadata payload operation for `OwnerStateV1`.
pub(crate) fn stage_metadata_payload(&self, payload: crate::MetadataPayloadV1) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::WalletInfo)> {
let metadata_key = match self.metadata_key.as_ref() {
std::option::Option::Some(value) => value,
@@ -138,6 +146,7 @@ impl OwnerStateV1 {
return std::result::Result::Ok((envelope, payload.into_info(crate::WalletCapability::Owner)));
}
/// Executes the crate-internal stage owner password rotation operation for `OwnerStateV1`.
pub(crate) async fn stage_owner_password_rotation(&self, new_password: crate::OwnerPassword) -> ksp_core_lib::Result<crate::KspWalletEnvelopeV1> {
let owner_root = match self.owner_root.as_ref() {
std::option::Option::Some(value) => value,
@@ -172,6 +181,7 @@ impl OwnerStateV1 {
return verify_and_return(envelope);
}
/// Executes the crate-internal stage view password rotation operation for `OwnerStateV1`.
pub(crate) async fn stage_view_password_rotation(&self, new_password: crate::ViewPassword) -> ksp_core_lib::Result<crate::KspWalletEnvelopeV1> {
let metadata_key = match self.metadata_key.as_ref() {
std::option::Option::Some(value) => value,
@@ -201,6 +211,7 @@ impl OwnerStateV1 {
return verify_and_return(envelope);
}
/// Executes the crate-internal stage disable view operation for `OwnerStateV1`.
pub(crate) fn stage_disable_view(&self) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::SecretKeyV1)> {
if !self.envelope.view_descriptor().enabled() {
return std::result::Result::Err(capability_error("Wallet VIEW capability is already disabled"));
@@ -208,6 +219,7 @@ impl OwnerStateV1 {
return self.stage_strong_view_change(std::option::Option::None);
}
/// Executes the crate-internal stage recreate view operation for `OwnerStateV1`.
pub(crate) async fn stage_recreate_view(
&self,
new_password: crate::ViewPassword,
@@ -260,7 +272,6 @@ impl OwnerStateV1 {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(crypto_operation_error()),
};
let current_plaintext_result = crate::decrypt_bytes(
current_metadata_key,
self.envelope.metadata().nonce(),
@@ -290,9 +301,8 @@ impl OwnerStateV1 {
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let metadata = crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::Metadata, metadata_nonce, metadata_ciphertext);
let mut admin_secret = admin_signing_key.to_bytes();
let mut owner_control_plaintext = crate::payload::encode_owner_control_payload(&admin_secret, &new_metadata_key, secret_key);
let mut owner_control_plaintext = crate::encode_owner_control_payload(&admin_secret, &new_metadata_key, secret_key);
admin_secret.zeroize();
let owner_control_nonce = match crate::random_nonce() {
std::result::Result::Ok(value) => value,
@@ -314,7 +324,6 @@ impl OwnerStateV1 {
};
let owner_control =
crate::WalletEncryptedCompartmentV1::new(crate::WalletCompartmentKindV1::OwnerControl, owner_control_nonce, owner_control_ciphertext);
let view_descriptor = match view_slot.as_ref() {
std::option::Option::Some(slot) => crate::WalletViewDescriptorV1::enabled_for_slot(*slot.slot_id()),
std::option::Option::None => crate::WalletViewDescriptorV1::disabled(),
@@ -352,25 +361,30 @@ impl std::ops::Drop for OwnerStateV1 {
}
}
/// Crate-internal `ViewStateV1` state shared across the owning crate.
pub(crate) struct ViewStateV1 {
envelope: crate::KspWalletEnvelopeV1,
metadata_key: std::option::Option<crate::SecretKeyV1>,
}
impl ViewStateV1 {
/// Creates a new `ViewStateV1` value.
pub(crate) fn new(envelope: crate::KspWalletEnvelopeV1, metadata_key: crate::SecretKeyV1) -> Self {
return Self { envelope, metadata_key: std::option::Option::Some(metadata_key) };
}
/// Returns the current envelope.
pub(crate) const fn envelope(&self) -> &crate::KspWalletEnvelopeV1 {
return &self.envelope;
}
/// Applies envelope.
pub(crate) fn apply_envelope(&mut self, envelope: crate::KspWalletEnvelopeV1) {
self.envelope = envelope;
return;
}
/// Executes the crate-internal stage view password rotation operation for `ViewStateV1`.
pub(crate) async fn stage_view_password_rotation(&self, new_password: crate::ViewPassword) -> ksp_core_lib::Result<crate::KspWalletEnvelopeV1> {
let metadata_key = match self.metadata_key.as_ref() {
std::option::Option::Some(value) => value,
@@ -526,7 +540,7 @@ pub async fn open_wallet_owner_v1(source: &[u8], password: crate::OwnerPassword)
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(owner_unlock_error()),
};
let control_result = crate::payload::decode_owner_control_payload(owner_control_plaintext.as_slice());
let control_result = crate::decode_owner_control_payload(owner_control_plaintext.as_slice());
owner_control_plaintext.zeroize();
let control = match control_result {
std::result::Result::Ok(value) => value,
@@ -538,7 +552,6 @@ pub async fn open_wallet_owner_v1(source: &[u8], password: crate::OwnerPassword)
if admin_signing_key.verifying_key().to_bytes() != *envelope.owner_auth_public_key() {
return std::result::Result::Err(authentication_error());
}
let metadata_plaintext_result = crate::decrypt_bytes(
&metadata_key,
envelope.metadata().nonce(),
@@ -555,7 +568,6 @@ pub async fn open_wallet_owner_v1(source: &[u8], password: crate::OwnerPassword)
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let secret_plaintext_result = crate::decrypt_bytes(
&secret_key,
envelope.secret().nonce(),
@@ -590,7 +602,6 @@ pub async fn open_wallet_owner_v1(source: &[u8], password: crate::OwnerPassword)
if &secret_pubkey != metadata_payload.pubkey() {
return std::result::Result::Err(key_material_error());
}
let info = metadata_payload.into_info(crate::WalletCapability::Owner);
let state = OwnerStateV1::new(envelope, owner_root, metadata_key, secret_key, admin_signing_key, solana_keypair);
ksp_logging_lib::debug!(
@@ -616,6 +627,7 @@ pub fn inspect_locked_wallet_v1(source: &[u8]) -> ksp_core_lib::Result<crate::Lo
return std::result::Result::Ok(crate::LockedWalletInfo::new(envelope.view_descriptor().enabled()));
}
/// Executes the crate-internal create wallet v1 from keypair operation for the owning module.
pub(crate) async fn create_wallet_v1_from_keypair(
owner_password: crate::OwnerPassword,
view_password: std::option::Option<crate::ViewPassword>,
@@ -634,14 +646,12 @@ pub(crate) async fn create_wallet_v1_from_keypair(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut admin_secret = match crate::random_bytes::<{ crate::SECRET_KEY_BYTES }>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let admin_signing_key = ed25519_dalek::SigningKey::from_bytes(&admin_secret);
let owner_auth_public_key = admin_signing_key.verifying_key().to_bytes();
let mut solana_keypair_bytes = solana_keypair.to_bytes();
let pubkey_result = pubkey_from_keypair_bytes(&solana_keypair_bytes);
let pubkey = match pubkey_result {
@@ -652,8 +662,7 @@ pub(crate) async fn create_wallet_v1_from_keypair(
return std::result::Result::Err(error);
},
};
let metadata_encoded_result = crate::payload::encode_initial_metadata_payload(pubkey, metadata);
let metadata_encoded_result = crate::encode_initial_metadata_payload(pubkey, metadata);
let (mut metadata_plaintext, metadata_payload) = match metadata_encoded_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
@@ -662,9 +671,8 @@ pub(crate) async fn create_wallet_v1_from_keypair(
return std::result::Result::Err(error);
},
};
let mut owner_control_plaintext = crate::payload::encode_owner_control_payload(&admin_secret, &metadata_key, &secret_key);
let mut owner_control_plaintext = crate::encode_owner_control_payload(&admin_secret, &metadata_key, &secret_key);
admin_secret.zeroize();
let owner_slot_id = match crate::random_bytes::<{ crate::KSPWALLET_V1_SLOT_ID_BYTES }>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
@@ -684,7 +692,6 @@ pub(crate) async fn create_wallet_v1_from_keypair(
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
},
};
let view_material_result = prepare_view_creation(view_password);
let view_material = match view_material_result {
std::result::Result::Ok(value) => value,
@@ -692,7 +699,6 @@ pub(crate) async fn create_wallet_v1_from_keypair(
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
},
};
let owner_control_nonce = match crate::random_nonce() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
@@ -711,7 +717,6 @@ pub(crate) async fn create_wallet_v1_from_keypair(
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
},
};
let provisional = provisional_envelope(
owner_auth_public_key,
owner_slot_id,
@@ -722,7 +727,6 @@ pub(crate) async fn create_wallet_v1_from_keypair(
metadata_nonce,
secret_nonce,
);
let owner_derived_result = derive_owner_password_key_async(owner_password, owner_kdf.clone()).await;
let owner_derived = match owner_derived_result {
std::result::Result::Ok(value) => value,
@@ -737,7 +741,6 @@ pub(crate) async fn create_wallet_v1_from_keypair(
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
},
};
let view_slot_result = seal_view_slot(&provisional, view_material, &metadata_key).await;
let view_slot = match view_slot_result {
std::result::Result::Ok(value) => value,
@@ -745,7 +748,6 @@ pub(crate) async fn create_wallet_v1_from_keypair(
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
},
};
let owner_control_ciphertext_result = crate::encrypt_bytes(
&owner_root,
&owner_control_nonce,
@@ -759,7 +761,6 @@ pub(crate) async fn create_wallet_v1_from_keypair(
},
};
owner_control_plaintext.zeroize();
let metadata_ciphertext_result = crate::encrypt_bytes(
&metadata_key,
&metadata_nonce,
@@ -773,7 +774,6 @@ pub(crate) async fn create_wallet_v1_from_keypair(
},
};
metadata_plaintext.zeroize();
let secret_ciphertext_result = crate::encrypt_bytes(
&secret_key,
&secret_nonce,
@@ -787,7 +787,6 @@ pub(crate) async fn create_wallet_v1_from_keypair(
},
};
solana_keypair_bytes.zeroize();
let owner_slot =
crate::WalletKeySlotV1::new(owner_slot_id, crate::WalletKeySlotRoleV1::Owner, owner_kdf, crate::WalletKeyWrapV1::new(owner_wrap_nonce, owner_wrapped));
let view_descriptor = match view_slot.as_ref() {
@@ -822,7 +821,6 @@ pub(crate) async fn create_wallet_v1_from_keypair(
if let std::result::Result::Err(error) = verify_result {
return std::result::Result::Err(error);
}
let info = metadata_payload.into_info(crate::WalletCapability::Owner);
let state = OwnerStateV1::new(envelope, owner_root, metadata_key, secret_key, admin_signing_key, solana_keypair);
ksp_logging_lib::debug!(
@@ -835,6 +833,7 @@ pub(crate) async fn create_wallet_v1_from_keypair(
return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, state));
}
/// Verifies state signature.
pub(crate) fn verify_state_signature(envelope: &crate::KspWalletEnvelopeV1) -> ksp_core_lib::Result<()> {
let verifying_key_result = ed25519_dalek::VerifyingKey::from_bytes(envelope.owner_auth_public_key());
let verifying_key = match verifying_key_result {

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-wallet-lib/src/wire.rs
// version: 6
// version: 7
//! Strict native `.kspwallet` V1 wire envelope.
use base64::Engine; // rust-rules: derive-import
use base64::Engine; // rust-rules: trait-import
/// Password KDF supported by `.kspwallet` V1 key slots.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -105,6 +105,7 @@ pub struct WalletKdfParametersV1 {
}
impl WalletKdfParametersV1 {
/// Creates a new creation value for `WalletKdfParametersV1`.
pub(crate) fn new_creation(salt: std::vec::Vec<u8>) -> Self {
return Self {
algorithm: WalletKdfAlgorithmV1::Argon2id,
@@ -176,6 +177,7 @@ pub struct WalletKeyWrapV1 {
}
impl WalletKeyWrapV1 {
/// Creates a new `WalletKeyWrapV1` value.
pub(crate) fn new(nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES], ciphertext: std::vec::Vec<u8>) -> Self {
return Self { algorithm: WalletAeadAlgorithmV1::XChaCha20Poly1305, nonce, ciphertext };
}
@@ -220,6 +222,7 @@ pub struct WalletKeySlotV1 {
}
impl WalletKeySlotV1 {
/// Creates a new `WalletKeySlotV1` value.
pub(crate) fn new(slot_id: [u8; crate::KSPWALLET_V1_SLOT_ID_BYTES], role: WalletKeySlotRoleV1, kdf: WalletKdfParametersV1, wrap: WalletKeyWrapV1) -> Self {
return Self { slot_id, role, kdf, wrap };
}
@@ -269,10 +272,12 @@ pub struct WalletViewDescriptorV1 {
}
impl WalletViewDescriptorV1 {
/// Executes the crate-internal disabled operation for `WalletViewDescriptorV1`.
pub(crate) const fn disabled() -> Self {
return Self { enabled: false, slot_id: std::option::Option::None };
}
/// Executes the crate-internal enabled for slot operation for `WalletViewDescriptorV1`.
pub(crate) const fn enabled_for_slot(slot_id: [u8; crate::KSPWALLET_V1_SLOT_ID_BYTES]) -> Self {
return Self { enabled: true, slot_id: std::option::Option::Some(slot_id) };
}
@@ -301,6 +306,7 @@ pub struct WalletEncryptedCompartmentV1 {
}
impl WalletEncryptedCompartmentV1 {
/// Creates a new `WalletEncryptedCompartmentV1` value.
pub(crate) fn new(kind: WalletCompartmentKindV1, nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES], ciphertext: std::vec::Vec<u8>) -> Self {
return Self {
kind,
@@ -363,6 +369,7 @@ pub struct WalletStateSignatureV1 {
}
impl WalletStateSignatureV1 {
/// Creates a new `WalletStateSignatureV1` value.
pub(crate) const fn new(signature: [u8; crate::KSPWALLET_V1_ED25519_SIGNATURE_BYTES]) -> Self {
return Self { algorithm: WalletStateSignatureAlgorithmV1::Ed25519, signature };
}
@@ -404,6 +411,7 @@ pub struct KspWalletEnvelopeV1 {
}
impl KspWalletEnvelopeV1 {
/// Creates a new internal value for `KspWalletEnvelopeV1`.
pub(crate) fn new_internal(
owner_auth_public_key: [u8; crate::KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES],
view_descriptor: WalletViewDescriptorV1,
@@ -434,7 +442,6 @@ impl KspWalletEnvelopeV1 {
if source.len() > crate::KSPWALLET_MAX_FILE_BYTES {
return std::result::Result::Err(format_error("Wallet document exceeds the V1 maximum size", "document"));
}
let probe_result = serde_json::from_slice::<RawVersionProbe>(source);
let probe = match probe_result {
std::result::Result::Ok(probe) => probe,
@@ -449,7 +456,6 @@ impl KspWalletEnvelopeV1 {
.with_context("format_version", probe.format_version.to_string()),
);
}
let raw_result = serde_json::from_slice::<RawEnvelopeV1>(source);
let raw = match raw_result {
std::result::Result::Ok(raw) => raw,
@@ -793,7 +799,6 @@ fn parse_raw_envelope(raw: RawEnvelopeV1) -> ksp_core_lib::Result<KspWalletEnvel
.with_context("format_version", raw.format_version.to_string()),
);
}
let owner_auth_public_key =
match decode_fixed::<{ crate::KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES }>(raw.owner_auth_public_key.as_str(), "owner_auth_public_key") {
std::result::Result::Ok(value) => value,
@@ -803,7 +808,6 @@ fn parse_raw_envelope(raw: RawEnvelopeV1) -> ksp_core_lib::Result<KspWalletEnvel
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if raw.key_slots.is_empty() || raw.key_slots.len() > crate::KSPWALLET_V1_MAX_KEY_SLOTS {
return std::result::Result::Err(format_error("Wallet key_slots count is invalid for V1", "key_slots"));
}
@@ -853,7 +857,6 @@ fn parse_raw_envelope(raw: RawEnvelopeV1) -> ksp_core_lib::Result<KspWalletEnvel
} else if view_slot.is_some() {
return std::result::Result::Err(format_error("Disabled VIEW descriptor forbids a VIEW key slot", "key_slots"));
}
let owner_control = match parse_compartment(
WalletCompartmentKindV1::OwnerControl,
raw.owner_control.control_version,
@@ -891,7 +894,6 @@ fn parse_raw_envelope(raw: RawEnvelopeV1) -> ksp_core_lib::Result<KspWalletEnvel
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(KspWalletEnvelopeV1 {
owner_auth_public_key,
view_descriptor,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/tests/dependency_boundary.rs
// version: 10
// version: 11
//! Wallet-specific dependency and ownership canaries.
@@ -38,6 +38,10 @@ fn wallet_manifest_preserves_dependency_firewall() -> std::io::Result<()> {
std::result::Result::Ok(manifest) => manifest,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let workspace_manifest = match std::fs::read_to_string(crate_root().join("../../Cargo.toml")) {
std::result::Result::Ok(workspace_manifest) => workspace_manifest,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert!(manifest.contains("ksp-core-lib"));
assert!(manifest.contains("ksp-logging-lib"));
assert!(manifest.contains("argon2 = { workspace = true, features = [\"alloc\", \"zeroize\"] }"));
@@ -51,6 +55,7 @@ fn wallet_manifest_preserves_dependency_firewall() -> std::io::Result<()> {
assert!(manifest.contains("tokio = { workspace = true, features = [\"rt\"] }"));
assert!(manifest.contains("tempfile.workspace = true"));
assert!(manifest.contains("zeroize.workspace = true"));
assert!(workspace_manifest.contains("ed25519-dalek = { version = \"^3.0\", default-features = false }"));
for forbidden in [
"ksp-config-lib",
"ksp-onchain-transport-lib",

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/tests/public_api.rs
// version: 8
// version: 9
//! Public API canaries for the Wallet foundation.
@@ -102,15 +102,12 @@ fn public_pre_006_file_persistence_surface_is_available_from_crate_root() {
let create_future =
ksp_wallet_lib::create_wallet_file_v1(path, owner_password, std::option::Option::None, ksp_wallet_lib::WalletCreateMetadataV1::default());
drop(create_future);
let view_password = ksp_wallet_lib::ViewPassword::new(std::string::String::from("public-pre006-view-password"));
let view_future = ksp_wallet_lib::open_wallet_view_file_v1(path, view_password);
drop(view_future);
let owner_password = ksp_wallet_lib::OwnerPassword::new(std::string::String::from("public-pre006-owner-password"));
let owner_future = ksp_wallet_lib::open_wallet_owner_file_v1(path, owner_password);
drop(owner_future);
let inspect_future = ksp_wallet_lib::inspect_locked_wallet_file_v1(path);
drop(inspect_future);
}
@@ -137,7 +134,6 @@ fn public_pre_008_transfer_adapters_are_available_from_crate_root() {
let inspect_method: fn(&[u8], ksp_wallet_lib::WalletTransferFormat) -> ksp_core_lib::Result<ksp_wallet_lib::WalletTransferInspection> =
ksp_wallet_lib::inspect_wallet_transfer;
let _ = inspect_method;
let path = std::path::Path::new("not-polled-transfer");
let inspect_file_future = ksp_wallet_lib::inspect_wallet_transfer_file(path, formats[0]);
drop(inspect_file_future);

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/unit_tests/administration.rs
// version: 1
// version: 2
const FULL_VECTOR: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_full_vector.json");
@@ -10,7 +10,7 @@ fn runtime() -> tokio::runtime::Runtime {
fn published_vector(name: &str) -> (tempfile::TempDir, std::path::PathBuf) {
let directory = tempfile::Builder::new().prefix("ksp-wallet-admin-").tempdir().expect("Wallet administration test directory must be creatable");
let path = directory.path().join(name);
crate::persistence::persist_new_wallet_for_test(path.as_path(), FULL_VECTOR).expect("full Wallet vector must publish for administration test");
crate::persist_new_wallet_for_test(path.as_path(), FULL_VECTOR).expect("full Wallet vector must publish for administration test");
return (directory, path);
}
@@ -26,14 +26,11 @@ fn owner_signing_is_deterministic_and_owner_rotation_preserves_solana_identity()
let signature_before = owner.sign(message).expect("OWNER must sign");
let signature_repeat = owner.sign(message).expect("OWNER repeated signature must succeed");
assert_eq!(signature_before, signature_repeat);
runtime
.block_on(owner.rotate_owner_password(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre007-new-owner-password"))))
.expect("OWNER password rotation must persist");
let old = runtime.block_on(crate::open_wallet_owner_file_v1(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))));
assert_eq!(old.expect_err("old OWNER password must stop unlocking current file").code(), crate::ERROR_CODE_OWNER_UNLOCK_FAILED);
let reopened = runtime
.block_on(crate::open_wallet_owner_file_v1(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre007-new-owner-password"))))
.expect("new OWNER password must unlock current file");
@@ -51,21 +48,17 @@ fn view_self_rotation_changes_only_the_current_view_credential() {
let pubkey = *view.pubkey();
let alias = view.alias().map(std::string::String::from);
let notes = view.notes().to_vec();
runtime
.block_on(view.rotate_view_password(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre007-new-view-password"))))
.expect("VIEW self-rotation must persist");
let old = runtime.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))));
assert_eq!(old.expect_err("old VIEW password must stop unlocking current file").code(), crate::ERROR_CODE_VIEW_UNLOCK_FAILED);
let reopened = runtime
.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre007-new-view-password"))))
.expect("new VIEW password must unlock current file");
assert_eq!(*reopened.pubkey(), pubkey);
assert_eq!(reopened.alias(), alias.as_deref());
assert_eq!(reopened.notes(), notes.as_slice());
let owner = runtime
.block_on(crate::open_wallet_owner_file_v1(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))))
.expect("VIEW self-rotation must not change OWNER credential");
@@ -80,11 +73,9 @@ fn owner_rotates_view_without_old_view_password() {
.block_on(crate::open_wallet_owner_file_v1(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))))
.expect("OWNER fixture must open");
let pubkey = *owner.pubkey();
runtime
.block_on(owner.rotate_view_password(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre007-owner-set-view-password"))))
.expect("OWNER must rotate VIEW without old VIEW password");
let old = runtime.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))));
assert_eq!(old.expect_err("old VIEW password must stop unlocking current file").code(), crate::ERROR_CODE_VIEW_UNLOCK_FAILED);
let reopened = runtime
@@ -101,7 +92,6 @@ fn owner_metadata_administration_is_visible_to_view_but_does_not_change_pubkey()
.block_on(crate::open_wallet_owner_file_v1(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))))
.expect("OWNER fixture must open");
let pubkey = *owner.pubkey();
runtime
.block_on(owner.update_alias(path.as_path(), std::option::Option::Some(std::string::String::from("pre007-updated-alias"))))
.expect("OWNER alias update must persist");
@@ -109,14 +99,12 @@ fn owner_metadata_administration_is_visible_to_view_but_does_not_change_pubkey()
runtime
.block_on(owner.update_note(path.as_path(), note_id.as_str(), std::string::String::from("pre007-updated-note")))
.expect("OWNER note update must persist");
let view = runtime
.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))))
.expect("VIEW must read OWNER-updated metadata");
assert_eq!(*view.pubkey(), pubkey);
assert_eq!(view.alias(), std::option::Option::Some("pre007-updated-alias"));
assert!(view.notes().iter().any(|note| return note.id() == note_id.as_str() && note.text() == "pre007-updated-note"));
runtime.block_on(owner.delete_note(path.as_path(), note_id.as_str())).expect("OWNER note delete must persist");
let view_after_delete = runtime
.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))))
@@ -139,14 +127,12 @@ fn strong_view_disable_and_recreate_change_current_view_authority_without_changi
let before = crate::KspWalletEnvelopeV1::parse_json(before_bytes.as_slice()).expect("current Wallet must parse");
let old_slot_id = *before.view_descriptor().slot_id().expect("fixture VIEW descriptor must have slot ID");
let old_metadata_ciphertext = before.metadata().ciphertext().to_vec();
runtime.block_on(owner.disable_view(path.as_path())).expect("OWNER strong VIEW disable must persist");
let locked_disabled = runtime.block_on(crate::inspect_locked_wallet_file_v1(path.as_path())).expect("disabled Wallet must inspect");
assert!(!locked_disabled.view_enabled());
let disabled_view =
runtime.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))));
assert_eq!(disabled_view.expect_err("disabled VIEW must not open").code(), crate::ERROR_CODE_CAPABILITY_INSUFFICIENT);
runtime
.block_on(owner.recreate_view(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre007-recreated-view-password"))))
.expect("OWNER strong VIEW recreate must persist");
@@ -155,7 +141,6 @@ fn strong_view_disable_and_recreate_change_current_view_authority_without_changi
let new_slot_id = *recreated.view_descriptor().slot_id().expect("recreated VIEW descriptor must have slot ID");
assert_ne!(new_slot_id, old_slot_id);
assert_ne!(recreated.metadata().ciphertext(), old_metadata_ciphertext.as_slice());
let old = runtime.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))));
assert_eq!(old.expect_err("historical VIEW password must not unlock recreated current state").code(), crate::ERROR_CODE_VIEW_UNLOCK_FAILED);
let recreated_view = runtime
@@ -173,7 +158,6 @@ fn owner_note_mutation_reports_missing_identifier_without_persisting() {
let mut owner = runtime
.block_on(crate::open_wallet_owner_file_v1(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))))
.expect("OWNER fixture must open");
let result = runtime.block_on(owner.update_note(path.as_path(), "AAAAAAAAAAAAAAAAAAAAAA", std::string::String::from("must-not-persist")));
assert_eq!(result.expect_err("missing note identifier must be rejected").code(), crate::ERROR_CODE_NOTE_NOT_FOUND);
assert_eq!(std::fs::read(path.as_path()).expect("Wallet bytes must remain readable"), before);
@@ -189,13 +173,11 @@ fn stale_owner_handle_cannot_overwrite_a_newer_authenticated_wallet_state() {
let mut stale = runtime
.block_on(crate::open_wallet_owner_file_v1(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))))
.expect("second OWNER handle must open on the same state");
runtime
.block_on(first.update_alias(path.as_path(), std::option::Option::Some(std::string::String::from("first-writer"))))
.expect("first OWNER mutation must persist");
let stale_result = runtime.block_on(stale.update_alias(path.as_path(), std::option::Option::Some(std::string::String::from("stale-writer"))));
assert_eq!(stale_result.expect_err("stale OWNER handle must not overwrite newer state").code(), crate::ERROR_CODE_STATE_CONFLICT);
let current = runtime
.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))))
.expect("VIEW must open the first writer's state");
@@ -213,14 +195,12 @@ fn owner_handle_cannot_replace_a_different_authenticated_wallet_state() {
let mut second = runtime
.block_on(crate::open_wallet_owner_file_v1(second_path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))))
.expect("second OWNER handle must open");
runtime
.block_on(second.update_alias(second_path.as_path(), std::option::Option::Some(std::string::String::from("different-authenticated-state"))))
.expect("second Wallet must diverge before wrong-target canary");
let wrong_target =
runtime.block_on(first.update_alias(second_path.as_path(), std::option::Option::Some(std::string::String::from("must-not-overwrite-second-wallet"))));
assert_eq!(wrong_target.expect_err("OWNER handle must not replace a different authenticated Wallet state").code(), crate::ERROR_CODE_STATE_CONFLICT);
let unchanged_first = runtime
.block_on(crate::open_wallet_view_file_v1(first_path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))))
.expect("first Wallet must remain unchanged");

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-wallet-lib/unit_tests/crypto.rs
// version: 2
// version: 3
use base64::Engine; // rust-rules: derive-import
use base64::Engine; // rust-rules: trait-import
const VECTOR: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_crypto_vectors.json");
@@ -62,14 +62,12 @@ fn deterministic_argon2id_and_xchacha_wrap_vector_matches_external_canary() -> k
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let derived_result = super::derive_argon2id(vector.password_utf8.as_bytes(), salt.as_slice(), vector.memory_kib, vector.iterations, vector.parallelism);
let derived = match derived_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(derived.as_bytes(), &expected_derived);
let content = crate::SecretKeyV1::from_bytes(content_key);
let wrapped_result = crate::wrap_key(&derived, &content, &nonce, aad.as_slice());
let wrapped = match wrapped_result {
@@ -77,7 +75,6 @@ fn deterministic_argon2id_and_xchacha_wrap_vector_matches_external_canary() -> k
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(wrapped, expected_wrapped);
let unwrapped_result = crate::unwrap_key(&derived, &nonce, aad.as_slice(), wrapped.as_slice());
let unwrapped = match unwrapped_result {
std::result::Result::Ok(value) => value,

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-wallet-lib/unit_tests/payload.rs
// version: 2
// version: 3
use base64::Engine; // rust-rules: derive-import
use base64::Engine; // rust-rules: trait-import
#[test]
fn metadata_payload_rejects_duplicate_note_identifiers() -> ksp_core_lib::Result<()> {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/unit_tests/persistence.rs
// version: 3
// version: 4
fn temp_directory() -> std::io::Result<tempfile::TempDir> {
return tempfile::tempdir();
@@ -11,9 +11,8 @@ fn no_clobber_create_keeps_the_first_published_document() {
let destination = directory.path().join("wallet.kspwallet");
let first = b"first-wallet";
let second = b"second-wallet";
crate::persistence::persist_new_wallet_for_test(destination.as_path(), first).expect("first no-clobber publication must succeed");
let second_result = crate::persistence::persist_new_wallet_for_test(destination.as_path(), second);
crate::persist_new_wallet_for_test(destination.as_path(), first).expect("first no-clobber publication must succeed");
let second_result = crate::persist_new_wallet_for_test(destination.as_path(), second);
let error = second_result.expect_err("second publication must not overwrite an existing wallet");
assert_eq!(error.code(), crate::ERROR_CODE_DESTINATION_EXISTS);
let persisted = std::fs::read(destination.as_path()).expect("published wallet must remain readable");
@@ -24,11 +23,10 @@ fn no_clobber_create_keeps_the_first_published_document() {
fn injected_failure_before_publish_leaves_no_destination_or_partial_wallet() {
let directory = temp_directory().expect("Wallet persistence test directory must be creatable");
let destination = directory.path().join("wallet.kspwallet");
let result = crate::persistence::persist_new_wallet_fault_before_publish(destination.as_path(), b"candidate-wallet");
let result = crate::persist_new_wallet_fault_before_publish(destination.as_path(), b"candidate-wallet");
let error = result.expect_err("fault injection must abort before publication");
assert_eq!(error.code(), crate::ERROR_CODE_ATOMIC_PERSISTENCE_FAILED);
assert!(!destination.exists());
let entries = std::fs::read_dir(directory.path()).expect("Wallet persistence test directory must remain readable");
let count = entries.count();
assert_eq!(count, 0, "temporary artifacts should be cleaned on ordinary error unwinding");
@@ -40,17 +38,15 @@ fn concurrent_no_clobber_publish_has_exactly_one_winner() {
let destination = directory.path().join("wallet.kspwallet");
let barrier = std::sync::Arc::new(std::sync::Barrier::new(8));
let mut handles = std::vec::Vec::new();
for index in 0_u8..8 {
let destination = destination.clone();
let barrier = std::sync::Arc::clone(&barrier);
handles.push(std::thread::spawn(move || {
barrier.wait();
let content = [index; 32];
return crate::persistence::persist_new_wallet_for_test(destination.as_path(), content.as_slice());
return crate::persist_new_wallet_for_test(destination.as_path(), content.as_slice());
}));
}
let mut success_count = 0_usize;
let mut exists_count = 0_usize;
let mut unexpected_code = std::option::Option::None;
@@ -74,7 +70,6 @@ fn bounded_reader_rejects_oversized_wallet_before_parser_allocation() {
let destination = directory.path().join("oversized.kspwallet");
let oversized = std::vec![b'x'; crate::KSPWALLET_MAX_FILE_BYTES + 1];
std::fs::write(destination.as_path(), oversized).expect("oversized fixture must be writable");
let runtime = tokio::runtime::Builder::new_current_thread().build().expect("Wallet persistence test runtime must build");
let result = runtime.block_on(crate::inspect_locked_wallet_file_v1(destination.as_path()));
let error = result.expect_err("oversized Wallet file must be rejected before parsing");
@@ -91,7 +86,6 @@ fn public_file_create_and_locked_inspect_round_trip_without_revealing_identity()
.block_on(crate::create_wallet_file_v1(destination.as_path(), owner_password, std::option::Option::None, crate::WalletCreateMetadataV1::default()))
.expect("native Wallet file creation must succeed");
let locked = runtime.block_on(crate::inspect_locked_wallet_file_v1(destination.as_path())).expect("persisted Wallet must inspect successfully");
assert!(!locked.view_enabled());
assert_eq!(created.capability(), crate::WalletCapability::Owner);
assert!(destination.exists());
@@ -110,8 +104,7 @@ fn persisted_full_vector_opens_view_and_owner_through_file_apis() {
let directory = temp_directory().expect("Wallet persistence test directory must be creatable");
let destination = directory.path().join("vector.kspwallet");
let vector = include_bytes!("../tests/fixtures/kspwallet_v1_full_vector.json");
crate::persistence::persist_new_wallet_for_test(destination.as_path(), vector).expect("full vector must publish through no-clobber persistence");
crate::persist_new_wallet_for_test(destination.as_path(), vector).expect("full vector must publish through no-clobber persistence");
let runtime = tokio::runtime::Builder::new_current_thread().build().expect("Wallet persistence test runtime must build");
let view_password = crate::ViewPassword::new(std::string::String::from("pre005-view-password"));
let view = runtime
@@ -121,7 +114,6 @@ fn persisted_full_vector_opens_view_and_owner_through_file_apis() {
let owner = runtime
.block_on(crate::open_wallet_owner_file_v1(destination.as_path(), owner_password))
.expect("persisted full vector must open through OWNER file API");
assert_eq!(view.capability(), crate::WalletCapability::View);
assert_eq!(owner.capability(), crate::WalletCapability::Owner);
assert_eq!(view.pubkey(), owner.pubkey());
@@ -136,8 +128,7 @@ fn replacement_fault_before_publish_preserves_the_previous_wallet_bytes() {
let original = b"ORIGINAL-WALLET-CANARY";
let replacement = b"REPLACEMENT-WALLET-CANARY";
std::fs::write(destination.as_path(), original).expect("replacement test original must be writable");
let result = crate::persistence::replace_wallet_fault_before_publish(destination.as_path(), replacement);
let result = crate::replace_wallet_fault_before_publish(destination.as_path(), replacement);
assert_eq!(result.expect_err("injected replacement fault must fail").code(), crate::ERROR_CODE_ATOMIC_PERSISTENCE_FAILED);
assert_eq!(std::fs::read(destination.as_path()).expect("original Wallet bytes must remain readable"), original);
}
@@ -146,10 +137,9 @@ fn replacement_fault_before_publish_preserves_the_previous_wallet_bytes() {
fn replacement_requires_an_existing_regular_file_and_replaces_complete_content() {
let directory = temp_directory().expect("Wallet replacement test directory must be creatable");
let destination = directory.path().join("replace.kspwallet");
let missing = crate::persistence::replace_wallet_for_test(destination.as_path(), b"replacement");
let missing = crate::replace_wallet_for_test(destination.as_path(), b"replacement");
assert_eq!(missing.expect_err("replacement must not create a missing Wallet destination").code(), crate::ERROR_CODE_IO_FAILED);
std::fs::write(destination.as_path(), b"old").expect("replacement test original must be writable");
crate::persistence::replace_wallet_for_test(destination.as_path(), b"new-complete-wallet").expect("existing Wallet replacement must succeed");
crate::replace_wallet_for_test(destination.as_path(), b"new-complete-wallet").expect("existing Wallet replacement must succeed");
assert_eq!(std::fs::read(destination.as_path()).expect("replaced Wallet must be readable"), b"new-complete-wallet");
}

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-wallet-lib/unit_tests/security.rs
// version: 2
// version: 3
//! Adversarial security canaries for native `.kspwallet` V1.
use base64::Engine; // rust-rules: derive-import
use base64::Engine; // rust-rules: trait-import
const FULL_VECTOR: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_full_vector.json");
@@ -27,10 +27,8 @@ fn owner_signed_regions_reject_canonical_tampering_before_unlock() {
fn owner_signature_verification_precedes_owner_and_view_password_kdf() {
let tampered = tamper_base64url_field(FULL_VECTOR, "/metadata/ciphertext");
let runtime = runtime();
let owner = runtime.block_on(crate::open_wallet_owner_v1(tampered.as_slice(), crate::OwnerPassword::new(std::string::String::new())));
assert_eq!(owner.expect_err("tampered state must fail before an empty OWNER password reaches Argon2").code(), crate::ERROR_CODE_AUTHENTICATION_FAILED);
let view = runtime.block_on(crate::open_wallet_view_v1(tampered.as_slice(), crate::ViewPassword::new(std::string::String::new())));
assert_eq!(view.expect_err("tampered state must fail before an empty VIEW password reaches Argon2").code(), crate::ERROR_CODE_AUTHENTICATION_FAILED);
}
@@ -40,12 +38,10 @@ fn view_credential_tampering_does_not_forge_owner_state_and_cannot_unlock_view()
let tampered = tamper_base64url_field(FULL_VECTOR, "/key_slots/1/wrap/ciphertext");
let locked = crate::inspect_locked_wallet_v1(tampered.as_slice()).expect("VIEW wrapping credentials are intentionally outside the OWNER state transcript");
assert!(locked.view_enabled());
let runtime = runtime();
let owner =
runtime.block_on(crate::open_wallet_owner_v1(tampered.as_slice(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))));
assert!(owner.is_ok(), "VIEW credential tampering must not invalidate OWNER unlock");
let view = runtime.block_on(crate::open_wallet_view_v1(tampered.as_slice(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))));
assert_eq!(view.expect_err("tampered VIEW wrapping must not unlock metadata").code(), crate::ERROR_CODE_VIEW_UNLOCK_FAILED);
}
@@ -55,13 +51,11 @@ fn unlock_failures_do_not_echo_password_material() {
let owner_canary = "OWNER-PASSWORD-LEAK-CANARY";
let view_canary = "VIEW-PASSWORD-LEAK-CANARY";
let runtime = runtime();
let owner = runtime.block_on(crate::open_wallet_owner_v1(FULL_VECTOR, crate::OwnerPassword::new(std::string::String::from(owner_canary))));
let owner_error = owner.expect_err("wrong OWNER password must fail");
assert_eq!(owner_error.code(), crate::ERROR_CODE_OWNER_UNLOCK_FAILED);
assert!(!owner_error.to_string().contains(owner_canary));
assert!(!format!("{owner_error:?}").contains(owner_canary));
let view = runtime.block_on(crate::open_wallet_view_v1(FULL_VECTOR, crate::ViewPassword::new(std::string::String::from(view_canary))));
let view_error = view.expect_err("wrong VIEW password must fail");
assert_eq!(view_error.code(), crate::ERROR_CODE_VIEW_UNLOCK_FAILED);

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-wallet-lib/unit_tests/transcript.rs
// version: 2
// version: 3
use base64::Engine; // rust-rules: derive-import
use base64::Engine; // rust-rules: trait-import
const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_wire_only.json");

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-wallet-lib/unit_tests/transfer.rs
// version: 2
// version: 3
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt; // rust-rules: derive-import
use zeroize::Zeroize; // rust-rules: derive-import
use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import
use zeroize::Zeroize; // rust-rules: trait-import
fn runtime() -> tokio::runtime::Runtime {
return tokio::runtime::Builder::new_current_thread().build().expect("Wallet transfer test runtime must build");
@@ -41,7 +41,6 @@ fn in_memory_inspection_accepts_cli_json_and_canonical_base58_without_exposing_s
crate::inspect_wallet_transfer(json.as_slice(), crate::WalletTransferFormat::SolanaCliJson).expect("Solana CLI JSON inspection must succeed");
let base58_inspection =
crate::inspect_wallet_transfer(base58.as_bytes(), crate::WalletTransferFormat::SolanaKeypairBase58).expect("canonical Base58 inspection must succeed");
assert_eq!(json_inspection.pubkey(), base58_inspection.pubkey());
assert_eq!(json_inspection.format(), crate::WalletTransferFormat::SolanaCliJson);
assert_eq!(base58_inspection.format(), crate::WalletTransferFormat::SolanaKeypairBase58);
@@ -63,7 +62,6 @@ fn external_seed7_transfer_canary_matches_known_solana_keypair_encodings() {
assert_eq!(actual_keypair_bytes, expected_keypair_bytes);
let mut actual_base58 = keypair.to_base58_string();
assert_eq!(actual_base58, expected_base58);
let mut json = serde_json::to_vec(expected_keypair_bytes.as_slice()).expect("external keypair canary must encode as JSON");
let json_inspection =
crate::inspect_wallet_transfer(json.as_slice(), crate::WalletTransferFormat::SolanaCliJson).expect("external Solana CLI JSON canary must inspect");
@@ -71,7 +69,6 @@ fn external_seed7_transfer_canary_matches_known_solana_keypair_encodings() {
.expect("external Base58 canary must inspect");
assert_eq!(json_inspection.pubkey().to_string(), expected_pubkey);
assert_eq!(base58_inspection.pubkey().to_string(), expected_pubkey);
json.zeroize();
actual_base58.zeroize();
actual_keypair_bytes.zeroize();
@@ -84,7 +81,6 @@ fn transfer_decoders_reject_short_inconsistent_or_noncanonical_sources() {
crate::inspect_wallet_transfer(short_json, crate::WalletTransferFormat::SolanaCliJson).expect_err("short Solana CLI JSON must fail").code(),
crate::ERROR_CODE_KEY_MATERIAL_INVALID
);
let keypair = test_keypair();
let mut bytes = keypair.to_bytes();
bytes[63] ^= 0x01;
@@ -97,7 +93,6 @@ fn transfer_decoders_reject_short_inconsistent_or_noncanonical_sources() {
);
bytes.zeroize();
inconsistent_json.zeroize();
assert_eq!(
crate::inspect_wallet_transfer(b"not valid base58 !!!", crate::WalletTransferFormat::SolanaKeypairBase58)
.expect_err("invalid Base58 must fail")
@@ -136,7 +131,6 @@ fn cli_json_import_creates_new_no_clobber_wallet_with_imported_identity_and_meta
.expect("Solana CLI JSON import must succeed");
assert_eq!(owner.pubkey(), expected.pubkey());
assert_eq!(owner.alias(), std::option::Option::Some("imported-wallet"));
let second_password = crate::OwnerPassword::new(std::string::String::from("pre008-second-owner-password"));
let second = runtime.block_on(crate::import_wallet_transfer_v1(
destination.as_path(),
@@ -172,7 +166,6 @@ fn transfer_file_import_is_non_destructive_and_bounded() {
))
.expect("transfer-file import must succeed");
assert_eq!(std::fs::read(source_path.as_path()).expect("source must survive import"), original);
let oversized_path = directory.path().join("oversized.json");
std::fs::write(oversized_path.as_path(), std::vec![b'1'; crate::KSPWALLET_TRANSFER_MAX_SOLANA_CLI_JSON_BYTES + 1])
.expect("oversized transfer fixture must be writable");
@@ -202,13 +195,11 @@ fn owner_exports_cli_json_and_base58_that_roundtrip_to_the_same_imported_keypair
crate::WalletCreateMetadataV1::default(),
))
.expect("Base58 import must succeed");
let mut exported_json = owner.export_transfer(crate::WalletTransferFormat::SolanaCliJson).expect("OWNER JSON export must succeed");
let mut decoded_json: std::vec::Vec<u8> = serde_json::from_slice(exported_json.as_slice()).expect("OWNER JSON export must be valid JSON");
assert_eq!(decoded_json.as_slice(), expected_bytes.as_slice());
let mut exported_base58 = owner.export_transfer(crate::WalletTransferFormat::SolanaKeypairBase58).expect("OWNER Base58 export must succeed");
assert_eq!(exported_base58.as_slice(), source_base58.as_bytes());
exported_json.zeroize();
decoded_json.zeroize();
exported_base58.zeroize();
@@ -233,7 +224,6 @@ fn owner_transfer_file_export_is_no_clobber() {
let second = runtime.block_on(owner.export_transfer_file(export_path.as_path(), crate::WalletTransferFormat::SolanaCliJson));
assert_eq!(second.expect_err("OWNER transfer-file export must not overwrite").code(), crate::ERROR_CODE_DESTINATION_EXISTS);
assert_eq!(std::fs::read(export_path.as_path()).expect("first export must remain intact"), first);
#[cfg(unix)]
{
let mode = std::fs::metadata(export_path.as_path()).expect("export metadata must be readable").permissions().mode() & 0o777;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/unit_tests/wallet.rs
// version: 3
// version: 4
const FULL_VECTOR: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_full_vector.json");
const FULL_VECTOR_META: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_full_vector_meta.json");
@@ -25,7 +25,6 @@ fn externally_generated_full_vector_opens_view_and_owner_independently() -> ksp_
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let view_result = runtime.block_on(crate::open_wallet_view_v1(FULL_VECTOR, crate::ViewPassword::new(vector.view_password_utf8)));
let view = match view_result {
std::result::Result::Ok(value) => value,
@@ -35,7 +34,6 @@ fn externally_generated_full_vector_opens_view_and_owner_independently() -> ksp_
assert_eq!(view.pubkey().to_string(), vector.expected_pubkey);
assert_eq!(view.alias(), std::option::Option::Some(vector.expected_alias.as_str()));
assert_note_texts(view.notes(), vector.expected_notes.as_slice());
let owner_result = runtime.block_on(crate::open_wallet_owner_v1(FULL_VECTOR, crate::OwnerPassword::new(vector.owner_password_utf8)));
let owner = match owner_result {
std::result::Result::Ok(value) => value,
@@ -58,21 +56,18 @@ fn wrong_passwords_do_not_cross_unlock_capabilities() -> ksp_core_lib::Result<()
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let wrong_view = runtime.block_on(crate::open_wallet_view_v1(FULL_VECTOR, crate::ViewPassword::new(std::string::String::from("wrong-view-password"))));
let wrong_view_error = match wrong_view {
std::result::Result::Ok(_) => return std::result::Result::Err(test_error("wrong VIEW password unexpectedly unlocked Wallet")),
std::result::Result::Err(error) => error,
};
assert_eq!(wrong_view_error.code(), crate::ERROR_CODE_VIEW_UNLOCK_FAILED);
let wrong_owner = runtime.block_on(crate::open_wallet_owner_v1(FULL_VECTOR, crate::OwnerPassword::new(std::string::String::from("wrong-owner-password"))));
let wrong_owner_error = match wrong_owner {
std::result::Result::Ok(_) => return std::result::Result::Err(test_error("wrong OWNER password unexpectedly unlocked Wallet")),
std::result::Result::Err(error) => error,
};
assert_eq!(wrong_owner_error.code(), crate::ERROR_CODE_OWNER_UNLOCK_FAILED);
let view_as_owner = runtime.block_on(crate::open_wallet_owner_v1(FULL_VECTOR, crate::OwnerPassword::new(vector.view_password_utf8)));
let view_as_owner_error = match view_as_owner {
std::result::Result::Ok(_) => return std::result::Result::Err(test_error("VIEW password unexpectedly unlocked OWNER")),
@@ -112,7 +107,6 @@ fn view_wrap_can_change_without_breaking_owner_authenticated_state() -> ksp_core
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert!(inspected.view_enabled());
let runtime = match test_runtime() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -121,7 +115,6 @@ fn view_wrap_can_change_without_breaking_owner_authenticated_state() -> ksp_core
if let std::result::Result::Err(error) = owner_result {
return std::result::Result::Err(error);
}
let view_result = runtime.block_on(crate::open_wallet_view_v1(tampered.as_slice(), crate::ViewPassword::new(vector.view_password_utf8)));
let view_error = match view_result {
std::result::Result::Ok(_) => return std::result::Result::Err(test_error("modified VIEW wrapping unexpectedly unlocked metadata")),
@@ -197,7 +190,6 @@ fn create_uses_calibrated_defaults_and_keeps_locked_projection_private() -> ksp_
std::option::Option::None => return std::result::Result::Err(test_error("created Wallet is missing initial note")),
};
assert_eq!(created_note.text(), note.as_str());
let locked_bytes = match owner.to_json_bytes() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -209,7 +201,6 @@ fn create_uses_calibrated_defaults_and_keeps_locked_projection_private() -> ksp_
assert!(!locked_text.contains(alias.as_str()));
assert!(!locked_text.contains(note.as_str()));
assert!(!locked_text.contains(owner.pubkey().to_string().as_str()));
let envelope = match crate::KspWalletEnvelopeV1::parse_json(locked_bytes.as_slice()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/unit_tests/wire.rs
// version: 3
// version: 4
const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_wire_only.json");
@@ -16,7 +16,6 @@ fn strict_v1_fixture_parses_and_round_trips_semantically() -> ksp_core_lib::Resu
assert_eq!(parsed.owner_control().payload_version(), 1);
assert_eq!(parsed.metadata().payload_version(), 1);
assert_eq!(parsed.secret().payload_version(), 1);
let serialized = match parsed.to_json_bytes() {
std::result::Result::Ok(serialized) => serialized,
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -89,7 +88,6 @@ fn zero_or_pathological_kdf_parameters_are_rejected_before_crypto() {
std::result::Result::Ok(_) => return,
};
assert_eq!(zero_error.code(), crate::ERROR_CODE_CRYPTO_PARAMETERS_INVALID);
let high_source = std::string::String::from_utf8_lossy(FIXTURE).replacen("\"memory_kib\": 65536", "\"memory_kib\": 1048577", 1);
let high_result = crate::KspWalletEnvelopeV1::parse_json(high_source.as_bytes());
assert!(high_result.is_err());

View File

@@ -0,0 +1,387 @@
<!-- file: deltas/0.2.5/pre.010-fix.001.md -->
<!-- version: 1 -->
# Delta `0.2.5-pre.010-fix.001` — Dalek 3 + normalisation Rust workspace + prompt `0.2.6`
## 1. Base et statut
Base opérateur autoritaire :
```text
0.2.5-pre.010
archive complète modifiée fournie par l'opérateur le 2026-08-20
workspace.package.version = 0.2.5-pre.9
```
`pre.010` n'est **pas** suivi directement de `rel.001`. Ce fix rouvre volontairement le gate technique avant toute publication stable.
Version technique cible :
```text
workspace.package.version = 0.2.5-pre.10.fix.1
```
Identifiant de livraison :
```text
0.2.5-pre.010-fix.001
```
Commit attendu après validation :
```text
v0.2.5-pre.010-fix.001
```
Aucun tag stable n'est créé par ce fix.
## 2. Objectifs
Ce fix corrige trois écarts distincts avant la stable :
1. mettre la dépendance KSP directe `ed25519-dalek` sur la génération stable `3.0` ;
2. normaliser **tout le workspace Rust actuel**, pas seulement `ksp-wallet-lib`, selon les conventions KSP qui ne sont pas couvertes par `rustfmt`/Clippy ;
3. renforcer le prompt d'ouverture `0.2.6` afin qu'une nouvelle session récupère de manière autonome les règles, le gate `pre.001`, le forecast souple et le workflow de validation sans dépendre d'une mémoire implicite.
Ce fix ne modifie pas le wire `.kspwallet` V1, les algorithmes cryptographiques retenus, les tailles/paramètres sérialisés ni le threat model Wallet.
## 3. `ed25519-dalek 3.0.0`
La dépendance workspace KSP devient :
```toml
ed25519-dalek = { version = "^3.0", default-features = false }
```
Le manifest Wallet conserve les features locales réellement consommées :
```toml
ed25519-dalek = { workspace = true, features = ["signature", "zeroize"] }
```
Le réaudit primaire effectué pendant la préparation confirme pour `ed25519-dalek 3.0.0` :
```text
release 2026-07-06
Rust edition 2024
MSRV 1.85
signature feature explicite disponible
zeroize feature explicite disponible
SigningKey API actuelle
VerifyingKey API actuelle
Signer trait actuel
Signature API actuelle
```
`ksp-wallet-lib` n'utilise pas les features par défaut ; il conserve donc uniquement `signature` + `zeroize`, sans activer `fast` implicitement.
### Coexistence Dalek 3.x / 2.x
`solana-keypair 3.1.2` dépend encore officiellement de :
```text
ed25519-dalek ^2.1.1
```
Le tree attendu après ce fix comporte donc **deux générations Dalek légitimes** :
```text
ed25519-dalek 3.0.0 <- dépendance KSP directe de ksp-wallet-lib
ed25519-dalek 2.2.x <- solana-keypair 3.1.2 <- ksp-wallet-lib
```
Il ne faut pas forcer leur unification en contournant les contraintes upstream. La convergence `solana-address` reste à confirmer par le tree opérateur.
## 4. Normalisation Rust workspace-wide
La base opérateur a été auditée avec le nouvel outil en mode report-only avant normalisation. Résultat initial :
```text
General Rust rule audit: 552 violation(s)
RUST-DOC-101: 130
RUST-DOC-102: 79
RUST-FMT-101: 150
RUST-FMT-102: 8
RUST-FMT-104: 58
RUST-FMT-105: 49
RUST-FMT-106: 1
RUST-FMT-107: 47
RUST-IMPORT-101: 29
RUST-IMPORT-104: 1
Rust export completeness audit: 50 candidate(s)
RUST-API-201: 38
RUST-IMPORT-201: 12
KSP workspace Rust rule audit: clean
```
Après normalisation de l'arbre cible :
```text
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
```
### Conventions désormais normalisées
Le fix applique notamment :
```text
use uniquement pour traits réellement nécessaires
marqueur canonique : // rust-rules: trait-import
aucun grouped import
aucun glob import
aucun alias use ... as ...
aucun extern crate ... as ...
use uniquement en tête de module, jamais dans une fonction/test
pub/pub(crate) partagé -> réexport crate-root
usage interne partagé -> crate::Item
super::Item réservé à un item strictement privé du parent dans un test séparé
pas de crate::module::Item pour un item visible et réexporté
rustdoc utile sur pub et pub(crate), champs/méthodes/reexports compris
pas de ligne vide à l'intérieur d'un corps fonction/méthode/struct/enum
blocs const ordonnés par visibilité puis nom lorsque l'ordre n'est pas sémantique
reexports pub puis pub(crate), ordonnés avec leur rustdoc/attributs
modules production avant modules test et ordre alphabétique lorsque possible
```
Les règles contextuelles qui ne peuvent pas être prouvées mécaniquement sans parser sémantique restent une responsabilité de revue humaine ; le script ne prétend pas remplacer `rustfmt`, Clippy, le compilateur ou les tests.
## 5. Nouveaux audits Python
Ajouts stdlib-only :
```text
scripts/audit_rust_general_rules.py
scripts/audit_rust_export_completeness.py
scripts/audit_ksp_workspace_rules.py
scripts/audit_rust_workspace_rules.py
```
Commande canonique :
```bash
python3 scripts/audit_rust_workspace_rules.py
```
Le wrapper exécute :
```text
general Rust structural audit
crate-root/export completeness audit
KSP workspace boundary audit
```
Le mode diagnostic existe également :
```bash
python3 scripts/audit_rust_workspace_rules.py --report-only
```
L'outil est volontairement un **audit**, pas un auto-formatter : les corrections restent explicites et reviewables.
## 6. Règles versionnées renforcées
### `docs/rules/RULES_RUST.md`
Formalise désormais les conventions qui étaient encore implicites ou incomplètement contrôlées :
- imports de traits seulement et marqueur canonique ;
- interdiction grouped/glob/alias et `extern crate ... as ...` ;
- imports au niveau module ;
- chemins internes les plus courts via crate-root ;
- `super::` seulement pour les privés ;
- complétude des réexports `pub(crate)` ;
- rustdoc sur toute visibilité KSP ;
- ordre/espacement des blocs ;
- validation Python obligatoire.
### `docs/rules/RULES_KSP.md`
Le workflow technique standard intègre l'audit Rust structurel après `cargo fmt` et avant compilation/Clippy.
### `docs/rules/PROMPT_STRUCTURE.md`
Un prompt de démarrage devient explicitement un **contrat opératoire autonome** avec :
- base exacte ;
- ordre de lecture obligatoire ;
- sources externes fraîches lorsque nécessaire ;
- première mission `pre.001` ;
- forecast souple visible ;
- version/delta/validation ;
- instruction d'ouverture ;
- interdiction de déclarer réussie une commande non exécutée.
## 7. Normalisation des façades et chemins internes
### `ksp-logging-lib`
L'ancien alias caché :
```text
pub extern crate tracing as __private_tracing
```
est remplacé par un bridge macro caché sous son nom canonique :
```text
#[doc(hidden)]
pub extern crate tracing
```
Les macros utilisent `$crate::tracing`. Le bridge reste réservé à l'hygiène des macros KSP et n'est pas une API autorisée aux autres crates ; l'audit KSP interdit `ksp_logging_lib::tracing::` côté consumer.
### `ksp-app-config-desk`
Les collisions de noms de réexports crate-root ont été résolues en renommant les **déclarations propriétaires**, pas par des alias de façade. Exemples :
```text
documents::inventory -> document_inventory
documents::detail -> document_detail
environment::report -> environment_report
logging_editor::document -> logging_document
logging_runtime::status -> logging_runtime_status
profiles::inventory -> profile_inventory
profiles::detail -> profile_detail
secrets::reveal -> reveal_secret
```
Les wrappers `#[tauri::command]` gardent leurs noms externes ; le contrat frontend Tauri n'est donc pas volontairement renommé.
### Autres crates
Les items `pub(crate)` réellement partagés sont réexportés par la racine de leur crate et leurs consumers utilisent `crate::Item`. Les tests séparés n'emploient plus `super::` pour des items visibles.
## 8. Prompt `0.2.6` renforcé
`prompts/011-V0_2_6_START_PROMPT.md` est réécrit comme prompt autonome de nouvelle session en reprenant les points qui étaient plus robustes dans les premiers prompts KSP et les workflows bot3 :
```text
base stable et priorité des sources de vérité
ordre de lecture obligatoire
première mission pre.001 audit/brainstorming/sizing avant UI lourde
baseline Rust + audit Python avant modification
forecast souple explicite et non contraignant
frontières Config/Wallet/Transport/Logging/Tauri
sécurité passwords/secrets/frontend
workflow Cargo/delta/fix/commit/tag
validation Tauri finale seulement en dernier
instruction d'ouverture explicite
```
Forecast initial exposé dès l'ouverture :
```text
pre.001 audit/sizing/UX/Config/bridge/security
pre.002 crate Tauri + frontend shell + logging + splash/main
pre.003 std.wallet + composite Wallet Desk + adapters Config
pre.004 inventory/selection + locked inspection + DTOs
pre.005 create/import + unlock VIEW/OWNER + lifecycle
pre.006 wallet details + balance HTTP + refresh/diagnostics
pre.007 metadata administration + rotations VIEW/OWNER
pre.008 strong VIEW disable/recreate + export OWNER + security UX
pre.009 integration tests + Devnet smoke + compliance/security review
pre.010 README/USAGE/build final + prompt suivant si nécessaire
rel.001 publication stable
```
Le prompt précise que ce forecast est ajustable et que la complétude prime toujours sur le numéro de prerelease.
## 9. Portée des fichiers Rust
La normalisation touche **83 fichiers Rust** dans les six crates actuelles :
```text
ksp-app-config-desk 19 fichiers
ksp-config-lib 11 fichiers
ksp-core-lib 2 fichiers
ksp-logging-lib 9 fichiers
ksp-onchain-transport-lib 18 fichiers
ksp-wallet-lib 24 fichiers
```
Chaque fichier Rust existant modifié consomme exactement une nouvelle version de fichier par rapport à la base opérateur fournie.
## 10. Documentation durable modifiée
```text
ROADMAP.md
docs/formats/KSPWALLET_V1.md
docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md
docs/rules/PROMPT_STRUCTURE.md
docs/rules/RULES_KSP.md
docs/rules/RULES_RUST.md
docs/validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md
prompts/011-V0_2_6_START_PROMPT.md
```
Le plan et la matrice de validation enregistrent explicitement que `pre.010-fix.001` bloque `rel.001` tant que la validation opérateur n'est pas propre.
## 11. Contrôles réellement exécutés dans l'environnement de préparation
L'environnement de préparation ne possède ni `cargo` ni `rustc`. Aucune compilation Rust n'est donc revendiquée.
Contrôles réellement exécutés :
```text
nouvel audit sur base opérateur en report-only -> 552 violations / 50 candidates
nouvel audit sur arbre cible -> clean / 0 candidate / clean
versions des 83 fichiers Rust modifiés -> +1 exact
réaudit primaire ed25519-dalek 3.0.0 -> effectué
réaudit primaire solana-keypair 3.1.2 -> effectué
```
Les APIs Dalek employées par Wallet (`SigningKey`, `VerifyingKey`, `Signer`, `Signature`) existent dans la documentation `3.0.0`, mais seule la compilation opérateur peut valider définitivement l'intégration du workspace réel.
## 12. Validation opérateur obligatoire
Après overlay :
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-wallet-lib
cargo test --workspace
cargo tree -p ksp-wallet-lib
cargo tree -p ksp-wallet-lib -d
cargo tree -i ed25519-dalek@3.0.0
cargo tree -i ed25519-dalek@2.2.0
cargo tree -i solana-keypair@3.1.2
cargo tree -i solana-address@2.7.0
```
Critères :
```text
aucun warning KSP
nouvel audit Python clean
aucune régression tests
Dalek 3.0.0 direct depuis Wallet
Dalek 2.2.x uniquement via solana-keypair 3.1.2 si l'upstream reste inchangé
solana-keypair parent KSP direct = Wallet
frontières Config/Transport/Logging/Solana inchangées
```
Si une validation échoue, produire `pre.010-fix.002` au lieu de passer à `rel.001`.
## 13. Suite
`rel.001` reste **bloqué** jusqu'à validation complète de ce fix.
Après validation seulement :
```text
0.2.5-rel.001
-> workspace.package.version = 0.2.5
-> validation finale
-> tag stable v0.2.5
-> ouverture 0.2.6 via prompts/011-V0_2_6_START_PROMPT.md
```

View File

@@ -1,5 +1,5 @@
<!-- file: docs/formats/KSPWALLET_V1.md -->
<!-- version: 9 -->
<!-- version: 10 -->
# `.kspwallet` V1 — spécification du format natif Wallet KSP
@@ -1029,7 +1029,9 @@ Cette sonde n'est pas une dépendance KSP et n'est pas requise au runtime ; elle
Le profil de création `64 MiB / 3 / 1` est cohérent avec la seconde recommandation Argon2id de RFC 9106 pour les environnements contraints. Les paramètres restent sérialisés par slot afin que les futurs defaults puissent évoluer sans rendre les wallets existants illisibles. XChaCha20-Poly1305 conserve une clé 256 bits et un nonce 192 bits généré par le CSPRNG OS. Les signatures d'état utilisent Ed25519 au format 64 octets défini par RFC 8032.
Le graphe Cargo final observé au gate `pre.009` conserve une seule génération `ed25519-dalek 2.2.0`, une seule `solana-address 2.7.0` et un unique parent KSP direct de `solana-keypair 3.1.2` : `ksp-wallet-lib`. Les doublons `digest 0.10/0.11`, `crypto-common 0.1/0.2`, `block-buffer 0.10/0.12`, `cpufeatures 0.2/0.3`, `getrandom 0.3/0.4`, `rand 0.9/0.10`, `rand_core 0.6/0.9/0.10`, `sha2 0.10/0.11` et `syn 2/3` sont transitoires et imposés par les générations actuellement consommées par RustCrypto, Solana et Logging ; KSP n'ajoute pas une deuxième dépendance directe pour les contourner.
Le graphe Cargo observé au gate historique `pre.009` conservait une seule génération `ed25519-dalek 2.2.0`. `pre.010-fix.001` met à niveau la dépendance directe KSP vers `ed25519-dalek 3.0.0`; `solana-keypair 3.1.2` restant sur Dalek `2.x`, le graphe post-fix doit donc contenir **deux générations Dalek intentionnelles** (`3.0.0` direct KSP et `2.2.0` transitive Solana), tout en conservant une seule `solana-address 2.7.0` et un unique parent KSP direct de `solana-keypair` : `ksp-wallet-lib`. Les doublons `digest 0.10/0.11`, `crypto-common 0.1/0.2`, `block-buffer 0.10/0.12`, `cpufeatures 0.2/0.3`, `getrandom 0.3/0.4`, `rand 0.9/0.10`, `rand_core 0.6/0.9/0.10`, `sha2 0.10/0.11` et `syn 2/3` restent des conséquences transitives des générations RustCrypto/Solana/Logging.
Cette évolution de dépendance d'implémentation **ne modifie aucun octet du format V1**, aucun domaine de transcript/AAD, aucune taille de clé/signature ni aucun algorithme sérialisé : Ed25519 reste Ed25519 et les fixtures/vecteurs V1 restent les contrats d'interopérabilité.
Limites explicitement conservées en V1 :

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md -->
<!-- version: 13 -->
<!-- version: 14 -->
# Plan `0.2.5` — Wallet foundation
@@ -363,7 +363,7 @@ Il faut distinguer :
**keypair Solana** : immuable dans V1 après création/import. Aucun `replace_keypair` n'est exposé. Toute tentative de modifier `secret` ou la Pubkey metadata sans une réécriture OWNER valide échoue sur la `state_signature`; même avec OWNER, l'import d'une autre keypair crée un nouveau `.kspwallet` au lieu de muter l'identité cryptographique existante.
## 8. Primitives et dépendances réauditées jusquau 2026-08-19
## 8. Primitives et dépendances réauditées jusquau 2026-08-20
Aucune dépendance ci-dessous n'est ajoutée dans `pre.001`. Ce sont les candidates pour les tranches qui les consommeront réellement.
@@ -380,7 +380,7 @@ Aucune dépendance ci-dessous n'est ajoutée dans `pre.001`. Ce sont les candida
La génération SDK récente définit `Pubkey` comme alias du type `Address`, mais l'écosystème a déjà connu des incompatibilités lorsque plusieurs générations de `solana-address` coexistent. `pre.002` verrouille donc la surface publique Wallet sur **`ksp_core_lib::Pubkey` exclusivement** et n'ajoute aucune dépendance directe `solana-pubkey`. `pre.005` introduit réellement `solana-keypair 3.1.2` uniquement pour posséder/valider la keypair Ed25519 ; la Pubkey Wallet reste construite via `ksp_core_lib::Pubkey` depuis les 32 octets publics du keypair, sans exposer un second type d'adresse KSP.
Un `cargo tree` est obligatoire avec `pre.005` afin de confirmer l'unification de `solana-address`, `ed25519-dalek`, `rand/getrandom` et d'éviter des duplications crypto injustifiées.
Un `cargo tree` est obligatoire avec `pre.005` puis à chaque changement de génération crypto afin de distinguer les convergences réellement possibles des doublons imposés par upstream. Le gate `pre.009` observait encore une génération unique `ed25519-dalek 2.2.0`; `pre.010-fix.001` réouvre explicitement cet audit après passage du Dalek direct KSP à `3.0.0`.
Audit source notable : `solana-keypair 3.1.2` contient un bloc `unsafe` interne dans sa conversion Base58 vers `String`. Cela ne modifie pas la règle `#![forbid(unsafe_code)]` du code KSP. `pre.008` conserve finalement le codec maintenu par `solana-keypair` au lieu d'ajouter un second codec `bs58` direct ; `pre.009` classe donc cet `unsafe` comme implémentation upstream auditée, sans `unsafe` KSP et sans duplication de codec.
@@ -415,7 +415,9 @@ AES-GCM-SIV apporte une meilleure tolérance à la réutilisation accidentelle d
`zeroize 1.9.0` est retenu et devient la seule nouvelle dépendance tierce de `pre.002`, car les wrappers `ViewPassword` / `OwnerPassword` lutilisent immédiatement pour leur nettoyage au `Drop`. `secrecy` n'est pas ajouté tant qu'un besoin ergonomique concret n'est pas démontré ; des types KSP simples imposent eux-mêmes redaction/non-Clone et utilisent `zeroize`.
`pre.005` introduit directement **`ed25519-dalek ^2.2`** avec `default-features = false` et les features `signature` + `zeroize`. La génération `3.0.0`, bien que plus récente, n'est volontairement pas ajoutée : `solana-keypair 3.1.2` dépend de `ed25519-dalek ^2.1.1`, donc la branche directe `2.2` permet à Cargo d'unifier une seule génération Dalek et d'activer `zeroize` sur la `SigningKey` utilisée à la fois par l'autorité de format KSP et par le wrapper Solana. Une duplication `2.x + 3.x` n'apporterait aucune capacité nécessaire à V1.
`pre.005` avait initialement introduit `ed25519-dalek ^2.2` afin de converger avec `solana-keypair 3.1.2`. Cette décision est **supersédée par `pre.010-fix.001`** : `ed25519-dalek 3.0.0` est stable depuis le 2026-07-06, utilise Rust 2024 et apporte la génération courante de l'API Dalek. KSP passe donc sa dépendance directe à **`ed25519-dalek ^3.0`**, conserve `default-features = false` et active localement uniquement `signature` + `zeroize`.
`solana-keypair 3.1.2` reste toutefois sur sa branche Dalek `2.x`. Le tree post-fix doit donc montrer **deux générations Dalek intentionnelles** : `3.0.0` directement pour KSP et `2.2.0` transitivement via `solana-keypair`. Cette duplication est désormais acceptée parce qu'elle est imposée par les contraintes upstream ; KSP ne rétrograde pas sa dépendance directe uniquement pour obtenir une unicité artificielle. Le wire `.kspwallet` V1 et les primitives Ed25519 restent inchangés.
### 8.6 État des audits de sécurité publics
@@ -1135,7 +1137,8 @@ pre.007 signature Solana + alias/notes + rotations passwords + disable/recreate
pre.008 import/export adapters + Solana CLI JSON + generic keypair Base58 + inspect
pre.009 audit security/interoperability/compliance + adversarial vectors/tests + cargo trees acquis
pre.010 spec finale + README/USAGE + graphes/docs + candidate de clôture + prompt 0.2.6 acquis
rel.001 publication strictement publicationnelle prochaine
pre.010-fix.001 Dalek direct ^3.0 + normalisation Rust workspace + audit structurel + prompt 0.2.6 renforcé en validation
rel.001 publication strictement publicationnelle après validation du fix
```
Une `fix` ou tranche supplémentaire est préférable à la suppression d'une garantie sécurité si un des gates révèle une incompatibilité.
@@ -1150,17 +1153,20 @@ pre.003 base64 ^0.23
pre.004 argon2 ^0.5
chacha20poly1305 ^0.11
getrandom ^0.4
pre.005 ed25519-dalek ^2.2
pre.005 ed25519-dalek ^2.2 à l'origine
solana-keypair ^3.1
pre.010-fix.001
ed25519-dalek ^3.0 direct KSP
tokio déjà workspace, feature locale rt pour spawn_blocking
pre.006 tempfile ^3.27
pre.007 aucune nouvelle dépendance tierce
pre.008 aucune nouvelle dépendance tierce
pre.009 aucune nouvelle dépendance tierce
pre.010 aucune nouvelle dépendance tierce (documentation uniquement)
pre.010-fix.001 mise à niveau de la dépendance directe ed25519-dalek ^3.0 ; aucune nouvelle famille de dépendance
```
Toutes les dépendances tierces communes restent centralisées sous `[workspace.dependencies]`; le membre Wallet active uniquement les features nécessaires. `ed25519-dalek ^2.2` est volontairement aligné avec la contrainte `^2.1.1` de `solana-keypair 3.1.2` afin de permettre une seule génération Dalek et d'activer `zeroize` sur la `SigningKey` partagée par résolution Cargo.
Toutes les dépendances tierces communes restent centralisées sous `[workspace.dependencies]`; le membre Wallet active uniquement les features nécessaires. À partir de `pre.010-fix.001`, le direct KSP est `ed25519-dalek ^3.0` avec `signature` + `zeroize`, tandis que `solana-keypair 3.1.2` conserve sa génération Dalek `2.x` transitive. Les deux générations sont auditées séparément et leur coexistence est acceptée tant que Solana n'a pas mig son propre contrat.
Dépendances réauditées mais **non retenues directement** en `0.2.5` :
@@ -1214,7 +1220,7 @@ https://docs.rs/chacha20poly1305/0.11.0/
https://docs.rs/aes-gcm-siv/0.12.0/
https://docs.rs/getrandom/0.4.3/
https://docs.rs/zeroize/1.9.0/
https://docs.rs/ed25519-dalek/2.2.0/
https://docs.rs/ed25519-dalek/3.0.0/
https://docs.rs/base64/0.23.1/
https://docs.rs/tempfile/3.27.0/
```

View File

@@ -1,74 +1,108 @@
<!-- file: docs/rules/PROMPT_STRUCTURE.md -->
<!-- version: 4 -->
<!-- version: 5 -->
# Structure des prompts KSP
## Portée
Ce document définit le contrat normatif des prompts de reprise KSP, leur cycle de vie et les règles de dimensionnement des releases/sessions qu'ils préparent.
Ce document définit le contrat normatif des prompts de reprise KSP, leur cycle de vie et les règles de dimensionnement des releases/sessions qu'ils préparent. Un prompt de démarrage est un **contrat opératoire autonome** : une nouvelle session ne doit pas dépendre d'une mémoire implicite pour retrouver les règles déjà acquises.
## Série, release concrète et session
Une notation de série telle que `0.1.x` regroupe des fonctionnalités apparentées. Elle n'est pas une unité de session et n'a pas à être réalisable en une seule session.
Exemple : `0.1.x` peut regrouper plusieurs releases concrètes comme `0.1.1`, `0.1.2`, `0.1.3`, chacune avec son propre cycle de prereleases et, en principe, sa propre session de travail principale.
Le contrôle de charge s'applique donc d'abord à **la release concrète préparée** et à ses prereleases, pas à toute la série fonctionnelle.
Le contrôle de charge s'applique d'abord à **la release concrète préparée** et à ses prereleases. Une release concrète doit être planifiée pour être ouverte et clôturée dans une seule session de chat ; si cette clôture paraît incertaine, elle est scindée avant l'implémentation lourde.
## Cycle d'une release concrète
Sauf raison explicitement documentée :
- `pre.001` = brainstorming/audit si nécessaire + planification + découpage de la release ;
- `pre.001` = lectures obligatoires + audit interne/externe + brainstorming + sizing + planification ;
- les prereleases intermédiaires = tranches bornées de développement/validation ;
- la dernière prerelease = validation finale + documentation + nettoyage/archivage + préparation du prompt/release suivante.
- la dernière prerelease = validation finale + documentation + nettoyage/archivage + prompt de la release suivante ;
- une `fix` corrige l'étape réellement livrée sans réécrire l'historique.
## Dimensionnement des prereleases
## Dimensionnement
Lors de `pre.001`, une prerelease intermédiaire estimée à plus d'environ **15 à 20 minutes de travail effectif de session** doit être scindée.
Lors de `pre.001`, une prerelease intermédiaire estimée à plus d'environ **15 à 20 minutes de travail effectif** est scindée. Cette durée est un budget de planification, jamais une promesse temporelle.
Cette durée est un budget de planification et non une promesse d'exécution.
La trajectoire initiale est **souple** : elle indique un nombre prévisionnel de prereleases, leur objectif et leur ordre, mais autorise l'insertion de tranches/fixes lorsqu'un audit ou une validation révèle un besoin réel. La fermeture ne doit jamais être forcée pour respecter un numéro prévu.
Si la complexité réelle augmente, la tranche est redécoupée plutôt que surchargée.
## Structure obligatoire d'un prompt de démarrage
## Dimensionnement d'une release/session
Un prompt de nouvelle session contient explicitement, dans un ordre facile à retrouver :
Avant de finaliser le prompt d'une release concrète, vérifier que son plan complet est compatible avec une session de qualité.
1. identité de la release et base exacte requise ;
2. mission et résultat attendu ;
3. **sources de vérité internes obligatoires et ordre de lecture** ;
4. sources externes normatives à réauditer lorsque la fraîcheur importe ;
5. état validé à préserver, y compris règles de dépendances et frontières ;
6. décisions acquises et questions réellement ouvertes ;
7. objectifs/livrables et hors périmètre ;
8. contraintes sécurité/API/architecture spécifiques ;
9. **première mission `pre.001` détaillée et critères de sortie de ce gate** ;
10. **prévision souple initiale des prereleases**, visible et détaillée ;
11. règles de versionnement, deltas, commits et tags ;
12. procédure d'application/validation opérateur ;
13. validations Rust/frontend/Tauri/réseau pertinentes ;
14. critères de clôture de la release ;
15. release/session suivante envisagée ;
16. **instruction d'ouverture** indiquant ce que la prochaine session doit faire en premier et ce qu'elle ne doit pas commencer avant le gate.
Si une release concrète paraît trop lourde, la scinder en plusieurs releases de la même série lorsque les fonctions restent du même groupe, ou changer de série si une frontière fonctionnelle différente le justifie.
## Sources de vérité et reprise
Exemple : si `0.1.1` devient trop large, créer `0.1.2` plutôt que forcer tout `0.1.x` dans une seule session.
- Le prompt ordonne explicitement la lecture de `RULES.md`, `docs/000-README.md`, règles spécialisées, architecture, plan/validation de la release précédente et sources métier pertinentes.
- Lorsqu'une archive/base opérateur est fournie, elle est déclarée autoritaire par rapport aux souvenirs, snippets ou artefacts anciens.
- Le prompt ne recopie pas toutes les règles, mais rappelle les règles qui conditionnent directement la session et exige la lecture des fichiers normatifs.
- Les décisions historiques ne sont pas réinventées lors de la nouvelle session ; une divergence avec la base réelle déclenche un audit, pas une supposition.
Une série complète peut naturellement s'étendre sur de nombreuses sessions.
## Force opératoire du `pre.001`
Une **release concrète**, en revanche, doit être planifiée pour être ouverte et clôturée dans une seule session de chat. Une version volontairement laissée ouverte pour être reprise dans une autre session n'est pas un découpage acceptable.
Le prompt doit rendre impossible une interprétation « commencer à coder immédiatement ». La première tranche exige au minimum :
Si `pre.001` révèle qu'une clôture dans la session est incertaine, la release est scindée **avant l'implémentation fonctionnelle lourde**. Cette contrainte s'ajoute au budget de 1520 minutes par prerelease ; elle ne le remplace pas.
```text
lecture des sources internes obligatoires
inventaire de l'état réel de la base
récupération des dépendances/versions actuelles si concernées
comparaison avec les références historiques utiles
brainstorming des risques/frontières
sizing de la release et de chaque tranche
prévision souple recalibrée
liste des validations/gates
```
## Structure recommandée d'un prompt
L'implémentation fonctionnelle lourde commence seulement lorsque ce gate est cohérent. Les corrections triviales nécessaires pour rendre l'audit lui-même exécutable restent autorisées.
1. Identité de la série et de la release concrète visée ;
2. Mission ;
3. Base requise ;
4. État validé à préserver ;
5. Sources de vérité internes ;
6. Sources externes normatives ;
7. Décisions acquises ;
8. Objectifs et livrables ;
9. Hors périmètre ;
10. Méthode de travail ;
11. Versionnement/deltas/commits ;
12. Contraintes techniques spécifiques ;
13. Plan initial souple et prereleases bornées ;
14. Validations attendues ;
15. Critères de sortie ;
16. Préparation de la release/session suivante.
## Validation Rust obligatoire dans les prompts
Toute release susceptible de modifier du Rust rappelle explicitement la séquence KSP :
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
```
Pendant le développement, les tests ciblés suivent ces contrôles. La fermeture d'une prerelease technique/release inclut `cargo test --workspace` et les `cargo tree` pertinents lorsque le graphe de dépendances a changé ou constitue un gate de la tranche.
Un prompt interdit de déclarer réussie une commande qui n'a pas réellement été exécutée. Il rappelle que `rustfmt` et Clippy ne remplacent pas l'audit structurel KSP.
## Dépendances et fraîcheur
Lorsqu'une tranche ajoute, met à jour ou dépend fortement d'une bibliothèque externe :
- vérifier la version stable réellement courante au début de la tranche ;
- préférer les sources primaires de la crate/projet ;
- vérifier les features réellement nécessaires et les contraintes de versions transitives ;
- documenter les doublons transitifs acceptés plutôt que forcer artificiellement une unification incompatible ;
- ne pas conserver dans un prompt une version historique comme si elle était nécessairement toujours actuelle.
## Règles de rédaction
- Le prompt référence les sources canoniques au lieu de recopier inutilement leur contenu.
- Une règle normative appartient à `docs/rules/`.
- Une décision durable appartient à `docs/architecture/`.
- Une idée non décidée appartient à `docs/IDEAS.md`.
- Un prompt doit signaler les questions ouvertes plutôt que les résoudre arbitrairement.
- Une release concrète manifestement surdimensionnée doit être redécoupée avant ouverture de sa session principale.
- Le prompt doit être autoportant sans devenir une copie intégrale des règles.
- Les titres `Première mission`, `Prévision souple`, `Validation` et `Instruction d'ouverture` doivent être immédiatement repérables.
- Une règle normative appartient à `docs/rules/`, une décision durable à `docs/architecture/`, une idée non décidée à `docs/IDEAS.md`.
- Le prompt signale les questions ouvertes au lieu de les résoudre arbitrairement.
- Une release surdimensionnée est redécoupée avant ouverture de son développement lourd.
- Le prompt conserve les conventions KSP de delta minimal, version de fichiers, Cargo version technique, commits et tags stables.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/rules/RULES_KSP.md -->
<!-- version: 34 -->
<!-- version: 35 -->
# Règles spécifiques à KSP
@@ -252,7 +252,7 @@
- **KSP-REL-006** — Une release ou prerelease trop grosse est scindée plutôt que compressée pour respecter un numéro prévu.
- **KSP-REL-007** — La dernière prerelease d'une release fonctionnelle réalise par défaut validations finales, documentation, nettoyage/archivage, changelog et prompt de la release suivante.
- **KSP-REL-008** — La première release fonctionnelle sélectionnée est `0.1.1`, dédiée à la stabilisation de `ksp-core-lib`.
- **KSP-REL-009** — Après toute modification d'un fichier Rust, la tranche concernée exécute avant livraison au minimum `cargo fmt --all`, `cargo check --workspace` et `cargo clippy --workspace --all-targets`. Une commande non exécutée n'est jamais déclarée réussie.
- **KSP-REL-009** — Après toute modification d'un fichier Rust, la tranche concernée exécute avant livraison au minimum `cargo fmt --all`, `python3 scripts/audit_rust_workspace_rules.py`, `cargo check --workspace` et `cargo clippy --workspace --all-targets`, dans cet ordre. L'audit structurel complète rustfmt/Clippy et ne peut être ignoré parce que la compilation est verte. Une commande non exécutée n'est jamais déclarée réussie.
- **KSP-REL-010** — Pendant le développement courant d'une crate, les tests Rust privilégient la portée ciblée `cargo test -p <crate>` et, si utile, ses tests d'intégration nommés. `cargo test --workspace` est conservé pour l'ouverture ou la fermeture d'une session/version et pour les validations globales explicitement justifiées.
- **KSP-REL-011** — Les validations d'une crate incluent les audits `cargo tree` pertinents pour son graphe réel : arbre normal, doublons et features lorsque ces vues apportent une information utile. Les crates fondamentales/complétées conservent leurs canaries de dépendances.
- **KSP-REL-012** — Toute crate ou composant KSP considéré comme complété possède un `README.md` descriptif durable avant clôture de sa release.

View File

@@ -1,11 +1,11 @@
<!-- file: docs/rules/RULES_RUST.md -->
<!-- version: 5 -->
<!-- version: 6 -->
# Règles Rust générales
## Portée
Les règles `RUST-*` s'appliquent aux crates, sources, tests, exemples et outils Rust de KSP. Elles sont conçues pour rester réutilisables hors de KSP lorsque la règle ne dépend pas de son architecture.
Les règles `RUST-*` s'appliquent à tous les fichiers Rust de KSP : crates, sources, tests, exemples et outils. Elles reprennent les règles déjà éprouvées sur les générations précédentes du projet et les rendent explicites afin que `rustfmt` et Clippy ne soient jamais considérés comme des contrôles structurels suffisants.
## Édition et lints
@@ -13,33 +13,54 @@ Les règles `RUST-*` s'appliquent aux crates, sources, tests, exemples et outils
- **RUST-BASE-002** — Chaque `lib.rs` et `main.rs` contient `#![warn(missing_docs)]`, `#![deny(unreachable_pub)]` et `#![forbid(unsafe_code)]`.
- **RUST-BASE-003** — Les lints communs sont déclarés au niveau workspace et hérités par les crates.
- **RUST-BASE-004** — Le code `unsafe` est interdit.
- **RUST-BASE-005** — Tout fichier Rust possède les en-têtes `// file: ...` et `// version: N`, et se termine par exactement une fin de ligne.
## Documentation des API
## Documentation des API et contrats internes
- **RUST-DOC-001** — Tout élément `pub` ou `pub(crate)` possède une rustdoc utile au point de déclaration.
- **RUST-DOC-002** — Toute réexportation `pub use` ou `pub(crate) use` dans `lib.rs` ou `main.rs` possède également une rustdoc utile.
- **RUST-DOC-003** — La façade d'une crate doit permettre de comprendre son API sans dépendre de ses chemins de modules internes.
- **RUST-DOC-001** — Tout élément `pub` ou `pub(crate)` possède une rustdoc utile au point de déclaration. Cette règle couvre aussi les méthodes associées et les champs nommés visibles à ces niveaux.
- **RUST-DOC-002** — Toute réexportation `pub use` ou `pub(crate) use` dans `lib.rs` ou `main.rs` possède sa propre rustdoc utile et adjacente.
- **RUST-DOC-003** — La façade d'une crate doit permettre de comprendre son API publique et ses contrats crate-wide sans dépendre de chemins de modules internes.
- **RUST-DOC-004** — Les rustdocs et commentaires du code sont en anglais ; la documentation Markdown KSP reste en français sauf document explicitement destiné à une audience différente.
## Imports et chemins
- **RUST-IMPORT-001** — `use` est interdit pour les constantes, fonctions, structures, énumérations, unions, alias de types, modules et macros.
- **RUST-IMPORT-002** — `use` est autorisé uniquement pour un trait lorsque la résolution de méthode, une macro de dérivation ou une contrainte du langage l'exige réellement.
- **RUST-IMPORT-003** — Un import de trait reste étroit et ne mélange pas de non-traits dans un import groupé.
- **RUST-IMPORT-002** — `use` est autorisé uniquement pour un trait lorsqu'une résolution de méthode, une dérivation ou une contrainte du langage l'exige réellement. Le motif est explicité sur la ligne par `rust-rules: trait-import`.
- **RUST-IMPORT-003** — Les imports de traits restent étroits ; un import de trait ne mélange jamais d'éléments non-traits.
- **RUST-IMPORT-004** — Les glob imports et imports groupés par accolades sont interdits.
- **RUST-IMPORT-005** — Pour un élément externe, utiliser directement le chemin public le plus court fourni par la crate propriétaire.
- **RUST-IMPORT-006** — Pour un élément `pub` d'une crate KSP, l'API est réexportée au crate-root et consommée depuis une autre crate via `owner_crate::Item`.
- **RUST-IMPORT-007** — Dans sa propre crate, un élément `pub` ou `pub(crate)` partagé est consommé via `crate::Item` après réexport approprié.
- **RUST-IMPORT-008** — Un helper strictement privé au module reste privé et est appelé localement ; dans un sous-module de tests, un élément privé du module parent est appelé via `super::Item`.
- **RUST-IMPORT-009** — Les réexports ne sont pas groupés par accolades et les alias `as` sont interdits dans les réexports.
- **RUST-IMPORT-010** — Un réexport d'un module interne commence par `self::`.
- **RUST-IMPORT-005** — Les alias `as` sont interdits dans tous les `use`, réexports et déclarations `extern crate`. Le symbole reçoit son nom canonique dans son module propriétaire.
- **RUST-IMPORT-006** — Les déclarations `use` sont au niveau du module, dans le bloc d'en-tête avant les déclarations métier. Aucun `use` n'est introduit dans une fonction, méthode, bloc, test ou branche conditionnelle.
- **RUST-IMPORT-007** — Pour un élément fourni par une crate externe, utiliser directement le chemin public le plus court exposé par cette crate ; ne pas créer un import non-trait pour raccourcir ce chemin.
- **RUST-IMPORT-008** — Pour un élément `pub` d'une crate KSP, la crate propriétaire le réexporte au crate-root. Une autre crate l'appelle via `owner_crate::Item`.
- **RUST-IMPORT-009** — Dans sa propre crate, un élément `pub` ou `pub(crate)` partagé est appelé via `crate::Item`, y compris depuis son module de déclaration lorsque le contrat est crate-wide.
- **RUST-IMPORT-010** — Un chemin `crate::module::Item` est interdit pour un élément partagé `pub`/`pub(crate)` qui peut être consommé via le crate-root. Le chemin du module interne n'est pas une façade.
- **RUST-IMPORT-011** — Un élément strictement privé à un module n'est pas réexporté et est appelé par son nom local dans ce module.
- **RUST-IMPORT-012** — Dans un sous-module de tests, `super::Item` est réservé à un élément strictement privé du module parent. Un élément `pub` ou `pub(crate)` continue d'être appelé via `crate::Item`.
- **RUST-IMPORT-013** — Les réexports internes commencent par `self::`. Un réexport d'une crate externe peut utiliser directement le chemin externe canonique.
- **RUST-IMPORT-014** — Un export correspond à une ligne de réexport distincte ; les accolades ne servent jamais à regrouper une façade.
- **RUST-IMPORT-015** — Une crate externe rendue publique uniquement pour l'hygiène d'une macro exportée conserve son nom canonique, porte `#[doc(hidden)]` et ne devient pas une API de consommation. `ksp-logging-lib::tracing` est ce bridge technique pour les macros Logging ; les autres crates KSP n'y accèdent jamais directement.
## Visibilité et façade de crate
- **RUST-API-001** — Aucun `pub mod` n'est autorisé ; les modules restent privés et l'API externe est constituée par des réexports explicites au crate-root.
- **RUST-API-001** — Aucun `pub mod` n'est autorisé ; les modules restent privés et l'API externe est constituée exclusivement par des réexports explicites au crate-root.
- **RUST-API-002** — `pub(in ...)` et `pub(super)` sont interdits.
- **RUST-API-003** — Un élément `pub` inaccessible depuis la façade de sa crate est une erreur de conception.
- **RUST-API-004** — Un élément `pub(crate)` utilisé hors de son module est réexporté au crate-root via `pub(crate) use`.
- **RUST-API-005** — Les chemins internes de modules ne constituent pas une API stable.
- **RUST-API-004** — Un élément `pub(crate)` consommé hors de son module est réexporté au crate-root via `pub(crate) use` puis appelé via `crate::Item`.
- **RUST-API-005** — Les chemins internes de modules ne constituent jamais une API stable.
- **RUST-API-006** — Si deux éléments crate-wide auraient le même nom au crate-root, ils sont renommés dans leurs modules propriétaires avec des noms canoniques non ambigus ; un alias de réexport n'est pas utilisé pour masquer la collision.
## Formatage, blocs et ordre
- **RUST-FMT-001** — `rustfmt.toml` à la racine est la configuration canonique du formatage Rust.
- **RUST-FMT-002** — `cargo fmt --all` est exécuté après chaque modification Rust avant l'audit structurel et les validations Cargo.
- **RUST-FMT-003** — Aucune ligne vide n'est conservée à l'intérieur du corps d'une fonction ou méthode, ni à l'intérieur d'une définition `struct` ou `enum`.
- **RUST-FMT-004** — Les lignes vides séparent les fonctions/méthodes, types, blocs `impl` et grandes sections logiques ; elles ne fragmentent pas un bloc homogène de déclarations.
- **RUST-FMT-005** — Les blocs homogènes sont ordonnés alphabétiquement par nom lorsque l'ordre n'a pas de signification sémantique.
- **RUST-FMT-006** — Les constantes de module forment des blocs sans ligne vide interne, ordonnés par visibilité `pub`, puis `pub(crate)`, puis privée, et alphabétiquement dans chaque niveau.
- **RUST-FMT-007** — Dans une façade, le bloc `pub use` précède le bloc `pub(crate) use`; une seule ligne vide sépare les deux blocs, aucune ligne vide n'existe à l'intérieur d'un bloc, et les symboles sont ordonnés alphabétiquement.
- **RUST-FMT-008** — Les déclarations `mod` d'un même bloc sont ordonnées alphabétiquement ; les modules de tests conditionnels restent après les modules de production.
- **RUST-FMT-009** — Lorsqu'un `struct` et ses blocs `impl` forment une unité locale sans contrainte de séparation, l'implémentation suit la structure. Les éléments visibles précèdent les helpers privés lorsqu'aucun ordre métier ou protocolaire n'impose l'inverse.
- **RUST-FMT-010** — L'ordre alphabétique n'écrase jamais un ordre contractuel ou sémantique : wire fields, comptes Solana, étapes de protocole, priorités, transitions d'état, tableaux de dispatch et séquences explicitement normatives conservent leur ordre défini.
## Contrôle de flux et erreurs
@@ -49,15 +70,7 @@ Les règles `RUST-*` s'appliquent aux crates, sources, tests, exemples et outils
- **RUST-ERR-004** — `anyhow` et `thiserror` ne sont pas utilisés par défaut ; leur introduction exige une justification architecturale.
- **RUST-ERR-005** — Les erreurs publiques sont typées lorsque leur contrat est stable.
- **RUST-ERR-006** — Les tests peuvent utiliser `unwrap` ou `expect` uniquement dans la limite explicitement autorisée par la configuration Clippy.
- **RUST-ERR-007** — Avant dattacher une erreur externe comme `source` de `ksp_core_lib::Error`, la crate propriétaire vérifie que sa chaîne `Debug`/`source` ne peut pas exposer de secret ou de donnée sensible. Lorsquune dépendance fournit une primitive de neutralisation, elle est appliquée avant `with_source`; en particulier une `reqwest::Error` issue dune requête vers un endpoint potentiellement credential-bearing est attachée uniquement après `without_url()`.
## Formatage
- **RUST-FMT-001** — `rustfmt.toml` à la racine est la configuration canonique du formatage Rust.
- **RUST-FMT-002** — `cargo fmt --all` est exécuté après chaque modification Rust avant les validations de compilation et de test.
- **RUST-FMT-003** — Les fichiers Rust ne contiennent pas de lignes vides à l'intérieur d'une fonction, structure, énumération ou implémentation courte.
- **RUST-FMT-004** — Les lignes vides séparent uniquement des fonctions, types, blocs `impl` ou sections logiques distinctes.
- **RUST-FMT-005** — Les groupes homogènes de `pub use` ou `pub(crate) use` ne contiennent pas de ligne vide interne ; `pub use` précède `pub(crate) use` lorsqu'ils coexistent.
- **RUST-ERR-007** — Avant d'attacher une erreur externe comme `source` de `ksp_core_lib::Error`, la crate propriétaire vérifie que sa chaîne `Debug`/`source` ne peut pas exposer de secret. Lorsqu'une dépendance fournit une primitive de neutralisation, elle est appliquée avant `with_source`.
## Helpers et duplication
@@ -71,31 +84,40 @@ Les règles `RUST-*` s'appliquent aux crates, sources, tests, exemples et outils
- **RUST-DEP-002** — Une dépendance uniquement utilisée par les tests reste dans `[dev-dependencies]`.
- **RUST-DEP-003** — Les features sont minimales et explicites.
- **RUST-DEP-004** — Les lockfiles de dépendances ne sont pas versionnés dans KSP.
- **RUST-DEP-005** — KSP privilégie les versions récentes compatibles. Toute version volontairement contrainte ou ancienne doit être documentée avec sa raison et la condition permettant de lever la contrainte.
- **RUST-DEP-005** — KSP privilégie les versions récentes compatibles. Toute version volontairement contrainte ou ancienne est documentée avec sa raison et la condition permettant de lever la contrainte.
## Assertions de collections
## Assertions et organisation des tests
- **RUST-TEST-001** — Un test vérifiant uniquement l'appartenance à un ensemble peut trier les valeurs obtenues et attendues avant `assert_eq!` lorsque l'ordre n'est pas contractuel.
- **RUST-TEST-002** — Une collection n'est jamais triée artificiellement dans un test lorsque l'ordre fait partie du contrat, notamment pour comptes Solana, metas, instructions, signers, événements, étapes de pipeline, priorités ou journaux ordonnés.
- **RUST-TEST-002** — Une collection n'est jamais triée artificiellement lorsque l'ordre fait partie du contrat, notamment pour comptes Solana, metas, instructions, signers, événements, étapes de pipeline, priorités ou journaux ordonnés.
- **RUST-TEST-003** — Les tests unitaires sont placés autant que possible dans `unit_tests/` et reflètent l'arborescence de `src/`.
- **RUST-TEST-004** — Un fichier `unit_tests/...` est rattaché explicitement au module de production via `#[cfg(test)]` et `#[path = ...]`; il peut tester les éléments privés de ce module.
- **RUST-TEST-005** — `tests/` est réservé aux vrais tests d'intégration consommant uniquement l'API publique de la crate.
- **RUST-TEST-006** — Toute partie pertinente de l'API publique est couverte, lorsque possible, par un test d'intégration afin de valider les réexports et la visibilité réellement consommables.
- **RUST-TEST-007** — La colocalisation d'un test unitaire dans le fichier de production reste une exception justifiée.
## Organisation des tests
## Audit structurel automatisé
- **RUST-TEST-003** — Les sources de tests unitaires sont placées autant que possible dans un répertoire `unit_tests/` à la racine de la crate et reflètent autant que possible l'arborescence et les noms des modules sous `src/`.
- **RUST-TEST-004** — Un fichier sous `unit_tests/` reste un vrai test unitaire : il est rattaché explicitement sous `#[cfg(test)]` au module de production qu'il teste et peut donc tester ses éléments privés.
- **RUST-TEST-005** — Le répertoire Cargo standard `tests/` est réservé aux tests d'intégration. Les fichiers qui y sont découverts comme cibles d'intégration testent la crate comme un consommateur externe et n'utilisent que son API publique.
- **RUST-TEST-006** — Toute partie pertinente de l'API publique d'une bibliothèque KSP doit, lorsque cela est techniquement possible, être couverte par de vrais tests d'intégration sous `tests/`, afin de valider notamment les réexports, la visibilité et le contrat réellement consommable depuis une autre crate.
- **RUST-TEST-007** — La séparation physique sous `unit_tests/` est la convention préférée et doit être utilisée au maximum. La colocalisation d'un test unitaire dans le fichier/module de production n'est autorisée qu'en dernier recours lorsqu'un rattachement externe serait techniquement impossible, incorrect ou créerait une complexité disproportionnée clairement justifiable.
- **RUST-TEST-008** — Un test unitaire séparé suit autant que possible le même chemin relatif et le même nom que le module testé, afin que la correspondance `src/...``unit_tests/...` soit immédiatement identifiable.
- **RUST-AUDIT-001** — `scripts/audit_rust_workspace_rules.py` est le point d'entrée obligatoire de l'audit Rust KSP. Il exécute les audits généraux, la complétude des réexports/chemins et les frontières KSP sans fusionner leurs responsabilités.
- **RUST-AUDIT-002** — L'audit est dependency-free côté Python standard et échoue avec un code non nul dès qu'une violation mécanique est détectée.
- **RUST-AUDIT-003** — L'audit contrôle au minimum : headers/version/newline, lints de crate-root, visibilité interdite, `use`/aliases/groupes/globs/scope, rustdocs visibles, ordre des imports/réexports/constantes, lignes vides dans fonctions/structs/enums, complétude des réexports crate-root, chemins `crate::module::Item`, usage de `super::` dans les tests séparés et frontières KSP directement vérifiables.
- **RUST-AUDIT-004** — Les règles contextuelles qui ne peuvent pas être prouvées sans interpréter la sémantique restent des critères de revue humaine ; le script ne doit pas produire de faux sentiment de complétude.
## Contrôle de clôture Rust
## Contrôle avant livraison
Lorsqu'une tranche contient du Rust, les contrôles minimaux sont :
Après toute modification Rust, la séquence minimale est :
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo test --workspace
cargo clippy --workspace --all-targets
```
Les audits structurels automatisés seront ajoutés lorsqu'ils existeront dans KSP ; une règle ne doit pas prétendre qu'un script inexistant a été exécuté.
Pendant le développement, des tests ciblés suivent ce gate. À la fermeture d'une prerelease technique/release, exécuter également :
```bash
cargo test --workspace
```
Une commande non exécutée n'est jamais déclarée réussie. `rustfmt`, `cargo check` et Clippy ne remplacent pas l'audit structurel KSP.

View File

@@ -1,11 +1,11 @@
<!-- file: docs/validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md -->
<!-- version: 3 -->
<!-- version: 4 -->
# Validation `0.2.5` — Wallet security / interoperability / compliance
## 1. Objet
Cette matrice constitue la validation durable de clôture de `0.2.5`. `pre.009` ferme le gate technique security/interoperability/compliance et `pre.010` y ajoute les preuves opérateur et le gate documentaire final. Elle ne remplace ni la spécification [`../formats/KSPWALLET_V1.md`](../formats/KSPWALLET_V1.md), ni le threat model du plan [`../plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md`](../plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md), ni les deltas.
Cette matrice constitue la validation durable de clôture de `0.2.5`. `pre.009` a fermé le premier gate technique security/interoperability/compliance et `pre.010` a ajouté le gate documentaire. `pre.010-fix.001` rouvre volontairement le gate technique pour la mise à niveau `ed25519-dalek 3.0.0` et la normalisation Rust workspace-wide avant toute publication stable. Elle ne remplace ni la spécification [`../formats/KSPWALLET_V1.md`](../formats/KSPWALLET_V1.md), ni le threat model du plan [`../plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md`](../plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md), ni les deltas.
Le verdict recherché porte sur quatre axes :
@@ -268,8 +268,51 @@ aucun fichier Rust ni manifest Cargo n'est modifié par pre.010
workspace.package.version reste 0.2.5-pre.9 car pre.010 est doc-only
```
## 12. Verdict final candidate `0.2.5`
## 12. Correctif technique `pre.010-fix.001`
Le verdict security/interoperability/compliance est **positif**. Le checkpoint opérateur `pre.009` est vert et n'impose aucune remédiation de code, de dépendance, de wire ou de cryptographie. `pre.010` reste donc strictement documentaire et nintroduit aucun changement technique.
Le correctif de clôture apporte deux changements qui exigent une nouvelle validation opérateur :
Après validation du delta documentaire, la suite est `0.2.5-rel.001`, strictement publicationnelle : version Cargo stable `0.2.5`, statut stable/CHANGELOG/delta de publication, validations finales puis tag Git `v0.2.5`. Aucune nouvelle fonctionnalité Wallet n'est attendue dans `rel.001`.
```text
ed25519-dalek direct KSP : ^2.2 -> ^3.0
normalisation Rust workspace + scripts/audit_rust_workspace_rules.py obligatoire
```
Le résultat `pre.009` des sections 6 et 10 reste une preuve historique valide pour l'état `2.2.0`; il ne doit pas être présenté comme le tree final après le fix. `solana-keypair 3.1.2` conserve actuellement sa branche Dalek `2.x`, donc le tree attendu après le fix est :
```text
ed25519-dalek 3.0.0
<- ksp-wallet-lib direct
ed25519-dalek 2.2.0
<- solana-keypair 3.1.2
<- ksp-wallet-lib
solana-address 2.7.0
<- solana-keypair 3.1.2
<- solana-pubkey 4.3.0 via ksp-core-lib
```
Gate opérateur post-fix :
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-wallet-lib
cargo test --workspace
cargo tree -p ksp-wallet-lib
cargo tree -p ksp-wallet-lib -d
cargo tree -i ed25519-dalek@3.0.0
cargo tree -i ed25519-dalek@2.2.0
cargo tree -i solana-keypair@3.1.2
cargo tree -i solana-address@2.7.0
```
Le fix ne modifie ni le wire `.kspwallet` V1, ni les paramètres Argon2/XChaCha, ni la sémantique Ed25519, ni les capabilities VIEW/OWNER. Le verdict final reste **en attente de ce checkpoint opérateur**.
## 13. Verdict final candidate `0.2.5`
Le verdict `pre.009` était positif pour l'état alors audité, mais **la candidate stable n'est plus considérée fermée tant que `pre.010-fix.001` n'a pas passé son gate opérateur**. Le changement de génération Dalek et la normalisation Rust sont intentionnels et ne changent pas le wire V1, mais ils constituent un changement technique réel qui doit être compilé, linté et testé dans l'environnement opérateur.
Après validation complète de `pre.010-fix.001`, le verdict peut redevenir positif et seulement alors `0.2.5-rel.001` redevient la prochaine étape publicationnelle.

View File

@@ -1,5 +1,5 @@
<!-- file: prompts/011-V0_2_6_START_PROMPT.md -->
<!-- version: 1 -->
<!-- version: 2 -->
# Prompt de démarrage `0.2.6` — Wallet Desk
@@ -91,7 +91,112 @@ strong disable/recreate VIEW
Ces capacités appartiennent déjà à Wallet ; l'application ne doit jamais les réimplémenter. `pre.001` décide leur découpage exact après sizing, mais ne réduit pas le MVP en dessous de l'ouverture VIEW/OWNER + identité autorisée + balance réseau.
## 3. Frontières architecturales
## 3. Contrat de session et sources de vérité obligatoires
Cette nouvelle session doit être autonome : **ne pas reconstruire les règles de mémoire, ne pas improviser les conventions et ne pas commencer par coder**.
La source de vérité est, dans cet ordre :
1. l'archive/repository opérateur réellement fourni pour la base stable `v0.2.5` ;
2. les règles KSP versionnées ;
3. les documents d'architecture/plans/validation actuels ;
4. les contrats publics réellement présents dans les crates ;
5. les sources externes officielles actuelles lorsque le comportement d'une dépendance ou de Tauri/Solana doit être vérifié ;
6. les anciennes générations bot3 uniquement comme référence historique, jamais comme autorité supérieure aux règles KSP actuelles.
Avant toute modification, relire au minimum, **dans cet ordre** :
```text
RULES.md
docs/000-README.md
docs/rules/RULES_GENERAL.md
docs/rules/RULES_KSP.md
docs/rules/RULES_RUST.md
docs/rules/RULES_DEPENDENCIES.md
docs/rules/PROMPT_STRUCTURE.md
docs/rules/VERSION_WORKFLOW.md
docs/rules/FILE_CONTRACTS.md
docs/architecture/002-LAYERS_AND_DEPENDENCIES.md
docs/architecture/003-COMPONENT_CONTRACTS.md
docs/architecture/004-COMPONENT_INVENTORY.md
docs/architecture/005-DEPENDENCY_GRAPH.md
docs/architecture/010-APPS_SERVICES_SCENARIOS_AND_CONTROL.md
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
docs/plans/007-V0_2_0_SERIES_PLANNING.md
docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md
docs/formats/KSPWALLET_V1.md
docs/validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md
crates/ksp-wallet-lib/README.md
crates/ksp-wallet-lib/USAGE.md
```
Réauditer ensuite les surfaces réellement consommées dans :
```text
crates/ksp-core-lib
crates/ksp-logging-lib
crates/ksp-config-lib
crates/ksp-onchain-transport-lib
crates/ksp-wallet-lib
crates/ksp-app-config-desk
```
Ne jamais supposer une API ou une convention parce qu'elle existait dans une session précédente. Si le repository réel contredit ce prompt, **le repository + les règles versionnées priment**, et l'écart est signalé avant de bâtir dessus.
## 4. Première mission `pre.001` — méthode d'ouverture obligatoire
`0.2.6-pre.001` est d'abord une tranche d'**audit, conception, inventaire et sizing**. L'implémentation UI lourde ne commence pas avant son gate.
Ordre de travail obligatoire :
```text
1. relire les sources internes obligatoires
2. vérifier la base stable et les versions réellement présentes
3. exécuter l'audit Rust structurel existant avant modification
4. auditer Config Desk comme référence Tauri sans le copier aveuglément
5. auditer les APIs publiques finales Config / Wallet / Transport / Logging
6. vérifier sur les sources officielles actuelles les dépendances Tauri/frontend réellement nécessaires
7. produire screen map + command/DTO map + threat model + matrice Config/composition
8. dimensionner les prereleases
9. seulement ensuite commencer l'implémentation de `pre.002` ou la petite fondation explicitement validée par le gate
```
Le premier audit Rust de la session doit inclure :
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
```
Une commande non exécutée n'est jamais déclarée réussie. Un warning, une exception globale ou une violation de règles n'est pas accepté comme « assez bon » pour avancer.
## 5. Prévision souple initiale des prereleases
Point de départ à réviser par `pre.001` :
```text
pre.001 audit/sizing/UX/Config/bridge/security
pre.002 crate Tauri + frontend shell + logging + splash/main
pre.003 std.wallet + composite Wallet Desk + adapters Config
pre.004 inventory/selection + locked inspection + DTOs
pre.005 create/import + unlock VIEW/OWNER + lifecycle
pre.006 wallet details + balance HTTP + refresh/diagnostics
pre.007 metadata administration + rotations VIEW/OWNER
pre.008 strong VIEW disable/recreate + export OWNER + security UX
pre.009 integration tests + Devnet smoke + compliance/security review
pre.010 README/USAGE/build final + prompt 0.2.7 si nécessaire
rel.001 publication stable
```
Cette séquence est un **forecast souple**, pas un engagement de clôture au numéro `pre.010`. `pre.001` doit la recalibrer avec un sizing concret. Si une tranche révèle un manque, insérer une prerelease ou un `fix` est préférable à forcer la clôture. Le numéro de tranche n'a jamais priorité sur la complétude du contrat.
## 6. Frontières architecturales
Direction autorisée :
@@ -121,7 +226,7 @@ frontend -X-> localStorage/sessionStorage pour passwords
L'application compose les couches ; elle ne déplace aucune logique métier ou cryptographique dans les commands Tauri ou TypeScript.
## 4. Config et composition
## 7. Config et composition
`0.2.6-pre.001` doit réauditer la surface Config existante avant de créer un document.
@@ -157,7 +262,7 @@ transport
Le nom exact des `component_id`, filenames et profils doit être fixé par `pre.001`/la tranche Config correspondante, pas inventé dans le frontend.
## 5. Wallet lifecycle dans l'application
## 8. Wallet lifecycle dans l'application
Le runtime Rust doit posséder les handles `WalletView` / `WalletOwner`. Le frontend ne reçoit que des DTO sûrs.
@@ -193,7 +298,7 @@ Les handles OWNER/VIEW ne doivent pas être sérialisés ou copiés dans le fron
Le lock explicite doit supprimer le handle Rust possédé par l'application et purger les projections frontend protégées.
## 6. Passwords et secrets
## 9. Passwords et secrets
Les passwords OWNER/VIEW sont des entrées éphémères :
@@ -215,7 +320,7 @@ Interdictions :
Les exports OWNER sont des opérations explicitement privilégiées et doivent utiliser une UX de confirmation intégrée.
## 7. Balance réseau
## 10. Balance réseau
Le cas réseau minimal est :
@@ -237,7 +342,7 @@ Le balance refresh doit :
- ne pas inventer de cache réseau dans Wallet ;
- préserver les règles de redaction URL/credentials de Transport.
## 8. UI/UX à auditer en `pre.001`
## 11. UI/UX à auditer en `pre.001`
Évaluer au minimum les vues/flows suivants :
@@ -261,7 +366,7 @@ Le sizing décide si tous ces flows tiennent dans `0.2.6` sans dégrader la qual
Les interactions destructives ou privilégiées utilisent des modals Bootstrap intégrés, pas `window.alert`, `window.confirm` ou `window.prompt`.
## 9. Référence Tauri existante
## 12. Référence Tauri existante
`ksp-app-config-desk` est le template principal à réauditer avant création :
@@ -284,7 +389,7 @@ Le port de développement doit être réservé distinctement de Config Desk apr
Le build frontend standalone `npm run check` reste un type-check/lint/test, pas un production build. Le build production final est déclenché par Tauri via `beforeBuildCommand`.
## 10. Logging
## 13. Logging
L'application doit rester dans l'écosystème `tracing` KSP :
@@ -300,7 +405,7 @@ Le bridge frontend -> Rust doit reprendre/refondre le pattern validé de Config
Les fichiers logs restent uniques par lancement selon la politique KSP existante.
## 11. DTOs Tauri
## 14. DTOs Tauri
Les commands Tauri doivent exposer des DTOs applicatifs sûrs, pas les types internes Wallet complexes lorsque cela élargit la surface inutilement.
@@ -323,7 +428,7 @@ Les noms sont indicatifs ; `pre.001` doit les rationaliser. Les DTOs ne contienn
TS-RS reste une frontière applicative Tauri. Ne pas ajouter des dérivations TS-RS dans `ksp-wallet-lib` uniquement pour cette app.
## 12. Inventory et chemins Wallet
## 15. Inventory et chemins Wallet
Wallet core reçoit toujours un chemin explicite. Si Wallet Desk affiche un inventaire de fichiers sous un répertoire configuré, cette énumération est une responsabilité de composition/application, pas une raison d'ajouter Config ou directory discovery dans `ksp-wallet-lib`.
@@ -339,7 +444,7 @@ L'audit doit fixer :
Aucun alias ne doit être dérivé du filename ; l'alias reste metadata protégée interne au wallet.
## 13. Administration OWNER
## 16. Administration OWNER
Les opérations OWNER existent déjà et doivent être appelées directement :
@@ -360,7 +465,7 @@ Le frontend doit refléter les permissions : les controls OWNER ne sont pas affi
Un `wallet.state_conflict` doit déclencher un message de conflit/refresh explicite, pas une réécriture forcée ou un retry aveugle.
## 14. Import/export
## 17. Import/export
Formats acquis :
@@ -386,7 +491,7 @@ OWNER uniquement
Le frontend ne doit pas recevoir le secret si un export fichier peut être effectué entièrement côté Rust. Un éventuel export texte/copie clipboard doit être traité comme une nouvelle surface de fuite et n'est pas supposé requis par défaut.
## 15. Tests
## 18. Tests
### 15.1 Rust déterministe
@@ -428,7 +533,7 @@ Config composite Devnet
Le smoke ne doit pas utiliser un secret de production ni exiger un wallet financé.
## 16. Sécurité desktop
## 19. Sécurité desktop
Préserver les règles acquises de Config Desk :
@@ -441,12 +546,77 @@ Préserver les règles acquises de Config Desk :
Les file pickers natifs éventuellement nécessaires doivent être évalués comme surface Tauri explicite, pas ajoutés implicitement.
## 17. Validation finale Tauri
## 20. Discipline Rust et normalisation workspace
Toute modification Rust de `0.2.6` respecte **simultanément** `rustfmt`, Clippy et les règles structurelles KSP. Le script d'audit n'est pas optionnel.
Rappels qui doivent être appliqués dès le premier patch, y compris dans les tests :
- `use` uniquement pour des traits réellement nécessaires à la résolution de méthode/contrainte du langage, annotés `rust-rules: trait-import` ;
- aucun import non-trait, alias de `use`, glob ou groupe `{...}` ;
- les `use` restent au début du module, jamais dans une fonction/méthode/test ;
- un item KSP `pub` ou `pub(crate)` partagé est réexporté au crate-root et consommé via le chemin le plus court (`crate::Item` dans sa crate, `ksp_*::Item` depuis une autre crate) ;
- `super::Item` dans un test n'est utilisé que pour un item strictement privé du module parent ;
- aucun `crate::module::Item` pour contourner une façade crate-root ;
- aucun alias de réexport pour résoudre une collision : renommer l'item dans son module propriétaire avec un nom canonique ;
- rustdoc utile pour tout `pub`/`pub(crate)` et leurs champs/méthodes visibles ;
- blocs homogènes ordonnés alphabétiquement lorsque l'ordre n'est pas sémantique ; façade `pub use` puis `pub(crate) use` ; constantes `pub`, puis `pub(crate)`, puis privées ;
- aucune ligne vide dans le corps d'une fonction/méthode ni à l'intérieur d'un `struct`/`enum` ; les lignes vides séparent les items ;
- les versions de fichiers modifiés sont incrémentées conformément aux règles KSP.
Le contrôle mécanique obligatoire est :
```bash
python3 scripts/audit_rust_workspace_rules.py
```
`cargo fmt` et Clippy **ne remplacent pas** ce contrôle.
## 21. Workflow version, delta et validation opérateur
Chaque tranche technique conserve les conventions KSP :
```text
Cargo workspace.package.version : 0.2.6-pre.N ou 0.2.6-pre.N.fix.M
Delta : deltas/0.2.6/pre.NNN.md ou pre.NNN-fix.MMM.md
Commit : v0.2.6-pre.NNN ou v0.2.6-pre.NNN-fix.MMM
Tag Git : uniquement pour la release stable finale v0.2.6
```
Une modification code/build/runtime/config/migration incrémente le signal technique Cargo. Une tranche réellement documentaire ne le fait pas. Le delta de livraison contient uniquement les fichiers ajoutés/modifiés nécessaires à la tranche, jamais une archive complète du repository.
Après toute modification Rust, ordre minimal :
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
```
Puis tests ciblés pendant le développement. Aux checkpoints globaux et à la clôture :
```bash
cargo test --workspace
```
Pour toute dépendance ajoutée/mise à niveau :
- vérifier la version stable réellement actuelle depuis la source primaire ;
- documenter les features réellement nécessaires ;
- exécuter les `cargo tree`/inverse trees pertinents ;
- ne pas conserver une génération ancienne uniquement par inertie ;
- ne pas forcer une unification impossible si deux dépendances externes imposent légitimement des générations différentes.
Les résultats opérateur collés dans la session deviennent la preuve de validation. Ne jamais inventer une validation locale non exécutée.
## 22. Validation finale Tauri
Après les validations ciblées et workspace :
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-app-wallet-desk
@@ -462,7 +632,7 @@ cargo tauri build -c crates/ksp-app-wallet-desk/tauri.conf.json
Le smoke Devnet opt-in, s'il est livré, est exécuté séparément avec une commande documentée.
## 18. Gate `pre.001`
## 23. Gate `pre.001`
Avant toute implémentation lourde, `0.2.6-pre.001` doit produire :
@@ -479,27 +649,9 @@ Avant toute implémentation lourde, `0.2.6-pre.001` doit produire :
Si le scope UI complet ne tient pas dans une release clôturable, scinder les fonctionnalités non essentielles avant implémentation lourde, mais préserver le MVP identité + balance.
## 19. Forecast initial non contraignant
Le gate n'est considéré franchi que si ces livrables sont écrits/consignés dans le delta/plan, que les dépendances nécessaires sont auditées et que la prévision de prereleases est explicitement recalibrée. Une simple discussion en chat sans matérialisation durable ne clôt pas `pre.001`.
Point de départ à réviser par `pre.001` :
```text
pre.001 audit/sizing/UX/Config/bridge/security
pre.002 crate Tauri + frontend shell + logging + splash/main
pre.003 std.wallet + composite Wallet Desk + adapters Config
pre.004 inventory/selection + locked inspection + DTOs
pre.005 create/import + unlock VIEW/OWNER + lifecycle
pre.006 wallet details + balance HTTP + refresh/diagnostics
pre.007 metadata administration + rotations VIEW/OWNER
pre.008 strong VIEW disable/recreate + export OWNER + security UX
pre.009 integration tests + Devnet smoke + compliance/security review
pre.010 README/USAGE/build final + prompt 0.2.7 si nécessaire
rel.001 publication stable
```
Cette séquence est un forecast, pas un engagement. `pre.001` peut la réduire, l'étendre ou la resegmenter selon l'audit réel.
## 20. Hors périmètre
## 24. Hors périmètre
```text
modification du wire .kspwallet V1 sans défaut démontré
@@ -516,7 +668,7 @@ trading
Le Wallet Desk valide et administre Wallet ; il ne devient pas l'application globale KSP.
## 21. Résultat attendu de `0.2.6`
## 25. Résultat attendu de `0.2.6`
À la clôture stable :
@@ -533,3 +685,17 @@ les validations Rust/frontend/Tauri sont vertes
un smoke Devnet de composition est disponible si retenu par le gate
README/USAGE et prompt de suite sont synchronisés
```
## 26. Instruction d'ouverture
Commencer la nouvelle session par **la relecture effective des sources internes obligatoires et l'audit de la base stable `v0.2.5`**. Ne pas répondre uniquement à partir de ce prompt ou de souvenirs de sessions précédentes.
Ensuite :
1. vérifier l'état réel de `ksp-app-config-desk`, Config, Wallet, Transport et Logging ;
2. exécuter la validation Rust structurelle de base ;
3. consulter les documentations officielles actuelles nécessaires pour Tauri/plugins/frontend ;
4. produire l'audit/sizing `0.2.6-pre.001`, avec screen map, DTO/commands, Config/composite, sécurité et forecast recalibré ;
5. **ne pas commencer l'implémentation UI lourde avant ce gate**.
Les règles de la session ne doivent pas être renégociées au fil des corrections : elles sont dans les documents KSP versionnés et doivent être appliquées dès chaque premier patch. Si un conflit ou une règle manquante est découvert, corriger la règle et son contrôle durable avant de propager une nouvelle convention.

View File

@@ -0,0 +1,130 @@
#!/usr/bin/env python3
# file: scripts/audit_ksp_workspace_rules.py
# version: 1
"""Audit mechanically verifiable KSP-specific Rust/dependency boundaries."""
from __future__ import annotations
import argparse
import dataclasses
import pathlib
import re
import sys
import tomllib
@dataclasses.dataclass(frozen=True)
class Violation:
"""One KSP-specific workspace violation."""
code: str
path: str
line: int
message: str
def line_number(text: str, needle: str) -> int:
"""Return one-based line number for a substring."""
offset = text.find(needle)
return 1 if offset < 0 else text[:offset].count("\n") + 1
def audit_manifests(root: pathlib.Path) -> list[Violation]:
"""Audit centralized third-party dependency ownership."""
violations: list[Violation] = []
workspace_manifest = root / "Cargo.toml"
workspace = tomllib.loads(workspace_manifest.read_text(encoding="utf-8"))
workspace_dependencies = set(workspace.get("workspace", {}).get("dependencies", {}))
for manifest in sorted((root / "crates").glob("*/Cargo.toml")):
text = manifest.read_text(encoding="utf-8")
data = tomllib.loads(text)
relative = manifest.relative_to(root).as_posix()
for section in ("dependencies", "dev-dependencies", "build-dependencies"):
values = data.get(section, {})
for name, value in values.items():
if isinstance(value, str):
violations.append(Violation("KSP-DEP-101", relative, line_number(text, name), f"third-party `{name}` must be owned by [workspace.dependencies] and consumed with workspace = true"))
continue
if not isinstance(value, dict):
continue
if "path" in value:
continue
if name not in workspace_dependencies:
violations.append(Violation("KSP-DEP-102", relative, line_number(text, name), f"`{name}` is missing from root [workspace.dependencies]"))
if value.get("workspace") is not True:
violations.append(Violation("KSP-DEP-103", relative, line_number(text, name), f"`{name}` must use workspace = true in member manifest"))
if "version" in value:
violations.append(Violation("KSP-DEP-104", relative, line_number(text, name), f"member dependency `{name}` must not own a version"))
return violations
def audit_source_boundaries(root: pathlib.Path) -> list[Violation]:
"""Audit central KSP ownership boundaries visible directly in Rust sources."""
violations: list[Violation] = []
for path in sorted((root / "crates").glob("*/src/**/*.rs")):
relative = path.relative_to(root).as_posix()
crate_name = path.relative_to(root / "crates").parts[0]
for idx, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
if crate_name != "ksp-logging-lib" and re.search(r"\btracing::", line) and "tauri_plugin_tracing" not in line:
violations.append(Violation("KSP-TRACE-101", relative, idx, "behavioral crates must use ksp-logging-lib rather than tracing directly"))
if crate_name != "ksp-logging-lib" and "ksp_logging_lib::tracing::" in line:
violations.append(Violation("KSP-TRACE-104", relative, idx, "the hidden Logging tracing bridge is reserved for exported KSP macros"))
if crate_name != "ksp-config-lib" and re.search(r"\bstd::env::(?:var|var_os|vars|vars_os|set_var|remove_var)\b", line):
violations.append(Violation("KSP-CONFIG-101", relative, idx, "only ksp-config-lib may access KSP process environment directly"))
if crate_name != "ksp-core-lib" and "solana_pubkey::" in line:
violations.append(Violation("KSP-SOL-101", relative, idx, "consume Pubkey through ksp-core-lib rather than solana-pubkey directly"))
if crate_name != "ksp-wallet-lib" and "solana_keypair::" in line:
violations.append(Violation("KSP-SOL-102", relative, idx, "solana-keypair is Wallet-owned and must not leak into other crates"))
return violations
def audit_tracing_targets(root: pathlib.Path) -> list[Violation]:
"""Require crate-root canonical tracing targets in behavioral libraries."""
violations: list[Violation] = []
for manifest in sorted((root / "crates").glob("*/Cargo.toml")):
crate = manifest.parent
name = tomllib.loads(manifest.read_text(encoding="utf-8")).get("package", {}).get("name")
if not isinstance(name, str) or name == "ksp-core-lib":
continue
source_files = sorted((crate / "src").rglob("*.rs"))
if not any("TRACING_TARGET" in path.read_text(encoding="utf-8") for path in source_files):
continue
constants = crate / "src/constants.rs"
if constants.is_file():
expected = f'pub(crate) const TRACING_TARGET: &str = "{name}";'
if expected not in constants.read_text(encoding="utf-8"):
violations.append(Violation("KSP-TRACE-102", constants.relative_to(root).as_posix(), 1, f"expected canonical `{expected}`"))
root_file = crate / "src/lib.rs"
if not root_file.is_file():
root_file = crate / "src/main.rs"
if root_file.is_file() and constants.is_file() and "pub(crate) use self::constants::TRACING_TARGET;" not in root_file.read_text(encoding="utf-8"):
violations.append(Violation("KSP-TRACE-103", root_file.relative_to(root).as_posix(), 1, "TRACING_TARGET must be re-exported at crate root"))
return violations
def main() -> int:
"""Run KSP-specific workspace audits."""
parser = argparse.ArgumentParser()
parser.add_argument("--root", default=".")
parser.add_argument("--report-only", action="store_true")
arguments = parser.parse_args()
root = pathlib.Path(arguments.root).resolve()
violations = audit_manifests(root) + audit_source_boundaries(root) + audit_tracing_targets(root)
violations.sort(key=lambda item: (item.code, item.path, item.line, item.message))
if not violations:
sys.stdout.write("KSP workspace Rust rule audit: clean\n")
return 0
sys.stdout.write(f"KSP workspace Rust rule audit: {len(violations)} violation(s)\n")
for item in violations:
sys.stdout.write(f"{item.code} {item.path}:{item.line}: {item.message}\n")
return 0 if arguments.report_only else 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,199 @@
#!/usr/bin/env python3
# file: scripts/audit_rust_export_completeness.py
# version: 1
"""Audit crate-root export completeness and canonical same-crate paths."""
from __future__ import annotations
import argparse
import dataclasses
import pathlib
import re
import sys
from audit_rust_general_rules import line_depths, mask_rust_source
@dataclasses.dataclass(frozen=True)
class Candidate:
"""One export/path normalization candidate."""
code: str
path: str
line: int
message: str
@dataclasses.dataclass(frozen=True)
class Declaration:
"""One module-level public or crate-public declaration."""
module: str
name: str
visibility: str
path: pathlib.Path
line: int
def crate_roots(root: pathlib.Path) -> list[pathlib.Path]:
"""Return Rust workspace crate directories under crates/."""
crates: list[pathlib.Path] = []
for manifest in (root / "crates").glob("*/Cargo.toml"):
crate = manifest.parent
if (crate / "src/lib.rs").is_file() or (crate / "src/main.rs").is_file():
crates.append(crate)
return sorted(crates)
def module_path(crate: pathlib.Path, path: pathlib.Path) -> str:
"""Return the Rust module path represented by a source file."""
relative = path.relative_to(crate / "src").with_suffix("")
return "::".join(relative.parts)
def declaration_candidates(crate: pathlib.Path, path: pathlib.Path) -> list[Declaration]:
"""Return module-level public declarations from one source module."""
text = path.read_text(encoding="utf-8")
lines = text.splitlines()
depths = line_depths(mask_rust_source(text))
found: list[Declaration] = []
pattern = re.compile(
r"^\s*(pub(?:\(crate\))?)\s+(?:(?:async|unsafe|const)\s+)*(?:const|static|type|struct|enum|trait|union|fn)\s+([A-Za-z_][A-Za-z0-9_]*)"
)
for idx, line in enumerate(lines, 1):
if depths[idx - 1] != 0:
continue
match = pattern.match(line)
if match is not None:
found.append(Declaration(module_path(crate, path), match.group(2), match.group(1), path, idx))
return found
def root_exports(crate: pathlib.Path) -> dict[tuple[str, str], str]:
"""Return explicit crate-root re-exports keyed by source module and symbol."""
crate_root = crate / "src/lib.rs"
if not crate_root.is_file():
crate_root = crate / "src/main.rs"
exports: dict[tuple[str, str], str] = {}
if not crate_root.is_file():
return exports
pattern = re.compile(
r"^pub(?:\(crate\))?\s+use\s+self::([A-Za-z_][A-Za-z0-9_:]*)::([A-Za-z_][A-Za-z0-9_]*)\s*;",
re.MULTILINE,
)
for match in pattern.finditer(crate_root.read_text(encoding="utf-8")):
exports[(match.group(1), match.group(2))] = match.group(2)
return exports
def unit_test_parents(crate: pathlib.Path) -> dict[pathlib.Path, pathlib.Path]:
"""Map separated unit-test files to the production module that owns them."""
mapping: dict[pathlib.Path, pathlib.Path] = {}
pattern = re.compile(r'#\[path\s*=\s*"\.\./unit_tests/([^\"]+)"\]')
for source in sorted((crate / "src").rglob("*.rs")):
for match in pattern.finditer(source.read_text(encoding="utf-8")):
test = crate / "unit_tests" / match.group(1)
if test.is_file():
mapping[test.resolve()] = source.resolve()
return mapping
def external_usage(crate: pathlib.Path, declaration: Declaration, test_parents: dict[pathlib.Path, pathlib.Path]) -> bool:
"""Return whether a crate-public item is referenced outside its declaration module."""
direct = f"crate::{declaration.module}::{declaration.name}"
root_direct = f"crate::{declaration.name}"
super_ref = f"super::{declaration.name}"
for path in sorted(crate.rglob("*.rs")):
if "target" in path.parts:
continue
resolved = path.resolve()
if resolved == declaration.path.resolve():
continue
text = path.read_text(encoding="utf-8")
if direct in text or root_direct in text:
return True
if test_parents.get(resolved) == declaration.path.resolve() and super_ref in text:
return True
return False
def audit_crate(workspace: pathlib.Path, crate: pathlib.Path) -> list[Candidate]:
"""Audit one crate for export completeness and canonical paths."""
crate_root = crate / "src/lib.rs"
if not crate_root.is_file():
crate_root = crate / "src/main.rs"
exports = root_exports(crate)
tests = unit_test_parents(crate)
declarations: dict[tuple[str, str], Declaration] = {}
candidates: list[Candidate] = []
for path in sorted((crate / "src").rglob("*.rs")):
if path == crate_root:
continue
for declaration in declaration_candidates(crate, path):
key = (declaration.module, declaration.name)
declarations[key] = declaration
required = declaration.visibility == "pub" or external_usage(crate, declaration, tests)
if required and key not in exports:
relative = path.relative_to(workspace).as_posix()
reason = "public item" if declaration.visibility == "pub" else "crate-public item used outside its module"
candidates.append(Candidate("RUST-API-201", relative, declaration.line, f"{reason} `{declaration.name}` requires a crate-root re-export"))
# Canonical crate::Item paths for exported items.
long_path = re.compile(r"\bcrate::([A-Za-z_][A-Za-z0-9_:]*)::([A-Za-z_][A-Za-z0-9_]*)")
for path in sorted(crate.rglob("*.rs")):
if "target" in path.parts or path == crate_root:
continue
relative = path.relative_to(workspace).as_posix()
for idx, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
for match in long_path.finditer(line):
alias = exports.get((match.group(1), match.group(2)))
if alias is not None:
candidates.append(Candidate("RUST-IMPORT-201", relative, idx, f"long internal path `crate::{match.group(1)}::{match.group(2)}` must use `crate::{alias}`"))
# `super::Item` is reserved to strictly private parent items in separated unit tests.
declarations_by_source: dict[pathlib.Path, dict[str, Declaration]] = {}
for declaration in declarations.values():
declarations_by_source.setdefault(declaration.path.resolve(), {})[declaration.name] = declaration
super_pattern = re.compile(r"\bsuper::([A-Za-z_][A-Za-z0-9_]*)")
for test, parent in tests.items():
parent_declarations = declarations_by_source.get(parent, {})
relative = test.relative_to(workspace).as_posix()
for idx, line in enumerate(test.read_text(encoding="utf-8").splitlines(), 1):
for match in super_pattern.finditer(line):
declaration = parent_declarations.get(match.group(1))
if declaration is not None and declaration.visibility in {"pub", "pub(crate)"}:
candidates.append(Candidate("RUST-IMPORT-202", relative, idx, f"`super::{declaration.name}` targets {declaration.visibility}; use crate-root `crate::{declaration.name}`"))
return candidates
def main() -> int:
"""Run the export-completeness audit."""
parser = argparse.ArgumentParser()
parser.add_argument("--root", default=".")
parser.add_argument("--report-only", action="store_true")
parser.add_argument("--summary-only", action="store_true")
arguments = parser.parse_args()
workspace = pathlib.Path(arguments.root).resolve()
candidates = [item for crate in crate_roots(workspace) for item in audit_crate(workspace, crate)]
candidates.sort(key=lambda item: (item.code, item.path, item.line, item.message))
counts: dict[str, int] = {}
for item in candidates:
counts[item.code] = counts.get(item.code, 0) + 1
sys.stdout.write(f"Rust export completeness audit: {len(candidates)} candidate(s)\n")
for code in sorted(counts):
sys.stdout.write(f"{code}: {counts[code]}\n")
if not arguments.summary_only:
for item in candidates:
sys.stdout.write(f"{item.code} {item.path}:{item.line}: {item.message}\n")
return 0 if arguments.report_only or not candidates else 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,455 @@
#!/usr/bin/env python3
# file: scripts/audit_rust_general_rules.py
# version: 1
"""Audit mechanically verifiable Rust normalization rules used by KSP."""
from __future__ import annotations
import argparse
import dataclasses
import pathlib
import re
import sys
@dataclasses.dataclass(frozen=True)
class Violation:
"""One mechanically detected general Rust rule violation."""
code: str
path: str
line: int
message: str
def rust_files(root: pathlib.Path) -> list[pathlib.Path]:
"""Return tracked-style Rust sources outside generated directories."""
crates = root / "crates"
return sorted(path for path in crates.rglob("*.rs") if "target" not in path.parts and ".git" not in path.parts)
def mask_rust_source(text: str) -> str:
"""Mask comments and literals while preserving braces, newlines and offsets."""
output = list(text)
index = 0
state = "code"
block_depth = 0
raw_hashes = 0
while index < len(text):
char = text[index]
nxt = text[index + 1] if index + 1 < len(text) else ""
if state == "code":
if char == "/" and nxt == "/":
output[index] = " "
output[index + 1] = " "
index += 2
state = "line_comment"
continue
if char == "/" and nxt == "*":
output[index] = " "
output[index + 1] = " "
index += 2
state = "block_comment"
block_depth = 1
continue
if char == '"':
output[index] = " "
index += 1
state = "string"
continue
if char == "'":
# Treat a quote as a char literal only when it can close shortly;
# lifetimes such as `'a` remain code.
probe = index + 1
escaped = False
found = False
while probe < min(len(text), index + 8) and text[probe] != "\n":
if not escaped and text[probe] == "'":
found = True
break
if not escaped and text[probe] == "\\":
escaped = True
else:
escaped = False
probe += 1
if found:
output[index] = " "
index += 1
state = "char"
continue
if char == "r":
probe = index + 1
hashes = 0
while probe < len(text) and text[probe] == "#":
hashes += 1
probe += 1
if probe < len(text) and text[probe] == '"':
for masked in range(index, probe + 1):
output[masked] = " "
index = probe + 1
raw_hashes = hashes
state = "raw_string"
continue
index += 1
continue
if state == "line_comment":
if char == "\n":
state = "code"
else:
output[index] = " "
index += 1
continue
if state == "block_comment":
if char == "/" and nxt == "*":
output[index] = " "
output[index + 1] = " "
block_depth += 1
index += 2
continue
if char == "*" and nxt == "/":
output[index] = " "
output[index + 1] = " "
block_depth -= 1
index += 2
if block_depth == 0:
state = "code"
continue
if char != "\n":
output[index] = " "
index += 1
continue
if state == "string":
if char == "\\" and index + 1 < len(text):
output[index] = " "
if text[index + 1] != "\n":
output[index + 1] = " "
index += 2
continue
if char == '"':
output[index] = " "
state = "code"
elif char != "\n":
output[index] = " "
index += 1
continue
if state == "char":
if char == "\\" and index + 1 < len(text):
output[index] = " "
if text[index + 1] != "\n":
output[index + 1] = " "
index += 2
continue
if char == "'":
output[index] = " "
state = "code"
elif char != "\n":
output[index] = " "
index += 1
continue
if state == "raw_string":
if char == '"' and text.startswith("#" * raw_hashes, index + 1):
output[index] = " "
for masked in range(index + 1, index + 1 + raw_hashes):
output[masked] = " "
index += 1 + raw_hashes
state = "code"
continue
if char != "\n":
output[index] = " "
index += 1
continue
return "".join(output)
def line_depths(masked: str) -> list[int]:
"""Return brace depth at the start of every line."""
depths: list[int] = []
depth = 0
for line in masked.splitlines():
depths.append(depth)
depth += line.count("{") - line.count("}")
if depth < 0:
depth = 0
return depths
def preceding_doc_line(lines: list[str], index: int) -> bool:
"""Return whether one declaration has adjacent useful line rustdoc."""
cursor = index - 2
while cursor >= 0 and lines[cursor].strip().startswith("#["):
cursor -= 1
return cursor >= 0 and lines[cursor].lstrip().startswith("///")
def export_symbol(line: str) -> str:
"""Return the re-exported symbol name from one ungrouped re-export."""
path = line.split(" use ", 1)[1].split(";", 1)[0].strip()
return path.rsplit("::", 1)[-1]
def natural_key(value: str) -> tuple[tuple[int, object], ...]:
"""Return a natural case-sensitive ordering key."""
tokens = re.findall(r"[A-Za-z_]+|[0-9]+", value)
return tuple((1, int(token)) if token.isdigit() else (0, token) for token in tokens)
def public_declaration(stripped: str) -> re.Match[str] | None:
"""Match a public or crate-public item or associated item declaration."""
return re.match(
r"^pub(?:\(crate\))?\s+(?:(?:async|const|unsafe)\s+)*(?:const|static|type|struct|enum|trait|union|fn)\s+([A-Za-z_][A-Za-z0-9_]*)",
stripped,
)
def public_field(stripped: str) -> re.Match[str] | None:
"""Match a public or crate-public named field declaration."""
return re.match(r"^pub(?:\(crate\))?\s+([A-Za-z_][A-Za-z0-9_]*)\s*:", stripped)
def declaration_start(stripped: str, kind: str) -> bool:
"""Return whether a line starts a declaration of one requested kind."""
prefixes = r"(?:(?:pub|pub\(crate\))\s+)?(?:(?:async|const|unsafe)\s+)*"
return re.match(rf"^{prefixes}{kind}\b", stripped) is not None
def audit_blank_lines_in_bodies(relative: str, lines: list[str], masked_lines: list[str], depths: list[int]) -> list[Violation]:
"""Reject blank lines inside functions/methods and struct/enum bodies."""
violations: list[Violation] = []
pending_kind: str | None = None
pending_depth = 0
body_kind: str | None = None
body_depth = 0
for idx, (line, masked_line) in enumerate(zip(lines, masked_lines, strict=True), 1):
stripped = line.strip()
if body_kind is not None:
if not stripped and depths[idx - 1] >= body_depth:
violations.append(Violation("RUST-FMT-101", relative, idx, f"blank line inside {body_kind} body"))
if depths[idx - 1] < body_depth:
body_kind = None
if body_kind is None and pending_kind is None:
if declaration_start(stripped, "fn"):
pending_kind = "function/method"
pending_depth = depths[idx - 1]
elif declaration_start(stripped, "struct"):
pending_kind = "struct"
pending_depth = depths[idx - 1]
elif declaration_start(stripped, "enum"):
pending_kind = "enum"
pending_depth = depths[idx - 1]
if pending_kind is not None and "{" in masked_line:
body_kind = pending_kind
body_depth = pending_depth + 1
pending_kind = None
elif pending_kind is not None and ";" in masked_line:
pending_kind = None
return violations
def audit_top_level_const_blocks(relative: str, lines: list[str], depths: list[int]) -> list[Violation]:
"""Audit visibility/order/spacing for mechanically homogeneous const blocks."""
violations: list[Violation] = []
const_pattern = re.compile(r"^(pub\s+|pub\(crate\)\s+)?const\s+([A-Za-z_][A-Za-z0-9_]*)")
previous: tuple[int, int, str] | None = None
# rank: public, crate-public, private
for idx, line in enumerate(lines, 1):
if depths[idx - 1] != 0:
continue
match = const_pattern.match(line.strip())
if match is None:
# Docs and attributes belong to the surrounding block and do not break it.
if line.strip() and not line.lstrip().startswith(("///", "#[")):
previous = None
continue
visibility = match.group(1) or ""
rank = 0 if visibility == "pub " else 1 if visibility == "pub(crate) " else 2
name = match.group(2)
if previous is not None:
previous_line, previous_rank, previous_name = previous
between = lines[previous_line:idx - 1]
if any(not candidate.strip() for candidate in between):
violations.append(Violation("RUST-FMT-102", relative, idx, "blank line inside one top-level const block"))
if rank < previous_rank:
violations.append(Violation("RUST-FMT-103", relative, idx, "const visibility order must be pub, pub(crate), then private"))
if rank == previous_rank and natural_key(name) < natural_key(previous_name):
violations.append(Violation("RUST-FMT-104", relative, idx, "const block is not alphabetically ordered"))
previous = (idx, rank, name)
return violations
def audit_module_order(relative: str, lines: list[str], depths: list[int]) -> list[Violation]:
"""Audit top-level module declaration ordering with test modules last."""
violations: list[Violation] = []
modules: list[tuple[int, bool, str]] = []
for idx, line in enumerate(lines, 1):
if depths[idx - 1] != 0:
continue
match = re.match(r"^mod\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:;|\{)", line.strip())
if match is None:
continue
cursor = idx - 2
is_test = False
while cursor >= 0 and lines[cursor].strip().startswith("#["):
if "cfg(test)" in lines[cursor].replace(" ", ""):
is_test = True
cursor -= 1
modules.append((idx, is_test, match.group(1)))
production = [(idx, name) for idx, is_test, name in modules if not is_test]
tests = [(idx, name) for idx, is_test, name in modules if is_test]
if tests and production and min(idx for idx, _ in tests) < max(idx for idx, _ in production):
first_test = min(idx for idx, _ in tests)
violations.append(Violation("RUST-FMT-108", relative, first_test, "test modules must follow production modules"))
for group, label in ((production, "production"), (tests, "test")):
names = [name for _, name in group]
if names != sorted(names, key=natural_key):
for idx, name in group:
expected = sorted(names, key=natural_key)
if names.index(name) != expected.index(name):
violations.append(Violation("RUST-FMT-109", relative, idx, f"{label} module declarations are not alphabetically ordered"))
break
return violations
def audit_file(root: pathlib.Path, path: pathlib.Path) -> list[Violation]:
"""Audit one Rust source against general normalization rules."""
relative = path.relative_to(root).as_posix()
text = path.read_text(encoding="utf-8")
lines = text.splitlines()
masked = mask_rust_source(text)
masked_lines = masked.splitlines()
depths = line_depths(masked)
violations: list[Violation] = []
expected_header = f"// file: {relative}"
if not lines or lines[0] != expected_header:
violations.append(Violation("RUST-BASE-101", relative, 1, f"expected `{expected_header}`"))
if len(lines) < 2 or re.fullmatch(r"// version: [1-9][0-9]*", lines[1]) is None:
violations.append(Violation("RUST-BASE-102", relative, 2, "missing positive file version"))
if not text.endswith("\n") or text.endswith("\n\n"):
violations.append(Violation("RUST-FMT-100", relative, max(len(lines), 1), "file must end with exactly one newline"))
local_modules = {match.group(1) for line in lines if (match := re.fullmatch(r"\s*mod\s+([A-Za-z_][A-Za-z0-9_]*)\s*;", line)) is not None}
seen_declaration_before_use = False
use_rows: list[tuple[int, str]] = []
for idx, line in enumerate(lines, 1):
stripped = line.strip()
depth = depths[idx - 1] if idx - 1 < len(depths) else 0
if re.match(r"^(?:pub\s+)?extern\s+crate\s+[A-Za-z_][A-Za-z0-9_]*\s+as\s+", stripped):
violations.append(Violation("RUST-IMPORT-104", relative, idx, "extern crate aliases are forbidden"))
if "pub(in " in stripped or "pub(super)" in stripped:
violations.append(Violation("RUST-API-102", relative, idx, "restricted visibility is forbidden; use private or pub(crate) with crate-root re-export"))
if stripped.startswith("pub mod "):
violations.append(Violation("RUST-API-101", relative, idx, "modules must remain private and APIs use explicit re-exports"))
if depth == 0 and re.match(r"^(?:pub(?:\(crate\))?\s+)?(?:const|static|type|struct|enum|trait|union|fn|impl)\b", stripped):
seen_declaration_before_use = True
use_match = re.match(r"^(pub(?:\(crate\))?\s+)?use\s+", stripped)
if use_match is not None:
is_export = stripped.startswith("pub use ") or stripped.startswith("pub(crate) use ")
if depth != 0:
violations.append(Violation("RUST-IMPORT-111", relative, idx, "use declarations must be at module scope, never inside a function/method/block"))
if not is_export:
use_rows.append((idx, stripped))
if seen_declaration_before_use:
violations.append(Violation("RUST-IMPORT-112", relative, idx, "trait imports must remain at the beginning of the module before declarations"))
if "rust-rules: trait-import" not in stripped:
violations.append(Violation("RUST-IMPORT-101", relative, idx, "ordinary use requires an explicit trait import justification"))
if "::*" in stripped:
violations.append(Violation("RUST-IMPORT-102", relative, idx, "glob imports are forbidden"))
if "{" in stripped or "}" in stripped:
violations.append(Violation("RUST-IMPORT-103", relative, idx, "grouped use/re-export declarations are forbidden"))
if re.search(r"\s+as\s+[A-Za-z_][A-Za-z0-9_]*", stripped):
violations.append(Violation("RUST-IMPORT-104", relative, idx, "use/re-export aliases are forbidden"))
if is_export and re.match(r"^pub(?:\(crate\))?\s+use\s+crate::", stripped):
violations.append(Violation("RUST-IMPORT-105", relative, idx, "internal re-export must start with self::"))
declaration = public_declaration(stripped)
field = public_field(stripped)
if declaration is not None or field is not None:
if not preceding_doc_line(lines, idx):
name = (declaration or field).group(1)
violations.append(Violation("RUST-DOC-101", relative, idx, f"public/crate-public `{name}` requires adjacent rustdoc"))
# The ordinary module import block is contiguous and alphabetic.
for (previous_idx, previous_line), (current_idx, current_line) in zip(use_rows, use_rows[1:]):
between = lines[previous_idx:current_idx - 1]
if any(not candidate.strip() for candidate in between):
violations.append(Violation("RUST-IMPORT-113", relative, current_idx, "module use block contains an empty line"))
previous_path = previous_line.split("use ", 1)[1].split(";", 1)[0]
current_path = current_line.split("use ", 1)[1].split(";", 1)[0]
if natural_key(current_path) < natural_key(previous_path):
violations.append(Violation("RUST-IMPORT-114", relative, current_idx, "module use block is not alphabetically ordered"))
# Crate façade constraints.
if path.name in {"lib.rs", "main.rs"}:
for attribute in ("#![warn(missing_docs)]", "#![deny(unreachable_pub)]", "#![forbid(unsafe_code)]"):
if attribute not in lines[:24]:
violations.append(Violation("RUST-BASE-103", relative, 1, f"missing `{attribute}`"))
exports: list[tuple[int, str, str]] = []
for idx, line in enumerate(lines, 1):
stripped = line.strip()
if stripped.startswith("pub use ") or stripped.startswith("pub(crate) use "):
visibility = "pub(crate)" if stripped.startswith("pub(crate) use ") else "pub"
exports.append((idx, visibility, export_symbol(stripped)))
if not preceding_doc_line(lines, idx):
violations.append(Violation("RUST-DOC-102", relative, idx, "crate-root re-export requires adjacent rustdoc"))
source_match = re.match(r"^pub(?:\(crate\))?\s+use\s+(?:self::)?([A-Za-z_][A-Za-z0-9_]*)::", stripped)
if source_match is not None and source_match.group(1) in local_modules and not re.match(r"^pub(?:\(crate\))?\s+use\s+self::", stripped):
violations.append(Violation("RUST-IMPORT-106", relative, idx, "crate-root internal re-export must start with self::"))
crate_seen = False
previous_by_visibility: dict[str, tuple[int, str]] = {}
for idx, visibility, symbol in exports:
if visibility == "pub(crate)":
crate_seen = True
elif crate_seen:
violations.append(Violation("RUST-FMT-105", relative, idx, "pub use block must precede pub(crate) use block"))
previous = previous_by_visibility.get(visibility)
if previous is not None:
previous_idx, previous_symbol = previous
# One homogeneous block may contain rustdocs but no blank line.
between = lines[previous_idx:idx - 1]
if any(not candidate.strip() for candidate in between):
violations.append(Violation("RUST-FMT-106", relative, idx, f"{visibility} use block contains an empty line"))
if natural_key(symbol) < natural_key(previous_symbol):
violations.append(Violation("RUST-FMT-107", relative, idx, f"{visibility} use block is not alphabetically ordered by exported symbol"))
previous_by_visibility[visibility] = (idx, symbol)
violations.extend(audit_blank_lines_in_bodies(relative, lines, masked_lines, depths))
violations.extend(audit_module_order(relative, lines, depths))
violations.extend(audit_top_level_const_blocks(relative, lines, depths))
return violations
def main() -> int:
"""Run the general Rust normalization audit."""
parser = argparse.ArgumentParser()
parser.add_argument("--root", default=".")
parser.add_argument("--report-only", action="store_true")
arguments = parser.parse_args()
root = pathlib.Path(arguments.root).resolve()
violations = [item for path in rust_files(root) for item in audit_file(root, path)]
violations.sort(key=lambda item: (item.code, item.path, item.line, item.message))
if not violations:
sys.stdout.write("General Rust rule audit: clean\n")
return 0
sys.stdout.write(f"General Rust rule audit: {len(violations)} violation(s)\n")
counts: dict[str, int] = {}
for item in violations:
counts[item.code] = counts.get(item.code, 0) + 1
for code in sorted(counts):
sys.stdout.write(f"{code}: {counts[code]}\n")
for item in violations:
sys.stdout.write(f"{item.code} {item.path}:{item.line}: {item.message}\n")
return 0 if arguments.report_only else 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,34 @@
#!/usr/bin/env python3
# file: scripts/audit_rust_workspace_rules.py
# version: 1
"""Run all Rust normalization and KSP workspace audits."""
from __future__ import annotations
import argparse
import pathlib
import subprocess
def main() -> int:
"""Run general, export-completeness and KSP-specific audits."""
parser = argparse.ArgumentParser()
parser.add_argument("--root", default=".")
parser.add_argument("--report-only", action="store_true")
arguments = parser.parse_args()
script_dir = pathlib.Path(__file__).resolve().parent
commands = [
["python3", str(script_dir / "audit_rust_general_rules.py"), "--root", arguments.root],
["python3", str(script_dir / "audit_rust_export_completeness.py"), "--root", arguments.root],
["python3", str(script_dir / "audit_ksp_workspace_rules.py"), "--root", arguments.root],
]
if arguments.report_only:
for command in commands:
command.append("--report-only")
return max(subprocess.run(command, check=False).returncode for command in commands)
if __name__ == "__main__":
raise SystemExit(main())