From 2b7afab6638336f0b57093cac5fd6023ab81097b Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Fri, 21 Aug 2026 12:35:01 +0200 Subject: [PATCH] v0.2.6-pre.009 --- Cargo.toml | 4 +- ROADMAP.md | 4 +- crates/ksp-app-wallet-desk/frontend/main.html | 51 +++- .../ksp-app-wallet-desk/frontend/ts/main.ts | 216 +++++++++++++++- crates/ksp-app-wallet-desk/package.json | 2 +- crates/ksp-app-wallet-desk/src/app_state.rs | 220 ++++++++++++++++- crates/ksp-app-wallet-desk/src/lib.rs | 11 +- crates/ksp-app-wallet-desk/src/tauri.rs | 54 +++- .../src/wallet_metadata.rs | 48 ++++ .../ksp-app-wallet-desk/src/wallet_session.rs | 22 +- crates/ksp-app-wallet-desk/tauri.conf.json | 2 +- .../tests/desktop_contract.rs | 32 ++- .../tests/desktop_security.rs | 23 +- .../unit_tests/wallet_metadata.rs | 23 ++ deltas/0.2.6/pre.009.md | 231 ++++++++++++++++++ docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md | 28 ++- docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md | 40 ++- 17 files changed, 970 insertions(+), 41 deletions(-) create mode 100644 crates/ksp-app-wallet-desk/src/wallet_metadata.rs create mode 100644 crates/ksp-app-wallet-desk/unit_tests/wallet_metadata.rs create mode 100644 deltas/0.2.6/pre.009.md diff --git a/Cargo.toml b/Cargo.toml index c635ef5..0288e50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 176 +# version: 177 [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.8.fix.1" +version = "0.2.6-pre.9" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/ROADMAP.md b/ROADMAP.md index 9a4cd7b..1d25dda 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. `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. `pre.005-fix.001` corrige le premier gate opérateur de cette tranche : `WalletOwner` est indirecté par `Box` dans `WalletSession::Owner` pour fermer `clippy::large_enum_variant`, et le canari `desktop_security` retrouve explicitement le début de `WalletCreateRequestDto` avant de vérifier que les passwords restent request-only. `pre.006` ajoute ensuite les unlock explicites VIEW/OWNER : saisie manuelle request-only, découverte des candidats `KSP_SECRET_WALLET_PASS_*` exclusivement via Config, ordre filename normalisé puis numérique puis nommé, tentative configurée uniquement sur action utilisateur, état transitoire `PrivilegedOperation` pendant Argon2 et projection frontend limitée au nombre de candidats sans nom/suffixe/valeur. `pre.006-fix.001` corrige uniquement les deux canaris statiques de sécurité : ils inspectent désormais les champs réels de `WalletAuthorizedDto` au lieu de considérer le mot `password` présent dans sa documentation de `configured_secret_candidate_count` comme une fuite de champ. `pre.007` ferme ensuite le MVP obligatoire : l'adapter Transport accepte un profil déjà sélectionné par composite avec provenance `Composite`, Wallet Desk construit `HttpTransportPool` au bootstrap, expose des diagnostics profile/cluster/provider sans URL, extrait la Pubkey uniquement d'une session VIEW/OWNER Rust, puis appelle le wrapper typé `getBalance` en `confirmed` et projette lamports exacts, SOL décimal, slot et `api_version`. Le frontend ne fournit ni Pubkey ni endpoint au refresh et toute balance est purgée lors du lock/deselect/changement de wallet. `pre.007-fix.001` corrige le canari d'intégration `config_composition` qui appelait le constructeur interne test-only `ConfigEnvironment::from_maps`; le test consomme désormais uniquement l'API publique `ConfigEnvironment::load`, comme le runtime Wallet Desk, sans modification du pool Transport ni du frontend. `pre.007-fix.002` corrige le diagnostic de ce même canari : l'assertion sur `ConfigEnvironment::load()` ne tente plus de formatter `Result` avec `Debug`, trait volontairement absent de `ConfigEnvironment`; le contrat runtime reste inchangé. Les tranches suivantes ajoutent 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-pre.008` ajoute ensuite l'import Solana CLI JSON/Base58 via un picker natif exclusivement Rust : `tauri-plugin-dialog` reste backend-only, aucun chemin source ni contenu secret ne traverse IPC, les octets sont lus avec une borne stricte puis conservés dans `Zeroizing>`, l'inspection ne projette que basename/format/Pubkey, et `import_wallet_transfer_v1` publie no-clobber sous la racine Config avec de nouveaux credentials OWNER/VIEW avant d'ouvrir la session OWNER. `pre.008-fix.001` ferme uniquement les deux warnings Clippy observés au gate opérateur en remplaçant `mem::replace(..., Some(...))` par `Option::replace` et le déréférencement explicite `&mut *bytes` par l’auto-deref `&mut bytes`; le staging zéroïsable, l’import et les frontières IPC restent inchangés. +- [/] `0.2.6` — `ksp-app-wallet-desk` : composition Config + Wallet + HTTP, inventory/création/unlock/balance/import acquis ; administration OWNER, rotations, lifecycle VIEW, export, intégration/sécurité, polish desktop puis documentation finale restent à clôturer. 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. diff --git a/crates/ksp-app-wallet-desk/frontend/main.html b/crates/ksp-app-wallet-desk/frontend/main.html index 933d194..d8b899c 100644 --- a/crates/ksp-app-wallet-desk/frontend/main.html +++ b/crates/ksp-app-wallet-desk/frontend/main.html @@ -252,6 +252,40 @@
Unlock VIEW ou OWNER pour interroger getBalance.
+
+
Administration metadata OWNER
+
+

Alias et notes sont protégés dans le wallet. Les contrôles restent désactivés tant qu’une session OWNER n’est pas ouverte.

+
+
+ +
+ + + +
+
Limite V1 : 256 octets UTF-8. La validation autoritative reste dans ksp-wallet-lib.
+
+
+ +
+ + +
+
+
+
+ Notes autorisées + 64 notes maximum en V1 +
+
Ouvrir OWNER pour administrer les notes.
+
+
+
Session OWNER requise.
+
+
+
+
+
- KSP Wallet Desk · native transfer import pre.008 + KSP Wallet Desk · OWNER metadata pre.009
diff --git a/crates/ksp-app-wallet-desk/frontend/ts/main.ts b/crates/ksp-app-wallet-desk/frontend/ts/main.ts index 73d3fe1..6744524 100644 --- a/crates/ksp-app-wallet-desk/frontend/ts/main.ts +++ b/crates/ksp-app-wallet-desk/frontend/ts/main.ts @@ -1,13 +1,14 @@ // file: crates/ksp-app-wallet-desk/frontend/ts/main.ts -// version: 8 +// version: 9 -import "bootstrap"; +import { Modal } from "bootstrap"; import DataTable from "datatables.net-bs5"; import "datatables.net-select-bs5"; import ResizeObserver from "resize-observer-polyfill"; import "simplebar"; import { getCurrentWindow } from "@tauri-apps/api/window"; import type { WalletBalanceDto } from "./bindings/ksp_app_wallet_desk/wallet_balance/WalletBalanceDto.ts"; +import type { CommandErrorDto } from "./bindings/ksp_app_wallet_desk/dto_common/CommandErrorDto.ts"; import type { RuntimeStatusDto } from "./bindings/ksp_app_wallet_desk/dto_common/RuntimeStatusDto.ts"; import type { WalletImportRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_import/WalletImportRequestDto.ts"; import type { WalletImportSourceRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_import/WalletImportSourceRequestDto.ts"; @@ -16,6 +17,10 @@ import type { WalletTransferInspectionDto } from "./bindings/ksp_app_wallet_desk 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 { WalletAliasUpdateRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_metadata/WalletAliasUpdateRequestDto.ts"; +import type { WalletNoteAddRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_metadata/WalletNoteAddRequestDto.ts"; +import type { WalletNoteDeleteRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_metadata/WalletNoteDeleteRequestDto.ts"; +import type { WalletNoteUpdateRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_metadata/WalletNoteUpdateRequestDto.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"; @@ -43,7 +48,10 @@ const viewTitles: Record = { let activeWalletId: string | null = null; let activeSessionState: ActiveSessionState = "no_selection"; let activeViewEnabled = false; +let authorizedWalletProjection: WalletAuthorizedDto | null = null; let configuredSecretCandidateCount = 0; +let deleteNoteModal: Modal | null = null; +let pendingDeleteNoteId: string | null = null; let stagedImportSource: WalletTransferInspectionDto | null = null; function isViewId(value: string): value is ViewId { @@ -212,6 +220,14 @@ function updateSessionActions(): void { refreshBalance.disabled = activeSessionState !== "view_open" && activeSessionState !== "owner_open"; } updateUnlockActions(); + updateOwnerMetadataActions(); +} + +function updateOwnerMetadataActions(): void { + const ownerOpen = activeSessionState === "owner_open"; + document.querySelectorAll("[data-owner-metadata-control]").forEach(control => { + control.disabled = !ownerOpen; + }); } function updateUnlockActions(): void { @@ -314,6 +330,8 @@ function clearBalanceProjection(): void { } function clearAuthorizedProjection(): void { + authorizedWalletProjection = null; + clearOwnerMetadataProjection(); setText("#currentWalletPubkey", "—"); setText("#currentWalletAlias", "—"); setText("#currentWalletNotes", "—"); @@ -371,6 +389,7 @@ function renderLockedWallet(wallet: LockedWalletDto): void { function renderAuthorizedWallet(wallet: WalletAuthorizedDto): void { activeWalletId = wallet.walletId; + authorizedWalletProjection = wallet; activeSessionState = wallet.capability === "view" ? "view_open" : "owner_open"; activeViewEnabled = wallet.viewEnabled; configuredSecretCandidateCount = wallet.configuredSecretCandidateCount; @@ -392,6 +411,7 @@ function renderAuthorizedWallet(wallet: WalletAuthorizedDto): void { setText("#detailsWalletPubkey", wallet.pubkey); setText("#detailsWalletAlias", wallet.alias ?? "—"); setText("#detailsWalletNotes", notesText); + renderOwnerMetadataProjection(wallet); clearBalanceProjection(); setText("#balanceStatus", "Session autorisée. Refresh balance appelle getBalance avec la Pubkey détenue par Rust."); setText("#unlockStatus", `${capabilityLabel} ouvert. Lock pour purger le handle autorisé.`); @@ -405,6 +425,195 @@ function renderAuthorizedWallet(wallet: WalletAuthorizedDto): void { }); } +function clearOwnerMetadataProjection(): void { + pendingDeleteNoteId = null; + const alias = document.querySelector("#ownerWalletAlias"); + const newNote = document.querySelector("#ownerNewNote"); + const list = document.querySelector("#ownerNotesList"); + if (alias) { + alias.value = ""; + } + if (newNote) { + newNote.value = ""; + } + if (list) { + const empty = document.createElement("span"); + empty.className = "text-body-secondary"; + empty.textContent = "Ouvrir OWNER pour administrer les notes."; + list.replaceChildren(empty); + } + setText("#ownerMetadataStatus", "Session OWNER requise."); +} + +function renderOwnerMetadataProjection(wallet: WalletAuthorizedDto): void { + const alias = document.querySelector("#ownerWalletAlias"); + const list = document.querySelector("#ownerNotesList"); + if (alias) { + alias.value = wallet.alias ?? ""; + } + if (list) { + if (wallet.notes.length === 0) { + const empty = document.createElement("span"); + empty.className = "text-body-secondary"; + empty.textContent = wallet.capability === "owner" ? "Aucune note protégée." : "Les notes sont visibles en VIEW mais modifiables uniquement en OWNER."; + list.replaceChildren(empty); + } else { + const rows = wallet.notes.map(note => { + const row = document.createElement("div"); + row.className = "border rounded p-2"; + const textarea = document.createElement("textarea"); + textarea.className = "form-control mb-2"; + textarea.rows = 2; + textarea.value = note.text; + textarea.disabled = wallet.capability !== "owner"; + textarea.autocomplete = "off"; + textarea.dataset.ownerMetadataControl = "true"; + const actions = document.createElement("div"); + actions.className = "d-flex gap-2 justify-content-end"; + const save = document.createElement("button"); + save.className = "btn btn-outline-primary btn-sm"; + save.type = "button"; + save.textContent = "Save note"; + save.disabled = wallet.capability !== "owner"; + save.dataset.ownerMetadataControl = "true"; + save.addEventListener("click", () => { + const request: WalletNoteUpdateRequestDto = { noteId: note.id, text: textarea.value }; + void runOwnerMetadataMutation("update_wallet_note", { request }, "Mise à jour de la note…", "Note mise à jour."); + }); + const remove = document.createElement("button"); + remove.className = "btn btn-outline-danger btn-sm"; + remove.type = "button"; + remove.textContent = "Delete"; + remove.disabled = wallet.capability !== "owner"; + remove.dataset.ownerMetadataControl = "true"; + remove.addEventListener("click", () => { + pendingDeleteNoteId = note.id; + deleteNoteModal?.show(); + frontendDebug("main", "OWNER note delete confirmation opened", { walletId: wallet.walletId }); + }); + actions.append(save, remove); + row.append(textarea, actions); + return row; + }); + list.replaceChildren(...rows); + } + } + setText("#ownerMetadataStatus", wallet.capability === "owner" ? "OWNER ouvert : metadata modifiable." : "VIEW ouvert : metadata en lecture seule."); + updateOwnerMetadataActions(); +} + +function isWalletStateConflict(caughtError: unknown): boolean { + if (typeof caughtError !== "object" || caughtError === null) { + return false; + } + const error = caughtError as Partial; + return error.domain === "wallet" && error.code === "state_conflict"; +} + +async function recoverOwnerMetadataStateConflict(walletId: string): Promise { + clearAuthorizedProjection(); + clearBalanceProjection(); + const request: WalletSelectionRequestDto = { walletId }; + try { + const wallet = await invokeKsp("main", "select_wallet", { request }); + renderLockedWallet(wallet); + setText("#ownerMetadataStatus", "Le fichier wallet a changé hors de cette session. Le handle OWNER stale a été purgé ; unlock OWNER requis avant toute nouvelle mutation."); + setText("#unlockStatus", "Conflit d’état détecté : wallet réinspecté et reverrouillé. Réautoriser OWNER explicitement."); + await loadWalletInventory("list_wallets", false); + activateView("security", "user"); + frontendWarn("main", "Wallet state conflict forced OWNER reauthorization", { walletId }); + } catch { + clearSelectedWallet(); + setText("#ownerMetadataStatus", "Conflit d’état détecté et réinspection impossible. Resélectionner le wallet depuis l’inventaire."); + frontendWarn("main", "Wallet state conflict recovery failed", { walletId }); + } +} + +async function runOwnerMetadataMutation( + command: "add_wallet_note" | "delete_wallet_note" | "update_wallet_alias" | "update_wallet_note", + args: Record, + pendingMessage: string, + successMessage: string, +): Promise { + if (activeSessionState !== "owner_open" || !activeWalletId || authorizedWalletProjection?.capability !== "owner") { + setText("#ownerMetadataStatus", "Session OWNER requise."); + return false; + } + const walletId = activeWalletId; + activeSessionState = "privileged_operation"; + setText("#currentWalletState", "OWNER mutation…"); + setText("#ownerMetadataStatus", pendingMessage); + updateSessionActions(); + frontendDebug("main", "OWNER metadata mutation requested", { command, walletId }); + try { + const wallet = await invokeKsp("main", command, args); + if (activeWalletId !== walletId) { + frontendWarn("main", "Stale OWNER metadata response ignored", { command, walletId }); + return false; + } + renderAuthorizedWallet(wallet); + setText("#ownerMetadataStatus", successMessage); + await loadWalletInventory("list_wallets", false); + return true; + } catch (caughtError) { + if (isWalletStateConflict(caughtError)) { + await recoverOwnerMetadataStateConflict(walletId); + return false; + } + if (activeWalletId === walletId) { + activeSessionState = "owner_open"; + setText("#currentWalletState", "OWNER open"); + setText("#ownerMetadataStatus", "Mutation refusée ; la session OWNER reste ouverte."); + updateSessionActions(); + } + frontendWarn("main", "OWNER metadata mutation failed", { command, walletId }); + return false; + } +} + +function bindOwnerMetadataActions(): void { + const modalElement = document.querySelector("#deleteWalletNoteModal"); + if (modalElement) { + deleteNoteModal = new Modal(modalElement); + } + document.querySelector("#saveOwnerAlias")?.addEventListener("click", () => { + const alias = document.querySelector("#ownerWalletAlias"); + if (!alias) { + return; + } + const request: WalletAliasUpdateRequestDto = { alias: alias.value.length === 0 ? null : alias.value }; + void runOwnerMetadataMutation("update_wallet_alias", { request }, "Mise à jour de l’alias…", "Alias mis à jour."); + }); + document.querySelector("#clearOwnerAlias")?.addEventListener("click", () => { + const request: WalletAliasUpdateRequestDto = { alias: null }; + void runOwnerMetadataMutation("update_wallet_alias", { request }, "Suppression de l’alias…", "Alias supprimé."); + }); + document.querySelector("#addOwnerNote")?.addEventListener("click", () => { + const text = document.querySelector("#ownerNewNote"); + if (!text || text.value.length === 0) { + setText("#ownerMetadataStatus", "Texte de note requis."); + return; + } + const request: WalletNoteAddRequestDto = { text: text.value }; + void runOwnerMetadataMutation("add_wallet_note", { request }, "Ajout de la note…", "Note ajoutée.").then(succeeded => { + if (succeeded && activeSessionState === "owner_open") { + text.value = ""; + } + }); + }); + document.querySelector("#confirmDeleteWalletNote")?.addEventListener("click", () => { + const noteId = pendingDeleteNoteId; + pendingDeleteNoteId = null; + deleteNoteModal?.hide(); + if (!noteId) { + return; + } + const request: WalletNoteDeleteRequestDto = { noteId }; + void runOwnerMetadataMutation("delete_wallet_note", { request }, "Suppression de la note…", "Note supprimée."); + }); + frontendTrace("main", "OWNER metadata action handlers installed"); +} + async function selectWallet(walletId: string): Promise { clearSelectedWallet(); const request: WalletSelectionRequestDto = { walletId }; @@ -824,7 +1033,7 @@ function renderRuntimeStatus(status: RuntimeStatusDto): void { setText("#runtimeEffectiveWalletsDirectory", status.effectiveWalletsDirectory); setText("#runtimeWalletDirectoryCreated", status.effectiveWalletsDirectoryCreatedOnStartup ? "oui" : "non, déjà présent"); setText("#runtimeShellPhase", status.shellPhase); - setText("#shellStatus", "Config/Transport résolus ; import natif, VIEW/OWNER et getBalance prêts."); + setText("#shellStatus", "Config/Transport résolus ; import, VIEW/OWNER, metadata OWNER et getBalance prêts."); frontendTrace("main", "Wallet Desk runtime status rendered", { compositeProfile: status.activeCompositeProfile, fallbackLoggingActive: status.fallbackLoggingActive, @@ -875,6 +1084,7 @@ async function initializeMain(): Promise { bindImportWalletForm(); bindUnlockActions(); bindBalanceActions(); + bindOwnerMetadataActions(); bindShellActions(); clearSelectedWallet(); activateView("dashboard", "startup"); diff --git a/crates/ksp-app-wallet-desk/package.json b/crates/ksp-app-wallet-desk/package.json index 58dec0a..68e5454 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.8.fix.1", + "version": "0.2.6-pre.9", "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 ce4054a..6e033a2 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: 10 +// version: 11 //! Shared backend state owned by the Wallet Desk Tauri application. @@ -126,7 +126,7 @@ impl AppState { effective_wallets_directory_created_on_startup: self.wallet_config_startup.effective_directory_created_on_startup(), fallback_logging_active: runtime.fallback_active, root_wallets_directory_created_on_startup: self.wallet_config_startup.root_directory_created_on_startup(), - shell_phase: "pre.008-wallet-import".to_owned(), + shell_phase: "pre.009-owner-metadata".to_owned(), startup_diagnostic: runtime.startup_diagnostic.clone(), transport_available_endpoint_count, transport_clusters: self.transport_runtime.clusters(), @@ -217,7 +217,7 @@ impl AppState { 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: std::boxed::Box::new(owner) }; + *session = crate::WalletSession::Owner { wallet_id: filename.clone(), path: destination, view_enabled, wallet: std::boxed::Box::new(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); } @@ -313,7 +313,7 @@ impl AppState { 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: std::boxed::Box::new(owner) }; + *session = crate::WalletSession::Owner { wallet_id: filename.clone(), path: destination, view_enabled, wallet: std::boxed::Box::new(owner) }; ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_IMPORT, wallet_id = filename.as_str(), transfer_format = transfer_format.code(), view_enabled, "Wallet transfer imported and OWNER session opened"); return std::result::Result::Ok(dto); } @@ -483,6 +483,18 @@ impl AppState { "Wallet unlock operation is already in progress", )); }, + crate::WalletSession::OwnerOperation { wallet_id, path, pubkey, 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::OwnerOperation { wallet_id, path, pubkey, view_enabled }; + return std::result::Result::Err(ksp_core_lib::Error::new( + crate::ERROR_CODE_WALLET_SESSION_INVALID, + "Wallet OWNER metadata operation is already in progress", + )); + }, crate::WalletSession::View { wallet_id, path, wallet, .. } => { drop(wallet); (wallet_id, path) @@ -595,7 +607,7 @@ impl AppState { }, }; let dto = crate::view_projection(wallet_id.as_str(), view_enabled, configured_secret_candidate_count, &view); - *session = crate::WalletSession::View { wallet_id: wallet_id.clone(), path, wallet: std::boxed::Box::new(view) }; + *session = crate::WalletSession::View { wallet_id: wallet_id.clone(), path, view_enabled, wallet: std::boxed::Box::new(view) }; ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), capability = "view", "Wallet VIEW session opened"); return std::result::Result::Ok(dto); } @@ -633,7 +645,7 @@ impl AppState { }, }; let dto = crate::owner_projection(wallet_id.as_str(), view_enabled, configured_secret_candidate_count, &owner); - *session = crate::WalletSession::Owner { wallet_id: wallet_id.clone(), path, wallet: std::boxed::Box::new(owner) }; + *session = crate::WalletSession::Owner { wallet_id: wallet_id.clone(), path, view_enabled, wallet: std::boxed::Box::new(owner) }; ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), capability = "owner", "Wallet OWNER session opened"); return std::result::Result::Ok(dto); } @@ -649,6 +661,178 @@ impl AppState { }; } + /// Updates or clears the protected alias through the current OWNER handle. + pub(crate) async fn update_wallet_alias(&self, request: crate::WalletAliasUpdateRequestDto) -> ksp_core_lib::Result { + let context = self.begin_owner_metadata_operation("alias_update"); + let mut context = match context { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let alias = normalize_optional_text(request.alias); + let result = context.owner.update_alias(context.path.as_path(), alias).await; + return self.finish_owner_metadata_operation(context, result).await; + } + + /// Appends one protected note through the current OWNER handle. + pub(crate) async fn add_wallet_note(&self, request: crate::WalletNoteAddRequestDto) -> ksp_core_lib::Result { + let context = self.begin_owner_metadata_operation("note_add"); + let mut context = match context { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let result = context.owner.add_note(context.path.as_path(), request.text).await; + let result = match result { + std::result::Result::Ok(_) => std::result::Result::Ok(()), + std::result::Result::Err(error) => std::result::Result::Err(error), + }; + return self.finish_owner_metadata_operation(context, result).await; + } + + /// Updates one protected note through the current OWNER handle. + pub(crate) async fn update_wallet_note(&self, request: crate::WalletNoteUpdateRequestDto) -> ksp_core_lib::Result { + let context = self.begin_owner_metadata_operation("note_update"); + let mut context = match context { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let result = context.owner.update_note(context.path.as_path(), request.note_id.as_str(), request.text).await; + return self.finish_owner_metadata_operation(context, result).await; + } + + /// Deletes one protected note through the current OWNER handle. + pub(crate) async fn delete_wallet_note(&self, request: crate::WalletNoteDeleteRequestDto) -> ksp_core_lib::Result { + let context = self.begin_owner_metadata_operation("note_delete"); + let mut context = match context { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let result = context.owner.delete_note(context.path.as_path(), request.note_id.as_str()).await; + return self.finish_owner_metadata_operation(context, result).await; + } + + fn begin_owner_metadata_operation(&self, operation: &'static str) -> ksp_core_lib::Result { + 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()), + }; + let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection()); + let (wallet_id, path, view_enabled, owner) = match previous { + crate::WalletSession::Owner { wallet_id, path, view_enabled, wallet } => (wallet_id, path, view_enabled, wallet), + other => { + *session = other; + return std::result::Result::Err(ksp_core_lib::Error::new( + crate::ERROR_CODE_WALLET_AUTHORIZATION_REQUIRED, + "Wallet metadata administration requires an authorized OWNER session", + )); + }, + }; + let pubkey = owner.pubkey().to_owned(); + let context = OwnerMetadataContext { operation, owner, path: path.clone(), pubkey, view_enabled, wallet_id: wallet_id.clone() }; + *session = crate::WalletSession::OwnerOperation { wallet_id, path, pubkey, view_enabled }; + ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = context.wallet_id.as_str(), operation, "Wallet OWNER metadata operation started"); + return std::result::Result::Ok(context); + } + + async fn finish_owner_metadata_operation( + &self, + context: OwnerMetadataContext, + operation_result: ksp_core_lib::Result<()>, + ) -> ksp_core_lib::Result { + if let std::result::Result::Err(error) = operation_result { + if error.code() == ksp_wallet_lib::ERROR_CODE_STATE_CONFLICT { + self.recover_owner_metadata_state_conflict(context).await; + return std::result::Result::Err(error); + } + self.restore_owner_after_metadata_failure(context); + return std::result::Result::Err(error); + } + return self.install_owner_after_metadata_success(context); + } + + fn install_owner_after_metadata_success(&self, context: OwnerMetadataContext) -> ksp_core_lib::Result { + let OwnerMetadataContext { operation, owner, path, pubkey, view_enabled, wallet_id } = context; + let configured_secret_candidate_count = self.secret_candidate_count_or_zero(wallet_id.as_str()); + let dto = crate::owner_projection(wallet_id.as_str(), view_enabled, configured_secret_candidate_count, &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()), + }; + let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection()); + match previous { + crate::WalletSession::OwnerOperation { + wallet_id: reserved_wallet_id, + path: reserved_path, + pubkey: reserved_pubkey, + view_enabled: reserved_view_enabled, + } 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, 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 metadata operation completed"); + return std::result::Result::Ok(dto); + }, + other => { + *session = other; + drop(owner); + return std::result::Result::Err(ksp_core_lib::Error::new( + crate::ERROR_CODE_WALLET_SESSION_INVALID, + "Wallet metadata completion no longer owns the selected OWNER session", + )); + }, + } + } + + fn restore_owner_after_metadata_failure(&self, context: OwnerMetadataContext) { + let OwnerMetadataContext { operation, owner, path, pubkey, view_enabled, wallet_id } = context; + let session = self.wallet_session.lock(); + let mut session = match session { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection()); + match previous { + crate::WalletSession::OwnerOperation { + wallet_id: reserved_wallet_id, + path: reserved_path, + pubkey: reserved_pubkey, + view_enabled: reserved_view_enabled, + } 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, wallet: owner }; + ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), operation, "Wallet OWNER metadata operation failed without invalidating the authenticated handle"); + }, + other => { + *session = other; + drop(owner); + }, + } + } + + async fn recover_owner_metadata_state_conflict(&self, context: OwnerMetadataContext) { + let OwnerMetadataContext { operation, owner, path, pubkey, view_enabled, wallet_id } = context; + drop(owner); + let inspected = ksp_wallet_lib::inspect_locked_wallet_file_v1(path.as_path()).await; + let session = self.wallet_session.lock(); + let mut session = match session { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let previous = std::mem::replace(&mut *session, crate::WalletSession::no_selection()); + match previous { + crate::WalletSession::OwnerOperation { + wallet_id: reserved_wallet_id, + path: reserved_path, + pubkey: reserved_pubkey, + view_enabled: reserved_view_enabled, + } if reserved_wallet_id == wallet_id && reserved_path == path && reserved_pubkey == pubkey && reserved_view_enabled == view_enabled => { + if let std::result::Result::Ok(locked_info) = inspected { + *session = crate::WalletSession::Locked { wallet_id: wallet_id.clone(), path, locked_info }; + } + ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), operation, "Wallet OWNER metadata state conflict purged the stale handle and forced reauthorization"); + }, + other => *session = other, + } + } + /// Refreshes the native SOL balance for the currently authorized VIEW/OWNER Wallet. pub(crate) async fn refresh_wallet_balance(&self) -> ksp_core_lib::Result { let context = self.authorized_balance_context(); @@ -702,10 +886,12 @@ impl AppState { crate::WalletSession::Owner { wallet_id, wallet, .. } => { std::result::Result::Ok(WalletBalanceContext { capability: "owner", pubkey: wallet.pubkey().to_owned(), wallet_id: wallet_id.clone() }) }, - crate::WalletSession::PrivilegedOperation { .. } => std::result::Result::Err(ksp_core_lib::Error::new( - crate::ERROR_CODE_WALLET_SESSION_INVALID, - "Wallet balance is unavailable while a privileged Wallet operation is running", - )), + crate::WalletSession::OwnerOperation { .. } | crate::WalletSession::PrivilegedOperation { .. } => { + std::result::Result::Err(ksp_core_lib::Error::new( + crate::ERROR_CODE_WALLET_SESSION_INVALID, + "Wallet balance is unavailable while a privileged Wallet operation is running", + )) + }, crate::WalletSession::Locked { .. } | crate::WalletSession::NoSelection => std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_WALLET_AUTHORIZATION_REQUIRED, "Wallet balance requires an authorized VIEW or OWNER session", @@ -726,7 +912,10 @@ impl AppState { crate::WalletSession::Owner { wallet_id, wallet, .. } => { context.capability == "owner" && wallet_id == &context.wallet_id && wallet.pubkey() == &context.pubkey }, - crate::WalletSession::NoSelection | crate::WalletSession::Locked { .. } | crate::WalletSession::PrivilegedOperation { .. } => false, + crate::WalletSession::NoSelection + | crate::WalletSession::Locked { .. } + | crate::WalletSession::OwnerOperation { .. } + | crate::WalletSession::PrivilegedOperation { .. } => false, }; } @@ -772,6 +961,15 @@ fn configured_secret_missing_error() -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_SECRET_CANDIDATES_MISSING, "No effective Config-owned Wallet password candidate is available"); } +struct OwnerMetadataContext { + operation: &'static str, + owner: std::boxed::Box, + path: std::path::PathBuf, + pubkey: ksp_core_lib::Pubkey, + view_enabled: bool, + wallet_id: String, +} + struct WalletBalanceContext { capability: &'static str, pubkey: ksp_core_lib::Pubkey, diff --git a/crates/ksp-app-wallet-desk/src/lib.rs b/crates/ksp-app-wallet-desk/src/lib.rs index f69d421..e17351b 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: 8 +// version: 9 //! Tauri desktop application shell for KSP Wallet management and inspection. @@ -23,6 +23,7 @@ mod wallet_balance; mod wallet_config; mod wallet_import; mod wallet_inventory; +mod wallet_metadata; mod wallet_secrets; mod wallet_session; @@ -186,6 +187,14 @@ 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; +/// OWNER alias update request carrying protected metadata only frontend -> Rust. +pub(crate) use self::wallet_metadata::WalletAliasUpdateRequestDto; +/// OWNER note append request carrying protected metadata only frontend -> Rust. +pub(crate) use self::wallet_metadata::WalletNoteAddRequestDto; +/// OWNER note delete request selecting one authorized stable note identifier. +pub(crate) use self::wallet_metadata::WalletNoteDeleteRequestDto; +/// OWNER note update request carrying protected metadata only frontend -> Rust. +pub(crate) use self::wallet_metadata::WalletNoteUpdateRequestDto; /// Discovers Config-owned Wallet password candidates without exposing their names or values. pub(crate) use self::wallet_secrets::discover_wallet_secret_candidates; /// Returns the configured Wallet password candidate count without exposing their names or values. diff --git a/crates/ksp-app-wallet-desk/src/tauri.rs b/crates/ksp-app-wallet-desk/src/tauri.rs index ff13256..c2c3ac3 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: 6 +// version: 7 //! Tauri runtime assembly for the KSP wallet desktop application. @@ -40,8 +40,10 @@ fn configure_plugins(builder: tauri::Builder) -> tauri::Builder) -> tauri::Builder { return builder.invoke_handler(tauri::generate_handler![ + add_wallet_note, clear_import_source, create_wallet, + delete_wallet_note, deselect_wallet, emit_frontend_log, get_runtime_status, @@ -57,6 +59,8 @@ fn configure_commands(builder: tauri::Builder) -> tauri::Builder) -> tauri::Builder, +) -> std::result::Result { + let result = state.add_wallet_note(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 clear_import_source(state: tauri::State<'_, crate::AppState>) -> std::result::Result<(), crate::CommandErrorDto> { let result = state.clear_import_source(); @@ -96,6 +112,18 @@ async fn create_wallet( }; } +#[tauri::command] +async fn delete_wallet_note( + request: crate::WalletNoteDeleteRequestDto, + state: tauri::State<'_, crate::AppState>, +) -> std::result::Result { + let result = state.delete_wallet_note(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(); @@ -286,3 +314,27 @@ async fn unlock_wallet_view_manual( std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), }; } + +#[tauri::command] +async fn update_wallet_alias( + request: crate::WalletAliasUpdateRequestDto, + state: tauri::State<'_, crate::AppState>, +) -> std::result::Result { + let result = state.update_wallet_alias(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] +async fn update_wallet_note( + request: crate::WalletNoteUpdateRequestDto, + state: tauri::State<'_, crate::AppState>, +) -> std::result::Result { + let result = state.update_wallet_note(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_metadata.rs b/crates/ksp-app-wallet-desk/src/wallet_metadata.rs new file mode 100644 index 0000000..2366c3d --- /dev/null +++ b/crates/ksp-app-wallet-desk/src/wallet_metadata.rs @@ -0,0 +1,48 @@ +// file: crates/ksp-app-wallet-desk/src/wallet_metadata.rs +// version: 1 + +//! OWNER-only protected metadata administration contracts for Wallet Desk. + +use ts_rs::TS; // rust-rules: trait-import + +/// OWNER request that updates or clears the protected internal Wallet alias. +#[derive(serde::Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_metadata/WalletAliasUpdateRequestDto.ts")] +pub(crate) struct WalletAliasUpdateRequestDto { + /// New protected alias, or `None` to clear it. + pub(crate) alias: std::option::Option, +} + +/// OWNER request that appends one protected Wallet note. +#[derive(serde::Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_metadata/WalletNoteAddRequestDto.ts")] +pub(crate) struct WalletNoteAddRequestDto { + /// Protected note text transported only for the explicit mutation call. + pub(crate) text: String, +} + +/// OWNER request that updates one protected Wallet note by stable identifier. +#[derive(serde::Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_metadata/WalletNoteUpdateRequestDto.ts")] +pub(crate) struct WalletNoteUpdateRequestDto { + /// Stable note identifier returned by the authorized Wallet projection. + pub(crate) note_id: String, + /// Replacement protected note text transported only for the explicit mutation call. + pub(crate) text: String, +} + +/// OWNER request that deletes one protected Wallet note by stable identifier. +#[derive(serde::Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_metadata/WalletNoteDeleteRequestDto.ts")] +pub(crate) struct WalletNoteDeleteRequestDto { + /// Stable note identifier returned by the authorized Wallet projection. + pub(crate) note_id: String, +} + +#[cfg(test)] +#[path = "../unit_tests/wallet_metadata.rs"] +mod tests; diff --git a/crates/ksp-app-wallet-desk/src/wallet_session.rs b/crates/ksp-app-wallet-desk/src/wallet_session.rs index 38bd688..e59feac 100644 --- a/crates/ksp-app-wallet-desk/src/wallet_session.rs +++ b/crates/ksp-app-wallet-desk/src/wallet_session.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-wallet-desk/src/wallet_session.rs -// version: 3 +// version: 4 //! Durable root-scoped Wallet session lifecycle for Wallet Desk. @@ -149,6 +149,8 @@ pub(crate) enum WalletSession { path: std::path::PathBuf, /// Authorized VIEW handle retaining metadata capability only in Rust. wallet: std::boxed::Box, + /// Whether the authenticated Wallet currently exposes a VIEW slot. + view_enabled: bool, }, /// One Wallet kept open with OWNER capability. Owner { @@ -158,6 +160,19 @@ pub(crate) enum WalletSession { path: std::path::PathBuf, /// Authorized OWNER handle retaining secret material only in Rust. wallet: std::boxed::Box, + /// Whether the authenticated Wallet currently exposes a VIEW slot. + view_enabled: bool, + }, + /// One OWNER metadata mutation temporarily owns the authorized handle outside the session mutex. + OwnerOperation { + /// Native Wallet filename / root-scoped identifier. + wallet_id: String, + /// Full root-scoped filesystem path retained only in Rust. + path: std::path::PathBuf, + /// Authorized Solana identity used only to reject stale completion. + pubkey: ksp_core_lib::Pubkey, + /// Whether the authenticated Wallet currently exposes a VIEW slot. + view_enabled: bool, }, } @@ -200,6 +215,11 @@ impl WalletSession { state: WalletSessionStateDto::OwnerOpen, wallet_id: std::option::Option::Some(wallet_id.clone()), }, + Self::OwnerOperation { wallet_id, .. } => WalletSessionDto { + filename: std::option::Option::Some(wallet_id.clone()), + state: WalletSessionStateDto::PrivilegedOperation, + wallet_id: std::option::Option::Some(wallet_id.clone()), + }, }; } } diff --git a/crates/ksp-app-wallet-desk/tauri.conf.json b/crates/ksp-app-wallet-desk/tauri.conf.json index 49809a5..3e21558 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.8.fix.1", + "version": "0.2.6-pre.9", "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 264dff3..5d5bb85 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: 8 +// version: 9 //! Desktop build, shell and Config-status contract audits for Wallet Desk. @@ -223,3 +223,33 @@ fn pre_008_import_uses_rust_native_picker_and_wallet_transfer_adapters() { assert!(main.contains("WalletTransferInspectionDto")); assert!(main.contains("bindImportWalletForm")); } + +#[test] +fn pre_009_owner_metadata_administration_uses_wallet_owner_and_refreshes_projection() { + let root = app_root(); + let state = read_text(root.join("src/app_state.rs").as_path()); + let metadata = read_text(root.join("src/wallet_metadata.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("owner.update_alias")); + assert!(state.contains("owner.add_note")); + assert!(state.contains("owner.update_note")); + assert!(state.contains("owner.delete_note")); + assert!(state.contains("ERROR_CODE_STATE_CONFLICT")); + assert!(session.contains("OwnerOperation")); + assert!(metadata.contains("WalletAliasUpdateRequestDto")); + assert!(metadata.contains("WalletNoteAddRequestDto")); + assert!(metadata.contains("WalletNoteUpdateRequestDto")); + assert!(metadata.contains("WalletNoteDeleteRequestDto")); + assert!(tauri.contains("update_wallet_alias")); + assert!(tauri.contains("add_wallet_note")); + assert!(tauri.contains("update_wallet_note")); + assert!(tauri.contains("delete_wallet_note")); + assert!(html.contains("id=\"ownerWalletAlias\"")); + assert!(html.contains("id=\"ownerNotesList\"")); + assert!(html.contains("id=\"deleteWalletNoteModal\"")); + assert!(main.contains("runOwnerMetadataMutation")); + assert!(main.contains("recoverOwnerMetadataStateConflict")); +} diff --git a/crates/ksp-app-wallet-desk/tests/desktop_security.rs b/crates/ksp-app-wallet-desk/tests/desktop_security.rs index 0924149..6cffa57 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: 8 +// version: 9 //! Static desktop security contracts for the Wallet Desk pre.002 shell. @@ -162,3 +162,24 @@ fn import_picker_path_and_transfer_secret_never_cross_into_frontend() { assert!(command.contains("WalletImportSourceRequestDto")); assert!(!command.contains("source_path")); } + +#[test] +fn owner_metadata_values_are_request_only_and_state_conflict_forces_reauthorization() { + let root = app_root(); + let metadata = read_text(root.join("src/wallet_metadata.rs").as_path()); + let state = read_text(root.join("src/app_state.rs").as_path()); + let main = read_text(root.join("frontend/ts/main.ts").as_path()); + assert!(!metadata.contains("derive(Clone")); + assert!(!metadata.contains("derive(Debug")); + assert!(state.contains("recover_owner_metadata_state_conflict")); + assert!(state.contains("inspect_locked_wallet_file_v1")); + assert!(state.contains("ERROR_CODE_STATE_CONFLICT")); + assert!(main.contains("isWalletStateConflict")); + assert!(main.contains("select_wallet")); + assert!(main.contains("clearAuthorizedProjection")); + assert!(!main.contains("frontendDebug(\"main\", request.text")); + assert!(!main.contains("frontendTrace(\"main\", request.text")); + assert!(!main.contains("frontendDebug(\"main\", request.alias")); + assert!(!main.contains("window.confirm")); + assert!(main.contains("new Modal(modalElement)")); +} diff --git a/crates/ksp-app-wallet-desk/unit_tests/wallet_metadata.rs b/crates/ksp-app-wallet-desk/unit_tests/wallet_metadata.rs new file mode 100644 index 0000000..ecc3d52 --- /dev/null +++ b/crates/ksp-app-wallet-desk/unit_tests/wallet_metadata.rs @@ -0,0 +1,23 @@ +// file: crates/ksp-app-wallet-desk/unit_tests/wallet_metadata.rs +// version: 1 + +#[test] +fn owner_metadata_requests_keep_protected_text_request_only() { + fn assert_deserializable() + where + T: serde::de::DeserializeOwned, + { + return; + } + assert_deserializable::(); + assert_deserializable::(); + assert_deserializable::(); + assert_deserializable::(); +} + +#[test] +fn wallet_metadata_limits_are_owned_by_wallet_library() { + assert_eq!(ksp_wallet_lib::KSPWALLET_V1_MAX_ALIAS_BYTES, 256); + assert_eq!(ksp_wallet_lib::KSPWALLET_V1_MAX_NOTES, 64); + assert_eq!(ksp_wallet_lib::KSPWALLET_V1_MAX_NOTE_TEXT_BYTES, 8 * 1024); +} diff --git a/deltas/0.2.6/pre.009.md b/deltas/0.2.6/pre.009.md new file mode 100644 index 0000000..c91e820 --- /dev/null +++ b/deltas/0.2.6/pre.009.md @@ -0,0 +1,231 @@ + + + +# Delta `0.2.6-pre.009` — administration metadata OWNER + recovery `wallet.state_conflict` + +## Base requise + +```text +0.2.6-pre.008-fix.001 appliquée +workspace.package.version = 0.2.6-pre.8.fix.1 +``` + +La base opérateur est propre : `fmt`, audit, `check`, Clippy sans warning, tests Wallet Desk et workspace complet sont verts. Le runtime d'import natif a déjà été validé sur la tranche précédente. + +## Type de livraison + +```text +ksp-general-0.2.6-pre.009.zip +``` + +Le signal technique devient : + +```text +workspace.package.version = 0.2.6-pre.9 +commit = v0.2.6-pre.009 +``` + +`ksp-app-wallet-desk/package.json` et `tauri.conf.json` suivent `0.2.6-pre.9`. + +## Objectif + +Cette tranche ouvre l'administration des metadata protégées du wallet exclusivement pour une session OWNER : + +```text +alias update / clear +note add +note update +note delete +projection autorisée rafraîchie après succès +wallet.state_conflict => purge du handle stale + réinspection Locked + réautorisation obligatoire +``` + +VIEW continue d'exposer Pubkey/alias/notes en lecture seule. Locked ne reçoit toujours aucune identité protégée. + +## Contrats frontend -> Rust + +Nouveaux DTOs request-only : + +```text +WalletAliasUpdateRequestDto +WalletNoteAddRequestDto +WalletNoteUpdateRequestDto +WalletNoteDeleteRequestDto +``` + +Ils transportent alias, texte ou identifiant de note uniquement pendant la commande explicite. Ils ne dérivent ni `Debug` ni `Clone` et aucune valeur de metadata n'est journalisée. + +## Délégation Wallet + +Wallet Desk ne réimplémente aucune mutation. Les commandes appellent directement : + +```text +WalletOwner::update_alias +WalletOwner::add_note +WalletOwner::update_note +WalletOwner::delete_note +``` + +Les bornes V1 restent autoritativement validées dans `ksp-wallet-lib` : alias 256 octets UTF-8, 64 notes maximum, 8 KiB par texte de note. + +## Lifecycle OWNER async + +Pendant une mutation persistante, le `WalletOwner` est déplacé hors du mutex de session avant l'`await` et remplacé par une réservation Rust-only : + +```text +Owner + -> OwnerOperation + -> Owner succès ou erreur non conflictuelle encore compatible + -> Locked wallet.state_conflict + réinspection valide + -> NoSelection conflit + réinspection impossible +``` + +La réservation conserve uniquement `wallet_id`, chemin Rust-only, Pubkey et état VIEW afin de détecter une complétion stale. Une sélection/désélection concurrente empêche la réinstallation de l'ancien handle. + +## Recovery `wallet.state_conflict` + +Un conflit d'état ne déclenche jamais de retry aveugle avec l'OWNER authentifié sur une version devenue ancienne. + +Le backend : + +```text +reçoit wallet.state_conflict +-> détruit WalletOwner stale +-> inspect_locked_wallet_file_v1 sur le fichier courant +-> revient au plus à Locked +``` + +Le frontend : + +```text +purge Pubkey / alias / notes / balance +-> réinspecte explicitement le wallet sélectionné +-> revient à Locked +-> ouvre la vue Security +-> impose un nouvel unlock OWNER +``` + +## UI + +La vue Details reçoit une carte `Administration metadata OWNER` : + +```text +alias Save / Clear +nouvelle note / Add note +édition Save note +Delete note +``` + +Les contrôles sont désactivés hors OWNER. La suppression utilise un modal Bootstrap intégré ; aucun `window.confirm`, `window.alert` ou `window.prompt` n'est introduit. Le contenu de la note n'est pas recopié dans le modal de confirmation. + +## Correction du workflow documentaire + +Cette tranche applique également la séparation documentaire demandée : + +- `ROADMAP.md` est ramené à une ligne synthétique globale pour `0.2.6` et ne sera plus modifié à chaque prerelease ; +- `CHANGELOG.md` reste inchangé et réservé à la clôture ; +- `docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md` est restructuré : état acquis condensé + trajectoire restante lisible, au lieu d'une accumulation sur une seule ligne ; +- `docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md` reste le plan détaillé de la série. + +La trajectoire est étendue ainsi : + +```text +pre.013 intégration / compliance / sécurité / smoke +pre.014 polish Bootstrap + splashscreen, détails opérateur à préciser +pre.015 documentation finale / prompt 0.2.7 / cargo tauri build en dernière opération +rel.001 +``` + +`pre.014` inclura notamment le défaut de scrollbars occasionnelles du splashscreen, sans préjuger des autres corrections visuelles qui seront précisées au moment de l'ouverture de la tranche. + +## Fichiers ajoutés + +```text +crates/ksp-app-wallet-desk/src/wallet_metadata.rs +crates/ksp-app-wallet-desk/unit_tests/wallet_metadata.rs +deltas/0.2.6/pre.009.md +``` + +## Fichiers modifiés + +```text +Cargo.toml +ROADMAP.md +crates/ksp-app-wallet-desk/frontend/main.html +crates/ksp-app-wallet-desk/frontend/ts/main.ts +crates/ksp-app-wallet-desk/package.json +crates/ksp-app-wallet-desk/src/app_state.rs +crates/ksp-app-wallet-desk/src/lib.rs +crates/ksp-app-wallet-desk/src/tauri.rs +crates/ksp-app-wallet-desk/src/wallet_session.rs +crates/ksp-app-wallet-desk/tauri.conf.json +crates/ksp-app-wallet-desk/tests/desktop_contract.rs +crates/ksp-app-wallet-desk/tests/desktop_security.rs +docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md +docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md +``` + +## Fichiers supprimés + +```text +aucun +``` + +## Hors scope + +Cette tranche ne réalise pas encore : + +```text +rotation OWNER / VIEW pre.010 +disable/recreate VIEW fort pre.011 +export OWNER fichier pre.012 +polish final Bootstrap / splash pre.014 +build Tauri final pre.015, dernière opération +``` + +Aucune nouvelle dépendance Cargo ou npm n'est ajoutée. + +## Contrôles assistant + +```text +General Rust rule audit clean +Rust export completeness audit clean +KSP workspace Rust rule audit clean +headers de fichiers modifiés +1 exact +TOML / JSON parse clean +CHANGELOG / README / RULES inchangés +frontend : aucun dialogue navigateur natif clean +frontend : diagnostics TypeScript locaux aucun diagnostic hors résolution de modules sans node_modules +``` + +Cargo n'est pas disponible dans l'environnement de génération ; compilation, Clippy et tests restent autoritatifs côté opérateur. + +## 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-pre009 \ + cargo tauri dev -c crates/ksp-app-wallet-desk/tauri.conf.json +``` + +Parcours runtime conseillé : + +```text +OWNER ouvert -> Details +alias Save puis Clear +Add note +Save note +Delete note via modal Bootstrap +Lock puis VIEW unlock -> metadata visible mais contrôles OWNER désactivés +Deselect/Lock -> projection protégée purgée +``` + +Canari de conflit recommandé : ouvrir OWNER, modifier/remplacer le fichier wallet par un état authentifié plus récent hors de ce handle, puis tenter une mutation. Le résultat attendu est `wallet.state_conflict`, purge de l'OWNER stale, retour Locked et nouvel unlock OWNER obligatoire. + +Aucune commande npm directe n'est nécessaire. `cargo tauri build -c crates/ksp-app-wallet-desk/tauri.conf.json` reste réservé à `pre.015` et doit rester la toute dernière opération de validation de la release. diff --git a/docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md b/docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md index 4e7733f..a9ebb27 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 @@ -422,15 +422,31 @@ La release fournit `docs/formats/KSPWALLET_V1.md` comme spécification séparée ## `0.2.6` — Wallet Desk -Mission : valider Config composite + `.kspwallet` + transport HTTP dans une application Tauri mince. +Mission : créer `ksp-app-wallet-desk` comme application Tauri mince de composition `Config + Wallet + Transport HTTP`, sans déplacer la cryptographie Wallet, la résolution Config ni le transport Solana dans le frontend. Le plan actif et le découpage fin restent dans `docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md`. -Le solde d'un wallet constitue un premier cas de validation réseau obligatoire. L'application garde les handles VIEW/OWNER côté Rust, ne persiste aucun password dans les documents JSON Config ni dans le frontend et délègue toute crypto/signature/administration à `ksp-wallet-lib`. Des secrets opérateur `KSP_SECRET_WALLET_PASS_*` peuvent être fournis par process env ou `.env`, exclusivement via les APIs Config et sans traverser le frontend. +État fonctionnel déjà acquis : -Le prompt [`../../prompts/011-V0_2_6_START_PROMPT.md`](../../prompts/011-V0_2_6_START_PROMPT.md), préparé par `0.2.5-pre.010`, impose un `pre.001` d'audit/sizing couvrant Config `std.wallet`/composite, inventory/path safety, lifecycle VIEW/OWNER, balance `getBalance`, bridge Tauri/TS-RS, sécurité password/export, logging et validation finale Tauri. +- `pre.001`–`pre.004` : sizing, shell desktop, `std.wallet`, composite Wallet Desk, répertoires Config et inventory locked `.kspwallet` root-scoped ; +- `pre.005`–`pre.006` : création native, `WalletSession`, unlock VIEW/OWNER manuel et via candidats `KSP_SECRET_WALLET_PASS_*` résolus exclusivement par Config ; +- `pre.007` : composition du pool HTTP et `getBalance` depuis la Pubkey d'un handle VIEW/OWNER Rust, sans Pubkey ni endpoint fournis par le frontend ; +- `pre.008` : import Solana CLI JSON/Base58 via picker natif Rust, staging borné zéroïsable et publication no-clobber vers un nouveau `.kspwallet`. -`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`. +Les correctifs de ces tranches restent tracés dans `deltas/0.2.6/` et ne sont pas dupliqués ici. -`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.6-pre.005-fix.001` ferme le premier gate opérateur sans changer ce contrat : `WalletOwner` est boxed dans la variante OWNER pour éviter un `WalletSession` de plus de 1 KiB et le test statique de sécurité initialise correctement son offset `request_start`. `0.2.6-pre.006` matérialise les unlock manuels VIEW/OWNER et les secrets configurés : Config découvre les variables `KSP_SECRET_WALLET_PASS_*`, Wallet Desk ordonne les candidats par filename normalisé puis labels numériques puis nommés, ne révèle chaque valeur qu’au moment d’une tentative explicitement demandée, conserve noms/suffixes/valeurs hors IPC/logs, et protège les KDF concurrents avec `PrivilegedOperation` avant d’installer `WalletView` ou `WalletOwner` uniquement en Rust. `0.2.6-pre.006-fix.001` corrige les deux canaris statiques de `desktop_security` qui recherchaient le token `password` dans toute la définition textuelle de `WalletAuthorizedDto` et confondaient ainsi la documentation sûre de `configured_secret_candidate_count` avec un champ secret ; les assertions sont désormais bornées aux lignes de champs Rust de la projection. `0.2.6-pre.007` ferme le MVP obligatoire Config composite + Wallet + Transport : `resolve_transport_config_profile` préserve la provenance `Composite`, Wallet Desk construit le pool HTTP au bootstrap et valide statiquement une route `getBalance` sur le rôle `default`, puis `refresh_wallet_balance` copie uniquement la Pubkey depuis un handle VIEW/OWNER avant de libérer le mutex session et d'effectuer l'I/O. La réponse expose lamports exacts, SOL formaté sans flottant, slot, `api_version`, profil/rôle Transport ; une complétion devenue stale après lock/deselect/changement de session est rejetée. Les diagnostics Transport restent limités à profile/cluster/provider/compteurs, sans URL. `0.2.6-pre.007-fix.001` corrige uniquement son canari cross-crates : `config_composition.rs` utilisait par erreur `ConfigEnvironment::from_maps`, API `pub(crate)` réservée aux unit tests de Config. Le canari charge désormais l'environnement via `ConfigEnvironment::load`, surface publique également utilisée par le runtime. `0.2.6-pre.007-fix.002` retire ensuite du message d'assertion la tentative de formatage `Debug` du `Result` : `ConfigEnvironment` reste volontairement non-`Debug`, et aucun contrat runtime n'est élargi pour les besoins du test. `0.2.6-pre.008` ajoute ensuite l'import Solana CLI JSON/Base58 via un picker natif exclusivement Rust : `tauri-plugin-dialog` reste backend-only, aucun chemin source ni contenu secret ne traverse IPC, les octets sont lus avec une borne stricte puis conservés dans `Zeroizing>`, l'inspection ne projette que basename/format/Pubkey, et `import_wallet_transfer_v1` publie no-clobber sous la racine Config avec de nouveaux credentials OWNER/VIEW avant d'ouvrir la session OWNER. `0.2.6-pre.008-fix.001` ferme ensuite les deux warnings Clippy de cette tranche avec `Option::replace` pour le slot de staging et l’auto-deref sur `Zeroizing>`; aucun contrat runtime, format d’import, secret ou frontend n’est modifié. +Trajectoire restante : + +```text +pre.009 administration OWNER alias/notes + recovery wallet.state_conflict +pre.010 rotations OWNER/VIEW +pre.011 disable/recreate VIEW fort +pre.012 export OWNER fichier CLI JSON/Base58 +pre.013 intégration, compliance, sécurité et smoke +pre.014 polish du gabarit Bootstrap et du splashscreen +pre.015 documentation finale, validations, prompt 0.2.7 et cargo tauri build en dernière opération +rel.001 publication stable 0.2.6 +``` + +La tranche `pre.014` est réservée aux défauts visuels/templating observés en usage réel, notamment les scrollbars occasionnelles du splashscreen ; son contenu précis sera borné à partir du retour opérateur avant implémentation. `ROADMAP.md` reste synthétique et `CHANGELOG.md` n'est synchronisé qu'à la phase documentaire finale. ## `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 32485a1..6a994dd 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 @@ -1487,17 +1487,25 @@ Aucun changement n’est apporté au picker natif, à `Zeroizing>`, aux ### `pre.009` — metadata OWNER -Objectifs : +Statut : **implémenté dans `0.2.6-pre.009`, validations opérateur à exécuter**. + +Objectifs matérialisés : ```text -update alias +update alias / clear alias notes add/update/delete -controls OWNER -projection refresh -wallet.state_conflict UX -DataTable/details coherence après mutation +controls strictement OWNER +projection autorisée rafraîchie après mutation +aucun mutex std::sync tenu pendant l'I/O async Wallet +wallet.state_conflict => purge handle stale + réinspection Locked + re-unlock obligatoire +DataTable/details cohérents après mutation +confirmation Bootstrap pour suppression de note ``` +Les mutations délèguent directement à `WalletOwner::{update_alias, add_note, update_note, delete_note}`. Pendant la persistence async, le handle OWNER est déplacé hors du mutex `AppState` et remplacé par un état de réservation `OwnerOperation`; une sélection/désélection concurrente empêche donc la réinstallation d'un handle devenu obsolète. Les alias/textes de note ne sont jamais journalisés. + +Un `wallet.state_conflict` n'est jamais retenté avec le même handle : Wallet Desk détruit l'OWNER stale, réinspecte le fichier courant sans KDF, revient au plus à `Locked`, puis le frontend purge Pubkey/alias/notes/balance et impose une nouvelle autorisation OWNER. + ### `pre.010` — rotations credentials Objectifs : @@ -1556,7 +1564,21 @@ workspace checkpoint complet Cette tranche doit produire la preuve que les surfaces ajoutées fonctionnent ensemble avant la documentation de clôture. -### `pre.014` — documentation finale, candidate build et prompt suivant +### `pre.014` — polish gabarit Bootstrap et splashscreen + +Tranche explicitement réservée au retour visuel/opérateur avant la clôture. Le détail sera borné au moment de l'ouverture de cette tranche ; la liste actuelle est volontairement indicative : + +```text +corrections du templating Bootstrap Wallet Desk +cohérence sizing/overflow des conteneurs desktop +splashscreen : suppression des scrollbars occasionnelles +régression visuelle splash -> main +réutilisation du gabarit commun sans déplacer de logique métier +``` + +Cette tranche n'est pas un prétexte pour refondre l'UX métier. Les détails précis des défauts et critères d'acceptation seront fournis par l'opérateur lorsqu'elle sera ouverte. + +### `pre.015` — documentation finale, candidate build et prompt suivant Dernière tranche prévue avant `rel.001` : @@ -1574,7 +1596,7 @@ parcours fonctionnel cargo tauri dev final cargo tauri build en toute dernière opération ``` -Si le build final ou la documentation révèle une correction technique, utiliser `pre.014-fix.NNN` ou insérer une tranche supplémentaire. `pre.014` n'est pas une obligation de clôture artificielle. +Si le build final ou la documentation révèle une correction technique, utiliser `pre.015-fix.NNN` ou insérer une tranche supplémentaire. `pre.015` n'est pas une obligation de clôture artificielle. ### `rel.001` — publication stable