v0.2.6-pre.013

This commit is contained in:
2026-08-21 17:15:40 +02:00
parent ecbd6b746d
commit 4b350b4161
17 changed files with 687 additions and 31 deletions

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-app-wallet-desk/Cargo.toml
# version: 4
# version: 5
[package]
name = "ksp-app-wallet-desk"
@@ -33,7 +33,7 @@ serde = { workspace = true, features = ["derive"] }
tauri.workspace = true
tauri-plugin-dialog.workspace = true
tauri-plugin-tracing.workspace = true
tokio = { workspace = true, features = ["fs", "io-util", "rt", "time"] }
tokio = { workspace = true, features = ["fs", "io-util", "macros", "rt", "time"] }
ts-rs.workspace = true
zeroize.workspace = true

View File

@@ -42,7 +42,7 @@
<h1 id="viewTitle" class="h3 mb-1">Dashboard</h1>
<p class="text-body-secondary mb-0">Identité autorisée et balance SOL via le Transport HTTP composite sont actives.</p>
</div>
<span class="badge text-bg-secondary">0.2.6-pre.012</span>
<span class="badge text-bg-secondary">0.2.6-pre.013</span>
</div>
<section data-view-panel="dashboard">
<div class="row g-3">
@@ -480,7 +480,7 @@
</div>
</div>
<footer class="app-footer bg-dark text-white-50 d-flex align-items-center px-4">
<span>KSP Wallet Desk · OWNER keypair export pre.012</span>
<span>KSP Wallet Desk · integration / compliance pre.013</span>
</footer>
<script type="module" src="./ts/main.ts"></script>
</body>

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/frontend/ts/main.ts
// version: 13
// version: 14
import { Modal } from "bootstrap";
import DataTable from "datatables.net-bs5";
@@ -190,7 +190,7 @@ function initializeWalletTable(): void {
zeroRecords: "Aucun wallet correspondant.",
},
});
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.012-owner-transfer-export" });
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.013-integration-compliance" });
}
function renderWalletInventory(entries: WalletInventoryEntryDto[]): void {

View File

@@ -1,7 +1,7 @@
{
"name": "ksp-app-wallet-desk",
"private": true,
"version": "0.2.6-pre.12",
"version": "0.2.6-pre.13",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/app_state.rs
// version: 16
// version: 17
//! Shared backend state owned by the Wallet Desk Tauri application.
@@ -126,7 +126,7 @@ impl AppState {
effective_wallets_directory_created_on_startup: self.wallet_config_startup.effective_directory_created_on_startup(),
fallback_logging_active: runtime.fallback_active,
root_wallets_directory_created_on_startup: self.wallet_config_startup.root_directory_created_on_startup(),
shell_phase: "pre.012-owner-transfer-export".to_owned(),
shell_phase: "pre.013-integration-compliance".to_owned(),
startup_diagnostic: runtime.startup_diagnostic.clone(),
transport_available_endpoint_count,
transport_clusters: self.transport_runtime.clusters(),

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "KSP Wallet Desk",
"version": "0.2.6-pre.12",
"version": "0.2.6-pre.13",
"identifier": "com.sasedev.ksp-app-wallet-desk",
"build": {
"beforeDevCommand": "npm run dev",

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs
// version: 13
// version: 14
//! Desktop build, shell and Config-status contract audits for Wallet Desk.
@@ -329,5 +329,21 @@ fn pre_012_owner_export_uses_rust_native_save_picker_and_wallet_file_adapter() {
assert!(html.contains("id=\"exportWalletKeypairModal\""));
assert!(main.contains("bindWalletExportActions"));
assert!(main.contains("export_wallet_transfer"));
assert!(main.contains("pre.012-owner-transfer-export"));
}
#[test]
fn pre_013_integration_compliance_and_devnet_smoke_are_durable_release_gates() {
let root = app_root();
let state = read_text(root.join("src/app_state.rs").as_path());
let main = read_text(root.join("frontend/ts/main.ts").as_path());
let compliance = read_text(root.join("tests/release_compliance.rs").as_path());
let smoke = read_text(root.join("tests/wallet_desk_devnet_smoke.rs").as_path());
let validation = read_text(root.join("../../docs/validation/009-V0_2_6_WALLET_DESK_COMPLIANCE.md").as_path());
assert!(state.contains("pre.013-integration-compliance"));
assert!(main.contains("pre.013-integration-compliance"));
assert!(compliance.contains("wallet_desk_dependency_firewall_and_tauri_capabilities_are_minimal"));
assert!(compliance.contains("frontend_has_no_direct_filesystem_network_or_secret_persistence_surface"));
assert!(smoke.contains("committed_wallet_desk_composite_creates_wallet_and_reaches_devnet_balance"));
assert!(smoke.contains("#[ignore"));
assert!(validation.contains("Config -> Wallet -> Transport -> Devnet getBalance"));
}

View File

@@ -0,0 +1,181 @@
// file: crates/ksp-app-wallet-desk/tests/release_compliance.rs
// version: 1
//! Release-wide deterministic compliance canaries for Wallet Desk.
fn app_root() -> std::path::PathBuf {
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
}
fn read_text(path: &std::path::Path) -> String {
let source = std::fs::read_to_string(path);
assert!(source.is_ok(), "unable to read {}", path.display());
return match source {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => String::new(),
};
}
fn read_rust_source_tree(root: &std::path::Path) -> String {
let entries = std::fs::read_dir(root);
assert!(entries.is_ok(), "unable to enumerate {}", root.display());
let entries = match entries {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return String::new(),
};
let mut paths = std::vec::Vec::new();
for entry in entries {
let entry = match entry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => continue,
};
let path = entry.path();
if path.extension().and_then(std::ffi::OsStr::to_str) == std::option::Option::Some("rs") {
paths.push(path);
}
}
paths.sort();
let mut combined = String::new();
for path in paths {
combined.push_str(read_text(path.as_path()).as_str());
combined.push('\n');
}
return combined;
}
fn struct_field_block<'a>(source: &'a str, name: &str) -> &'a str {
let marker = std::format!("struct {name}");
let start = source.find(marker.as_str());
assert!(start.is_some(), "missing struct {name}");
let start = match start {
std::option::Option::Some(value) => value,
std::option::Option::None => return "",
};
let tail = &source[start..];
return tail.split("}\n").next().unwrap_or_default();
}
#[test]
fn backend_keeps_config_wallet_transport_and_logging_ownership_boundaries() {
let root = app_root();
let source = read_rust_source_tree(root.join("src").as_path());
for forbidden in [
"std::env::var(",
"std::env::var_os(",
"std::env::vars(",
"reqwest::",
"solana_keypair::",
"solana_pubkey::",
"solana_signer::",
"solana_signature::",
] {
assert!(!source.contains(forbidden), "Wallet Desk backend bypasses an owned boundary through {forbidden}");
}
for line in source.lines() {
let trimmed = line.trim_start();
assert!(!trimmed.starts_with("use tracing::"), "Wallet Desk backend must not import tracing directly: {trimmed}");
assert!(!trimmed.starts_with("tracing::"), "Wallet Desk backend must not emit through tracing directly: {trimmed}");
}
assert!(source.contains("ksp_config_lib::ConfigEnvironment::load"));
assert!(source.contains("ksp_wallet_lib::"));
assert!(source.contains("ksp_onchain_transport_lib::"));
assert!(source.contains("ksp_logging_lib::"));
}
#[test]
fn frontend_has_no_direct_filesystem_network_or_secret_persistence_surface() {
let root = app_root();
let package = read_text(root.join("package.json").as_path());
let main = read_text(root.join("frontend/ts/main.ts").as_path());
let invoke = read_text(root.join("frontend/ts/invoke.ts").as_path());
let frontend_log = read_text(root.join("frontend/ts/frontend_log.ts").as_path());
let splash = read_text(root.join("frontend/ts/splash.ts").as_path());
let html = read_text(root.join("frontend/main.html").as_path());
let combined = std::format!("{main}\n{invoke}\n{frontend_log}\n{splash}\n{html}");
for forbidden in [
"@tauri-apps/plugin-dialog",
"@tauri-apps/plugin-fs",
"navigator.clipboard",
"localStorage",
"sessionStorage",
"window.alert",
"window.confirm",
"window.prompt",
"XMLHttpRequest",
"new WebSocket",
"EventSource",
"fetch(",
"KSP_SECRET_WALLET_PASS_",
"sourcePath",
"source_path",
"destinationPath",
"destination_path",
] {
assert!(!combined.contains(forbidden), "frontend contains forbidden direct surface {forbidden}");
}
assert!(!package.contains("@tauri-apps/plugin-dialog"));
assert!(!package.contains("@tauri-apps/plugin-fs"));
}
#[test]
fn response_dtos_keep_secret_key_material_out_of_ipc() {
let root = app_root();
let session = read_text(root.join("src/wallet_session.rs").as_path());
let balance = read_text(root.join("src/wallet_balance.rs").as_path());
let import = read_text(root.join("src/wallet_import.rs").as_path());
let export = read_text(root.join("src/wallet_export.rs").as_path());
let security = read_text(root.join("src/wallet_security.rs").as_path());
let common = read_text(root.join("src/dto_common.rs").as_path());
for (source, name) in [
(session.as_str(), "WalletAuthorizedDto"),
(session.as_str(), "WalletSessionDto"),
(balance.as_str(), "WalletBalanceDto"),
(import.as_str(), "WalletTransferInspectionDto"),
(export.as_str(), "WalletExportResultDto"),
(security.as_str(), "WalletViewSecurityStatusDto"),
(common.as_str(), "RuntimeStatusDto"),
] {
let fields = struct_field_block(source, name);
for forbidden in ["password", "secret_key", "private_key", "keypair_bytes", "ciphertext"] {
assert!(!fields.contains(forbidden), "response DTO {name} contains forbidden field material {forbidden}");
}
}
let inspection = struct_field_block(import.as_str(), "WalletTransferInspectionDto");
let export_result = struct_field_block(export.as_str(), "WalletExportResultDto");
assert!(!inspection.contains("path"));
assert!(!export_result.contains("path"));
assert!(!export_result.contains("content"));
assert!(!export_result.contains("bytes"));
}
#[test]
fn wallet_desk_dependency_firewall_and_tauri_capabilities_are_minimal() {
let root = app_root();
let manifest = read_text(root.join("Cargo.toml").as_path());
for line in manifest.lines() {
let trimmed = line.trim_start();
assert!(!trimmed.starts_with("solana-"), "Wallet Desk must consume Solana behavior through KSP crates: {trimmed}");
assert!(!trimmed.starts_with("reqwest "), "Wallet Desk must consume HTTP through Transport: {trimmed}");
assert!(!trimmed.starts_with("tracing "), "Wallet Desk must consume logging through the KSP facade/Tauri adapter: {trimmed}");
}
let capability_source = read_text(root.join("capabilities/default.json").as_path());
let capability = serde_json::from_str::<serde_json::Value>(capability_source.as_str());
assert!(capability.is_ok(), "Wallet Desk capability JSON should parse");
let capability = match capability {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let permissions = capability.get("permissions").and_then(serde_json::Value::as_array);
assert!(permissions.is_some());
let permissions = match permissions {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
assert_eq!(permissions.len(), 2);
assert_eq!(permissions[0].as_str(), std::option::Option::Some("core:default"));
assert_eq!(permissions[1].as_str(), std::option::Option::Some("tracing:default"));
let tauri = read_text(root.join("tauri.conf.json").as_path());
assert!(tauri.contains("\"csp\": null"));
assert!(!capability_source.contains("dialog:"));
assert!(!capability_source.contains("fs:"));
}

View File

@@ -0,0 +1,143 @@
// file: crates/ksp-app-wallet-desk/tests/wallet_desk_devnet_smoke.rs
// version: 1
//! Opt-in live composition smoke owned by the Wallet Desk orchestration surface.
fn workspace_engine() -> ksp_core_lib::Result<ksp_config_lib::ConfigDocumentEngine> {
let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
let bootstrap = match bootstrap {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let registry = ksp_config_lib::ConfigFileRegistry::defaults();
return match registry {
std::result::Result::Ok(value) => std::result::Result::Ok(ksp_config_lib::ConfigDocumentEngine::new(bootstrap, value)),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
#[tokio::test(flavor = "current_thread")]
#[ignore = "opt-in live Wallet Desk Devnet smoke; creates one temporary .kspwallet under the Config-selected root and performs an external getBalance request"]
async fn committed_wallet_desk_composite_creates_wallet_and_reaches_devnet_balance() {
let engine = workspace_engine();
assert!(engine.is_ok(), "workspace Config engine should be constructible");
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
assert!(file_id.is_ok(), "Wallet Desk composite file_id should be valid");
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let composite = engine.load_resolved_composite(&file_id, std::option::Option::None);
assert!(composite.is_ok(), "committed Wallet Desk composite should resolve");
let composite = match composite {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(composite.profile_id(), "devnet");
let environment = ksp_config_lib::ConfigEnvironment::load();
assert!(environment.is_ok(), "Config-owned environment snapshot should load");
let environment = match environment {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let wallet_component = composite.component("wallet");
assert!(wallet_component.is_some(), "Wallet Desk composite should contain Wallet");
let wallet_component = match wallet_component {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let wallet_config = engine.resolve_wallet_config_profile(wallet_component.resolved(), &environment);
assert!(wallet_config.is_ok(), "composite-selected Wallet profile should map");
let wallet_config = match wallet_config {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(wallet_config.selection_source(), ksp_config_lib::ConfigProfileSelectionSource::Composite);
let wallet_root = wallet_config.effective_wallets_directory().to_path_buf();
assert!(!wallet_root.as_os_str().is_empty());
let directory = tokio::fs::create_dir_all(wallet_root.as_path()).await;
assert!(directory.is_ok(), "Config-selected Wallet smoke directory should be creatable");
if directory.is_err() {
return;
}
let stamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH);
assert!(stamp.is_ok(), "system clock should be after Unix epoch");
let stamp = match stamp {
std::result::Result::Ok(value) => value.as_nanos(),
std::result::Result::Err(_) => return,
};
let wallet_path = wallet_root.join(std::format!("wallet-desk-pre013-smoke-{}-{stamp}.kspwallet", std::process::id()));
let owner = ksp_wallet_lib::create_wallet_file_v1(
wallet_path.as_path(),
ksp_wallet_lib::OwnerPassword::new("ksp-pre013-devnet-owner".to_owned()),
std::option::Option::Some(ksp_wallet_lib::ViewPassword::new("ksp-pre013-devnet-view".to_owned())),
ksp_wallet_lib::WalletCreateMetadataV1::new(std::option::Option::Some("pre013-devnet-smoke".to_owned()), std::vec::Vec::new()),
)
.await;
assert!(owner.is_ok(), "Wallet smoke file should be created through the public Wallet API");
let owner = match owner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let locked = ksp_wallet_lib::inspect_locked_wallet_file_v1(wallet_path.as_path()).await;
assert!(locked.is_ok(), "created smoke wallet should inspect while locked");
if let std::result::Result::Ok(locked) = locked {
assert_eq!(locked.format_version(), ksp_wallet_lib::KSPWALLET_FORMAT_VERSION_V1);
assert!(locked.view_enabled());
}
let view = ksp_wallet_lib::open_wallet_view_file_v1(wallet_path.as_path(), ksp_wallet_lib::ViewPassword::new("ksp-pre013-devnet-view".to_owned())).await;
assert!(view.is_ok(), "created smoke wallet should open with VIEW");
let view = match view {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
let _ = tokio::fs::remove_file(wallet_path.as_path()).await;
return;
},
};
assert_eq!(owner.pubkey(), view.pubkey());
let transport_component = composite.component("transport");
assert!(transport_component.is_some(), "Wallet Desk composite should contain Transport");
let transport_component = match transport_component {
std::option::Option::Some(value) => value,
std::option::Option::None => {
let _ = tokio::fs::remove_file(wallet_path.as_path()).await;
return;
},
};
let transport = engine.resolve_transport_config_profile(transport_component.resolved(), &environment);
assert!(transport.is_ok(), "composite-selected Transport profile should map");
let transport = match transport {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
let _ = tokio::fs::remove_file(wallet_path.as_path()).await;
return;
},
};
assert_eq!(transport.selection_source(), ksp_config_lib::ConfigProfileSelectionSource::Composite);
let pool = ksp_onchain_transport_lib::HttpTransportPool::new(transport.into_settings());
assert!(pool.is_ok(), "composite-selected Transport should construct the HTTP pool");
let pool = match pool {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
let _ = tokio::fs::remove_file(wallet_path.as_path()).await;
return;
},
};
let role = ksp_onchain_transport_lib::HttpRoleName::new("default");
let config = ksp_onchain_transport_lib::GetBalanceConfig::new(
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
std::option::Option::None,
);
let balance = pool.get_balance(&role, view.pubkey(), std::option::Option::Some(&config)).await;
let cleanup = tokio::fs::remove_file(wallet_path.as_path()).await;
assert!(cleanup.is_ok(), "Wallet Desk Devnet smoke wallet should be removed after the request");
assert!(balance.is_ok(), "Wallet Desk Config -> Wallet -> Transport getBalance smoke should succeed");
if let std::result::Result::Ok(balance) = balance {
assert!(balance.context().slot() > 0);
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/unit_tests/wallet_inventory.rs
// version: 1
// version: 2
const VALID_WALLET: &[u8] = include_bytes!("../../ksp-wallet-lib/tests/fixtures/kspwallet_v1_full_vector.json");
@@ -13,7 +13,11 @@ fn inventory_lists_regular_wallets_reports_invalid_candidates_and_ignores_other_
assert!(std::fs::write(root.join("beta.kspwallet"), VALID_WALLET).is_ok());
assert!(std::fs::write(root.join("alpha.kspwallet"), b"not-json").is_ok());
assert!(std::fs::write(root.join("ignored.txt"), VALID_WALLET).is_ok());
assert!(std::fs::write(root.join("upper.KSPWALLET"), VALID_WALLET).is_ok());
assert!(std::fs::write(root.join("backup.kspwallet.bak"), VALID_WALLET).is_ok());
assert!(std::fs::create_dir(root.join("directory.kspwallet")).is_ok());
assert!(std::fs::create_dir(root.join("nested")).is_ok());
assert!(std::fs::write(root.join("nested/hidden.kspwallet"), VALID_WALLET).is_ok());
let runtime = tokio::runtime::Builder::new_current_thread().build();
assert!(runtime.is_ok(), "test runtime should be constructible: {runtime:?}");
if let std::result::Result::Ok(runtime) = runtime {
@@ -60,6 +64,14 @@ fn selection_rejects_traversal_and_reinspects_one_valid_locked_wallet() {
if let std::result::Result::Err(error) = rejected_backslash {
assert_eq!(error.code(), crate::ERROR_CODE_WALLET_SELECTION_INVALID);
}
for wallet_id in ["", "/selected.kspwallet", "nested/selected.kspwallet", "selected.kspwallet.bak"] {
let rejected =
runtime.block_on(crate::select_locked_wallet(root.as_path(), crate::WalletSelectionRequestDto { wallet_id: wallet_id.to_owned() }));
assert!(rejected.is_err(), "invalid wallet selection should be rejected: {wallet_id}");
if let std::result::Result::Err(error) = rejected {
assert_eq!(error.code(), crate::ERROR_CODE_WALLET_SELECTION_INVALID);
}
}
let selected =
runtime.block_on(crate::select_locked_wallet(root.as_path(), crate::WalletSelectionRequestDto { wallet_id: "selected.kspwallet".to_owned() }));
assert!(selected.is_ok(), "valid locked Wallet should be selectable: {selected:?}");