v0.2.6-pre.004
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 167
|
||||
# version: 168
|
||||
|
||||
[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.3.fix.4"
|
||||
version = "0.2.6-pre.4"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: ROADMAP.md -->
|
||||
<!-- version: 63 -->
|
||||
<!-- version: 64 -->
|
||||
|
||||
# Roadmap KSP
|
||||
|
||||
@@ -50,7 +50,7 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
|
||||
- [X] `0.2.3` — HTTP Transactions stable : 11/11 wrappers typés publiés, classification `8 Read / 2 WriteSubmission / 1 Simulation`, no-resend ambigu prouvé pour les write submissions, `KSP-TRANSPORT-007` réaudité conforme sur les 37 wrappers HTTP courants, graphes Cargo et deux smokes Devnet validés ; `0.2.4` reprend les 15 Blocks/Economics restants.
|
||||
- [X] `0.2.4` — HTTP Blocks + Economics stable : 15/15 wrappers `V0_2_4` publiés, surface typed complète à 52/52 méthodes courantes, 14/14 historiques conservées, réaudit SIMD/inventaire final et `KSP-TRANSPORT-007` global validés ; deux smokes Devnet passés avant publication.
|
||||
- [X] `0.2.5` — Wallet foundation stable : `.kspwallet` V1, VIEW/OWNER indépendants, Argon2id/XChaCha20-Poly1305, autorité Ed25519 OWNER, persistence no-clobber, signature, administration/rotations/révocation VIEW forte, import/export Solana CLI JSON + Base58, canaris adversariaux, interop externe et documentation durable publiés. La clôture `pre.010-fix.001`–`fix.003` ajoute `ed25519-dalek 3.0.0` direct, normalise le Rust workspace et installe l’audit structurel Python complémentaire à rustfmt/Clippy. `Pubkey` reste via `ksp-core-lib`, la keypair reste encapsulée dans Wallet et Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet.
|
||||
- [ ] `0.2.6` — Introduire `ksp-app-wallet-desk` utilisant Config composite + Wallet + transport HTTP. `pre.001` fixe le sizing et le gabarit ; `pre.002` matérialise la crate Tauri et son shell splash/main avec Config + Logging bootstrap, ports `1432/1433`, Bootstrap, Font Awesome, DataTables/Select, SimpleBar, resize-observer-polyfill, TS-RS et bridge frontend Logging. `pre.003` matérialise `cfg.std.wallet`/`schema.std.wallet`, le composite `cfg.composite.ksp-app-wallet-desk`, `ResolvedWalletConfig`, `wallets_directory` global + `wallets_subdirectory` par profil et la préparation automatique des répertoires côté application avec logs debug/error. `pre.003-fix.001` corrige les premiers canaris Config et active la trace des interactions frontend sans valeur de contrôle. `pre.003-fix.002` corrige le scanner `.env.example` sur les fragments d’identifiants Rust, généralise `std.logging` avec les profils console-only, `file_info`, `superdev` et `supertrace`, et sélectionne temporairement `supertrace` dans le composite Wallet Desk. `pre.003-fix.003` corrige les canaris de ce changement : closures explicites compatibles avec `clippy::implicit_return` et distinction entre fragment KSP incorporé dans un identifiant et nom d’environnement concret suffixé. `pre.003-fix.004` corrige les cinq closures restantes du canari de composition Wallet Desk afin que `cargo clippy --workspace --all-targets` respecte intégralement `clippy::implicit_return`, sans modifier Config ni Logging. Les tranches suivantes ajoutent inventory, lifecycle VIEW/OWNER, secrets `KSP_SECRET_WALLET_PASS_*`, identité autorisée + `getBalance`, puis administration/import/export, avec une dernière tranche prévue pour README/USAGE/docs/validation, prompt `0.2.7` et build Tauri final.
|
||||
- [ ] `0.2.6` — Introduire `ksp-app-wallet-desk` utilisant Config composite + Wallet + transport HTTP. `pre.001` fixe le sizing et le gabarit ; `pre.002` matérialise la crate Tauri et son shell splash/main avec Config + Logging bootstrap, ports `1432/1433`, Bootstrap, Font Awesome, DataTables/Select, SimpleBar, resize-observer-polyfill, TS-RS et bridge frontend Logging. `pre.003` matérialise `cfg.std.wallet`/`schema.std.wallet`, le composite `cfg.composite.ksp-app-wallet-desk`, `ResolvedWalletConfig`, `wallets_directory` global + `wallets_subdirectory` par profil et la préparation automatique des répertoires côté application avec logs debug/error. `pre.003-fix.001` corrige les premiers canaris Config et active la trace des interactions frontend sans valeur de contrôle. `pre.003-fix.002` corrige le scanner `.env.example` sur les fragments d’identifiants Rust, généralise `std.logging` avec les profils console-only, `file_info`, `superdev` et `supertrace`, et sélectionne temporairement `supertrace` dans le composite Wallet Desk. `pre.003-fix.003` corrige les canaris de ce changement : closures explicites compatibles avec `clippy::implicit_return` et distinction entre fragment KSP incorporé dans un identifiant et nom d’environnement concret suffixé. `pre.003-fix.004` corrige les cinq closures restantes du canari de composition Wallet Desk afin que `cargo clippy --workspace --all-targets` respecte intégralement `clippy::implicit_return`, sans modifier Config ni Logging. `pre.004` branche ensuite l’inventory réel `.kspwallet` sur le répertoire effectif Config : enumeration non récursive, extension exacte, fichiers réguliers uniquement, rejet des symlinks, inspection locked via `ksp-wallet-lib`, diagnostics sûrs par ligne, DataTable réel, refresh et sélection root-scoped revalidée côté Rust sans Pubkey/alias/notes. Les tranches suivantes ajoutent création/lifecycle VIEW/OWNER, secrets `KSP_SECRET_WALLET_PASS_*`, identité autorisée + `getBalance`, puis administration/import/export, avec une dernière tranche prévue pour README/USAGE/docs/validation, prompt `0.2.7` et build Tauri final.
|
||||
- [ ] `0.2.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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# file: crates/ksp-app-wallet-desk/Cargo.toml
|
||||
# version: 1
|
||||
# version: 2
|
||||
|
||||
[package]
|
||||
name = "ksp-app-wallet-desk"
|
||||
@@ -27,10 +27,11 @@ fs2.workspace = true
|
||||
ksp-config-lib = { path = "../ksp-config-lib" }
|
||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
||||
ksp-wallet-lib = { path = "../ksp-wallet-lib" }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tauri.workspace = true
|
||||
tauri-plugin-tracing.workspace = true
|
||||
tokio = { workspace = true, features = ["time"] }
|
||||
tokio = { workspace = true, features = ["fs", "rt", "time"] }
|
||||
ts-rs.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -37,9 +37,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">Composition Config Wallet Desk active — inventory Wallet branché en pre.004.</p>
|
||||
<p class="text-body-secondary mb-0">Inventaire `.kspwallet` root-scoped et inspection locked actifs.</p>
|
||||
</div>
|
||||
<span class="badge text-bg-secondary">0.2.6-pre.003</span>
|
||||
<span class="badge text-bg-secondary">0.2.6-pre.004</span>
|
||||
</div>
|
||||
<section data-view-panel="dashboard">
|
||||
<div class="row g-3">
|
||||
@@ -66,9 +66,14 @@
|
||||
<div class="col-xl-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header fw-semibold">Wallet courant</div>
|
||||
<div class="card-body d-flex flex-column align-items-center justify-content-center text-center text-body-secondary">
|
||||
<i class="fa-solid fa-lock fa-2x mb-3" aria-hidden="true"></i>
|
||||
<p class="mb-0">Aucun wallet sélectionné.</p>
|
||||
<div class="card-body text-center">
|
||||
<i id="currentWalletIcon" class="fa-solid fa-lock fa-2x mb-3 text-body-secondary" aria-hidden="true"></i>
|
||||
<p id="currentWalletFilename" class="fw-semibold mb-2">Aucun wallet sélectionné.</p>
|
||||
<dl class="row small text-start mb-0">
|
||||
<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>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -81,14 +86,14 @@
|
||||
<span class="fw-semibold">Wallets disponibles</span>
|
||||
<small class="text-body-secondary"><i class="fa-solid fa-lock me-1" aria-hidden="true"></i>verrouillé · <i class="fa-solid fa-lock-open me-1" aria-hidden="true"></i>ouvert</small>
|
||||
</div>
|
||||
<button class="btn btn-outline-primary btn-sm" type="button" data-shell-action="refresh-wallets" disabled>
|
||||
<button class="btn btn-outline-primary btn-sm" type="button" data-shell-action="refresh-wallets">
|
||||
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table id="walletInventoryTable" class="table table-striped table-hover align-middle w-100">
|
||||
<thead><tr><th>État</th><th>Filename</th><th>Format</th><th>VIEW</th><th>Inspection</th></tr></thead>
|
||||
<tbody></tbody>
|
||||
<tbody id="walletInventoryBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,7 +107,7 @@
|
||||
</div>
|
||||
</main>
|
||||
<footer class="app-footer bg-dark text-white-50 d-flex align-items-center px-4">
|
||||
<span>KSP Wallet Desk · Config composition pre.003</span>
|
||||
<span>KSP Wallet Desk · locked inventory pre.004</span>
|
||||
</footer>
|
||||
<script type="module" src="./ts/main.ts"></script>
|
||||
</body>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/frontend/ts/main.ts
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
import "bootstrap";
|
||||
import DataTable from "datatables.net-bs5";
|
||||
@@ -8,7 +8,10 @@ import ResizeObserver from "resize-observer-polyfill";
|
||||
import "simplebar";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import type { RuntimeStatusDto } from "./bindings/ksp_app_wallet_desk/dto_common/RuntimeStatusDto.ts";
|
||||
import { frontendDebug, frontendInfo, frontendTrace, installFrontendConsoleBridge } from "./frontend_log";
|
||||
import type { LockedWalletDto } from "./bindings/ksp_app_wallet_desk/wallet_inventory/LockedWalletDto.ts";
|
||||
import type { WalletInventoryEntryDto } from "./bindings/ksp_app_wallet_desk/wallet_inventory/WalletInventoryEntryDto.ts";
|
||||
import type { WalletSelectionRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_inventory/WalletSelectionRequestDto.ts";
|
||||
import { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log";
|
||||
import { invokeKsp } from "./invoke";
|
||||
import "../sass/main.scss";
|
||||
|
||||
@@ -95,20 +98,169 @@ function bindNavigation(): void {
|
||||
frontendTrace("main", "Wallet Desk navigation handlers installed");
|
||||
}
|
||||
|
||||
function appendIconText(cell: HTMLTableCellElement, iconClass: string, text: string): void {
|
||||
const icon = document.createElement("i");
|
||||
icon.className = `fa-solid ${iconClass} me-2`;
|
||||
icon.setAttribute("aria-hidden", "true");
|
||||
const label = document.createElement("span");
|
||||
label.textContent = text;
|
||||
cell.append(icon, label);
|
||||
}
|
||||
|
||||
function renderWalletInventoryRow(entry: WalletInventoryEntryDto): HTMLTableRowElement {
|
||||
const row = document.createElement("tr");
|
||||
row.dataset.walletId = entry.walletId;
|
||||
row.dataset.walletSelectable = entry.inspectionStatus === "valid" ? "true" : "false";
|
||||
row.dataset.inspectionStatus = entry.inspectionStatus;
|
||||
const state = document.createElement("td");
|
||||
if (entry.state === "locked") {
|
||||
appendIconText(state, "fa-lock", "Locked");
|
||||
} else {
|
||||
appendIconText(state, "fa-triangle-exclamation", "Erreur");
|
||||
}
|
||||
const filename = document.createElement("td");
|
||||
filename.textContent = entry.filename;
|
||||
const format = document.createElement("td");
|
||||
format.textContent = entry.formatVersion === null ? "—" : entry.formatVersion.toString();
|
||||
const view = document.createElement("td");
|
||||
view.textContent = entry.viewEnabled === null ? "—" : entry.viewEnabled ? "enabled" : "disabled";
|
||||
const inspection = document.createElement("td");
|
||||
if (entry.inspectionStatus === "valid") {
|
||||
inspection.textContent = "Valide";
|
||||
} else if (entry.diagnostic) {
|
||||
inspection.textContent = `Invalide — ${entry.diagnostic.domain}.${entry.diagnostic.code}`;
|
||||
inspection.title = entry.diagnostic.message;
|
||||
} else {
|
||||
inspection.textContent = "Invalide";
|
||||
}
|
||||
row.append(state, filename, format, view, inspection);
|
||||
return row;
|
||||
}
|
||||
|
||||
function initializeWalletTable(): void {
|
||||
if (DataTable.isDataTable("#walletInventoryTable")) {
|
||||
new DataTable("#walletInventoryTable").destroy();
|
||||
}
|
||||
new DataTable("#walletInventoryTable", {
|
||||
order: [[1, "asc"]],
|
||||
pageLength: 10,
|
||||
select: {
|
||||
selector: "tbody tr[data-wallet-selectable='true'] td",
|
||||
style: "single",
|
||||
},
|
||||
language: {
|
||||
emptyTable: "L'inventaire Wallet sera branché en pre.004.",
|
||||
emptyTable: "Aucun wallet .kspwallet disponible.",
|
||||
search: "Filtrer :",
|
||||
zeroRecords: "Aucun wallet correspondant.",
|
||||
},
|
||||
});
|
||||
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.003-config-wallet" });
|
||||
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.004-wallet-inventory" });
|
||||
}
|
||||
|
||||
function renderWalletInventory(entries: WalletInventoryEntryDto[]): void {
|
||||
if (DataTable.isDataTable("#walletInventoryTable")) {
|
||||
new DataTable("#walletInventoryTable").destroy();
|
||||
}
|
||||
const body = document.querySelector<HTMLTableSectionElement>("#walletInventoryBody");
|
||||
if (body) {
|
||||
body.replaceChildren(...entries.map(entry => renderWalletInventoryRow(entry)));
|
||||
}
|
||||
initializeWalletTable();
|
||||
const invalidCount = entries.filter(entry => entry.inspectionStatus === "invalid").length;
|
||||
frontendDebug("main", "Wallet inventory rendered", { entryCount: entries.length, invalidCount });
|
||||
}
|
||||
|
||||
function clearSelectedWallet(): void {
|
||||
const icon = document.querySelector<HTMLElement>("#currentWalletIcon");
|
||||
const filename = document.querySelector<HTMLElement>("#currentWalletFilename");
|
||||
const state = document.querySelector<HTMLElement>("#currentWalletState");
|
||||
const format = document.querySelector<HTMLElement>("#currentWalletFormat");
|
||||
const view = document.querySelector<HTMLElement>("#currentWalletView");
|
||||
if (icon) {
|
||||
icon.className = "fa-solid fa-lock fa-2x mb-3 text-body-secondary";
|
||||
}
|
||||
if (filename) {
|
||||
filename.textContent = "Aucun wallet sélectionné.";
|
||||
}
|
||||
if (state) {
|
||||
state.textContent = "—";
|
||||
}
|
||||
if (format) {
|
||||
format.textContent = "—";
|
||||
}
|
||||
if (view) {
|
||||
view.textContent = "—";
|
||||
}
|
||||
}
|
||||
|
||||
function renderSelectedWallet(wallet: LockedWalletDto): void {
|
||||
const icon = document.querySelector<HTMLElement>("#currentWalletIcon");
|
||||
const filename = document.querySelector<HTMLElement>("#currentWalletFilename");
|
||||
const state = document.querySelector<HTMLElement>("#currentWalletState");
|
||||
const format = document.querySelector<HTMLElement>("#currentWalletFormat");
|
||||
const view = document.querySelector<HTMLElement>("#currentWalletView");
|
||||
if (icon) {
|
||||
icon.className = "fa-solid fa-lock fa-2x mb-3";
|
||||
}
|
||||
if (filename) {
|
||||
filename.textContent = wallet.filename;
|
||||
}
|
||||
if (state) {
|
||||
state.textContent = "Locked";
|
||||
}
|
||||
if (format) {
|
||||
format.textContent = wallet.formatVersion.toString();
|
||||
}
|
||||
if (view) {
|
||||
view.textContent = wallet.viewEnabled ? "enabled" : "disabled";
|
||||
}
|
||||
frontendDebug("main", "Locked Wallet selection rendered", { walletId: wallet.walletId, formatVersion: wallet.formatVersion, viewEnabled: wallet.viewEnabled });
|
||||
}
|
||||
|
||||
async function selectWallet(walletId: string): Promise<void> {
|
||||
const request: WalletSelectionRequestDto = { walletId };
|
||||
try {
|
||||
const wallet = await invokeKsp<LockedWalletDto>("main", "select_wallet", { request });
|
||||
renderSelectedWallet(wallet);
|
||||
} catch {
|
||||
clearSelectedWallet();
|
||||
frontendWarn("main", "Locked Wallet selection failed", { walletId });
|
||||
}
|
||||
}
|
||||
|
||||
function bindWalletTableSelection(): void {
|
||||
const table = document.querySelector<HTMLTableElement>("#walletInventoryTable");
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
table.addEventListener("click", event => {
|
||||
const source = event.target;
|
||||
if (!(source instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
const row = source.closest<HTMLTableRowElement>("tbody tr[data-wallet-id]");
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
const walletId = row.dataset.walletId;
|
||||
if (!walletId) {
|
||||
return;
|
||||
}
|
||||
if (row.dataset.walletSelectable !== "true") {
|
||||
frontendDebug("main", "Invalid Wallet inventory row selection ignored", { walletId });
|
||||
return;
|
||||
}
|
||||
frontendTrace("main", "Wallet inventory row selected", { walletId });
|
||||
void selectWallet(walletId);
|
||||
});
|
||||
frontendTrace("main", "Wallet inventory row selection handler installed");
|
||||
}
|
||||
|
||||
async function loadWalletInventory(command: "list_wallets" | "refresh_wallets"): Promise<void> {
|
||||
frontendDebug("main", "Wallet inventory load requested", { command });
|
||||
const entries = await invokeKsp<WalletInventoryEntryDto[]>("main", command);
|
||||
clearSelectedWallet();
|
||||
renderWalletInventory(entries);
|
||||
}
|
||||
|
||||
function renderRuntimeStatus(status: RuntimeStatusDto): void {
|
||||
@@ -158,7 +310,7 @@ function renderRuntimeStatus(status: RuntimeStatusDto): void {
|
||||
phase.textContent = status.shellPhase;
|
||||
}
|
||||
if (shellStatus) {
|
||||
shellStatus.textContent = "Config Wallet Desk résolue ; répertoire Wallet prêt.";
|
||||
shellStatus.textContent = "Config résolue ; inventaire Wallet prêt.";
|
||||
}
|
||||
frontendTrace("main", "Wallet Desk runtime status rendered", {
|
||||
compositeProfile: status.activeCompositeProfile,
|
||||
@@ -177,7 +329,13 @@ async function loadRuntimeStatus(): Promise<void> {
|
||||
function bindShellActions(): void {
|
||||
document.querySelectorAll<HTMLButtonElement>("[data-shell-action]").forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
frontendDebug("main", "Wallet Desk shell action clicked", { action: button.dataset.shellAction ?? "unknown", enabled: !button.disabled });
|
||||
const action = button.dataset.shellAction ?? "unknown";
|
||||
frontendDebug("main", "Wallet Desk shell action clicked", { action, enabled: !button.disabled });
|
||||
if (action === "refresh-wallets" && !button.disabled) {
|
||||
void loadWalletInventory("refresh_wallets").catch(() => {
|
||||
frontendWarn("main", "Wallet inventory refresh failed");
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
frontendTrace("main", "Wallet Desk shell action handlers installed");
|
||||
@@ -188,17 +346,19 @@ async function initializeMain(): Promise<void> {
|
||||
frontendInfo("main", "Wallet Desk main frontend loaded", { windowLabel });
|
||||
bindFrontendInteractions();
|
||||
bindNavigation();
|
||||
bindWalletTableSelection();
|
||||
bindShellActions();
|
||||
initializeWalletTable();
|
||||
activateView("dashboard", "startup");
|
||||
try {
|
||||
await loadRuntimeStatus();
|
||||
await loadWalletInventory("list_wallets");
|
||||
} catch {
|
||||
renderWalletInventory([]);
|
||||
const shellStatus = document.querySelector<HTMLElement>("#shellStatus");
|
||||
if (shellStatus) {
|
||||
shellStatus.textContent = "Le statut runtime n'a pas pu être chargé.";
|
||||
shellStatus.textContent = "Le statut runtime ou l'inventaire Wallet n'a pas pu être chargé.";
|
||||
}
|
||||
frontendTrace("main", "Wallet Desk shell status replaced", { status: "runtime_error" });
|
||||
frontendWarn("main", "Wallet Desk startup data load failed");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ksp-app-wallet-desk",
|
||||
"private": true,
|
||||
"version": "0.2.6-pre.3.fix.4",
|
||||
"version": "0.2.6-pre.4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/app_state.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Shared backend state owned by the Wallet Desk Tauri application.
|
||||
|
||||
@@ -61,7 +61,7 @@ impl AppState {
|
||||
});
|
||||
}
|
||||
|
||||
/// Builds the safe Config-composition status DTO exposed during pre.003.
|
||||
/// Builds the safe runtime status DTO exposed by the Wallet Desk shell.
|
||||
pub(crate) fn runtime_status(&self) -> ksp_core_lib::Result<crate::RuntimeStatusDto> {
|
||||
let document_count = self.config_management.engine().registry().descriptors().count();
|
||||
let document_count = u32::try_from(document_count);
|
||||
@@ -100,13 +100,19 @@ 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.003-config-wallet".to_owned(),
|
||||
shell_phase: "pre.004-wallet-inventory".to_owned(),
|
||||
startup_diagnostic: runtime.startup_diagnostic.clone(),
|
||||
wallets_directory: resolved.wallets_directory().to_string_lossy().into_owned(),
|
||||
wallets_subdirectory,
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the effective Config-managed Wallet directory used by inventory operations.
|
||||
#[must_use]
|
||||
pub(crate) fn wallet_inventory_root(&self) -> &std::path::Path {
|
||||
return self.wallet_config_startup.resolved().effective_wallets_directory();
|
||||
}
|
||||
|
||||
/// Returns the resolved common splash timings captured during bootstrap.
|
||||
#[must_use]
|
||||
pub(crate) const fn splash_settings(&self) -> crate::SplashSettings {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/constants.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Logging targets, domains and composite component identifiers owned by Wallet Desk.
|
||||
|
||||
@@ -17,6 +17,8 @@ pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
|
||||
pub(crate) const TRACING_DOMAIN_SHELL: &str = "wallet.shell";
|
||||
/// Structured domain used while preparing Wallet filesystem roots.
|
||||
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 Tauri window lifecycle operations.
|
||||
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
|
||||
/// Owning target for backend events emitted by Wallet Desk.
|
||||
@@ -27,3 +29,5 @@ pub(crate) const TRACING_TARGET_FRONTEND: &str = "ksp-app-wallet-desk.frontend";
|
||||
pub(crate) const TRACING_TARGET_FRONTEND_MAIN: &str = "ksp-app-wallet-desk.frontend.main";
|
||||
/// Owning target for splash-window frontend events.
|
||||
pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-wallet-desk.frontend.splash";
|
||||
/// Native Wallet filename suffix accepted by the Wallet Desk inventory.
|
||||
pub(crate) const WALLET_FILE_SUFFIX: &str = ".kspwallet";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/dto_common.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Common Tauri DTOs shared by Wallet Desk shell commands.
|
||||
|
||||
@@ -53,7 +53,7 @@ pub(crate) struct RuntimeStatusDto {
|
||||
pub(crate) fallback_logging_active: bool,
|
||||
/// Whether bootstrap created the configured global Wallet root.
|
||||
pub(crate) root_wallets_directory_created_on_startup: bool,
|
||||
/// Current implementation phase exposed for the Config composition tranche.
|
||||
/// Current implementation phase exposed by the Wallet Desk shell.
|
||||
pub(crate) shell_phase: String,
|
||||
/// Safe startup diagnostic that caused fallback Logging, when applicable.
|
||||
pub(crate) startup_diagnostic: std::option::Option<CommandErrorDto>,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/errors.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Application-local error codes for Wallet Desk composition and desktop runtime surfaces.
|
||||
|
||||
@@ -31,3 +31,7 @@ pub(crate) const ERROR_CODE_WALLET_DIRECTORY_INVALID: ksp_core_lib::ErrorCode =
|
||||
/// Wallet Desk could not inspect or create the configured Wallet directory tree.
|
||||
pub(crate) const ERROR_CODE_WALLET_DIRECTORY_PREPARE_FAILED: ksp_core_lib::ErrorCode =
|
||||
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");
|
||||
/// 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");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/lib.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Tauri desktop application shell for KSP Wallet management and inspection.
|
||||
|
||||
@@ -19,6 +19,7 @@ mod tauri;
|
||||
mod tw_main;
|
||||
mod tw_splash;
|
||||
mod wallet_config;
|
||||
mod wallet_inventory;
|
||||
|
||||
/// Runs the KSP wallet desktop application.
|
||||
pub use self::tauri::run;
|
||||
@@ -49,6 +50,8 @@ pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
|
||||
/// Structured domain used while preparing Wallet filesystem roots.
|
||||
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 Tauri window lifecycle operations.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
|
||||
/// Owning target for backend events emitted by Wallet Desk.
|
||||
@@ -59,6 +62,8 @@ pub(crate) use self::constants::TRACING_TARGET_FRONTEND;
|
||||
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN;
|
||||
/// Owning target for splash-window frontend events.
|
||||
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH;
|
||||
/// Native Wallet filename suffix accepted by inventory operations.
|
||||
pub(crate) use self::constants::WALLET_FILE_SUFFIX;
|
||||
/// Safe command error projection exposed to Tauri commands.
|
||||
pub(crate) use self::dto_common::CommandErrorDto;
|
||||
/// Initial application/runtime snapshot exposed to the Wallet Desk shell.
|
||||
@@ -89,6 +94,10 @@ pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED;
|
||||
pub(crate) use self::errors::ERROR_CODE_WALLET_DIRECTORY_INVALID;
|
||||
/// Wallet Desk could not inspect or create the configured Wallet directory tree.
|
||||
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;
|
||||
/// Wallet selection identifier is invalid or no longer eligible.
|
||||
pub(crate) use self::errors::ERROR_CODE_WALLET_SELECTION_INVALID;
|
||||
/// Log payload sent by Wallet Desk frontend scripts.
|
||||
pub(crate) use self::frontend_logging::FrontendLogPayloadDto;
|
||||
/// Emits one validated frontend event through the KSP Logging facade.
|
||||
@@ -111,3 +120,17 @@ pub(crate) use self::tw_splash::splash_frontend_ready_service;
|
||||
pub(crate) use self::wallet_config::WalletConfigStartup;
|
||||
/// Resolves the composite-selected Wallet Config and prepares its application-owned directory tree.
|
||||
pub(crate) use self::wallet_config::initialize_wallet_config;
|
||||
/// Safe locked Wallet projection returned after selection.
|
||||
pub(crate) use self::wallet_inventory::LockedWalletDto;
|
||||
/// Locked inspection outcome exposed by Wallet inventory rows.
|
||||
pub(crate) use self::wallet_inventory::WalletInspectionStatusDto;
|
||||
/// Safe Wallet inventory row projection.
|
||||
pub(crate) use self::wallet_inventory::WalletInventoryEntryDto;
|
||||
/// Visual lock/error state exposed by Wallet inventory rows.
|
||||
pub(crate) use self::wallet_inventory::WalletInventoryStateDto;
|
||||
/// Request DTO used for one root-scoped Wallet selection.
|
||||
pub(crate) use self::wallet_inventory::WalletSelectionRequestDto;
|
||||
/// Enumerates native Wallet files under the effective Config-managed root.
|
||||
pub(crate) use self::wallet_inventory::list_wallet_inventory;
|
||||
/// Re-inspects one root-scoped Wallet selection.
|
||||
pub(crate) use self::wallet_inventory::select_locked_wallet;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/tauri.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Tauri runtime assembly for the KSP wallet desktop application.
|
||||
|
||||
@@ -37,7 +37,14 @@ fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<taur
|
||||
|
||||
#[allow(clippy::question_mark_used)] // Tauri generates the question-mark operator internally for async command dispatch.
|
||||
fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||
return builder.invoke_handler(tauri::generate_handler![emit_frontend_log, get_runtime_status, splash_frontend_ready]);
|
||||
return builder.invoke_handler(tauri::generate_handler![
|
||||
emit_frontend_log,
|
||||
get_runtime_status,
|
||||
list_wallets,
|
||||
refresh_wallets,
|
||||
select_wallet,
|
||||
splash_frontend_ready
|
||||
]);
|
||||
}
|
||||
|
||||
fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||
@@ -73,6 +80,41 @@ fn get_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn list_wallets(state: tauri::State<'_, crate::AppState>) -> std::result::Result<std::vec::Vec<crate::WalletInventoryEntryDto>, crate::CommandErrorDto> {
|
||||
let root = state.wallet_inventory_root().to_path_buf();
|
||||
let result = crate::list_wallet_inventory(root.as_path()).await;
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn refresh_wallets(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<std::vec::Vec<crate::WalletInventoryEntryDto>, crate::CommandErrorDto> {
|
||||
let root = state.wallet_inventory_root().to_path_buf();
|
||||
let result = crate::list_wallet_inventory(root.as_path()).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 select_wallet(
|
||||
request: crate::WalletSelectionRequestDto,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::LockedWalletDto, crate::CommandErrorDto> {
|
||||
let root = state.wallet_inventory_root().to_path_buf();
|
||||
let result = crate::select_locked_wallet(root.as_path(), 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 splash_frontend_ready(
|
||||
app: tauri::AppHandle,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/wallet_config.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Wallet Desk composition adapter for the standard Wallet Config and its application-owned directory preparation.
|
||||
|
||||
@@ -105,8 +105,9 @@ fn prepare_wallet_directories(resolved: &ksp_config_lib::ResolvedWalletConfig) -
|
||||
}
|
||||
|
||||
fn ensure_global_wallet_root(path: &std::path::Path) -> ksp_core_lib::Result<bool> {
|
||||
let metadata = std::fs::metadata(path);
|
||||
let metadata = std::fs::symlink_metadata(path);
|
||||
return match metadata {
|
||||
std::result::Result::Ok(metadata) if metadata.file_type().is_symlink() => directory_invalid(path, "configured Wallet root cannot be a symbolic link"),
|
||||
std::result::Result::Ok(metadata) if metadata.is_dir() => std::result::Result::Ok(false),
|
||||
std::result::Result::Ok(_) => directory_invalid(path, "configured Wallet root exists but is not a directory"),
|
||||
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
||||
|
||||
233
crates/ksp-app-wallet-desk/src/wallet_inventory.rs
Normal file
233
crates/ksp-app-wallet-desk/src/wallet_inventory.rs
Normal file
@@ -0,0 +1,233 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/wallet_inventory.rs
|
||||
// version: 1
|
||||
|
||||
//! Root-scoped Wallet inventory and locked-file selection for Wallet Desk.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Visual lock state projected for one Wallet inventory row.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_inventory/WalletInventoryStateDto.ts")]
|
||||
pub(crate) enum WalletInventoryStateDto {
|
||||
/// Eligible Wallet remains locked.
|
||||
Locked,
|
||||
/// Candidate could not be inspected as a valid locked Wallet.
|
||||
Error,
|
||||
}
|
||||
|
||||
/// Locked inspection outcome projected for one Wallet inventory row.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_inventory/WalletInspectionStatusDto.ts")]
|
||||
pub(crate) enum WalletInspectionStatusDto {
|
||||
/// Native Wallet inspection succeeded.
|
||||
Valid,
|
||||
/// Candidate inspection failed with a safe diagnostic.
|
||||
Invalid,
|
||||
}
|
||||
|
||||
/// Safe inventory row exposed while Wallet contents remain locked.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_inventory/WalletInventoryEntryDto.ts")]
|
||||
pub(crate) struct WalletInventoryEntryDto {
|
||||
/// Safe inspection diagnostic when the candidate is invalid.
|
||||
pub(crate) diagnostic: std::option::Option<crate::CommandErrorDto>,
|
||||
/// Native Wallet filename without its parent path.
|
||||
pub(crate) filename: String,
|
||||
/// Native Wallet format version when locked inspection succeeds.
|
||||
pub(crate) format_version: std::option::Option<u32>,
|
||||
/// Locked inspection outcome.
|
||||
pub(crate) inspection_status: WalletInspectionStatusDto,
|
||||
/// Current inventory lock/error state.
|
||||
pub(crate) state: WalletInventoryStateDto,
|
||||
/// Root-scoped identifier accepted by selection commands.
|
||||
pub(crate) wallet_id: String,
|
||||
/// Whether the locked Wallet advertises an enabled VIEW slot.
|
||||
pub(crate) view_enabled: std::option::Option<bool>,
|
||||
}
|
||||
|
||||
/// Safe locked Wallet projection returned after an explicit row selection.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_inventory/LockedWalletDto.ts")]
|
||||
pub(crate) struct LockedWalletDto {
|
||||
/// Native Wallet filename without its parent path.
|
||||
pub(crate) filename: String,
|
||||
/// Native Wallet format version.
|
||||
pub(crate) format_version: u32,
|
||||
/// Root-scoped identifier accepted by later Wallet Desk operations.
|
||||
pub(crate) wallet_id: String,
|
||||
/// Whether the locked Wallet advertises an enabled VIEW slot.
|
||||
pub(crate) view_enabled: bool,
|
||||
}
|
||||
|
||||
/// Request DTO for selecting one root-scoped Wallet inventory entry.
|
||||
#[derive(serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_inventory/WalletSelectionRequestDto.ts")]
|
||||
pub(crate) struct WalletSelectionRequestDto {
|
||||
/// Root-scoped Wallet identifier returned by the inventory.
|
||||
pub(crate) wallet_id: String,
|
||||
}
|
||||
|
||||
/// Enumerates eligible native Wallet files and returns deterministic locked projections.
|
||||
pub(crate) async fn list_wallet_inventory(root: &std::path::Path) -> ksp_core_lib::Result<std::vec::Vec<WalletInventoryEntryDto>> {
|
||||
let root_text = root.to_string_lossy().into_owned();
|
||||
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_INVENTORY, root_path = root_text.as_str(), "Wallet inventory refresh started");
|
||||
let reader = tokio::fs::read_dir(root).await;
|
||||
let mut reader = match reader {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(inventory_io_error(root, "effective Wallet directory cannot be enumerated", error)),
|
||||
};
|
||||
let mut entries = std::vec::Vec::new();
|
||||
loop {
|
||||
let next = reader.next_entry().await;
|
||||
let entry = match next {
|
||||
std::result::Result::Ok(std::option::Option::Some(value)) => value,
|
||||
std::result::Result::Ok(std::option::Option::None) => break,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(inventory_io_error(root, "Wallet directory entry cannot be read", error)),
|
||||
};
|
||||
let filename = entry.file_name();
|
||||
let filename = match filename.to_str() {
|
||||
std::option::Option::Some(value) if is_wallet_filename(value) => value.to_owned(),
|
||||
std::option::Option::Some(_) => continue,
|
||||
std::option::Option::None => {
|
||||
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_INVENTORY, "Wallet inventory skipped a non-UTF-8 filename");
|
||||
continue;
|
||||
},
|
||||
};
|
||||
let path = entry.path();
|
||||
let metadata = tokio::fs::symlink_metadata(path.as_path()).await;
|
||||
let metadata = match metadata {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
entries.push(invalid_inventory_entry(filename, inventory_io_error(path.as_path(), "Wallet candidate metadata cannot be inspected", error)));
|
||||
continue;
|
||||
},
|
||||
};
|
||||
if metadata.file_type().is_symlink() || !metadata.is_file() {
|
||||
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_INVENTORY, filename = filename.as_str(), is_symlink = metadata.file_type().is_symlink(), "Wallet inventory skipped a non-regular candidate");
|
||||
continue;
|
||||
}
|
||||
let inspected = ksp_wallet_lib::inspect_locked_wallet_file_v1(path.as_path()).await;
|
||||
match inspected {
|
||||
std::result::Result::Ok(locked) => entries.push(valid_inventory_entry(filename, locked)),
|
||||
std::result::Result::Err(error) => {
|
||||
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_INVENTORY, filename = filename.as_str(), error_domain = error.code().domain(), error_code = error.code().code(), "Wallet inventory candidate failed locked inspection");
|
||||
entries.push(invalid_inventory_entry(filename, error));
|
||||
},
|
||||
}
|
||||
}
|
||||
entries.sort_by(|left, right| {
|
||||
return left.filename.cmp(&right.filename);
|
||||
});
|
||||
let invalid_count = entries
|
||||
.iter()
|
||||
.filter(|entry| {
|
||||
return entry.inspection_status == WalletInspectionStatusDto::Invalid;
|
||||
})
|
||||
.count();
|
||||
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_INVENTORY, root_path = root_text.as_str(), entry_count = entries.len(), invalid_count, "Wallet inventory refresh completed");
|
||||
return std::result::Result::Ok(entries);
|
||||
}
|
||||
|
||||
/// Re-resolves and re-inspects one selected inventory identifier without exposing its full path.
|
||||
pub(crate) async fn select_locked_wallet(root: &std::path::Path, request: WalletSelectionRequestDto) -> ksp_core_lib::Result<LockedWalletDto> {
|
||||
let path = resolve_wallet_path(root, request.wallet_id.as_str()).await;
|
||||
let path = match path {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let locked = ksp_wallet_lib::inspect_locked_wallet_file_v1(path.as_path()).await;
|
||||
let locked = match locked {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_INVENTORY, wallet_id = request.wallet_id.as_str(), format_version = locked.format_version(), view_enabled = locked.view_enabled(), "Locked Wallet selected from inventory");
|
||||
return std::result::Result::Ok(LockedWalletDto {
|
||||
filename: request.wallet_id.clone(),
|
||||
format_version: locked.format_version(),
|
||||
wallet_id: request.wallet_id,
|
||||
view_enabled: locked.view_enabled(),
|
||||
});
|
||||
}
|
||||
|
||||
fn valid_inventory_entry(filename: String, locked: ksp_wallet_lib::LockedWalletInfo) -> WalletInventoryEntryDto {
|
||||
return WalletInventoryEntryDto {
|
||||
diagnostic: std::option::Option::None,
|
||||
filename: filename.clone(),
|
||||
format_version: std::option::Option::Some(locked.format_version()),
|
||||
inspection_status: WalletInspectionStatusDto::Valid,
|
||||
state: WalletInventoryStateDto::Locked,
|
||||
wallet_id: filename,
|
||||
view_enabled: std::option::Option::Some(locked.view_enabled()),
|
||||
};
|
||||
}
|
||||
|
||||
fn invalid_inventory_entry(filename: String, error: ksp_core_lib::Error) -> WalletInventoryEntryDto {
|
||||
return WalletInventoryEntryDto {
|
||||
diagnostic: std::option::Option::Some(crate::CommandErrorDto::from_error(&error)),
|
||||
filename: filename.clone(),
|
||||
format_version: std::option::Option::None,
|
||||
inspection_status: WalletInspectionStatusDto::Invalid,
|
||||
state: WalletInventoryStateDto::Error,
|
||||
wallet_id: filename,
|
||||
view_enabled: std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
fn is_wallet_filename(filename: &str) -> bool {
|
||||
let path = std::path::Path::new(filename);
|
||||
let components = path.components().count();
|
||||
return components == 1
|
||||
&& !filename.contains('/')
|
||||
&& !filename.contains('\\')
|
||||
&& filename.ends_with(crate::WALLET_FILE_SUFFIX)
|
||||
&& filename.len() > crate::WALLET_FILE_SUFFIX.len();
|
||||
}
|
||||
|
||||
async fn resolve_wallet_path(root: &std::path::Path, wallet_id: &str) -> ksp_core_lib::Result<std::path::PathBuf> {
|
||||
if !is_wallet_filename(wallet_id) {
|
||||
return selection_invalid(wallet_id, "Wallet identifier must be one UTF-8 filename ending in .kspwallet");
|
||||
}
|
||||
let path = root.join(wallet_id);
|
||||
let metadata = tokio::fs::symlink_metadata(path.as_path()).await;
|
||||
let metadata = match metadata {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
||||
return selection_invalid(wallet_id, "Wallet selection no longer exists in the effective inventory directory");
|
||||
},
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(inventory_io_error(path.as_path(), "Wallet selection metadata cannot be inspected", error));
|
||||
},
|
||||
};
|
||||
if metadata.file_type().is_symlink() || !metadata.is_file() {
|
||||
return selection_invalid(wallet_id, "Wallet selection must resolve to a regular non-symlink file");
|
||||
}
|
||||
return std::result::Result::Ok(path);
|
||||
}
|
||||
|
||||
fn selection_invalid<T>(wallet_id: &str, reason: &'static str) -> ksp_core_lib::Result<T> {
|
||||
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_INVENTORY, wallet_id, reason, "Wallet inventory selection rejected");
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_SELECTION_INVALID, "Wallet inventory selection is invalid")
|
||||
.with_context("wallet_id", wallet_id)
|
||||
.with_context("reason", reason),
|
||||
);
|
||||
}
|
||||
|
||||
fn inventory_io_error(path: &std::path::Path, reason: &'static str, source: std::io::Error) -> ksp_core_lib::Error {
|
||||
let path_text = path.to_string_lossy().into_owned();
|
||||
let source_kind = std::format!("{:?}", source.kind());
|
||||
ksp_logging_lib::error!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_INVENTORY, path = path_text.as_str(), reason, source_kind = source_kind.as_str(), "Wallet inventory filesystem operation failed");
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_INVENTORY_FAILED, "Wallet inventory filesystem operation failed")
|
||||
.with_context("path", path_text)
|
||||
.with_context("reason", reason)
|
||||
.with_source(source);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/wallet_inventory.rs"]
|
||||
mod tests;
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "KSP Wallet Desk",
|
||||
"version": "0.2.6-pre.3.fix.4",
|
||||
"version": "0.2.6-pre.4",
|
||||
"identifier": "com.sasedev.ksp-app-wallet-desk",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Desktop build, shell and Config-status contract audits for Wallet Desk.
|
||||
|
||||
@@ -86,6 +86,28 @@ fn shell_contains_wallet_navigation_datatable_and_lock_state_icons() {
|
||||
assert!(main.contains("simplebar"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_004_inventory_uses_wallet_library_and_exposes_only_locked_safe_fields() {
|
||||
let root = app_root();
|
||||
let manifest = read_text(root.join("Cargo.toml").as_path());
|
||||
let tauri = read_text(root.join("src/tauri.rs").as_path());
|
||||
let inventory = read_text(root.join("src/wallet_inventory.rs").as_path());
|
||||
let main = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||
assert!(manifest.contains("ksp-wallet-lib = { path = \"../ksp-wallet-lib\" }"));
|
||||
assert!(inventory.contains("ksp_wallet_lib::inspect_locked_wallet_file_v1"));
|
||||
assert!(inventory.contains("symlink_metadata"));
|
||||
assert!(inventory.contains("WALLET_FILE_SUFFIX"));
|
||||
assert!(tauri.contains("list_wallets"));
|
||||
assert!(tauri.contains("refresh_wallets"));
|
||||
assert!(tauri.contains("select_wallet"));
|
||||
assert!(main.contains("WalletInventoryEntryDto"));
|
||||
assert!(main.contains("LockedWalletDto"));
|
||||
assert!(main.contains("refresh_wallets"));
|
||||
assert!(!inventory.contains("pubkey"));
|
||||
assert!(!inventory.contains("alias"));
|
||||
assert!(!inventory.contains("notes"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frontend_control_interactions_are_trace_logged_without_control_values() {
|
||||
let root = app_root();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/tests/desktop_security.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Static desktop security contracts for the Wallet Desk pre.002 shell.
|
||||
|
||||
@@ -36,3 +36,13 @@ fn frontend_shell_uses_no_browser_native_dialog_or_secret_storage() {
|
||||
assert!(!html.contains(forbidden), "forbidden frontend primitive {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn locked_inventory_frontend_never_receives_protected_wallet_identity_fields() {
|
||||
let root = app_root();
|
||||
let inventory = read_text(root.join("src/wallet_inventory.rs").as_path());
|
||||
assert!(!inventory.contains("pubkey:"));
|
||||
assert!(!inventory.contains("alias:"));
|
||||
assert!(!inventory.contains("notes:"));
|
||||
assert!(!inventory.contains("password:"));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/unit_tests/wallet_config.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn directory_preparation_creates_missing_root_and_nested_profile_path() {
|
||||
@@ -37,6 +37,30 @@ fn directory_preparation_rejects_existing_non_directory_root() {
|
||||
cleanup_fixture(root.as_path());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn global_wallet_root_rejects_symbolic_link() {
|
||||
let root = fixture_path("root-symlink");
|
||||
let outside = fixture_path("root-symlink-outside");
|
||||
cleanup_fixture(root.as_path());
|
||||
cleanup_fixture(outside.as_path());
|
||||
let outside_created = std::fs::create_dir_all(outside.as_path());
|
||||
assert!(outside_created.is_ok());
|
||||
if outside_created.is_ok() {
|
||||
let linked = std::os::unix::fs::symlink(outside.as_path(), root.as_path());
|
||||
assert!(linked.is_ok());
|
||||
if linked.is_ok() {
|
||||
let result = super::ensure_global_wallet_root(root.as_path());
|
||||
assert!(result.is_err());
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_WALLET_DIRECTORY_INVALID);
|
||||
}
|
||||
}
|
||||
}
|
||||
cleanup_fixture(root.as_path());
|
||||
cleanup_fixture(outside.as_path());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn profile_directory_preparation_rejects_symbolic_link_components() {
|
||||
|
||||
124
crates/ksp-app-wallet-desk/unit_tests/wallet_inventory.rs
Normal file
124
crates/ksp-app-wallet-desk/unit_tests/wallet_inventory.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
// file: crates/ksp-app-wallet-desk/unit_tests/wallet_inventory.rs
|
||||
// version: 1
|
||||
|
||||
const VALID_WALLET: &[u8] = include_bytes!("../../ksp-wallet-lib/tests/fixtures/kspwallet_v1_full_vector.json");
|
||||
|
||||
#[test]
|
||||
fn inventory_lists_regular_wallets_reports_invalid_candidates_and_ignores_other_entries() {
|
||||
let root = fixture_path("inventory");
|
||||
cleanup_fixture(root.as_path());
|
||||
let created = std::fs::create_dir_all(root.as_path());
|
||||
assert!(created.is_ok(), "inventory fixture root should be creatable: {created:?}");
|
||||
if created.is_ok() {
|
||||
assert!(std::fs::write(root.join("beta.kspwallet"), VALID_WALLET).is_ok());
|
||||
assert!(std::fs::write(root.join("alpha.kspwallet"), b"not-json").is_ok());
|
||||
assert!(std::fs::write(root.join("ignored.txt"), VALID_WALLET).is_ok());
|
||||
assert!(std::fs::create_dir(root.join("directory.kspwallet")).is_ok());
|
||||
let runtime = tokio::runtime::Builder::new_current_thread().build();
|
||||
assert!(runtime.is_ok(), "test runtime should be constructible: {runtime:?}");
|
||||
if let std::result::Result::Ok(runtime) = runtime {
|
||||
let inventory = runtime.block_on(crate::list_wallet_inventory(root.as_path()));
|
||||
assert!(inventory.is_ok(), "inventory should complete: {inventory:?}");
|
||||
if let std::result::Result::Ok(entries) = inventory {
|
||||
assert_eq!(entries.len(), 2);
|
||||
assert_eq!(entries[0].filename, "alpha.kspwallet");
|
||||
assert_eq!(entries[0].inspection_status, crate::WalletInspectionStatusDto::Invalid);
|
||||
assert_eq!(entries[0].state, crate::WalletInventoryStateDto::Error);
|
||||
assert!(entries[0].diagnostic.is_some());
|
||||
assert_eq!(entries[1].filename, "beta.kspwallet");
|
||||
assert_eq!(entries[1].inspection_status, crate::WalletInspectionStatusDto::Valid);
|
||||
assert_eq!(entries[1].state, crate::WalletInventoryStateDto::Locked);
|
||||
assert_eq!(entries[1].format_version, std::option::Option::Some(1));
|
||||
assert!(entries[1].view_enabled.is_some());
|
||||
assert!(entries[1].diagnostic.is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
cleanup_fixture(root.as_path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_rejects_traversal_and_reinspects_one_valid_locked_wallet() {
|
||||
let root = fixture_path("selection");
|
||||
cleanup_fixture(root.as_path());
|
||||
let created = std::fs::create_dir_all(root.as_path());
|
||||
assert!(created.is_ok(), "selection fixture root should be creatable: {created:?}");
|
||||
if created.is_ok() {
|
||||
assert!(std::fs::write(root.join("selected.kspwallet"), VALID_WALLET).is_ok());
|
||||
let runtime = tokio::runtime::Builder::new_current_thread().build();
|
||||
assert!(runtime.is_ok(), "test runtime should be constructible: {runtime:?}");
|
||||
if let std::result::Result::Ok(runtime) = runtime {
|
||||
let rejected = runtime
|
||||
.block_on(crate::select_locked_wallet(root.as_path(), crate::WalletSelectionRequestDto { wallet_id: "../selected.kspwallet".to_owned() }));
|
||||
assert!(rejected.is_err());
|
||||
if let std::result::Result::Err(error) = rejected {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_WALLET_SELECTION_INVALID);
|
||||
}
|
||||
let rejected_backslash = runtime
|
||||
.block_on(crate::select_locked_wallet(root.as_path(), crate::WalletSelectionRequestDto { wallet_id: "..\\selected.kspwallet".to_owned() }));
|
||||
assert!(rejected_backslash.is_err());
|
||||
if let std::result::Result::Err(error) = rejected_backslash {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_WALLET_SELECTION_INVALID);
|
||||
}
|
||||
let selected =
|
||||
runtime.block_on(crate::select_locked_wallet(root.as_path(), crate::WalletSelectionRequestDto { wallet_id: "selected.kspwallet".to_owned() }));
|
||||
assert!(selected.is_ok(), "valid locked Wallet should be selectable: {selected:?}");
|
||||
if let std::result::Result::Ok(selected) = selected {
|
||||
assert_eq!(selected.wallet_id, "selected.kspwallet");
|
||||
assert_eq!(selected.filename, "selected.kspwallet");
|
||||
assert_eq!(selected.format_version, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
cleanup_fixture(root.as_path());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn inventory_and_selection_exclude_symbolic_link_wallet_entries() {
|
||||
let root = fixture_path("symlink-inventory");
|
||||
let outside = fixture_path("symlink-wallet");
|
||||
cleanup_fixture(root.as_path());
|
||||
cleanup_fixture(outside.as_path());
|
||||
let root_created = std::fs::create_dir_all(root.as_path());
|
||||
let outside_written = std::fs::write(outside.as_path(), VALID_WALLET);
|
||||
assert!(root_created.is_ok());
|
||||
assert!(outside_written.is_ok());
|
||||
if root_created.is_ok() && outside_written.is_ok() {
|
||||
let linked = std::os::unix::fs::symlink(outside.as_path(), root.join("linked.kspwallet"));
|
||||
assert!(linked.is_ok());
|
||||
if linked.is_ok() {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread().build();
|
||||
assert!(runtime.is_ok());
|
||||
if let std::result::Result::Ok(runtime) = runtime {
|
||||
let inventory = runtime.block_on(crate::list_wallet_inventory(root.as_path()));
|
||||
assert_eq!(inventory.as_ref().ok().map(std::vec::Vec::len), std::option::Option::Some(0));
|
||||
let selected = runtime
|
||||
.block_on(crate::select_locked_wallet(root.as_path(), crate::WalletSelectionRequestDto { wallet_id: "linked.kspwallet".to_owned() }));
|
||||
assert!(selected.is_err());
|
||||
if let std::result::Result::Err(error) = selected {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_WALLET_SELECTION_INVALID);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cleanup_fixture(root.as_path());
|
||||
cleanup_fixture(outside.as_path());
|
||||
}
|
||||
|
||||
fn fixture_path(name: &str) -> std::path::PathBuf {
|
||||
return std::env::temp_dir().join(std::format!("ksp-wallet-desk-{name}-{}", std::process::id()));
|
||||
}
|
||||
|
||||
fn cleanup_fixture(path: &std::path::Path) {
|
||||
let metadata = std::fs::symlink_metadata(path);
|
||||
match metadata {
|
||||
std::result::Result::Ok(metadata) if metadata.is_dir() && !metadata.file_type().is_symlink() => {
|
||||
let _ = std::fs::remove_dir_all(path);
|
||||
},
|
||||
std::result::Result::Ok(_) => {
|
||||
let _ = std::fs::remove_file(path);
|
||||
},
|
||||
std::result::Result::Err(_) => {},
|
||||
}
|
||||
}
|
||||
180
deltas/0.2.6/pre.004.md
Normal file
180
deltas/0.2.6/pre.004.md
Normal file
@@ -0,0 +1,180 @@
|
||||
<!-- file: deltas/0.2.6/pre.004.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.6-pre.004` — inventory `.kspwallet` et inspection locked
|
||||
|
||||
## Base requise
|
||||
|
||||
```text
|
||||
0.2.6-pre.003-fix.004 appliquée
|
||||
workspace.package.version = 0.2.6-pre.3.fix.4
|
||||
```
|
||||
|
||||
Les preuves opérateur de `pre.003-fix.004` sont vertes : `cargo fmt --all`, audit structurel Python, `cargo check --workspace`, `cargo clippy --workspace --all-targets`, `cargo test -p ksp-app-config-desk`, `cargo test --workspace` et `cargo tauri dev -c crates/ksp-app-wallet-desk/tauri.conf.json`. Le runtime utilise le profil composite Logging `supertrace` et démarre correctement.
|
||||
|
||||
## Type de livraison
|
||||
|
||||
```text
|
||||
ksp-general-0.2.6-pre.004.zip
|
||||
```
|
||||
|
||||
Cette tranche ajoute une dépendance interne Wallet et du runtime Rust/frontend ; le signal technique devient :
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.2.6-pre.4
|
||||
commit = v0.2.6-pre.004
|
||||
```
|
||||
|
||||
Aucun tag stable.
|
||||
|
||||
## Objet
|
||||
|
||||
Brancher l'inventory réel des fichiers natifs `.kspwallet` dans Wallet Desk sans introduire encore de password, de handle VIEW/OWNER ni de projection d'identité protégée.
|
||||
|
||||
## Frontière Wallet
|
||||
|
||||
`ksp-app-wallet-desk` dépend désormais directement de `ksp-wallet-lib` et délègue l'inspection locked à :
|
||||
|
||||
```text
|
||||
ksp_wallet_lib::inspect_locked_wallet_file_v1
|
||||
```
|
||||
|
||||
Aucune crypto, parser `.kspwallet`, keypair ou signature n'est réimplémenté dans l'application.
|
||||
|
||||
## Inventory filesystem
|
||||
|
||||
La racine unique est :
|
||||
|
||||
```text
|
||||
ResolvedWalletConfig::effective_wallets_directory
|
||||
```
|
||||
|
||||
Politique :
|
||||
|
||||
```text
|
||||
read_dir asynchrone non récursif
|
||||
filename UTF-8
|
||||
suffixe exact .kspwallet
|
||||
un seul composant filename
|
||||
slash et backslash rejetés pour les sélections frontend
|
||||
fichiers réguliers uniquement
|
||||
symlinks d'entrée exclus
|
||||
répertoires même nommés *.kspwallet exclus
|
||||
tri déterministe par filename
|
||||
```
|
||||
|
||||
Le bootstrap Config durcit aussi la racine globale : un `wallets_directory` existant sous forme de symlink est désormais refusé au lieu d'être suivi par `metadata()`.
|
||||
|
||||
Une erreur de lecture de la racine fait échouer la commande avec `wallet_desk.wallet_inventory_failed`. Une entrée `.kspwallet` présente mais invalide ne fait pas échouer tout l'inventory : elle reste visible avec état `error`, inspection `invalid` et `CommandErrorDto` sans context/source.
|
||||
|
||||
## DTOs locked
|
||||
|
||||
La tranche ajoute :
|
||||
|
||||
```text
|
||||
WalletInventoryEntryDto
|
||||
WalletInventoryStateDto
|
||||
WalletInspectionStatusDto
|
||||
LockedWalletDto
|
||||
WalletSelectionRequestDto
|
||||
```
|
||||
|
||||
Une ligne valide expose uniquement :
|
||||
|
||||
```text
|
||||
wallet_id / filename
|
||||
format_version
|
||||
view_enabled
|
||||
state = locked
|
||||
inspection_status = valid
|
||||
```
|
||||
|
||||
Une ligne invalide expose filename + diagnostic sûr. Aucun DTO ne porte Pubkey, alias, notes, password, path complet ou handle Wallet.
|
||||
|
||||
## Commandes Tauri
|
||||
|
||||
```text
|
||||
list_wallets
|
||||
refresh_wallets
|
||||
select_wallet
|
||||
```
|
||||
|
||||
`select_wallet` reçoit uniquement un `wallet_id`/filename, reconstruit le path sous la racine effective, rejette traversal/séparateurs/symlink/non-file puis relance l'inspection locked avant de répondre. La sélection n'installe pas encore un `WalletSession` durable ; celui-ci reste `pre.005`.
|
||||
|
||||
## Frontend
|
||||
|
||||
Le tableau Wallet devient un vrai DataTable alimenté par le backend :
|
||||
|
||||
```text
|
||||
État -> fa-lock ou fa-triangle-exclamation
|
||||
Filename -> texte DOM sûr
|
||||
Format -> version ou —
|
||||
VIEW -> enabled / disabled / —
|
||||
Inspection -> Valide ou code diagnostic sûr
|
||||
```
|
||||
|
||||
Le bouton Refresh appelle `refresh_wallets`. La sélection d'une ligne valide appelle `select_wallet` et affiche seulement filename/format/VIEW dans la carte « Wallet courant ». Les lignes invalides restent visibles mais ne sont pas sélectionnables. Un refresh purge la projection sélectionnée frontend afin de ne pas conserver une vue stale.
|
||||
|
||||
Le rendu utilise `textContent`/création DOM pour les valeurs issues du filesystem ; aucun filename n'est injecté par HTML.
|
||||
|
||||
## Logging
|
||||
|
||||
Les opérations suivantes sont instrumentées :
|
||||
|
||||
```text
|
||||
inventory refresh start/completion
|
||||
invalid locked inspection
|
||||
non-regular/symlink skip
|
||||
selection accepted/rejected
|
||||
frontend inventory load/render/refresh/row selection
|
||||
```
|
||||
|
||||
Le profil composite reste `supertrace`; aucun changement de `std.logging` n'est requis.
|
||||
|
||||
## Tests
|
||||
|
||||
Les canaris ajoutés couvrent :
|
||||
|
||||
```text
|
||||
valid .kspwallet + invalid .kspwallet dans le même inventory
|
||||
wrong extension ignorée
|
||||
subdirectory *.kspwallet ignoré
|
||||
tri déterministe
|
||||
traversal slash/backslash rejeté
|
||||
symlink Wallet ignoré et non sélectionnable sur Unix
|
||||
root wallets_directory symlink rejeté sur Unix
|
||||
DTO locked sans Pubkey/alias/notes/password
|
||||
commands Tauri list/refresh/select présentes
|
||||
frontend DataTable réellement branché sur les DTOs Wallet
|
||||
```
|
||||
|
||||
## Dépendances
|
||||
|
||||
Cargo :
|
||||
|
||||
```text
|
||||
ksp-wallet-lib = { path = "../ksp-wallet-lib" }
|
||||
tokio features += fs, rt
|
||||
```
|
||||
|
||||
Aucune dépendance npm et aucun script npm ne changent. **Aucune commande npm directe n'est nécessaire pour cette tranche.**
|
||||
|
||||
## 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-pre004 cargo tauri dev -c crates/ksp-app-wallet-desk/tauri.conf.json
|
||||
```
|
||||
|
||||
Pour la vérification UI, placer si souhaité un ou plusieurs `.kspwallet` existants dans `var/wallet-desk-pre004`, ainsi qu'un fichier `invalid.kspwallet` non valide, puis tester recherche/tri, Refresh et sélection des lignes valides.
|
||||
|
||||
Toujours aucun `cargo tauri build` à ce stade.
|
||||
|
||||
## Suite
|
||||
|
||||
Après validation de `pre.004`, `pre.005` introduira la création de Wallet et le lifecycle/session Rust sans avancer encore la balance HTTP.
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
|
||||
<!-- version: 62 -->
|
||||
<!-- version: 63 -->
|
||||
|
||||
# Séquence des releases fonctionnelles KSP
|
||||
|
||||
@@ -430,7 +430,7 @@ Le prompt [`../../prompts/011-V0_2_6_START_PROMPT.md`](../../prompts/011-V0_2_6_
|
||||
|
||||
`0.2.6-pre.001` confirme que le registre Config doit être étendu pour `cfg.std.wallet`/`schema.std.wallet` et `cfg.composite.ksp-app-wallet-desk`. `wallets_directory` reste global avec fallback `${KSP_WALLETS_DIRECTORY:-wallets}` et chaque profil peut définir un `wallets_subdirectory` relatif pour isoler tests, temporaires ou futurs scénarios. Wallet Desk prépare/crée le répertoire effectif après résolution Config et journalise le succès en debug ou l'échec en error. Le gabarit Config Desk est réutilisé avec Bootstrap, Font Awesome, DataTables/Select, SimpleBar et `resize-observer-polyfill`. L'inventory reste non récursive `.kspwallet`, sans symlink d'entrée et root-scoped; les handles VIEW/OWNER restent exclusivement côté Rust. L'ouverture accepte un password éphémère saisi ou une tentative explicite de secrets Config `KSP_SECRET_WALLET_PASS_*`, dont ni nom ni valeur ne traversent le frontend. `getBalance` utilise la Pubkey du handle autorisé et les flows OWNER metadata/rotations/revocation/export fichier délèguent à Wallet. La CSP reste `null` par défaut comme dans Config Desk et n'est réauditée qu'en présence d'un besoin WebView concret. Le forecast souple détaillé s'étend jusqu'à `pre.014`, dernière tranche prévue pour README/USAGE/docs/validation, prompt `0.2.7` et build Tauri final; le plan actif est `docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md`.
|
||||
|
||||
`0.2.6-pre.002` matérialise le shell Tauri spécialisé. `0.2.6-pre.003` matérialise ensuite la première composition Config propre à Wallet Desk : `cfg.std.wallet`/`schema.std.wallet`, le composite `cfg.composite.ksp-app-wallet-desk`, `ResolvedWalletConfig`, la racine `${KSP_WALLETS_DIRECTORY:-wallets}`, les sous-répertoires de profils `temporary`/`tests`, la préservation de provenance `Composite` dans les adapters Logging/Wallet et la création sûre des répertoires par l’application. Aucun inventory ou fichier `.kspwallet` n’est encore ouvert dans cette tranche. `0.2.6-pre.003-fix.001` corrige ensuite les tests de registry/fixtures devenus obsolètes et instrumente les clics frontend génériques sans sérialiser la valeur des contrôles. `0.2.6-pre.003-fix.002` corrige le scanner d’inventaire `.env.example` qui interprétait `FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK` comme une variable d’environnement, remplace le profil Logging ad hoc Wallet Desk par les profils génériques console-only/`file_info`/`superdev`/`supertrace`, et sélectionne temporairement `supertrace` dans le composite Wallet Desk. `0.2.6-pre.003-fix.003` corrige les deux canaris révélés par la validation opérateur de `fix.002` sans modifier cette surface Logging : closures avec `return` explicite sous la politique Clippy KSP et test lexical séparant fragment d’identifiant d’un vrai nom KSP suffixé. `0.2.6-pre.003-fix.004` complète cette remédiation en ajoutant les `return` explicites aux cinq closures restantes du canari `config_composition` de Wallet Desk ; aucune surface Config/Logging/frontend n’est modifiée.
|
||||
`0.2.6-pre.002` matérialise le shell Tauri spécialisé. `0.2.6-pre.003` matérialise ensuite la première composition Config propre à Wallet Desk : `cfg.std.wallet`/`schema.std.wallet`, le composite `cfg.composite.ksp-app-wallet-desk`, `ResolvedWalletConfig`, la racine `${KSP_WALLETS_DIRECTORY:-wallets}`, les sous-répertoires de profils `temporary`/`tests`, la préservation de provenance `Composite` dans les adapters Logging/Wallet et la création sûre des répertoires par l’application. Aucun inventory ou fichier `.kspwallet` n’est encore ouvert dans cette tranche. `0.2.6-pre.003-fix.001` corrige ensuite les tests de registry/fixtures devenus obsolètes et instrumente les clics frontend génériques sans sérialiser la valeur des contrôles. `0.2.6-pre.003-fix.002` corrige le scanner d’inventaire `.env.example` qui interprétait `FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK` comme une variable d’environnement, remplace le profil Logging ad hoc Wallet Desk par les profils génériques console-only/`file_info`/`superdev`/`supertrace`, et sélectionne temporairement `supertrace` dans le composite Wallet Desk. `0.2.6-pre.003-fix.003` corrige les deux canaris révélés par la validation opérateur de `fix.002` sans modifier cette surface Logging : closures avec `return` explicite sous la politique Clippy KSP et test lexical séparant fragment d’identifiant d’un vrai nom KSP suffixé. `0.2.6-pre.003-fix.004` complète cette remédiation en ajoutant les `return` explicites aux cinq closures restantes du canari `config_composition` de Wallet Desk ; aucune surface Config/Logging/frontend n’est modifiée. `0.2.6-pre.004` ajoute l’inventory `.kspwallet` réel sous `effective_wallets_directory` : lecture non récursive, extension exacte, exclusion des symlinks et non-fichiers, inspection par `ksp-wallet-lib::inspect_locked_wallet_file_v1`, lignes invalides conservées avec diagnostic sûr, DTOs locked sans identité protégée, DataTable/refresh actifs et sélection filename root-scoped revalidée côté Rust. Aucun handle VIEW/OWNER ni password n’est introduit dans cette tranche.
|
||||
|
||||
## `0.2.7` — WebSocket Solana standard
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md -->
|
||||
<!-- version: 10 -->
|
||||
<!-- version: 11 -->
|
||||
|
||||
# Plan `0.2.6` — Wallet Desk
|
||||
|
||||
@@ -1307,6 +1307,27 @@ refresh manuel + après opérations locales futures
|
||||
|
||||
À la fin de cette tranche, l'application sait lister et sélectionner un wallet sans révéler son identité protégée.
|
||||
|
||||
État matérialisé par `pre.004` :
|
||||
|
||||
```text
|
||||
ksp-app-wallet-desk -> ksp-wallet-lib ajouté explicitement
|
||||
effective_wallets_directory comme unique racine d'inventory
|
||||
read_dir asynchrone non récursif
|
||||
filename UTF-8 + suffixe exact .kspwallet
|
||||
regular file uniquement ; symlink/non-file exclus
|
||||
inspection via inspect_locked_wallet_file_v1
|
||||
ligne invalide conservée avec CommandErrorDto sûr
|
||||
WalletInventoryEntryDto / LockedWalletDto / WalletSelectionRequestDto
|
||||
list_wallets / refresh_wallets / select_wallet
|
||||
DataTable réel + refresh + sélection
|
||||
fa-lock pour valid locked ; fa-triangle-exclamation pour invalid
|
||||
selection revalidée côté Rust, filename single-component uniquement
|
||||
root symlink désormais rejeté lors de la préparation Config
|
||||
aucun Pubkey / alias / notes / password dans la projection locked
|
||||
```
|
||||
|
||||
La sélection de `pre.004` reste une projection locked sans handle durable : le `WalletSession` Rust et les handles OWNER/VIEW arrivent en `pre.005`/`pre.006`. Un refresh invalide donc la sélection frontend pour éviter de conserver une projection potentiellement stale.
|
||||
|
||||
### `pre.005` — création Wallet et lifecycle de session Rust
|
||||
|
||||
Objectifs :
|
||||
|
||||
Reference in New Issue
Block a user