v0.2.6-pre.006

This commit is contained in:
2026-08-21 10:01:11 +02:00
parent 63e711629f
commit a207cf7161
21 changed files with 1136 additions and 63 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 171
# version: 172
[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.5.fix.1"
version = "0.2.6-pre.6"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md -->
<!-- version: 67 -->
<!-- version: 68 -->
# Roadmap KSP
@@ -50,7 +50,7 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
- [X] `0.2.3` — HTTP Transactions stable : 11/11 wrappers typés publiés, classification `8 Read / 2 WriteSubmission / 1 Simulation`, no-resend ambigu prouvé pour les write submissions, `KSP-TRANSPORT-007` réaudité conforme sur les 37 wrappers HTTP courants, graphes Cargo et deux smokes Devnet validés ; `0.2.4` reprend les 15 Blocks/Economics restants.
- [X] `0.2.4` — HTTP Blocks + Economics stable : 15/15 wrappers `V0_2_4` publiés, surface typed complète à 52/52 méthodes courantes, 14/14 historiques conservées, réaudit SIMD/inventaire final et `KSP-TRANSPORT-007` global validés ; deux smokes Devnet passés avant publication.
- [X] `0.2.5` — Wallet foundation stable : `.kspwallet` V1, VIEW/OWNER indépendants, Argon2id/XChaCha20-Poly1305, autorité Ed25519 OWNER, persistence no-clobber, signature, administration/rotations/révocation VIEW forte, import/export Solana CLI JSON + Base58, canaris adversariaux, interop externe et documentation durable publiés. La clôture `pre.010-fix.001``fix.003` ajoute `ed25519-dalek 3.0.0` direct, normalise le Rust workspace et installe laudit structurel Python complémentaire à rustfmt/Clippy. `Pubkey` reste via `ksp-core-lib`, la keypair reste encapsulée dans Wallet et Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet.
- [ ] `0.2.6` — 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 didentifiants 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 denvironnement 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 linventory 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 dinventory. `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. 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.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 didentifiants 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 denvironnement 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 linventory 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 dinventory. `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. 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.

View File

@@ -40,9 +40,9 @@
<div class="d-flex align-items-center justify-content-between mb-4">
<div>
<h1 id="viewTitle" class="h3 mb-1">Dashboard</h1>
<p class="text-body-secondary mb-0">Inventaire `.kspwallet` root-scoped et inspection locked actifs.</p>
<p class="text-body-secondary mb-0">Création native, sessions durables et unlock explicite VIEW/OWNER actifs.</p>
</div>
<span class="badge text-bg-secondary">0.2.6-pre.005</span>
<span class="badge text-bg-secondary">0.2.6-pre.006</span>
</div>
<section data-view-panel="dashboard">
<div class="row g-3">
@@ -76,6 +76,7 @@
<dt class="col-5">État</dt><dd id="currentWalletState" class="col-7"></dd>
<dt class="col-5">Format</dt><dd id="currentWalletFormat" class="col-7"></dd>
<dt class="col-5">VIEW</dt><dd id="currentWalletView" class="col-7"></dd>
<dt class="col-5">Secrets Config</dt><dd id="currentWalletSecretCandidates" class="col-7">0</dd>
<dt class="col-5">Pubkey</dt><dd id="currentWalletPubkey" class="col-7 text-break"></dd>
<dt class="col-5">Alias</dt><dd id="currentWalletAlias" class="col-7 text-break"></dd>
<dt class="col-5">Notes</dt><dd id="currentWalletNotes" class="col-7 text-break"></dd>
@@ -159,14 +160,57 @@
</div>
</section>
<section data-view-panel="details" hidden><div class="app-placeholder"><i class="fa-solid fa-id-card"></i><p>Les détails protégés seront affichés uniquement après VIEW/OWNER.</p></div></section>
<section data-view-panel="security" hidden><div class="app-placeholder"><i class="fa-solid fa-shield-halved"></i><p>Les opérations OWNER seront ajoutées après le MVP identité + balance.</p></div></section>
<section data-view-panel="security" hidden>
<div class="row g-3">
<div class="col-xl-7">
<div class="card shadow-sm">
<div class="card-header fw-semibold">Unlock manuel du wallet sélectionné</div>
<div class="card-body">
<p class="text-body-secondary">Chaque tentative Argon2 est explicite et peut prendre plusieurs secondes. Le password est envoyé uniquement frontend → Rust puis purgé.</p>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label" for="unlockWalletViewPassword">VIEW password</label>
<div class="input-group">
<input id="unlockWalletViewPassword" class="form-control" type="password" autocomplete="current-password">
<button id="unlockViewManual" class="btn btn-outline-primary" type="button" disabled>Unlock VIEW</button>
</div>
</div>
<div class="col-md-6">
<label class="form-label" for="unlockWalletOwnerPassword">OWNER password</label>
<div class="input-group">
<input id="unlockWalletOwnerPassword" class="form-control" type="password" autocomplete="current-password">
<button id="unlockOwnerManual" class="btn btn-outline-danger" type="button" disabled>Unlock OWNER</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-5">
<div class="card shadow-sm">
<div class="card-header fw-semibold">Secrets configurés</div>
<div class="card-body">
<p class="mb-2">Candidats effectifs : <strong id="unlockSecretCandidateCount">0</strong></p>
<p class="small text-body-secondary">Les noms, suffixes et valeurs des secrets Wallet configurés restent exclusivement côté Rust/Config. Cette action tente les candidats uniquement sur demande.</p>
<div class="d-grid gap-2">
<button id="unlockViewConfigured" class="btn btn-outline-primary" type="button" disabled>Unlock VIEW avec secret configuré</button>
<button id="unlockOwnerConfigured" class="btn btn-outline-danger" type="button" disabled>Unlock OWNER avec secret configuré</button>
</div>
</div>
</div>
</div>
<div class="col-12">
<div id="unlockStatus" class="alert alert-secondary mb-0" role="status">Sélectionner un wallet verrouillé.</div>
</div>
</div>
</section>
<section data-view-panel="diagnostics" hidden><div class="app-placeholder"><i class="fa-solid fa-stethoscope"></i><p>Les diagnostics Config/Wallet/Transport seront enrichis progressivement.</p></div></section>
</div>
</section>
</div>
</main>
<footer class="app-footer bg-dark text-white-50 d-flex align-items-center px-4">
<span>KSP Wallet Desk · create + session lifecycle pre.005</span>
<span>KSP Wallet Desk · manual/configured unlock pre.006</span>
</footer>
<script type="module" src="./ts/main.ts"></script>
</body>

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/frontend/ts/main.ts
// version: 5
// version: 6
import "bootstrap";
import DataTable from "datatables.net-bs5";
@@ -14,6 +14,7 @@ import type { WalletSelectionRequestDto } from "./bindings/ksp_app_wallet_desk/w
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 type { WalletUnlockRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_session/WalletUnlockRequestDto.ts";
import { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log";
import { invokeKsp } from "./invoke";
import "../sass/main.scss";
@@ -22,7 +23,8 @@ import "../sass/main.scss";
installFrontendConsoleBridge("main");
type ViewId = "dashboard" | "wallets" | "create-import" | "details" | "security" | "diagnostics";
type ActiveSessionState = "no_selection" | "locked" | "owner_open";
type ActiveSessionState = "no_selection" | "locked" | "privileged_operation" | "view_open" | "owner_open";
type UnlockCapability = "view" | "owner";
const viewTitles: Record<ViewId, string> = {
dashboard: "Dashboard",
@@ -35,6 +37,8 @@ const viewTitles: Record<ViewId, string> = {
let activeWalletId: string | null = null;
let activeSessionState: ActiveSessionState = "no_selection";
let activeViewEnabled = false;
let configuredSecretCandidateCount = 0;
function isViewId(value: string): value is ViewId {
return value in viewTitles;
@@ -121,8 +125,8 @@ function renderWalletInventoryRow(entry: WalletInventoryEntryDto): HTMLTableRowE
row.dataset.inspectionStatus = entry.inspectionStatus;
const state = document.createElement("td");
if (entry.state === "locked") {
if (activeWalletId === entry.walletId && activeSessionState === "owner_open") {
appendIconText(state, "fa-lock-open", "OWNER");
if (activeWalletId === entry.walletId && (activeSessionState === "view_open" || activeSessionState === "owner_open")) {
appendIconText(state, "fa-lock-open", activeSessionState === "view_open" ? "VIEW" : "OWNER");
} else {
appendIconText(state, "fa-lock", "Locked");
}
@@ -165,7 +169,7 @@ function initializeWalletTable(): void {
zeroRecords: "Aucun wallet correspondant.",
},
});
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.005-wallet-session-create" });
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.006-wallet-unlock" });
}
function renderWalletInventory(entries: WalletInventoryEntryDto[]): void {
@@ -192,11 +196,33 @@ function updateSessionActions(): void {
const lock = document.querySelector<HTMLButtonElement>('[data-shell-action="lock"]');
const deselect = document.querySelector<HTMLButtonElement>('[data-shell-action="deselect"]');
if (lock) {
lock.disabled = activeSessionState !== "owner_open";
lock.disabled = activeSessionState !== "view_open" && activeSessionState !== "owner_open";
}
if (deselect) {
deselect.disabled = activeSessionState === "no_selection";
deselect.disabled = activeSessionState === "no_selection" || activeSessionState === "privileged_operation";
}
updateUnlockActions();
}
function updateUnlockActions(): void {
const locked = activeSessionState === "locked";
const viewManual = document.querySelector<HTMLButtonElement>("#unlockViewManual");
const ownerManual = document.querySelector<HTMLButtonElement>("#unlockOwnerManual");
const viewConfigured = document.querySelector<HTMLButtonElement>("#unlockViewConfigured");
const ownerConfigured = document.querySelector<HTMLButtonElement>("#unlockOwnerConfigured");
if (viewManual) {
viewManual.disabled = !locked || !activeViewEnabled;
}
if (ownerManual) {
ownerManual.disabled = !locked;
}
if (viewConfigured) {
viewConfigured.disabled = !locked || !activeViewEnabled || configuredSecretCandidateCount === 0;
}
if (ownerConfigured) {
ownerConfigured.disabled = !locked || configuredSecretCandidateCount === 0;
}
setText("#unlockSecretCandidateCount", configuredSecretCandidateCount.toString());
}
function clearCreateFormSensitiveInputs(): void {
@@ -218,16 +244,30 @@ function clearCreateFormSensitiveInputs(): void {
}
}
function clearUnlockSensitiveInputs(): void {
const viewPassword = document.querySelector<HTMLInputElement>("#unlockWalletViewPassword");
const ownerPassword = document.querySelector<HTMLInputElement>("#unlockWalletOwnerPassword");
if (viewPassword) {
viewPassword.value = "";
}
if (ownerPassword) {
ownerPassword.value = "";
}
}
function clearAuthorizedProjection(): void {
setText("#currentWalletPubkey", "—");
setText("#currentWalletAlias", "—");
setText("#currentWalletNotes", "—");
clearCreateFormSensitiveInputs();
clearUnlockSensitiveInputs();
}
function clearSelectedWallet(): void {
activeWalletId = null;
activeSessionState = "no_selection";
activeViewEnabled = false;
configuredSecretCandidateCount = 0;
const icon = document.querySelector<HTMLElement>("#currentWalletIcon");
if (icon) {
icon.className = "fa-solid fa-lock fa-2x mb-3 text-body-secondary";
@@ -236,6 +276,8 @@ function clearSelectedWallet(): void {
setText("#currentWalletState", "—");
setText("#currentWalletFormat", "—");
setText("#currentWalletView", "—");
setText("#currentWalletSecretCandidates", "0");
setText("#unlockStatus", "Sélectionner un wallet verrouillé.");
clearAuthorizedProjection();
updateSessionActions();
}
@@ -243,6 +285,8 @@ function clearSelectedWallet(): void {
function renderLockedWallet(wallet: LockedWalletDto): void {
activeWalletId = wallet.walletId;
activeSessionState = "locked";
activeViewEnabled = wallet.viewEnabled;
configuredSecretCandidateCount = wallet.configuredSecretCandidateCount;
clearAuthorizedProjection();
const icon = document.querySelector<HTMLElement>("#currentWalletIcon");
if (icon) {
@@ -252,26 +296,45 @@ function renderLockedWallet(wallet: LockedWalletDto): void {
setText("#currentWalletState", "Locked");
setText("#currentWalletFormat", wallet.formatVersion.toString());
setText("#currentWalletView", wallet.viewEnabled ? "enabled" : "disabled");
setText("#currentWalletSecretCandidates", configuredSecretCandidateCount.toString());
setText("#unlockStatus", "Prêt pour une tentative explicite. Argon2 peut prendre plusieurs secondes.");
updateSessionActions();
frontendDebug("main", "Locked Wallet selection rendered", { walletId: wallet.walletId, formatVersion: wallet.formatVersion, viewEnabled: wallet.viewEnabled });
frontendDebug("main", "Locked Wallet selection rendered", {
configuredSecretCandidateCount,
formatVersion: wallet.formatVersion,
viewEnabled: wallet.viewEnabled,
walletId: wallet.walletId,
});
}
function renderAuthorizedWallet(wallet: WalletAuthorizedDto): void {
activeWalletId = wallet.walletId;
activeSessionState = "owner_open";
activeSessionState = wallet.capability === "view" ? "view_open" : "owner_open";
activeViewEnabled = wallet.viewEnabled;
configuredSecretCandidateCount = wallet.configuredSecretCandidateCount;
clearUnlockSensitiveInputs();
const icon = document.querySelector<HTMLElement>("#currentWalletIcon");
if (icon) {
icon.className = "fa-solid fa-lock-open fa-2x mb-3 text-success";
}
const capabilityLabel = wallet.capability === "view" ? "VIEW" : "OWNER";
setText("#currentWalletFilename", wallet.filename);
setText("#currentWalletState", "OWNER open");
setText("#currentWalletState", `${capabilityLabel} open`);
setText("#currentWalletFormat", wallet.formatVersion.toString());
setText("#currentWalletView", wallet.viewEnabled ? "enabled" : "disabled");
setText("#currentWalletSecretCandidates", configuredSecretCandidateCount.toString());
setText("#currentWalletPubkey", wallet.pubkey);
setText("#currentWalletAlias", wallet.alias ?? "—");
setText("#currentWalletNotes", wallet.notes.length === 0 ? "—" : wallet.notes.map(note => note.text).join(" · "));
setText("#unlockStatus", `${capabilityLabel} ouvert. Lock pour purger le handle autorisé.`);
updateSessionActions();
frontendDebug("main", "OWNER Wallet session rendered after creation", { walletId: wallet.walletId, viewEnabled: wallet.viewEnabled, noteCount: wallet.notes.length });
frontendDebug("main", "Authorized Wallet session rendered", {
capability: wallet.capability,
configuredSecretCandidateCount,
noteCount: wallet.notes.length,
viewEnabled: wallet.viewEnabled,
walletId: wallet.walletId,
});
}
async function selectWallet(walletId: string): Promise<void> {
@@ -395,6 +458,94 @@ function bindCreateWalletForm(): void {
frontendTrace("main", "Create Wallet form handlers installed");
}
function beginFrontendUnlock(capability: UnlockCapability, provider: "manual" | "configured"): string | null {
if (activeSessionState !== "locked" || !activeWalletId) {
return null;
}
const walletId = activeWalletId;
activeSessionState = "privileged_operation";
setText("#currentWalletState", `Unlock ${capability.toUpperCase()}`);
setText("#unlockStatus", provider === "manual" ? "Dérivation Argon2 en cours…" : `Tentative explicite sur ${configuredSecretCandidateCount} secret(s) configuré(s)…`);
updateSessionActions();
return walletId;
}
function restoreFrontendLockedAfterUnlockFailure(walletId: string): void {
if (activeWalletId !== walletId) {
return;
}
activeSessionState = "locked";
clearAuthorizedProjection();
const icon = document.querySelector<HTMLElement>("#currentWalletIcon");
if (icon) {
icon.className = "fa-solid fa-lock fa-2x mb-3";
}
setText("#currentWalletState", "Locked");
setText("#unlockStatus", "Unlock refusé. Le wallet reste verrouillé.");
updateSessionActions();
}
async function unlockManual(capability: UnlockCapability): Promise<void> {
const input = document.querySelector<HTMLInputElement>(capability === "view" ? "#unlockWalletViewPassword" : "#unlockWalletOwnerPassword");
if (!input || input.value.length === 0) {
setText("#unlockStatus", "Password manuel requis.");
return;
}
const request: WalletUnlockRequestDto = { password: input.value };
const walletId = beginFrontendUnlock(capability, "manual");
if (!walletId) {
clearUnlockSensitiveInputs();
return;
}
const command = capability === "view" ? "unlock_wallet_view_manual" : "unlock_wallet_owner_manual";
frontendDebug("main", "Manual Wallet unlock requested", { capability, walletId });
try {
const wallet = await invokeKsp<WalletAuthorizedDto>("main", command, { request });
clearUnlockSensitiveInputs();
renderAuthorizedWallet(wallet);
await loadWalletInventory("list_wallets", false);
activateView("dashboard", "user");
} catch {
clearUnlockSensitiveInputs();
restoreFrontendLockedAfterUnlockFailure(walletId);
frontendWarn("main", "Manual Wallet unlock failed", { capability, walletId });
}
}
async function unlockConfigured(capability: UnlockCapability): Promise<void> {
const walletId = beginFrontendUnlock(capability, "configured");
if (!walletId) {
return;
}
const command = capability === "view" ? "unlock_wallet_view_configured" : "unlock_wallet_owner_configured";
frontendDebug("main", "Configured-secret Wallet unlock requested", { capability, configuredSecretCandidateCount, walletId });
try {
const wallet = await invokeKsp<WalletAuthorizedDto>("main", command);
renderAuthorizedWallet(wallet);
await loadWalletInventory("list_wallets", false);
activateView("dashboard", "user");
} catch {
restoreFrontendLockedAfterUnlockFailure(walletId);
frontendWarn("main", "Configured-secret Wallet unlock failed", { capability, configuredSecretCandidateCount, walletId });
}
}
function bindUnlockActions(): void {
document.querySelector<HTMLButtonElement>("#unlockViewManual")?.addEventListener("click", () => {
void unlockManual("view");
});
document.querySelector<HTMLButtonElement>("#unlockOwnerManual")?.addEventListener("click", () => {
void unlockManual("owner");
});
document.querySelector<HTMLButtonElement>("#unlockViewConfigured")?.addEventListener("click", () => {
void unlockConfigured("view");
});
document.querySelector<HTMLButtonElement>("#unlockOwnerConfigured")?.addEventListener("click", () => {
void unlockConfigured("owner");
});
frontendTrace("main", "Wallet unlock action handlers installed");
}
async function lockCurrentWallet(): Promise<void> {
clearAuthorizedProjection();
try {
@@ -431,7 +582,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 résolue ; création et session Wallet prêtes.");
setText("#shellStatus", "Config résolue ; création et unlock VIEW/OWNER prêts.");
frontendTrace("main", "Wallet Desk runtime status rendered", {
compositeProfile: status.activeCompositeProfile,
fallbackLoggingActive: status.fallbackLoggingActive,
@@ -477,6 +628,7 @@ async function initializeMain(): Promise<void> {
bindNavigation();
bindWalletTableSelection();
bindCreateWalletForm();
bindUnlockActions();
bindShellActions();
clearSelectedWallet();
activateView("dashboard", "startup");

View File

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

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/app_state.rs
// version: 6
// version: 7
//! Shared backend state owned by the Wallet Desk Tauri application.
@@ -102,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.005-wallet-session-create".to_owned(),
shell_phase: "pre.006-wallet-unlock".to_owned(),
startup_diagnostic: runtime.startup_diagnostic.clone(),
wallets_directory: resolved.wallets_directory().to_string_lossy().into_owned(),
wallets_subdirectory,
@@ -123,10 +123,11 @@ impl AppState {
}
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 {
let (path, locked_info, mut dto) = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
dto.configured_secret_candidate_count = self.secret_candidate_count_or_zero(dto.wallet_id.as_str());
let session = self.wallet_session.lock();
let mut session = match session {
std::result::Result::Ok(value) => value,
@@ -180,7 +181,8 @@ impl AppState {
return std::result::Result::Err(error);
},
};
let dto = crate::owner_projection(filename.as_str(), view_enabled, &owner);
let configured_secret_candidate_count = self.secret_candidate_count_or_zero(filename.as_str());
let dto = crate::owner_projection(filename.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,
@@ -191,6 +193,144 @@ impl AppState {
return std::result::Result::Ok(dto);
}
/// Opens the selected locked Wallet with one manual VIEW password supplied frontend -> Rust.
pub(crate) async fn unlock_wallet_view_manual(&self, request: crate::WalletUnlockRequestDto) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
let context = self.begin_unlock_operation(crate::WalletUnlockCapability::View);
let context = match context {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let password = ksp_wallet_lib::ViewPassword::new(request.password);
let opened = ksp_wallet_lib::open_wallet_view_file_v1(context.path.as_path(), password).await;
let view = match opened {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
self.restore_locked_after_unlock_failure(&context);
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = context.wallet_id.as_str(), capability = context.capability.label(), error_domain = error.code().domain(), error_code = error.code().code(), "Manual Wallet unlock failed");
return std::result::Result::Err(error);
},
};
return self.finish_view_unlock(context, view);
}
/// Opens the selected locked Wallet with one manual OWNER password supplied frontend -> Rust.
pub(crate) async fn unlock_wallet_owner_manual(&self, request: crate::WalletUnlockRequestDto) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
let context = self.begin_unlock_operation(crate::WalletUnlockCapability::Owner);
let context = match context {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let password = ksp_wallet_lib::OwnerPassword::new(request.password);
let opened = ksp_wallet_lib::open_wallet_owner_file_v1(context.path.as_path(), password).await;
let owner = match opened {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
self.restore_locked_after_unlock_failure(&context);
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = context.wallet_id.as_str(), capability = context.capability.label(), error_domain = error.code().domain(), error_code = error.code().code(), "Manual Wallet unlock failed");
return std::result::Result::Err(error);
},
};
return self.finish_owner_unlock(context, owner);
}
/// Explicitly attempts Config-owned Wallet password candidates until VIEW unlock succeeds or candidates are exhausted.
pub(crate) async fn unlock_wallet_view_configured(&self) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
let context = self.begin_unlock_operation(crate::WalletUnlockCapability::View);
let context = match context {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let candidates = crate::discover_wallet_secret_candidates(&self.config_management, context.wallet_id.as_str());
let candidates = match candidates {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
self.restore_locked_after_unlock_failure(&context);
return std::result::Result::Err(error);
},
};
let candidate_count = candidates.len();
if candidate_count == 0 {
self.restore_locked_after_unlock_failure(&context);
return std::result::Result::Err(configured_secret_missing_error());
}
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SECRET, wallet_id = context.wallet_id.as_str(), capability = context.capability.label(), candidate_count, "Explicit configured-secret Wallet unlock started");
let mut last_error = std::option::Option::None;
for candidate in candidates {
let secret = candidate.reveal(&self.config_management);
let secret = match secret {
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => continue,
std::result::Result::Err(error) => {
self.restore_locked_after_unlock_failure(&context);
return std::result::Result::Err(error);
},
};
let opened = ksp_wallet_lib::open_wallet_view_file_v1(context.path.as_path(), ksp_wallet_lib::ViewPassword::new(secret)).await;
match opened {
std::result::Result::Ok(view) => {
ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SECRET, wallet_id = context.wallet_id.as_str(), capability = context.capability.label(), candidate_count, "Configured-secret Wallet unlock succeeded");
return self.finish_view_unlock_with_count(context, view, candidate_count);
},
std::result::Result::Err(error) => last_error = std::option::Option::Some(error),
}
}
self.restore_locked_after_unlock_failure(&context);
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SECRET, wallet_id = context.wallet_id.as_str(), capability = context.capability.label(), candidate_count, "Configured-secret Wallet unlock exhausted all candidates");
return match last_error {
std::option::Option::Some(error) => std::result::Result::Err(error),
std::option::Option::None => std::result::Result::Err(configured_secret_missing_error()),
};
}
/// Explicitly attempts Config-owned Wallet password candidates until OWNER unlock succeeds or candidates are exhausted.
pub(crate) async fn unlock_wallet_owner_configured(&self) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
let context = self.begin_unlock_operation(crate::WalletUnlockCapability::Owner);
let context = match context {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let candidates = crate::discover_wallet_secret_candidates(&self.config_management, context.wallet_id.as_str());
let candidates = match candidates {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
self.restore_locked_after_unlock_failure(&context);
return std::result::Result::Err(error);
},
};
let candidate_count = candidates.len();
if candidate_count == 0 {
self.restore_locked_after_unlock_failure(&context);
return std::result::Result::Err(configured_secret_missing_error());
}
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SECRET, wallet_id = context.wallet_id.as_str(), capability = context.capability.label(), candidate_count, "Explicit configured-secret Wallet unlock started");
let mut last_error = std::option::Option::None;
for candidate in candidates {
let secret = candidate.reveal(&self.config_management);
let secret = match secret {
std::result::Result::Ok(std::option::Option::Some(value)) => value,
std::result::Result::Ok(std::option::Option::None) => continue,
std::result::Result::Err(error) => {
self.restore_locked_after_unlock_failure(&context);
return std::result::Result::Err(error);
},
};
let opened = ksp_wallet_lib::open_wallet_owner_file_v1(context.path.as_path(), ksp_wallet_lib::OwnerPassword::new(secret)).await;
match opened {
std::result::Result::Ok(owner) => {
ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SECRET, wallet_id = context.wallet_id.as_str(), capability = context.capability.label(), candidate_count, "Configured-secret Wallet unlock succeeded");
return self.finish_owner_unlock_with_count(context, owner, candidate_count);
},
std::result::Result::Err(error) => last_error = std::option::Option::Some(error),
}
}
self.restore_locked_after_unlock_failure(&context);
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SECRET, wallet_id = context.wallet_id.as_str(), capability = context.capability.label(), candidate_count, "Configured-secret Wallet unlock exhausted all candidates");
return match last_error {
std::option::Option::Some(error) => std::result::Result::Err(error),
std::option::Option::None => std::result::Result::Err(configured_secret_missing_error()),
};
}
/// 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<crate::LockedWalletDto> {
let previous = {
@@ -206,7 +346,23 @@ impl AppState {
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 } => {
crate::WalletSession::PrivilegedOperation { wallet_id, path, locked_info, capability } => {
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::PrivilegedOperation { wallet_id, path, locked_info, capability };
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_WALLET_SESSION_INVALID,
"Wallet unlock operation is already in progress",
));
},
crate::WalletSession::View { wallet_id, path, wallet, .. } => {
drop(wallet);
(wallet_id, path)
},
crate::WalletSession::Owner { wallet_id, path, wallet, .. } => {
drop(wallet);
(wallet_id, path)
},
@@ -216,7 +372,9 @@ impl AppState {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let configured_secret_candidate_count = self.secret_candidate_count_or_zero(wallet_id.as_str());
let dto = crate::LockedWalletDto {
configured_secret_candidate_count,
filename: wallet_id.clone(),
format_version: locked.format_version(),
wallet_id: wallet_id.clone(),
@@ -232,6 +390,140 @@ impl AppState {
return std::result::Result::Ok(dto);
}
fn begin_unlock_operation(&self, capability: crate::WalletUnlockCapability) -> ksp_core_lib::Result<WalletUnlockContext> {
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, locked_info) = match previous {
crate::WalletSession::Locked { wallet_id, path, locked_info } => (wallet_id, path, locked_info),
other => {
*session = other;
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_WALLET_SESSION_INVALID,
"Wallet must be selected and locked before unlock",
));
},
};
if capability == crate::WalletUnlockCapability::View && !locked_info.view_enabled() {
*session = crate::WalletSession::Locked { wallet_id, path, locked_info };
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_WALLET_VIEW_DISABLED,
"Selected Wallet does not expose an enabled VIEW capability",
));
}
let context = WalletUnlockContext { capability, path: path.clone(), wallet_id: wallet_id.clone() };
*session = crate::WalletSession::PrivilegedOperation { capability, wallet_id, path, locked_info };
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = context.wallet_id.as_str(), capability = context.capability.label(), "Explicit Wallet unlock operation started");
return std::result::Result::Ok(context);
}
fn restore_locked_after_unlock_failure(&self, context: &WalletUnlockContext) {
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::PrivilegedOperation { capability, wallet_id, path, locked_info }
if capability == context.capability && wallet_id == context.wallet_id && path == context.path =>
{
*session = crate::WalletSession::Locked { wallet_id, path, locked_info };
},
other => *session = other,
}
}
fn finish_view_unlock(&self, context: WalletUnlockContext, view: ksp_wallet_lib::WalletView) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
let candidate_count = self.secret_candidate_count_or_zero(context.wallet_id.as_str());
return self.finish_view_unlock_with_count(context, view, candidate_count);
}
fn finish_view_unlock_with_count(
&self,
context: WalletUnlockContext,
view: ksp_wallet_lib::WalletView,
configured_secret_candidate_count: usize,
) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
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) = match previous {
crate::WalletSession::PrivilegedOperation { capability, wallet_id, path, locked_info }
if capability == context.capability && wallet_id == context.wallet_id && path == context.path =>
{
(wallet_id, path, locked_info.view_enabled())
},
other => {
*session = other;
drop(view);
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_WALLET_SESSION_INVALID,
"Wallet unlock completion no longer owns the selected session",
));
},
};
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) };
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);
}
fn finish_owner_unlock(&self, context: WalletUnlockContext, owner: ksp_wallet_lib::WalletOwner) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
let candidate_count = self.secret_candidate_count_or_zero(context.wallet_id.as_str());
return self.finish_owner_unlock_with_count(context, owner, candidate_count);
}
fn finish_owner_unlock_with_count(
&self,
context: WalletUnlockContext,
owner: ksp_wallet_lib::WalletOwner,
configured_secret_candidate_count: usize,
) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
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) = match previous {
crate::WalletSession::PrivilegedOperation { capability, wallet_id, path, locked_info }
if capability == context.capability && wallet_id == context.wallet_id && path == context.path =>
{
(wallet_id, path, locked_info.view_enabled())
},
other => {
*session = other;
drop(owner);
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_WALLET_SESSION_INVALID,
"Wallet unlock completion no longer owns the selected session",
));
},
};
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) };
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);
}
fn secret_candidate_count_or_zero(&self, wallet_id: &str) -> usize {
let count = crate::wallet_secret_candidate_count(&self.config_management, wallet_id);
return match count {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SECRET, wallet_id, error_domain = error.code().domain(), error_code = error.code().code(), "Wallet configured-secret candidate count is unavailable");
0
},
};
}
/// Returns the resolved common splash timings captured during bootstrap.
#[must_use]
pub(crate) const fn splash_settings(&self) -> crate::SplashSettings {
@@ -260,6 +552,16 @@ 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 WalletUnlockContext {
capability: crate::WalletUnlockCapability,
path: std::path::PathBuf,
wallet_id: String,
}
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 LoggingRuntimeState {
guard: ksp_logging_lib::LoggingGuard,
active_profile_id: std::option::Option<String>,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/constants.rs
// version: 4
// version: 5
//! Logging targets, domains and composite component identifiers owned by Wallet Desk.
@@ -19,7 +19,9 @@ 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.
/// Structured domain used by Config-owned Wallet password candidate operations.
pub(crate) const TRACING_DOMAIN_WALLET_SECRET: &str = "wallet.secret";
/// Structured domain used by durable Wallet session, creation and unlock 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";

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/errors.rs
// version: 4
// version: 5
//! Application-local error codes for Wallet Desk composition and desktop runtime surfaces.
@@ -33,7 +33,12 @@ pub(crate) const ERROR_CODE_WALLET_DIRECTORY_PREPARE_FAILED: ksp_core_lib::Error
ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_directory_prepare_failed");
/// Wallet Desk cannot enumerate or inspect its effective Wallet inventory directory.
pub(crate) const ERROR_CODE_WALLET_INVENTORY_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_inventory_failed");
/// No effective Config-owned Wallet password candidate is available for the explicit configured-secret unlock action.
pub(crate) const ERROR_CODE_WALLET_SECRET_CANDIDATES_MISSING: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_secret_candidates_missing");
/// 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");
/// The selected Wallet does not expose an enabled VIEW capability.
pub(crate) const ERROR_CODE_WALLET_VIEW_DISABLED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_view_disabled");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/lib.rs
// version: 5
// version: 6
//! 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_secrets;
mod wallet_session;
/// Runs the KSP wallet desktop application.
@@ -53,7 +54,9 @@ 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.
/// Structured domain used by Config-owned Wallet password candidate operations.
pub(crate) use self::constants::TRACING_DOMAIN_WALLET_SECRET;
/// Structured domain used by durable Wallet session, creation and unlock 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;
@@ -99,10 +102,14 @@ pub(crate) use self::errors::ERROR_CODE_WALLET_DIRECTORY_INVALID;
pub(crate) use self::errors::ERROR_CODE_WALLET_DIRECTORY_PREPARE_FAILED;
/// Wallet inventory filesystem operation failed.
pub(crate) use self::errors::ERROR_CODE_WALLET_INVENTORY_FAILED;
/// No effective Config-owned Wallet password candidate is available.
pub(crate) use self::errors::ERROR_CODE_WALLET_SECRET_CANDIDATES_MISSING;
/// 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;
/// Selected Wallet does not expose an enabled VIEW capability.
pub(crate) use self::errors::ERROR_CODE_WALLET_VIEW_DISABLED;
/// 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.
@@ -146,6 +153,10 @@ 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;
/// 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.
pub(crate) use self::wallet_secrets::wallet_secret_candidate_count;
/// Authorized OWNER projection exposed only after successful creation.
pub(crate) use self::wallet_session::WalletAuthorizedDto;
/// Create request whose password strings move frontend -> Rust only.
@@ -157,5 +168,11 @@ 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;
/// Capability targeted by one explicit unlock operation.
pub(crate) use self::wallet_session::WalletUnlockCapability;
/// Manual unlock request whose password moves frontend -> Rust only.
pub(crate) use self::wallet_session::WalletUnlockRequestDto;
/// Builds an authorized OWNER DTO from a Rust-only Wallet handle.
pub(crate) use self::wallet_session::owner_projection;
/// Builds an authorized VIEW DTO from a Rust-only Wallet handle.
pub(crate) use self::wallet_session::view_projection;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/tauri.rs
// version: 3
// version: 4
//! Tauri runtime assembly for the KSP wallet desktop application.
@@ -47,6 +47,10 @@ fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tau
refresh_wallets,
select_wallet,
splash_frontend_ready,
unlock_wallet_owner_configured,
unlock_wallet_owner_manual,
unlock_wallet_view_configured,
unlock_wallet_view_manual,
]);
}
@@ -163,3 +167,45 @@ async fn splash_frontend_ready(
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn unlock_wallet_owner_configured(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::WalletAuthorizedDto, crate::CommandErrorDto> {
let result = state.unlock_wallet_owner_configured().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 unlock_wallet_owner_manual(
request: crate::WalletUnlockRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::WalletAuthorizedDto, crate::CommandErrorDto> {
let result = state.unlock_wallet_owner_manual(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 unlock_wallet_view_configured(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::WalletAuthorizedDto, crate::CommandErrorDto> {
let result = state.unlock_wallet_view_configured().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 unlock_wallet_view_manual(
request: crate::WalletUnlockRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::WalletAuthorizedDto, crate::CommandErrorDto> {
let result = state.unlock_wallet_view_manual(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)),
};
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/wallet_inventory.rs
// version: 2
// version: 3
//! Root-scoped Wallet inventory and locked-file selection for Wallet Desk.
@@ -53,6 +53,8 @@ pub(crate) struct WalletInventoryEntryDto {
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_inventory/LockedWalletDto.ts")]
pub(crate) struct LockedWalletDto {
/// Number of effective Config-owned Wallet password candidates without their names or values.
pub(crate) configured_secret_candidate_count: usize,
/// Native Wallet filename without its parent path.
pub(crate) filename: String,
/// Native Wallet format version.
@@ -159,6 +161,7 @@ pub(crate) async fn resolve_locked_wallet(
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let dto = LockedWalletDto {
configured_secret_candidate_count: 0,
filename: wallet_id.clone(),
format_version: locked.format_version(),
wallet_id: wallet_id.clone(),

View File

@@ -0,0 +1,96 @@
// file: crates/ksp-app-wallet-desk/src/wallet_secrets.rs
// version: 1
//! Config-owned Wallet password candidate discovery for explicit unlock operations.
const WALLET_SECRET_PREFIX: &str = "KSP_SECRET_WALLET_PASS_";
/// One Config-owned secret candidate retained only inside Rust.
pub(crate) struct WalletSecretCandidate {
variable_name: String,
}
impl WalletSecretCandidate {
/// Explicitly reveals this candidate through Config's privileged management boundary.
pub(crate) fn reveal(self, management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result<std::option::Option<String>> {
return management.reveal_effective_environment_value(self.variable_name.as_str());
}
}
/// Discovers configured Wallet password candidates without revealing values.
pub(crate) fn discover_wallet_secret_candidates(
management: &ksp_config_lib::ConfigManagement,
wallet_id: &str,
) -> ksp_core_lib::Result<std::vec::Vec<WalletSecretCandidate>> {
let reports = management.environment_report();
let reports = match reports {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut names = reports
.into_iter()
.filter(|report| {
return report.sensitivity() == ksp_config_lib::ConfigSensitivity::Secret
&& report.effective_source().is_some()
&& report.variable_name().starts_with(WALLET_SECRET_PREFIX)
&& report.variable_name().len() > WALLET_SECRET_PREFIX.len();
})
.map(|report| return report.variable_name().to_owned())
.collect::<std::vec::Vec<_>>();
order_wallet_secret_candidate_names(wallet_id, names.as_mut_slice());
return std::result::Result::Ok(names.into_iter().map(|variable_name| return WalletSecretCandidate { variable_name }).collect());
}
/// Returns the number of effective configured Wallet password candidates without revealing names or values.
pub(crate) fn wallet_secret_candidate_count(management: &ksp_config_lib::ConfigManagement, wallet_id: &str) -> ksp_core_lib::Result<usize> {
let candidates = discover_wallet_secret_candidates(management, wallet_id);
return candidates.map(|values| return values.len());
}
/// Normalizes a native Wallet filename to the deterministic Config secret label candidate.
#[must_use]
fn normalize_wallet_secret_label(wallet_id: &str) -> String {
let stem = wallet_id.strip_suffix(crate::WALLET_FILE_SUFFIX).unwrap_or(wallet_id);
let mut output = String::new();
let mut previous_separator = false;
for byte in stem.bytes() {
let normalized = if byte.is_ascii_alphanumeric() { byte.to_ascii_uppercase() as char } else { '_' };
if normalized == '_' {
if output.is_empty() || previous_separator {
continue;
}
output.push('_');
previous_separator = true;
} else {
output.push(normalized);
previous_separator = false;
}
}
while output.ends_with('_') {
output.pop();
}
return output;
}
fn order_wallet_secret_candidate_names(wallet_id: &str, names: &mut [String]) {
let normalized = normalize_wallet_secret_label(wallet_id);
names.sort_by(|left, right| {
return wallet_secret_sort_key(left.as_str(), normalized.as_str()).cmp(&wallet_secret_sort_key(right.as_str(), normalized.as_str()));
});
}
fn wallet_secret_sort_key(variable_name: &str, normalized_filename: &str) -> (u8, u64, String) {
let suffix = variable_name.strip_prefix(WALLET_SECRET_PREFIX).unwrap_or(variable_name);
if suffix == normalized_filename && !normalized_filename.is_empty() {
return (0, 0, suffix.to_owned());
}
if !suffix.is_empty() && suffix.bytes().all(|byte| return byte.is_ascii_digit()) {
let parsed = suffix.parse::<u64>().unwrap_or(u64::MAX);
return (1, parsed, suffix.to_owned());
}
return (2, 0, suffix.to_owned());
}
#[cfg(test)]
#[path = "../unit_tests/wallet_secrets.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/wallet_session.rs
// version: 2
// version: 3
//! Durable root-scoped Wallet session lifecycle for Wallet Desk.
@@ -14,6 +14,10 @@ pub(crate) enum WalletSessionStateDto {
NoSelection,
/// One Wallet is selected but remains locked.
Locked,
/// One explicit unlock operation is running in Rust.
PrivilegedOperation,
/// One Wallet is open with VIEW capability.
ViewOpen,
/// One Wallet is open with OWNER capability.
OwnerOpen,
}
@@ -42,7 +46,7 @@ pub(crate) struct WalletNoteDto {
pub(crate) text: String,
}
/// Authorized Wallet projection returned after creation with OWNER capability.
/// Authorized Wallet projection returned after explicit VIEW/OWNER authorization.
#[derive(serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_session/WalletAuthorizedDto.ts")]
@@ -51,17 +55,19 @@ pub(crate) struct WalletAuthorizedDto {
pub(crate) alias: std::option::Option<String>,
/// Capability that produced this projection.
pub(crate) capability: String,
/// Number of effective Config-owned Wallet password candidates without their names or values.
pub(crate) configured_secret_candidate_count: usize,
/// 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.
/// Protected notes available to the authorized session.
pub(crate) notes: std::vec::Vec<WalletNoteDto>,
/// 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.
/// Whether the Wallet has an enabled VIEW slot.
pub(crate) view_enabled: bool,
}
@@ -82,7 +88,36 @@ pub(crate) struct WalletCreateRequestDto {
pub(crate) view_password: std::option::Option<String>,
}
/// Backend Wallet session. Full paths and authorized handles never cross IPC.
/// Manual unlock request whose password exists only for the explicit frontend -> Rust call.
#[derive(serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_session/WalletUnlockRequestDto.ts")]
pub(crate) struct WalletUnlockRequestDto {
/// Manual password transported only frontend -> Rust.
pub(crate) password: String,
}
/// Capability targeted by one explicit unlock operation.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum WalletUnlockCapability {
/// VIEW metadata-only authorization.
View,
/// OWNER full authorization.
Owner,
}
impl WalletUnlockCapability {
/// Returns a safe lowercase capability label for logs and projections.
#[must_use]
pub(crate) const fn label(self) -> &'static str {
return match self {
Self::View => "view",
Self::Owner => "owner",
};
}
}
/// Backend Wallet session. Full paths, configured secret names and authorized handles never cross IPC.
pub(crate) enum WalletSession {
/// No current Wallet selection.
NoSelection,
@@ -95,6 +130,26 @@ pub(crate) enum WalletSession {
/// Locked information re-inspected from the file.
locked_info: ksp_wallet_lib::LockedWalletInfo,
},
/// One explicit unlock operation currently owns the selected locked state.
PrivilegedOperation {
/// Capability being attempted.
capability: WalletUnlockCapability,
/// Native Wallet filename / root-scoped identifier.
wallet_id: String,
/// Full root-scoped filesystem path retained only in Rust.
path: std::path::PathBuf,
/// Locked information retained so a failed attempt can return to `Locked` without another KDF.
locked_info: ksp_wallet_lib::LockedWalletInfo,
},
/// One Wallet kept open with VIEW capability.
View {
/// Native Wallet filename / root-scoped identifier.
wallet_id: String,
/// Full root-scoped filesystem path retained only in Rust.
path: std::path::PathBuf,
/// Authorized VIEW handle retaining metadata capability only in Rust.
wallet: std::boxed::Box<ksp_wallet_lib::WalletView>,
},
/// One Wallet kept open with OWNER capability.
Owner {
/// Native Wallet filename / root-scoped identifier.
@@ -130,6 +185,16 @@ impl WalletSession {
wallet_id: std::option::Option::Some(wallet_id.clone()),
}
},
Self::PrivilegedOperation { wallet_id, .. } => WalletSessionDto {
filename: std::option::Option::Some(wallet_id.clone()),
state: WalletSessionStateDto::PrivilegedOperation,
wallet_id: std::option::Option::Some(wallet_id.clone()),
},
Self::View { wallet_id, .. } => WalletSessionDto {
filename: std::option::Option::Some(wallet_id.clone()),
state: WalletSessionStateDto::ViewOpen,
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,
@@ -140,8 +205,32 @@ impl WalletSession {
}
/// 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();
pub(crate) fn owner_projection(
wallet_id: &str,
view_enabled: bool,
configured_secret_candidate_count: usize,
owner: &ksp_wallet_lib::WalletOwner,
) -> WalletAuthorizedDto {
return authorized_projection(wallet_id, view_enabled, configured_secret_candidate_count, "owner", owner.info());
}
/// Converts a VIEW handle into the authorized response DTO without exposing OWNER material.
pub(crate) fn view_projection(
wallet_id: &str,
view_enabled: bool,
configured_secret_candidate_count: usize,
view: &ksp_wallet_lib::WalletView,
) -> WalletAuthorizedDto {
return authorized_projection(wallet_id, view_enabled, configured_secret_candidate_count, "view", view.info());
}
fn authorized_projection(
wallet_id: &str,
view_enabled: bool,
configured_secret_candidate_count: usize,
capability: &str,
info: &ksp_wallet_lib::WalletInfo,
) -> WalletAuthorizedDto {
let notes = info
.notes()
.iter()
@@ -151,7 +240,8 @@ pub(crate) fn owner_projection(wallet_id: &str, view_enabled: bool, owner: &ksp_
.collect();
return WalletAuthorizedDto {
alias: info.alias().map(|value| return value.to_owned()),
capability: "owner".to_owned(),
capability: capability.to_owned(),
configured_secret_candidate_count,
filename: wallet_id.to_owned(),
format_version: info.format_version(),
notes,

View File

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

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs
// version: 5
// version: 6
//! Desktop build, shell and Config-status contract audits for Wallet Desk.
@@ -141,5 +141,34 @@ fn pre_005_create_and_session_lifecycle_are_backend_owned() {
assert!(html.contains("id=\"createWalletOwnerPassword\""));
assert!(html.contains("id=\"createWalletViewPassword\""));
assert!(main.contains("clearCreateFormSensitiveInputs"));
assert!(main.contains("OWNER Wallet session rendered after creation"));
assert!(main.contains("Authorized Wallet session rendered"));
}
#[test]
fn pre_006_unlock_surface_is_explicit_backend_owned_and_supports_view_owner() {
let root = app_root();
let state = read_text(root.join("src/app_state.rs").as_path());
let secrets = read_text(root.join("src/wallet_secrets.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("open_wallet_view_file_v1"));
assert!(state.contains("open_wallet_owner_file_v1"));
assert!(state.contains("WalletSession::PrivilegedOperation"));
assert!(state.contains("WalletSession::View"));
assert!(secrets.contains("environment_report"));
assert!(secrets.contains("reveal_effective_environment_value"));
assert!(secrets.contains("normalize_wallet_secret_label"));
assert!(session.contains("WalletUnlockRequestDto"));
assert!(tauri.contains("unlock_wallet_view_manual"));
assert!(tauri.contains("unlock_wallet_owner_manual"));
assert!(tauri.contains("unlock_wallet_view_configured"));
assert!(tauri.contains("unlock_wallet_owner_configured"));
assert!(html.contains("id=\"unlockViewManual\""));
assert!(html.contains("id=\"unlockOwnerManual\""));
assert!(html.contains("id=\"unlockViewConfigured\""));
assert!(html.contains("id=\"unlockOwnerConfigured\""));
assert!(main.contains("beginFrontendUnlock"));
assert!(main.contains("configuredSecretCandidateCount"));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/tests/desktop_security.rs
// version: 4
// version: 5
//! Static desktop security contracts for the Wallet Desk pre.002 shell.
@@ -66,3 +66,37 @@ fn create_passwords_are_request_only_and_protected_projection_is_purged() {
assert!(!main.contains("frontendDebug(\"main\", request.ownerPassword"));
assert!(!main.contains("frontendTrace(\"main\", request.viewPassword"));
}
#[test]
fn unlock_passwords_and_config_secret_names_never_cross_rust_to_frontend() {
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 html = read_text(root.join("frontend/main.html").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"));
let unlock_request = session.find("pub(crate) struct WalletUnlockRequestDto");
assert!(unlock_request.is_some());
assert!(session[unlock_request.unwrap_or_default()..].contains("password: String"));
assert!(main.contains("clearUnlockSensitiveInputs"));
assert!(!main.contains("KSP_SECRET_WALLET_PASS_"));
assert!(!html.contains("KSP_SECRET_WALLET_PASS_"));
assert!(!main.contains("variableName"));
assert!(!main.contains("effectiveSource"));
}
#[test]
fn wallet_selection_does_not_auto_unlock_configured_secrets() {
let root = app_root();
let main = read_text(root.join("frontend/ts/main.ts").as_path());
let selection_start = main.find("async function selectWallet");
let selection_end = main.find("function bindWalletTableSelection");
assert!(selection_start.is_some());
assert!(selection_end.is_some());
let selection = &main[selection_start.unwrap_or_default()..selection_end.unwrap_or(main.len())];
assert!(selection.contains("select_wallet"));
assert!(!selection.contains("unlock_wallet_view_configured"));
assert!(!selection.contains("unlock_wallet_owner_configured"));
}

View File

@@ -0,0 +1,34 @@
// file: crates/ksp-app-wallet-desk/unit_tests/wallet_secrets.rs
// version: 1
#[test]
fn filename_secret_label_normalization_is_deterministic_and_never_uses_internal_alias() {
assert_eq!(super::normalize_wallet_secret_label("wallet-01.kspwallet"), "WALLET_01");
assert_eq!(super::normalize_wallet_secret_label(" treasury.main .kspwallet"), "TREASURY_MAIN");
assert_eq!(super::normalize_wallet_secret_label("a___b...c.kspwallet"), "A_B_C");
assert_eq!(super::normalize_wallet_secret_label(".kspwallet"), "");
}
#[test]
fn candidate_order_prefers_filename_then_numeric_then_named_labels() {
let mut names = vec![
format!("{}TREASURY", super::WALLET_SECRET_PREFIX),
format!("{}10", super::WALLET_SECRET_PREFIX),
format!("{}WALLET_01", super::WALLET_SECRET_PREFIX),
format!("{}02", super::WALLET_SECRET_PREFIX),
format!("{}01", super::WALLET_SECRET_PREFIX),
format!("{}ALPHA", super::WALLET_SECRET_PREFIX),
];
super::order_wallet_secret_candidate_names("wallet-01.kspwallet", names.as_mut_slice());
assert_eq!(
names,
vec![
format!("{}WALLET_01", super::WALLET_SECRET_PREFIX),
format!("{}01", super::WALLET_SECRET_PREFIX),
format!("{}02", super::WALLET_SECRET_PREFIX),
format!("{}10", super::WALLET_SECRET_PREFIX),
format!("{}ALPHA", super::WALLET_SECRET_PREFIX),
format!("{}TREASURY", super::WALLET_SECRET_PREFIX),
]
);
}

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-app-wallet-desk/unit_tests/wallet_session.rs
// version: 1
// version: 2
//! Unit tests for Wallet Desk session DTOs and OWNER projection.
//! Unit tests for Wallet Desk session DTOs and password request boundaries.
#[test]
fn no_selection_projection_contains_no_wallet_identity() {
@@ -24,3 +24,16 @@ fn create_request_does_not_require_debug_or_clone_password_contracts() {
};
accepts_request(request);
}
#[test]
fn unlock_request_does_not_require_debug_or_clone_password_contracts() {
fn accepts_request(_: crate::WalletUnlockRequestDto) {}
let request = crate::WalletUnlockRequestDto { password: "operator-test-unlock".to_owned() };
accepts_request(request);
}
#[test]
fn session_state_contract_includes_view_owner_and_privileged_operation() {
assert_ne!(crate::WalletSessionStateDto::ViewOpen, crate::WalletSessionStateDto::OwnerOpen);
assert_ne!(crate::WalletSessionStateDto::PrivilegedOperation, crate::WalletSessionStateDto::Locked);
}

205
deltas/0.2.6/pre.006.md Normal file
View File

@@ -0,0 +1,205 @@
<!-- file: deltas/0.2.6/pre.006.md -->
<!-- version: 1 -->
# Delta `0.2.6-pre.006` — unlock VIEW/OWNER manuel et secrets Config
## Base requise
```text
0.2.6-pre.005-fix.001 appliquée
workspace.package.version = 0.2.6-pre.5.fix.1
```
Les preuves opérateur de `pre.005-fix.001` sont vertes : formatage, audit structurel, `cargo check --workspace`, Clippy, tests Wallet Desk, workspace complet et `cargo tauri dev`. La création native, le no-clobber, la sélection locked, Deselect et Refresh ont été vérifiés en runtime.
## Type de livraison
```text
ksp-general-0.2.6-pre.006.zip
```
Cette tranche modifie le runtime Rust/frontend ; le signal technique devient :
```text
workspace.package.version = 0.2.6-pre.6
commit = v0.2.6-pre.006
```
Aucun tag stable.
## Objet
Permettre l'ouverture explicite d'un `.kspwallet` existant avec VIEW ou OWNER, soit par password manuel éphémère, soit par les secrets Wallet possédés par Config depuis le process ou `.env`, sans auto-unlock et sans faire traverser à ces secrets la frontière Rust -> frontend.
## Session Rust
`WalletSession` matérialise désormais :
```text
NoSelection
Locked
PrivilegedOperation
View
Owner
```
Transitions d'un unlock :
```text
Locked
-> PrivilegedOperation
-> View / Owner sur succès
-> Locked sur échec
```
`PrivilegedOperation` conserve le path et `LockedWalletInfo` côté Rust pendant Argon2 sans conserver le mutex `std::sync::Mutex` à travers `.await`. Une seconde tentative concurrente est refusée. Si une autre action purge/change la session pendant la KDF, la complétion obsolète ne peut pas réinstaller son handle.
`WalletView` et `WalletOwner` sont tous deux boxed dans la session ; chemins complets et handles ne traversent jamais IPC.
## Unlock manuel
Nouvelles commandes :
```text
unlock_wallet_view_manual
unlock_wallet_owner_manual
```
`WalletUnlockRequestDto` transporte uniquement :
```text
password: String
```
Le DTO ne dérive ni `Debug` ni `Clone`. Le `String` est immédiatement déplacé dans `ViewPassword` ou `OwnerPassword`, puis zeroized par le wrapper Wallet à sa destruction. Rust ne renvoie jamais ce password.
VIEW est refusé avant KDF lorsque le wallet locked annonce `view_enabled = false`.
## Secrets Config `KSP_SECRET_WALLET_PASS_*`
Wallet Desk utilise exclusivement les APIs publiques Config :
```text
ConfigManagement::environment_report
ConfigManagement::reveal_effective_environment_value
```
La première API découvre les noms disponibles sans valeur réelle. La seconde n'est appelée que pendant une action d'unlock configuré explicitement demandée.
Normalisation filename :
```text
strip .kspwallet
uppercase ASCII
hors [A-Z0-9] -> _
runs de _ compactés
_ de bord supprimés
```
Ordre des candidats :
```text
1. suffixe exactement égal au filename normalisé
2. suffixes numériques dans l'ordre numérique
3. autres suffixes nommés dans l'ordre lexicographique
```
Les suffixes nommés restent des identifiants opérateur/filename ; l'alias interne protégé du wallet n'est jamais utilisé pour choisir un secret.
Nouvelles commandes explicites :
```text
unlock_wallet_view_configured
unlock_wallet_owner_configured
```
Aucun candidat n'est essayé lors de `select_wallet`. L'action configurée tente les candidats jusqu'au premier succès ou épuisement.
Le frontend reçoit uniquement :
```text
configured_secret_candidate_count
```
Les noms, suffixes, sources et valeurs des variables Config restent côté Rust et ne sont pas journalisés.
## Projection autorisée
`WalletAuthorizedDto` est maintenant commun à VIEW et OWNER et ajoute seulement :
```text
configured_secret_candidate_count
```
Après VIEW/OWNER, l'UI peut afficher Pubkey, alias et notes puisque la capability est explicitement autorisée. Aucun secret Solana/password/path complet n'est sérialisé.
## Frontend
L'écran Security devient fonctionnel :
```text
Unlock VIEW manuel
Unlock OWNER manuel
Unlock VIEW avec secret configuré
Unlock OWNER avec secret configuré
```
L'UI affiche le nombre de candidats Config, jamais leurs identifiants. Les actions sont désactivées si aucun wallet locked n'est sélectionné ; VIEW est aussi désactivé si le wallet ne possède pas de capability VIEW.
Pendant Argon2, l'état frontend devient `privileged_operation`, les actions concurrentes sont désactivées et un message indique le coût attendu. Les inputs password sont purgés après succès ou échec.
La DataTable distingue désormais une session ouverte VIEW ou OWNER avec `fa-lock-open`.
## Tests et canaris
Ajouts/renforcements :
```text
normalisation filename -> label secret
ordre filename / numérique / nommé
WalletUnlockRequestDto request-only
états ViewOpen / OwnerOpen / PrivilegedOperation
quatre commandes unlock présentes
Config environment_report + reveal utilisés côté Rust
aucun nom de secret Config dans frontend
candidate count uniquement
pas d'auto-unlock dans selectWallet
purge des passwords manuels
```
Les tests cryptographiques et non-oracle restent la responsabilité de `ksp-wallet-lib`.
## Dépendances
Aucune dépendance Cargo ou npm n'est ajoutée/modifiée.
**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-pre006 cargo tauri dev -c crates/ksp-app-wallet-desk/tauri.conf.json
```
Pour tester les secrets Config sans modifier `.env`, il est possible de lancer explicitement par exemple :
```bash
KSP_WALLETS_DIRECTORY=var/wallet-desk-pre006 \
KSP_SECRET_WALLET_PASS_WALLET_01='<OWNER_PASSWORD>' \
KSP_SECRET_WALLET_PASS_01='<VIEW_PASSWORD>' \
cargo tauri dev -c crates/ksp-app-wallet-desk/tauri.conf.json
```
Sur `wallet-01.kspwallet`, le candidat `WALLET_01` sera essayé avant `_01`. Les deux actions VIEW/OWNER restent distinctes ; aucun essai n'est déclenché à la simple sélection du wallet.
Toujours aucun `cargo tauri build` à ce stade.
## Suite
Après validation de `pre.006`, `pre.007` branchera la composition Transport du composite Wallet Desk et `getBalance` à partir de la Pubkey du handle VIEW/OWNER autorisé afin de fermer le MVP identité + balance.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
<!-- version: 66 -->
<!-- version: 67 -->
# 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 lapplication. Aucun inventory ou fichier `.kspwallet` nest 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 dinventaire `.env.example` qui interprétait `FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK` comme une variable denvironnement, 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 didentifiant dun 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 nest modifiée. `0.2.6-pre.004` ajoute linventory `.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 nest 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 dexports 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.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 lapplication. Aucun inventory ou fichier `.kspwallet` nest 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 dinventaire `.env.example` qui interprétait `FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK` comme une variable denvironnement, 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 didentifiant dun 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 nest modifiée. `0.2.6-pre.004` ajoute linventory `.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 nest 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 dexports 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 quau moment dune tentative explicitement demandée, conserve noms/suffixes/valeurs hors IPC/logs, et protège les KDF concurrents avec `PrivilegedOperation` avant dinstaller `WalletView` ou `WalletOwner` uniquement en Rust.
## `0.2.7` — WebSocket Solana standard

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md -->
<!-- version: 14 -->
<!-- version: 15 -->
# 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` nest 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 dinventaire `.env.example` qui confondait un fragment didentifiant 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 à lapplication. `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 denvironnement 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 linventory 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 dexports 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`. `pre.005-fix.001` corrige le gate de compilation/tests révélé par l'opérateur sans modifier le lifecycle : la variante `WalletSession::Owner` contient désormais `Box<WalletOwner>` afin de fermer `clippy::large_enum_variant`, et le canari de sécurité définit explicitement `request_start` avant de contrôler les champs password request-only.
`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` nest 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 dinventaire `.env.example` qui confondait un fragment didentifiant 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 à lapplication. `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 denvironnement 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 linventory 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 dexports 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`. `pre.005-fix.001` corrige le gate de compilation/tests révélé par l'opérateur sans modifier le lifecycle : la variante `WalletSession::Owner` contient désormais `Box<WalletOwner>` afin de fermer `clippy::large_enum_variant`, et le canari de sécurité définit explicitement `request_start` avant de contrôler les champs password request-only. `pre.006` matérialise ensuite les unlock existants : VIEW/OWNER manuel, découverte Config des candidats `KSP_SECRET_WALLET_PASS_*`, ordre déterministe filename normalisé -> numérique -> nommé, actions configurées explicites et jamais automatiques, `PrivilegedOperation` pendant Argon2, puis sessions `View`/`Owner` Rust-only avec candidate count seul projeté vers lUI.
## 2. Sources relues et hiérarchie appliquée
@@ -1367,22 +1367,23 @@ Les unlock manuels/configurés d'un wallet existant restent strictement `pre.006
### `pre.006` — unlock manuel + `KSP_SECRET_WALLET_PASS_*`
Objectifs :
État matérialisé :
```text
unlock VIEW manuel
unlock OWNER manuel
Config-owned wallet secret candidate discovery
filename normalization
numeric/named candidate ordering
unlock VIEW avec secret configuré
unlock OWNER avec secret configuré
secrets Config jamais Rust -> frontend ; password manuel request-only frontend -> Rust
zeroization via OwnerPassword/ViewPassword
UX coût Argon2 / tentative explicite
Locked -> PrivilegedOperation -> ViewOpen / OwnerOpen
unlock VIEW/OWNER manuel via WalletUnlockRequestDto request-only
ConfigManagement::environment_report pour découvrir les candidats sans valeur
ConfigManagement::reveal_effective_environment_value uniquement pendant laction explicite
normalisation stem : uppercase ASCII, hors [A-Z0-9] -> _, runs compactés, bords supprimés
ordre : filename normalisé, puis numériques en ordre numérique, puis nommés lexicographiques
aucun auto-unlock lors de select_wallet
frontend reçoit uniquement configured_secret_candidate_count
VIEW disabled refuse laction VIEW avant KDF
OwnerPassword/ViewPassword prennent immédiatement possession des String révélés/saisis
Lock/Deselect/Refresh/changement de wallet détruisent les handles autorisés
```
Tests spécifiques de redaction, ordre et non-auto-unlock.
Le frontend Security expose quatre actions distinctes : VIEW manuel, OWNER manuel, VIEW configuré et OWNER configuré. Les noms/suffixes/valeurs des secrets Config ne traversent jamais IPC et ne sont pas journalisés ; les logs se limitent au wallet_id root-scoped, capability, candidate_count et résultat. `PrivilegedOperation` évite de tenir un mutex pendant Argon2 et empêche une complétion de KDF obsolète de réinstaller un handle après changement de session. Tests spécifiques : normalisation/ordre, requests non Clone/Debug, redaction, candidate-count only et non-auto-unlock.
### `pre.007` — détails autorisés + balance HTTP