v0.2.6-pre.016

This commit is contained in:
2026-08-22 08:18:38 +02:00
parent 946d88322b
commit 92a2c4fff3
43 changed files with 2813 additions and 280 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 188
# version: 189
[workspace]
resolver = "3"
members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
[workspace.package]
version = "0.2.6-pre.15.fix.1"
version = "0.2.6-pre.16"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md -->
<!-- version: 77 -->
<!-- version: 78 -->
# Roadmap KSP
@@ -50,7 +50,7 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
- [X] `0.2.3` — HTTP Transactions stable : 11/11 wrappers typés publiés, classification `8 Read / 2 WriteSubmission / 1 Simulation`, no-resend ambigu prouvé pour les write submissions, `KSP-TRANSPORT-007` réaudité conforme sur les 37 wrappers HTTP courants, graphes Cargo et deux smokes Devnet validés ; `0.2.4` reprend les 15 Blocks/Economics restants.
- [X] `0.2.4` — HTTP Blocks + Economics stable : 15/15 wrappers `V0_2_4` publiés, surface typed complète à 52/52 méthodes courantes, 14/14 historiques conservées, réaudit SIMD/inventaire final et `KSP-TRANSPORT-007` global validés ; deux smokes Devnet passés avant publication.
- [X] `0.2.5` — Wallet foundation stable : `.kspwallet` V1, VIEW/OWNER indépendants, Argon2id/XChaCha20-Poly1305, autorité Ed25519 OWNER, persistence no-clobber, signature, administration/rotations/révocation VIEW forte, import/export Solana CLI JSON + Base58, canaris adversariaux, interop externe et documentation durable publiés. La clôture `pre.010-fix.001``fix.003` ajoute `ed25519-dalek 3.0.0` direct, normalise le Rust workspace et installe laudit structurel Python complémentaire à rustfmt/Clippy. `Pubkey` reste via `ksp-core-lib`, la keypair reste encapsulée dans Wallet et Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet.
- [/] `0.2.6``ksp-app-wallet-desk` + évolution `.kspwallet` V2 : surface Desk acquise ; `pre.014` polish, `pre.015` wire binaire V2, `pre.016` APIs génériques/versionnées + V2, `pre.017` migration/canaris, `pre.018` candidate finale. Plan détaillé : `docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md`.
- [/] `0.2.6``ksp-app-wallet-desk` + évolution `.kspwallet` V2 : surface Desk acquise ; `pre.014` polish et `pre.015` wire binaire V2 acquis ; `pre.016` matérialise APIs génériques/versionnées + runtime V2 ; prochain gate `pre.017` migration/canaris, puis `pre.018` candidate finale. Plan détaillé : `docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md`.
- [ ] `0.2.7` — Étendre `ksp-onchain-transport-lib` au WebSocket Solana standard complet ; permettre plusieurs sessions sur une même URL sans imposer encore un pool automatique complexe.
- [ ] `0.2.8` — Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client.
- [ ] `0.2.9` — Ajouter une première fondation Yellowstone gRPC standard/provider-neutral ; dimensionner la surface exacte à `pre.001` selon la documentation normative actuelle.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-config-desk/README.md -->
<!-- version: 26 -->
<!-- version: 27 -->
# `ksp-app-config-desk`
@@ -111,7 +111,6 @@ Les bibliothèques consommées par le bundle applicatif sont déclarées sous `d
Les outils de compilation/développement et déclarations de types appartiennent à `devDependencies`, notamment `@tauri-apps/cli`, `@types/bootstrap`, `@types/node`, `sass-embedded`, `typescript` et `vite`. Les commandes npm directes sont réservées à l'installation ou à la mise à jour de ces dépendances. Le cycle normal de développement et de build est **crate-local** : l'opérateur peut rester positionné à la racine du workspace, mais la commande exécute d'abord `cd crates/ksp-app-config-desk` puis `cargo tauri dev`/`build`. `-c/--config` n'est pas utilisé comme sélecteur de crate.
## Logging frontend KSP
Le frontend utilise `frontend/ts/frontend_log.ts`. Les helpers `frontendTrace`, `frontendDebug`, `frontendInfo`, `frontendWarn` et `frontendError`, ainsi que le bridge `console.*`, invoquent la commande Tauri `emit_frontend_log`.
@@ -154,7 +153,6 @@ Après la readiness frontend, Rust émet `fade_in`, attend la durée minimale,
Le shell principal expose les cinq routes de référence `Vue d'ensemble`, `Documents`, `Profils`, `Environnement / .env` et `Logging`. Le logo porte déjà l'identité KSP ; le texte du header suit donc la forme `Config Desk — <vue active>` au lieu de répéter `KSP`. Les quelques commandes principales restent des pills/tabs à droite ; un dropdown sera préféré lorsqu'une application possède trop de commandes pour conserver ce format lisible. La route Logging expose maintenant un brouillon typé éditable, sa persistence atomique et lapplication runtime immédiate par hot reload.
## Panneau Profils
La vue **Profils** inspecte les documents validés qui exposent le contrat standard `default_profile` / `profiles`. Elle ne revalide ni ne fusionne les valeurs dans le frontend : le backend utilise `ConfigDocumentEngine::load_resolved_profile()` puis `ResolvedConfigProfile::resolve_effective_environment_detailed()`.
@@ -183,7 +181,6 @@ Les actions utilisateur significatives et changements d'état sont journalisés
Le splash utilise `DOS_Amazigh.ttf` (`font-family: Dos Amazigh`) pour son titre, via le `@font-face` déclaré dans `frontend/sass/splash.scss`. Le répertoire `frontend/fonts/` documente la provenance et l'empreinte de l'asset de référence. Le lifecycle du splash est désormais actif et utilise cette police pendant la transition vers la fenêtre principale.
## Panneau Environnement / `.env`
La vue **Environnement / `.env`** expose la projection sûre de `ConfigManagement::environment_report()` puis une zone de management qui appelle exclusivement `ConfigManagement::set_dotenv_value()` et `ConfigManagement::remove_dotenv_value()`. Elle ne lit jamais directement le process environment ni le fichier `.env`. Pour chaque variable KSP/KSPB actuellement présente dans le process ou `.env`, le frontend reçoit uniquement : nom, namespace, sensibilité, valeur desired sûre, valeur effective sûre, source effective et indicateur de shadowing.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-config-desk/USAGE.md -->
<!-- version: 26 -->
<!-- version: 27 -->
# Utilisation de `ksp-app-config-desk`
@@ -97,7 +97,6 @@ frontend/ts/bindings/ksp_app_config_desk/...
Les bindings sont générés au premier DTO Tauri réel ; aucune structure factice n'est ajoutée uniquement pour créer le répertoire.
## Bootstrap backend
Les arguments `--cfgpath`, `--schemapath` et `--filemap=...` sont transmis tels quels à `ksp-config-lib`. En build debug, le launcher replace le current working directory Rust à la racine du workspace avant ce bootstrap afin que les defaults relatifs `config/`, `config/schemas/` et `.env` désignent les ressources racine même lorsque Tauri lance `cargo run` depuis la crate de l'application. Le launcher ne lit pas ces ressources lui-même. Le profil Logging initial est le `default_profile` de `std.logging.json`.
@@ -156,12 +155,10 @@ La sélection charge le source brut via `ConfigManagement::read_source()`, y com
Les clics, sélections, chargements, remplacements DOM et sauvegardes sont tracés via le bridge frontend KSP sans journaliser le contenu du source.
### Ouvrir un éditeur spécialisé depuis Documents
Le panneau Documents demeure générique. Lorsqu'un `file_id` possède un adapter déclaré dans `shell_registry.ts`, le détail affiche **Ouvrir l'éditeur spécialisé**. Pour `cfg.std.logging`, ce bouton active la vue Logging sans dupliquer la lecture, la validation ou la persistence Config. Un document sans adapter spécialisé reste entièrement inspectable/réparable dans le panneau Documents et n'affiche pas ce bouton.
## Panneau Profils
La vue **Profils** charge l'inventaire des documents Config validés qui possèdent `default_profile` et `profiles`. Pour le document sélectionné :
@@ -175,7 +172,6 @@ La vue **Profils** charge l'inventaire des documents Config validés qui possèd
Avec le `std.logging.json` actuel et `KSP_LOGS_DIRECTORY` défini dans `.env`, la provenance attendue pour `/logs_directory` est `KSP_LOGS_DIRECTORY`, source `dotenv`, sensibilité `Internal`. Le contenu des vues JSON n'est pas recopié dans les logs ; seuls les identifiants de document/profil et métadonnées de résolution sont tracés.
## Panneau Environnement / `.env`
La vue **Environnement / `.env`** charge `get_environment_report`, qui projette exclusivement `ConfigManagement::environment_report()`. La table affiche :
@@ -219,8 +215,6 @@ La vue **Logging** appelle `get_logging_document`. Le backend charge le document
Le sélecteur **Profil édité** travaille sur un brouillon local. `logs_directory`, `default_profile`, profils, console, file sinks, filtres et target overrides sont modifiables. **Créer**, **Cloner**, **Renommer** et **Supprimer** agissent d'abord sur le brouillon ; la suppression de profil est confirmée par modal Bootstrap. **Sauvegarder et appliquer** envoie un candidat typé à `save_logging_document`, qui reconstruit les contrats Config, persiste atomiquement après validation, recharge un `ConfigEnvironment` frais, résout le `default_profile` et hot-reload le `LoggingGuard`. Si le runtime ne peut pas être préparé/rechargé, l'ancien runtime reste actif et la source précédente est restaurée. **Recharger le document** resynchronise uniquement le brouillon depuis la source persistée et demande confirmation si des changements non sauvegardés existent. Les messages Cargo/Tauri/Vite affichés par `cargo tauri dev` sont externes au runtime Logging KSP et ne dépendent pas de `console.enabled`.
## Test Logging — routing backend et bridge frontend
La section **Test Logging — routing contrôlé** utilise un message explicitement destiné aux logs. Ne jamais y copier une valeur `KSP_SECRET_*` / `KSPB_SECRET_*`.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/app_state.rs
// version: 18
// version: 19
//! Shared backend state owned by the Wallet Desk Tauri application.
@@ -202,7 +202,7 @@ impl AppState {
let note_texts = note.into_iter().collect::<std::vec::Vec<_>>();
let metadata = ksp_wallet_lib::WalletCreateMetadataV1::new(alias, note_texts);
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = filename.as_str(), view_enabled, "Wallet creation requested under effective Config root");
let created = ksp_wallet_lib::create_wallet_file_v1(destination.as_path(), owner_password, view_password, metadata).await;
let created = ksp_wallet_lib::create_wallet_file(destination.as_path(), owner_password, view_password, metadata).await;
let owner = match created {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
@@ -297,7 +297,7 @@ impl AppState {
let transfer_format = pending.wallet_format();
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_IMPORT, wallet_id = filename.as_str(), transfer_format = transfer_format.code(), view_enabled, "Wallet transfer import requested under effective Config root");
let imported =
ksp_wallet_lib::import_wallet_transfer_v1(destination.as_path(), pending.source_bytes(), transfer_format, owner_password, view_password, metadata)
ksp_wallet_lib::import_wallet_transfer(destination.as_path(), pending.source_bytes(), transfer_format, owner_password, view_password, metadata)
.await;
let owner = match imported {
std::result::Result::Ok(value) => value,
@@ -326,7 +326,7 @@ impl AppState {
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let password = ksp_wallet_lib::ViewPassword::new(request.password);
let opened = ksp_wallet_lib::open_wallet_view_file_v1(context.path.as_path(), password).await;
let opened = ksp_wallet_lib::open_wallet_view_file(context.path.as_path(), password).await;
let view = match opened {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
@@ -346,7 +346,7 @@ impl AppState {
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let password = ksp_wallet_lib::OwnerPassword::new(request.password);
let opened = ksp_wallet_lib::open_wallet_owner_file_v1(context.path.as_path(), password).await;
let opened = ksp_wallet_lib::open_wallet_owner_file(context.path.as_path(), password).await;
let owner = match opened {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
@@ -390,7 +390,7 @@ impl AppState {
return std::result::Result::Err(error);
},
};
let opened = ksp_wallet_lib::open_wallet_view_file_v1(context.path.as_path(), ksp_wallet_lib::ViewPassword::new(secret)).await;
let opened = ksp_wallet_lib::open_wallet_view_file(context.path.as_path(), ksp_wallet_lib::ViewPassword::new(secret)).await;
match opened {
std::result::Result::Ok(view) => {
ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SECRET, wallet_id = context.wallet_id.as_str(), capability = context.capability.label(), candidate_count, "Configured-secret Wallet unlock succeeded");
@@ -439,7 +439,7 @@ impl AppState {
return std::result::Result::Err(error);
},
};
let opened = ksp_wallet_lib::open_wallet_owner_file_v1(context.path.as_path(), ksp_wallet_lib::OwnerPassword::new(secret)).await;
let opened = ksp_wallet_lib::open_wallet_owner_file(context.path.as_path(), ksp_wallet_lib::OwnerPassword::new(secret)).await;
match opened {
std::result::Result::Ok(owner) => {
ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SECRET, wallet_id = context.wallet_id.as_str(), capability = context.capability.label(), candidate_count, "Configured-secret Wallet unlock succeeded");
@@ -516,7 +516,7 @@ impl AppState {
(wallet_id, path)
},
};
let locked = ksp_wallet_lib::inspect_locked_wallet_file_v1(path.as_path()).await;
let locked = ksp_wallet_lib::inspect_locked_wallet_file(path.as_path()).await;
let locked = match locked {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -914,23 +914,23 @@ impl AppState {
std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()),
};
let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection());
return match previous {
match previous {
crate::WalletSession::ViewOperation { wallet_id: reserved_wallet_id, path: reserved_path, pubkey: reserved_pubkey }
if reserved_wallet_id == wallet_id && reserved_path == path && reserved_pubkey == pubkey =>
{
*session = crate::WalletSession::View { wallet_id: wallet_id.clone(), path, wallet: view };
ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), operation, "Wallet VIEW privileged operation completed");
std::result::Result::Ok(dto)
return std::result::Result::Ok(dto);
},
other => {
*session = other;
drop(view);
std::result::Result::Err(ksp_core_lib::Error::new(
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_WALLET_SESSION_INVALID,
"Wallet VIEW operation completion no longer owns the selected session",
))
));
},
};
}
}
fn restore_view_after_operation_failure(&self, context: ViewOperationContext) {
@@ -958,7 +958,7 @@ impl AppState {
async fn recover_view_state_conflict(&self, context: ViewOperationContext) {
let ViewOperationContext { operation, path, pubkey, view, wallet_id } = context;
drop(view);
let inspected = ksp_wallet_lib::inspect_locked_wallet_file_v1(path.as_path()).await;
let inspected = ksp_wallet_lib::inspect_locked_wallet_file(path.as_path()).await;
let session = self.wallet_session.lock();
let mut session = match session {
std::result::Result::Ok(value) => value,
@@ -1039,7 +1039,7 @@ impl AppState {
std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()),
};
let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection());
return match previous {
match previous {
crate::WalletSession::OwnerOperation {
wallet_id: reserved_wallet_id,
path: reserved_path,
@@ -1048,17 +1048,17 @@ impl AppState {
} if reserved_wallet_id == wallet_id && reserved_path == path && reserved_pubkey == pubkey && reserved_view_enabled == view_enabled => {
*session = crate::WalletSession::Owner { wallet_id: wallet_id.clone(), path, view_enabled: result_view_enabled, wallet: owner };
ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), operation, "Wallet OWNER privileged operation completed");
std::result::Result::Ok(dto)
return std::result::Result::Ok(dto);
},
other => {
*session = other;
drop(owner);
std::result::Result::Err(ksp_core_lib::Error::new(
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_WALLET_SESSION_INVALID,
"Wallet OWNER operation completion no longer owns the selected session",
))
));
},
};
}
}
fn restore_owner_after_operation_failure(&self, context: OwnerOperationContext) {
@@ -1089,7 +1089,7 @@ impl AppState {
async fn recover_owner_state_conflict(&self, context: OwnerOperationContext) {
let OwnerOperationContext { operation, owner, path, pubkey, result_view_enabled: _, view_enabled, wallet_id } = context;
drop(owner);
let inspected = ksp_wallet_lib::inspect_locked_wallet_file_v1(path.as_path()).await;
let inspected = ksp_wallet_lib::inspect_locked_wallet_file(path.as_path()).await;
let session = self.wallet_session.lock();
let mut session = match session {
std::result::Result::Ok(value) => value,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/wallet_inventory.rs
// version: 3
// version: 4
//! Root-scoped Wallet inventory and locked-file selection for Wallet Desk.
@@ -113,7 +113,7 @@ pub(crate) async fn list_wallet_inventory(root: &std::path::Path) -> ksp_core_li
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_INVENTORY, filename = filename.as_str(), is_symlink = metadata.file_type().is_symlink(), "Wallet inventory skipped a non-regular candidate");
continue;
}
let inspected = ksp_wallet_lib::inspect_locked_wallet_file_v1(path.as_path()).await;
let inspected = ksp_wallet_lib::inspect_locked_wallet_file(path.as_path()).await;
match inspected {
std::result::Result::Ok(locked) => entries.push(valid_inventory_entry(filename, locked)),
std::result::Result::Err(error) => {
@@ -155,7 +155,7 @@ pub(crate) async fn resolve_locked_wallet(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let locked = ksp_wallet_lib::inspect_locked_wallet_file_v1(path.as_path()).await;
let locked = ksp_wallet_lib::inspect_locked_wallet_file(path.as_path()).await;
let locked = match locked {
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-app-wallet-desk/tests/desktop_contract.rs
// version: 16
// version: 17
//! Desktop build, shell and Config-status contract audits for Wallet Desk.
@@ -98,7 +98,7 @@ fn pre_004_inventory_uses_wallet_library_and_exposes_only_locked_safe_fields() {
let inventory = read_text(root.join("src/wallet_inventory.rs").as_path());
let main = read_text(root.join("frontend/ts/main.ts").as_path());
assert!(manifest.contains("ksp-wallet-lib = { path = \"../ksp-wallet-lib\" }"));
assert!(inventory.contains("ksp_wallet_lib::inspect_locked_wallet_file_v1"));
assert!(inventory.contains("ksp_wallet_lib::inspect_locked_wallet_file"));
assert!(inventory.contains("symlink_metadata"));
assert!(inventory.contains("WALLET_FILE_SUFFIX"));
assert!(tauri.contains("list_wallets"));
@@ -132,7 +132,7 @@ fn pre_005_create_and_session_lifecycle_are_backend_owned() {
let tauri = read_text(root.join("src/tauri.rs").as_path());
let html = read_text(root.join("frontend/main.html").as_path());
let main = read_text(root.join("frontend/ts/main.ts").as_path());
assert!(state.contains("ksp_wallet_lib::create_wallet_file_v1"));
assert!(state.contains("ksp_wallet_lib::create_wallet_file"));
assert!(state.contains("WalletSession::Owner"));
assert!(state.contains("WalletSession::Locked"));
assert!(state.contains("std::mem::replace"));
@@ -157,8 +157,8 @@ fn pre_006_unlock_surface_is_explicit_backend_owned_and_supports_view_owner() {
let tauri = read_text(root.join("src/tauri.rs").as_path());
let html = read_text(root.join("frontend/main.html").as_path());
let main = read_text(root.join("frontend/ts/main.ts").as_path());
assert!(state.contains("open_wallet_view_file_v1"));
assert!(state.contains("open_wallet_owner_file_v1"));
assert!(state.contains("open_wallet_view_file"));
assert!(state.contains("open_wallet_owner_file"));
assert!(state.contains("WalletSession::PrivilegedOperation"));
assert!(state.contains("WalletSession::View"));
assert!(secrets.contains("environment_report"));
@@ -220,7 +220,7 @@ fn pre_008_import_uses_rust_native_picker_and_wallet_transfer_adapters() {
assert!(tauri.contains("import_wallet"));
assert!(import.contains("ksp_wallet_lib::inspect_wallet_transfer"));
assert!(import.contains("zeroize::Zeroizing"));
assert!(state.contains("ksp_wallet_lib::import_wallet_transfer_v1"));
assert!(state.contains("ksp_wallet_lib::import_wallet_transfer"));
assert!(html.contains("id=\"inspectImportSource\""));
assert!(html.contains("id=\"importWalletForm\""));
assert!(html.contains("id=\"importSourcePubkey\""));
@@ -384,3 +384,19 @@ fn pre_014_template_has_normalized_html_headers_light_header_and_kbot_style_spla
assert!(splash_ts.contains("add_message"));
assert!(!splash_ts.contains("../sass/splash.scss"));
}
#[test]
fn pre_016_wallet_desk_uses_version_neutral_wallet_dispatch() {
let state = read_source("src/app_state.rs");
let inventory = read_source("src/wallet_inventory.rs");
assert!(state.contains("ksp_wallet_lib::create_wallet_file"));
assert!(state.contains("ksp_wallet_lib::import_wallet_transfer"));
assert!(state.contains("ksp_wallet_lib::open_wallet_view_file"));
assert!(state.contains("ksp_wallet_lib::open_wallet_owner_file"));
assert!(inventory.contains("ksp_wallet_lib::inspect_locked_wallet_file"));
assert!(!state.contains("create_wallet_file_v1"));
assert!(!state.contains("import_wallet_transfer_v1"));
assert!(!state.contains("open_wallet_view_file_v1"));
assert!(!state.contains("open_wallet_owner_file_v1"));
assert!(!inventory.contains("inspect_locked_wallet_file_v1"));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/tests/desktop_security.rs
// version: 13
// version: 14
//! Static desktop security contracts for the Wallet Desk pre.002 shell.
@@ -172,7 +172,7 @@ fn owner_metadata_values_are_request_only_and_state_conflict_forces_reauthorizat
assert!(!metadata.contains("derive(Clone"));
assert!(!metadata.contains("derive(Debug"));
assert!(state.contains("recover_owner_state_conflict"));
assert!(state.contains("inspect_locked_wallet_file_v1"));
assert!(state.contains("inspect_locked_wallet_file"));
assert!(state.contains("ERROR_CODE_STATE_CONFLICT"));
assert!(main.contains("isWalletStateConflict"));
assert!(main.contains("select_wallet"));

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/tests/wallet_desk_devnet_smoke.rs
// version: 1
// version: 2
//! Opt-in live composition smoke owned by the Wallet Desk orchestration surface.
@@ -72,7 +72,7 @@ async fn committed_wallet_desk_composite_creates_wallet_and_reaches_devnet_balan
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(
let owner = ksp_wallet_lib::create_wallet_file(
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())),
@@ -84,13 +84,13 @@ async fn committed_wallet_desk_composite_creates_wallet_and_reaches_devnet_balan
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;
let locked = ksp_wallet_lib::inspect_locked_wallet_file(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_eq!(locked.format_version(), ksp_wallet_lib::KSPWALLET_FORMAT_VERSION_V2);
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;
let view = ksp_wallet_lib::open_wallet_view_file(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,

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-logging-lib/USAGE.md -->
<!-- version: 6 -->
<!-- version: 7 -->
# Utilisation de ksp-logging-lib
@@ -92,8 +92,6 @@ let mut logging_guard = match initialize_result {
Une configuration sans output actif est valide et installe une infrastructure initialement silencieuse qui pourra être activée plus tard par hot reload.
## Identité de lancement et isolation des fichiers persistants
Une application qui utilise des sorties fichiers persistantes peut attacher une identité stable au runtime :

View File

@@ -1,11 +1,11 @@
<!-- file: crates/ksp-wallet-lib/README.md -->
<!-- version: 3 -->
<!-- version: 4 -->
# `ksp-wallet-lib`
Statut : **stable depuis KSP `0.2.5`**.
`ksp-wallet-lib` est la bibliothèque KSP propriétaire du Wallet Solana natif. Elle possède le format autonome `.kspwallet` V1 et, depuis `0.2.6-pre.015`, le wire binaire V2 canonique, les capacités indépendantes VIEW/OWNER, la protection du secret Solana, la signature, l'administration des metadata, les rotations de credentials, la persistence native et les adapters d'import/export explicitement supportés.
`ksp-wallet-lib` est la bibliothèque KSP propriétaire du Wallet Solana natif. Elle possède le format autonome `.kspwallet` V1 et le format binaire V2 canonique, les capacités indépendantes VIEW/OWNER, la protection du secret Solana, la signature, l'administration des metadata, les rotations de credentials, la persistence native et les adapters d'import/export explicitement supportés. Depuis `0.2.6-pre.016`, les APIs non versionnées créent/importent en V2 par default explicite et lisent V1/V2 par détection bornée.
La crate est volontairement indépendante de Config, du réseau et de Tauri. Un consumer fournit les chemins, passwords et metadata ; Wallet ouvre, protège, signe et persiste sans décider d'une policy de dépense ni contacter un RPC.
@@ -14,7 +14,8 @@ La crate est volontairement indépendante de Config, du réseau et de Tauri. Un
La crate possède :
- le format natif `.kspwallet` V1 et son parser JSON strict ;
- le wire binaire `.kspwallet` V2, son codec borné/canonique et ses domains/transcripts distincts ;
- le wire binaire `.kspwallet` V2, son codec borné/canonique, sa création/ouverture/persistence et ses domains/transcripts distincts ;
- la façade générique V1/V2 et les variantes `_v1`/`_v2` permettant soit le default, soit un wire forcé ;
- les key slots OWNER/VIEW indépendants ;
- Argon2id pour les KDF de passwords ;
- XChaCha20-Poly1305 pour le wrapping et les compartiments ;
@@ -194,6 +195,20 @@ Elles couvrent le wire, Argon2id/XChaCha20-Poly1305, l'ouverture VIEW/OWNER, la
- [`../../docs/validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md`](../../docs/validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md) — matrice de sécurité/interoperabilité/compliance ;
- [`../../prompts/011-V0_2_6_START_PROMPT.md`](../../prompts/011-V0_2_6_START_PROMPT.md) — reprise vers Wallet Desk après publication stable de `0.2.5`.
## V2 en `0.2.6-pre.015`
## V2 en `0.2.6-pre.015` / `pre.016`
`pre.015` ajoute uniquement le codec structurel V2 et sa spécification. Les APIs de persistence runtime restent V1 jusquà `pre.016`, qui ajoute `create_wallet_file(...)` default V2, les variantes `_v1`/`_v2` et le dispatch générique de lecture. Le default est une décision explicite et ne suit pas automatiquement une future V3.
`pre.015` a figé le wire structurel V2, son codec et ses transcripts/AAD. `pre.016` matérialise le runtime V2 complet et la façade multi-version :
```text
DEFAULT_WALLET_FORMAT = V2
LATEST_SUPPORTED_WALLET_FORMAT = V2
create_wallet_file(...) -> V2
create_wallet_file_v1(...) -> V1 forcé
create_wallet_file_v2(...) -> V2 forcé
open/inspect génériques -> détection V1/V2
open/inspect _v1/_v2 -> format forcé strict
```
`WalletOwner` et `WalletView` conservent le format natif qu'ils ont ouvert : metadata, rotations OWNER/VIEW, disable/recreate VIEW, self-rotation VIEW, signature et export ne transcodent jamais implicitement le fichier. Le default est une décision explicite et ne suit pas automatiquement une future V3. La migration authentifiée V1 -> V2 reste une opération séparée de `pre.017`.

View File

@@ -1,9 +1,9 @@
<!-- file: crates/ksp-wallet-lib/USAGE.md -->
<!-- version: 3 -->
<!-- version: 4 -->
# Utilisation de `ksp-wallet-lib`
Ce guide présente les principales surfaces publiques de Wallet V1. La spécification cryptographique du fichier reste [`../../docs/formats/KSPWALLET_V1.md`](../../docs/formats/KSPWALLET_V1.md).
Ce guide présente les principales surfaces publiques multi-version de Wallet. V1 reste spécifié par [`../../docs/formats/KSPWALLET_V1.md`](../../docs/formats/KSPWALLET_V1.md) et V2 par [`../../docs/formats/KSPWALLET_V2.md`](../../docs/formats/KSPWALLET_V2.md).
Les exemples utilisent des chemins explicites : Wallet ne lit ni Config ni environnement pour découvrir un répertoire.
@@ -11,13 +11,13 @@ Les exemples utilisent des chemins explicites : Wallet ne lit ni Config ni envir
```rust
async fn create_example() -> ksp_core_lib::Result<()> {
let metadata = ksp_wallet_lib::WalletCreateMetadataV1::new(
let metadata = ksp_wallet_lib::WalletCreateMetadata::new(
Some(std::string::String::from("devnet-main")),
vec![std::string::String::from("wallet de test")],
);
let owner_password = ksp_wallet_lib::OwnerPassword::new(std::string::String::from("OWNER-PASSWORD"));
let view_password = ksp_wallet_lib::ViewPassword::new(std::string::String::from("VIEW-PASSWORD"));
let created = ksp_wallet_lib::create_wallet_file_v1(
let created = ksp_wallet_lib::create_wallet_file(
"wallets/devnet-main.kspwallet",
owner_password,
Some(view_password),
@@ -35,13 +35,13 @@ async fn create_example() -> ksp_core_lib::Result<()> {
La destination doit avoir un parent existant. Une destination existante n'est jamais remplacée par une création.
Pour créer uniquement en mémoire, utiliser `create_wallet_v1` puis `WalletOwner::to_json_bytes()` si le caller possède lui-même une autre boundary de stockage.
Pour créer uniquement en mémoire avec le default, utiliser `create_wallet`. `WalletOwner::to_native_bytes()` restitue ensuite le wire natif courant. Les APIs `_v1` et `_v2` restent disponibles lorsquun caller doit forcer une version précise.
## 2. Inspecter un wallet verrouillé
```rust
async fn inspect_example() -> ksp_core_lib::Result<()> {
let inspected = ksp_wallet_lib::inspect_locked_wallet_file_v1("wallets/devnet-main.kspwallet").await;
let inspected = ksp_wallet_lib::inspect_locked_wallet_file("wallets/devnet-main.kspwallet").await;
let locked = match inspected {
Ok(value) => value,
Err(error) => return Err(error),
@@ -59,7 +59,7 @@ Cette projection ne contient volontairement ni Pubkey, ni alias, ni notes.
```rust
async fn open_view_example() -> ksp_core_lib::Result<()> {
let password = ksp_wallet_lib::ViewPassword::new(std::string::String::from("VIEW-PASSWORD"));
let opened = ksp_wallet_lib::open_wallet_view_file_v1("wallets/devnet-main.kspwallet", password).await;
let opened = ksp_wallet_lib::open_wallet_view_file("wallets/devnet-main.kspwallet", password).await;
let mut view = match opened {
Ok(value) => value,
Err(error) => return Err(error),
@@ -89,7 +89,7 @@ VIEW ne possède aucune API `sign`, `export_transfer`, `update_alias`, `add_note
```rust
async fn sign_example(message: &[u8]) -> ksp_core_lib::Result<[u8; ksp_wallet_lib::KSPWALLET_SOLANA_SIGNATURE_BYTES]> {
let password = ksp_wallet_lib::OwnerPassword::new(std::string::String::from("OWNER-PASSWORD"));
let opened = ksp_wallet_lib::open_wallet_owner_file_v1("wallets/devnet-main.kspwallet", password).await;
let opened = ksp_wallet_lib::open_wallet_owner_file("wallets/devnet-main.kspwallet", password).await;
let owner = match opened {
Ok(value) => value,
Err(error) => return Err(error),
@@ -105,7 +105,7 @@ La signature retournée contient 64 octets Ed25519. Aucun getter public ne retou
```rust
async fn metadata_example() -> ksp_core_lib::Result<()> {
let password = ksp_wallet_lib::OwnerPassword::new(std::string::String::from("OWNER-PASSWORD"));
let opened = ksp_wallet_lib::open_wallet_owner_file_v1("wallets/devnet-main.kspwallet", password).await;
let opened = ksp_wallet_lib::open_wallet_owner_file("wallets/devnet-main.kspwallet", password).await;
let mut owner = match opened {
Ok(value) => value,
Err(error) => return Err(error),
@@ -207,13 +207,13 @@ async fn inspect_transfer_example(source: &[u8]) -> ksp_core_lib::Result<()> {
Import fichier vers un nouveau `.kspwallet` :
```rust
let imported = ksp_wallet_lib::import_wallet_transfer_file_v1(
let imported = ksp_wallet_lib::import_wallet_transfer_file(
"wallets/imported.kspwallet",
"wallets/legacy-id.json",
ksp_wallet_lib::WalletTransferFormat::SolanaCliJson,
ksp_wallet_lib::OwnerPassword::new(std::string::String::from("OWNER-PASSWORD")),
None,
ksp_wallet_lib::WalletCreateMetadataV1::new(Some(std::string::String::from("imported")), vec![]),
ksp_wallet_lib::WalletCreateMetadata::new(Some(std::string::String::from("imported")), vec![]),
)
.await;
```
@@ -246,14 +246,16 @@ Les octets retournés par `export_transfer` contiennent volontairement le secret
Les équivalents sans I/O filesystem sont :
```text
create_wallet_v1
open_wallet_view_v1
open_wallet_owner_v1
inspect_locked_wallet_v1
inspect_wallet_transfer
create_wallet -> default V2
create_wallet_v1 / _v2 -> version forcée
open_wallet_view -> détection V1/V2
open_wallet_owner -> détection V1/V2
inspect_locked_wallet -> détection V1/V2
open/inspect *_v1 / *_v2 -> version forcée
inspect_wallet_transfer -> format transfer explicite
```
Les handles `WalletOwner` et `WalletView` peuvent être sérialisés vers le document natif courant avec `to_json_bytes()`. Ces bytes restent un `.kspwallet` chiffré, pas un export de la keypair.
Les handles `WalletOwner` et `WalletView` sérialisent leur document courant avec `to_native_bytes()`. `to_json_bytes()` est conservé comme compatibilité V1 et renvoie une erreur de format pour un handle V2 au lieu de transcoder implicitement.
## 11. Erreurs et diagnostics
@@ -278,13 +280,24 @@ ksp-onchain-transport-lib
Cette composition est le rôle de `0.2.6 — ksp-app-wallet-desk`, pas de `ksp-wallet-lib`.
## Wire V2 (`0.2.6-pre.015`)
## Wire/runtime V2 (`0.2.6-pre.015` / `pre.016`)
Le codec structurel V2 peut être utilisé pour analyser une fixture/document V2 déjà produit :
Le codec structurel V2 reste disponible directement pour les outils qui travaillent explicitement au niveau wire :
```rust
let envelope = ksp_wallet_lib::KspWalletEnvelopeV2::parse_binary(bytes)?;
let canonical = envelope.to_binary_bytes()?;
```
La persistence applicative ne doit pas encore appeler ce codec directement pour créer un wallet : `pre.016` introduit les APIs génériques/versionnées et le default V2.
Une application normale doit préférer les façades Wallet :
```text
create_wallet_file(...) default V2
create_wallet_file_v1(...) V1 forcé
create_wallet_file_v2(...) V2 forcé
open_wallet_*_file(...) auto-détection V1/V2
open_wallet_*_file_v1/_v2 version forcée
inspect_locked_wallet_file(...) auto-détection V1/V2
```
`DEFAULT_WALLET_FORMAT` et `LATEST_SUPPORTED_WALLET_FORMAT` sont intentionnellement indépendants. L'arrivée d'un futur V3 ne changera pas automatiquement le default V2.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/constants.rs
// version: 8
// version: 9
//! Wallet-owned constants.
@@ -91,6 +91,14 @@ pub const KSPWALLET_V1_XCHACHA_NONCE_BYTES: usize = 24;
pub const KSPWALLET_V2_AEAD_TAG_BYTES: usize = KSPWALLET_V1_AEAD_TAG_BYTES;
/// Argon2 version serialized by V2 key slots.
pub const KSPWALLET_V2_ARGON2_VERSION: u32 = KSPWALLET_V1_ARGON2_VERSION;
/// Default Argon2id iteration count for newly created V2 key slots.
pub const KSPWALLET_V2_DEFAULT_ARGON2_ITERATIONS: u32 = KSPWALLET_V1_DEFAULT_ARGON2_ITERATIONS;
/// Default Argon2id memory cost for newly created V2 key slots.
pub const KSPWALLET_V2_DEFAULT_ARGON2_MEMORY_KIB: u32 = KSPWALLET_V1_DEFAULT_ARGON2_MEMORY_KIB;
/// Default Argon2id parallelism for newly created V2 key slots.
pub const KSPWALLET_V2_DEFAULT_ARGON2_PARALLELISM: u32 = KSPWALLET_V1_DEFAULT_ARGON2_PARALLELISM;
/// Default KDF salt size generated independently for every newly created V2 key slot.
pub const KSPWALLET_V2_DEFAULT_KDF_SALT_BYTES: usize = KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES;
/// Byte length of an Ed25519 public key used as V2 Wallet format authority.
pub const KSPWALLET_V2_ED25519_PUBLIC_KEY_BYTES: usize = KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES;
/// Byte length of an Ed25519 detached V2 state signature.
@@ -123,6 +131,8 @@ pub const KSPWALLET_V2_OWNER_CONTROL_AAD_DOMAIN: &[u8] = b"KSPWALLET-V2-AAD-OWNE
pub const KSPWALLET_V2_OWNER_SLOT_AAD_DOMAIN: &[u8] = b"KSPWALLET-V2-AAD-OWNER-SLOT";
/// Domain separator for V2 OWNER-only secret compartment AEAD AAD.
pub const KSPWALLET_V2_SECRET_AAD_DOMAIN: &[u8] = b"KSPWALLET-V2-AAD-SECRET";
/// Exact Solana keypair plaintext size retained by the initial V2 secret payload profile.
pub const KSPWALLET_V2_SECRET_PLAINTEXT_BYTES: usize = KSPWALLET_V1_SECRET_PLAINTEXT_BYTES;
/// Byte length of every V2 key-slot identifier.
pub const KSPWALLET_V2_SLOT_ID_BYTES: usize = KSPWALLET_V1_SLOT_ID_BYTES;
/// Domain separator for the V2 OWNER state-signature transcript.

View File

@@ -1,12 +1,12 @@
// file: crates/ksp-wallet-lib/src/crypto.rs
// version: 4
// version: 5
//! In-memory cryptographic primitives for native `.kspwallet` V1.
//! In-memory cryptographic primitives shared by native `.kspwallet` V1/V2 runtime.
use chacha20poly1305::KeyInit; // rust-rules: trait-import
use chacha20poly1305::aead::Aead; // rust-rules: trait-import
/// Exact V1 content-key and password-derived-key size in bytes.
/// Exact content-key and password-derived-key size in bytes for the current V1/V2 profiles.
pub(crate) const SECRET_KEY_BYTES: usize = 32;
/// Owned 32-byte secret key with redacted diagnostics and drop-time zeroization.
@@ -68,6 +68,11 @@ pub(crate) fn derive_password_key(password: &[u8], kdf: &crate::WalletKdfParamet
return derive_argon2id(password, kdf.salt(), kdf.memory_kib(), kdf.iterations(), kdf.parallelism());
}
/// Derives one V2 password wrapping key from serialized Argon2id parameters.
pub(crate) fn derive_password_key_v2(password: &[u8], kdf: &crate::WalletKdfParametersV2) -> ksp_core_lib::Result<crate::SecretKeyV1> {
return derive_argon2id(password, kdf.salt(), kdf.memory_kib(), kdf.iterations(), kdf.parallelism());
}
/// Wraps one 32-byte content key with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
pub(crate) fn wrap_key(
wrapping_key: &crate::SecretKeyV1,

View File

@@ -0,0 +1,111 @@
// file: crates/ksp-wallet-lib/src/format.rs
// version: 2
//! Native `.kspwallet` version selection and bounded dispatch detection.
/// Explicit native `.kspwallet` wire format understood by this Wallet release.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WalletFormat {
/// Stable historical JSON/Base64url format.
V1,
/// Canonical binary KSP format.
V2,
}
impl WalletFormat {
/// Returns the serialized native format version.
#[must_use]
pub const fn version(self) -> u32 {
return match self {
Self::V1 => crate::KSPWALLET_FORMAT_VERSION_V1,
Self::V2 => crate::KSPWALLET_FORMAT_VERSION_V2,
};
}
}
/// Default format selected by the non-versioned native Wallet creation/import APIs.
///
/// This is intentionally independent from [`LATEST_SUPPORTED_WALLET_FORMAT`]. Adding a future format does not implicitly move this default.
pub const DEFAULT_WALLET_FORMAT: WalletFormat = WalletFormat::V2;
/// Highest native Wallet format understood by this release.
pub const LATEST_SUPPORTED_WALLET_FORMAT: WalletFormat = WalletFormat::V2;
/// Creates a new in-memory native Wallet using [`DEFAULT_WALLET_FORMAT`].
pub async fn create_wallet(
owner_password: crate::OwnerPassword,
view_password: std::option::Option<crate::ViewPassword>,
metadata: crate::WalletCreateMetadata,
) -> ksp_core_lib::Result<crate::WalletOwner> {
return match DEFAULT_WALLET_FORMAT {
WalletFormat::V1 => crate::create_wallet_v1(owner_password, view_password, metadata).await,
WalletFormat::V2 => crate::create_wallet_v2(owner_password, view_password, metadata).await,
};
}
/// Opens VIEW from any currently supported native Wallet wire format.
pub async fn open_wallet_view(source: &[u8], password: crate::ViewPassword) -> ksp_core_lib::Result<crate::WalletView> {
return match detect_wallet_format(source) {
std::result::Result::Ok(WalletFormat::V1) => crate::open_wallet_view_v1(source, password).await,
std::result::Result::Ok(WalletFormat::V2) => crate::open_wallet_view_v2(source, password).await,
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
/// Opens OWNER from any currently supported native Wallet wire format.
pub async fn open_wallet_owner(source: &[u8], password: crate::OwnerPassword) -> ksp_core_lib::Result<crate::WalletOwner> {
return match detect_wallet_format(source) {
std::result::Result::Ok(WalletFormat::V1) => crate::open_wallet_owner_v1(source, password).await,
std::result::Result::Ok(WalletFormat::V2) => crate::open_wallet_owner_v2(source, password).await,
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
/// Inspects the authenticated locked projection of any currently supported native Wallet wire format.
pub fn inspect_locked_wallet(source: &[u8]) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
return match detect_wallet_format(source) {
std::result::Result::Ok(WalletFormat::V1) => crate::inspect_locked_wallet_v1(source),
std::result::Result::Ok(WalletFormat::V2) => crate::inspect_locked_wallet_v2(source),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
/// Detects the native wire format without decrypting or authenticating the document.
///
/// Detection only selects the strict parser. The selected V1/V2 parser remains authoritative for all structural and cryptographic validation.
pub fn detect_wallet_format(source: &[u8]) -> ksp_core_lib::Result<WalletFormat> {
if source.len() > crate::KSPWALLET_MAX_FILE_BYTES {
return std::result::Result::Err(format_error("Wallet document exceeds the maximum size"));
}
if source.starts_with(crate::KSPWALLET_MAGIC.as_bytes()) {
let version_start = crate::KSPWALLET_MAGIC.len();
let version_end = version_start + 2;
let version_bytes = match source.get(version_start..version_end) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(format_error("Wallet binary header is truncated")),
};
let version = u16::from_be_bytes([version_bytes[0], version_bytes[1]]);
return match u32::from(version) {
crate::KSPWALLET_FORMAT_VERSION_V2 => std::result::Result::Ok(WalletFormat::V2),
other => std::result::Result::Err(version_error(other)),
};
}
let first_non_whitespace = source.iter().copied().find(|byte| return !byte.is_ascii_whitespace());
if first_non_whitespace == std::option::Option::Some(b'{') {
return std::result::Result::Ok(WalletFormat::V1);
}
return std::result::Result::Err(format_error("Wallet document does not match a supported native framing"));
}
fn format_error(message: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, message);
}
fn version_error(version: u32) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_VERSION_UNSUPPORTED, "Wallet format version is not supported")
.with_context("format_version", version.to_string());
}
#[cfg(test)]
#[path = "../unit_tests/format.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/lib.rs
// version: 12
// version: 13
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -16,7 +16,8 @@
//! replacement. `0.2.5-pre.008` adds bounded Solana CLI JSON and canonical full-keypair Base58 import/export adapters with safe inspection and no-clobber
//! native import/export publication. `0.2.5-pre.009` adds adversarial security/compliance canaries and records the final dependency/interop audit before
//! documentation closure. `0.2.6-pre.015` defines the strict binary `.kspwallet` V2 wire and its bounded canonical codec without yet changing the public
//! persistence default or Wallet Desk dispatch. Public keys are consumed exclusively through the [`ksp_core_lib::Pubkey`] re-export owned by KSP Core, and behavioral
//! persistence default or Wallet Desk dispatch. `0.2.6-pre.016` adds complete V2 create/open/administration, stable version-neutral dispatch, explicit V1/V2
//! APIs and moves the default native creation/import format to V2 without coupling that default to the latest supported version. 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`.
@@ -24,16 +25,19 @@ mod capability;
mod constants;
mod crypto;
mod error;
mod format;
mod metadata;
mod owner;
mod password;
mod payload;
mod persistence;
mod runtime;
mod transcript;
mod transcript_v2;
mod transfer;
mod view;
mod wallet;
mod wallet_v2;
mod wire;
mod wire_v2;
@@ -131,6 +135,14 @@ pub use self::constants::KSPWALLET_V1_XCHACHA_NONCE_BYTES;
pub use self::constants::KSPWALLET_V2_AEAD_TAG_BYTES;
/// Argon2 version serialized by `.kspwallet` V2 key slots.
pub use self::constants::KSPWALLET_V2_ARGON2_VERSION;
/// Default Argon2id iteration count for newly created V2 slots.
pub use self::constants::KSPWALLET_V2_DEFAULT_ARGON2_ITERATIONS;
/// Default Argon2id memory cost for newly created V2 slots.
pub use self::constants::KSPWALLET_V2_DEFAULT_ARGON2_MEMORY_KIB;
/// Default Argon2id parallelism for newly created V2 slots.
pub use self::constants::KSPWALLET_V2_DEFAULT_ARGON2_PARALLELISM;
/// Default KDF salt size generated for newly created V2 slots.
pub use self::constants::KSPWALLET_V2_DEFAULT_KDF_SALT_BYTES;
/// Byte length of the V2 Ed25519 format-authority public key.
pub use self::constants::KSPWALLET_V2_ED25519_PUBLIC_KEY_BYTES;
/// Byte length of the V2 Ed25519 detached state signature.
@@ -163,6 +175,8 @@ pub use self::constants::KSPWALLET_V2_OWNER_CONTROL_AAD_DOMAIN;
pub use self::constants::KSPWALLET_V2_OWNER_SLOT_AAD_DOMAIN;
/// Domain separator for V2 OWNER-only secret compartment AEAD AAD.
pub use self::constants::KSPWALLET_V2_SECRET_AAD_DOMAIN;
/// Exact Solana keypair plaintext size retained by the initial V2 secret payload profile.
pub use self::constants::KSPWALLET_V2_SECRET_PLAINTEXT_BYTES;
/// Byte length of every V2 key-slot identifier.
pub use self::constants::KSPWALLET_V2_SLOT_ID_BYTES;
/// Domain separator for the V2 OWNER state-signature transcript.
@@ -205,9 +219,27 @@ pub use self::error::ERROR_CODE_STATE_CONFLICT;
pub use self::error::ERROR_CODE_TRANSFER_FORMAT_UNSUPPORTED;
/// Error code used when a VIEW unlock attempt fails without exposing a finer cryptographic oracle.
pub use self::error::ERROR_CODE_VIEW_UNLOCK_FAILED;
/// Default native Wallet format selected by non-versioned create/import APIs.
pub use self::format::DEFAULT_WALLET_FORMAT;
/// Highest native Wallet format supported by this release.
pub use self::format::LATEST_SUPPORTED_WALLET_FORMAT;
/// Explicit native Wallet wire-format selector.
pub use self::format::WalletFormat;
/// Creates a new in-memory native Wallet using the explicit default format.
pub use self::format::create_wallet;
/// Detects V1 JSON versus V2 binary framing before strict parser dispatch.
pub use self::format::detect_wallet_format;
/// Inspects any supported in-memory native Wallet document.
pub use self::format::inspect_locked_wallet;
/// Opens OWNER from any supported in-memory native Wallet document.
pub use self::format::open_wallet_owner;
/// Opens VIEW from any supported in-memory native Wallet document.
pub use self::format::open_wallet_view;
/// Minimal non-secret information available while a native Wallet remains locked.
pub use self::metadata::LockedWalletInfo;
/// Protected initial metadata supplied to native Wallet creation.
/// Protected initial metadata supplied to version-neutral native Wallet creation.
pub type WalletCreateMetadata = self::metadata::WalletCreateMetadataV1;
/// Protected initial metadata payload profile retained for explicit V1 compatibility.
pub use self::metadata::WalletCreateMetadataV1;
/// Safe metadata projection produced after VIEW or OWNER authorization.
pub use self::metadata::WalletInfo;
@@ -219,22 +251,46 @@ pub use self::owner::WalletOwner;
pub use self::password::OwnerPassword;
/// Owned VIEW password material with redacted diagnostics and drop-time zeroization.
pub use self::password::ViewPassword;
/// Creates and no-clobber persists a native Wallet using the explicit default format.
pub use self::persistence::create_wallet_file;
/// Creates and no-clobber persists a new native `.kspwallet` V1 file.
pub use self::persistence::create_wallet_file_v1;
/// Creates and no-clobber persists a new native `.kspwallet` V2 binary file.
pub use self::persistence::create_wallet_file_v2;
/// Reads and verifies a locked native Wallet using V1/V2 auto-detection.
pub use self::persistence::inspect_locked_wallet_file;
/// Reads and verifies a locked native `.kspwallet` V1 file.
pub use self::persistence::inspect_locked_wallet_file_v1;
/// Reads and verifies a locked native `.kspwallet` V2 binary file.
pub use self::persistence::inspect_locked_wallet_file_v2;
/// Opens a native Wallet with OWNER capability using V1/V2 auto-detection.
pub use self::persistence::open_wallet_owner_file;
/// Opens a native `.kspwallet` V1 file with OWNER capability.
pub use self::persistence::open_wallet_owner_file_v1;
/// Opens a native `.kspwallet` V2 binary file with OWNER capability.
pub use self::persistence::open_wallet_owner_file_v2;
/// Opens a native Wallet with VIEW capability using V1/V2 auto-detection.
pub use self::persistence::open_wallet_view_file;
/// Opens a native `.kspwallet` V1 file with VIEW capability.
pub use self::persistence::open_wallet_view_file_v1;
/// Opens a native `.kspwallet` V2 binary file with VIEW capability.
pub use self::persistence::open_wallet_view_file_v2;
/// Explicit secret-transfer format supported by Wallet.
pub use self::transfer::WalletTransferFormat;
/// Safe public identity projection of one validated secret-transfer source.
pub use self::transfer::WalletTransferInspection;
/// Imports one in-memory transfer payload into a new no-clobber native Wallet using the explicit default format.
pub use self::transfer::import_wallet_transfer;
/// Imports one bounded external transfer file into a new no-clobber native Wallet using the explicit default format.
pub use self::transfer::import_wallet_transfer_file;
/// Imports one bounded external transfer file into a new no-clobber native Wallet V1.
pub use self::transfer::import_wallet_transfer_file_v1;
/// Imports one bounded external transfer file into a new no-clobber native Wallet V2.
pub use self::transfer::import_wallet_transfer_file_v2;
/// Imports one in-memory transfer payload into a new no-clobber native Wallet V1.
pub use self::transfer::import_wallet_transfer_v1;
/// Imports one in-memory transfer payload into a new no-clobber native Wallet V2.
pub use self::transfer::import_wallet_transfer_v2;
/// Validates one in-memory transfer payload and exposes only its derived public identity.
pub use self::transfer::inspect_wallet_transfer;
/// Validates one bounded external transfer file and exposes only its derived public identity.
@@ -249,6 +305,14 @@ pub use self::wallet::inspect_locked_wallet_v1;
pub use self::wallet::open_wallet_owner_v1;
/// Opens the VIEW capability from a native Wallet V1 document.
pub use self::wallet::open_wallet_view_v1;
/// Creates a new in-memory native Wallet V2.
pub use self::wallet_v2::create_wallet_v2;
/// Parses and verifies locked native Wallet V2 state without unlocking protected metadata.
pub use self::wallet_v2::inspect_locked_wallet_v2;
/// Opens the OWNER capability from a native Wallet V2 document.
pub use self::wallet_v2::open_wallet_owner_v2;
/// Opens the VIEW capability from a native Wallet V2 document.
pub use self::wallet_v2::open_wallet_view_v2;
/// Strict semantic representation of one parsed native `.kspwallet` V1 envelope.
pub use self::wire::KspWalletEnvelopeV1;
/// Authenticated-encryption algorithm fixed by native Wallet V1.
@@ -308,6 +372,8 @@ pub(crate) use self::crypto::SecretKeyV1;
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;
/// Derives one V2 password wrapping key from serialized Argon2id parameters.
pub(crate) use self::crypto::derive_password_key_v2;
/// 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.
@@ -328,7 +394,9 @@ pub(crate) use self::payload::decode_owner_control_payload;
pub(crate) use self::payload::encode_initial_metadata_payload;
/// Encodes owner control payload.
pub(crate) use self::payload::encode_owner_control_payload;
/// Internal no-clobber native persistence path shared by transfer adapters.
/// Internal no-clobber native persistence path shared by version-neutral transfer adapters.
pub(crate) use self::persistence::persist_new_wallet_content;
/// Internal V1 no-clobber native persistence path retained for explicit compatibility adapters.
pub(crate) use self::persistence::persist_new_wallet_content_v1;
/// Persists new wallet fault before publish.
#[cfg(test)]
@@ -341,9 +409,15 @@ pub(crate) use self::persistence::persist_new_wallet_for_test;
pub(crate) use self::persistence::replace_wallet_fault_before_publish;
/// Replaces wallet file v1.
pub(crate) use self::persistence::replace_wallet_file_v1;
/// Replaces wallet file v2.
pub(crate) use self::persistence::replace_wallet_file_v2;
/// Replaces wallet for test.
#[cfg(test)]
pub(crate) use self::persistence::replace_wallet_for_test;
/// Version-neutral OWNER runtime state shared by public Wallet handles.
pub(crate) use self::runtime::OwnerState;
/// Version-neutral VIEW runtime state shared by public Wallet handles.
pub(crate) use self::runtime::ViewState;
/// Internal deterministic compartment-AAD codec shared by Wallet crypto layers.
pub(crate) use self::transcript::compartment_aad;
/// Internal deterministic key-slot-AAD codec shared by Wallet crypto layers.
@@ -357,12 +431,20 @@ pub(crate) use self::transcript_v2::slot_aad_v2;
/// Internal deterministic V2 OWNER-state transcript codec shared by Wallet crypto layers.
pub(crate) use self::transcript_v2::state_transcript_v2;
/// Internal no-clobber transfer-file writer used only by OWNER export.
pub(crate) use self::transfer::write_wallet_transfer_file_v1;
pub(crate) use self::transfer::write_wallet_transfer_file;
/// Crate-internal `OwnerStateV1` state shared across the owning crate.
pub(crate) use self::wallet::OwnerStateV1;
/// Crate-internal `ViewStateV1` state shared across the owning crate.
pub(crate) use self::wallet::ViewStateV1;
/// Internal imported-keypair creation path shared by transfer adapters.
/// Internal imported-keypair V1 creation path shared by transfer adapters.
pub(crate) use self::wallet::create_wallet_v1_from_keypair;
/// Verifies state signature.
/// Verifies V1 state signature.
pub(crate) use self::wallet::verify_state_signature;
/// Crate-internal `OwnerStateV2` state shared across the owning crate.
pub(crate) use self::wallet_v2::OwnerStateV2;
/// Crate-internal `ViewStateV2` state shared across the owning crate.
pub(crate) use self::wallet_v2::ViewStateV2;
/// Internal imported-keypair V2 creation path shared by transfer adapters.
pub(crate) use self::wallet_v2::create_wallet_v2_from_keypair;
/// Verifies V2 state signature.
pub(crate) use self::wallet_v2::verify_state_signature_v2;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/metadata.rs
// version: 4
// version: 5
/// Protected metadata requested when creating a new native Wallet.
///
@@ -78,12 +78,13 @@ pub struct WalletInfo {
impl WalletInfo {
/// Creates a new `WalletInfo` value.
pub(crate) fn new(
format_version: u32,
capability: crate::WalletCapability,
pubkey: ksp_core_lib::Pubkey,
alias: std::option::Option<std::string::String>,
notes: std::vec::Vec<crate::WalletNote>,
) -> Self {
return Self { format_version: crate::KSPWALLET_FORMAT_VERSION_V1, capability, pubkey, alias, notes };
return Self { format_version, capability, pubkey, alias, notes };
}
/// Returns the native Wallet format version parsed for this projection.
@@ -139,8 +140,8 @@ pub struct LockedWalletInfo {
impl LockedWalletInfo {
/// Creates a new `LockedWalletInfo` value.
pub(crate) const fn new(view_enabled: bool) -> Self {
return Self { format_version: crate::KSPWALLET_FORMAT_VERSION_V1, view_enabled };
pub(crate) const fn new(format_version: u32, view_enabled: bool) -> Self {
return Self { format_version, view_enabled };
}
/// Returns the native Wallet format version.

View File

@@ -1,21 +1,27 @@
// file: crates/ksp-wallet-lib/src/owner.rs
// version: 6
// version: 7
/// Authorized OWNER capability handle.
///
/// OWNER exposes protected metadata, Solana message signing and the authenticated administration operations defined by native Wallet V1. Secret key
/// OWNER exposes protected metadata, Solana message signing and the authenticated administration operations defined by the authenticated native Wallet format. Secret key
/// material remains encapsulated and is never exposed through a general-purpose getter.
pub struct WalletOwner {
info: crate::WalletInfo,
state: crate::OwnerStateV1,
state: crate::OwnerState,
}
impl WalletOwner {
/// Builds `WalletOwner` from unlocked.
pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::OwnerStateV1) -> Self {
pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::OwnerState) -> Self {
return Self { info, state };
}
/// Returns the native Wallet format version backing this authorized handle.
#[must_use]
pub const fn format_version(&self) -> u32 {
return self.info.format_version();
}
/// Returns the authorization capability represented by this handle.
#[must_use]
pub const fn capability(&self) -> crate::WalletCapability {
@@ -84,7 +90,7 @@ impl WalletOwner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let result = crate::write_wallet_transfer_file_v1(destination.as_ref().to_path_buf(), encoded, format).await;
let result = crate::write_wallet_transfer_file(destination.as_ref().to_path_buf(), encoded, format).await;
if result.is_ok() {
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
@@ -181,11 +187,13 @@ impl WalletOwner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await;
let persist_result = self.state.persist_staged(destination.as_ref().to_path_buf(), &envelope).await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
self.state.apply_envelope(envelope);
if let std::result::Result::Err(error) = self.state.apply_envelope(envelope) {
return std::result::Result::Err(error);
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_rotate_owner_password",
@@ -207,11 +215,13 @@ impl WalletOwner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await;
let persist_result = self.state.persist_staged(destination.as_ref().to_path_buf(), &envelope).await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
self.state.apply_envelope(envelope);
if let std::result::Result::Err(error) = self.state.apply_envelope(envelope) {
return std::result::Result::Err(error);
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_rotate_view_password",
@@ -229,11 +239,13 @@ impl WalletOwner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await;
let persist_result = self.state.persist_staged(destination.as_ref().to_path_buf(), &envelope).await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
self.state.apply_strong_view_state(envelope, metadata_key);
if let std::result::Result::Err(error) = self.state.apply_strong_view_state(envelope, metadata_key) {
return std::result::Result::Err(error);
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_disable_view",
@@ -253,11 +265,13 @@ impl WalletOwner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await;
let persist_result = self.state.persist_staged(destination.as_ref().to_path_buf(), &envelope).await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
self.state.apply_strong_view_state(envelope, metadata_key);
if let std::result::Result::Err(error) = self.state.apply_strong_view_state(envelope, metadata_key) {
return std::result::Result::Err(error);
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_recreate_view",
@@ -267,9 +281,16 @@ impl WalletOwner {
return std::result::Result::Ok(());
}
/// Serializes the complete locked `.kspwallet` V1 document without exposing any unlocked secret material.
/// Serializes the complete locked native Wallet in its current V1 or V2 wire format.
pub fn to_native_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return self.state.native_bytes();
}
/// Serializes a V1 handle as its historical JSON document.
///
/// V2 handles return a format error instead of being converted implicitly.
pub fn to_json_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return self.state.envelope().to_json_bytes();
return self.state.json_bytes_v1();
}
async fn persist_metadata_payload(
@@ -282,11 +303,13 @@ impl WalletOwner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = persist_staged(destination, self.state.envelope(), &envelope).await;
let persist_result = self.state.persist_staged(destination, &envelope).await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
self.state.apply_envelope(envelope);
if let std::result::Result::Err(error) = self.state.apply_envelope(envelope) {
return std::result::Result::Err(error);
}
self.info = info;
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, operation = operation, capability = "owner", "wallet protected metadata updated");
return std::result::Result::Ok(());
@@ -299,18 +322,6 @@ impl std::fmt::Debug for WalletOwner {
}
}
async fn persist_staged(
destination: std::path::PathBuf,
expected_current: &crate::KspWalletEnvelopeV1,
envelope: &crate::KspWalletEnvelopeV1,
) -> ksp_core_lib::Result<()> {
let serialized = match envelope.to_json_bytes() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::replace_wallet_file_v1(destination, expected_current.clone(), serialized).await;
}
#[cfg(test)]
#[path = "../unit_tests/administration.rs"]
mod tests;

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-wallet-lib/src/payload.rs
// version: 5
// version: 6
//! Plaintext payload codecs protected inside native `.kspwallet` V1 compartments.
//! Plaintext payload codecs shared by the initial native `.kspwallet` V1/V2 protected compartments.
use base64::Engine; // rust-rules: trait-import
use std::str::FromStr; // rust-rules: trait-import
@@ -119,8 +119,8 @@ impl crate::MetadataPayloadV1 {
}
/// Consumes this value and returns info.
pub(crate) fn into_info(self, capability: crate::WalletCapability) -> crate::WalletInfo {
return crate::WalletInfo::new(capability, self.pubkey, self.alias, self.notes);
pub(crate) fn into_info(self, format_version: u32, capability: crate::WalletCapability) -> crate::WalletInfo {
return crate::WalletInfo::new(format_version, capability, self.pubkey, self.alias, self.notes);
}
}

View File

@@ -1,11 +1,54 @@
// file: crates/ksp-wallet-lib/src/persistence.rs
// version: 7
// version: 8
//! Async-first native Wallet V1 filesystem persistence.
//! Async-first native Wallet V1/V2 filesystem persistence and version-neutral dispatch.
use std::io::Read; // rust-rules: trait-import
use std::io::Write; // rust-rules: trait-import
/// Creates a new native `.kspwallet` using [`crate::DEFAULT_WALLET_FORMAT`].
///
/// The default is explicitly V2 in this release and does not track future `LATEST_SUPPORTED_WALLET_FORMAT` values automatically.
pub async fn create_wallet_file(
destination: impl std::convert::AsRef<std::path::Path>,
owner_password: crate::OwnerPassword,
view_password: std::option::Option<crate::ViewPassword>,
metadata: crate::WalletCreateMetadata,
) -> ksp_core_lib::Result<crate::WalletOwner> {
return match crate::DEFAULT_WALLET_FORMAT {
crate::WalletFormat::V1 => create_wallet_file_v1(destination, owner_password, view_password, metadata).await,
crate::WalletFormat::V2 => create_wallet_file_v2(destination, owner_password, view_password, metadata).await,
};
}
/// Creates and no-clobber persists a new native `.kspwallet` V2 binary file.
pub async fn create_wallet_file_v2(
destination: impl std::convert::AsRef<std::path::Path>,
owner_password: crate::OwnerPassword,
view_password: std::option::Option<crate::ViewPassword>,
metadata: crate::WalletCreateMetadata,
) -> ksp_core_lib::Result<crate::WalletOwner> {
let destination = destination.as_ref().to_path_buf();
let owner = match crate::create_wallet_v2(owner_password, view_password, metadata).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let serialized = match owner.to_native_bytes() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = persist_new_wallet_async(destination, serialized).await {
return std::result::Result::Err(error);
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_create_file",
format_version = crate::KSPWALLET_FORMAT_VERSION_V2,
"native wallet persisted with no-clobber semantics"
);
return std::result::Result::Ok(owner);
}
/// Creates a new native `.kspwallet` V1 at `destination` without overwriting an existing path.
///
/// The complete encrypted document is created in memory first, written and synchronized through a temporary file in the destination directory, then
@@ -40,6 +83,30 @@ pub async fn create_wallet_file_v1(
return std::result::Result::Ok(owner);
}
/// Opens a supported native `.kspwallet` file with VIEW capability after bounded V1/V2 detection.
pub async fn open_wallet_view_file(
source: impl std::convert::AsRef<std::path::Path>,
password: crate::ViewPassword,
) -> ksp_core_lib::Result<crate::WalletView> {
let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::open_wallet_view(bytes.as_slice(), password).await;
}
/// Opens a native `.kspwallet` V2 binary file with VIEW capability.
pub async fn open_wallet_view_file_v2(
source: impl std::convert::AsRef<std::path::Path>,
password: crate::ViewPassword,
) -> ksp_core_lib::Result<crate::WalletView> {
let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::open_wallet_view_v2(bytes.as_slice(), password).await;
}
/// Opens a native `.kspwallet` V1 from `source` with VIEW capability.
///
/// The file is read through the bounded async persistence boundary before the normal strict parser, OWNER state-signature verification and VIEW KDF flow.
@@ -56,6 +123,30 @@ pub async fn open_wallet_view_file_v1(
return crate::open_wallet_view_v1(bytes.as_slice(), password).await;
}
/// Opens a supported native `.kspwallet` file with OWNER capability after bounded V1/V2 detection.
pub async fn open_wallet_owner_file(
source: impl std::convert::AsRef<std::path::Path>,
password: crate::OwnerPassword,
) -> ksp_core_lib::Result<crate::WalletOwner> {
let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::open_wallet_owner(bytes.as_slice(), password).await;
}
/// Opens a native `.kspwallet` V2 binary file with OWNER capability.
pub async fn open_wallet_owner_file_v2(
source: impl std::convert::AsRef<std::path::Path>,
password: crate::OwnerPassword,
) -> ksp_core_lib::Result<crate::WalletOwner> {
let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::open_wallet_owner_v2(bytes.as_slice(), password).await;
}
/// Opens a native `.kspwallet` V1 from `source` with OWNER capability.
///
/// The file is read through the bounded async persistence boundary before the normal strict parser, OWNER state-signature verification and OWNER KDF flow.
@@ -72,6 +163,24 @@ pub async fn open_wallet_owner_file_v1(
return crate::open_wallet_owner_v1(bytes.as_slice(), password).await;
}
/// Reads and verifies the locked projection of a supported native `.kspwallet` file after bounded V1/V2 detection.
pub async fn inspect_locked_wallet_file(source: impl std::convert::AsRef<std::path::Path>) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::inspect_locked_wallet(bytes.as_slice());
}
/// Reads and verifies the locked projection of a native `.kspwallet` V2 binary file without running a password KDF.
pub async fn inspect_locked_wallet_file_v2(source: impl std::convert::AsRef<std::path::Path>) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::inspect_locked_wallet_v2(bytes.as_slice());
}
/// Reads and verifies the locked projection of a native `.kspwallet` V1 from `source` without running a password KDF.
pub async fn inspect_locked_wallet_file_v1(source: impl std::convert::AsRef<std::path::Path>) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
let source = source.as_ref().to_path_buf();
@@ -88,6 +197,11 @@ pub(crate) async fn persist_new_wallet_content_v1(destination: std::path::PathBu
return persist_new_wallet_async(destination, content).await;
}
/// Persists one already serialized native Wallet document with no-clobber semantics.
pub(crate) async fn persist_new_wallet_content(destination: std::path::PathBuf, content: std::vec::Vec<u8>) -> ksp_core_lib::Result<()> {
return persist_new_wallet_async(destination, content).await;
}
/// Replaces wallet file v1.
pub(crate) async fn replace_wallet_file_v1(
destination: std::path::PathBuf,
@@ -103,6 +217,21 @@ pub(crate) async fn replace_wallet_file_v1(
};
}
/// Replaces one authenticated V2 Wallet file only if the current V2 state still matches the caller's expected state.
pub(crate) async fn replace_wallet_file_v2(
destination: std::path::PathBuf,
expected_current: crate::KspWalletEnvelopeV2,
content: std::vec::Vec<u8>,
) -> ksp_core_lib::Result<()> {
let task = tokio::task::spawn_blocking(move || {
return replace_wallet_file_v2_checked_blocking(destination.as_path(), &expected_current, 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("replace_task", error)),
};
}
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 {
@@ -162,6 +291,18 @@ fn replace_wallet_file_checked_blocking(
return replace_wallet_file_with_hook(destination, content, || return verify_expected_wallet_state(destination, expected_current));
}
fn replace_wallet_file_v2_checked_blocking(
destination: &std::path::Path,
expected_current: &crate::KspWalletEnvelopeV2,
content: &[u8],
) -> ksp_core_lib::Result<()> {
let current_check = verify_expected_wallet_state_v2(destination, expected_current);
if let std::result::Result::Err(error) = current_check {
return std::result::Result::Err(error);
}
return replace_wallet_file_with_hook(destination, content, || return verify_expected_wallet_state_v2(destination, expected_current));
}
#[cfg(test)]
fn replace_wallet_file_blocking(destination: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
return replace_wallet_file_with_hook(destination, content, || return std::result::Result::Ok(()));
@@ -186,6 +327,24 @@ fn verify_expected_wallet_state(destination: &std::path::Path, expected_current:
return std::result::Result::Ok(());
}
fn verify_expected_wallet_state_v2(destination: &std::path::Path, expected_current: &crate::KspWalletEnvelopeV2) -> ksp_core_lib::Result<()> {
let current_bytes = match read_wallet_file_blocking(destination) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let current = match crate::KspWalletEnvelopeV2::parse_binary(current_bytes.as_slice()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = crate::verify_state_signature_v2(&current) {
return std::result::Result::Err(error);
}
if &current != expected_current {
return std::result::Result::Err(state_conflict_error());
}
return std::result::Result::Ok(());
}
fn replace_wallet_file_with_hook<F>(destination: &std::path::Path, content: &[u8], before_publish: F) -> ksp_core_lib::Result<()>
where
F: std::ops::FnOnce() -> ksp_core_lib::Result<()>,

View File

@@ -0,0 +1,233 @@
// file: crates/ksp-wallet-lib/src/runtime.rs
// version: 1
//! Version-neutral unlocked Wallet state dispatch used by stable OWNER/VIEW handles.
/// One staged native envelope produced by an authenticated administration operation.
pub(crate) enum StagedEnvelope {
V1(crate::KspWalletEnvelopeV1),
V2(crate::KspWalletEnvelopeV2),
}
/// Version-neutral OWNER runtime state.
pub(crate) enum OwnerState {
V1(crate::OwnerStateV1),
V2(crate::OwnerStateV2),
}
impl OwnerState {
/// Returns the native format version owned by this OWNER state.
pub(crate) const fn format_version(&self) -> u32 {
return match self {
Self::V1(_) => crate::KSPWALLET_FORMAT_VERSION_V1,
Self::V2(_) => crate::KSPWALLET_FORMAT_VERSION_V2,
};
}
/// Exports the Solana identity through the requested external transfer format.
pub(crate) fn export_transfer(&self, format: crate::WalletTransferFormat) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return match self {
Self::V1(state) => state.export_transfer(format),
Self::V2(state) => state.export_transfer(format),
};
}
/// Signs one message with the OWNER-authorized Solana identity.
pub(crate) fn sign_message(&self, message: &[u8]) -> ksp_core_lib::Result<[u8; crate::KSPWALLET_SOLANA_SIGNATURE_BYTES]> {
return match self {
Self::V1(state) => state.sign_message(message),
Self::V2(state) => state.sign_message(message),
};
}
/// Stages an authenticated metadata replacement without publishing it.
pub(crate) fn stage_metadata_payload(&self, payload: crate::MetadataPayloadV1) -> ksp_core_lib::Result<(StagedEnvelope, crate::WalletInfo)> {
return match self {
Self::V1(state) => state.stage_metadata_payload(payload).map(|(envelope, info)| (StagedEnvelope::V1(envelope), info)),
Self::V2(state) => state.stage_metadata_payload(payload).map(|(envelope, info)| (StagedEnvelope::V2(envelope), info)),
};
}
/// Stages an OWNER credential rotation in the state native format.
pub(crate) async fn stage_owner_password_rotation(&self, password: crate::OwnerPassword) -> ksp_core_lib::Result<StagedEnvelope> {
return match self {
Self::V1(state) => state.stage_owner_password_rotation(password).await.map(StagedEnvelope::V1),
Self::V2(state) => state.stage_owner_password_rotation(password).await.map(StagedEnvelope::V2),
};
}
/// Stages a VIEW credential rotation in the state native format.
pub(crate) async fn stage_view_password_rotation(&self, password: crate::ViewPassword) -> ksp_core_lib::Result<StagedEnvelope> {
return match self {
Self::V1(state) => state.stage_view_password_rotation(password).await.map(StagedEnvelope::V1),
Self::V2(state) => state.stage_view_password_rotation(password).await.map(StagedEnvelope::V2),
};
}
/// Stages strong VIEW disable while retaining OWNER authority.
pub(crate) fn stage_disable_view(&self) -> ksp_core_lib::Result<(StagedEnvelope, crate::SecretKeyV1)> {
return match self {
Self::V1(state) => state.stage_disable_view().map(|(envelope, key)| (StagedEnvelope::V1(envelope), key)),
Self::V2(state) => state.stage_disable_view().map(|(envelope, key)| (StagedEnvelope::V2(envelope), key)),
};
}
/// Stages strong VIEW recreation under a new credential.
pub(crate) async fn stage_recreate_view(&self, password: crate::ViewPassword) -> ksp_core_lib::Result<(StagedEnvelope, crate::SecretKeyV1)> {
return match self {
Self::V1(state) => state.stage_recreate_view(password).await.map(|(envelope, key)| (StagedEnvelope::V1(envelope), key)),
Self::V2(state) => state.stage_recreate_view(password).await.map(|(envelope, key)| (StagedEnvelope::V2(envelope), key)),
};
}
/// Atomically publishes a staged envelope after format-specific state-conflict verification.
pub(crate) async fn persist_staged(&self, destination: std::path::PathBuf, envelope: &StagedEnvelope) -> ksp_core_lib::Result<()> {
return match (self, envelope) {
(Self::V1(state), StagedEnvelope::V1(staged)) => {
let serialized = match staged.to_json_bytes() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
crate::replace_wallet_file_v1(destination, state.envelope().clone(), serialized).await
},
(Self::V2(state), StagedEnvelope::V2(staged)) => {
let serialized = match staged.to_binary_bytes() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
crate::replace_wallet_file_v2(destination, state.envelope().clone(), serialized).await
},
_ => std::result::Result::Err(version_state_error()),
};
}
/// Replaces the in-memory authenticated envelope after successful persistence.
pub(crate) fn apply_envelope(&mut self, envelope: StagedEnvelope) -> ksp_core_lib::Result<()> {
return match (self, envelope) {
(Self::V1(state), StagedEnvelope::V1(value)) => {
state.apply_envelope(value);
std::result::Result::Ok(())
},
(Self::V2(state), StagedEnvelope::V2(value)) => {
state.apply_envelope(value);
std::result::Result::Ok(())
},
_ => std::result::Result::Err(version_state_error()),
};
}
/// Replaces the envelope and metadata key after a strong VIEW administration operation.
pub(crate) fn apply_strong_view_state(&mut self, envelope: StagedEnvelope, metadata_key: crate::SecretKeyV1) -> ksp_core_lib::Result<()> {
return match (self, envelope) {
(Self::V1(state), StagedEnvelope::V1(value)) => {
state.apply_strong_view_state(value, metadata_key);
std::result::Result::Ok(())
},
(Self::V2(state), StagedEnvelope::V2(value)) => {
state.apply_strong_view_state(value, metadata_key);
std::result::Result::Ok(())
},
_ => std::result::Result::Err(version_state_error()),
};
}
/// Serializes the authenticated state in its native V1 or V2 wire format.
pub(crate) fn native_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return match self {
Self::V1(state) => state.envelope().to_json_bytes(),
Self::V2(state) => state.envelope().to_binary_bytes(),
};
}
/// Serializes only V1 state as JSON and rejects V2 instead of converting formats.
pub(crate) fn json_bytes_v1(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return match self {
Self::V1(state) => state.envelope().to_json_bytes(),
Self::V2(_) => std::result::Result::Err(v1_serialization_error()),
};
}
}
/// Version-neutral VIEW runtime state.
pub(crate) enum ViewState {
V1(crate::ViewStateV1),
V2(crate::ViewStateV2),
}
impl ViewState {
/// Returns the native format version owned by this VIEW state.
pub(crate) const fn format_version(&self) -> u32 {
return match self {
Self::V1(_) => crate::KSPWALLET_FORMAT_VERSION_V1,
Self::V2(_) => crate::KSPWALLET_FORMAT_VERSION_V2,
};
}
/// Stages a VIEW credential rotation in the state native format.
pub(crate) async fn stage_view_password_rotation(&self, password: crate::ViewPassword) -> ksp_core_lib::Result<StagedEnvelope> {
return match self {
Self::V1(state) => state.stage_view_password_rotation(password).await.map(StagedEnvelope::V1),
Self::V2(state) => state.stage_view_password_rotation(password).await.map(StagedEnvelope::V2),
};
}
/// Atomically publishes a staged envelope after format-specific state-conflict verification.
pub(crate) async fn persist_staged(&self, destination: std::path::PathBuf, envelope: &StagedEnvelope) -> ksp_core_lib::Result<()> {
return match (self, envelope) {
(Self::V1(state), StagedEnvelope::V1(staged)) => {
let serialized = match staged.to_json_bytes() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
crate::replace_wallet_file_v1(destination, state.envelope().clone(), serialized).await
},
(Self::V2(state), StagedEnvelope::V2(staged)) => {
let serialized = match staged.to_binary_bytes() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
crate::replace_wallet_file_v2(destination, state.envelope().clone(), serialized).await
},
_ => std::result::Result::Err(version_state_error()),
};
}
/// Replaces the in-memory authenticated envelope after successful persistence.
pub(crate) fn apply_envelope(&mut self, envelope: StagedEnvelope) -> ksp_core_lib::Result<()> {
return match (self, envelope) {
(Self::V1(state), StagedEnvelope::V1(value)) => {
state.apply_envelope(value);
std::result::Result::Ok(())
},
(Self::V2(state), StagedEnvelope::V2(value)) => {
state.apply_envelope(value);
std::result::Result::Ok(())
},
_ => std::result::Result::Err(version_state_error()),
};
}
/// Serializes the authenticated state in its native V1 or V2 wire format.
pub(crate) fn native_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return match self {
Self::V1(state) => state.envelope().to_json_bytes(),
Self::V2(state) => state.envelope().to_binary_bytes(),
};
}
/// Serializes only V1 state as JSON and rejects V2 instead of converting formats.
pub(crate) fn json_bytes_v1(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return match self {
Self::V1(state) => state.envelope().to_json_bytes(),
Self::V2(_) => std::result::Result::Err(v1_serialization_error()),
};
}
}
fn version_state_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, "Wallet runtime state and staged format are inconsistent");
}
fn v1_serialization_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, "Wallet is not a V1 JSON document");
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/transfer.rs
// version: 3
// version: 4
//! Explicit OWNER-only Solana keypair import/export adapters.
@@ -9,7 +9,7 @@ use std::io::Write; // rust-rules: trait-import
use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import
use zeroize::Zeroize; // rust-rules: trait-import
/// Explicit secret-transfer formats supported by Wallet `0.2.5`.
/// Explicit secret-transfer formats supported by Wallet.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WalletTransferFormat {
@@ -106,6 +106,55 @@ pub async fn inspect_wallet_transfer_file(
return inspect_wallet_transfer(bytes.as_slice(), format);
}
/// Imports one in-memory Solana keypair transfer payload into a new native Wallet using [`crate::DEFAULT_WALLET_FORMAT`].
pub async fn import_wallet_transfer(
destination: impl std::convert::AsRef<std::path::Path>,
source: &[u8],
format: WalletTransferFormat,
owner_password: crate::OwnerPassword,
view_password: std::option::Option<crate::ViewPassword>,
metadata: crate::WalletCreateMetadata,
) -> ksp_core_lib::Result<crate::WalletOwner> {
return match crate::DEFAULT_WALLET_FORMAT {
crate::WalletFormat::V1 => import_wallet_transfer_v1(destination, source, format, owner_password, view_password, metadata).await,
crate::WalletFormat::V2 => import_wallet_transfer_v2(destination, source, format, owner_password, view_password, metadata).await,
};
}
/// Imports one in-memory Solana keypair transfer payload into a new native `.kspwallet` V2 binary file.
pub async fn import_wallet_transfer_v2(
destination: impl std::convert::AsRef<std::path::Path>,
source: &[u8],
format: WalletTransferFormat,
owner_password: crate::OwnerPassword,
view_password: std::option::Option<crate::ViewPassword>,
metadata: crate::WalletCreateMetadata,
) -> ksp_core_lib::Result<crate::WalletOwner> {
let keypair = match decode_transfer_keypair(source, format) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let owner = match crate::create_wallet_v2_from_keypair(owner_password, view_password, metadata, keypair).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let serialized = match owner.to_native_bytes() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = crate::persist_new_wallet_content(destination.as_ref().to_path_buf(), serialized).await {
return std::result::Result::Err(error);
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_import_transfer",
format_version = crate::KSPWALLET_FORMAT_VERSION_V2,
transfer_format = format.code(),
"external Solana keypair imported into a new native wallet"
);
return std::result::Result::Ok(owner);
}
/// Imports one in-memory Solana keypair transfer payload into a new native `.kspwallet` V1 file.
///
/// Import always creates a fresh native Wallet envelope around the validated immutable Solana keypair and publishes with the same no-clobber semantics as
@@ -143,6 +192,38 @@ pub async fn import_wallet_transfer_v1(
return std::result::Result::Ok(owner);
}
/// Imports one bounded external transfer file into a new native Wallet using [`crate::DEFAULT_WALLET_FORMAT`].
pub async fn import_wallet_transfer_file(
destination: impl std::convert::AsRef<std::path::Path>,
source: impl std::convert::AsRef<std::path::Path>,
format: WalletTransferFormat,
owner_password: crate::OwnerPassword,
view_password: std::option::Option<crate::ViewPassword>,
metadata: crate::WalletCreateMetadata,
) -> ksp_core_lib::Result<crate::WalletOwner> {
let bytes = match read_transfer_file_async(source.as_ref().to_path_buf(), format).await {
std::result::Result::Ok(value) => zeroize::Zeroizing::new(value),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return import_wallet_transfer(destination, bytes.as_slice(), format, owner_password, view_password, metadata).await;
}
/// Imports one bounded external transfer file into a new native `.kspwallet` V2 binary file.
pub async fn import_wallet_transfer_file_v2(
destination: impl std::convert::AsRef<std::path::Path>,
source: impl std::convert::AsRef<std::path::Path>,
format: WalletTransferFormat,
owner_password: crate::OwnerPassword,
view_password: std::option::Option<crate::ViewPassword>,
metadata: crate::WalletCreateMetadata,
) -> ksp_core_lib::Result<crate::WalletOwner> {
let bytes = match read_transfer_file_async(source.as_ref().to_path_buf(), format).await {
std::result::Result::Ok(value) => zeroize::Zeroizing::new(value),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return import_wallet_transfer_v2(destination, bytes.as_slice(), format, owner_password, view_password, metadata).await;
}
/// Imports one bounded external transfer file into a new native `.kspwallet` V1 file.
///
/// The source file is never modified and the destination remains no-clobber.
@@ -161,8 +242,8 @@ pub async fn import_wallet_transfer_file_v1(
return import_wallet_transfer_v1(destination, bytes.as_slice(), format, owner_password, view_password, metadata).await;
}
/// Executes the crate-internal write wallet transfer file v1 operation for the owning module.
pub(crate) async fn write_wallet_transfer_file_v1(
/// Writes one bounded OWNER export using no-clobber external transfer semantics.
pub(crate) async fn write_wallet_transfer_file(
destination: std::path::PathBuf,
content: std::vec::Vec<u8>,
format: WalletTransferFormat,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/view.rs
// version: 4
// version: 5
/// Authorized VIEW capability handle.
///
@@ -7,15 +7,21 @@
/// Solana secret, OWNER administration material or metadata-write authority.
pub struct WalletView {
info: crate::WalletInfo,
state: crate::ViewStateV1,
state: crate::ViewState,
}
impl WalletView {
/// Builds `WalletView` from unlocked.
pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::ViewStateV1) -> Self {
pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::ViewState) -> Self {
return Self { info, state };
}
/// Returns the native Wallet format version backing this authorized handle.
#[must_use]
pub const fn format_version(&self) -> u32 {
return self.info.format_version();
}
/// Returns the authorization capability represented by this handle.
#[must_use]
pub const fn capability(&self) -> crate::WalletCapability {
@@ -65,15 +71,13 @@ impl WalletView {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let serialized = match envelope.to_json_bytes() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = crate::replace_wallet_file_v1(destination.as_ref().to_path_buf(), self.state.envelope().clone(), serialized).await;
let persist_result = self.state.persist_staged(destination.as_ref().to_path_buf(), &envelope).await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
self.state.apply_envelope(envelope);
if let std::result::Result::Err(error) = self.state.apply_envelope(envelope) {
return std::result::Result::Err(error);
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_rotate_view_password",
@@ -83,9 +87,16 @@ impl WalletView {
return std::result::Result::Ok(());
}
/// Serializes the unchanged locked `.kspwallet` V1 document without exposing the metadata content key.
/// Serializes the unchanged locked native Wallet in its current V1 or V2 wire format.
pub fn to_native_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return self.state.native_bytes();
}
/// Serializes a V1 handle as its historical JSON document.
///
/// V2 handles return a format error instead of being converted implicitly.
pub fn to_json_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return self.state.envelope().to_json_bytes();
return self.state.json_bytes_v1();
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/wallet.rs
// version: 8
// version: 9
//! In-memory native Wallet V1 create/open orchestration.
@@ -143,7 +143,7 @@ impl OwnerStateV1 {
if let std::result::Result::Err(error) = verify_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok((envelope, payload.into_info(crate::WalletCapability::Owner)));
return std::result::Result::Ok((envelope, payload.into_info(crate::KSPWALLET_FORMAT_VERSION_V1, crate::WalletCapability::Owner)));
}
/// Executes the crate-internal stage owner password rotation operation for `OwnerStateV1`.
@@ -497,7 +497,7 @@ pub async fn open_wallet_view_v1(source: &[u8], password: crate::ViewPassword) -
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 info = metadata_payload.into_info(crate::KSPWALLET_FORMAT_VERSION_V1, crate::WalletCapability::View);
let state = ViewStateV1::new(envelope, metadata_key);
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
@@ -506,7 +506,7 @@ pub async fn open_wallet_view_v1(source: &[u8], password: crate::ViewPassword) -
capability = "view",
"native wallet VIEW capability opened"
);
return std::result::Result::Ok(crate::WalletView::from_unlocked(info, state));
return std::result::Result::Ok(crate::WalletView::from_unlocked(info, crate::ViewState::V1(state)));
}
/// Opens the OWNER capability from a native `.kspwallet` V1 JSON document.
@@ -602,7 +602,7 @@ pub async fn open_wallet_owner_v1(source: &[u8], password: crate::OwnerPassword)
if &secret_pubkey != metadata_payload.pubkey() {
return std::result::Result::Err(key_material_error());
}
let info = metadata_payload.into_info(crate::WalletCapability::Owner);
let info = metadata_payload.into_info(crate::KSPWALLET_FORMAT_VERSION_V1, 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,
@@ -611,7 +611,7 @@ pub async fn open_wallet_owner_v1(source: &[u8], password: crate::OwnerPassword)
capability = "owner",
"native wallet OWNER capability opened"
);
return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, state));
return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, crate::OwnerState::V1(state)));
}
/// Parses and verifies the OWNER-authenticated locked state without unlocking metadata or secret material.
@@ -624,7 +624,7 @@ pub fn inspect_locked_wallet_v1(source: &[u8]) -> ksp_core_lib::Result<crate::Lo
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()));
return std::result::Result::Ok(crate::LockedWalletInfo::new(crate::KSPWALLET_FORMAT_VERSION_V1, envelope.view_descriptor().enabled()));
}
/// Executes the crate-internal create wallet v1 from keypair operation for the owning module.
@@ -821,7 +821,7 @@ pub(crate) async fn create_wallet_v1_from_keypair(
if let std::result::Result::Err(error) = verify_result {
return std::result::Result::Err(error);
}
let info = metadata_payload.into_info(crate::WalletCapability::Owner);
let info = metadata_payload.into_info(crate::KSPWALLET_FORMAT_VERSION_V1, 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,
@@ -830,7 +830,7 @@ pub(crate) async fn create_wallet_v1_from_keypair(
view_enabled = state.envelope().view_descriptor().enabled(),
"native wallet created in memory"
);
return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, state));
return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, crate::OwnerState::V1(state)));
}
/// Verifies state signature.

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/wire_v2.rs
// version: 2
// version: 3
//! Strict native `.kspwallet` V2 binary wire envelope.
@@ -120,6 +120,17 @@ impl WalletKdfParametersV2 {
return Self { algorithm: WalletKdfAlgorithmV2::Argon2id, version, memory_kib, iterations, parallelism, salt };
}
/// Creates one V2 KDF value using the release-calibrated default creation profile.
pub(crate) fn new_creation(salt: std::vec::Vec<u8>) -> Self {
return Self::new_internal(
crate::KSPWALLET_V2_ARGON2_VERSION,
crate::KSPWALLET_V2_DEFAULT_ARGON2_MEMORY_KIB,
crate::KSPWALLET_V2_DEFAULT_ARGON2_ITERATIONS,
crate::KSPWALLET_V2_DEFAULT_ARGON2_PARALLELISM,
salt,
);
}
/// Returns the KDF algorithm.
#[must_use]
pub const fn algorithm(&self) -> WalletKdfAlgorithmV2 {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/tests/public_api.rs
// version: 10
// version: 11
//! Public API canaries for the Wallet foundation.
@@ -177,3 +177,31 @@ fn public_pre_008_transfer_adapters_are_available_from_crate_root() {
);
drop(import_file_future);
}
#[test]
fn public_pre_016_version_neutral_and_explicit_v2_surfaces_are_available_from_crate_root() {
assert_eq!(ksp_wallet_lib::DEFAULT_WALLET_FORMAT, ksp_wallet_lib::WalletFormat::V2);
assert_eq!(ksp_wallet_lib::LATEST_SUPPORTED_WALLET_FORMAT, ksp_wallet_lib::WalletFormat::V2);
assert_eq!(ksp_wallet_lib::WalletFormat::V1.version(), ksp_wallet_lib::KSPWALLET_FORMAT_VERSION_V1);
assert_eq!(ksp_wallet_lib::WalletFormat::V2.version(), ksp_wallet_lib::KSPWALLET_FORMAT_VERSION_V2);
let _ = ksp_wallet_lib::create_wallet;
let _ = ksp_wallet_lib::create_wallet_v2;
let _ = ksp_wallet_lib::open_wallet_view;
let _ = ksp_wallet_lib::open_wallet_view_v2;
let _ = ksp_wallet_lib::open_wallet_owner;
let _ = ksp_wallet_lib::open_wallet_owner_v2;
let _ = ksp_wallet_lib::inspect_locked_wallet;
let _ = ksp_wallet_lib::inspect_locked_wallet_v2;
let path = std::path::Path::new("not-polled-pre016.kspwallet");
let owner_password = ksp_wallet_lib::OwnerPassword::new(std::string::String::from("public-pre016-owner-password"));
let create_future = ksp_wallet_lib::create_wallet_file(path, owner_password, std::option::Option::None, ksp_wallet_lib::WalletCreateMetadata::default());
drop(create_future);
let owner_password = ksp_wallet_lib::OwnerPassword::new(std::string::String::from("public-pre016-owner-password"));
let create_v2_future =
ksp_wallet_lib::create_wallet_file_v2(path, owner_password, std::option::Option::None, ksp_wallet_lib::WalletCreateMetadata::default());
drop(create_v2_future);
let inspect_future = ksp_wallet_lib::inspect_locked_wallet_file(path);
drop(inspect_future);
let inspect_v2_future = ksp_wallet_lib::inspect_locked_wallet_file_v2(path);
drop(inspect_v2_future);
}

View File

@@ -0,0 +1,18 @@
// file: crates/ksp-wallet-lib/unit_tests/format.rs
// version: 1
#[test]
fn default_and_latest_are_explicit_and_currently_v2() {
assert_eq!(crate::DEFAULT_WALLET_FORMAT, crate::WalletFormat::V2);
assert_eq!(crate::LATEST_SUPPORTED_WALLET_FORMAT, crate::WalletFormat::V2);
assert_eq!(crate::DEFAULT_WALLET_FORMAT.version(), crate::KSPWALLET_FORMAT_VERSION_V2);
}
#[test]
fn detector_distinguishes_v1_json_and_v2_binary_without_crypto() -> ksp_core_lib::Result<()> {
let v1 = include_bytes!("../tests/fixtures/kspwallet_v1_wire_only.json");
let v2 = include_bytes!("../tests/fixtures/kspwallet_v2_wire_only.bin");
assert_eq!(crate::detect_wallet_format(v1)?, crate::WalletFormat::V1);
assert_eq!(crate::detect_wallet_format(v2)?, crate::WalletFormat::V2);
return std::result::Result::Ok(());
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/unit_tests/transfer.rs
// version: 3
// version: 4
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import
@@ -144,6 +144,41 @@ fn cli_json_import_creates_new_no_clobber_wallet_with_imported_identity_and_meta
source.zeroize();
}
#[test]
fn default_transfer_import_creates_v2_without_removing_explicit_v1_import() {
let directory = tempfile::tempdir().expect("Wallet transfer test directory must be creatable");
let default_destination = directory.path().join("default-v2.kspwallet");
let explicit_v1_destination = directory.path().join("explicit-v1.kspwallet");
let keypair = test_keypair();
let mut source = cli_json(&keypair);
let runtime = runtime();
let default_owner = runtime
.block_on(crate::import_wallet_transfer(
default_destination.as_path(),
source.as_slice(),
crate::WalletTransferFormat::SolanaCliJson,
crate::OwnerPassword::new(std::string::String::from("pre016-default-import-owner")),
std::option::Option::None,
crate::WalletCreateMetadata::default(),
))
.expect("default transfer import must succeed");
assert_eq!(default_owner.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2);
let default_bytes = std::fs::read(default_destination).expect("default imported Wallet must be readable");
assert_eq!(crate::detect_wallet_format(default_bytes.as_slice()).expect("default imported Wallet must detect"), crate::WalletFormat::V2);
let explicit_owner = runtime
.block_on(crate::import_wallet_transfer_v1(
explicit_v1_destination.as_path(),
source.as_slice(),
crate::WalletTransferFormat::SolanaCliJson,
crate::OwnerPassword::new(std::string::String::from("pre016-explicit-v1-import-owner")),
std::option::Option::None,
crate::WalletCreateMetadataV1::default(),
))
.expect("explicit V1 transfer import must remain available");
assert_eq!(explicit_owner.format_version(), crate::KSPWALLET_FORMAT_VERSION_V1);
source.zeroize();
}
#[test]
fn transfer_file_import_is_non_destructive_and_bounded() {
let directory = tempfile::tempdir().expect("Wallet transfer test directory must be creatable");

View File

@@ -0,0 +1,126 @@
// file: crates/ksp-wallet-lib/unit_tests/wallet_v2.rs
// version: 1
const V1_FULL_VECTOR: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_full_vector.json");
fn runtime() -> tokio::runtime::Runtime {
return tokio::runtime::Builder::new_current_thread().build().expect("Wallet V2 test runtime must build");
}
#[test]
fn v2_create_open_and_generic_dispatch_preserve_authorized_identity() {
let runtime = runtime();
let owner_password_text = std::string::String::from("pre016-v2-owner-password");
let view_password_text = std::string::String::from("pre016-v2-view-password");
let owner = runtime
.block_on(crate::create_wallet_v2(
crate::OwnerPassword::new(owner_password_text.clone()),
std::option::Option::Some(crate::ViewPassword::new(view_password_text.clone())),
crate::WalletCreateMetadataV1::new(std::option::Option::Some(std::string::String::from("pre016-v2")), std::vec::Vec::new()),
))
.expect("V2 wallet creation must succeed");
assert_eq!(owner.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2);
let pubkey = *owner.pubkey();
let bytes = owner.to_native_bytes().expect("V2 owner must serialize native bytes");
assert_eq!(crate::detect_wallet_format(bytes.as_slice()).expect("V2 framing must detect"), crate::WalletFormat::V2);
assert!(owner.to_json_bytes().is_err());
let view = runtime
.block_on(crate::open_wallet_view(bytes.as_slice(), crate::ViewPassword::new(view_password_text)))
.expect("generic VIEW open must dispatch V2");
assert_eq!(view.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2);
assert_eq!(*view.pubkey(), pubkey);
assert_eq!(view.alias(), std::option::Option::Some("pre016-v2"));
let reopened = runtime
.block_on(crate::open_wallet_owner(bytes.as_slice(), crate::OwnerPassword::new(owner_password_text)))
.expect("generic OWNER open must dispatch V2");
assert_eq!(reopened.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2);
assert_eq!(*reopened.pubkey(), pubkey);
}
#[test]
fn generic_dispatch_keeps_v1_read_compatibility() {
let runtime = runtime();
assert_eq!(crate::detect_wallet_format(V1_FULL_VECTOR).expect("V1 framing must detect"), crate::WalletFormat::V1);
let locked = crate::inspect_locked_wallet(V1_FULL_VECTOR).expect("generic inspect must keep V1 compatibility");
assert_eq!(locked.format_version(), crate::KSPWALLET_FORMAT_VERSION_V1);
let owner = runtime
.block_on(crate::open_wallet_owner(V1_FULL_VECTOR, crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))))
.expect("generic OWNER open must dispatch V1");
assert_eq!(owner.format_version(), crate::KSPWALLET_FORMAT_VERSION_V1);
}
#[test]
fn default_file_creation_is_v2_and_v2_administration_stays_v2() {
let directory = tempfile::Builder::new().prefix("ksp-pre016-v2-").tempdir().expect("V2 test directory must be creatable");
let path = directory.path().join("default.kspwallet");
let runtime = runtime();
let mut owner = runtime
.block_on(crate::create_wallet_file(
path.as_path(),
crate::OwnerPassword::new(std::string::String::from("pre016-default-owner")),
std::option::Option::Some(crate::ViewPassword::new(std::string::String::from("pre016-default-view"))),
crate::WalletCreateMetadataV1::default(),
))
.expect("default file creation must succeed");
assert_eq!(owner.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2);
let locked = runtime.block_on(crate::inspect_locked_wallet_file(path.as_path())).expect("generic inspect must read default file");
assert_eq!(locked.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2);
runtime
.block_on(owner.update_alias(path.as_path(), std::option::Option::Some(std::string::String::from("pre016-updated"))))
.expect("V2 metadata update must persist in V2");
runtime
.block_on(owner.rotate_owner_password(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre016-owner-rotated"))))
.expect("V2 OWNER rotation must persist in V2");
runtime
.block_on(owner.rotate_view_password(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre016-view-rotated"))))
.expect("V2 VIEW rotation by OWNER must persist in V2");
let reopened_owner = runtime
.block_on(crate::open_wallet_owner_file(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre016-owner-rotated"))))
.expect("rotated V2 OWNER must reopen through generic dispatch");
assert_eq!(reopened_owner.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2);
assert_eq!(reopened_owner.alias(), std::option::Option::Some("pre016-updated"));
let reopened_view = runtime
.block_on(crate::open_wallet_view_file(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre016-view-rotated"))))
.expect("rotated V2 VIEW must reopen through generic dispatch");
assert_eq!(reopened_view.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2);
assert_eq!(reopened_view.alias(), std::option::Option::Some("pre016-updated"));
runtime.block_on(owner.disable_view(path.as_path())).expect("V2 strong VIEW disable must persist");
let disabled = runtime.block_on(crate::inspect_locked_wallet_file(path.as_path())).expect("disabled V2 must remain inspectable");
assert!(!disabled.view_enabled());
let old_view = runtime.block_on(crate::open_wallet_view_file(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre016-view-rotated"))));
assert_eq!(old_view.expect_err("disabled V2 VIEW must not reopen").code(), crate::ERROR_CODE_CAPABILITY_INSUFFICIENT);
runtime
.block_on(owner.recreate_view(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre016-view-recreated"))))
.expect("V2 strong VIEW recreation must persist");
let mut recreated_view = runtime
.block_on(crate::open_wallet_view_file(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre016-view-recreated"))))
.expect("recreated V2 VIEW must open");
runtime
.block_on(recreated_view.rotate_view_password(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre016-view-self-rotated"))))
.expect("V2 VIEW self-rotation must persist");
let final_view = runtime
.block_on(crate::open_wallet_view_file(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre016-view-self-rotated"))))
.expect("self-rotated V2 VIEW must reopen");
assert_eq!(final_view.format_version(), crate::KSPWALLET_FORMAT_VERSION_V2);
let bytes = std::fs::read(path).expect("V2 file must remain readable for framing canary");
assert_eq!(crate::detect_wallet_format(bytes.as_slice()).expect("administered file must still be V2"), crate::WalletFormat::V2);
}
#[test]
fn explicit_versioned_readers_reject_the_other_native_wire_format() {
let runtime = runtime();
let v1_as_v2 =
runtime.block_on(crate::open_wallet_owner_v2(V1_FULL_VECTOR, crate::OwnerPassword::new(std::string::String::from("irrelevant-before-v2-parse"))));
assert_eq!(v1_as_v2.expect_err("explicit V2 reader must reject V1 framing").code(), crate::ERROR_CODE_FORMAT_INVALID);
let owner = runtime
.block_on(crate::create_wallet_v2(
crate::OwnerPassword::new(std::string::String::from("pre016-strict-v2-owner")),
std::option::Option::None,
crate::WalletCreateMetadata::default(),
))
.expect("strict-reader V2 fixture creation must succeed");
let bytes = owner.to_native_bytes().expect("strict-reader V2 fixture must serialize");
let v2_as_v1 =
runtime.block_on(crate::open_wallet_owner_v1(bytes.as_slice(), crate::OwnerPassword::new(std::string::String::from("irrelevant-before-v1-parse"))));
assert_eq!(v2_as_v1.expect_err("explicit V1 reader must reject V2 framing").code(), crate::ERROR_CODE_FORMAT_INVALID);
}

391
deltas/0.2.6/pre.016.md Normal file
View File

@@ -0,0 +1,391 @@
<!-- file: deltas/0.2.6/pre.016.md -->
<!-- version: 1 -->
# Delta `0.2.6-pre.016` — runtime `.kspwallet` V2, APIs multi-version et Wallet Desk version-neutral
## Base requise
```text
0.2.6-pre.015-fix.001 appliquée
workspace.package.version = 0.2.6-pre.15.fix.1
```
Le checkpoint opérateur de cette base est intégralement vert :
```text
cargo fmt --all OK
python3 scripts/audit_rust_workspace_rules.py clean
cargo check --workspace OK
cargo clippy --workspace --all-targets OK
cargo test --workspace OK
ksp-wallet-lib 71 passed / 1 ignored
Wallet Desk release_compliance 4/4 OK
smokes réseau / benchmark ignored comme prévu
```
Le correctif `pre.015-fix.001` est donc accepté comme base de `pre.016`.
## Signal technique
Cette tranche modifie les APIs/runtime Wallet et le backend Wallet Desk :
```text
workspace.package.version = 0.2.6-pre.16
commit = v0.2.6-pre.016
```
Aucun tag prerelease.
Les `package.json`/`tauri.conf.json` ne sont pas modifiés : aucune dépendance frontend, configuration Tauri, permission ou contrat de bundle n'est changé. Le signal technique Rust reste porté par `workspace.package.version`.
## Objectif de la tranche
`pre.016` matérialise le contrat décidé en `pre.015` : V2 devient le **format natif default explicite** des créations/imports non versionnés, sans rendre le default dépendant de la dernière version supportée.
La tranche ajoute simultanément :
```text
runtime cryptographique V2 create/open VIEW/OWNER
persistence V2 create no-clobber + remplacement atomique
state-conflict V2 authentifié
façade in-memory multi-version
façade filesystem multi-version
variantes explicites V1/V2
import transfer default V2 + variantes V1/V2
handles OWNER/VIEW version-neutral
administration V2 complète
Wallet Desk exclusivement sur APIs génériques
canaris default/latest, V1/V2 et strict version dispatch
```
La migration persistée V1 -> V2 reste volontairement hors scope et appartient à `pre.017`.
## Politique durable : default distinct de latest
La politique est désormais matérialisée dans `ksp-wallet-lib` :
```rust
DEFAULT_WALLET_FORMAT = WalletFormat::V2
LATEST_SUPPORTED_WALLET_FORMAT = WalletFormat::V2
```
`WalletFormat` est public et `non_exhaustive` afin qu'un futur V3 puisse être ajouté sans inviter les consumers à supposer que V2 est la dernière version possible.
Invariant durable :
```text
DEFAULT_WALLET_FORMAT != conceptuellement LATEST_SUPPORTED_WALLET_FORMAT
```
Les deux valeurs sont V2 dans cette release, mais leur égalité est contingente. Si V3 apparaît plus tard :
```text
DEFAULT_WALLET_FORMAT peut rester V2
LATEST_SUPPORTED_WALLET_FORMAT peut devenir V3
```
Une nouvelle version supportée ne change jamais automatiquement le default.
## APIs de création
### In-memory
```text
create_wallet(...) -> DEFAULT_WALLET_FORMAT, donc V2
create_wallet_v1(...) -> V1 forcé
create_wallet_v2(...) -> V2 forcé
```
### Filesystem
```text
create_wallet_file(...) -> DEFAULT_WALLET_FORMAT, donc V2
create_wallet_file_v1(...) -> V1 forcé
create_wallet_file_v2(...) -> V2 forcé
```
Le type version-neutral `WalletCreateMetadata` est exposé pour les nouvelles APIs ; `WalletCreateMetadataV1` reste conservé pour compatibilité explicite V1. Le profil plaintext initial de metadata V2 conserve la sémantique établie en V1, conformément à la spec V2.
## APIs de lecture et inspection
Les APIs génériques détectent uniquement le framing nécessaire à la sélection du parser :
```text
open_wallet_view(...)
open_wallet_owner(...)
inspect_locked_wallet(...)
open_wallet_view_file(...)
open_wallet_owner_file(...)
inspect_locked_wallet_file(...)
```
Dispatch :
```text
JSON dont le premier octet significatif est '{' -> parser strict V1
magic binaire KSPWALLET + version 2 -> parser strict V2
autre framing/version -> erreur format/version
```
La détection n'est jamais une validation de substitution : le parser V1/V2 sélectionné reste autoritatif pour toutes les contraintes structurelles et cryptographiques.
Les APIs explicitement versionnées restent strictes :
```text
*_v1 refuse V2
*_v2 refuse V1
```
## Runtime V2
`wallet_v2.rs` compose le wire/transcript figé en `pre.015` avec les primitives Wallet existantes :
```text
Argon2id password KDF
XChaCha20-Poly1305 key wrapping
XChaCha20-Poly1305 OWNER-CONTROL / METADATA / SECRET
Ed25519 OWNER state signature
VIEW / OWNER indépendants
Solana keypair secret compartment
```
V2 conserve le profil plaintext fonctionnel initial de V1 pour owner-control, metadata et secret, mais utilise exclusivement :
```text
wire V2
IDs V2
AAD V2
state transcript V2
format_version = 2
```
Les domains V1/V2 restent non interchangeables.
## Handles OWNER / VIEW version-neutral
`WalletOwner` et `WalletView` ne sont plus couplés à `OwnerStateV1` / `ViewStateV1`.
Un dispatch runtime interne conserve le format authentifié :
```text
OwnerState::V1 / OwnerState::V2
ViewState::V1 / ViewState::V2
```
Les opérations publiques restent stables et dispatchent dans le format du handle :
```text
signature OWNER
metadata alias/notes
rotation OWNER
rotation VIEW par OWNER
VIEW self-rotation
disable VIEW fort
recreate VIEW fort
export transfer
```
Une mutation V1 reste V1. Une mutation V2 reste V2.
Aucune de ces opérations ne constitue une migration.
### Sérialisation des handles
Nouvelle surface :
```text
to_native_bytes() -> V1 JSON si handle V1, V2 binaire si handle V2
```
Compatibilité historique :
```text
to_json_bytes() -> fonctionne uniquement pour V1
rejette V2
```
Il n'existe aucune transcodification implicite V2 -> V1 ou V1 -> V2.
## Persistence V2 et stale-handle protection
La création V2 conserve les garanties V1 :
```text
construction complète en mémoire
écriture temporaire
sync
publication no-clobber
aucun overwrite d'une destination existante
```
Les mutations V2 réutilisent la discipline atomique avec une vérification format-spécifique :
```text
relire destination bornée
parse_binary V2 strict
vérifier state signature V2
comparer l'enveloppe authentifiée attendue
publier seulement si état courant identique
```
Un handle V2 stale reçoit le même contrat `wallet.state_conflict` qu'un handle V1.
## Import transfer
Les formats de transfert externes restent indépendants du format natif `.kspwallet` :
```text
Solana CLI JSON
Solana keypair Base58 complet
```
La sortie native suit désormais :
```text
import_wallet_transfer(...) -> V2 default
import_wallet_transfer_v1(...) -> V1 forcé
import_wallet_transfer_v2(...) -> V2 forcé
import_wallet_transfer_file(...) -> V2 default
import_wallet_transfer_file_v1(...) -> V1 forcé
import_wallet_transfer_file_v2(...) -> V2 forcé
```
La keypair Solana reste identique ; seul le nouvel environnement natif KSP est créé dans la version choisie.
## Wallet Desk
`ksp-app-wallet-desk` ne référence plus aucune API native versionnée en production.
Ses callsites utilisent uniquement :
```text
create_wallet_file
import_wallet_transfer
open_wallet_view_file
open_wallet_owner_file
inspect_locked_wallet_file
```
Conséquences :
```text
nouveau Wallet créé depuis le Desk V2
nouvel import depuis le Desk V2
wallet V1 existant toujours inspectable/ouvrable
wallet V2 inspectable/ouvrable
mutations après unlock format natif conservé
frontend ne connaît pas V1/V2 pour dispatcher
```
La version reste visible dans les projections métier (`format_version`) mais n'est pas une responsabilité de routing du frontend/Tauri.
## Canaris `pre.016`
Les nouveaux tests couvrent notamment :
```text
DEFAULT_WALLET_FORMAT = V2
LATEST_SUPPORTED_WALLET_FORMAT = V2
framing detector V1/V2
création V2 + open générique VIEW/OWNER
lecture générique d'une fixture V1 stable
création fichier générique -> V2
metadata V2 persistée
rotation OWNER V2
rotation VIEW V2 par OWNER
disable/recreate VIEW V2
VIEW self-rotation V2
wire V2 conservé après administration
lecteur V1 forcé refuse V2
lecteur V2 forcé refuse V1
import transfer générique -> V2
import transfer V1 explicite toujours disponible
Wallet Desk sans callsite *_v1/*_v2
```
Les anciens canaris Desk `pre.006`/`pre.009` sont réalignés sur la façade générique sans supprimer leurs garanties de sécurité historiques.
## Hors scope
`pre.016` ne réalise pas :
```text
migration V1 -> V2
réécriture automatique à l'ouverture
suppression des APIs V1
V3 / second facteur
changement de default futur automatique
frontend spécifique au format
nouvelle permission Tauri
build Tauri de production
```
## Documentation synchronisée
Cette tranche met à jour :
```text
ROADMAP.md
crates/ksp-wallet-lib/README.md
crates/ksp-wallet-lib/USAGE.md
docs/IDEAS.md
docs/architecture/004-COMPONENT_INVENTORY.md
docs/formats/KSPWALLET_V2.md
docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md
docs/validation/009-V0_2_6_WALLET_DESK_COMPLIANCE.md
prompts/011-V0_2_6_START_PROMPT.md
```
`CHANGELOG.md` reste réservé à la clôture `pre.018`.
## Prochaine tranche
`0.2.6-pre.017` doit maintenant porter la migration **explicite et authentifiée** V1 -> V2 :
```text
API de migration nommée explicitement
source V1 authentifiée
nouveau document V2 cryptographiquement valide
aucune migration lors d'un open ordinaire
stratégie destination/no-clobber ou remplacement explicitement choisie
préservation identité Solana + metadata + policy VIEW applicable
canaris stale/tampering/failure atomicity
régression complète Wallet Desk V1/V2
```
La candidate/documentation finale reste `pre.018`.
## Validation opérateur requise
Après application du delta :
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-wallet-lib
cargo test -p ksp-app-wallet-desk
cargo test --workspace
```
Un contrôle fonctionnel Wallet Desk est également utile avant `pre.017` :
```bash
(cd crates/ksp-app-wallet-desk && cargo tauri dev)
```
Vérifier au minimum :
```text
création -> format_version 2
import -> format_version 2
inventory V1/V2
unlock OWNER/VIEW
metadata + rotations sur V2
balance depuis handle autorisé
```
**Ne pas exécuter `cargo tauri build` en `pre.016`.** Le build de production reste réservé à `pre.018` et doit demeurer l'ultime opération de validation de la candidate.

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
<!-- file: docs/IDEAS.md -->
<!-- version: 21 -->
<!-- version: 22 -->
# Idées à explorer
@@ -206,7 +206,7 @@ Chaque format doit être étudié côté sécurité, round-trip, secret/public,
**Status :** V2 retenu et matérialisé en `0.2.6-pre.015` / facteurs futurs à explorer
Le JSON V1 actuel reste le format historique stable et lisible. Base64 seul napporte aucune sécurité et resterait un texte trivialement décodable avec environ un tiers de surcharge. La décision initialement envisagée pour `0.2.7` a été ramenée dans `0.2.6` : `pre.015` définit un **wire binaire V2 KSP** avec magic/framing explicite, entiers big-endian, identifiants numériques stables, longueurs bornées et lecture/écriture canonique stricte. V1 reste supporté sans réinterprétation ; la façade de lecture multi-version, la création V2 et la politique `DEFAULT_WALLET_FORMAT = V2` arrivent en `pre.016`, puis la migration explicite V1 -> V2 en `pre.017`. Le V2 ne modifie pas à lui seul les garanties cryptographiques de VIEW/OWNER, keypair ou import/export.
Le JSON V1 actuel reste le format historique stable et lisible. Base64 seul napporte aucune sécurité et resterait un texte trivialement décodable avec environ un tiers de surcharge. La décision initialement envisagée pour `0.2.7` a été ramenée dans `0.2.6` : `pre.015` définit un **wire binaire V2 KSP** avec magic/framing explicite, entiers big-endian, identifiants numériques stables, longueurs bornées et lecture/écriture canonique stricte. V1 reste supporté sans réinterprétation ; la façade de lecture multi-version, la création/persistence V2, les APIs `_v1/_v2` et la politique `DEFAULT_WALLET_FORMAT = V2` sont matérialisées en `pre.016`, puis la migration explicite V1 -> V2 reste planifiée en `pre.017`. Le V2 ne modifie pas à lui seul les garanties cryptographiques de VIEW/OWNER, keypair ou import/export.
V2 est désormais réservé au wire binaire KSP sans second facteur. Un futur V3 pourra introduire dautres modèles dautorisation, notamment password + facteur supplémentaire. `ksp-wallet-lib` restera propriétaire du format, des challenges et de la vérification, mais toute interaction réelle (OTP, enrollment/recovery, hardware/WebAuthn, validation distante) exigera une évolution de Wallet Desk ou du client concerné. Un seed TOTP stocké uniquement dans le même fichier que le wallet ne doit pas être présenté automatiquement comme un second facteur indépendant contre un attaquant possédant ce fichier.
@@ -258,7 +258,6 @@ Définir la politique lorsqu'un registry reçoit plusieurs implémentations capa
Le principe `domain/program/capability` est retenu. Les noms exacts des dossiers courts (`dec`, `exec_prep`) seront validés avec la première vraie arborescence.
## Execution — idées d'implémentation
### Composition de policies

View File

@@ -1,5 +1,5 @@
<!-- file: docs/architecture/004-COMPONENT_INVENTORY.md -->
<!-- version: 17 -->
<!-- version: 18 -->
# Inventaire initial des composants KSP
@@ -26,7 +26,7 @@ Ce document maintient l'inventaire synthétique des composants retenus ou presse
| On-chain HTTP | `ksp-onchain-transport-lib` | lib | Stable | `0.2.1``0.2.4` | HTTP standard complet : 52/52 current + 14/14 historical |
| Wallet | `ksp-wallet-lib` | lib | Stable | `0.2.5` | `.kspwallet`, VIEW/OWNER, secrets, signature, import/export |
| Wallet Desk | `ksp-app-wallet-desk` | app | Retenu | `0.2.6` | Wallet + Config composite + HTTP/balance |
| Wallet V2 | `ksp-wallet-lib` | lib | En cours | `0.2.6` | wire binaire V2 + API default/versionnée + migration |
| Wallet V2 | `ksp-wallet-lib` | lib | En cours | `0.2.6` | wire/runtime V2 + API default/versionnée ; migration pre.017 |
| Standard WS | `ksp-onchain-transport-lib` | lib | Retenu | `0.2.7` | WebSocket Solana complet, sessions/subscriptions |
| Helius WS | `ksp-onchain-transport-lib` | lib | Retenu | `0.2.8` | LaserStream WebSocket comme extension du moteur standard |
| Yellowstone | `ksp-onchain-transport-lib` | lib | Pressenti | `0.2.9` | client gRPC standard/provider-neutral |

View File

@@ -1,5 +1,5 @@
<!-- file: docs/formats/000-README.md -->
<!-- version: 6 -->
<!-- version: 7 -->
# Formats KSP
@@ -10,4 +10,4 @@ Une spécification de format décrit le wire exact, les encodages, les limites,
## Formats actifs
- [`KSPWALLET_V1.md`](KSPWALLET_V1.md) — spécification du format natif autonome `.kspwallet` V1. `0.2.5-pre.003` fige l'enveloppe/wire et les transcripts/AAD, `pre.004` ajoute Argon2id/XChaCha20-Poly1305/CSPRNG OS, `pre.005` fixe les payloads plaintext, le profil de création KSP calibré, l'autorité Ed25519 OWNER et le vecteur complet, `pre.006``pre.008` matérialisent persistence/administration/transfert, `pre.009` ferme l'audit adversarial/interoperabilité/compliance et `pre.010` synchronise la documentation finale sans modifier le wire V1.
- [`KSPWALLET_V2.md`](KSPWALLET_V2.md) — wire binaire natif V2 introduit par `0.2.6-pre.015` : framing canonique KSP, IDs numériques, longueurs big-endian, aucun Base64/compression, domains/transcripts V2 distincts et politique `default != latest`.
- [`KSPWALLET_V2.md`](KSPWALLET_V2.md) — wire binaire natif V2 introduit par `0.2.6-pre.015` puis runtime multi-version matérialisé en `pre.016` : framing canonique KSP, IDs numériques, longueurs big-endian, aucun Base64/compression, domains/transcripts V2 distincts et politique `default != latest`.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/formats/KSPWALLET_V1.md -->
<!-- version: 12 -->
<!-- version: 13 -->
# `.kspwallet` V1 — spécification du format natif Wallet KSP
@@ -1043,7 +1043,6 @@ Limites explicitement conservées en V1 :
- la zeroization réduit les copies possédées mais ne constitue pas une preuve d'effacement physique de toute copie potentielle produite par le compilateur, l'OS ou le matériel ;
- aucune revendication de résistance side-channel supplémentaire au-delà des primitives et bibliothèques retenues.
## 26. Statut de clôture V1
À la publication stable `0.2.5`, cette spécification constitue la version normative V1 du format `.kspwallet`. Les guides d'utilisation KSP sont [`../../crates/ksp-wallet-lib/README.md`](../../crates/ksp-wallet-lib/README.md) et [`../../crates/ksp-wallet-lib/USAGE.md`](../../crates/ksp-wallet-lib/USAGE.md) ; ils ne remplacent pas le présent document comme autorité normative du wire.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/formats/KSPWALLET_V2.md -->
<!-- version: 1 -->
<!-- version: 2 -->
# `.kspwallet` V2 — spécification du wire binaire natif KSP
@@ -19,7 +19,7 @@ préserver les frontières cryptographiques Wallet
Le caractère binaire **n'ajoute aucune propriété cryptographique**. La confidentialité et l'authenticité continuent de dépendre d'Argon2id, XChaCha20-Poly1305 et Ed25519, jamais de l'absence de JSON.
`0.2.6-pre.015` fige la grammaire binaire, les identifiants numériques, les bornes structurelles et les transcripts/AAD V2. La création/ouverture/persistence V2, l'auto-détection V1/V2 et le basculement de l'API générique vers V2 sont matérialisés dans les tranches suivantes de `0.2.6`.
`0.2.6-pre.015` fige la grammaire binaire, les identifiants numériques, les bornes structurelles et les transcripts/AAD V2. `0.2.6-pre.016` matérialise la création/ouverture/persistence V2, l'auto-détection V1/V2, les APIs génériques/versionnées et le basculement de Wallet Desk vers la façade non versionnée.
V1 reste un format stable supporté. V2 ne réinterprète jamais un document V1 avec sa propre grammaire.
@@ -57,6 +57,30 @@ open_wallet_*_file_v3(...) -> exige V3 si V3 existe un jour
Le même principe s'applique aux opérations dont la version de fichier est pertinente, notamment inspection et import natif.
### 2.1 Surface matérialisée en `pre.016`
La politique ci-dessus est désormais du code, pas uniquement une cible documentaire :
```text
create_wallet(...) -> V2 default
create_wallet_v1(...) -> V1 forcé
create_wallet_v2(...) -> V2 forcé
create_wallet_file(...) -> V2 default
create_wallet_file_v1(...) -> V1 forcé
create_wallet_file_v2(...) -> V2 forcé
open_wallet_view/owner(...) -> détecte V1/V2
open_wallet_*_v1/_v2(...) -> exige exactement V1/V2
inspect_locked_wallet(...) -> détecte V1/V2
inspect_locked_wallet_v1/_v2 -> exige exactement V1/V2
import_wallet_transfer(...) -> crée le default V2
import_wallet_transfer_v1/_v2 -> force le format natif produit
```
`WalletOwner` et `WalletView` mémorisent le format natif authentifié. Les opérations d'administration et de rotation persistent dans ce même format ; aucune mutation ordinaire ne réalise de conversion V1/V2. `to_native_bytes()` sérialise le format courant, tandis que `to_json_bytes()` reste une compatibilité V1 et rejette V2.
## 3. Encodage général
Un document V2 est :
@@ -81,7 +105,7 @@ Une implémentation conforme doit vérifier les bornes **avant** toute allocatio
Le début de fichier est strictement :
| Ordre | Champ | Taille | Valeur / règle |
|------:|---------------------------|-----------------:|------------------------------------------------|
|---:|---|---:|---|
| 1 | `magic` | 9 | ASCII exact `KSPWALLET` |
| 2 | `format_version` | 2 | `0x0002` |
| 3 | `document_length` | 4 | longueur totale exacte du fichier |
@@ -131,7 +155,7 @@ Aucun compteur de slots ou de compartiments n'est nécessaire : leur cardinalit
Chaque key slot est encodé ainsi :
| Champ | Taille | Valeur / règle |
|--------------------------|---------:|-----------------------------------------|
|---|---:|---|
| `role` | 1 | `0x01` OWNER, `0x02` VIEW |
| `slot_id` | 16 | identifiant binaire exact |
| `kdf_algorithm` | 1 | `0x01` Argon2id |
@@ -166,7 +190,7 @@ Toute divergence est invalide avant KDF/déchiffrement.
Chaque compartiment est encodé :
| Champ | Taille | Valeur / règle |
|---------------------|---------:|------------------------------------------------------|
|---|---:|---|
| `kind` | 1 | `0x01` OWNER-CONTROL, `0x02` METADATA, `0x03` SECRET |
| `payload_version` | 4 | `1` pour le profil initial V2 |
| `algorithm` | 1 | `0x01` XChaCha20-Poly1305 |
@@ -191,7 +215,7 @@ Les payloads plaintext V2 conservent le modèle fonctionnel établi en V1 pour c
La fin du document est :
| Champ | Taille | Valeur / règle |
|-----------------------------|-------:|---------------------------|
|---|---:|---|
| `state_signature.algorithm` | 1 | `0x01` Ed25519 |
| `state_signature.signature` | 64 | signature detached exacte |
@@ -354,7 +378,7 @@ Le nonce/ciphertext n'est pas inclus dans son propre AAD.
V1 et V2 sont deux formats explicites :
| Propriété | V1 | V2 |
|--------------------------|------------------------------------|---------------|
|---|---|---|
| enveloppe | JSON UTF-8 | binaire KSP |
| champs binaires | Base64url no-pad | bytes directs |
| version | `1` | `2` |
@@ -402,7 +426,7 @@ Lecture :
0001 = VIEW_ENABLED
```
La fixture `wire_only` vérifie framing/canonicalité ; elle n'est pas présentée comme une signature cryptographique V2 valide tant que la création/open V2 n'est pas intégrée par la tranche suivante.
La fixture `wire_only` vérifie uniquement framing/canonicalité et reste volontairement distincte des wallets V2 runtime produits depuis `pre.016`; elle n'est pas présentée comme une signature cryptographique valide.
## 14. Compatibilité et migration

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/000-README.md -->
<!-- version: 50 -->
<!-- version: 51 -->
# Plans KSP
@@ -21,7 +21,7 @@ Un plan décrit le périmètre, les décisions déjà acquises, les questions ou
- [`010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md`](010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md) — plan historique clôturé de la release stable `0.2.3 — HTTP Transactions`, ouvert par `pre.001`, exécuté jusqu'à `pre.009` puis publié par `rel.001`; il couvre les 11 méthodes, la classification `8 Read / 2 WriteSubmission / 1 Simulation`, `KSP-TRANSPORT-007`, le no-resend et la préparation de `0.2.4`.
- [`011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md`](011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md) — plan historique clôturé de la release stable `0.2.4`, ouvert par `pre.001`, exécuté jusquà `pre.009`, complété par le fix documentaire Wallet `pre.009-fix.001` puis publié par `rel.001`; il couvre les 10 Blocks + 5 Economics et la compliance finale `52/52 + 14/14` sous `KSP-TRANSPORT-007`.
- [`012-V0_2_5_WALLET_FOUNDATION_PLAN.md`](012-V0_2_5_WALLET_FOUNDATION_PLAN.md) — plan historique clôturé de la release stable `0.2.5 — Wallet foundation`, ouvert par `pre.001`, livré jusquà `pre.010`, renforcé par `pre.010-fix.001``fix.003` pour Dalek 3 et la normalisation Rust/audit structurel, puis publié par `rel.001`; il couvre `.kspwallet` V1, VIEW/OWNER, crypto, persistence, administration, transfer et compliance.
- [`013-V0_2_6_WALLET_DESK_PLAN.md`](013-V0_2_6_WALLET_DESK_PLAN.md) — plan actif de `0.2.6 — Wallet Desk`, ouvert par `pre.001`; `pre.002` matérialise le shell Tauri et `pre.003` la composition `std.wallet`/composite ainsi que la préparation des répertoires. Le plan fixe aussi secrets `KSP_SECRET_WALLET_PASS_*`, inventory DataTables/path safety, lifecycle VIEW/OWNER, screen/command/DTO maps, balance HTTP, administration retenue et forecast étendu jusquà `pre.018` : `pre.014` polish Bootstrap/splashscreen, `pre.015` wire binaire `.kspwallet` V2, `pre.016` APIs génériques/versionnées + V2, `pre.017` migration/canaris, puis `pre.018` documentation candidate et build Tauri final.
- [`013-V0_2_6_WALLET_DESK_PLAN.md`](013-V0_2_6_WALLET_DESK_PLAN.md) — plan actif de `0.2.6 — Wallet Desk`, ouvert par `pre.001`; `pre.002` matérialise le shell Tauri et `pre.003` la composition `std.wallet`/composite ainsi que la préparation des répertoires. Le plan fixe aussi secrets `KSP_SECRET_WALLET_PASS_*`, inventory DataTables/path safety, lifecycle VIEW/OWNER, screen/command/DTO maps, balance HTTP, administration retenue et forecast étendu jusquà `pre.018` : `pre.014` polish Bootstrap/splashscreen, `pre.015` wire binaire `.kspwallet` V2 et `pre.016` APIs génériques/versionnées + runtime V2 désormais matérialisés ; prochain gate `pre.017` migration/canaris, puis `pre.018` documentation candidate et build Tauri final.
Le `pre.001` de chaque release fonctionnelle peut introduire son propre plan détaillé lorsque la release s'ouvre.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md -->
<!-- version: 27 -->
<!-- version: 28 -->
# Plan `0.1.4` — `ksp-app-config-desk`
@@ -1302,7 +1302,6 @@ La validation finale a suivi KSP-APP-034 : tous les contrôles Rust et frontend,
`0.1.4` est clôturée et publiée comme release stable. `ksp-app-config-desk` valide les frontières Config/Logging/Tauri prévues par ce plan, la baseline Logging canonique est revenue à `info`/`warn`, les TODO non bloquants sont reportés explicitement, et le prochain point de reprise est `0.2.0-pre.001` via `prompts/005-V0_2_0_START_PROMPT.md`.
## 19. Dépendances et ordre d'introduction
Aucune dépendance n'est ajoutée par `pre.001`.
@@ -1458,7 +1457,6 @@ Aucune question n'empêche d'ouvrir le développement après validation du prés
Ces points doivent être résolus par code/tests dans les prereleases prévues, pas par contournement applicatif.
### Harmonisation desktop `0.2.6-pre.014`
Le polish partagé avec Wallet Desk déplace les pills de navigation Config dans une sidebar verticale du contenu principal, tout en conservant la palette claire historique de Config Desk. Les quatre fichiers HTML des deux Desks utilisent les en-têtes normalisés `file/version`. Le splash reprend les flux kbot3 (messages généraux en bas, diagnostics debug en haut) avec trois timings Config-owned distincts `KSP_DESK_SPLASH_FADE_IN_MS`, `KSP_DESK_SPLASH_MINIMUM_MS` et `KSP_DESK_SPLASH_FADE_OUT_MS`.

File diff suppressed because one or more lines are too long

View File

@@ -26,7 +26,6 @@
- **KSP-API-006** — `ksp-store-lib` contient PostgreSQL comme implémentation officielle de référence derrière `ksp-store-api`.
- **KSP-API-007** — Une crate `*-api` n'est créée que lorsqu'un vrai besoin d'extension, backend ou lifecycle le justifie ; la symétrie de nommage n'est jamais une justification suffisante.
## Configuration et environnement
- **KSP-CONFIG-001** — `ksp-config-lib` est l'unique propriétaire KSP de la lecture des documents Config, du `.env`, des variables applicatives `KSP_*` / `KSPB_*` et de leur résolution ; les autres crates ne lisent pas directement ces sources.
@@ -112,7 +111,6 @@
- **KSP-MAT-003** — Une extension externe de materializer doit pouvoir produire du D3 générique sans migration PostgreSQL spécialisée.
- **KSP-MAT-004** — Une nouvelle projection relationnelle D4 exige explicitement un contrat Store/migration/backend correspondant ; cette responsabilité n'est pas cachée dans `ksp-materializer-api`.
## Backlog, claims et reprise
- **KSP-PROC-001** — Le backlog est défini relativement à l'identité/version/capability du processor et non par simple absence d'une row de sortie.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/009-V0_2_6_WALLET_DESK_COMPLIANCE.md -->
<!-- version: 4 -->
<!-- version: 5 -->
# Validation `0.2.6` — Wallet Desk integration / compliance / security
@@ -52,7 +52,7 @@ Le checkpoint opérateur `pre.012` du 21 août 2026 est vert : `cargo fmt`, audi
## 3. Matrice de frontières
| Frontière | Contrat `0.2.6` | Preuve durable |
|------------------------------|--------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
|---|---|---|
| Frontend -> Config | aucun accès direct env/.env | `release_compliance::backend_keeps_config_wallet_transport_and_logging_ownership_boundaries` + ownership Config |
| Frontend -> Wallet secret | passwords uniquement request-only ; jamais de keypair en IPC | `desktop_security` + `release_compliance::response_dtos_keep_secret_key_material_out_of_ipc` |
| Frontend -> filesystem | aucun path arbitraire ; picker natif Rust | tests import/export + capability audit |
@@ -240,7 +240,19 @@ Le blocage workspace est limité à `ksp-logging-lib/tests/ownership.rs::workspa
La matrice reste ouverte jusqu'à `pre.018`, après les tranches V2 `pre.015``pre.017`; `pre.018` ajoute le parcours fonctionnel final, README/USAGE et le build Tauri exécuté en toute dernière opération.
## 11. Intercalation V2 à partir de `pre.015`
`pre.015` ne modifie pas encore Wallet Desk : il ajoute dans `ksp-wallet-lib` un codec binaire V2 strict et une fixture canonique. Les gates Desk `pre.013`/`pre.014` restent donc applicables sans changement. `pre.016` devra faire migrer le Desk vers les APIs génériques afin quil ne connaisse plus `_v1`/`_v2`.
`pre.015` ne modifie pas encore Wallet Desk : il ajoute dans `ksp-wallet-lib` un codec binaire V2 strict et une fixture canonique. Les gates Desk `pre.013`/`pre.014` restent donc applicables sans changement.
`pre.016` matérialise ensuite la façade version-neutral et fait migrer Wallet Desk vers elle :
```text
create/import Desk API générique -> default V2
open VIEW/OWNER Desk API générique -> détection V1/V2
inventory/selection Desk inspect générique -> détection V1/V2
aucun appel Desk à *_v1 ou *_v2 gate durable
V1 existant reste ouvrable sans migration
V2 nouvellement créé/importé administration complète conservée en V2
```
Les canaris `pre.016` couvrent également le refus croisé des lecteurs explicitement versionnés et le maintien du format V2 après metadata/rotations/disable/recreate/self-rotation VIEW. La migration V1 -> V2 reste explicitement hors de cette tranche et appartient à `pre.017`.

View File

@@ -1,5 +1,5 @@
<!-- file: prompts/011-V0_2_6_START_PROMPT.md -->
<!-- version: 6 -->
<!-- version: 7 -->
# Prompt de démarrage `0.2.6` — Wallet Desk
@@ -704,7 +704,6 @@ Ensuite :
Les règles de la session ne doivent pas être renégociées au fil des corrections : elles sont dans les documents KSP versionnés et doivent être appliquées dès chaque premier patch. Si un conflit ou une règle manquante est découvert, corriger la règle et son contrôle durable avant de propager une nouvelle convention.
## `pre.014-fix.001` — retour opérateur après reprise de session
Le correctif de polish après `pre.014` traite uniquement les régressions de gabarit et de canari :
@@ -720,7 +719,12 @@ version technique : 0.2.6-pre.14.fix.1
Le workflow Tauri reste crate-local. En debug, les deux binaires recalent ensuite leur current working directory sur la racine du workspace via `CARGO_MANIFEST_DIR/../..`; la stratégie release/bundle reste un point explicite à fermer dans la candidate finale `pre.018`.
## Addendum `pre.015` — V2 binaire intercalé
Après validation du polish `pre.014-fix.001`, la release est volontairement étendue : `pre.015` wire/codec V2, `pre.016` APIs génériques/versionnées et V2 runtime, `pre.017` migration/régression, `pre.018` candidate finale. Le prompt `0.2.7` redevient WebSocket Solana standard.
## Addendum `pre.016` — runtime V2 et façade multi-version matérialisés
`pre.016` matérialise la politique figée en `pre.015` : `DEFAULT_WALLET_FORMAT = V2`, APIs génériques de création/import en V2, lecture/inspection V1/V2 auto-détectée, variantes `_v1`/`_v2` strictes et Wallet Desk consommant uniquement la façade non versionnée. Les handles OWNER/VIEW persistent leurs mutations dans le format natif ouvert ; aucune conversion implicite V1 -> V2 n'est effectuée.
La prochaine tranche est `pre.017` : migration explicite authentifiée V1 -> V2, canaris adversariaux/persistence et régression Wallet Desk. `pre.018` conserve la documentation/candidate finale et le build Tauri en dernière opération.