v0.2.6-pre.005

This commit is contained in:
2026-08-21 09:33:04 +02:00
parent 2132dfd884
commit 7aa38b0daf
19 changed files with 980 additions and 126 deletions

View File

@@ -13,6 +13,9 @@
<span class="fw-semibold">Wallet Desk — <span id="headerViewTitle">Dashboard</span></span>
<small id="shellStatus" class="text-white-50 app-shell-status">Initialisation du shell…</small>
</div>
<button class="btn btn-outline-light btn-sm" type="button" data-shell-action="deselect" disabled>
<i class="fa-solid fa-xmark me-2" aria-hidden="true"></i>Deselect
</button>
<button class="btn btn-outline-light btn-sm" type="button" data-shell-action="lock" disabled>
<i class="fa-solid fa-lock me-2" aria-hidden="true"></i>Lock
</button>
@@ -39,7 +42,7 @@
<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>
</div>
<span class="badge text-bg-secondary">0.2.6-pre.004</span>
<span class="badge text-bg-secondary">0.2.6-pre.005</span>
</div>
<section data-view-panel="dashboard">
<div class="row g-3">
@@ -73,6 +76,9 @@
<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">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>
</dl>
</div>
</div>
@@ -98,7 +104,60 @@
</div>
</div>
</section>
<section data-view-panel="create-import" hidden><div class="app-placeholder"><i class="fa-solid fa-circle-plus"></i><p>Create / Import sera branché après la fondation Config et inventory.</p></div></section>
<section data-view-panel="create-import" hidden>
<div class="row g-3">
<div class="col-xl-8">
<div class="card shadow-sm">
<div class="card-header fw-semibold">Créer un wallet natif KSP</div>
<div class="card-body">
<form id="createWalletForm" autocomplete="off">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label" for="createWalletFilename">Filename</label>
<input id="createWalletFilename" class="form-control" type="text" value="wallet-01.kspwallet" required spellcheck="false">
<div class="form-text">Un seul filename sous le répertoire Wallet effectif, suffixe exact .kspwallet.</div>
</div>
<div class="col-md-6">
<label class="form-label" for="createWalletAlias">Alias interne optionnel</label>
<input id="createWalletAlias" class="form-control" type="text" autocomplete="off">
</div>
<div class="col-md-6">
<label class="form-label" for="createWalletOwnerPassword">OWNER password</label>
<input id="createWalletOwnerPassword" class="form-control" type="password" autocomplete="new-password" required>
</div>
<div class="col-md-6 d-flex align-items-end">
<div class="form-check mb-2">
<input id="createWalletEnableView" class="form-check-input" type="checkbox">
<label class="form-check-label" for="createWalletEnableView">Créer aussi une capability VIEW</label>
</div>
</div>
<div class="col-md-6">
<label class="form-label" for="createWalletViewPassword">VIEW password</label>
<input id="createWalletViewPassword" class="form-control" type="password" autocomplete="new-password" disabled>
</div>
<div class="col-12">
<label class="form-label" for="createWalletInitialNote">Note initiale optionnelle</label>
<textarea id="createWalletInitialNote" class="form-control" rows="3" autocomplete="off"></textarea>
</div>
<div class="col-12 d-flex align-items-center gap-3">
<button id="createWalletSubmit" class="btn btn-primary" type="submit">
<i class="fa-solid fa-circle-plus me-2" aria-hidden="true"></i>Create wallet
</button>
<span id="createWalletStatus" class="text-body-secondary" role="status">Prêt.</span>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="col-xl-4">
<div class="alert alert-info mb-0">
<i class="fa-solid fa-shield-halved me-2" aria-hidden="true"></i>
Les passwords sont envoyés uniquement du frontend vers Rust pour l'opération courante. Ils ne sont jamais renvoyés par Rust ni journalisés.
</div>
</div>
</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="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>
@@ -107,7 +166,7 @@
</div>
</main>
<footer class="app-footer bg-dark text-white-50 d-flex align-items-center px-4">
<span>KSP Wallet Desk · locked inventory pre.004</span>
<span>KSP Wallet Desk · create + session lifecycle pre.005</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: 4
// version: 5
import "bootstrap";
import DataTable from "datatables.net-bs5";
@@ -11,6 +11,9 @@ import type { RuntimeStatusDto } from "./bindings/ksp_app_wallet_desk/dto_common
import type { LockedWalletDto } from "./bindings/ksp_app_wallet_desk/wallet_inventory/LockedWalletDto.ts";
import type { WalletInventoryEntryDto } from "./bindings/ksp_app_wallet_desk/wallet_inventory/WalletInventoryEntryDto.ts";
import type { WalletSelectionRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_inventory/WalletSelectionRequestDto.ts";
import type { WalletAuthorizedDto } from "./bindings/ksp_app_wallet_desk/wallet_session/WalletAuthorizedDto.ts";
import type { WalletCreateRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_session/WalletCreateRequestDto.ts";
import type { WalletSessionDto } from "./bindings/ksp_app_wallet_desk/wallet_session/WalletSessionDto.ts";
import { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log";
import { invokeKsp } from "./invoke";
import "../sass/main.scss";
@@ -19,6 +22,7 @@ import "../sass/main.scss";
installFrontendConsoleBridge("main");
type ViewId = "dashboard" | "wallets" | "create-import" | "details" | "security" | "diagnostics";
type ActiveSessionState = "no_selection" | "locked" | "owner_open";
const viewTitles: Record<ViewId, string> = {
dashboard: "Dashboard",
@@ -29,6 +33,9 @@ const viewTitles: Record<ViewId, string> = {
diagnostics: "Diagnostics",
};
let activeWalletId: string | null = null;
let activeSessionState: ActiveSessionState = "no_selection";
function isViewId(value: string): value is ViewId {
return value in viewTitles;
}
@@ -114,7 +121,11 @@ function renderWalletInventoryRow(entry: WalletInventoryEntryDto): HTMLTableRowE
row.dataset.inspectionStatus = entry.inspectionStatus;
const state = document.createElement("td");
if (entry.state === "locked") {
appendIconText(state, "fa-lock", "Locked");
if (activeWalletId === entry.walletId && activeSessionState === "owner_open") {
appendIconText(state, "fa-lock-open", "OWNER");
} else {
appendIconText(state, "fa-lock", "Locked");
}
} else {
appendIconText(state, "fa-triangle-exclamation", "Erreur");
}
@@ -154,7 +165,7 @@ function initializeWalletTable(): void {
zeroRecords: "Aucun wallet correspondant.",
},
});
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.004-wallet-inventory" });
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.005-wallet-session-create" });
}
function renderWalletInventory(entries: WalletInventoryEntryDto[]): void {
@@ -170,58 +181,105 @@ function renderWalletInventory(entries: WalletInventoryEntryDto[]): void {
frontendDebug("main", "Wallet inventory rendered", { entryCount: entries.length, invalidCount });
}
function setText(selector: string, value: string): void {
const element = document.querySelector<HTMLElement>(selector);
if (element) {
element.textContent = value;
}
}
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";
}
if (deselect) {
deselect.disabled = activeSessionState === "no_selection";
}
}
function clearCreateFormSensitiveInputs(): void {
const ownerPassword = document.querySelector<HTMLInputElement>("#createWalletOwnerPassword");
const viewPassword = document.querySelector<HTMLInputElement>("#createWalletViewPassword");
const alias = document.querySelector<HTMLInputElement>("#createWalletAlias");
const note = document.querySelector<HTMLTextAreaElement>("#createWalletInitialNote");
if (ownerPassword) {
ownerPassword.value = "";
}
if (viewPassword) {
viewPassword.value = "";
}
if (alias) {
alias.value = "";
}
if (note) {
note.value = "";
}
}
function clearAuthorizedProjection(): void {
setText("#currentWalletPubkey", "—");
setText("#currentWalletAlias", "—");
setText("#currentWalletNotes", "—");
clearCreateFormSensitiveInputs();
}
function clearSelectedWallet(): void {
activeWalletId = null;
activeSessionState = "no_selection";
const icon = document.querySelector<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 = "—";
}
setText("#currentWalletFilename", "Aucun wallet sélectionné.");
setText("#currentWalletState", "—");
setText("#currentWalletFormat", "—");
setText("#currentWalletView", "—");
clearAuthorizedProjection();
updateSessionActions();
}
function renderSelectedWallet(wallet: LockedWalletDto): void {
function renderLockedWallet(wallet: LockedWalletDto): void {
activeWalletId = wallet.walletId;
activeSessionState = "locked";
clearAuthorizedProjection();
const icon = document.querySelector<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";
}
setText("#currentWalletFilename", wallet.filename);
setText("#currentWalletState", "Locked");
setText("#currentWalletFormat", wallet.formatVersion.toString());
setText("#currentWalletView", wallet.viewEnabled ? "enabled" : "disabled");
updateSessionActions();
frontendDebug("main", "Locked Wallet selection rendered", { walletId: wallet.walletId, formatVersion: wallet.formatVersion, viewEnabled: wallet.viewEnabled });
}
function renderAuthorizedWallet(wallet: WalletAuthorizedDto): void {
activeWalletId = wallet.walletId;
activeSessionState = "owner_open";
const icon = document.querySelector<HTMLElement>("#currentWalletIcon");
if (icon) {
icon.className = "fa-solid fa-lock-open fa-2x mb-3 text-success";
}
setText("#currentWalletFilename", wallet.filename);
setText("#currentWalletState", "OWNER open");
setText("#currentWalletFormat", wallet.formatVersion.toString());
setText("#currentWalletView", wallet.viewEnabled ? "enabled" : "disabled");
setText("#currentWalletPubkey", wallet.pubkey);
setText("#currentWalletAlias", wallet.alias ?? "—");
setText("#currentWalletNotes", wallet.notes.length === 0 ? "—" : wallet.notes.map(note => note.text).join(" · "));
updateSessionActions();
frontendDebug("main", "OWNER Wallet session rendered after creation", { walletId: wallet.walletId, viewEnabled: wallet.viewEnabled, noteCount: wallet.notes.length });
}
async function selectWallet(walletId: string): Promise<void> {
clearSelectedWallet();
const request: WalletSelectionRequestDto = { walletId };
try {
const wallet = await invokeKsp<LockedWalletDto>("main", "select_wallet", { request });
renderSelectedWallet(wallet);
renderLockedWallet(wallet);
} catch {
clearSelectedWallet();
frontendWarn("main", "Locked Wallet selection failed", { walletId });
@@ -256,62 +314,124 @@ function bindWalletTableSelection(): void {
frontendTrace("main", "Wallet inventory row selection handler installed");
}
async function loadWalletInventory(command: "list_wallets" | "refresh_wallets"): Promise<void> {
async function loadWalletInventory(command: "list_wallets" | "refresh_wallets", clearSession: boolean): Promise<void> {
frontendDebug("main", "Wallet inventory load requested", { command });
const entries = await invokeKsp<WalletInventoryEntryDto[]>("main", command);
clearSelectedWallet();
if (clearSession) {
clearSelectedWallet();
}
renderWalletInventory(entries);
}
function createRequestFromForm(): WalletCreateRequestDto | null {
const filename = document.querySelector<HTMLInputElement>("#createWalletFilename");
const ownerPassword = document.querySelector<HTMLInputElement>("#createWalletOwnerPassword");
const enableView = document.querySelector<HTMLInputElement>("#createWalletEnableView");
const viewPassword = document.querySelector<HTMLInputElement>("#createWalletViewPassword");
const alias = document.querySelector<HTMLInputElement>("#createWalletAlias");
const note = document.querySelector<HTMLTextAreaElement>("#createWalletInitialNote");
if (!filename || !ownerPassword || !enableView || !viewPassword || !alias || !note) {
return null;
}
const normalizedFilename = filename.value.trim();
if (!normalizedFilename.endsWith(".kspwallet") || normalizedFilename.length <= ".kspwallet".length || ownerPassword.value.length === 0) {
return null;
}
if (enableView.checked && viewPassword.value.length === 0) {
return null;
}
return {
alias: alias.value.length === 0 ? null : alias.value,
filename: normalizedFilename,
initialNote: note.value.length === 0 ? null : note.value,
ownerPassword: ownerPassword.value,
viewPassword: enableView.checked ? viewPassword.value : null,
};
}
function bindCreateWalletForm(): void {
const form = document.querySelector<HTMLFormElement>("#createWalletForm");
const enableView = document.querySelector<HTMLInputElement>("#createWalletEnableView");
const viewPassword = document.querySelector<HTMLInputElement>("#createWalletViewPassword");
if (enableView && viewPassword) {
enableView.addEventListener("change", () => {
viewPassword.disabled = !enableView.checked;
if (!enableView.checked) {
viewPassword.value = "";
}
frontendTrace("main", "Create Wallet VIEW option changed", { enabled: enableView.checked });
});
}
if (!form) {
return;
}
form.addEventListener("submit", event => {
event.preventDefault();
const request = createRequestFromForm();
if (!request) {
setText("#createWalletStatus", "Filename/passwords invalides ou incomplets.");
frontendWarn("main", "Create Wallet form validation rejected request");
return;
}
const requestedWalletId = request.filename;
const requestedViewEnabled = request.viewPassword !== null;
setText("#createWalletStatus", "Création en cours…");
frontendDebug("main", "Create Wallet request submitted", { walletId: requestedWalletId, viewEnabled: requestedViewEnabled });
clearSelectedWallet();
void invokeKsp<WalletAuthorizedDto>("main", "create_wallet", { request })
.then(async wallet => {
clearCreateFormSensitiveInputs();
renderAuthorizedWallet(wallet);
setText("#createWalletStatus", "Wallet créé ; session OWNER ouverte.");
await loadWalletInventory("list_wallets", false);
activateView("dashboard", "user");
})
.catch(() => {
clearCreateFormSensitiveInputs();
setText("#createWalletStatus", "Création impossible. Consulter le diagnostic et les logs.");
frontendWarn("main", "Create Wallet operation failed", { walletId: requestedWalletId });
});
});
frontendTrace("main", "Create Wallet form handlers installed");
}
async function lockCurrentWallet(): Promise<void> {
clearAuthorizedProjection();
try {
const wallet = await invokeKsp<LockedWalletDto>("main", "lock_wallet");
renderLockedWallet(wallet);
await loadWalletInventory("list_wallets", false);
} catch {
clearSelectedWallet();
frontendWarn("main", "Wallet lock operation failed");
}
}
async function deselectCurrentWallet(): Promise<void> {
try {
const session = await invokeKsp<WalletSessionDto>("main", "deselect_wallet");
clearSelectedWallet();
frontendDebug("main", "Wallet session deselected", { state: session.state });
await loadWalletInventory("list_wallets", false);
} catch {
clearSelectedWallet();
frontendWarn("main", "Wallet deselect operation failed");
}
}
function renderRuntimeStatus(status: RuntimeStatusDto): void {
const version = document.querySelector<HTMLElement>("#runtimeVersion");
const compositeProfile = document.querySelector<HTMLElement>("#runtimeCompositeProfile");
const loggingProfile = document.querySelector<HTMLElement>("#runtimeLoggingProfile");
const walletProfile = document.querySelector<HTMLElement>("#runtimeWalletProfile");
const fallback = document.querySelector<HTMLElement>("#runtimeLoggingFallback");
const documents = document.querySelector<HTMLElement>("#runtimeConfigDocuments");
const walletsRoot = document.querySelector<HTMLElement>("#runtimeWalletsDirectory");
const walletsSubdirectory = document.querySelector<HTMLElement>("#runtimeWalletsSubdirectory");
const effectiveWalletsDirectory = document.querySelector<HTMLElement>("#runtimeEffectiveWalletsDirectory");
const walletDirectoryCreated = document.querySelector<HTMLElement>("#runtimeWalletDirectoryCreated");
const phase = document.querySelector<HTMLElement>("#runtimeShellPhase");
const shellStatus = document.querySelector<HTMLElement>("#shellStatus");
if (version) {
version.textContent = status.applicationVersion;
}
if (compositeProfile) {
compositeProfile.textContent = status.activeCompositeProfile;
}
if (loggingProfile) {
loggingProfile.textContent = status.activeLoggingProfile ?? "fallback transitoire";
}
if (walletProfile) {
walletProfile.textContent = status.activeWalletProfile;
}
if (fallback) {
fallback.textContent = status.fallbackLoggingActive ? "oui" : "non";
}
if (documents) {
documents.textContent = status.configDocumentCount.toString();
}
if (walletsRoot) {
walletsRoot.textContent = status.walletsDirectory;
}
if (walletsSubdirectory) {
walletsSubdirectory.textContent = status.walletsSubdirectory ?? "— (racine globale)";
}
if (effectiveWalletsDirectory) {
effectiveWalletsDirectory.textContent = status.effectiveWalletsDirectory;
}
if (walletDirectoryCreated) {
walletDirectoryCreated.textContent = status.effectiveWalletsDirectoryCreatedOnStartup ? "oui" : "non, déjà présent";
}
if (phase) {
phase.textContent = status.shellPhase;
}
if (shellStatus) {
shellStatus.textContent = "Config résolue ; inventaire Wallet prêt.";
}
setText("#runtimeVersion", status.applicationVersion);
setText("#runtimeCompositeProfile", status.activeCompositeProfile);
setText("#runtimeLoggingProfile", status.activeLoggingProfile ?? "fallback transitoire");
setText("#runtimeWalletProfile", status.activeWalletProfile);
setText("#runtimeLoggingFallback", status.fallbackLoggingActive ? "oui" : "non");
setText("#runtimeConfigDocuments", status.configDocumentCount.toString());
setText("#runtimeWalletsDirectory", status.walletsDirectory);
setText("#runtimeWalletsSubdirectory", status.walletsSubdirectory ?? "— (racine globale)");
setText("#runtimeEffectiveWalletsDirectory", status.effectiveWalletsDirectory);
setText("#runtimeWalletDirectoryCreated", status.effectiveWalletsDirectoryCreatedOnStartup ? "oui" : "non, déjà présent");
setText("#runtimeShellPhase", status.shellPhase);
setText("#shellStatus", "Config résolue ; création et session Wallet prêtes.");
frontendTrace("main", "Wallet Desk runtime status rendered", {
compositeProfile: status.activeCompositeProfile,
fallbackLoggingActive: status.fallbackLoggingActive,
@@ -331,10 +451,19 @@ function bindShellActions(): void {
button.addEventListener("click", () => {
const action = button.dataset.shellAction ?? "unknown";
frontendDebug("main", "Wallet Desk shell action clicked", { action, enabled: !button.disabled });
if (action === "refresh-wallets" && !button.disabled) {
void loadWalletInventory("refresh_wallets").catch(() => {
if (button.disabled) {
return;
}
if (action === "refresh-wallets") {
clearSelectedWallet();
void loadWalletInventory("refresh_wallets", true).catch(() => {
clearSelectedWallet();
frontendWarn("main", "Wallet inventory refresh failed");
});
} else if (action === "lock") {
void lockCurrentWallet();
} else if (action === "deselect") {
void deselectCurrentWallet();
}
});
});
@@ -347,17 +476,16 @@ async function initializeMain(): Promise<void> {
bindFrontendInteractions();
bindNavigation();
bindWalletTableSelection();
bindCreateWalletForm();
bindShellActions();
clearSelectedWallet();
activateView("dashboard", "startup");
try {
await loadRuntimeStatus();
await loadWalletInventory("list_wallets");
await loadWalletInventory("list_wallets", false);
} catch {
renderWalletInventory([]);
const shellStatus = document.querySelector<HTMLElement>("#shellStatus");
if (shellStatus) {
shellStatus.textContent = "Le statut runtime ou l'inventaire Wallet n'a pas pu être chargé.";
}
setText("#shellStatus", "Le statut runtime ou l'inventaire Wallet n'a pas pu être chargé.");
frontendWarn("main", "Wallet Desk startup data load failed");
}
}

View File

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

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/app_state.rs
// version: 4
// version: 5
//! Shared backend state owned by the Wallet Desk Tauri application.
@@ -10,6 +10,7 @@ pub(crate) struct AppState {
splash_settings: crate::SplashSettings,
splash_sequence_started: std::sync::atomic::AtomicBool,
wallet_config_startup: crate::WalletConfigStartup,
wallet_session: std::sync::Mutex<crate::WalletSession>,
}
impl AppState {
@@ -58,6 +59,7 @@ impl AppState {
splash_settings,
splash_sequence_started: std::sync::atomic::AtomicBool::new(false),
wallet_config_startup,
wallet_session: std::sync::Mutex::new(crate::WalletSession::no_selection()),
});
}
@@ -100,7 +102,7 @@ impl AppState {
effective_wallets_directory_created_on_startup: self.wallet_config_startup.effective_directory_created_on_startup(),
fallback_logging_active: runtime.fallback_active,
root_wallets_directory_created_on_startup: self.wallet_config_startup.root_directory_created_on_startup(),
shell_phase: "pre.004-wallet-inventory".to_owned(),
shell_phase: "pre.005-wallet-session-create".to_owned(),
startup_diagnostic: runtime.startup_diagnostic.clone(),
wallets_directory: resolved.wallets_directory().to_string_lossy().into_owned(),
wallets_subdirectory,
@@ -113,6 +115,123 @@ impl AppState {
return self.wallet_config_startup.resolved().effective_wallets_directory();
}
/// Selects one locked Wallet and installs it as the durable backend session.
pub(crate) async fn select_wallet(&self, request: crate::WalletSelectionRequestDto) -> ksp_core_lib::Result<crate::LockedWalletDto> {
let deselected = self.deselect_wallet();
if let std::result::Result::Err(error) = deselected {
return std::result::Result::Err(error);
}
let root = self.wallet_inventory_root().to_path_buf();
let resolved = crate::resolve_locked_wallet(root.as_path(), request.wallet_id).await;
let (path, locked_info, dto) = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let session = self.wallet_session.lock();
let mut session = match session {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()),
};
*session = crate::WalletSession::Locked { wallet_id: dto.wallet_id.clone(), path, locked_info };
return std::result::Result::Ok(dto);
}
/// Drops any selected or authorized Wallet state and returns to `NoSelection`.
pub(crate) fn deselect_wallet(&self) -> ksp_core_lib::Result<crate::WalletSessionDto> {
let previous = {
let session = self.wallet_session.lock();
let mut session = match session {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()),
};
std::mem::replace(&mut *session, crate::WalletSession::no_selection())
};
drop(previous);
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, "Wallet session deselected and protected handle state purged");
return std::result::Result::Ok(crate::WalletSession::no_selection().safe_projection());
}
/// Creates a new Wallet under the effective root and retains the returned OWNER handle only in Rust.
pub(crate) async fn create_wallet(&self, request: crate::WalletCreateRequestDto) -> ksp_core_lib::Result<crate::WalletAuthorizedDto> {
let deselected = self.deselect_wallet();
if let std::result::Result::Err(error) = deselected {
return std::result::Result::Err(error);
}
let crate::WalletCreateRequestDto { alias, filename, initial_note, owner_password, view_password } = request;
let root = self.wallet_inventory_root().to_path_buf();
let destination = crate::new_wallet_destination(root.as_path(), filename.as_str());
let destination = match destination {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let view_enabled = view_password.is_some();
let owner_password = ksp_wallet_lib::OwnerPassword::new(owner_password);
let view_password = view_password.map(ksp_wallet_lib::ViewPassword::new);
let alias = normalize_optional_text(alias);
let note = normalize_optional_text(initial_note);
let note_texts = note.into_iter().collect::<std::vec::Vec<_>>();
let metadata = ksp_wallet_lib::WalletCreateMetadataV1::new(alias, note_texts);
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = filename.as_str(), view_enabled, "Wallet creation requested under effective Config root");
let created = ksp_wallet_lib::create_wallet_file_v1(destination.as_path(), owner_password, view_password, metadata).await;
let owner = match created {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = filename.as_str(), error_domain = error.code().domain(), error_code = error.code().code(), "Wallet creation failed");
return std::result::Result::Err(error);
},
};
let dto = crate::owner_projection(filename.as_str(), view_enabled, &owner);
let session = self.wallet_session.lock();
let mut session = match session {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()),
};
*session = crate::WalletSession::Owner { wallet_id: filename.clone(), path: destination, wallet: owner };
ksp_logging_lib::info!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = filename.as_str(), view_enabled, "Wallet created and OWNER session opened");
return std::result::Result::Ok(dto);
}
/// Drops an authorized handle, re-inspects the selected file and returns the session to `Locked`.
pub(crate) async fn lock_wallet(&self) -> ksp_core_lib::Result<crate::LockedWalletDto> {
let previous = {
let session = self.wallet_session.lock();
let mut session = match session {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()),
};
std::mem::replace(&mut *session, crate::WalletSession::no_selection())
};
let (wallet_id, path) = match previous {
crate::WalletSession::NoSelection => {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_SESSION_INVALID, "No Wallet is selected to lock"));
},
crate::WalletSession::Locked { wallet_id, path, .. } => (wallet_id, path),
crate::WalletSession::Owner { wallet_id, path, wallet } => {
drop(wallet);
(wallet_id, path)
},
};
let locked = ksp_wallet_lib::inspect_locked_wallet_file_v1(path.as_path()).await;
let locked = match locked {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let dto = crate::LockedWalletDto {
filename: wallet_id.clone(),
format_version: locked.format_version(),
wallet_id: wallet_id.clone(),
view_enabled: locked.view_enabled(),
};
let session = self.wallet_session.lock();
let mut session = match session {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(session_lock_error()),
};
*session = crate::WalletSession::Locked { wallet_id: wallet_id.clone(), path, locked_info: locked };
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_SESSION, wallet_id = wallet_id.as_str(), "Wallet session locked and authorized handle state purged");
return std::result::Result::Ok(dto);
}
/// Returns the resolved common splash timings captured during bootstrap.
#[must_use]
pub(crate) const fn splash_settings(&self) -> crate::SplashSettings {
@@ -128,6 +247,19 @@ impl AppState {
}
}
fn normalize_optional_text(value: std::option::Option<String>) -> std::option::Option<String> {
return value.and_then(|text| {
if text.trim().is_empty() {
return std::option::Option::None;
}
return std::option::Option::Some(text);
});
}
fn session_lock_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_LOCK_FAILED, "Wallet Desk Wallet session state lock is poisoned");
}
struct LoggingRuntimeState {
guard: ksp_logging_lib::LoggingGuard,
active_profile_id: std::option::Option<String>,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/constants.rs
// version: 3
// version: 4
//! Logging targets, domains and composite component identifiers owned by Wallet Desk.
@@ -19,6 +19,8 @@ pub(crate) const TRACING_DOMAIN_SHELL: &str = "wallet.shell";
pub(crate) const TRACING_DOMAIN_WALLET_CONFIG: &str = "wallet.config";
/// Structured domain used while enumerating and inspecting locked Wallet files.
pub(crate) const TRACING_DOMAIN_WALLET_INVENTORY: &str = "wallet.inventory";
/// Structured domain used by durable Wallet session and creation operations.
pub(crate) const TRACING_DOMAIN_WALLET_SESSION: &str = "wallet.session";
/// Structured domain used by Tauri window lifecycle operations.
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
/// Owning target for backend events emitted by Wallet Desk.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/errors.rs
// version: 3
// version: 4
//! Application-local error codes for Wallet Desk composition and desktop runtime surfaces.
@@ -35,3 +35,5 @@ pub(crate) const ERROR_CODE_WALLET_DIRECTORY_PREPARE_FAILED: ksp_core_lib::Error
pub(crate) const ERROR_CODE_WALLET_INVENTORY_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_inventory_failed");
/// A requested Wallet inventory identifier is unsafe or does not resolve to an eligible regular `.kspwallet` file.
pub(crate) const ERROR_CODE_WALLET_SELECTION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_selection_invalid");
/// A Wallet session lifecycle operation is incompatible with the current state.
pub(crate) const ERROR_CODE_WALLET_SESSION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "wallet_session_invalid");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/lib.rs
// version: 4
// version: 5
//! Tauri desktop application shell for KSP Wallet management and inspection.
@@ -20,6 +20,7 @@ mod tw_main;
mod tw_splash;
mod wallet_config;
mod wallet_inventory;
mod wallet_session;
/// Runs the KSP wallet desktop application.
pub use self::tauri::run;
@@ -52,6 +53,8 @@ pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
pub(crate) use self::constants::TRACING_DOMAIN_WALLET_CONFIG;
/// Structured domain used while enumerating and inspecting locked Wallet files.
pub(crate) use self::constants::TRACING_DOMAIN_WALLET_INVENTORY;
/// Structured domain used by durable Wallet session and creation operations.
pub(crate) use self::constants::TRACING_DOMAIN_WALLET_SESSION;
/// Structured domain used by Tauri window lifecycle operations.
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
/// Owning target for backend events emitted by Wallet Desk.
@@ -98,6 +101,8 @@ pub(crate) use self::errors::ERROR_CODE_WALLET_DIRECTORY_PREPARE_FAILED;
pub(crate) use self::errors::ERROR_CODE_WALLET_INVENTORY_FAILED;
/// Wallet selection identifier is invalid or no longer eligible.
pub(crate) use self::errors::ERROR_CODE_WALLET_SELECTION_INVALID;
/// Wallet session lifecycle operation is incompatible with the current state.
pub(crate) use self::errors::ERROR_CODE_WALLET_SESSION_INVALID;
/// Log payload sent by Wallet Desk frontend scripts.
pub(crate) use self::frontend_logging::FrontendLogPayloadDto;
/// Emits one validated frontend event through the KSP Logging facade.
@@ -134,5 +139,23 @@ pub(crate) use self::wallet_inventory::WalletInventoryStateDto;
pub(crate) use self::wallet_inventory::WalletSelectionRequestDto;
/// Enumerates native Wallet files under the effective Config-managed root.
pub(crate) use self::wallet_inventory::list_wallet_inventory;
/// Re-inspects one root-scoped Wallet selection.
/// Builds a validated destination path for one new root-scoped Wallet.
pub(crate) use self::wallet_inventory::new_wallet_destination;
/// Re-resolves one locked Wallet into Rust-only path state plus safe DTO.
pub(crate) use self::wallet_inventory::resolve_locked_wallet;
/// Re-inspects one root-scoped Wallet selection in crate unit tests.
#[cfg(test)]
pub(crate) use self::wallet_inventory::select_locked_wallet;
/// Authorized OWNER projection exposed only after successful creation.
pub(crate) use self::wallet_session::WalletAuthorizedDto;
/// Create request whose password strings move frontend -> Rust only.
pub(crate) use self::wallet_session::WalletCreateRequestDto;
/// Durable backend Wallet session retaining full paths and handles only in Rust.
pub(crate) use self::wallet_session::WalletSession;
/// Minimal safe projection of the durable Wallet session.
pub(crate) use self::wallet_session::WalletSessionDto;
/// Safe state code for the durable Wallet session in crate unit tests.
#[cfg(test)]
pub(crate) use self::wallet_session::WalletSessionStateDto;
/// Builds an authorized OWNER DTO from a Rust-only Wallet handle.
pub(crate) use self::wallet_session::owner_projection;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/tauri.rs
// version: 2
// version: 3
//! Tauri runtime assembly for the KSP wallet desktop application.
@@ -38,12 +38,15 @@ fn configure_plugins(builder: tauri::Builder<tauri::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![
create_wallet,
deselect_wallet,
emit_frontend_log,
get_runtime_status,
list_wallets,
lock_wallet,
refresh_wallets,
select_wallet,
splash_frontend_ready
splash_frontend_ready,
]);
}
@@ -62,6 +65,27 @@ fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri:
});
}
#[tauri::command]
async fn create_wallet(
request: crate::WalletCreateRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::WalletAuthorizedDto, crate::CommandErrorDto> {
let result = state.create_wallet(request).await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
fn deselect_wallet(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::WalletSessionDto, crate::CommandErrorDto> {
let result = state.deselect_wallet();
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> {
let result = crate::emit_frontend_log_event(payload);
@@ -90,10 +114,23 @@ async fn list_wallets(state: tauri::State<'_, crate::AppState>) -> std::result::
};
}
#[tauri::command]
async fn lock_wallet(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::LockedWalletDto, crate::CommandErrorDto> {
let result = state.lock_wallet().await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
async fn refresh_wallets(
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<std::vec::Vec<crate::WalletInventoryEntryDto>, crate::CommandErrorDto> {
let deselected = state.deselect_wallet();
if let std::result::Result::Err(error) = deselected {
return std::result::Result::Err(crate::CommandErrorDto::from_error(&error));
}
let root = state.wallet_inventory_root().to_path_buf();
let result = crate::list_wallet_inventory(root.as_path()).await;
return match result {
@@ -107,8 +144,7 @@ async fn select_wallet(
request: crate::WalletSelectionRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::LockedWalletDto, crate::CommandErrorDto> {
let root = state.wallet_inventory_root().to_path_buf();
let result = crate::select_locked_wallet(root.as_path(), request).await;
let result = state.select_wallet(request).await;
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/src/wallet_inventory.rs
// version: 1
// version: 2
//! Root-scoped Wallet inventory and locked-file selection for Wallet Desk.
@@ -134,8 +134,21 @@ pub(crate) async fn list_wallet_inventory(root: &std::path::Path) -> ksp_core_li
}
/// Re-resolves and re-inspects one selected inventory identifier without exposing its full path.
#[cfg(test)]
pub(crate) async fn select_locked_wallet(root: &std::path::Path, request: WalletSelectionRequestDto) -> ksp_core_lib::Result<LockedWalletDto> {
let path = resolve_wallet_path(root, request.wallet_id.as_str()).await;
let resolved = resolve_locked_wallet(root, request.wallet_id).await;
return match resolved {
std::result::Result::Ok((_, _, dto)) => std::result::Result::Ok(dto),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
/// Resolves one locked Wallet into its Rust-only path, locked info and safe DTO.
pub(crate) async fn resolve_locked_wallet(
root: &std::path::Path,
wallet_id: String,
) -> ksp_core_lib::Result<(std::path::PathBuf, ksp_wallet_lib::LockedWalletInfo, LockedWalletDto)> {
let path = resolve_wallet_path(root, wallet_id.as_str()).await;
let path = match path {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -145,13 +158,22 @@ pub(crate) async fn select_locked_wallet(root: &std::path::Path, request: Wallet
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_INVENTORY, wallet_id = request.wallet_id.as_str(), format_version = locked.format_version(), view_enabled = locked.view_enabled(), "Locked Wallet selected from inventory");
return std::result::Result::Ok(LockedWalletDto {
filename: request.wallet_id.clone(),
let dto = LockedWalletDto {
filename: wallet_id.clone(),
format_version: locked.format_version(),
wallet_id: request.wallet_id,
wallet_id: wallet_id.clone(),
view_enabled: locked.view_enabled(),
});
};
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_INVENTORY, wallet_id = wallet_id.as_str(), format_version = locked.format_version(), view_enabled = locked.view_enabled(), "Locked Wallet selected from inventory");
return std::result::Result::Ok((path, locked, dto));
}
/// Validates a root-scoped destination filename for a new native Wallet.
pub(crate) fn new_wallet_destination(root: &std::path::Path, filename: &str) -> ksp_core_lib::Result<std::path::PathBuf> {
if !is_wallet_filename(filename) {
return selection_invalid(filename, "Wallet destination must be one UTF-8 filename ending in .kspwallet");
}
return std::result::Result::Ok(root.join(filename));
}
fn valid_inventory_entry(filename: String, locked: ksp_wallet_lib::LockedWalletInfo) -> WalletInventoryEntryDto {

View File

@@ -0,0 +1,166 @@
// file: crates/ksp-app-wallet-desk/src/wallet_session.rs
// version: 1
//! Durable root-scoped Wallet session lifecycle for Wallet Desk.
use ts_rs::TS; // rust-rules: trait-import
/// Safe state code projected for the current Wallet session.
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_session/WalletSessionStateDto.ts")]
pub(crate) enum WalletSessionStateDto {
/// No Wallet is selected.
NoSelection,
/// One Wallet is selected but remains locked.
Locked,
/// One Wallet is open with OWNER capability.
OwnerOpen,
}
/// Minimal non-secret projection of the backend Wallet session.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_session/WalletSessionDto.ts")]
pub(crate) struct WalletSessionDto {
/// Filename when a Wallet is selected.
pub(crate) filename: std::option::Option<String>,
/// Current session state.
pub(crate) state: WalletSessionStateDto,
/// Root-scoped identifier when a Wallet is selected.
pub(crate) wallet_id: std::option::Option<String>,
}
/// One protected Wallet note exposed only from an authorized session.
#[derive(serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_session/WalletNoteDto.ts")]
pub(crate) struct WalletNoteDto {
/// Stable note identifier generated by Wallet.
pub(crate) id: String,
/// Protected note text available only after authorization.
pub(crate) text: String,
}
/// Authorized Wallet projection returned after creation with OWNER capability.
#[derive(serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_session/WalletAuthorizedDto.ts")]
pub(crate) struct WalletAuthorizedDto {
/// Protected internal alias when configured.
pub(crate) alias: std::option::Option<String>,
/// Capability that produced this projection.
pub(crate) capability: String,
/// Native Wallet filename without its parent path.
pub(crate) filename: String,
/// Native Wallet format version.
pub(crate) format_version: u32,
/// Protected notes available to the OWNER session.
pub(crate) notes: std::vec::Vec<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.
pub(crate) view_enabled: bool,
}
/// Create request moved from the frontend directly into Wallet password wrappers.
#[derive(serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/wallet_session/WalletCreateRequestDto.ts")]
pub(crate) struct WalletCreateRequestDto {
/// Optional protected internal alias stored inside the Wallet.
pub(crate) alias: std::option::Option<String>,
/// Root-scoped destination filename.
pub(crate) filename: String,
/// Optional first protected note stored inside the Wallet.
pub(crate) initial_note: std::option::Option<String>,
/// OWNER password transported only frontend -> Rust.
pub(crate) owner_password: String,
/// Optional VIEW password; `None` keeps VIEW disabled.
pub(crate) view_password: std::option::Option<String>,
}
/// Backend Wallet session. Full paths and authorized handles never cross IPC.
pub(crate) enum WalletSession {
/// No current Wallet selection.
NoSelection,
/// One re-inspected locked Wallet.
Locked {
/// Native Wallet filename / root-scoped identifier.
wallet_id: String,
/// Full root-scoped filesystem path retained only in Rust.
path: std::path::PathBuf,
/// Locked information re-inspected from the file.
locked_info: ksp_wallet_lib::LockedWalletInfo,
},
/// One Wallet kept open with OWNER capability.
Owner {
/// Native Wallet filename / root-scoped identifier.
wallet_id: String,
/// Full root-scoped filesystem path retained only in Rust.
path: std::path::PathBuf,
/// Authorized OWNER handle retaining secret material only in Rust.
wallet: ksp_wallet_lib::WalletOwner,
},
}
impl WalletSession {
/// Creates an empty application session.
#[must_use]
pub(crate) const fn no_selection() -> Self {
return Self::NoSelection;
}
/// Returns a minimal safe session projection.
#[must_use]
pub(crate) fn safe_projection(&self) -> WalletSessionDto {
return match self {
Self::NoSelection => WalletSessionDto {
filename: std::option::Option::None,
state: WalletSessionStateDto::NoSelection,
wallet_id: std::option::Option::None,
},
Self::Locked { wallet_id, locked_info, .. } => {
let _ = locked_info.format_version();
WalletSessionDto {
filename: std::option::Option::Some(wallet_id.clone()),
state: WalletSessionStateDto::Locked,
wallet_id: std::option::Option::Some(wallet_id.clone()),
}
},
Self::Owner { wallet_id, .. } => WalletSessionDto {
filename: std::option::Option::Some(wallet_id.clone()),
state: WalletSessionStateDto::OwnerOpen,
wallet_id: std::option::Option::Some(wallet_id.clone()),
},
};
}
}
/// Converts an OWNER handle into the authorized response DTO without exposing secret key material.
pub(crate) fn owner_projection(wallet_id: &str, view_enabled: bool, owner: &ksp_wallet_lib::WalletOwner) -> WalletAuthorizedDto {
let info = owner.info();
let notes = info
.notes()
.iter()
.map(|note| {
return WalletNoteDto { id: note.id().to_owned(), text: note.text().to_owned() };
})
.collect();
return WalletAuthorizedDto {
alias: info.alias().map(|value| return value.to_owned()),
capability: "owner".to_owned(),
filename: wallet_id.to_owned(),
format_version: info.format_version(),
notes,
pubkey: info.pubkey().to_string(),
wallet_id: wallet_id.to_owned(),
view_enabled,
};
}
#[cfg(test)]
#[path = "../unit_tests/wallet_session.rs"]
mod tests;

View File

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

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs
// version: 4
// version: 5
//! Desktop build, shell and Config-status contract audits for Wallet Desk.
@@ -119,3 +119,27 @@ fn frontend_control_interactions_are_trace_logged_without_control_values() {
assert!(main.contains("viewId: control.dataset.view"));
assert!(!main.contains("control.value"));
}
#[test]
fn pre_005_create_and_session_lifecycle_are_backend_owned() {
let root = app_root();
let state = read_text(root.join("src/app_state.rs").as_path());
let session = read_text(root.join("src/wallet_session.rs").as_path());
let tauri = read_text(root.join("src/tauri.rs").as_path());
let html = read_text(root.join("frontend/main.html").as_path());
let main = read_text(root.join("frontend/ts/main.ts").as_path());
assert!(state.contains("ksp_wallet_lib::create_wallet_file_v1"));
assert!(state.contains("WalletSession::Owner"));
assert!(state.contains("WalletSession::Locked"));
assert!(state.contains("std::mem::replace"));
assert!(session.contains("WalletCreateRequestDto"));
assert!(session.contains("WalletAuthorizedDto"));
assert!(tauri.contains("create_wallet"));
assert!(tauri.contains("deselect_wallet"));
assert!(tauri.contains("lock_wallet"));
assert!(html.contains("id=\"createWalletForm\""));
assert!(html.contains("id=\"createWalletOwnerPassword\""));
assert!(html.contains("id=\"createWalletViewPassword\""));
assert!(main.contains("clearCreateFormSensitiveInputs"));
assert!(main.contains("OWNER Wallet session rendered after creation"));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/tests/desktop_security.rs
// version: 2
// version: 3
//! Static desktop security contracts for the Wallet Desk pre.002 shell.
@@ -46,3 +46,20 @@ fn locked_inventory_frontend_never_receives_protected_wallet_identity_fields() {
assert!(!inventory.contains("notes:"));
assert!(!inventory.contains("password:"));
}
#[test]
fn create_passwords_are_request_only_and_protected_projection_is_purged() {
let root = app_root();
let session = read_text(root.join("src/wallet_session.rs").as_path());
let main = read_text(root.join("frontend/ts/main.ts").as_path());
let authorized = session.split("pub(crate) struct WalletAuthorizedDto").nth(1);
assert!(authorized.is_some());
let authorized = authorized.unwrap_or_default().split("}\n\n/// Create request").next().unwrap_or_default();
assert!(!authorized.contains("password"));
assert!(session[request_start..].contains("owner_password"));
assert!(session[request_start..].contains("view_password"));
assert!(main.contains("clearAuthorizedProjection"));
assert!(main.contains("clearCreateFormSensitiveInputs"));
assert!(!main.contains("frontendDebug(\"main\", request.ownerPassword"));
assert!(!main.contains("frontendTrace(\"main\", request.viewPassword"));
}

View File

@@ -0,0 +1,26 @@
// file: crates/ksp-app-wallet-desk/unit_tests/wallet_session.rs
// version: 1
//! Unit tests for Wallet Desk session DTOs and OWNER projection.
#[test]
fn no_selection_projection_contains_no_wallet_identity() {
let session = crate::WalletSession::no_selection();
let dto = session.safe_projection();
assert_eq!(dto.state, crate::WalletSessionStateDto::NoSelection);
assert!(dto.wallet_id.is_none());
assert!(dto.filename.is_none());
}
#[test]
fn create_request_does_not_require_debug_or_clone_password_contracts() {
fn accepts_request(_: crate::WalletCreateRequestDto) {}
let request = crate::WalletCreateRequestDto {
alias: std::option::Option::None,
filename: "test.kspwallet".to_owned(),
initial_note: std::option::Option::None,
owner_password: "operator-test-owner".to_owned(),
view_password: std::option::Option::Some("operator-test-view".to_owned()),
};
accepts_request(request);
}