v0.2.5-pre.009

This commit is contained in:
2026-08-20 09:24:10 +02:00
parent 1125ade4c1
commit e91bf36e9e
83 changed files with 2467 additions and 1801 deletions

View File

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

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/bootstrap.rs
// version: 3
// version: 4
//! Config and Logging bootstrap for the desktop application.
@@ -71,6 +71,15 @@ pub(crate) fn initialize_logging(
};
}
fn fallback_logging_settings() -> ksp_logging_lib::LoggingSettings {
return ksp_logging_lib::LoggingSettings::new(
ksp_logging_lib::LogFilterLevel::Info,
ksp_logging_lib::SpanEvents::Off,
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
std::vec::Vec::new(),
);
}
fn resolve_logging_startup(management: &ksp_config_lib::ConfigManagement) -> LoggingStartupPlan {
let environment = ksp_config_lib::ConfigEnvironment::load();
let environment = match environment {
@@ -132,15 +141,6 @@ fn initialize_planned_fallback_logging(
});
}
pub(crate) fn fallback_logging_settings() -> ksp_logging_lib::LoggingSettings {
return ksp_logging_lib::LoggingSettings::new(
ksp_logging_lib::LogFilterLevel::Info,
ksp_logging_lib::SpanEvents::Off,
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
std::vec::Vec::new(),
);
}
#[cfg(test)]
#[path = "../unit_tests/bootstrap.rs"]
mod tests;

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/lib.rs
// version: 14
// version: 15
//! Tauri desktop application for managing and validating KSP configuration.
@@ -73,16 +73,25 @@ pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_MISSING;
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED;
pub(crate) use self::frontend_logging::FrontendLogPayloadDto;
pub(crate) use self::frontend_logging::emit_frontend_log_event;
pub(crate) use self::logging_editor::LoggingConsoleDto;
pub(crate) use self::logging_editor::LoggingDocumentCandidateDto;
pub(crate) use self::logging_editor::LoggingDocumentDto;
pub(crate) use self::logging_editor::LoggingDocumentSaveResultDto;
pub(crate) use self::logging_runtime::{LoggingRuntimeFileDto, LoggingRuntimeStatusDto};
pub(crate) use self::logging_test::{LoggingTestRequestDto, LoggingTestResultDto};
pub(crate) use self::logging_editor::LoggingFileDto;
pub(crate) use self::logging_editor::LoggingOutputFilterDto;
pub(crate) use self::logging_editor::LoggingProfileDto;
pub(crate) use self::logging_editor::LoggingTargetFilterDto;
pub(crate) use self::logging_runtime::LoggingRuntimeFileDto;
pub(crate) use self::logging_runtime::LoggingRuntimeStatusDto;
pub(crate) use self::logging_runtime::count_to_u64;
pub(crate) use self::logging_runtime::launch_identity;
pub(crate) use self::logging_test::LoggingTestRequestDto;
pub(crate) use self::logging_test::LoggingTestResultDto;
pub(crate) use self::profiles::ConfigProfileDetailDto;
pub(crate) use self::profiles::ConfigProfileDocumentDto;
pub(crate) use self::secrets::SecretRevealRequestDto;
pub(crate) use self::secrets::SecretRevealResponseDto;
pub(crate) use self::splash::SplashOrderDto;
pub(crate) use self::splash::SplashSettings;
pub(crate) use self::tw_main::show_and_focus as show_main_window;
pub(crate) use self::tw_splash::frontend_ready as splash_frontend_ready_service;
pub(crate) use self::tw_main::show_main_window;
pub(crate) use self::tw_splash::splash_frontend_ready_service;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/logging_editor.rs
// version: 4
// version: 5
//! Typed Logging document projection and validated persistence for the Config Desk Logging editor.
@@ -32,7 +32,7 @@ pub(crate) struct LoggingConsoleDto {
/// Console format source text.
pub(crate) format: String,
/// Sink-local selectors.
pub(crate) filter: LoggingOutputFilterDto,
pub(crate) filter: crate::LoggingOutputFilterDto,
}
/// One persistent file sink exposed to the editor.
@@ -53,7 +53,7 @@ pub(crate) struct LoggingFileDto {
/// Whether ANSI output is requested.
pub(crate) ansi: bool,
/// Sink-local selectors.
pub(crate) filter: LoggingOutputFilterDto,
pub(crate) filter: crate::LoggingOutputFilterDto,
}
/// One global Logging target override exposed to the editor.
@@ -79,11 +79,11 @@ pub(crate) struct LoggingProfileDto {
/// Span lifecycle source text.
pub(crate) span_events: String,
/// Console sink configuration.
pub(crate) console: LoggingConsoleDto,
pub(crate) console: crate::LoggingConsoleDto,
/// Persistent file sinks in source order.
pub(crate) files: std::vec::Vec<LoggingFileDto>,
pub(crate) files: std::vec::Vec<crate::LoggingFileDto>,
/// Global target overrides in source order.
pub(crate) target_filters: std::vec::Vec<LoggingTargetFilterDto>,
pub(crate) target_filters: std::vec::Vec<crate::LoggingTargetFilterDto>,
}
/// Complete typed Logging document exposed to the editor.
@@ -102,7 +102,7 @@ pub(crate) struct LoggingDocumentDto {
/// Autonomous default profile identifier.
pub(crate) default_profile: String,
/// Typed profiles in source order.
pub(crate) profiles: std::vec::Vec<LoggingProfileDto>,
pub(crate) profiles: std::vec::Vec<crate::LoggingProfileDto>,
}
/// Complete editable Logging candidate accepted from the frontend.
@@ -115,7 +115,7 @@ pub(crate) struct LoggingDocumentCandidateDto {
/// Autonomous default profile identifier.
pub(crate) default_profile: String,
/// Typed profiles in source order.
pub(crate) profiles: std::vec::Vec<LoggingProfileDto>,
pub(crate) profiles: std::vec::Vec<crate::LoggingProfileDto>,
}
/// Result of one validated typed Logging persistence operation.
@@ -255,7 +255,7 @@ fn document_from_management(management: &ksp_config_lib::ConfigManagement) -> ks
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut profiles = std::vec::Vec::<LoggingProfileDto>::with_capacity(source.profiles().len());
let mut profiles = std::vec::Vec::<crate::LoggingProfileDto>::with_capacity(source.profiles().len());
let mut file_count = 0usize;
let mut target_filter_count = 0usize;
for profile in source.profiles() {
@@ -291,7 +291,7 @@ fn config_candidate(candidate: LoggingDocumentCandidateDto) -> ksp_config_lib::L
return ksp_config_lib::LoggingConfigDocument::new(candidate.logs_directory, candidate.default_profile, profiles);
}
fn config_profile(profile: LoggingProfileDto) -> ksp_config_lib::LoggingProfileConfig {
fn config_profile(profile: crate::LoggingProfileDto) -> ksp_config_lib::LoggingProfileConfig {
let mut files = std::vec::Vec::<ksp_config_lib::LoggingFileConfig>::with_capacity(profile.files.len());
for file in profile.files {
let mut config =
@@ -319,14 +319,14 @@ fn config_profile(profile: LoggingProfileDto) -> ksp_config_lib::LoggingProfileC
);
}
fn config_output_filter(filter: LoggingOutputFilterDto) -> ksp_config_lib::LoggingOutputFilterConfig {
fn config_output_filter(filter: crate::LoggingOutputFilterDto) -> ksp_config_lib::LoggingOutputFilterConfig {
return ksp_config_lib::LoggingOutputFilterConfig::new(filter.level, filter.targets, filter.domains);
}
fn project_profile(profile: &ksp_config_lib::LoggingProfileConfig) -> LoggingProfileDto {
let mut files = std::vec::Vec::<LoggingFileDto>::with_capacity(profile.files().len());
fn project_profile(profile: &ksp_config_lib::LoggingProfileConfig) -> crate::LoggingProfileDto {
let mut files = std::vec::Vec::<crate::LoggingFileDto>::with_capacity(profile.files().len());
for file in profile.files() {
files.push(LoggingFileDto {
files.push(crate::LoggingFileDto {
output_id: file.output_id().to_owned(),
enabled: file.enabled(),
path: file.path().to_owned(),
@@ -336,15 +336,15 @@ fn project_profile(profile: &ksp_config_lib::LoggingProfileConfig) -> LoggingPro
filter: project_output_filter(file.filter()),
});
}
let mut target_filters = std::vec::Vec::<LoggingTargetFilterDto>::with_capacity(profile.target_filters().len());
let mut target_filters = std::vec::Vec::<crate::LoggingTargetFilterDto>::with_capacity(profile.target_filters().len());
for target_filter in profile.target_filters() {
target_filters.push(LoggingTargetFilterDto { target_prefix: target_filter.target_prefix().to_owned(), level: target_filter.level().to_owned() });
target_filters.push(crate::LoggingTargetFilterDto { target_prefix: target_filter.target_prefix().to_owned(), level: target_filter.level().to_owned() });
}
return LoggingProfileDto {
return crate::LoggingProfileDto {
profile_id: profile.profile_id().to_owned(),
default_filter: profile.default_filter().to_owned(),
span_events: profile.span_events().to_owned(),
console: LoggingConsoleDto {
console: crate::LoggingConsoleDto {
enabled: profile.console().enabled(),
output: profile.console().output().to_owned(),
ansi: profile.console().ansi(),
@@ -356,8 +356,8 @@ fn project_profile(profile: &ksp_config_lib::LoggingProfileConfig) -> LoggingPro
};
}
fn project_output_filter(filter: &ksp_config_lib::LoggingOutputFilterConfig) -> LoggingOutputFilterDto {
return LoggingOutputFilterDto {
fn project_output_filter(filter: &ksp_config_lib::LoggingOutputFilterConfig) -> crate::LoggingOutputFilterDto {
return crate::LoggingOutputFilterDto {
level: filter.level().to_owned(),
targets: filter.targets().to_vec(),
domains: filter.domains().to_vec(),

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/logging_runtime.rs
// version: 2
// version: 3
//! Runtime Logging metadata, launch identity and explicit profile application for Config Desk.
@@ -97,7 +97,7 @@ pub(crate) fn project_file(metadata: &ksp_logging_lib::RuntimeFileMetadata) -> L
};
}
pub(crate) const fn rotation_label(rotation: ksp_logging_lib::FileRotation) -> &'static str {
const fn rotation_label(rotation: ksp_logging_lib::FileRotation) -> &'static str {
return match rotation {
ksp_logging_lib::FileRotation::Never => "never",
ksp_logging_lib::FileRotation::Hourly => "hourly",

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/profiles.rs
// version: 1
// version: 2
//! Safe Config profile inspection and provenance projections for Config Desk.
@@ -77,6 +77,11 @@ pub(crate) struct ConfigProfileDetailDto {
pub(crate) environment_provenance: std::vec::Vec<ConfigProfileEnvironmentProvenanceDto>,
}
struct ProfileContract {
default_profile: String,
profile_ids: std::vec::Vec<String>,
}
/// Lists validated Config documents that expose standard profiles.
pub(crate) fn inventory(state: &crate::AppState) -> ksp_core_lib::Result<std::vec::Vec<ConfigProfileDocumentDto>> {
return inventory_from_management(state.config_management());
@@ -213,11 +218,6 @@ fn detail_from_management(
});
}
struct ProfileContract {
default_profile: String,
profile_ids: std::vec::Vec<String>,
}
fn profile_contract(document: &ksp_config_lib::ConfigJsonDocument) -> ksp_core_lib::Result<std::option::Option<ProfileContract>> {
let root = match document.value().as_object() {
std::option::Option::Some(value) => value,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/splash.rs
// version: 3
// version: 4
//! Common splash settings and frontend event contracts for Config Desk.
@@ -94,14 +94,6 @@ impl SplashSettings {
}
}
const fn environment_source_code(source: ksp_config_lib::ConfigEnvironmentSource) -> &'static str {
return match source {
ksp_config_lib::ConfigEnvironmentSource::Process => "process",
ksp_config_lib::ConfigEnvironmentSource::DotEnv => "dotenv",
ksp_config_lib::ConfigEnvironmentSource::Fallback => "fallback",
};
}
/// Command emitted by Rust to the splash frontend.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
@@ -122,6 +114,14 @@ impl SplashOrderDto {
}
}
const fn environment_source_code(source: ksp_config_lib::ConfigEnvironmentSource) -> &'static str {
return match source {
ksp_config_lib::ConfigEnvironmentSource::Process => "process",
ksp_config_lib::ConfigEnvironmentSource::DotEnv => "dotenv",
ksp_config_lib::ConfigEnvironmentSource::Fallback => "fallback",
};
}
fn parse_u64_setting(variable_name: &str, value: &str, maximum: u64) -> ksp_core_lib::Result<u64> {
let parsed = value.parse::<u64>();
let parsed = match parsed {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/tw_main.rs
// version: 1
// version: 2
//! Tauri-window helpers for the Config Desk main window.
@@ -18,7 +18,7 @@ pub(crate) fn require_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib
};
}
pub(crate) fn show_and_focus(app: &tauri::AppHandle) -> ksp_core_lib::Result<()> {
pub(crate) fn show_main_window(app: &tauri::AppHandle) -> ksp_core_lib::Result<()> {
let window = require_window(app);
let window = match window {
std::result::Result::Ok(value) => value,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/tw_splash.rs
// version: 2
// version: 3
//! Tauri-window lifecycle for the Config Desk splash window.
@@ -20,7 +20,11 @@ pub(crate) fn require_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib
};
}
pub(crate) async fn frontend_ready(app: tauri::AppHandle, invoking_window: tauri::WebviewWindow, state: &crate::AppState) -> ksp_core_lib::Result<()> {
pub(crate) async fn splash_frontend_ready_service(
app: tauri::AppHandle,
invoking_window: tauri::WebviewWindow,
state: &crate::AppState,
) -> ksp_core_lib::Result<()> {
if invoking_window.label() != WINDOW_LABEL_SPLASH {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_ORIGIN_INVALID, "Splash readiness may only originate from the splash window")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/environment.rs
// version: 5
// version: 6
/// Default local environment file read by Config from the process launch directory.
pub const DEFAULT_DOTENV_PATH: &str = ".env";
@@ -258,43 +258,6 @@ impl ConfigEnvironment {
}
}
fn collect_process_environment<I>(values: I) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>>
where
I: std::iter::IntoIterator<Item = (std::ffi::OsString, std::ffi::OsString)>,
{
let mut output = std::collections::BTreeMap::<String, String>::new();
for (name, value) in values {
let name = match name.to_str() {
std::option::Option::Some(value) => value,
std::option::Option::None => continue,
};
if !has_supported_namespace(name) {
continue;
}
let validation = validate_supported_variable_name(name);
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error);
}
let value = value.into_string();
let value = match value {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(invalid_environment_value_error(name)),
};
output.insert(name.to_owned(), value);
}
return std::result::Result::Ok(output);
}
fn load_dotenv_file(path: &std::path::Path) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>> {
let content = std::fs::read_to_string(path);
let content = match content {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => return std::result::Result::Ok(std::collections::BTreeMap::new()),
std::result::Result::Err(error) => return std::result::Result::Err(dotenv_read_error(path, error)),
};
return parse_dotenv_content(path, content.as_str());
}
pub(crate) fn parse_dotenv_content(path: &std::path::Path, content: &str) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>> {
let mut output = std::collections::BTreeMap::<String, String>::new();
for (line_index, raw_line) in content.lines().enumerate() {
@@ -342,6 +305,60 @@ pub(crate) fn parse_dotenv_content(path: &std::path::Path, content: &str) -> ksp
return std::result::Result::Ok(output);
}
pub(crate) fn validate_supported_variable_name(variable_name: &str) -> ksp_core_lib::Result<()> {
if !has_supported_namespace(variable_name) {
return std::result::Result::Err(invalid_variable_error(variable_name, "variable must use the KSP_ or KSPB_ namespace"));
}
let prefix_length = if variable_name.starts_with("KSPB_") { 5 } else { 4 };
if variable_name.len() <= prefix_length {
return std::result::Result::Err(invalid_variable_error(variable_name, "variable namespace must be followed by a name"));
}
for byte in variable_name.bytes() {
let valid = byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_';
if !valid {
return std::result::Result::Err(invalid_variable_error(variable_name, "variable names use uppercase ASCII letters, digits and underscores"));
}
}
return std::result::Result::Ok(());
}
fn collect_process_environment<I>(values: I) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>>
where
I: std::iter::IntoIterator<Item = (std::ffi::OsString, std::ffi::OsString)>,
{
let mut output = std::collections::BTreeMap::<String, String>::new();
for (name, value) in values {
let name = match name.to_str() {
std::option::Option::Some(value) => value,
std::option::Option::None => continue,
};
if !has_supported_namespace(name) {
continue;
}
let validation = validate_supported_variable_name(name);
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error);
}
let value = value.into_string();
let value = match value {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(invalid_environment_value_error(name)),
};
output.insert(name.to_owned(), value);
}
return std::result::Result::Ok(output);
}
fn load_dotenv_file(path: &std::path::Path) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>> {
let content = std::fs::read_to_string(path);
let content = match content {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => return std::result::Result::Ok(std::collections::BTreeMap::new()),
std::result::Result::Err(error) => return std::result::Result::Err(dotenv_read_error(path, error)),
};
return crate::parse_dotenv_content(path, content.as_str());
}
fn parse_dotenv_value(path: &std::path::Path, line_number: usize, raw_value: &str) -> ksp_core_lib::Result<String> {
if raw_value.starts_with('\'') && (raw_value.len() < 2 || !raw_value.ends_with('\'')) {
return std::result::Result::Err(dotenv_syntax_error(path, line_number, "single-quoted value is not terminated"));
@@ -516,23 +533,6 @@ fn escape_json_pointer_token(value: &str) -> String {
return value.replace('~', "~0").replace('/', "~1");
}
pub(crate) fn validate_supported_variable_name(variable_name: &str) -> ksp_core_lib::Result<()> {
if !has_supported_namespace(variable_name) {
return std::result::Result::Err(invalid_variable_error(variable_name, "variable must use the KSP_ or KSPB_ namespace"));
}
let prefix_length = if variable_name.starts_with("KSPB_") { 5 } else { 4 };
if variable_name.len() <= prefix_length {
return std::result::Result::Err(invalid_variable_error(variable_name, "variable namespace must be followed by a name"));
}
for byte in variable_name.bytes() {
let valid = byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_';
if !valid {
return std::result::Result::Err(invalid_variable_error(variable_name, "variable names use uppercase ASCII letters, digits and underscores"));
}
}
return std::result::Result::Ok(());
}
fn has_supported_namespace(variable_name: &str) -> bool {
return variable_name.starts_with("KSP_") || variable_name.starts_with("KSPB_");
}

View File

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

View File

@@ -1,5 +1,6 @@
// file: crates/ksp-config-lib/src/lib.rs
// version: 12
// version: 13
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
@@ -25,8 +26,6 @@ mod registry;
mod sensitivity;
mod transport;
pub(crate) use self::constants::TRACING_TARGET;
/// Bootstrap argument used to replace the configuration document root.
pub use self::bootstrap::ARG_CFG_PATH;
/// Bootstrap argument used to replace the schema root.
@@ -171,3 +170,7 @@ pub use self::sensitivity::ResolvedConfigJson;
pub use self::sensitivity::ResolvedConfigText;
/// Effective standard HTTP Transport configuration mapped to `ksp_onchain_transport_lib::HttpTransportSettings`.
pub use self::transport::ResolvedTransportConfig;
pub(crate) use self::constants::TRACING_TARGET;
pub(crate) use self::environment::parse_dotenv_content;
pub(crate) use self::registry::build_registry;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/management.rs
// version: 4
// version: 5
/// Raw source of one registered Config document read for explicit management/correction.
#[derive(Clone, Eq, PartialEq)]
@@ -921,7 +921,7 @@ fn read_dotenv_source(path: &std::path::Path) -> ksp_core_lib::Result<String> {
let content = std::fs::read_to_string(path);
return match content {
std::result::Result::Ok(value) => {
let validation = crate::environment::parse_dotenv_content(path, value.as_str());
let validation = crate::parse_dotenv_content(path, value.as_str());
match validation {
std::result::Result::Ok(_) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(error),
@@ -937,7 +937,7 @@ fn read_dotenv_source(path: &std::path::Path) -> ksp_core_lib::Result<String> {
}
fn update_dotenv_source(path: &std::path::Path, source: &str, variable_name: &str, replacement: std::option::Option<&str>) -> ksp_core_lib::Result<String> {
let validation = crate::environment::parse_dotenv_content(path, source);
let validation = crate::parse_dotenv_content(path, source);
if let std::result::Result::Err(error) = validation {
return std::result::Result::Err(error);
}
@@ -967,7 +967,7 @@ fn update_dotenv_source(path: &std::path::Path, source: &str, variable_name: &st
if !candidate.is_empty() {
candidate.push('\n');
}
let candidate_validation = crate::environment::parse_dotenv_content(path, candidate.as_str());
let candidate_validation = crate::parse_dotenv_content(path, candidate.as_str());
return match candidate_validation {
std::result::Result::Ok(_) => std::result::Result::Ok(candidate),
std::result::Result::Err(error) => std::result::Result::Err(error),

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/registry.rs
// version: 5
// version: 6
/// Bootstrap argument used to replace a known Config filename mapping.
pub const ARG_FILE_MAP: &str = "--filemap";
@@ -165,7 +165,7 @@ impl ConfigFileRegistry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return build_registry([logging, logging_schema, transport, transport_schema, composite_schema]);
return crate::build_registry([logging, logging_schema, transport, transport_schema, composite_schema]);
}
/// Creates the default registry and applies repeatable `--filemap=<file_id>=<filename>` overrides from raw process arguments.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,34 +1,10 @@
// file: crates/ksp-logging-lib/src/domain.rs
// version: 1
// version: 2
std::thread_local! {
static CURRENT_DOMAIN: std::cell::RefCell<std::option::Option<std::string::String>> = const { std::cell::RefCell::new(std::option::Option::None) };
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
struct SpanDomain {
value: std::option::Option<std::string::String>,
}
#[derive(Default)]
struct DomainVisitor {
value: std::option::Option<std::string::String>,
}
impl tracing::field::Visit for DomainVisitor {
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
if field.name() == "domain" {
self.value = std::option::Option::Some(value.to_string());
}
}
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
if field.name() == "domain" {
self.value = std::option::Option::Some(format!("{value:?}"));
}
}
}
pub(crate) struct DomainContextLayer;
impl DomainContextLayer {
@@ -99,6 +75,30 @@ where
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
struct SpanDomain {
value: std::option::Option<std::string::String>,
}
#[derive(Default)]
struct DomainVisitor {
value: std::option::Option<std::string::String>,
}
impl tracing::field::Visit for DomainVisitor {
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
if field.name() == "domain" {
self.value = std::option::Option::Some(value.to_string());
}
}
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
if field.name() == "domain" {
self.value = std::option::Option::Some(format!("{value:?}"));
}
}
}
pub(crate) fn current_domain_matches(selectors: &[std::string::String]) -> bool {
if let [selector] = selectors
&& selector == "*"

View File

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

View File

@@ -1,5 +1,6 @@
// file: crates/ksp-logging-lib/src/lib.rs
// version: 8
// version: 9
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
@@ -69,6 +70,10 @@ pub use self::span::Span;
/// Instruments an asynchronous future with a KSP span.
pub use self::span::instrument;
pub(crate) use self::domain::current_domain_matches;
pub(crate) use self::writer::RoutedWriter;
pub(crate) use self::writer::StripAnsiWriter;
#[doc(hidden)]
/// Internal macro bridge. KSP consumers must not use this reexport directly.
pub extern crate tracing as __private_tracing;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/src/runtime.rs
// version: 13
// version: 14
use tracing_subscriber::Layer; // rust-rules: trait-import
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
@@ -228,31 +228,6 @@ pub fn initialize_with_identity(settings: &crate::LoggingSettings, identity: &cr
return initialize_runtime(settings, std::option::Option::Some(identity.clone()));
}
fn initialize_runtime(
settings: &crate::LoggingSettings,
runtime_identity: std::option::Option<crate::LoggingRuntimeIdentity>,
) -> ksp_core_lib::Result<crate::LoggingGuard> {
return prepare_runtime_with_identity(settings, runtime_identity.as_ref()).and_then(|prepared| -> ksp_core_lib::Result<crate::LoggingGuard> {
let PreparedRuntime { layers, outputs } = prepared;
let (reload_layer, reload_handle) = tracing_subscriber::reload::Layer::new(layers);
let subscriber = tracing_subscriber::registry().with(reload_layer);
let install_result = tracing::subscriber::set_global_default(subscriber);
return match install_result {
std::result::Result::Ok(()) => std::result::Result::Ok(crate::LoggingGuard {
reload_handle,
settings: settings.clone(),
runtime_identity,
outputs,
retired_dropped_lines: crate::DroppedLines::zero(),
retired_file_dropped_lines: std::collections::HashMap::new(),
}),
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_ALREADY_INITIALIZED, "the global KSP tracing subscriber is already installed").with_source(error),
),
};
});
}
/// Replaces the active KSP logging settings and non-blocking outputs without reinstalling the global subscriber.
///
/// New runtime layers, writers and guards are fully prepared before the reload is attempted. If validation or preparation fails, the currently active
@@ -282,6 +257,31 @@ pub fn reinitialize(guard: &mut crate::LoggingGuard, settings: &crate::LoggingSe
});
}
fn initialize_runtime(
settings: &crate::LoggingSettings,
runtime_identity: std::option::Option<crate::LoggingRuntimeIdentity>,
) -> ksp_core_lib::Result<crate::LoggingGuard> {
return prepare_runtime_with_identity(settings, runtime_identity.as_ref()).and_then(|prepared| -> ksp_core_lib::Result<crate::LoggingGuard> {
let PreparedRuntime { layers, outputs } = prepared;
let (reload_layer, reload_handle) = tracing_subscriber::reload::Layer::new(layers);
let subscriber = tracing_subscriber::registry().with(reload_layer);
let install_result = tracing::subscriber::set_global_default(subscriber);
return match install_result {
std::result::Result::Ok(()) => std::result::Result::Ok(crate::LoggingGuard {
reload_handle,
settings: settings.clone(),
runtime_identity,
outputs,
retired_dropped_lines: crate::DroppedLines::zero(),
retired_file_dropped_lines: std::collections::HashMap::new(),
}),
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_ALREADY_INITIALIZED, "the global KSP tracing subscriber is already installed").with_source(error),
),
};
});
}
fn prepare_runtime_with_identity(
settings: &crate::LoggingSettings,
runtime_identity: std::option::Option<&crate::LoggingRuntimeIdentity>,
@@ -368,7 +368,7 @@ fn build_file_output(
);
},
};
let stripped_writer = crate::writer::StripAnsiWriter::new(appender);
let stripped_writer = crate::StripAnsiWriter::new(appender);
let thread_name = format!("ksp-logging-{}", file.output_id());
let prepared = build_non_blocking_output(stripped_writer, thread_name.as_str(), settings.span_events(), false, false, file.format(), file.filter());
let metadata = crate::RuntimeFileMetadata {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-logging-lib/src/writer.rs
// version: 3
// version: 4
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum StripAnsiState {
@@ -117,7 +117,7 @@ pub(crate) enum RoutedWriter<W> {
Disabled,
}
impl<W> std::io::Write for RoutedWriter<W>
impl<W> std::io::Write for crate::RoutedWriter<W>
where
W: std::io::Write,
{
@@ -140,17 +140,17 @@ impl<'writer, W> tracing_subscriber::fmt::MakeWriter<'writer> for RouteMakeWrite
where
W: tracing_subscriber::fmt::MakeWriter<'writer>,
{
type Writer = RoutedWriter<W::Writer>;
type Writer = crate::RoutedWriter<W::Writer>;
fn make_writer(&'writer self) -> Self::Writer {
return RoutedWriter::Enabled(tracing_subscriber::fmt::MakeWriter::make_writer(&self.inner));
return crate::RoutedWriter::Enabled(tracing_subscriber::fmt::MakeWriter::make_writer(&self.inner));
}
fn make_writer_for(&'writer self, metadata: &tracing::Metadata<'_>) -> Self::Writer {
if metadata_matches_filter(metadata, &self.filter) {
return RoutedWriter::Enabled(tracing_subscriber::fmt::MakeWriter::make_writer_for(&self.inner, metadata));
return crate::RoutedWriter::Enabled(tracing_subscriber::fmt::MakeWriter::make_writer_for(&self.inner, metadata));
}
return RoutedWriter::Disabled;
return crate::RoutedWriter::Disabled;
}
}
@@ -163,7 +163,7 @@ fn metadata_matches_filter(metadata: &tracing::Metadata<'_>, filter: &crate::Out
}) {
return false;
}
return crate::domain::current_domain_matches(filter.domains());
return crate::current_domain_matches(filter.domains());
}
fn level_is_enabled(level: &tracing::Level, filter: crate::LogFilterLevel) -> bool {

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/client.rs
// version: 5
// version: 6
/// Passive runtime availability reported for one logical HTTP endpoint or role.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -163,26 +163,6 @@ impl HttpEndpointSnapshot {
}
}
pub(crate) struct HttpEndpointHttpResponse {
status: u16,
retry_after: std::option::Option<std::time::Duration>,
body: std::vec::Vec<u8>,
}
impl HttpEndpointHttpResponse {
pub(crate) const fn status(&self) -> u16 {
return self.status;
}
pub(crate) const fn retry_after(&self) -> std::option::Option<std::time::Duration> {
return self.retry_after;
}
pub(crate) fn body(&self) -> &[u8] {
return self.body.as_slice();
}
}
/// Shareable logical HTTP endpoint client owned by KSP Transport.
///
/// The underlying `reqwest::Client` owns socket pooling. KSP keeps the configured URL private from diagnostics and exposes only safe routing metadata.
@@ -191,12 +171,6 @@ pub struct HttpEndpointClient {
inner: std::sync::Arc<HttpEndpointClientInner>,
}
struct HttpEndpointClientInner {
settings: crate::HttpEndpointSettings,
client: reqwest::Client,
role_runtimes: std::vec::Vec<std::sync::Arc<crate::resilience::HttpRoleRuntime>>,
}
impl HttpEndpointClient {
/// Builds one logical endpoint client from KSP-owned runtime settings.
pub fn new(settings: crate::HttpEndpointSettings) -> ksp_core_lib::Result<Self> {
@@ -221,7 +195,7 @@ impl HttpEndpointClient {
};
let mut role_runtimes = std::vec::Vec::with_capacity(settings.roles().len());
for role in settings.roles() {
role_runtimes.push(std::sync::Arc::new(crate::resilience::HttpRoleRuntime::new(role, std::sync::Arc::clone(&notify))));
role_runtimes.push(std::sync::Arc::new(crate::HttpRoleRuntime::new(role, std::sync::Arc::clone(&notify))));
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
@@ -352,7 +326,7 @@ impl HttpEndpointClient {
&self,
role: &crate::HttpRoleName,
request_kind: &crate::HttpRequestKind,
) -> std::option::Option<(u32, std::sync::Arc<crate::resilience::HttpRoleRuntime>)> {
) -> std::option::Option<(u32, std::sync::Arc<crate::HttpRoleRuntime>)> {
if !self.enabled() {
return std::option::Option::None;
}
@@ -419,10 +393,36 @@ impl std::fmt::Debug for HttpEndpointClient {
}
}
pub(crate) struct HttpEndpointHttpResponse {
status: u16,
retry_after: std::option::Option<std::time::Duration>,
body: std::vec::Vec<u8>,
}
impl HttpEndpointHttpResponse {
pub(crate) const fn status(&self) -> u16 {
return self.status;
}
pub(crate) const fn retry_after(&self) -> std::option::Option<std::time::Duration> {
return self.retry_after;
}
pub(crate) fn body(&self) -> &[u8] {
return self.body.as_slice();
}
}
struct HttpEndpointClientInner {
settings: crate::HttpEndpointSettings,
client: reqwest::Client,
role_runtimes: std::vec::Vec<std::sync::Arc<crate::HttpRoleRuntime>>,
}
fn role_snapshot(
role: &crate::HttpEndpointRoleSettings,
request_kinds: std::vec::Vec<std::string::String>,
runtime: &crate::resilience::HttpRoleRuntime,
runtime: &crate::HttpRoleRuntime,
) -> crate::HttpEndpointRoleSnapshot {
let availability = if role.enabled() { runtime.availability(std::time::Instant::now()) } else { crate::HttpEndpointAvailability::Disabled };
return crate::HttpEndpointRoleSnapshot {

View File

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

View File

@@ -1,5 +1,6 @@
// file: crates/ksp-onchain-transport-lib/src/lib.rs
// version: 19
// version: 20
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
@@ -34,8 +35,6 @@ mod rpc_tokens;
mod rpc_transactions;
mod settings;
pub(crate) use self::constants::TRACING_TARGET;
/// Passive runtime availability reported for one logical HTTP endpoint.
pub use self::client::HttpEndpointAvailability;
/// Shareable logical HTTP endpoint client owned by KSP Transport.
@@ -296,3 +295,8 @@ pub use self::settings::HttpRoleLimits;
pub use self::settings::HttpRoleName;
/// Complete runtime settings consumed by the Solana HTTP transport foundation.
pub use self::settings::HttpTransportSettings;
pub(crate) use self::constants::TRACING_TARGET;
pub(crate) use self::resilience::HttpConcurrencyPermit;
pub(crate) use self::resilience::HttpRoleRuntime;
pub(crate) use self::resilience::RoleAdmissionAttempt;

View File

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

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/resilience.rs
// version: 1
// version: 2
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,61 +71,6 @@ impl HttpRetryDecision {
}
}
/// Evaluates the centralized bounded HTTP retry policy for one audited RPC method.
///
/// `completed_retries` counts retries already performed after the initial attempt. Provider `Retry-After` values are defensively bounded to sixty seconds
/// before they can extend the local exponential backoff. RPC application errors are never converted into transport retries.
#[must_use]
pub fn evaluate_transport_retry(
method: &crate::HttpRpcMethodDescriptor,
settings: &crate::HttpRetrySettings,
cause: crate::HttpRetryCause,
dispatch_state: crate::HttpDispatchState,
completed_retries: u32,
provider_retry_after: std::option::Option<std::time::Duration>,
) -> crate::HttpRetryDecision {
if completed_retries >= settings.max_retries() || !cause.is_retryable() {
return crate::HttpRetryDecision::Stop;
}
if method.transport_retry_class() == crate::TransportRetryClass::NotApplicable {
return crate::HttpRetryDecision::Stop;
}
if method.transport_retry_class() == crate::TransportRetryClass::NeverAfterDispatch && dispatch_state == crate::HttpDispatchState::DispatchedAmbiguous {
return crate::HttpRetryDecision::Stop;
}
let retry_number = completed_retries.saturating_add(1);
let mut delay = retry_backoff(settings, retry_number);
if cause == crate::HttpRetryCause::RateLimited
&& let std::option::Option::Some(provider_delay) = provider_retry_after
{
let bounded_provider_delay = std::cmp::min(provider_delay, MAX_PROVIDER_RETRY_AFTER);
if bounded_provider_delay > delay {
delay = bounded_provider_delay;
}
}
return crate::HttpRetryDecision::RetryAfter(delay);
}
pub(crate) fn retry_backoff(settings: &crate::HttpRetrySettings, retry_number: u32) -> std::time::Duration {
let mut delay = settings.initial_backoff();
if retry_number <= 1 {
return std::cmp::min(delay, settings.max_backoff());
}
let mut step = 1_u32;
while step < retry_number {
let doubled = match delay.checked_mul(2) {
std::option::Option::Some(value) => value,
std::option::Option::None => settings.max_backoff(),
};
delay = std::cmp::min(doubled, settings.max_backoff());
if delay >= settings.max_backoff() {
return settings.max_backoff();
}
step = step.saturating_add(1);
}
return delay;
}
pub(crate) struct HttpRoleRuntime {
limits: crate::HttpRoleLimits,
bucket: std::sync::Mutex<std::option::Option<HttpTokenBucketState>>,
@@ -216,21 +161,21 @@ impl HttpRoleRuntime {
return self.rate_limit_count.load(std::sync::atomic::Ordering::Relaxed);
}
pub(crate) fn try_acquire(self: &std::sync::Arc<Self>, now: std::time::Instant) -> RoleAdmissionAttempt {
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) {
std::option::Option::Some(value) => value,
std::option::Option::None => now,
};
return RoleAdmissionAttempt::BlockedUntil(ready_at);
return crate::RoleAdmissionAttempt::BlockedUntil(ready_at);
}
let semaphore_permit = match &self.semaphore {
std::option::Option::Some(semaphore) => {
let permit_result = std::sync::Arc::clone(semaphore).try_acquire_owned();
match permit_result {
std::result::Result::Ok(permit) => std::option::Option::Some(permit),
std::result::Result::Err(tokio::sync::TryAcquireError::NoPermits) => return RoleAdmissionAttempt::ConcurrencySaturated,
std::result::Result::Err(tokio::sync::TryAcquireError::Closed) => return RoleAdmissionAttempt::Unavailable,
std::result::Result::Err(tokio::sync::TryAcquireError::NoPermits) => return crate::RoleAdmissionAttempt::ConcurrencySaturated,
std::result::Result::Err(tokio::sync::TryAcquireError::Closed) => return crate::RoleAdmissionAttempt::Unavailable,
}
},
std::option::Option::None => std::option::Option::None,
@@ -239,9 +184,9 @@ impl HttpRoleRuntime {
if let std::option::Option::Some(ready_at) = token_result {
drop(semaphore_permit);
self.notify.notify_one();
return RoleAdmissionAttempt::BlockedUntil(ready_at);
return crate::RoleAdmissionAttempt::BlockedUntil(ready_at);
}
return RoleAdmissionAttempt::Ready(HttpConcurrencyPermit { semaphore_permit, notify: std::sync::Arc::clone(&self.notify) });
return crate::RoleAdmissionAttempt::Ready(HttpConcurrencyPermit { semaphore_permit, notify: std::sync::Arc::clone(&self.notify) });
}
pub(crate) fn record_success(&self) {
@@ -385,6 +330,61 @@ impl HttpTokenBucketState {
}
}
/// Evaluates the centralized bounded HTTP retry policy for one audited RPC method.
///
/// `completed_retries` counts retries already performed after the initial attempt. Provider `Retry-After` values are defensively bounded to sixty seconds
/// before they can extend the local exponential backoff. RPC application errors are never converted into transport retries.
#[must_use]
pub fn evaluate_transport_retry(
method: &crate::HttpRpcMethodDescriptor,
settings: &crate::HttpRetrySettings,
cause: crate::HttpRetryCause,
dispatch_state: crate::HttpDispatchState,
completed_retries: u32,
provider_retry_after: std::option::Option<std::time::Duration>,
) -> crate::HttpRetryDecision {
if completed_retries >= settings.max_retries() || !cause.is_retryable() {
return crate::HttpRetryDecision::Stop;
}
if method.transport_retry_class() == crate::TransportRetryClass::NotApplicable {
return crate::HttpRetryDecision::Stop;
}
if method.transport_retry_class() == crate::TransportRetryClass::NeverAfterDispatch && dispatch_state == crate::HttpDispatchState::DispatchedAmbiguous {
return crate::HttpRetryDecision::Stop;
}
let retry_number = completed_retries.saturating_add(1);
let mut delay = retry_backoff(settings, retry_number);
if cause == crate::HttpRetryCause::RateLimited
&& let std::option::Option::Some(provider_delay) = provider_retry_after
{
let bounded_provider_delay = std::cmp::min(provider_delay, MAX_PROVIDER_RETRY_AFTER);
if bounded_provider_delay > delay {
delay = bounded_provider_delay;
}
}
return crate::HttpRetryDecision::RetryAfter(delay);
}
fn retry_backoff(settings: &crate::HttpRetrySettings, retry_number: u32) -> std::time::Duration {
let mut delay = settings.initial_backoff();
if retry_number <= 1 {
return std::cmp::min(delay, settings.max_backoff());
}
let mut step = 1_u32;
while step < retry_number {
let doubled = match delay.checked_mul(2) {
std::option::Option::Some(value) => value,
std::option::Option::None => settings.max_backoff(),
};
delay = std::cmp::min(doubled, settings.max_backoff());
if delay >= settings.max_backoff() {
return settings.max_backoff();
}
step = step.saturating_add(1);
}
return delay;
}
#[cfg(test)]
#[path = "../unit_tests/resilience.rs"]
mod tests;

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_cluster.rs
// version: 4
// version: 5
const MAX_GET_SLOT_LEADERS: u64 = 5_000;
@@ -807,55 +807,6 @@ impl crate::HttpTransportPool {
}
}
fn push_context_config(params: &mut std::vec::Vec<serde_json::Value>, config: std::option::Option<&crate::SolanaContextConfig>) {
if let std::option::Option::Some(config) = config
&& (config.commitment().is_some() || config.min_context_slot().is_some())
{
params.push((*config).to_json_value());
}
return;
}
fn decode_pubkey_list(method: &str, field: &'static str, value: serde_json::Value) -> ksp_core_lib::Result<std::vec::Vec<ksp_core_lib::Pubkey>> {
let decoded = crate::decode_wire_json::<std::vec::Vec<std::string::String>>(method, value);
let values = match decoded {
std::result::Result::Ok(values) => values,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut pubkeys = std::vec::Vec::with_capacity(values.len());
for value in values {
let pubkey = crate::parse_wire_pubkey(method, field, value.as_str());
match pubkey {
std::result::Result::Ok(pubkey) => pubkeys.push(pubkey),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
}
return std::result::Result::Ok(pubkeys);
}
fn invalid_cluster_parameters<T>(method: &str, message: &str, field: &'static str, value: u64) -> ksp_core_lib::Result<T> {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, message)
.with_context("rpc_method", method)
.with_context(field, value.to_string()),
);
}
fn cluster_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> {
let descriptor = crate::find_http_rpc_method(method);
return match descriptor {
std::option::Option::Some(descriptor)
if descriptor.category() == crate::HttpRpcCategory::Cluster && descriptor.coverage_release() == crate::HttpRpcCoverageRelease::V0_2_2 =>
{
std::result::Result::Ok(descriptor)
},
_ => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Cluster descriptor is missing from the audited 0.2.2 registry")
.with_context("rpc_method", method),
),
};
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WireClusterNode {
@@ -938,6 +889,55 @@ struct WireVoteAccountStatus {
delinquent: std::vec::Vec<serde_json::Value>,
}
fn push_context_config(params: &mut std::vec::Vec<serde_json::Value>, config: std::option::Option<&crate::SolanaContextConfig>) {
if let std::option::Option::Some(config) = config
&& (config.commitment().is_some() || config.min_context_slot().is_some())
{
params.push((*config).to_json_value());
}
return;
}
fn decode_pubkey_list(method: &str, field: &'static str, value: serde_json::Value) -> ksp_core_lib::Result<std::vec::Vec<ksp_core_lib::Pubkey>> {
let decoded = crate::decode_wire_json::<std::vec::Vec<std::string::String>>(method, value);
let values = match decoded {
std::result::Result::Ok(values) => values,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut pubkeys = std::vec::Vec::with_capacity(values.len());
for value in values {
let pubkey = crate::parse_wire_pubkey(method, field, value.as_str());
match pubkey {
std::result::Result::Ok(pubkey) => pubkeys.push(pubkey),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
}
return std::result::Result::Ok(pubkeys);
}
fn invalid_cluster_parameters<T>(method: &str, message: &str, field: &'static str, value: u64) -> ksp_core_lib::Result<T> {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, message)
.with_context("rpc_method", method)
.with_context(field, value.to_string()),
);
}
fn cluster_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> {
let descriptor = crate::find_http_rpc_method(method);
return match descriptor {
std::option::Option::Some(descriptor)
if descriptor.category() == crate::HttpRpcCategory::Cluster && descriptor.coverage_release() == crate::HttpRpcCoverageRelease::V0_2_2 =>
{
std::result::Result::Ok(descriptor)
},
_ => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Cluster descriptor is missing from the audited 0.2.2 registry")
.with_context("rpc_method", method),
),
};
}
#[cfg(test)]
#[path = "../unit_tests/rpc_cluster.rs"]
mod tests;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
// file: crates/ksp-wallet-lib/src/crypto.rs
// version: 2
// version: 3
//! In-memory cryptographic primitives for native `.kspwallet` V1.
use chacha20poly1305::KeyInit as _;
use chacha20poly1305::aead::Aead as _;
use chacha20poly1305::KeyInit; // rust-rules: derive-import
use chacha20poly1305::aead::Aead; // rust-rules: derive-import
/// Exact V1 content-key and password-derived-key size in bytes.
pub(crate) const SECRET_KEY_BYTES: usize = 32;
@@ -14,7 +14,7 @@ pub(crate) struct SecretKeyV1 {
bytes: [u8; SECRET_KEY_BYTES],
}
impl SecretKeyV1 {
impl crate::SecretKeyV1 {
/// Takes ownership of exact 32-byte secret material.
pub(crate) const fn from_bytes(bytes: [u8; SECRET_KEY_BYTES]) -> Self {
return Self { bytes };
@@ -35,13 +35,13 @@ impl SecretKeyV1 {
}
}
impl std::fmt::Debug for SecretKeyV1 {
impl std::fmt::Debug for crate::SecretKeyV1 {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.write_str("SecretKeyV1(<redacted>)");
}
}
impl std::ops::Drop for SecretKeyV1 {
impl std::ops::Drop for crate::SecretKeyV1 {
fn drop(&mut self) {
zeroize::Zeroize::zeroize(&mut self.bytes);
}
@@ -64,28 +64,28 @@ pub(crate) fn random_nonce() -> ksp_core_lib::Result<[u8; crate::KSPWALLET_V1_XC
}
/// Derives one V1 password wrapping key from serialized Argon2id parameters.
pub(crate) fn derive_password_key(password: &[u8], kdf: &crate::WalletKdfParametersV1) -> ksp_core_lib::Result<SecretKeyV1> {
pub(crate) fn derive_password_key(password: &[u8], kdf: &crate::WalletKdfParametersV1) -> ksp_core_lib::Result<crate::SecretKeyV1> {
return derive_argon2id(password, kdf.salt(), kdf.memory_kib(), kdf.iterations(), kdf.parallelism());
}
/// Wraps one 32-byte content key with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
pub(crate) fn wrap_key(
wrapping_key: &SecretKeyV1,
key_to_wrap: &SecretKeyV1,
wrapping_key: &crate::SecretKeyV1,
key_to_wrap: &crate::SecretKeyV1,
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
aad: &[u8],
) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return encrypt_bytes(wrapping_key, nonce, aad, key_to_wrap.as_bytes());
return crate::encrypt_bytes(wrapping_key, nonce, aad, key_to_wrap.as_bytes());
}
/// Unwraps one 32-byte content key and maps every AEAD authentication failure to the generic Wallet authentication error.
pub(crate) fn unwrap_key(
wrapping_key: &SecretKeyV1,
wrapping_key: &crate::SecretKeyV1,
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
aad: &[u8],
ciphertext: &[u8],
) -> ksp_core_lib::Result<SecretKeyV1> {
let plaintext_result = decrypt_bytes(wrapping_key, nonce, aad, ciphertext);
) -> ksp_core_lib::Result<crate::SecretKeyV1> {
let plaintext_result = crate::decrypt_bytes(wrapping_key, nonce, aad, ciphertext);
let mut plaintext = match plaintext_result {
std::result::Result::Ok(plaintext) => plaintext,
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -96,7 +96,7 @@ pub(crate) fn unwrap_key(
}
let converted = <[u8; SECRET_KEY_BYTES]>::try_from(plaintext.as_slice());
let key = match converted {
std::result::Result::Ok(key) => SecretKeyV1::from_bytes(key),
std::result::Result::Ok(key) => crate::SecretKeyV1::from_bytes(key),
std::result::Result::Err(_) => {
zeroize::Zeroize::zeroize(plaintext.as_mut_slice());
return std::result::Result::Err(authentication_error());
@@ -108,7 +108,7 @@ pub(crate) fn unwrap_key(
/// Encrypts bounded plaintext bytes with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
pub(crate) fn encrypt_bytes(
key: &SecretKeyV1,
key: &crate::SecretKeyV1,
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
aad: &[u8],
plaintext: &[u8],
@@ -129,7 +129,7 @@ pub(crate) fn encrypt_bytes(
/// Decrypts authenticated ciphertext bytes and returns a generic authentication error on tag failure.
pub(crate) fn decrypt_bytes(
key: &SecretKeyV1,
key: &crate::SecretKeyV1,
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
aad: &[u8],
ciphertext: &[u8],
@@ -148,7 +148,7 @@ pub(crate) fn decrypt_bytes(
};
}
pub(crate) fn derive_argon2id(password: &[u8], salt: &[u8], memory_kib: u32, iterations: u32, parallelism: u32) -> ksp_core_lib::Result<SecretKeyV1> {
fn derive_argon2id(password: &[u8], salt: &[u8], memory_kib: u32, iterations: u32, parallelism: u32) -> ksp_core_lib::Result<crate::SecretKeyV1> {
if password.is_empty() || password.len() > crate::KSPWALLET_V1_MAX_PASSWORD_BYTES {
return std::result::Result::Err(crypto_parameter_error());
}
@@ -178,7 +178,7 @@ pub(crate) fn derive_argon2id(password: &[u8], salt: &[u8], memory_kib: u32, ite
zeroize::Zeroize::zeroize(&mut output);
return std::result::Result::Err(crypto_parameter_error());
}
return std::result::Result::Ok(SecretKeyV1::from_bytes(output));
return std::result::Result::Ok(crate::SecretKeyV1::from_bytes(output));
}
fn randomness_error() -> ksp_core_lib::Error {

View File

@@ -1,5 +1,6 @@
// file: crates/ksp-wallet-lib/src/lib.rs
// version: 8
// version: 10
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
@@ -13,7 +14,8 @@
//! `0.2.5-pre.006` adds bounded async-first filesystem reads plus same-directory synchronized no-clobber publication for new native files. `0.2.5-pre.007`
//! adds Solana message signing, protected metadata administration, password rotation and strong VIEW disable/recreate with capability-bound atomic
//! replacement. `0.2.5-pre.008` adds bounded Solana CLI JSON and canonical full-keypair Base58 import/export adapters with safe inspection and no-clobber
//! native import/export publication. Public keys are consumed exclusively through the [`ksp_core_lib::Pubkey`] re-export owned by KSP Core, and behavioral
//! native import/export publication. `0.2.5-pre.009` adds adversarial security/compliance canaries and records the final dependency/interop audit before
//! documentation closure. Public keys are consumed exclusively through the [`ksp_core_lib::Pubkey`] re-export owned by KSP Core, and behavioral
//! observability uses only
//! `ksp-logging-lib` with the explicit crate target defined in `src/constants.rs`.
@@ -32,6 +34,10 @@ mod view;
mod wallet;
mod wire;
#[cfg(test)]
#[path = "../unit_tests/security.rs"]
mod security_tests;
/// Authorized capability represented by an unlocked Wallet handle.
pub use self::capability::WalletCapability;
/// Native `.kspwallet` V1 format version.
@@ -221,6 +227,26 @@ pub use self::wire::WalletViewDescriptorV1;
/// Wallet-owned tracing target used by the KSP logging facade.
pub(crate) use self::constants::TRACING_TARGET;
/// Exact V1 content-key and password-derived-key size in bytes.
pub(crate) use self::crypto::SECRET_KEY_BYTES;
/// Owned 32-byte secret key with redacted diagnostics and drop-time zeroization.
pub(crate) use self::crypto::SecretKeyV1;
/// Decrypts authenticated ciphertext bytes and returns a generic authentication error on tag failure.
pub(crate) use self::crypto::decrypt_bytes;
/// Derives one V1 password wrapping key from serialized Argon2id parameters.
pub(crate) use self::crypto::derive_password_key;
/// Encrypts bounded plaintext bytes with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
pub(crate) use self::crypto::encrypt_bytes;
/// Generates a fresh fixed-size byte array from the operating-system CSPRNG.
pub(crate) use self::crypto::random_bytes;
/// Generates a fresh XChaCha20-Poly1305 nonce from the operating-system CSPRNG.
pub(crate) use self::crypto::random_nonce;
/// Unwraps one 32-byte content key and maps every AEAD authentication failure to the generic Wallet authentication error.
pub(crate) use self::crypto::unwrap_key;
/// Wraps one 32-byte content key with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
pub(crate) use self::crypto::wrap_key;
pub(crate) use self::payload::MetadataPayloadV1;
pub(crate) use self::payload::decode_metadata_payload;
/// Internal no-clobber native persistence path shared by transfer adapters.
pub(crate) use self::persistence::persist_new_wallet_content_v1;
/// Internal deterministic compartment-AAD codec shared by Wallet crypto layers.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/owner.rs
// version: 4
// version: 5
/// Authorized OWNER capability handle.
///
@@ -119,7 +119,7 @@ impl WalletOwner {
destination: impl std::convert::AsRef<std::path::Path>,
alias: std::option::Option<std::string::String>,
) -> ksp_core_lib::Result<()> {
let mut payload = crate::payload::MetadataPayloadV1::from_info(&self.info);
let mut payload = crate::MetadataPayloadV1::from_info(&self.info);
let mutation_result = payload.set_alias(alias);
if let std::result::Result::Err(error) = mutation_result {
return std::result::Result::Err(error);
@@ -133,7 +133,7 @@ impl WalletOwner {
destination: impl std::convert::AsRef<std::path::Path>,
text: std::string::String,
) -> ksp_core_lib::Result<std::string::String> {
let mut payload = crate::payload::MetadataPayloadV1::from_info(&self.info);
let mut payload = crate::MetadataPayloadV1::from_info(&self.info);
let note_id = match payload.add_note(text) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -152,7 +152,7 @@ impl WalletOwner {
note_id: &str,
text: std::string::String,
) -> ksp_core_lib::Result<()> {
let mut payload = crate::payload::MetadataPayloadV1::from_info(&self.info);
let mut payload = crate::MetadataPayloadV1::from_info(&self.info);
let mutation_result = payload.update_note(note_id, text);
if let std::result::Result::Err(error) = mutation_result {
return std::result::Result::Err(error);
@@ -162,7 +162,7 @@ impl WalletOwner {
/// Deletes one protected note selected by its stable identifier.
pub async fn delete_note(&mut self, destination: impl std::convert::AsRef<std::path::Path>, note_id: &str) -> ksp_core_lib::Result<()> {
let mut payload = crate::payload::MetadataPayloadV1::from_info(&self.info);
let mut payload = crate::MetadataPayloadV1::from_info(&self.info);
let mutation_result = payload.delete_note(note_id);
if let std::result::Result::Err(error) = mutation_result {
return std::result::Result::Err(error);
@@ -274,7 +274,7 @@ impl WalletOwner {
async fn persist_metadata_payload(
&mut self,
destination: std::path::PathBuf,
payload: crate::payload::MetadataPayloadV1,
payload: crate::MetadataPayloadV1,
operation: &'static str,
) -> ksp_core_lib::Result<()> {
let (envelope, info) = match self.state.stage_metadata_payload(payload) {
@@ -292,6 +292,12 @@ impl WalletOwner {
}
}
impl std::fmt::Debug for WalletOwner {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.debug_struct("WalletOwner").field("info", &self.info).field("unlocked_state", &"<redacted>").finish();
}
}
async fn persist_staged(
destination: std::path::PathBuf,
expected_current: &crate::KspWalletEnvelopeV1,
@@ -304,12 +310,6 @@ async fn persist_staged(
return crate::persistence::replace_wallet_file_v1(destination, expected_current.clone(), serialized).await;
}
impl std::fmt::Debug for WalletOwner {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.debug_struct("WalletOwner").field("info", &self.info).field("unlocked_state", &"<redacted>").finish();
}
}
#[cfg(test)]
#[path = "../unit_tests/administration.rs"]
mod tests;

View File

@@ -1,10 +1,10 @@
// file: crates/ksp-wallet-lib/src/payload.rs
// version: 3
// version: 4
//! Plaintext payload codecs protected inside native `.kspwallet` V1 compartments.
use base64::Engine as _;
use std::str::FromStr as _;
use base64::Engine; // rust-rules: derive-import
use std::str::FromStr; // rust-rules: derive-import
pub(crate) struct MetadataPayloadV1 {
pubkey: ksp_core_lib::Pubkey,
@@ -12,7 +12,7 @@ pub(crate) struct MetadataPayloadV1 {
notes: std::vec::Vec<crate::WalletNote>,
}
impl MetadataPayloadV1 {
impl crate::MetadataPayloadV1 {
pub(crate) const fn pubkey(&self) -> &ksp_core_lib::Pubkey {
return &self.pubkey;
}
@@ -117,16 +117,16 @@ impl MetadataPayloadV1 {
pub(crate) struct OwnerControlMaterialV1 {
admin_signing_secret: [u8; crate::crypto::SECRET_KEY_BYTES],
metadata_key: crate::crypto::SecretKeyV1,
secret_key: crate::crypto::SecretKeyV1,
metadata_key: crate::SecretKeyV1,
secret_key: crate::SecretKeyV1,
}
impl OwnerControlMaterialV1 {
pub(crate) fn into_parts(mut self) -> ([u8; crate::crypto::SECRET_KEY_BYTES], crate::crypto::SecretKeyV1, crate::crypto::SecretKeyV1) {
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];
std::mem::swap(&mut admin_signing_secret, &mut self.admin_signing_secret);
let metadata_key = std::mem::replace(&mut self.metadata_key, crate::crypto::SecretKeyV1::from_bytes([0_u8; crate::crypto::SECRET_KEY_BYTES]));
let secret_key = std::mem::replace(&mut self.secret_key, crate::crypto::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::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]));
return (admin_signing_secret, metadata_key, secret_key);
}
}
@@ -155,7 +155,7 @@ struct RawMetadataNoteV1 {
pub(crate) fn encode_initial_metadata_payload(
pubkey: ksp_core_lib::Pubkey,
metadata: crate::WalletCreateMetadataV1,
) -> ksp_core_lib::Result<(std::vec::Vec<u8>, MetadataPayloadV1)> {
) -> ksp_core_lib::Result<(std::vec::Vec<u8>, crate::MetadataPayloadV1)> {
let (alias, note_texts) = metadata.into_parts();
let validation_result = validate_alias(alias.as_deref());
if let std::result::Result::Err(error) = validation_result {
@@ -183,7 +183,7 @@ pub(crate) fn encode_initial_metadata_payload(
notes.push(crate::WalletNote::new(note_id, text));
}
let payload = MetadataPayloadV1 { pubkey, alias, notes };
let payload = crate::MetadataPayloadV1 { pubkey, alias, notes };
let serialized = match payload.encode() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -191,7 +191,7 @@ pub(crate) fn encode_initial_metadata_payload(
return std::result::Result::Ok((serialized, payload));
}
pub(crate) fn decode_metadata_payload(source: &[u8]) -> ksp_core_lib::Result<MetadataPayloadV1> {
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"));
}
@@ -236,13 +236,13 @@ pub(crate) fn decode_metadata_payload(source: &[u8]) -> ksp_core_lib::Result<Met
}
notes.push(crate::WalletNote::new(raw_note.id, raw_note.text));
}
return std::result::Result::Ok(MetadataPayloadV1 { pubkey, alias: raw.alias, notes });
return std::result::Result::Ok(crate::MetadataPayloadV1 { pubkey, alias: raw.alias, notes });
}
pub(crate) fn encode_owner_control_payload(
admin_signing_secret: &[u8; crate::crypto::SECRET_KEY_BYTES],
metadata_key: &crate::crypto::SecretKeyV1,
secret_key: &crate::crypto::SecretKeyV1,
metadata_key: &crate::SecretKeyV1,
secret_key: &crate::SecretKeyV1,
) -> [u8; crate::KSPWALLET_V1_OWNER_CONTROL_PLAINTEXT_BYTES] {
let mut output = [0_u8; crate::KSPWALLET_V1_OWNER_CONTROL_PLAINTEXT_BYTES];
output[0..32].copy_from_slice(admin_signing_secret);
@@ -263,8 +263,8 @@ pub(crate) fn decode_owner_control_payload(source: &[u8]) -> ksp_core_lib::Resul
secret_key.copy_from_slice(&source[64..96]);
return std::result::Result::Ok(OwnerControlMaterialV1 {
admin_signing_secret,
metadata_key: crate::crypto::SecretKeyV1::from_bytes(metadata_key),
secret_key: crate::crypto::SecretKeyV1::from_bytes(secret_key),
metadata_key: crate::SecretKeyV1::from_bytes(metadata_key),
secret_key: crate::SecretKeyV1::from_bytes(secret_key),
});
}

View File

@@ -1,9 +1,10 @@
// file: crates/ksp-wallet-lib/src/persistence.rs
// version: 4
// version: 6
//! Async-first native Wallet V1 filesystem persistence.
use std::io::{Read as _, Write as _};
use std::io::Read; // rust-rules: derive-import
use std::io::Write; // rust-rules: derive-import
/// Creates a new native `.kspwallet` V1 at `destination` without overwriting an existing path.
///
@@ -82,26 +83,10 @@ pub async fn inspect_locked_wallet_file_v1(source: impl std::convert::AsRef<std:
return crate::inspect_locked_wallet_v1(bytes.as_slice());
}
async fn read_wallet_file_async(source: std::path::PathBuf) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
let task = tokio::task::spawn_blocking(move || return read_wallet_file_blocking(source.as_path()));
return match task.await {
std::result::Result::Ok(result) => result,
std::result::Result::Err(error) => std::result::Result::Err(blocking_io_error("read_task", error)),
};
}
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;
}
async fn persist_new_wallet_async(destination: std::path::PathBuf, content: std::vec::Vec<u8>) -> ksp_core_lib::Result<()> {
let task = tokio::task::spawn_blocking(move || return persist_new_wallet_blocking(destination.as_path(), content.as_slice()));
return match task.await {
std::result::Result::Ok(result) => result,
std::result::Result::Err(error) => std::result::Result::Err(blocking_atomic_error("create_task", error)),
};
}
pub(crate) async fn replace_wallet_file_v1(
destination: std::path::PathBuf,
expected_current: crate::KspWalletEnvelopeV1,
@@ -116,6 +101,22 @@ pub(crate) async fn replace_wallet_file_v1(
};
}
async fn read_wallet_file_async(source: std::path::PathBuf) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
let task = tokio::task::spawn_blocking(move || return read_wallet_file_blocking(source.as_path()));
return match task.await {
std::result::Result::Ok(result) => result,
std::result::Result::Err(error) => std::result::Result::Err(blocking_io_error("read_task", error)),
};
}
async fn persist_new_wallet_async(destination: std::path::PathBuf, content: std::vec::Vec<u8>) -> ksp_core_lib::Result<()> {
let task = tokio::task::spawn_blocking(move || return persist_new_wallet_blocking(destination.as_path(), content.as_slice()));
return match task.await {
std::result::Result::Ok(result) => result,
std::result::Result::Err(error) => std::result::Result::Err(blocking_atomic_error("create_task", error)),
};
}
fn read_wallet_file_blocking(source: &std::path::Path) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
let opened = std::fs::File::open(source);
let file = match opened {

View File

@@ -1,10 +1,13 @@
// file: crates/ksp-wallet-lib/src/transfer.rs
// version: 1
// version: 2
//! Explicit OWNER-only Solana keypair import/export adapters.
use std::io::{Read as _, Write as _};
use zeroize::Zeroize as _;
use std::io::Read; // rust-rules: derive-import
use std::io::Write; // rust-rules: derive-import
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt; // rust-rules: derive-import
use zeroize::Zeroize; // rust-rules: derive-import
/// Explicit secret-transfer formats supported by Wallet `0.2.5`.
#[non_exhaustive]
@@ -317,8 +320,6 @@ fn destination_parent(destination: &std::path::Path) -> ksp_core_lib::Result<&st
#[cfg(unix)]
fn set_private_permissions_best_effort(file: &std::fs::File) {
use std::os::unix::fs::PermissionsExt as _;
let permissions = std::fs::Permissions::from_mode(0o600);
if let std::result::Result::Err(error) = file.set_permissions(permissions) {
ksp_logging_lib::warn!(

View File

@@ -1,16 +1,16 @@
// file: crates/ksp-wallet-lib/src/wallet.rs
// version: 5
// version: 7
//! In-memory native Wallet V1 create/open orchestration.
use ed25519_dalek::Signer as _;
use zeroize::Zeroize as _;
use ed25519_dalek::Signer; // rust-rules: derive-import
use zeroize::Zeroize; // rust-rules: derive-import
pub(crate) struct OwnerStateV1 {
envelope: crate::KspWalletEnvelopeV1,
owner_root: std::option::Option<crate::crypto::SecretKeyV1>,
metadata_key: std::option::Option<crate::crypto::SecretKeyV1>,
secret_key: std::option::Option<crate::crypto::SecretKeyV1>,
owner_root: std::option::Option<crate::SecretKeyV1>,
metadata_key: std::option::Option<crate::SecretKeyV1>,
secret_key: std::option::Option<crate::SecretKeyV1>,
admin_signing_key: std::option::Option<ed25519_dalek::SigningKey>,
solana_keypair: std::option::Option<solana_keypair::Keypair>,
}
@@ -18,9 +18,9 @@ pub(crate) struct OwnerStateV1 {
impl OwnerStateV1 {
pub(crate) fn new(
envelope: crate::KspWalletEnvelopeV1,
owner_root: crate::crypto::SecretKeyV1,
metadata_key: crate::crypto::SecretKeyV1,
secret_key: crate::crypto::SecretKeyV1,
owner_root: crate::SecretKeyV1,
metadata_key: crate::SecretKeyV1,
secret_key: crate::SecretKeyV1,
admin_signing_key: ed25519_dalek::SigningKey,
solana_keypair: solana_keypair::Keypair,
) -> Self {
@@ -43,7 +43,7 @@ impl OwnerStateV1 {
return;
}
pub(crate) fn apply_strong_view_state(&mut self, envelope: crate::KspWalletEnvelopeV1, metadata_key: crate::crypto::SecretKeyV1) {
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);
drop(previous);
@@ -80,8 +80,8 @@ impl OwnerStateV1 {
std::option::Option::None => return std::result::Result::Err(signing_error()),
};
let mut keypair_bytes = keypair.to_bytes();
let mut seed = [0_u8; crate::crypto::SECRET_KEY_BYTES];
seed.copy_from_slice(&keypair_bytes[0..crate::crypto::SECRET_KEY_BYTES]);
let mut seed = [0_u8; crate::SECRET_KEY_BYTES];
seed.copy_from_slice(&keypair_bytes[0..crate::SECRET_KEY_BYTES]);
let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed);
let signature = signing_key.sign(message).to_bytes();
seed.zeroize();
@@ -89,10 +89,7 @@ impl OwnerStateV1 {
return std::result::Result::Ok(signature);
}
pub(crate) fn stage_metadata_payload(
&self,
payload: crate::payload::MetadataPayloadV1,
) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::WalletInfo)> {
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,
std::option::Option::None => return std::result::Result::Err(crypto_operation_error()),
@@ -105,14 +102,14 @@ impl OwnerStateV1 {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let nonce = match crate::crypto::random_nonce() {
let nonce = match crate::random_nonce() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
plaintext.zeroize();
return std::result::Result::Err(error);
},
};
let encrypted = crate::crypto::encrypt_bytes(
let encrypted = crate::encrypt_bytes(
metadata_key,
&nonce,
self.envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(),
@@ -204,7 +201,7 @@ impl OwnerStateV1 {
return verify_and_return(envelope);
}
pub(crate) fn stage_disable_view(&self) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::crypto::SecretKeyV1)> {
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"));
}
@@ -214,12 +211,12 @@ impl OwnerStateV1 {
pub(crate) async fn stage_recreate_view(
&self,
new_password: crate::ViewPassword,
) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::crypto::SecretKeyV1)> {
let slot_id = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_SLOT_ID_BYTES }>() {
) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::SecretKeyV1)> {
let slot_id = match crate::random_bytes::<{ crate::KSPWALLET_V1_SLOT_ID_BYTES }>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let new_metadata_key = match crate::crypto::SecretKeyV1::random() {
let new_metadata_key = match crate::SecretKeyV1::random() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -234,8 +231,8 @@ impl OwnerStateV1 {
fn stage_strong_view_change(
&self,
view_slot: std::option::Option<crate::WalletKeySlotV1>,
) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::crypto::SecretKeyV1)> {
let new_metadata_key = match crate::crypto::SecretKeyV1::random() {
) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::SecretKeyV1)> {
let new_metadata_key = match crate::SecretKeyV1::random() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -245,8 +242,8 @@ impl OwnerStateV1 {
fn stage_strong_view_change_with_key(
&self,
view_slot: std::option::Option<crate::WalletKeySlotV1>,
new_metadata_key: crate::crypto::SecretKeyV1,
) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::crypto::SecretKeyV1)> {
new_metadata_key: crate::SecretKeyV1,
) -> ksp_core_lib::Result<(crate::KspWalletEnvelopeV1, crate::SecretKeyV1)> {
let current_metadata_key = match self.metadata_key.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(crypto_operation_error()),
@@ -264,7 +261,7 @@ impl OwnerStateV1 {
std::option::Option::None => return std::result::Result::Err(crypto_operation_error()),
};
let current_plaintext_result = crate::crypto::decrypt_bytes(
let current_plaintext_result = crate::decrypt_bytes(
current_metadata_key,
self.envelope.metadata().nonce(),
self.envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(),
@@ -274,14 +271,14 @@ impl OwnerStateV1 {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()),
};
let metadata_nonce = match crate::crypto::random_nonce() {
let metadata_nonce = match crate::random_nonce() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
metadata_plaintext.zeroize();
return std::result::Result::Err(error);
},
};
let metadata_ciphertext_result = crate::crypto::encrypt_bytes(
let metadata_ciphertext_result = crate::encrypt_bytes(
&new_metadata_key,
&metadata_nonce,
self.envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(),
@@ -297,14 +294,14 @@ impl OwnerStateV1 {
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);
admin_secret.zeroize();
let owner_control_nonce = match crate::crypto::random_nonce() {
let owner_control_nonce = match crate::random_nonce() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
owner_control_plaintext.zeroize();
return std::result::Result::Err(error);
},
};
let owner_control_ciphertext_result = crate::crypto::encrypt_bytes(
let owner_control_ciphertext_result = crate::encrypt_bytes(
owner_root,
&owner_control_nonce,
self.envelope.compartment_aad(crate::WalletCompartmentKindV1::OwnerControl).as_slice(),
@@ -357,11 +354,11 @@ impl std::ops::Drop for OwnerStateV1 {
pub(crate) struct ViewStateV1 {
envelope: crate::KspWalletEnvelopeV1,
metadata_key: std::option::Option<crate::crypto::SecretKeyV1>,
metadata_key: std::option::Option<crate::SecretKeyV1>,
}
impl ViewStateV1 {
pub(crate) fn new(envelope: crate::KspWalletEnvelopeV1, metadata_key: crate::crypto::SecretKeyV1) -> Self {
pub(crate) fn new(envelope: crate::KspWalletEnvelopeV1, metadata_key: crate::SecretKeyV1) -> Self {
return Self { envelope, metadata_key: std::option::Option::Some(metadata_key) };
}
@@ -411,6 +408,18 @@ impl std::ops::Drop for ViewStateV1 {
}
}
enum PasswordRotationV1 {
Owner(crate::OwnerPassword),
View(crate::ViewPassword),
}
struct ViewCreationMaterialV1 {
slot_id: [u8; crate::KSPWALLET_V1_SLOT_ID_BYTES],
kdf: crate::WalletKdfParametersV1,
wrap_nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
password: crate::ViewPassword,
}
/// Creates a new in-memory native `.kspwallet` V1 with a fresh Solana keypair.
///
/// The optional VIEW password creates an independent VIEW slot. This function performs no filesystem I/O; use [`crate::create_wallet_file_v1`] when
@@ -420,7 +429,7 @@ pub async fn create_wallet_v1(
view_password: std::option::Option<crate::ViewPassword>,
metadata: crate::WalletCreateMetadataV1,
) -> ksp_core_lib::Result<crate::WalletOwner> {
let mut solana_secret = match crate::crypto::random_bytes::<32>() {
let mut solana_secret = match crate::random_bytes::<32>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -429,26 +438,204 @@ pub async fn create_wallet_v1(
return crate::create_wallet_v1_from_keypair(owner_password, view_password, metadata, solana_keypair).await;
}
/// Opens the VIEW capability from a native `.kspwallet` V1 JSON document.
pub async fn open_wallet_view_v1(source: &[u8], password: crate::ViewPassword) -> ksp_core_lib::Result<crate::WalletView> {
let envelope = match crate::KspWalletEnvelopeV1::parse_json(source) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let verify_result = verify_state_signature(&envelope);
if let std::result::Result::Err(error) = verify_result {
return std::result::Result::Err(error);
}
let view_slot = match envelope.view_slot() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(capability_error("Wallet VIEW capability is disabled")),
};
let derived_result = derive_view_password_key_async(password, view_slot.kdf().clone()).await;
let derived = match derived_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let aad = envelope.view_slot_aad();
let view_aad = match aad {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(capability_error("Wallet VIEW capability is disabled")),
};
let metadata_key_result = crate::unwrap_key(&derived, view_slot.wrap().nonce(), view_aad.as_slice(), view_slot.wrap().ciphertext());
let metadata_key = match metadata_key_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(view_unlock_error()),
};
let metadata_plaintext_result = crate::decrypt_bytes(
&metadata_key,
envelope.metadata().nonce(),
envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(),
envelope.metadata().ciphertext(),
);
let mut metadata_plaintext = match metadata_plaintext_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(view_unlock_error()),
};
let metadata_payload_result = crate::decode_metadata_payload(metadata_plaintext.as_slice());
metadata_plaintext.zeroize();
let metadata_payload = match metadata_payload_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let info = metadata_payload.into_info(crate::WalletCapability::View);
let state = ViewStateV1::new(envelope, metadata_key);
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_open_view",
format_version = crate::KSPWALLET_FORMAT_VERSION_V1,
capability = "view",
"native wallet VIEW capability opened"
);
return std::result::Result::Ok(crate::WalletView::from_unlocked(info, state));
}
/// Opens the OWNER capability from a native `.kspwallet` V1 JSON document.
pub async fn open_wallet_owner_v1(source: &[u8], password: crate::OwnerPassword) -> ksp_core_lib::Result<crate::WalletOwner> {
let envelope = match crate::KspWalletEnvelopeV1::parse_json(source) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let verify_result = verify_state_signature(&envelope);
if let std::result::Result::Err(error) = verify_result {
return std::result::Result::Err(error);
}
let owner_slot = envelope.owner_slot();
let derived_result = derive_owner_password_key_async(password, owner_slot.kdf().clone()).await;
let derived = match derived_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let owner_root_result = crate::unwrap_key(&derived, owner_slot.wrap().nonce(), envelope.owner_slot_aad().as_slice(), owner_slot.wrap().ciphertext());
let owner_root = match owner_root_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(owner_unlock_error()),
};
let owner_control_plaintext_result = crate::decrypt_bytes(
&owner_root,
envelope.owner_control().nonce(),
envelope.compartment_aad(crate::WalletCompartmentKindV1::OwnerControl).as_slice(),
envelope.owner_control().ciphertext(),
);
let mut owner_control_plaintext = match owner_control_plaintext_result {
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());
owner_control_plaintext.zeroize();
let control = match control_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let (mut admin_secret, metadata_key, secret_key) = control.into_parts();
let admin_signing_key = ed25519_dalek::SigningKey::from_bytes(&admin_secret);
admin_secret.zeroize();
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(),
envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(),
envelope.metadata().ciphertext(),
);
let mut metadata_plaintext = match metadata_plaintext_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()),
};
let metadata_payload_result = crate::decode_metadata_payload(metadata_plaintext.as_slice());
metadata_plaintext.zeroize();
let metadata_payload = match metadata_payload_result {
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(),
envelope.compartment_aad(crate::WalletCompartmentKindV1::Secret).as_slice(),
envelope.secret().ciphertext(),
);
let mut secret_plaintext = match secret_plaintext_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()),
};
if secret_plaintext.len() != crate::KSPWALLET_V1_SECRET_PLAINTEXT_BYTES {
secret_plaintext.zeroize();
return std::result::Result::Err(key_material_error());
}
let keypair_result = solana_keypair::Keypair::try_from(secret_plaintext.as_slice());
let solana_keypair = match keypair_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
secret_plaintext.zeroize();
return std::result::Result::Err(key_material_error());
},
};
let secret_pubkey_result = pubkey_from_keypair_bytes(secret_plaintext.as_slice());
let secret_pubkey = match secret_pubkey_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
secret_plaintext.zeroize();
return std::result::Result::Err(error);
},
};
secret_plaintext.zeroize();
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!(
target: crate::TRACING_TARGET,
operation = "wallet_open_owner",
format_version = crate::KSPWALLET_FORMAT_VERSION_V1,
capability = "owner",
"native wallet OWNER capability opened"
);
return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, state));
}
/// Parses and verifies the OWNER-authenticated locked state without unlocking metadata or secret material.
pub fn inspect_locked_wallet_v1(source: &[u8]) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
let envelope = match crate::KspWalletEnvelopeV1::parse_json(source) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let verify_result = verify_state_signature(&envelope);
if let std::result::Result::Err(error) = verify_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(crate::LockedWalletInfo::new(envelope.view_descriptor().enabled()));
}
pub(crate) async fn create_wallet_v1_from_keypair(
owner_password: crate::OwnerPassword,
view_password: std::option::Option<crate::ViewPassword>,
metadata: crate::WalletCreateMetadataV1,
solana_keypair: solana_keypair::Keypair,
) -> ksp_core_lib::Result<crate::WalletOwner> {
let owner_root = match crate::crypto::SecretKeyV1::random() {
let owner_root = match crate::SecretKeyV1::random() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let metadata_key = match crate::crypto::SecretKeyV1::random() {
let metadata_key = match crate::SecretKeyV1::random() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let secret_key = match crate::crypto::SecretKeyV1::random() {
let secret_key = match crate::SecretKeyV1::random() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut admin_secret = match crate::crypto::random_bytes::<{ crate::crypto::SECRET_KEY_BYTES }>() {
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),
};
@@ -478,20 +665,20 @@ pub(crate) async fn create_wallet_v1_from_keypair(
let mut owner_control_plaintext = crate::payload::encode_owner_control_payload(&admin_secret, &metadata_key, &secret_key);
admin_secret.zeroize();
let owner_slot_id = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_SLOT_ID_BYTES }>() {
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) => {
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
},
};
let owner_salt = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES }>() {
let owner_salt = match crate::random_bytes::<{ crate::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES }>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
},
};
let owner_kdf = crate::WalletKdfParametersV1::new_creation(owner_salt.to_vec());
let owner_wrap_nonce = match crate::crypto::random_nonce() {
let owner_wrap_nonce = match crate::random_nonce() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
@@ -506,19 +693,19 @@ pub(crate) async fn create_wallet_v1_from_keypair(
},
};
let owner_control_nonce = match crate::crypto::random_nonce() {
let owner_control_nonce = match crate::random_nonce() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
},
};
let metadata_nonce = match crate::crypto::random_nonce() {
let metadata_nonce = match crate::random_nonce() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
},
};
let secret_nonce = match crate::crypto::random_nonce() {
let secret_nonce = match crate::random_nonce() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return cleanup_create_error(error, &mut owner_control_plaintext, &mut metadata_plaintext, &mut solana_keypair_bytes);
@@ -543,7 +730,7 @@ 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_wrapped_result = crate::crypto::wrap_key(&owner_derived, &owner_root, &owner_wrap_nonce, provisional.owner_slot_aad().as_slice());
let owner_wrapped_result = crate::wrap_key(&owner_derived, &owner_root, &owner_wrap_nonce, provisional.owner_slot_aad().as_slice());
let owner_wrapped = match owner_wrapped_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
@@ -559,7 +746,7 @@ pub(crate) async fn create_wallet_v1_from_keypair(
},
};
let owner_control_ciphertext_result = crate::crypto::encrypt_bytes(
let owner_control_ciphertext_result = crate::encrypt_bytes(
&owner_root,
&owner_control_nonce,
provisional.compartment_aad(crate::WalletCompartmentKindV1::OwnerControl).as_slice(),
@@ -573,7 +760,7 @@ pub(crate) async fn create_wallet_v1_from_keypair(
};
owner_control_plaintext.zeroize();
let metadata_ciphertext_result = crate::crypto::encrypt_bytes(
let metadata_ciphertext_result = crate::encrypt_bytes(
&metadata_key,
&metadata_nonce,
provisional.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(),
@@ -587,7 +774,7 @@ pub(crate) async fn create_wallet_v1_from_keypair(
};
metadata_plaintext.zeroize();
let secret_ciphertext_result = crate::crypto::encrypt_bytes(
let secret_ciphertext_result = crate::encrypt_bytes(
&secret_key,
&secret_nonce,
provisional.compartment_aad(crate::WalletCompartmentKindV1::Secret).as_slice(),
@@ -648,185 +835,6 @@ pub(crate) async fn create_wallet_v1_from_keypair(
return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, state));
}
/// Opens the VIEW capability from a native `.kspwallet` V1 JSON document.
pub async fn open_wallet_view_v1(source: &[u8], password: crate::ViewPassword) -> ksp_core_lib::Result<crate::WalletView> {
let envelope = match crate::KspWalletEnvelopeV1::parse_json(source) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let verify_result = verify_state_signature(&envelope);
if let std::result::Result::Err(error) = verify_result {
return std::result::Result::Err(error);
}
let view_slot = match envelope.view_slot() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(capability_error("Wallet VIEW capability is disabled")),
};
let derived_result = derive_view_password_key_async(password, view_slot.kdf().clone()).await;
let derived = match derived_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let aad = envelope.view_slot_aad();
let view_aad = match aad {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(capability_error("Wallet VIEW capability is disabled")),
};
let metadata_key_result = crate::crypto::unwrap_key(&derived, view_slot.wrap().nonce(), view_aad.as_slice(), view_slot.wrap().ciphertext());
let metadata_key = match metadata_key_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(view_unlock_error()),
};
let metadata_plaintext_result = crate::crypto::decrypt_bytes(
&metadata_key,
envelope.metadata().nonce(),
envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(),
envelope.metadata().ciphertext(),
);
let mut metadata_plaintext = match metadata_plaintext_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(view_unlock_error()),
};
let metadata_payload_result = crate::payload::decode_metadata_payload(metadata_plaintext.as_slice());
metadata_plaintext.zeroize();
let metadata_payload = match metadata_payload_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let info = metadata_payload.into_info(crate::WalletCapability::View);
let state = ViewStateV1::new(envelope, metadata_key);
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_open_view",
format_version = crate::KSPWALLET_FORMAT_VERSION_V1,
capability = "view",
"native wallet VIEW capability opened"
);
return std::result::Result::Ok(crate::WalletView::from_unlocked(info, state));
}
/// Opens the OWNER capability from a native `.kspwallet` V1 JSON document.
pub async fn open_wallet_owner_v1(source: &[u8], password: crate::OwnerPassword) -> ksp_core_lib::Result<crate::WalletOwner> {
let envelope = match crate::KspWalletEnvelopeV1::parse_json(source) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let verify_result = verify_state_signature(&envelope);
if let std::result::Result::Err(error) = verify_result {
return std::result::Result::Err(error);
}
let owner_slot = envelope.owner_slot();
let derived_result = derive_owner_password_key_async(password, owner_slot.kdf().clone()).await;
let derived = match derived_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let owner_root_result =
crate::crypto::unwrap_key(&derived, owner_slot.wrap().nonce(), envelope.owner_slot_aad().as_slice(), owner_slot.wrap().ciphertext());
let owner_root = match owner_root_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(owner_unlock_error()),
};
let owner_control_plaintext_result = crate::crypto::decrypt_bytes(
&owner_root,
envelope.owner_control().nonce(),
envelope.compartment_aad(crate::WalletCompartmentKindV1::OwnerControl).as_slice(),
envelope.owner_control().ciphertext(),
);
let mut owner_control_plaintext = match owner_control_plaintext_result {
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());
owner_control_plaintext.zeroize();
let control = match control_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let (mut admin_secret, metadata_key, secret_key) = control.into_parts();
let admin_signing_key = ed25519_dalek::SigningKey::from_bytes(&admin_secret);
admin_secret.zeroize();
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::crypto::decrypt_bytes(
&metadata_key,
envelope.metadata().nonce(),
envelope.compartment_aad(crate::WalletCompartmentKindV1::Metadata).as_slice(),
envelope.metadata().ciphertext(),
);
let mut metadata_plaintext = match metadata_plaintext_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()),
};
let metadata_payload_result = crate::payload::decode_metadata_payload(metadata_plaintext.as_slice());
metadata_plaintext.zeroize();
let metadata_payload = match metadata_payload_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let secret_plaintext_result = crate::crypto::decrypt_bytes(
&secret_key,
envelope.secret().nonce(),
envelope.compartment_aad(crate::WalletCompartmentKindV1::Secret).as_slice(),
envelope.secret().ciphertext(),
);
let mut secret_plaintext = match secret_plaintext_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(authentication_error()),
};
if secret_plaintext.len() != crate::KSPWALLET_V1_SECRET_PLAINTEXT_BYTES {
secret_plaintext.zeroize();
return std::result::Result::Err(key_material_error());
}
let keypair_result = solana_keypair::Keypair::try_from(secret_plaintext.as_slice());
let solana_keypair = match keypair_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
secret_plaintext.zeroize();
return std::result::Result::Err(key_material_error());
},
};
let secret_pubkey_result = pubkey_from_keypair_bytes(secret_plaintext.as_slice());
let secret_pubkey = match secret_pubkey_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
secret_plaintext.zeroize();
return std::result::Result::Err(error);
},
};
secret_plaintext.zeroize();
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!(
target: crate::TRACING_TARGET,
operation = "wallet_open_owner",
format_version = crate::KSPWALLET_FORMAT_VERSION_V1,
capability = "owner",
"native wallet OWNER capability opened"
);
return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, state));
}
/// Parses and verifies the OWNER-authenticated locked state without unlocking metadata or secret material.
pub fn inspect_locked_wallet_v1(source: &[u8]) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
let envelope = match crate::KspWalletEnvelopeV1::parse_json(source) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let verify_result = verify_state_signature(&envelope);
if let std::result::Result::Err(error) = verify_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(crate::LockedWalletInfo::new(envelope.view_descriptor().enabled()));
}
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 {
@@ -841,24 +849,19 @@ pub(crate) fn verify_state_signature(envelope: &crate::KspWalletEnvelopeV1) -> k
};
}
enum PasswordRotationV1 {
Owner(crate::OwnerPassword),
View(crate::ViewPassword),
}
async fn rewrap_slot(
envelope: &crate::KspWalletEnvelopeV1,
slot_id: [u8; crate::KSPWALLET_V1_SLOT_ID_BYTES],
role: crate::WalletKeySlotRoleV1,
capability_key: &crate::crypto::SecretKeyV1,
capability_key: &crate::SecretKeyV1,
password: PasswordRotationV1,
) -> ksp_core_lib::Result<crate::WalletKeySlotV1> {
let salt = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES }>() {
let salt = match crate::random_bytes::<{ crate::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES }>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let kdf = crate::WalletKdfParametersV1::new_creation(salt.to_vec());
let nonce = match crate::crypto::random_nonce() {
let nonce = match crate::random_nonce() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -874,10 +877,10 @@ async fn rewrap_slot(
slot_id,
role,
kdf.clone(),
crate::WalletKeyWrapV1::new(nonce, std::vec![0_u8; crate::crypto::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
crate::WalletKeyWrapV1::new(nonce, std::vec![0_u8; crate::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
);
let aad = crate::slot_aad(envelope, &provisional);
let wrapped = match crate::crypto::wrap_key(&derived, capability_key, &nonce, aad.as_slice()) {
let wrapped = match crate::wrap_key(&derived, capability_key, &nonce, aad.as_slice()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -925,27 +928,20 @@ fn verify_and_return(envelope: crate::KspWalletEnvelopeV1) -> ksp_core_lib::Resu
return std::result::Result::Ok(envelope);
}
struct ViewCreationMaterialV1 {
slot_id: [u8; crate::KSPWALLET_V1_SLOT_ID_BYTES],
kdf: crate::WalletKdfParametersV1,
wrap_nonce: [u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
password: crate::ViewPassword,
}
fn prepare_view_creation(password: std::option::Option<crate::ViewPassword>) -> ksp_core_lib::Result<std::option::Option<ViewCreationMaterialV1>> {
let password = match password {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
};
let slot_id = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_SLOT_ID_BYTES }>() {
let slot_id = match crate::random_bytes::<{ crate::KSPWALLET_V1_SLOT_ID_BYTES }>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let salt = match crate::crypto::random_bytes::<{ crate::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES }>() {
let salt = match crate::random_bytes::<{ crate::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES }>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let wrap_nonce = match crate::crypto::random_nonce() {
let wrap_nonce = match crate::random_nonce() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -960,7 +956,7 @@ fn prepare_view_creation(password: std::option::Option<crate::ViewPassword>) ->
async fn seal_view_slot(
envelope: &crate::KspWalletEnvelopeV1,
material: std::option::Option<ViewCreationMaterialV1>,
metadata_key: &crate::crypto::SecretKeyV1,
metadata_key: &crate::SecretKeyV1,
) -> ksp_core_lib::Result<std::option::Option<crate::WalletKeySlotV1>> {
let material = match material {
std::option::Option::Some(value) => value,
@@ -975,10 +971,10 @@ async fn seal_view_slot(
material.slot_id,
crate::WalletKeySlotRoleV1::View,
material.kdf.clone(),
crate::WalletKeyWrapV1::new(material.wrap_nonce, std::vec![0_u8; crate::crypto::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
crate::WalletKeyWrapV1::new(material.wrap_nonce, std::vec![0_u8; crate::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
);
let aad = crate::slot_aad(envelope, &provisional_slot);
let wrapped_result = crate::crypto::wrap_key(&derived, metadata_key, &material.wrap_nonce, aad.as_slice());
let wrapped_result = crate::wrap_key(&derived, metadata_key, &material.wrap_nonce, aad.as_slice());
let wrapped = match wrapped_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -1005,7 +1001,7 @@ fn provisional_envelope(
owner_slot_id,
crate::WalletKeySlotRoleV1::Owner,
owner_kdf,
crate::WalletKeyWrapV1::new(owner_wrap_nonce, std::vec![0_u8; crate::crypto::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
crate::WalletKeyWrapV1::new(owner_wrap_nonce, std::vec![0_u8; crate::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
);
let (view_descriptor, view_slot) = match view_material {
std::option::Option::Some(material) => (
@@ -1014,7 +1010,7 @@ fn provisional_envelope(
material.slot_id,
crate::WalletKeySlotRoleV1::View,
material.kdf.clone(),
crate::WalletKeyWrapV1::new(material.wrap_nonce, std::vec![0_u8; crate::crypto::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
crate::WalletKeyWrapV1::new(material.wrap_nonce, std::vec![0_u8; crate::SECRET_KEY_BYTES + crate::KSPWALLET_V1_AEAD_TAG_BYTES]),
)),
),
std::option::Option::None => (crate::WalletViewDescriptorV1::disabled(), std::option::Option::None),
@@ -1035,19 +1031,16 @@ fn provisional_envelope(
);
}
async fn derive_owner_password_key_async(
password: crate::OwnerPassword,
kdf: crate::WalletKdfParametersV1,
) -> ksp_core_lib::Result<crate::crypto::SecretKeyV1> {
let task = tokio::task::spawn_blocking(move || return crate::crypto::derive_password_key(password.as_bytes(), &kdf));
async fn derive_owner_password_key_async(password: crate::OwnerPassword, kdf: crate::WalletKdfParametersV1) -> ksp_core_lib::Result<crate::SecretKeyV1> {
let task = tokio::task::spawn_blocking(move || return crate::derive_password_key(password.as_bytes(), &kdf));
return match task.await {
std::result::Result::Ok(result) => result,
std::result::Result::Err(_) => std::result::Result::Err(crypto_operation_error()),
};
}
async fn derive_view_password_key_async(password: crate::ViewPassword, kdf: crate::WalletKdfParametersV1) -> ksp_core_lib::Result<crate::crypto::SecretKeyV1> {
let task = tokio::task::spawn_blocking(move || return crate::crypto::derive_password_key(password.as_bytes(), &kdf));
async fn derive_view_password_key_async(password: crate::ViewPassword, kdf: crate::WalletKdfParametersV1) -> ksp_core_lib::Result<crate::SecretKeyV1> {
let task = tokio::task::spawn_blocking(move || return crate::derive_password_key(password.as_bytes(), &kdf));
return match task.await {
std::result::Result::Ok(result) => result,
std::result::Result::Err(_) => std::result::Result::Err(crypto_operation_error()),

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-wallet-lib/src/wire.rs
// version: 5
// version: 6
//! Strict native `.kspwallet` V1 wire envelope.
use base64::Engine as _;
use base64::Engine; // rust-rules: derive-import
/// Password KDF supported by `.kspwallet` V1 key slots.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/tests/dependency_boundary.rs
// version: 9
// version: 10
//! Wallet-specific dependency and ownership canaries.
@@ -60,6 +60,8 @@ fn wallet_manifest_preserves_dependency_firewall() -> std::io::Result<()> {
"tauri",
"tracing =",
"solana-pubkey",
"solana-signer",
"solana-signature",
"bs58",
] {
assert!(!manifest.contains(forbidden), "forbidden direct Wallet dependency detected: {forbidden}");
@@ -90,3 +92,26 @@ fn wallet_sources_use_core_pubkey_logging_facade_and_no_environment() -> std::io
assert!(!all_source.contains(direct_tracing_path.as_str()));
return std::result::Result::Ok(());
}
#[test]
fn wallet_public_surface_does_not_reexport_secret_or_signer_types() -> std::io::Result<()> {
let lib_source = match std::fs::read_to_string(crate_root().join("src/lib.rs")) {
std::result::Result::Ok(source) => source,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
for forbidden in ["pub use solana_keypair", "pub use solana_signer", "pub use solana_signature"] {
assert!(!lib_source.contains(forbidden), "Wallet public surface reexports secret/signing implementation type: {forbidden}");
}
let view_source = match std::fs::read_to_string(crate_root().join("src/view.rs")) {
std::result::Result::Ok(source) => source,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert!(!view_source.contains("export_transfer("));
assert!(!view_source.contains("sign_message("));
let transfer_source = match std::fs::read_to_string(crate_root().join("src/transfer.rs")) {
std::result::Result::Ok(source) => source,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert!(transfer_source.contains("#[non_exhaustive]"));
return std::result::Result::Ok(());
}

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-wallet-lib/unit_tests/capability.rs
// version: 1
// version: 2
#[test]
fn capability_variants_are_distinct_and_stable() {
assert_ne!(super::WalletCapability::View, super::WalletCapability::Owner);
assert_eq!(format!("{:?}", super::WalletCapability::View), "View");
assert_eq!(format!("{:?}", super::WalletCapability::Owner), "Owner");
assert_ne!(crate::WalletCapability::View, crate::WalletCapability::Owner);
assert_eq!(format!("{:?}", crate::WalletCapability::View), "View");
assert_eq!(format!("{:?}", crate::WalletCapability::Owner), "Owner");
}

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-wallet-lib/unit_tests/crypto.rs
// version: 1
// version: 2
use base64::Engine as _;
use base64::Engine; // rust-rules: derive-import
const VECTOR: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_crypto_vectors.json");
@@ -70,15 +70,15 @@ fn deterministic_argon2id_and_xchacha_wrap_vector_matches_external_canary() -> k
};
assert_eq!(derived.as_bytes(), &expected_derived);
let content = super::SecretKeyV1::from_bytes(content_key);
let wrapped_result = super::wrap_key(&derived, &content, &nonce, aad.as_slice());
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 {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(wrapped, expected_wrapped);
let unwrapped_result = super::unwrap_key(&derived, &nonce, aad.as_slice(), wrapped.as_slice());
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,
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -89,16 +89,16 @@ fn deterministic_argon2id_and_xchacha_wrap_vector_matches_external_canary() -> k
#[test]
fn xchacha_tampering_is_reported_as_generic_authentication_failure() -> ksp_core_lib::Result<()> {
let key = super::SecretKeyV1::from_bytes([0x11_u8; 32]);
let key = crate::SecretKeyV1::from_bytes([0x11_u8; 32]);
let nonce = [0x22_u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES];
let plaintext = [0x33_u8; 32];
let encrypted_result = super::encrypt_bytes(&key, &nonce, b"kspwallet-test-aad", plaintext.as_slice());
let encrypted_result = crate::encrypt_bytes(&key, &nonce, b"kspwallet-test-aad", plaintext.as_slice());
let mut encrypted = match encrypted_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
encrypted[0] ^= 1;
let result = super::decrypt_bytes(&key, &nonce, b"kspwallet-test-aad", encrypted.as_slice());
let result = crate::decrypt_bytes(&key, &nonce, b"kspwallet-test-aad", encrypted.as_slice());
assert!(result.is_err());
let error = match result {
std::result::Result::Err(error) => error,
@@ -110,12 +110,12 @@ fn xchacha_tampering_is_reported_as_generic_authentication_failure() -> ksp_core
#[test]
fn secret_key_debug_is_redacted_and_random_sources_are_callable() -> ksp_core_lib::Result<()> {
let key_result = super::SecretKeyV1::random();
let key_result = crate::SecretKeyV1::random();
let key = match key_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let nonce_result = super::random_nonce();
let nonce_result = crate::random_nonce();
let nonce = match nonce_result {
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/metadata.rs
// version: 1
// version: 2
fn test_pubkey() -> ksp_core_lib::Pubkey {
return ksp_core_lib::PRGIDPK_SOLANA_SYSTEM;
@@ -7,8 +7,8 @@ fn test_pubkey() -> ksp_core_lib::Pubkey {
#[test]
fn authorized_info_exposes_metadata_after_authorization() {
let note = super::WalletNote { id: std::string::String::from("purpose"), text: std::string::String::from("devnet test wallet") };
let info = super::WalletInfo {
let note = crate::WalletNote { id: std::string::String::from("purpose"), text: std::string::String::from("devnet test wallet") };
let info = crate::WalletInfo {
format_version: 1,
capability: crate::WalletCapability::View,
pubkey: test_pubkey(),
@@ -25,11 +25,11 @@ fn authorized_info_exposes_metadata_after_authorization() {
#[test]
fn authorized_info_debug_redacts_alias_and_note_text() {
let note = super::WalletNote {
let note = crate::WalletNote {
id: std::string::String::from("NOTE-ID-CANARY"),
text: std::string::String::from("NOTE-SECRET-CANARY"),
};
let info = super::WalletInfo {
let info = crate::WalletInfo {
format_version: 1,
capability: crate::WalletCapability::Owner,
pubkey: test_pubkey(),
@@ -46,7 +46,7 @@ fn authorized_info_debug_redacts_alias_and_note_text() {
#[test]
fn locked_info_does_not_carry_authorized_identity_or_metadata() {
let info = super::LockedWalletInfo { format_version: 1, view_enabled: true };
let info = crate::LockedWalletInfo { format_version: 1, view_enabled: true };
assert_eq!(info.format_version(), 1);
assert!(info.view_enabled());
let rendered = format!("{info:?}");

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-wallet-lib/unit_tests/password.rs
// version: 1
// version: 2
#[test]
fn view_password_debug_is_redacted() {
let password = super::ViewPassword::new(std::string::String::from("VIEW-SECRET-CANARY"));
let password = crate::ViewPassword::new(std::string::String::from("VIEW-SECRET-CANARY"));
let rendered = format!("{password:?}");
assert_eq!(rendered, "ViewPassword(<redacted>)");
assert!(!rendered.contains("VIEW-SECRET-CANARY"));
@@ -11,7 +11,7 @@ fn view_password_debug_is_redacted() {
#[test]
fn owner_password_debug_is_redacted() {
let password = super::OwnerPassword::new(std::string::String::from("OWNER-SECRET-CANARY"));
let password = crate::OwnerPassword::new(std::string::String::from("OWNER-SECRET-CANARY"));
let rendered = format!("{password:?}");
assert_eq!(rendered, "OwnerPassword(<redacted>)");
assert!(!rendered.contains("OWNER-SECRET-CANARY"));
@@ -19,6 +19,6 @@ fn owner_password_debug_is_redacted() {
#[test]
fn password_wrappers_require_drop_for_zeroization() {
assert!(std::mem::needs_drop::<super::ViewPassword>());
assert!(std::mem::needs_drop::<super::OwnerPassword>());
assert!(std::mem::needs_drop::<crate::ViewPassword>());
assert!(std::mem::needs_drop::<crate::OwnerPassword>());
}

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-wallet-lib/unit_tests/payload.rs
// version: 1
// version: 2
use base64::Engine as _;
use base64::Engine; // rust-rules: derive-import
#[test]
fn metadata_payload_rejects_duplicate_note_identifiers() -> ksp_core_lib::Result<()> {
@@ -18,7 +18,7 @@ fn metadata_payload_rejects_duplicate_note_identifiers() -> ksp_core_lib::Result
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(test_error("test metadata payload could not be serialized")),
};
let result = super::decode_metadata_payload(encoded.as_slice());
let result = crate::decode_metadata_payload(encoded.as_slice());
let error = match result {
std::result::Result::Ok(_) => return std::result::Result::Err(test_error("duplicate note identifiers unexpectedly parsed")),
std::result::Result::Err(error) => error,

View File

@@ -0,0 +1,86 @@
// file: crates/ksp-wallet-lib/unit_tests/security.rs
// version: 2
//! Adversarial security canaries for native `.kspwallet` V1.
use base64::Engine; // rust-rules: derive-import
const FULL_VECTOR: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_full_vector.json");
#[test]
fn owner_signed_regions_reject_canonical_tampering_before_unlock() {
for pointer in [
"/key_slots/0/kdf/salt",
"/key_slots/0/wrap/ciphertext",
"/owner_control/ciphertext",
"/metadata/ciphertext",
"/secret/ciphertext",
"/state_signature/signature",
] {
let tampered = tamper_base64url_field(FULL_VECTOR, pointer);
let error = crate::inspect_locked_wallet_v1(tampered.as_slice()).expect_err("OWNER-signed canonical tampering must be rejected");
assert_eq!(error.code(), crate::ERROR_CODE_AUTHENTICATION_FAILED, "unexpected error code for tampered field {pointer}");
}
}
#[test]
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);
}
#[test]
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);
}
#[test]
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);
assert!(!view_error.to_string().contains(view_canary));
assert!(!format!("{view_error:?}").contains(view_canary));
}
fn tamper_base64url_field(source: &[u8], pointer: &str) -> std::vec::Vec<u8> {
let mut value: serde_json::Value = serde_json::from_slice(source).expect("security fixture must be valid JSON");
let target = value.pointer_mut(pointer).expect("security fixture pointer must exist");
let encoded = target.as_str().expect("security fixture target must be a Base64url string");
let mut decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(encoded.as_bytes()).expect("security fixture Base64url must decode");
let first = decoded.first_mut().expect("security fixture Base64url field must not be empty");
*first ^= 0x01;
*target = serde_json::Value::String(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(decoded.as_slice()));
zeroize::Zeroize::zeroize(decoded.as_mut_slice());
return serde_json::to_vec(&value).expect("tampered security fixture must serialize");
}
fn runtime() -> tokio::runtime::Runtime {
return tokio::runtime::Builder::new_current_thread().build().expect("security test runtime must be creatable");
}

View File

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

View File

@@ -1,7 +1,9 @@
// file: crates/ksp-wallet-lib/unit_tests/transfer.rs
// version: 1
// version: 2
use zeroize::Zeroize as _;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt; // rust-rules: derive-import
use zeroize::Zeroize; // rust-rules: derive-import
fn runtime() -> tokio::runtime::Runtime {
return tokio::runtime::Builder::new_current_thread().build().expect("Wallet transfer test runtime must build");
@@ -234,7 +236,6 @@ fn owner_transfer_file_export_is_no_clobber() {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
let mode = std::fs::metadata(export_path.as_path()).expect("export metadata must be readable").permissions().mode() & 0o777;
assert_eq!(mode, 0o600);
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/unit_tests/wallet.rs
// version: 2
// version: 3
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");
@@ -26,7 +26,7 @@ fn externally_generated_full_vector_opens_view_and_owner_independently() -> ksp_
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let view_result = runtime.block_on(super::open_wallet_view_v1(FULL_VECTOR, crate::ViewPassword::new(vector.view_password_utf8)));
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,
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -36,7 +36,7 @@ fn externally_generated_full_vector_opens_view_and_owner_independently() -> ksp_
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(super::open_wallet_owner_v1(FULL_VECTOR, crate::OwnerPassword::new(vector.owner_password_utf8)));
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,
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -59,21 +59,21 @@ fn wrong_passwords_do_not_cross_unlock_capabilities() -> ksp_core_lib::Result<()
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let wrong_view = runtime.block_on(super::open_wallet_view_v1(FULL_VECTOR, crate::ViewPassword::new(std::string::String::from("wrong-view-password"))));
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(super::open_wallet_owner_v1(FULL_VECTOR, crate::OwnerPassword::new(std::string::String::from("wrong-owner-password"))));
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(super::open_wallet_owner_v1(FULL_VECTOR, crate::OwnerPassword::new(vector.view_password_utf8)));
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")),
std::result::Result::Err(error) => error,
@@ -88,7 +88,7 @@ fn owner_signed_metadata_tampering_is_rejected_before_unlock() -> ksp_core_lib::
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let inspect_result = super::inspect_locked_wallet_v1(tampered.as_slice());
let inspect_result = crate::inspect_locked_wallet_v1(tampered.as_slice());
let error = match inspect_result {
std::result::Result::Ok(_) => return std::result::Result::Err(test_error("tampered OWNER-signed metadata unexpectedly verified")),
std::result::Result::Err(error) => error,
@@ -107,7 +107,7 @@ fn view_wrap_can_change_without_breaking_owner_authenticated_state() -> ksp_core
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let inspected = match super::inspect_locked_wallet_v1(tampered.as_slice()) {
let inspected = match crate::inspect_locked_wallet_v1(tampered.as_slice()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -117,12 +117,12 @@ fn view_wrap_can_change_without_breaking_owner_authenticated_state() -> ksp_core
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let owner_result = runtime.block_on(super::open_wallet_owner_v1(tampered.as_slice(), crate::OwnerPassword::new(vector.owner_password_utf8)));
let owner_result = runtime.block_on(crate::open_wallet_owner_v1(tampered.as_slice(), crate::OwnerPassword::new(vector.owner_password_utf8)));
if let std::result::Result::Err(error) = owner_result {
return std::result::Result::Err(error);
}
let view_result = runtime.block_on(super::open_wallet_view_v1(tampered.as_slice(), crate::ViewPassword::new(vector.view_password_utf8)));
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")),
std::result::Result::Err(error) => error,
@@ -147,7 +147,7 @@ fn locked_full_vector_contains_no_authorized_identity_or_metadata_plaintext() ->
for note in vector.expected_notes {
assert!(!document.contains(note.as_str()));
}
let locked = match super::inspect_locked_wallet_v1(FULL_VECTOR) {
let locked = match crate::inspect_locked_wallet_v1(FULL_VECTOR) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -181,7 +181,7 @@ fn create_uses_calibrated_defaults_and_keeps_locked_projection_private() -> ksp_
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let owner_result = runtime.block_on(super::create_wallet_v1(
let owner_result = runtime.block_on(crate::create_wallet_v1(
crate::OwnerPassword::new(std::string::String::from("pre005-create-owner-password")),
std::option::Option::None,
metadata,

View File

@@ -1,18 +1,18 @@
// file: crates/ksp-wallet-lib/unit_tests/wire.rs
// version: 2
// version: 3
const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_wire_only.json");
#[test]
fn strict_v1_fixture_parses_and_round_trips_semantically() -> ksp_core_lib::Result<()> {
let parsed = match super::KspWalletEnvelopeV1::parse_json(FIXTURE) {
let parsed = match crate::KspWalletEnvelopeV1::parse_json(FIXTURE) {
std::result::Result::Ok(parsed) => parsed,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(parsed.format_version(), crate::KSPWALLET_FORMAT_VERSION_V1);
assert!(parsed.view_descriptor().enabled());
assert_eq!(parsed.owner_slot().role(), super::WalletKeySlotRoleV1::Owner);
assert_eq!(parsed.view_slot().map(super::WalletKeySlotV1::role), std::option::Option::Some(super::WalletKeySlotRoleV1::View));
assert_eq!(parsed.owner_slot().role(), crate::WalletKeySlotRoleV1::Owner);
assert_eq!(parsed.view_slot().map(crate::WalletKeySlotV1::role), std::option::Option::Some(crate::WalletKeySlotRoleV1::View));
assert_eq!(parsed.owner_control().payload_version(), 1);
assert_eq!(parsed.metadata().payload_version(), 1);
assert_eq!(parsed.secret().payload_version(), 1);
@@ -21,7 +21,7 @@ fn strict_v1_fixture_parses_and_round_trips_semantically() -> ksp_core_lib::Resu
std::result::Result::Ok(serialized) => serialized,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let reparsed = match super::KspWalletEnvelopeV1::parse_json(serialized.as_slice()) {
let reparsed = match crate::KspWalletEnvelopeV1::parse_json(serialized.as_slice()) {
std::result::Result::Ok(reparsed) => reparsed,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -32,7 +32,7 @@ fn strict_v1_fixture_parses_and_round_trips_semantically() -> ksp_core_lib::Resu
#[test]
fn unknown_format_version_is_rejected_before_v1_shape_validation() {
let source = std::string::String::from_utf8_lossy(FIXTURE).replace("\"format_version\": 1", "\"format_version\": 2");
let result = super::KspWalletEnvelopeV1::parse_json(source.as_bytes());
let result = crate::KspWalletEnvelopeV1::parse_json(source.as_bytes());
assert!(result.is_err());
let error = match result {
std::result::Result::Err(error) => error,
@@ -44,7 +44,7 @@ fn unknown_format_version_is_rejected_before_v1_shape_validation() {
#[test]
fn unknown_top_level_field_is_rejected() {
let source = std::string::String::from_utf8_lossy(FIXTURE).replacen("{", "{\n \"unexpected\": true,", 1);
let result = super::KspWalletEnvelopeV1::parse_json(source.as_bytes());
let result = crate::KspWalletEnvelopeV1::parse_json(source.as_bytes());
assert!(result.is_err());
let error = match result {
std::result::Result::Err(error) => error,
@@ -57,7 +57,7 @@ fn unknown_top_level_field_is_rejected() {
fn padded_or_noncanonical_base64url_is_rejected() {
let source = std::string::String::from_utf8_lossy(FIXTURE)
.replace("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8\"", "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=\"");
let result = super::KspWalletEnvelopeV1::parse_json(source.as_bytes());
let result = crate::KspWalletEnvelopeV1::parse_json(source.as_bytes());
assert!(result.is_err());
let error = match result {
std::result::Result::Err(error) => error,
@@ -70,7 +70,7 @@ fn padded_or_noncanonical_base64url_is_rejected() {
fn enabled_view_descriptor_must_match_the_view_slot() {
let source =
std::string::String::from_utf8_lossy(FIXTURE).replacen("\"slot_id\": \"ICEiIyQlJicoKSorLC0uLw\"", "\"slot_id\": \"EBESExQVFhcYGRobHB0eHw\"", 1);
let result = super::KspWalletEnvelopeV1::parse_json(source.as_bytes());
let result = crate::KspWalletEnvelopeV1::parse_json(source.as_bytes());
assert!(result.is_err());
let error = match result {
std::result::Result::Err(error) => error,
@@ -82,7 +82,7 @@ fn enabled_view_descriptor_must_match_the_view_slot() {
#[test]
fn zero_or_pathological_kdf_parameters_are_rejected_before_crypto() {
let zero_source = std::string::String::from_utf8_lossy(FIXTURE).replacen("\"memory_kib\": 65536", "\"memory_kib\": 0", 1);
let zero_result = super::KspWalletEnvelopeV1::parse_json(zero_source.as_bytes());
let zero_result = crate::KspWalletEnvelopeV1::parse_json(zero_source.as_bytes());
assert!(zero_result.is_err());
let zero_error = match zero_result {
std::result::Result::Err(error) => error,
@@ -91,7 +91,7 @@ fn zero_or_pathological_kdf_parameters_are_rejected_before_crypto() {
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 = super::KspWalletEnvelopeV1::parse_json(high_source.as_bytes());
let high_result = crate::KspWalletEnvelopeV1::parse_json(high_source.as_bytes());
assert!(high_result.is_err());
let high_error = match high_result {
std::result::Result::Err(error) => error,
@@ -107,7 +107,7 @@ fn argon2_memory_must_cover_all_lanes_before_crypto() {
"\"parallelism\": 2",
1,
);
let result = super::KspWalletEnvelopeV1::parse_json(source.as_bytes());
let result = crate::KspWalletEnvelopeV1::parse_json(source.as_bytes());
assert!(result.is_err());
let error = match result {
std::result::Result::Err(error) => error,
@@ -119,7 +119,7 @@ fn argon2_memory_must_cover_all_lanes_before_crypto() {
#[test]
fn oversized_document_is_rejected_before_json_or_crypto() {
let oversized = std::vec![b' '; crate::KSPWALLET_MAX_FILE_BYTES + 1];
let result = super::KspWalletEnvelopeV1::parse_json(oversized.as_slice());
let result = crate::KspWalletEnvelopeV1::parse_json(oversized.as_slice());
assert!(result.is_err());
let error = match result {
std::result::Result::Err(error) => error,
@@ -130,7 +130,7 @@ fn oversized_document_is_rejected_before_json_or_crypto() {
#[test]
fn envelope_debug_does_not_render_ciphertext_contents() -> ksp_core_lib::Result<()> {
let parsed = match super::KspWalletEnvelopeV1::parse_json(FIXTURE) {
let parsed = match crate::KspWalletEnvelopeV1::parse_json(FIXTURE) {
std::result::Result::Ok(parsed) => parsed,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};