diff --git a/Cargo.toml b/Cargo.toml index 33161bf..5ef90cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 169 +# version: 170 [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.4.fix.1" +version = "0.2.6-pre.5" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/ROADMAP.md b/ROADMAP.md index 9cdbc2d..9fe8eb0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,5 @@ - + # 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 l’audit structurel Python complémentaire à rustfmt/Clippy. `Pubkey` reste via `ksp-core-lib`, la keypair reste encapsulée dans Wallet et Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet. -- [ ] `0.2.6` — Introduire `ksp-app-wallet-desk` utilisant Config composite + Wallet + transport HTTP. `pre.001` fixe le sizing et le gabarit ; `pre.002` matérialise la crate Tauri et son shell splash/main avec Config + Logging bootstrap, ports `1432/1433`, Bootstrap, Font Awesome, DataTables/Select, SimpleBar, resize-observer-polyfill, TS-RS et bridge frontend Logging. `pre.003` matérialise `cfg.std.wallet`/`schema.std.wallet`, le composite `cfg.composite.ksp-app-wallet-desk`, `ResolvedWalletConfig`, `wallets_directory` global + `wallets_subdirectory` par profil et la préparation automatique des répertoires côté application avec logs debug/error. `pre.003-fix.001` corrige les premiers canaris Config et active la trace des interactions frontend sans valeur de contrôle. `pre.003-fix.002` corrige le scanner `.env.example` sur les fragments d’identifiants Rust, généralise `std.logging` avec les profils console-only, `file_info`, `superdev` et `supertrace`, et sélectionne temporairement `supertrace` dans le composite Wallet Desk. `pre.003-fix.003` corrige les canaris de ce changement : closures explicites compatibles avec `clippy::implicit_return` et distinction entre fragment KSP incorporé dans un identifiant et nom d’environnement concret suffixé. `pre.003-fix.004` corrige les cinq closures restantes du canari de composition Wallet Desk afin que `cargo clippy --workspace --all-targets` respecte intégralement `clippy::implicit_return`, sans modifier Config ni Logging. `pre.004` branche ensuite l’inventory réel `.kspwallet` sur le répertoire effectif Config : enumeration non récursive, extension exacte, fichiers réguliers uniquement, rejet des symlinks, inspection locked via `ksp-wallet-lib`, diagnostics sûrs par ligne, DataTable réel, refresh et sélection root-scoped revalidée côté Rust sans Pubkey/alias/notes. `pre.004-fix.001` limite les deux réexports crate-root internes utilisés uniquement par les unit tests à `#[cfg(test)]`, afin de fermer les warnings `unused_imports` observés par `cargo check`, Clippy, tests et `tauri dev` sans modifier le comportement d’inventory. Les tranches suivantes ajoutent création/lifecycle VIEW/OWNER, secrets `KSP_SECRET_WALLET_PASS_*`, identité autorisée + `getBalance`, puis administration/import/export, avec une dernière tranche prévue pour README/USAGE/docs/validation, prompt `0.2.7` et build Tauri final. +- [ ] `0.2.6` — Introduire `ksp-app-wallet-desk` utilisant Config composite + Wallet + transport HTTP. `pre.001` fixe le sizing et le gabarit ; `pre.002` matérialise la crate Tauri et son shell splash/main avec Config + Logging bootstrap, ports `1432/1433`, Bootstrap, Font Awesome, DataTables/Select, SimpleBar, resize-observer-polyfill, TS-RS et bridge frontend Logging. `pre.003` matérialise `cfg.std.wallet`/`schema.std.wallet`, le composite `cfg.composite.ksp-app-wallet-desk`, `ResolvedWalletConfig`, `wallets_directory` global + `wallets_subdirectory` par profil et la préparation automatique des répertoires côté application avec logs debug/error. `pre.003-fix.001` corrige les premiers canaris Config et active la trace des interactions frontend sans valeur de contrôle. `pre.003-fix.002` corrige le scanner `.env.example` sur les fragments d’identifiants Rust, généralise `std.logging` avec les profils console-only, `file_info`, `superdev` et `supertrace`, et sélectionne temporairement `supertrace` dans le composite Wallet Desk. `pre.003-fix.003` corrige les canaris de ce changement : closures explicites compatibles avec `clippy::implicit_return` et distinction entre fragment KSP incorporé dans un identifiant et nom d’environnement concret suffixé. `pre.003-fix.004` corrige les cinq closures restantes du canari de composition Wallet Desk afin que `cargo clippy --workspace --all-targets` respecte intégralement `clippy::implicit_return`, sans modifier Config ni Logging. `pre.004` branche ensuite l’inventory réel `.kspwallet` sur le répertoire effectif Config : enumeration non récursive, extension exacte, fichiers réguliers uniquement, rejet des symlinks, inspection locked via `ksp-wallet-lib`, diagnostics sûrs par ligne, DataTable réel, refresh et sélection root-scoped revalidée côté Rust sans Pubkey/alias/notes. `pre.004-fix.001` limite les deux réexports crate-root internes utilisés uniquement par les unit tests à `#[cfg(test)]`, afin de fermer les warnings `unused_imports` observés par `cargo check`, Clippy, tests et `tauri dev` sans modifier le comportement d’inventory. `pre.005` ajoute la création native `create_wallet_file_v1`, le formulaire OWNER + VIEW optionnel et le premier `WalletSession` durable côté Rust : OWNER reste ouvert après création, sélection existante reste Locked, tandis que lock/deselect/refresh/changement de wallet détruisent le handle et purgent les projections protégées. Les tranches suivantes ajoutent unlock manuel/configuré, secrets `KSP_SECRET_WALLET_PASS_*`, identité autorisée + `getBalance`, puis administration/import/export, avec une dernière tranche prévue pour README/USAGE/docs/validation, prompt `0.2.7` et build Tauri final. - [ ] `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. diff --git a/crates/ksp-app-wallet-desk/frontend/main.html b/crates/ksp-app-wallet-desk/frontend/main.html index 16d472a..a4229bc 100644 --- a/crates/ksp-app-wallet-desk/frontend/main.html +++ b/crates/ksp-app-wallet-desk/frontend/main.html @@ -13,6 +13,9 @@ Wallet Desk — Dashboard Initialisation du shell… + @@ -39,7 +42,7 @@

Dashboard

Inventaire `.kspwallet` root-scoped et inspection locked actifs.

- 0.2.6-pre.004 + 0.2.6-pre.005
@@ -73,6 +76,9 @@
État
Format
VIEW
+
Pubkey
+
Alias
+
Notes
@@ -98,7 +104,60 @@
- + @@ -107,7 +166,7 @@ diff --git a/crates/ksp-app-wallet-desk/frontend/ts/main.ts b/crates/ksp-app-wallet-desk/frontend/ts/main.ts index 03aaeb5..7212f2a 100644 --- a/crates/ksp-app-wallet-desk/frontend/ts/main.ts +++ b/crates/ksp-app-wallet-desk/frontend/ts/main.ts @@ -1,5 +1,5 @@ // file: crates/ksp-app-wallet-desk/frontend/ts/main.ts -// version: 4 +// version: 5 import "bootstrap"; import DataTable from "datatables.net-bs5"; @@ -11,6 +11,9 @@ import type { RuntimeStatusDto } from "./bindings/ksp_app_wallet_desk/dto_common import type { LockedWalletDto } from "./bindings/ksp_app_wallet_desk/wallet_inventory/LockedWalletDto.ts"; import type { WalletInventoryEntryDto } from "./bindings/ksp_app_wallet_desk/wallet_inventory/WalletInventoryEntryDto.ts"; import type { WalletSelectionRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_inventory/WalletSelectionRequestDto.ts"; +import type { WalletAuthorizedDto } from "./bindings/ksp_app_wallet_desk/wallet_session/WalletAuthorizedDto.ts"; +import type { WalletCreateRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_session/WalletCreateRequestDto.ts"; +import type { WalletSessionDto } from "./bindings/ksp_app_wallet_desk/wallet_session/WalletSessionDto.ts"; import { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log"; import { invokeKsp } from "./invoke"; import "../sass/main.scss"; @@ -19,6 +22,7 @@ import "../sass/main.scss"; installFrontendConsoleBridge("main"); type ViewId = "dashboard" | "wallets" | "create-import" | "details" | "security" | "diagnostics"; +type ActiveSessionState = "no_selection" | "locked" | "owner_open"; const viewTitles: Record = { dashboard: "Dashboard", @@ -29,6 +33,9 @@ const viewTitles: Record = { diagnostics: "Diagnostics", }; +let activeWalletId: string | null = null; +let activeSessionState: ActiveSessionState = "no_selection"; + function isViewId(value: string): value is ViewId { return value in viewTitles; } @@ -114,7 +121,11 @@ function renderWalletInventoryRow(entry: WalletInventoryEntryDto): HTMLTableRowE row.dataset.inspectionStatus = entry.inspectionStatus; const state = document.createElement("td"); if (entry.state === "locked") { - appendIconText(state, "fa-lock", "Locked"); + if (activeWalletId === entry.walletId && activeSessionState === "owner_open") { + appendIconText(state, "fa-lock-open", "OWNER"); + } else { + appendIconText(state, "fa-lock", "Locked"); + } } else { appendIconText(state, "fa-triangle-exclamation", "Erreur"); } @@ -154,7 +165,7 @@ function initializeWalletTable(): void { zeroRecords: "Aucun wallet correspondant.", }, }); - frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.004-wallet-inventory" }); + frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.005-wallet-session-create" }); } function renderWalletInventory(entries: WalletInventoryEntryDto[]): void { @@ -170,58 +181,105 @@ function renderWalletInventory(entries: WalletInventoryEntryDto[]): void { frontendDebug("main", "Wallet inventory rendered", { entryCount: entries.length, invalidCount }); } +function setText(selector: string, value: string): void { + const element = document.querySelector(selector); + if (element) { + element.textContent = value; + } +} + +function updateSessionActions(): void { + const lock = document.querySelector('[data-shell-action="lock"]'); + const deselect = document.querySelector('[data-shell-action="deselect"]'); + if (lock) { + lock.disabled = activeSessionState !== "owner_open"; + } + if (deselect) { + deselect.disabled = activeSessionState === "no_selection"; + } +} + +function clearCreateFormSensitiveInputs(): void { + const ownerPassword = document.querySelector("#createWalletOwnerPassword"); + const viewPassword = document.querySelector("#createWalletViewPassword"); + const alias = document.querySelector("#createWalletAlias"); + const note = document.querySelector("#createWalletInitialNote"); + if (ownerPassword) { + ownerPassword.value = ""; + } + if (viewPassword) { + viewPassword.value = ""; + } + if (alias) { + alias.value = ""; + } + if (note) { + note.value = ""; + } +} + +function clearAuthorizedProjection(): void { + setText("#currentWalletPubkey", "—"); + setText("#currentWalletAlias", "—"); + setText("#currentWalletNotes", "—"); + clearCreateFormSensitiveInputs(); +} + function clearSelectedWallet(): void { + activeWalletId = null; + activeSessionState = "no_selection"; const icon = document.querySelector("#currentWalletIcon"); - const filename = document.querySelector("#currentWalletFilename"); - const state = document.querySelector("#currentWalletState"); - const format = document.querySelector("#currentWalletFormat"); - const view = document.querySelector("#currentWalletView"); if (icon) { icon.className = "fa-solid fa-lock fa-2x mb-3 text-body-secondary"; } - if (filename) { - filename.textContent = "Aucun wallet sélectionné."; - } - if (state) { - state.textContent = "—"; - } - if (format) { - format.textContent = "—"; - } - if (view) { - view.textContent = "—"; - } + setText("#currentWalletFilename", "Aucun wallet sélectionné."); + setText("#currentWalletState", "—"); + setText("#currentWalletFormat", "—"); + setText("#currentWalletView", "—"); + clearAuthorizedProjection(); + updateSessionActions(); } -function renderSelectedWallet(wallet: LockedWalletDto): void { +function renderLockedWallet(wallet: LockedWalletDto): void { + activeWalletId = wallet.walletId; + activeSessionState = "locked"; + clearAuthorizedProjection(); const icon = document.querySelector("#currentWalletIcon"); - const filename = document.querySelector("#currentWalletFilename"); - const state = document.querySelector("#currentWalletState"); - const format = document.querySelector("#currentWalletFormat"); - const view = document.querySelector("#currentWalletView"); if (icon) { icon.className = "fa-solid fa-lock fa-2x mb-3"; } - if (filename) { - filename.textContent = wallet.filename; - } - if (state) { - state.textContent = "Locked"; - } - if (format) { - format.textContent = wallet.formatVersion.toString(); - } - if (view) { - view.textContent = wallet.viewEnabled ? "enabled" : "disabled"; - } + setText("#currentWalletFilename", wallet.filename); + setText("#currentWalletState", "Locked"); + setText("#currentWalletFormat", wallet.formatVersion.toString()); + setText("#currentWalletView", wallet.viewEnabled ? "enabled" : "disabled"); + updateSessionActions(); frontendDebug("main", "Locked Wallet selection rendered", { walletId: wallet.walletId, formatVersion: wallet.formatVersion, viewEnabled: wallet.viewEnabled }); } +function renderAuthorizedWallet(wallet: WalletAuthorizedDto): void { + activeWalletId = wallet.walletId; + activeSessionState = "owner_open"; + const icon = document.querySelector("#currentWalletIcon"); + if (icon) { + icon.className = "fa-solid fa-lock-open fa-2x mb-3 text-success"; + } + setText("#currentWalletFilename", wallet.filename); + setText("#currentWalletState", "OWNER open"); + setText("#currentWalletFormat", wallet.formatVersion.toString()); + setText("#currentWalletView", wallet.viewEnabled ? "enabled" : "disabled"); + setText("#currentWalletPubkey", wallet.pubkey); + setText("#currentWalletAlias", wallet.alias ?? "—"); + setText("#currentWalletNotes", wallet.notes.length === 0 ? "—" : wallet.notes.map(note => note.text).join(" · ")); + updateSessionActions(); + frontendDebug("main", "OWNER Wallet session rendered after creation", { walletId: wallet.walletId, viewEnabled: wallet.viewEnabled, noteCount: wallet.notes.length }); +} + async function selectWallet(walletId: string): Promise { + clearSelectedWallet(); const request: WalletSelectionRequestDto = { walletId }; try { const wallet = await invokeKsp("main", "select_wallet", { request }); - renderSelectedWallet(wallet); + renderLockedWallet(wallet); } catch { clearSelectedWallet(); frontendWarn("main", "Locked Wallet selection failed", { walletId }); @@ -256,62 +314,124 @@ function bindWalletTableSelection(): void { frontendTrace("main", "Wallet inventory row selection handler installed"); } -async function loadWalletInventory(command: "list_wallets" | "refresh_wallets"): Promise { +async function loadWalletInventory(command: "list_wallets" | "refresh_wallets", clearSession: boolean): Promise { frontendDebug("main", "Wallet inventory load requested", { command }); const entries = await invokeKsp("main", command); - clearSelectedWallet(); + if (clearSession) { + clearSelectedWallet(); + } renderWalletInventory(entries); } +function createRequestFromForm(): WalletCreateRequestDto | null { + const filename = document.querySelector("#createWalletFilename"); + const ownerPassword = document.querySelector("#createWalletOwnerPassword"); + const enableView = document.querySelector("#createWalletEnableView"); + const viewPassword = document.querySelector("#createWalletViewPassword"); + const alias = document.querySelector("#createWalletAlias"); + const note = document.querySelector("#createWalletInitialNote"); + if (!filename || !ownerPassword || !enableView || !viewPassword || !alias || !note) { + return null; + } + const normalizedFilename = filename.value.trim(); + if (!normalizedFilename.endsWith(".kspwallet") || normalizedFilename.length <= ".kspwallet".length || ownerPassword.value.length === 0) { + return null; + } + if (enableView.checked && viewPassword.value.length === 0) { + return null; + } + return { + alias: alias.value.length === 0 ? null : alias.value, + filename: normalizedFilename, + initialNote: note.value.length === 0 ? null : note.value, + ownerPassword: ownerPassword.value, + viewPassword: enableView.checked ? viewPassword.value : null, + }; +} + +function bindCreateWalletForm(): void { + const form = document.querySelector("#createWalletForm"); + const enableView = document.querySelector("#createWalletEnableView"); + const viewPassword = document.querySelector("#createWalletViewPassword"); + if (enableView && viewPassword) { + enableView.addEventListener("change", () => { + viewPassword.disabled = !enableView.checked; + if (!enableView.checked) { + viewPassword.value = ""; + } + frontendTrace("main", "Create Wallet VIEW option changed", { enabled: enableView.checked }); + }); + } + if (!form) { + return; + } + form.addEventListener("submit", event => { + event.preventDefault(); + const request = createRequestFromForm(); + if (!request) { + setText("#createWalletStatus", "Filename/passwords invalides ou incomplets."); + frontendWarn("main", "Create Wallet form validation rejected request"); + return; + } + const requestedWalletId = request.filename; + const requestedViewEnabled = request.viewPassword !== null; + setText("#createWalletStatus", "Création en cours…"); + frontendDebug("main", "Create Wallet request submitted", { walletId: requestedWalletId, viewEnabled: requestedViewEnabled }); + clearSelectedWallet(); + void invokeKsp("main", "create_wallet", { request }) + .then(async wallet => { + clearCreateFormSensitiveInputs(); + renderAuthorizedWallet(wallet); + setText("#createWalletStatus", "Wallet créé ; session OWNER ouverte."); + await loadWalletInventory("list_wallets", false); + activateView("dashboard", "user"); + }) + .catch(() => { + clearCreateFormSensitiveInputs(); + setText("#createWalletStatus", "Création impossible. Consulter le diagnostic et les logs."); + frontendWarn("main", "Create Wallet operation failed", { walletId: requestedWalletId }); + }); + }); + frontendTrace("main", "Create Wallet form handlers installed"); +} + +async function lockCurrentWallet(): Promise { + clearAuthorizedProjection(); + try { + const wallet = await invokeKsp("main", "lock_wallet"); + renderLockedWallet(wallet); + await loadWalletInventory("list_wallets", false); + } catch { + clearSelectedWallet(); + frontendWarn("main", "Wallet lock operation failed"); + } +} + +async function deselectCurrentWallet(): Promise { + try { + const session = await invokeKsp("main", "deselect_wallet"); + clearSelectedWallet(); + frontendDebug("main", "Wallet session deselected", { state: session.state }); + await loadWalletInventory("list_wallets", false); + } catch { + clearSelectedWallet(); + frontendWarn("main", "Wallet deselect operation failed"); + } +} + function renderRuntimeStatus(status: RuntimeStatusDto): void { - const version = document.querySelector("#runtimeVersion"); - const compositeProfile = document.querySelector("#runtimeCompositeProfile"); - const loggingProfile = document.querySelector("#runtimeLoggingProfile"); - const walletProfile = document.querySelector("#runtimeWalletProfile"); - const fallback = document.querySelector("#runtimeLoggingFallback"); - const documents = document.querySelector("#runtimeConfigDocuments"); - const walletsRoot = document.querySelector("#runtimeWalletsDirectory"); - const walletsSubdirectory = document.querySelector("#runtimeWalletsSubdirectory"); - const effectiveWalletsDirectory = document.querySelector("#runtimeEffectiveWalletsDirectory"); - const walletDirectoryCreated = document.querySelector("#runtimeWalletDirectoryCreated"); - const phase = document.querySelector("#runtimeShellPhase"); - const shellStatus = document.querySelector("#shellStatus"); - if (version) { - version.textContent = status.applicationVersion; - } - if (compositeProfile) { - compositeProfile.textContent = status.activeCompositeProfile; - } - if (loggingProfile) { - loggingProfile.textContent = status.activeLoggingProfile ?? "fallback transitoire"; - } - if (walletProfile) { - walletProfile.textContent = status.activeWalletProfile; - } - if (fallback) { - fallback.textContent = status.fallbackLoggingActive ? "oui" : "non"; - } - if (documents) { - documents.textContent = status.configDocumentCount.toString(); - } - if (walletsRoot) { - walletsRoot.textContent = status.walletsDirectory; - } - if (walletsSubdirectory) { - walletsSubdirectory.textContent = status.walletsSubdirectory ?? "— (racine globale)"; - } - if (effectiveWalletsDirectory) { - effectiveWalletsDirectory.textContent = status.effectiveWalletsDirectory; - } - if (walletDirectoryCreated) { - walletDirectoryCreated.textContent = status.effectiveWalletsDirectoryCreatedOnStartup ? "oui" : "non, déjà présent"; - } - if (phase) { - phase.textContent = status.shellPhase; - } - if (shellStatus) { - shellStatus.textContent = "Config résolue ; inventaire Wallet prêt."; - } + setText("#runtimeVersion", status.applicationVersion); + setText("#runtimeCompositeProfile", status.activeCompositeProfile); + setText("#runtimeLoggingProfile", status.activeLoggingProfile ?? "fallback transitoire"); + setText("#runtimeWalletProfile", status.activeWalletProfile); + setText("#runtimeLoggingFallback", status.fallbackLoggingActive ? "oui" : "non"); + setText("#runtimeConfigDocuments", status.configDocumentCount.toString()); + setText("#runtimeWalletsDirectory", status.walletsDirectory); + setText("#runtimeWalletsSubdirectory", status.walletsSubdirectory ?? "— (racine globale)"); + setText("#runtimeEffectiveWalletsDirectory", status.effectiveWalletsDirectory); + setText("#runtimeWalletDirectoryCreated", status.effectiveWalletsDirectoryCreatedOnStartup ? "oui" : "non, déjà présent"); + setText("#runtimeShellPhase", status.shellPhase); + setText("#shellStatus", "Config résolue ; création et session Wallet prêtes."); frontendTrace("main", "Wallet Desk runtime status rendered", { compositeProfile: status.activeCompositeProfile, fallbackLoggingActive: status.fallbackLoggingActive, @@ -331,10 +451,19 @@ function bindShellActions(): void { button.addEventListener("click", () => { const action = button.dataset.shellAction ?? "unknown"; frontendDebug("main", "Wallet Desk shell action clicked", { action, enabled: !button.disabled }); - if (action === "refresh-wallets" && !button.disabled) { - void loadWalletInventory("refresh_wallets").catch(() => { + if (button.disabled) { + return; + } + if (action === "refresh-wallets") { + clearSelectedWallet(); + void loadWalletInventory("refresh_wallets", true).catch(() => { + clearSelectedWallet(); frontendWarn("main", "Wallet inventory refresh failed"); }); + } else if (action === "lock") { + void lockCurrentWallet(); + } else if (action === "deselect") { + void deselectCurrentWallet(); } }); }); @@ -347,17 +476,16 @@ async function initializeMain(): Promise { bindFrontendInteractions(); bindNavigation(); bindWalletTableSelection(); + bindCreateWalletForm(); bindShellActions(); + clearSelectedWallet(); activateView("dashboard", "startup"); try { await loadRuntimeStatus(); - await loadWalletInventory("list_wallets"); + await loadWalletInventory("list_wallets", false); } catch { renderWalletInventory([]); - const shellStatus = document.querySelector("#shellStatus"); - if (shellStatus) { - shellStatus.textContent = "Le statut runtime ou l'inventaire Wallet n'a pas pu être chargé."; - } + setText("#shellStatus", "Le statut runtime ou l'inventaire Wallet n'a pas pu être chargé."); frontendWarn("main", "Wallet Desk startup data load failed"); } } diff --git a/crates/ksp-app-wallet-desk/package.json b/crates/ksp-app-wallet-desk/package.json index f205d72..d230c57 100644 --- a/crates/ksp-app-wallet-desk/package.json +++ b/crates/ksp-app-wallet-desk/package.json @@ -1,7 +1,7 @@ { "name": "ksp-app-wallet-desk", "private": true, - "version": "0.2.6-pre.4.fix.1", + "version": "0.2.6-pre.5", "type": "module", "scripts": { "dev": "vite", diff --git a/crates/ksp-app-wallet-desk/src/app_state.rs b/crates/ksp-app-wallet-desk/src/app_state.rs index 413beab..4d14295 100644 --- a/crates/ksp-app-wallet-desk/src/app_state.rs +++ b/crates/ksp-app-wallet-desk/src/app_state.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-wallet-desk/src/app_state.rs -// version: 4 +// version: 5 //! Shared backend state owned by the Wallet Desk Tauri application. @@ -10,6 +10,7 @@ pub(crate) struct AppState { splash_settings: crate::SplashSettings, splash_sequence_started: std::sync::atomic::AtomicBool, wallet_config_startup: crate::WalletConfigStartup, + wallet_session: std::sync::Mutex, } impl AppState { @@ -58,6 +59,7 @@ impl AppState { splash_settings, splash_sequence_started: std::sync::atomic::AtomicBool::new(false), wallet_config_startup, + wallet_session: std::sync::Mutex::new(crate::WalletSession::no_selection()), }); } @@ -100,7 +102,7 @@ impl AppState { effective_wallets_directory_created_on_startup: self.wallet_config_startup.effective_directory_created_on_startup(), fallback_logging_active: runtime.fallback_active, root_wallets_directory_created_on_startup: self.wallet_config_startup.root_directory_created_on_startup(), - shell_phase: "pre.004-wallet-inventory".to_owned(), + shell_phase: "pre.005-wallet-session-create".to_owned(), startup_diagnostic: runtime.startup_diagnostic.clone(), wallets_directory: resolved.wallets_directory().to_string_lossy().into_owned(), wallets_subdirectory, @@ -113,6 +115,123 @@ impl AppState { return self.wallet_config_startup.resolved().effective_wallets_directory(); } + /// Selects one locked Wallet and installs it as the durable backend session. + pub(crate) async fn select_wallet(&self, request: crate::WalletSelectionRequestDto) -> ksp_core_lib::Result { + let deselected = self.deselect_wallet(); + if let std::result::Result::Err(error) = deselected { + return std::result::Result::Err(error); + } + let root = self.wallet_inventory_root().to_path_buf(); + let resolved = crate::resolve_locked_wallet(root.as_path(), request.wallet_id).await; + let (path, locked_info, dto) = match resolved { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let session = self.wallet_session.lock(); + let mut session = match session { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), + }; + *session = crate::WalletSession::Locked { wallet_id: dto.wallet_id.clone(), path, locked_info }; + return std::result::Result::Ok(dto); + } + + /// Drops any selected or authorized Wallet state and returns to `NoSelection`. + pub(crate) fn deselect_wallet(&self) -> ksp_core_lib::Result { + let previous = { + let session = self.wallet_session.lock(); + let mut session = match session { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), + }; + std::mem::replace(&mut *session, crate::WalletSession::no_selection()) + }; + drop(previous); + ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, "Wallet session deselected and protected handle state purged"); + return std::result::Result::Ok(crate::WalletSession::no_selection().safe_projection()); + } + + /// Creates a new Wallet under the effective root and retains the returned OWNER handle only in Rust. + pub(crate) async fn create_wallet(&self, request: crate::WalletCreateRequestDto) -> ksp_core_lib::Result { + let deselected = self.deselect_wallet(); + if let std::result::Result::Err(error) = deselected { + return std::result::Result::Err(error); + } + let crate::WalletCreateRequestDto { alias, filename, initial_note, owner_password, view_password } = request; + let root = self.wallet_inventory_root().to_path_buf(); + let destination = crate::new_wallet_destination(root.as_path(), filename.as_str()); + let destination = match destination { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let view_enabled = view_password.is_some(); + let owner_password = ksp_wallet_lib::OwnerPassword::new(owner_password); + let view_password = view_password.map(ksp_wallet_lib::ViewPassword::new); + let alias = normalize_optional_text(alias); + let note = normalize_optional_text(initial_note); + let note_texts = note.into_iter().collect::>(); + 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 owner = match created { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = filename.as_str(), error_domain = error.code().domain(), error_code = error.code().code(), "Wallet creation failed"); + return std::result::Result::Err(error); + }, + }; + let dto = crate::owner_projection(filename.as_str(), view_enabled, &owner); + let session = self.wallet_session.lock(); + let mut session = match session { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), + }; + *session = crate::WalletSession::Owner { wallet_id: filename.clone(), path: destination, wallet: owner }; + ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = filename.as_str(), view_enabled, "Wallet created and OWNER session opened"); + return std::result::Result::Ok(dto); + } + + /// Drops an authorized handle, re-inspects the selected file and returns the session to `Locked`. + pub(crate) async fn lock_wallet(&self) -> ksp_core_lib::Result { + let previous = { + let session = self.wallet_session.lock(); + let mut session = match session { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), + }; + std::mem::replace(&mut *session, crate::WalletSession::no_selection()) + }; + let (wallet_id, path) = match previous { + crate::WalletSession::NoSelection => { + return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_SESSION_INVALID, "No Wallet is selected to lock")); + }, + crate::WalletSession::Locked { wallet_id, path, .. } => (wallet_id, path), + crate::WalletSession::Owner { wallet_id, path, wallet } => { + drop(wallet); + (wallet_id, path) + }, + }; + let locked = ksp_wallet_lib::inspect_locked_wallet_file_v1(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), + }; + let dto = crate::LockedWalletDto { + filename: wallet_id.clone(), + format_version: locked.format_version(), + wallet_id: wallet_id.clone(), + view_enabled: locked.view_enabled(), + }; + let session = self.wallet_session.lock(); + let mut session = match session { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()), + }; + *session = crate::WalletSession::Locked { wallet_id: wallet_id.clone(), path, locked_info: locked }; + ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), "Wallet session locked and authorized handle state purged"); + return std::result::Result::Ok(dto); + } + /// Returns the resolved common splash timings captured during bootstrap. #[must_use] pub(crate) const fn splash_settings(&self) -> crate::SplashSettings { @@ -128,6 +247,19 @@ impl AppState { } } +fn normalize_optional_text(value: std::option::Option) -> std::option::Option { + return value.and_then(|text| { + if text.trim().is_empty() { + return std::option::Option::None; + } + return std::option::Option::Some(text); + }); +} + +fn session_lock_error() -> ksp_core_lib::Error { + return ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_LOCK_FAILED, "Wallet Desk Wallet session state lock is poisoned"); +} + struct LoggingRuntimeState { guard: ksp_logging_lib::LoggingGuard, active_profile_id: std::option::Option, diff --git a/crates/ksp-app-wallet-desk/src/constants.rs b/crates/ksp-app-wallet-desk/src/constants.rs index 04eb8d7..9fe31ea 100644 --- a/crates/ksp-app-wallet-desk/src/constants.rs +++ b/crates/ksp-app-wallet-desk/src/constants.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-wallet-desk/src/constants.rs -// version: 3 +// version: 4 //! Logging targets, domains and composite component identifiers owned by Wallet Desk. @@ -19,6 +19,8 @@ pub(crate) const TRACING_DOMAIN_SHELL: &str = "wallet.shell"; pub(crate) const TRACING_DOMAIN_WALLET_CONFIG: &str = "wallet.config"; /// Structured domain used while enumerating and inspecting locked Wallet files. pub(crate) const TRACING_DOMAIN_WALLET_INVENTORY: &str = "wallet.inventory"; +/// Structured domain used by durable Wallet session and creation operations. +pub(crate) const TRACING_DOMAIN_WALLET_SESSION: &str = "wallet.session"; /// Structured domain used by Tauri window lifecycle operations. pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window"; /// Owning target for backend events emitted by Wallet Desk. diff --git a/crates/ksp-app-wallet-desk/src/errors.rs b/crates/ksp-app-wallet-desk/src/errors.rs index 74ea019..806359d 100644 --- a/crates/ksp-app-wallet-desk/src/errors.rs +++ b/crates/ksp-app-wallet-desk/src/errors.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-wallet-desk/src/errors.rs -// version: 3 +// version: 4 //! Application-local error codes for Wallet Desk composition and desktop runtime surfaces. @@ -35,3 +35,5 @@ pub(crate) const ERROR_CODE_WALLET_DIRECTORY_PREPARE_FAILED: ksp_core_lib::Error pub(crate) const ERROR_CODE_WALLET_INVENTORY_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_inventory_failed"); /// A requested Wallet inventory identifier is unsafe or does not resolve to an eligible regular `.kspwallet` file. pub(crate) const ERROR_CODE_WALLET_SELECTION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_selection_invalid"); +/// A Wallet session lifecycle operation is incompatible with the current state. +pub(crate) const ERROR_CODE_WALLET_SESSION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_session_invalid"); diff --git a/crates/ksp-app-wallet-desk/src/lib.rs b/crates/ksp-app-wallet-desk/src/lib.rs index 89e534b..8015a88 100644 --- a/crates/ksp-app-wallet-desk/src/lib.rs +++ b/crates/ksp-app-wallet-desk/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-wallet-desk/src/lib.rs -// version: 4 +// version: 5 //! Tauri desktop application shell for KSP Wallet management and inspection. @@ -20,6 +20,7 @@ mod tw_main; mod tw_splash; mod wallet_config; mod wallet_inventory; +mod wallet_session; /// Runs the KSP wallet desktop application. pub use self::tauri::run; @@ -52,6 +53,8 @@ pub(crate) use self::constants::TRACING_DOMAIN_SHELL; pub(crate) use self::constants::TRACING_DOMAIN_WALLET_CONFIG; /// Structured domain used while enumerating and inspecting locked Wallet files. pub(crate) use self::constants::TRACING_DOMAIN_WALLET_INVENTORY; +/// Structured domain used by durable Wallet session and creation operations. +pub(crate) use self::constants::TRACING_DOMAIN_WALLET_SESSION; /// Structured domain used by Tauri window lifecycle operations. pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS; /// Owning target for backend events emitted by Wallet Desk. @@ -98,6 +101,8 @@ pub(crate) use self::errors::ERROR_CODE_WALLET_DIRECTORY_PREPARE_FAILED; pub(crate) use self::errors::ERROR_CODE_WALLET_INVENTORY_FAILED; /// Wallet selection identifier is invalid or no longer eligible. pub(crate) use self::errors::ERROR_CODE_WALLET_SELECTION_INVALID; +/// Wallet session lifecycle operation is incompatible with the current state. +pub(crate) use self::errors::ERROR_CODE_WALLET_SESSION_INVALID; /// Log payload sent by Wallet Desk frontend scripts. pub(crate) use self::frontend_logging::FrontendLogPayloadDto; /// Emits one validated frontend event through the KSP Logging facade. @@ -134,5 +139,23 @@ pub(crate) use self::wallet_inventory::WalletInventoryStateDto; pub(crate) use self::wallet_inventory::WalletSelectionRequestDto; /// Enumerates native Wallet files under the effective Config-managed root. pub(crate) use self::wallet_inventory::list_wallet_inventory; -/// Re-inspects one root-scoped Wallet selection. +/// Builds a validated destination path for one new root-scoped Wallet. +pub(crate) use self::wallet_inventory::new_wallet_destination; +/// Re-resolves one locked Wallet into Rust-only path state plus safe DTO. +pub(crate) use self::wallet_inventory::resolve_locked_wallet; +/// Re-inspects one root-scoped Wallet selection in crate unit tests. +#[cfg(test)] pub(crate) use self::wallet_inventory::select_locked_wallet; +/// Authorized OWNER projection exposed only after successful creation. +pub(crate) use self::wallet_session::WalletAuthorizedDto; +/// Create request whose password strings move frontend -> Rust only. +pub(crate) use self::wallet_session::WalletCreateRequestDto; +/// Durable backend Wallet session retaining full paths and handles only in Rust. +pub(crate) use self::wallet_session::WalletSession; +/// Minimal safe projection of the durable Wallet session. +pub(crate) use self::wallet_session::WalletSessionDto; +/// Safe state code for the durable Wallet session in crate unit tests. +#[cfg(test)] +pub(crate) use self::wallet_session::WalletSessionStateDto; +/// Builds an authorized OWNER DTO from a Rust-only Wallet handle. +pub(crate) use self::wallet_session::owner_projection; diff --git a/crates/ksp-app-wallet-desk/src/tauri.rs b/crates/ksp-app-wallet-desk/src/tauri.rs index f73816c..36fb739 100644 --- a/crates/ksp-app-wallet-desk/src/tauri.rs +++ b/crates/ksp-app-wallet-desk/src/tauri.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-wallet-desk/src/tauri.rs -// version: 2 +// version: 3 //! Tauri runtime assembly for the KSP wallet desktop application. @@ -38,12 +38,15 @@ fn configure_plugins(builder: tauri::Builder) -> tauri::Builder) -> tauri::Builder { return builder.invoke_handler(tauri::generate_handler![ + create_wallet, + deselect_wallet, emit_frontend_log, get_runtime_status, list_wallets, + lock_wallet, refresh_wallets, select_wallet, - splash_frontend_ready + splash_frontend_ready, ]); } @@ -62,6 +65,27 @@ fn configure_setup(builder: tauri::Builder) -> tauri::Builder, +) -> std::result::Result { + let result = state.create_wallet(request).await; + return match result { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), + }; +} + +#[tauri::command] +fn deselect_wallet(state: tauri::State<'_, crate::AppState>) -> std::result::Result { + let result = state.deselect_wallet(); + return match result { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), + }; +} + #[tauri::command] fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> { let result = crate::emit_frontend_log_event(payload); @@ -90,10 +114,23 @@ async fn list_wallets(state: tauri::State<'_, crate::AppState>) -> std::result:: }; } +#[tauri::command] +async fn lock_wallet(state: tauri::State<'_, crate::AppState>) -> std::result::Result { + let result = state.lock_wallet().await; + return match result { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), + }; +} + #[tauri::command] async fn refresh_wallets( state: tauri::State<'_, crate::AppState>, ) -> std::result::Result, crate::CommandErrorDto> { + let deselected = state.deselect_wallet(); + if let std::result::Result::Err(error) = deselected { + return std::result::Result::Err(crate::CommandErrorDto::from_error(&error)); + } let root = state.wallet_inventory_root().to_path_buf(); let result = crate::list_wallet_inventory(root.as_path()).await; return match result { @@ -107,8 +144,7 @@ async fn select_wallet( request: crate::WalletSelectionRequestDto, state: tauri::State<'_, crate::AppState>, ) -> std::result::Result { - let root = state.wallet_inventory_root().to_path_buf(); - let result = crate::select_locked_wallet(root.as_path(), request).await; + let result = state.select_wallet(request).await; return match result { std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), diff --git a/crates/ksp-app-wallet-desk/src/wallet_inventory.rs b/crates/ksp-app-wallet-desk/src/wallet_inventory.rs index a5057b3..8d20972 100644 --- a/crates/ksp-app-wallet-desk/src/wallet_inventory.rs +++ b/crates/ksp-app-wallet-desk/src/wallet_inventory.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-wallet-desk/src/wallet_inventory.rs -// version: 1 +// version: 2 //! Root-scoped Wallet inventory and locked-file selection for Wallet Desk. @@ -134,8 +134,21 @@ pub(crate) async fn list_wallet_inventory(root: &std::path::Path) -> ksp_core_li } /// Re-resolves and re-inspects one selected inventory identifier without exposing its full path. +#[cfg(test)] pub(crate) async fn select_locked_wallet(root: &std::path::Path, request: WalletSelectionRequestDto) -> ksp_core_lib::Result { - let path = resolve_wallet_path(root, request.wallet_id.as_str()).await; + let resolved = resolve_locked_wallet(root, request.wallet_id).await; + return match resolved { + std::result::Result::Ok((_, _, dto)) => std::result::Result::Ok(dto), + std::result::Result::Err(error) => std::result::Result::Err(error), + }; +} + +/// Resolves one locked Wallet into its Rust-only path, locked info and safe DTO. +pub(crate) async fn resolve_locked_wallet( + root: &std::path::Path, + wallet_id: String, +) -> ksp_core_lib::Result<(std::path::PathBuf, ksp_wallet_lib::LockedWalletInfo, LockedWalletDto)> { + let path = resolve_wallet_path(root, wallet_id.as_str()).await; let path = match path { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -145,13 +158,22 @@ pub(crate) async fn select_locked_wallet(root: &std::path::Path, request: Wallet std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_INVENTORY, wallet_id = request.wallet_id.as_str(), format_version = locked.format_version(), view_enabled = locked.view_enabled(), "Locked Wallet selected from inventory"); - return std::result::Result::Ok(LockedWalletDto { - filename: request.wallet_id.clone(), + let dto = LockedWalletDto { + filename: wallet_id.clone(), format_version: locked.format_version(), - wallet_id: request.wallet_id, + wallet_id: wallet_id.clone(), view_enabled: locked.view_enabled(), - }); + }; + ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_INVENTORY, wallet_id = wallet_id.as_str(), format_version = locked.format_version(), view_enabled = locked.view_enabled(), "Locked Wallet selected from inventory"); + return std::result::Result::Ok((path, locked, dto)); +} + +/// Validates a root-scoped destination filename for a new native Wallet. +pub(crate) fn new_wallet_destination(root: &std::path::Path, filename: &str) -> ksp_core_lib::Result { + if !is_wallet_filename(filename) { + return selection_invalid(filename, "Wallet destination must be one UTF-8 filename ending in .kspwallet"); + } + return std::result::Result::Ok(root.join(filename)); } fn valid_inventory_entry(filename: String, locked: ksp_wallet_lib::LockedWalletInfo) -> WalletInventoryEntryDto { diff --git a/crates/ksp-app-wallet-desk/src/wallet_session.rs b/crates/ksp-app-wallet-desk/src/wallet_session.rs new file mode 100644 index 0000000..79dbc68 --- /dev/null +++ b/crates/ksp-app-wallet-desk/src/wallet_session.rs @@ -0,0 +1,166 @@ +// file: crates/ksp-app-wallet-desk/src/wallet_session.rs +// version: 1 + +//! Durable root-scoped Wallet session lifecycle for Wallet Desk. + +use ts_rs::TS; // rust-rules: trait-import + +/// Safe state code projected for the current Wallet session. +#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_session/WalletSessionStateDto.ts")] +pub(crate) enum WalletSessionStateDto { + /// No Wallet is selected. + NoSelection, + /// One Wallet is selected but remains locked. + Locked, + /// One Wallet is open with OWNER capability. + OwnerOpen, +} + +/// Minimal non-secret projection of the backend Wallet session. +#[derive(Clone, Debug, serde::Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_session/WalletSessionDto.ts")] +pub(crate) struct WalletSessionDto { + /// Filename when a Wallet is selected. + pub(crate) filename: std::option::Option, + /// Current session state. + pub(crate) state: WalletSessionStateDto, + /// Root-scoped identifier when a Wallet is selected. + pub(crate) wallet_id: std::option::Option, +} + +/// One protected Wallet note exposed only from an authorized session. +#[derive(serde::Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_session/WalletNoteDto.ts")] +pub(crate) struct WalletNoteDto { + /// Stable note identifier generated by Wallet. + pub(crate) id: String, + /// Protected note text available only after authorization. + pub(crate) text: String, +} + +/// Authorized Wallet projection returned after creation with OWNER capability. +#[derive(serde::Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_session/WalletAuthorizedDto.ts")] +pub(crate) struct WalletAuthorizedDto { + /// Protected internal alias when configured. + pub(crate) alias: std::option::Option, + /// Capability that produced this projection. + pub(crate) capability: String, + /// Native Wallet filename without its parent path. + pub(crate) filename: String, + /// Native Wallet format version. + pub(crate) format_version: u32, + /// Protected notes available to the OWNER session. + pub(crate) notes: std::vec::Vec, + /// Authorized Solana public key. + pub(crate) pubkey: String, + /// Root-scoped identifier accepted by later Wallet Desk operations. + pub(crate) wallet_id: String, + /// Whether the created Wallet has an enabled VIEW slot. + pub(crate) view_enabled: bool, +} + +/// Create request moved from the frontend directly into Wallet password wrappers. +#[derive(serde::Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_session/WalletCreateRequestDto.ts")] +pub(crate) struct WalletCreateRequestDto { + /// Optional protected internal alias stored inside the Wallet. + pub(crate) alias: std::option::Option, + /// Root-scoped destination filename. + pub(crate) filename: String, + /// Optional first protected note stored inside the Wallet. + pub(crate) initial_note: std::option::Option, + /// OWNER password transported only frontend -> Rust. + pub(crate) owner_password: String, + /// Optional VIEW password; `None` keeps VIEW disabled. + pub(crate) view_password: std::option::Option, +} + +/// Backend Wallet session. Full paths and authorized handles never cross IPC. +pub(crate) enum WalletSession { + /// No current Wallet selection. + NoSelection, + /// One re-inspected locked Wallet. + Locked { + /// Native Wallet filename / root-scoped identifier. + wallet_id: String, + /// Full root-scoped filesystem path retained only in Rust. + path: std::path::PathBuf, + /// Locked information re-inspected from the file. + locked_info: ksp_wallet_lib::LockedWalletInfo, + }, + /// One Wallet kept open with OWNER capability. + Owner { + /// Native Wallet filename / root-scoped identifier. + wallet_id: String, + /// Full root-scoped filesystem path retained only in Rust. + path: std::path::PathBuf, + /// Authorized OWNER handle retaining secret material only in Rust. + wallet: ksp_wallet_lib::WalletOwner, + }, +} + +impl WalletSession { + /// Creates an empty application session. + #[must_use] + pub(crate) const fn no_selection() -> Self { + return Self::NoSelection; + } + + /// Returns a minimal safe session projection. + #[must_use] + pub(crate) fn safe_projection(&self) -> WalletSessionDto { + return match self { + Self::NoSelection => WalletSessionDto { + filename: std::option::Option::None, + state: WalletSessionStateDto::NoSelection, + wallet_id: std::option::Option::None, + }, + Self::Locked { wallet_id, locked_info, .. } => { + let _ = locked_info.format_version(); + WalletSessionDto { + filename: std::option::Option::Some(wallet_id.clone()), + state: WalletSessionStateDto::Locked, + wallet_id: std::option::Option::Some(wallet_id.clone()), + } + }, + Self::Owner { wallet_id, .. } => WalletSessionDto { + filename: std::option::Option::Some(wallet_id.clone()), + state: WalletSessionStateDto::OwnerOpen, + wallet_id: std::option::Option::Some(wallet_id.clone()), + }, + }; + } +} + +/// Converts an OWNER handle into the authorized response DTO without exposing secret key material. +pub(crate) fn owner_projection(wallet_id: &str, view_enabled: bool, owner: &ksp_wallet_lib::WalletOwner) -> WalletAuthorizedDto { + let info = owner.info(); + let notes = info + .notes() + .iter() + .map(|note| { + return WalletNoteDto { id: note.id().to_owned(), text: note.text().to_owned() }; + }) + .collect(); + return WalletAuthorizedDto { + alias: info.alias().map(|value| return value.to_owned()), + capability: "owner".to_owned(), + filename: wallet_id.to_owned(), + format_version: info.format_version(), + notes, + pubkey: info.pubkey().to_string(), + wallet_id: wallet_id.to_owned(), + view_enabled, + }; +} + +#[cfg(test)] +#[path = "../unit_tests/wallet_session.rs"] +mod tests; diff --git a/crates/ksp-app-wallet-desk/tauri.conf.json b/crates/ksp-app-wallet-desk/tauri.conf.json index 13d9ea2..cd46c61 100644 --- a/crates/ksp-app-wallet-desk/tauri.conf.json +++ b/crates/ksp-app-wallet-desk/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "KSP Wallet Desk", - "version": "0.2.6-pre.4.fix.1", + "version": "0.2.6-pre.5", "identifier": "com.sasedev.ksp-app-wallet-desk", "build": { "beforeDevCommand": "npm run dev", diff --git a/crates/ksp-app-wallet-desk/tests/desktop_contract.rs b/crates/ksp-app-wallet-desk/tests/desktop_contract.rs index 02b9915..4b751c6 100644 --- a/crates/ksp-app-wallet-desk/tests/desktop_contract.rs +++ b/crates/ksp-app-wallet-desk/tests/desktop_contract.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs -// version: 4 +// version: 5 //! Desktop build, shell and Config-status contract audits for Wallet Desk. @@ -119,3 +119,27 @@ fn frontend_control_interactions_are_trace_logged_without_control_values() { assert!(main.contains("viewId: control.dataset.view")); assert!(!main.contains("control.value")); } + +#[test] +fn pre_005_create_and_session_lifecycle_are_backend_owned() { + let root = app_root(); + let state = read_text(root.join("src/app_state.rs").as_path()); + let session = read_text(root.join("src/wallet_session.rs").as_path()); + 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("WalletSession::Owner")); + assert!(state.contains("WalletSession::Locked")); + assert!(state.contains("std::mem::replace")); + assert!(session.contains("WalletCreateRequestDto")); + assert!(session.contains("WalletAuthorizedDto")); + assert!(tauri.contains("create_wallet")); + assert!(tauri.contains("deselect_wallet")); + assert!(tauri.contains("lock_wallet")); + assert!(html.contains("id=\"createWalletForm\"")); + assert!(html.contains("id=\"createWalletOwnerPassword\"")); + assert!(html.contains("id=\"createWalletViewPassword\"")); + assert!(main.contains("clearCreateFormSensitiveInputs")); + assert!(main.contains("OWNER Wallet session rendered after creation")); +} diff --git a/crates/ksp-app-wallet-desk/tests/desktop_security.rs b/crates/ksp-app-wallet-desk/tests/desktop_security.rs index 80cced4..28c72a2 100644 --- a/crates/ksp-app-wallet-desk/tests/desktop_security.rs +++ b/crates/ksp-app-wallet-desk/tests/desktop_security.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-wallet-desk/tests/desktop_security.rs -// version: 2 +// version: 3 //! Static desktop security contracts for the Wallet Desk pre.002 shell. @@ -46,3 +46,20 @@ fn locked_inventory_frontend_never_receives_protected_wallet_identity_fields() { assert!(!inventory.contains("notes:")); assert!(!inventory.contains("password:")); } + +#[test] +fn create_passwords_are_request_only_and_protected_projection_is_purged() { + let root = app_root(); + let session = read_text(root.join("src/wallet_session.rs").as_path()); + let main = read_text(root.join("frontend/ts/main.ts").as_path()); + let authorized = session.split("pub(crate) struct WalletAuthorizedDto").nth(1); + assert!(authorized.is_some()); + let authorized = authorized.unwrap_or_default().split("}\n\n/// Create request").next().unwrap_or_default(); + assert!(!authorized.contains("password")); + assert!(session[request_start..].contains("owner_password")); + assert!(session[request_start..].contains("view_password")); + assert!(main.contains("clearAuthorizedProjection")); + assert!(main.contains("clearCreateFormSensitiveInputs")); + assert!(!main.contains("frontendDebug(\"main\", request.ownerPassword")); + assert!(!main.contains("frontendTrace(\"main\", request.viewPassword")); +} diff --git a/crates/ksp-app-wallet-desk/unit_tests/wallet_session.rs b/crates/ksp-app-wallet-desk/unit_tests/wallet_session.rs new file mode 100644 index 0000000..c58ff6f --- /dev/null +++ b/crates/ksp-app-wallet-desk/unit_tests/wallet_session.rs @@ -0,0 +1,26 @@ +// file: crates/ksp-app-wallet-desk/unit_tests/wallet_session.rs +// version: 1 + +//! Unit tests for Wallet Desk session DTOs and OWNER projection. + +#[test] +fn no_selection_projection_contains_no_wallet_identity() { + let session = crate::WalletSession::no_selection(); + let dto = session.safe_projection(); + assert_eq!(dto.state, crate::WalletSessionStateDto::NoSelection); + assert!(dto.wallet_id.is_none()); + assert!(dto.filename.is_none()); +} + +#[test] +fn create_request_does_not_require_debug_or_clone_password_contracts() { + fn accepts_request(_: crate::WalletCreateRequestDto) {} + let request = crate::WalletCreateRequestDto { + alias: std::option::Option::None, + filename: "test.kspwallet".to_owned(), + initial_note: std::option::Option::None, + owner_password: "operator-test-owner".to_owned(), + view_password: std::option::Option::Some("operator-test-view".to_owned()), + }; + accepts_request(request); +} diff --git a/deltas/0.2.6/pre.005.md b/deltas/0.2.6/pre.005.md new file mode 100644 index 0000000..c878cac --- /dev/null +++ b/deltas/0.2.6/pre.005.md @@ -0,0 +1,200 @@ + + + +# Delta `0.2.6-pre.005` — création Wallet et lifecycle de session Rust + +## Base requise + +```text +0.2.6-pre.004-fix.001 appliquée +workspace.package.version = 0.2.6-pre.4.fix.1 +``` + +Les preuves opérateur de `pre.004-fix.001` sont vertes : `cargo fmt --all`, audit structurel Python, `cargo check --workspace`, `cargo clippy --workspace --all-targets`, `cargo test -p ksp-app-wallet-desk`, `cargo test --workspace` et `cargo tauri dev -c crates/ksp-app-wallet-desk/tauri.conf.json`, sans les warnings `unused_imports` corrigés par le fix. + +## Type de livraison + +```text +ksp-general-0.2.6-pre.005.zip +``` + +Cette tranche modifie le runtime Rust/frontend ; le signal technique devient : + +```text +workspace.package.version = 0.2.6-pre.5 +commit = v0.2.6-pre.005 +``` + +Aucun tag stable. + +## Objet + +Rendre enfin possible la création d'un vrai `.kspwallet` depuis Wallet Desk et poser le lifecycle de session backend avant les unlock VIEW/OWNER de wallets existants. + +## Session Rust durable + +`AppState` possède désormais : + +```text +Mutex +``` + +avec les états actuellement matérialisés : + +```text +NoSelection +Locked { wallet_id, path, locked_info } +Owner { wallet_id, path, view_enabled, wallet: WalletOwner } +``` + +Le chemin complet et `WalletOwner` restent exclusivement en Rust. + +Transitions `pre.005` : + +```text +select_wallet -> drop session précédente -> inspect -> Locked +create_wallet -> drop session précédente -> create file -> Owner +deselect_wallet -> drop handle/path -> NoSelection +lock_wallet -> drop Owner -> inspect file -> Locked +refresh_wallets -> deselect backend -> refresh inventory +``` + +Les unlock VIEW/OWNER de fichiers existants restent hors scope et arrivent en `pre.006`. + +## Création native + +Le formulaire Create transmet : + +```text +filename +OWNER password +VIEW password optionnel +alias interne optionnel +note initiale optionnelle +``` + +Rust valide le filename root-scoped puis appelle exclusivement : + +```text +ksp_wallet_lib::create_wallet_file_v1 +``` + +La publication reste no-clobber et la destination reste sous `ResolvedWalletConfig::effective_wallets_directory()`. + +Le `String` OWNER est immédiatement déplacé dans `OwnerPassword`; le `String` VIEW optionnel est immédiatement déplacé dans `ViewPassword`. Les request DTOs de password ne dérivent ni `Debug` ni `Clone`. + +Après succès, le `WalletOwner` retourné est conservé dans `WalletSession::Owner`. La réponse `WalletAuthorizedDto` expose seulement les informations autorisées nécessaires à l'UI : + +```text +wallet_id / filename +capability = owner +format_version +view_enabled +Pubkey +alias optionnel +notes protégées +``` + +Aucun password, seed, keypair, secret bytes ou path complet n'est sérialisé. + +## Frontend et purge + +La carte Wallet courant sait maintenant afficher l'état OWNER après création avec `fa-lock-open`, Pubkey, alias et notes. Le formulaire Create est fonctionnel ; le bouton VIEW active/désactive son champ password. + +Les opérations suivantes purgent les inputs password/alias/note et toute projection autorisée devenue invalide : + +```text +sélection d'un autre wallet +Lock +Deselect +Refresh +échec de transition +``` + +Après une création réussie, l'inventory est relu par `list_wallets` sans détruire la session OWNER. Un Refresh utilisateur appelle au contraire `refresh_wallets`, qui purge d'abord la session backend pour garder UI et Rust synchronisés. + +Les logs frontend/backend n'incluent ni password, ni alias, ni note. Ils peuvent inclure le filename/root-scoped wallet id et les états/capabilities. + +## Commandes Tauri + +Ajouts : + +```text +create_wallet +deselect_wallet +lock_wallet +``` + +`select_wallet` installe désormais réellement la session Locked au lieu de retourner seulement une projection éphémère. + +## DTOs + +Ajouts : + +```text +WalletCreateRequestDto +WalletAuthorizedDto +WalletNoteDto +WalletSessionDto +WalletSessionStateDto +``` + +`WalletCreateRequestDto` est strictement frontend -> Rust. Les autres DTOs ne transportent aucun password. + +## Tests et canaris + +La tranche ajoute/étend des canaris pour : + +```text +NoSelection sans identité +request de création non Clone/Debug par conception +create/session backend owned +commands create/deselect/lock présentes +form Create présente +passwords absents de WalletAuthorizedDto +purge frontend explicite +inventory locked toujours sans identité protégée +``` + +Les tests existants de `ksp-wallet-lib` restent l'autorité pour la cryptographie, le no-clobber, la persistance et les garanties VIEW/OWNER du format V1. + +## Dépendances + +Aucune dépendance Cargo ou npm n'est ajoutée/modifiée dans cette tranche. + +**Aucune commande npm directe n'est nécessaire.** + +## Validation opérateur requise + +```bash +cargo fmt --all +python3 scripts/audit_rust_workspace_rules.py +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-app-wallet-desk +cargo test --workspace +KSP_WALLETS_DIRECTORY=var/wallet-desk-pre005 cargo tauri dev -c crates/ksp-app-wallet-desk/tauri.conf.json +``` + +Vérification UI recommandée : + +```text +Create / Import +-> choisir un filename *.kspwallet neuf +-> saisir OWNER password +-> activer ou non VIEW et saisir son password si activé +-> alias/note optionnels +-> Create wallet +-> vérifier session OWNER + fa-lock-open + Pubkey +-> vérifier apparition dans DataTable +-> Lock -> fa-lock + purge Pubkey/alias/notes +-> Deselect -> NoSelection +-> recréer même filename -> échec no-clobber +-> Refresh pendant OWNER -> session purgée +``` + +Toujours aucun `cargo tauri build` à ce stade. + +## Suite + +Après validation de `pre.005`, `pre.006` ajoutera l'unlock manuel VIEW/OWNER et le provider Config `KSP_SECRET_WALLET_PASS_*`, sans auto-unlock silencieux. diff --git a/docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md b/docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md index e5fb82c..7f96db9 100644 --- a/docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md +++ b/docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md @@ -1,5 +1,5 @@ - + # Séquence des releases fonctionnelles KSP @@ -430,7 +430,7 @@ Le prompt [`../../prompts/011-V0_2_6_START_PROMPT.md`](../../prompts/011-V0_2_6_ `0.2.6-pre.001` confirme que le registre Config doit être étendu pour `cfg.std.wallet`/`schema.std.wallet` et `cfg.composite.ksp-app-wallet-desk`. `wallets_directory` reste global avec fallback `${KSP_WALLETS_DIRECTORY:-wallets}` et chaque profil peut définir un `wallets_subdirectory` relatif pour isoler tests, temporaires ou futurs scénarios. Wallet Desk prépare/crée le répertoire effectif après résolution Config et journalise le succès en debug ou l'échec en error. Le gabarit Config Desk est réutilisé avec Bootstrap, Font Awesome, DataTables/Select, SimpleBar et `resize-observer-polyfill`. L'inventory reste non récursive `.kspwallet`, sans symlink d'entrée et root-scoped; les handles VIEW/OWNER restent exclusivement côté Rust. L'ouverture accepte un password éphémère saisi ou une tentative explicite de secrets Config `KSP_SECRET_WALLET_PASS_*`, dont ni nom ni valeur ne traversent le frontend. `getBalance` utilise la Pubkey du handle autorisé et les flows OWNER metadata/rotations/revocation/export fichier délèguent à Wallet. La CSP reste `null` par défaut comme dans Config Desk et n'est réauditée qu'en présence d'un besoin WebView concret. Le forecast souple détaillé s'étend jusqu'à `pre.014`, dernière tranche prévue pour README/USAGE/docs/validation, prompt `0.2.7` et build Tauri final; le plan actif est `docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md`. -`0.2.6-pre.002` matérialise le shell Tauri spécialisé. `0.2.6-pre.003` matérialise ensuite la première composition Config propre à Wallet Desk : `cfg.std.wallet`/`schema.std.wallet`, le composite `cfg.composite.ksp-app-wallet-desk`, `ResolvedWalletConfig`, la racine `${KSP_WALLETS_DIRECTORY:-wallets}`, les sous-répertoires de profils `temporary`/`tests`, la préservation de provenance `Composite` dans les adapters Logging/Wallet et la création sûre des répertoires par l’application. Aucun inventory ou fichier `.kspwallet` n’est encore ouvert dans cette tranche. `0.2.6-pre.003-fix.001` corrige ensuite les tests de registry/fixtures devenus obsolètes et instrumente les clics frontend génériques sans sérialiser la valeur des contrôles. `0.2.6-pre.003-fix.002` corrige le scanner d’inventaire `.env.example` qui interprétait `FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK` comme une variable d’environnement, remplace le profil Logging ad hoc Wallet Desk par les profils génériques console-only/`file_info`/`superdev`/`supertrace`, et sélectionne temporairement `supertrace` dans le composite Wallet Desk. `0.2.6-pre.003-fix.003` corrige les deux canaris révélés par la validation opérateur de `fix.002` sans modifier cette surface Logging : closures avec `return` explicite sous la politique Clippy KSP et test lexical séparant fragment d’identifiant d’un vrai nom KSP suffixé. `0.2.6-pre.003-fix.004` complète cette remédiation en ajoutant les `return` explicites aux cinq closures restantes du canari `config_composition` de Wallet Desk ; aucune surface Config/Logging/frontend n’est modifiée. `0.2.6-pre.004` ajoute l’inventory `.kspwallet` réel sous `effective_wallets_directory` : lecture non récursive, extension exacte, exclusion des symlinks et non-fichiers, inspection par `ksp-wallet-lib::inspect_locked_wallet_file_v1`, lignes invalides conservées avec diagnostic sûr, DTOs locked sans identité protégée, DataTable/refresh actifs et sélection filename root-scoped revalidée côté Rust. Aucun handle VIEW/OWNER ni password n’est introduit dans cette tranche. `0.2.6-pre.004-fix.001` limite ensuite à `#[cfg(test)]` les deux réexports crate-root internes des enums d’état/inspection, uniquement consommés par les unit tests ; le build runtime ne les importe plus, tandis que les tests conservent le chemin `crate::...` imposé par les règles d’exports KSP. Le contrat runtime et TS-RS reste inchangé. +`0.2.6-pre.002` matérialise le shell Tauri spécialisé. `0.2.6-pre.003` matérialise ensuite la première composition Config propre à Wallet Desk : `cfg.std.wallet`/`schema.std.wallet`, le composite `cfg.composite.ksp-app-wallet-desk`, `ResolvedWalletConfig`, la racine `${KSP_WALLETS_DIRECTORY:-wallets}`, les sous-répertoires de profils `temporary`/`tests`, la préservation de provenance `Composite` dans les adapters Logging/Wallet et la création sûre des répertoires par l’application. Aucun inventory ou fichier `.kspwallet` n’est encore ouvert dans cette tranche. `0.2.6-pre.003-fix.001` corrige ensuite les tests de registry/fixtures devenus obsolètes et instrumente les clics frontend génériques sans sérialiser la valeur des contrôles. `0.2.6-pre.003-fix.002` corrige le scanner d’inventaire `.env.example` qui interprétait `FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK` comme une variable d’environnement, remplace le profil Logging ad hoc Wallet Desk par les profils génériques console-only/`file_info`/`superdev`/`supertrace`, et sélectionne temporairement `supertrace` dans le composite Wallet Desk. `0.2.6-pre.003-fix.003` corrige les deux canaris révélés par la validation opérateur de `fix.002` sans modifier cette surface Logging : closures avec `return` explicite sous la politique Clippy KSP et test lexical séparant fragment d’identifiant d’un vrai nom KSP suffixé. `0.2.6-pre.003-fix.004` complète cette remédiation en ajoutant les `return` explicites aux cinq closures restantes du canari `config_composition` de Wallet Desk ; aucune surface Config/Logging/frontend n’est modifiée. `0.2.6-pre.004` ajoute l’inventory `.kspwallet` réel sous `effective_wallets_directory` : lecture non récursive, extension exacte, exclusion des symlinks et non-fichiers, inspection par `ksp-wallet-lib::inspect_locked_wallet_file_v1`, lignes invalides conservées avec diagnostic sûr, DTOs locked sans identité protégée, DataTable/refresh actifs et sélection filename root-scoped revalidée côté Rust. Aucun handle VIEW/OWNER ni password n’est introduit dans cette tranche. `0.2.6-pre.004-fix.001` limite ensuite à `#[cfg(test)]` les deux réexports crate-root internes des enums d’état/inspection, uniquement consommés par les unit tests ; le build runtime ne les importe plus, tandis que les tests conservent le chemin `crate::...` imposé par les règles d’exports KSP. Le contrat runtime et TS-RS reste inchangé. `0.2.6-pre.005` branche ensuite la création native sur `create_wallet_file_v1` et introduit un `WalletSession` durable dans `AppState` : création -> OWNER ouvert, sélection existante -> Locked, puis lock/deselect/refresh/changement de wallet -> drop des handles et purge des projections protégées. Les passwords de création transitent uniquement frontend -> Rust et sont immédiatement déplacés dans les wrappers Wallet ; les unlock de wallets existants restent `pre.006`. ## `0.2.7` — WebSocket Solana standard diff --git a/docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md b/docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md index 1fd3ec0..ba114e2 100644 --- a/docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md +++ b/docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md @@ -1,5 +1,5 @@ - + # Plan `0.2.6` — Wallet Desk @@ -23,7 +23,7 @@ Les validations opérateur du `2026-08-20` ont ensuite confirmé la base `pre.00 `0.2.6-pre.002` matérialise ensuite la première tranche technique : la crate `ksp-app-wallet-desk` rejoint le workspace avec le shell splash/main du gabarit Config Desk, le bootstrap Config + Logging commun, les ports `1432/1433`, Bootstrap, Font Awesome, DataTables/Select, SimpleBar, `resize-observer-polyfill`, TS-RS et le bridge frontend -> Rust -> `ksp-logging-lib`. Cette tranche ne branche encore ni `std.wallet`, ni inventory filesystem réel, ni `ksp-wallet-lib`, ni Transport : ces responsabilités restent dans les prereleases déjà dimensionnées. -`0.2.6-pre.003` matérialise la composition Config prévue : `cfg.std.wallet`/`schema.std.wallet`, le composite `cfg.composite.ksp-app-wallet-desk`, les profils Wallet `default`/`temporary`/`tests`, `ResolvedWalletConfig` et les adapters qui peuvent consommer un profil déjà sélectionné par composite sans perdre la provenance `Composite`. Wallet Desk valide les composants `logging`/`transport`/`wallet`, crée la racine et le sous-répertoire effectif absents avec logs `debug`, refuse les objets filesystem invalides/symlinks de sous-répertoire avec logs `error`, et expose uniquement les chemins/profils non secrets dans le statut runtime. Aucun inventory `.kspwallet` n’est encore effectué. `pre.003-fix.001` corrige les deux régressions de tests révélées par la validation workspace et le warning Clippy, puis rend visibles les événements `trace` frontend. `pre.003-fix.002` corrige ensuite le canari d’inventaire `.env.example` qui confondait un fragment d’identifiant Rust avec une variable KSP, remplace le profil temporaire `wallet_desk_dev` par une famille Logging réutilisable (`console_*`, `file_info`, `superdev`, `supertrace`) et fait sélectionner temporairement `supertrace` par le composite Wallet Desk afin de conserver la visibilité des actions frontend sans profil spécifique à l’application. `pre.003-fix.003` corrige uniquement les canaris révélés par la validation de `fix.002` : conformité `clippy::implicit_return` et séparation explicite entre fragment lexical KSP incorporé dans un identifiant et nom d’environnement concret suffixé. `pre.003-fix.004` ferme le dernier défaut Clippy de cette série en ajoutant les `return` explicites aux cinq closures restantes du test de composition `supertrace`, sans changer les profils Logging ni le composite. `pre.004` matérialise ensuite l’inventory locked réel. Sa validation opérateur confirme le comportement fonctionnel et le workspace complet, mais révèle deux warnings `unused_imports` sur des réexports crate-root internes uniquement utiles aux unit tests ; `pre.004-fix.001` les limite à `#[cfg(test)]` : ils disparaissent du build runtime mais restent disponibles au crate-root pour les unit tests, conformément aux règles d’exports KSP, sans changement de comportement. +`0.2.6-pre.003` matérialise la composition Config prévue : `cfg.std.wallet`/`schema.std.wallet`, le composite `cfg.composite.ksp-app-wallet-desk`, les profils Wallet `default`/`temporary`/`tests`, `ResolvedWalletConfig` et les adapters qui peuvent consommer un profil déjà sélectionné par composite sans perdre la provenance `Composite`. Wallet Desk valide les composants `logging`/`transport`/`wallet`, crée la racine et le sous-répertoire effectif absents avec logs `debug`, refuse les objets filesystem invalides/symlinks de sous-répertoire avec logs `error`, et expose uniquement les chemins/profils non secrets dans le statut runtime. Aucun inventory `.kspwallet` n’est encore effectué. `pre.003-fix.001` corrige les deux régressions de tests révélées par la validation workspace et le warning Clippy, puis rend visibles les événements `trace` frontend. `pre.003-fix.002` corrige ensuite le canari d’inventaire `.env.example` qui confondait un fragment d’identifiant Rust avec une variable KSP, remplace le profil temporaire `wallet_desk_dev` par une famille Logging réutilisable (`console_*`, `file_info`, `superdev`, `supertrace`) et fait sélectionner temporairement `supertrace` par le composite Wallet Desk afin de conserver la visibilité des actions frontend sans profil spécifique à l’application. `pre.003-fix.003` corrige uniquement les canaris révélés par la validation de `fix.002` : conformité `clippy::implicit_return` et séparation explicite entre fragment lexical KSP incorporé dans un identifiant et nom d’environnement concret suffixé. `pre.003-fix.004` ferme le dernier défaut Clippy de cette série en ajoutant les `return` explicites aux cinq closures restantes du test de composition `supertrace`, sans changer les profils Logging ni le composite. `pre.004` matérialise ensuite l’inventory locked réel. Sa validation opérateur confirme le comportement fonctionnel et le workspace complet, mais révèle deux warnings `unused_imports` sur des réexports crate-root internes uniquement utiles aux unit tests ; `pre.004-fix.001` les limite à `#[cfg(test)]` : ils disparaissent du build runtime mais restent disponibles au crate-root pour les unit tests, conformément aux règles d’exports KSP, sans changement de comportement. `pre.005` matérialise ensuite la création native dans Wallet Desk et le premier lifecycle durable `WalletSession` : création no-clobber sous la racine Config via `create_wallet_file_v1`, session OWNER conservée uniquement en Rust après création, sélection locked durable, `Lock`/`Deselect`/Refresh/changement de wallet avec drop des handles et purge frontend des projections protégées/passwords. Les unlock VIEW/OWNER de wallets existants restent strictement `pre.006`. ## 2. Sources relues et hiérarchie appliquée @@ -1348,6 +1348,23 @@ purge frontend protégée Cette tranche pose le lifecycle avant d'ajouter les providers de password existants. +État matérialisé par `pre.005` : + +```text +WalletSession Rust = NoSelection | Locked | Owner +create_wallet -> create_wallet_file_v1 -> session OWNER +select_wallet -> session Locked +deselect_wallet -> drop handle/path state -> NoSelection +lock_wallet -> drop OWNER -> re-inspect -> Locked +refresh_wallets -> deselect backend -> inventory +``` + +Le formulaire Create transmet OWNER/VIEW passwords uniquement frontend -> Rust. `WalletCreateRequestDto` n'est ni `Debug` ni `Clone`; les wrappers `OwnerPassword`/`ViewPassword` prennent immédiatement possession des `String`. Les réponses ne contiennent aucun password ni secret key. Après création, `WalletAuthorizedDto` peut exposer Pubkey, alias et notes parce que la session OWNER est explicitement autorisée. Les valeurs protégées ne sont pas journalisées. + +Après création, l'inventory est relu avec `list_wallets` sans détruire la session OWNER nouvellement créée. En revanche, un Refresh utilisateur appelle `refresh_wallets`, qui purge d'abord la session Rust afin que l'UI et le backend ne divergent jamais. Lock, deselect, sélection d'un autre wallet et échec de transition purgent aussi les projections protégées et inputs frontend. + +Les unlock manuels/configurés d'un wallet existant restent strictement `pre.006`. + ### `pre.006` — unlock manuel + `KSP_SECRET_WALLET_PASS_*` Objectifs :