v0.2.6-pre.012

This commit is contained in:
2026-08-21 16:22:16 +02:00
parent 8ba554306a
commit ecbd6b746d
21 changed files with 634 additions and 94 deletions

View File

@@ -42,7 +42,7 @@
<h1 id="viewTitle" class="h3 mb-1">Dashboard</h1>
<p class="text-body-secondary mb-0">Identité autorisée et balance SOL via le Transport HTTP composite sont actives.</p>
</div>
<span class="badge text-bg-secondary">0.2.6-pre.008</span>
<span class="badge text-bg-secondary">0.2.6-pre.012</span>
</div>
<section data-view-panel="dashboard">
<div class="row g-3">
@@ -380,6 +380,27 @@
</div>
</div>
</div>
<div class="col-12">
<div class="card shadow-sm border-danger-subtle">
<div class="card-header fw-semibold">Export keypair Solana — OWNER uniquement</div>
<div class="card-body">
<p class="small text-body-secondary">Lexport produit un fichier secret non chiffré contenant la keypair Solana complète. Le contenu et le chemin complet restent exclusivement côté Rust ; le frontend choisit seulement le format puis confirme lopération.</p>
<div class="row g-3 align-items-end">
<div class="col-lg-6">
<label class="form-label" for="walletExportFormat">Format dexport</label>
<select id="walletExportFormat" class="form-select" data-owner-export-control disabled>
<option value="solana_cli_json">Solana CLI JSON</option>
<option value="solana_keypair_base58">Keypair Base58 complet</option>
</select>
</div>
<div class="col-lg-6 d-grid">
<button id="exportWalletKeypair" class="btn btn-outline-danger" type="button" data-owner-export-control disabled>Export secret keypair file</button>
</div>
</div>
<div id="walletExportStatus" class="alert alert-secondary mt-3 mb-0" role="status">Session OWNER requise. Aucun export texte ou clipboard nest disponible.</div>
</div>
</div>
</div>
<div class="col-12">
<div id="unlockStatus" class="alert alert-secondary mb-0" role="status">Sélectionner un wallet verrouillé.</div>
</div>
@@ -443,8 +464,23 @@
</div>
</div>
</div>
<div class="modal fade" id="exportWalletKeypairModal" tabindex="-1" aria-labelledby="exportWalletKeypairModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title fs-5" id="exportWalletKeypairModalLabel">Exporter la keypair Solana ?</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">Cette opération OWNER crée un fichier secret non chiffré contenant les 64 octets de la keypair Solana. Le fichier doit être protégé comme une clé privée. Un fichier existant ne sera jamais écrasé par Wallet.</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button id="confirmExportWalletKeypair" type="button" class="btn btn-danger">Choose destination and export</button>
</div>
</div>
</div>
</div>
<footer class="app-footer bg-dark text-white-50 d-flex align-items-center px-4">
<span>KSP Wallet Desk · strong VIEW administration pre.011</span>
<span>KSP Wallet Desk · OWNER keypair export pre.012</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: 12
// version: 13
import { Modal } from "bootstrap";
import DataTable from "datatables.net-bs5";
@@ -10,9 +10,11 @@ import { getCurrentWindow } from "@tauri-apps/api/window";
import type { WalletBalanceDto } from "./bindings/ksp_app_wallet_desk/wallet_balance/WalletBalanceDto.ts";
import type { CommandErrorDto } from "./bindings/ksp_app_wallet_desk/dto_common/CommandErrorDto.ts";
import type { RuntimeStatusDto } from "./bindings/ksp_app_wallet_desk/dto_common/RuntimeStatusDto.ts";
import type { WalletExportRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_export/WalletExportRequestDto.ts";
import type { WalletExportResultDto } from "./bindings/ksp_app_wallet_desk/wallet_export/WalletExportResultDto.ts";
import type { WalletImportRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_import/WalletImportRequestDto.ts";
import type { WalletImportSourceRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_import/WalletImportSourceRequestDto.ts";
import type { WalletTransferFormatDto } from "./bindings/ksp_app_wallet_desk/wallet_import/WalletTransferFormatDto.ts";
import type { WalletTransferFormatDto } from "./bindings/ksp_app_wallet_desk/wallet_transfer/WalletTransferFormatDto.ts";
import type { WalletTransferInspectionDto } from "./bindings/ksp_app_wallet_desk/wallet_import/WalletTransferInspectionDto.ts";
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";
@@ -54,6 +56,7 @@ let authorizedWalletProjection: WalletAuthorizedDto | null = null;
let configuredSecretCandidateCount = 0;
let deleteNoteModal: Modal | null = null;
let disableViewModal: Modal | null = null;
let exportWalletModal: Modal | null = null;
let recreateViewModal: Modal | null = null;
let pendingDeleteNoteId: string | null = null;
let stagedImportSource: WalletTransferInspectionDto | null = null;
@@ -187,7 +190,7 @@ function initializeWalletTable(): void {
zeroRecords: "Aucun wallet correspondant.",
},
});
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.011-strong-view-administration" });
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.012-owner-transfer-export" });
}
function renderWalletInventory(entries: WalletInventoryEntryDto[]): void {
@@ -227,6 +230,7 @@ function updateSessionActions(): void {
updateOwnerMetadataActions();
updateSecurityRotationActions();
updateStrongViewSecurityActions();
updateWalletExportActions();
}
function updateOwnerMetadataActions(): void {
@@ -259,6 +263,13 @@ function updateStrongViewSecurityActions(): void {
});
}
function updateWalletExportActions(): void {
const ownerOpen = activeSessionState === "owner_open" && authorizedWalletProjection?.capability === "owner";
document.querySelectorAll<HTMLSelectElement | HTMLButtonElement>("[data-owner-export-control]").forEach(control => {
control.disabled = !ownerOpen;
});
}
function updateUnlockActions(): void {
const locked = activeSessionState === "locked";
const viewManual = document.querySelector<HTMLButtonElement>("#unlockViewManual");
@@ -392,6 +403,7 @@ function clearAuthorizedProjection(): void {
clearUnlockSensitiveInputs();
setText("#rotationStatus", "Session OWNER requise.");
setText("#viewSecurityStatus", "Session OWNER requise pour administrer fortement VIEW.");
setText("#walletExportStatus", "Session OWNER requise. Aucun export texte ou clipboard nest disponible.");
}
function clearSelectedWallet(): void {
@@ -430,6 +442,7 @@ function renderLockedWallet(wallet: LockedWalletDto): void {
setText("#currentWalletSecretCandidates", configuredSecretCandidateCount.toString());
setText("#unlockStatus", "Prêt pour une tentative explicite. Argon2 peut prendre plusieurs secondes.");
setText("#viewSecurityStatus", "Unlock OWNER requis pour administrer fortement VIEW.");
setText("#walletExportStatus", "Unlock OWNER requis pour exporter la keypair Solana.");
updateSessionActions();
frontendDebug("main", "Locked Wallet selection rendered", {
configuredSecretCandidateCount,
@@ -481,6 +494,12 @@ function renderAuthorizedWallet(wallet: WalletAuthorizedDto): void {
: "OWNER ouvert : VIEW disabled. Strong recreate disponible."
: "VIEW ouvert : ladministration forte VIEW est réservée à OWNER.",
);
setText(
"#walletExportStatus",
wallet.capability === "owner"
? "OWNER ouvert : export fichier Solana CLI JSON ou Base58 disponible via save picker natif."
: "VIEW ouvert : export de keypair interdit ; OWNER requis.",
);
updateSessionActions();
frontendDebug("main", "Authorized Wallet session rendered", {
capability: wallet.capability,
@@ -1244,6 +1263,67 @@ function bindStrongViewSecurityActions(): void {
frontendTrace("main", "Strong VIEW security action handlers installed");
}
async function runWalletExport(walletId: string, format: WalletTransferFormatDto): Promise<void> {
activeSessionState = "privileged_operation";
setText("#currentWalletState", "OWNER export…");
setText("#walletExportStatus", "Save picker natif / export secret en cours…");
updateSessionActions();
frontendDebug("main", "Wallet OWNER transfer export requested", { format, walletId });
const request: WalletExportRequestDto = { format };
try {
const result = await invokeKsp<WalletExportResultDto | null>("main", "export_wallet_transfer", { request });
if (activeWalletId !== walletId || authorizedWalletProjection?.capability !== "owner") {
frontendWarn("main", "Stale Wallet export response ignored", { format, walletId });
return;
}
activeSessionState = "owner_open";
setText("#currentWalletState", "OWNER open");
if (result === null) {
setText("#walletExportStatus", "Export annulé dans le save picker natif ; aucun fichier créé.");
updateSessionActions();
frontendDebug("main", "Wallet OWNER transfer export cancelled", { format, walletId });
return;
}
setText("#walletExportStatus", `Export ${importFormatLabel(result.format)} créé : ${result.destinationName}. Le chemin complet et le contenu secret nont pas traversé IPC.`);
updateSessionActions();
frontendInfo("main", "Wallet OWNER transfer export completed", { format: result.format, walletId });
} catch {
if (activeWalletId === walletId && authorizedWalletProjection?.capability === "owner") {
activeSessionState = "owner_open";
setText("#currentWalletState", "OWNER open");
setText("#walletExportStatus", "Export refusé ou impossible ; aucun écrasement nest autorisé et la session OWNER reste ouverte.");
updateSessionActions();
}
frontendWarn("main", "Wallet OWNER transfer export failed", { format, walletId });
}
}
function bindWalletExportActions(): void {
const modalElement = document.querySelector<HTMLElement>("#exportWalletKeypairModal");
if (modalElement) {
exportWalletModal = new Modal(modalElement);
}
document.querySelector<HTMLButtonElement>("#exportWalletKeypair")?.addEventListener("click", () => {
if (!activeWalletId || activeSessionState !== "owner_open" || authorizedWalletProjection?.capability !== "owner") {
setText("#walletExportStatus", "Session OWNER requise.");
return;
}
frontendDebug("main", "Wallet OWNER transfer export confirmation opened", { walletId: activeWalletId });
exportWalletModal?.show();
});
document.querySelector<HTMLButtonElement>("#confirmExportWalletKeypair")?.addEventListener("click", () => {
const walletId = activeWalletId;
const formatControl = document.querySelector<HTMLSelectElement>("#walletExportFormat");
exportWalletModal?.hide();
if (!walletId || activeSessionState !== "owner_open" || authorizedWalletProjection?.capability !== "owner" || !formatControl) {
return;
}
const format = formatControl.value as WalletTransferFormatDto;
void runWalletExport(walletId, format);
});
frontendTrace("main", "Wallet OWNER transfer export handlers installed");
}
function bindRotationActions(): void {
document.querySelector<HTMLButtonElement>("#rotateOwnerPasswordSubmit")?.addEventListener("click", () => {
void rotateWalletPassword("owner");
@@ -1357,7 +1437,7 @@ function renderRuntimeStatus(status: RuntimeStatusDto): void {
setText("#runtimeEffectiveWalletsDirectory", status.effectiveWalletsDirectory);
setText("#runtimeWalletDirectoryCreated", status.effectiveWalletsDirectoryCreatedOnStartup ? "oui" : "non, déjà présent");
setText("#runtimeShellPhase", status.shellPhase);
setText("#shellStatus", "Config/Transport résolus ; import, VIEW/OWNER, metadata, rotations, strong VIEW et getBalance prêts.");
setText("#shellStatus", "Config/Transport résolus ; import/export, VIEW/OWNER, metadata, rotations, strong VIEW et getBalance prêts.");
frontendTrace("main", "Wallet Desk runtime status rendered", {
compositeProfile: status.activeCompositeProfile,
fallbackLoggingActive: status.fallbackLoggingActive,
@@ -1411,6 +1491,7 @@ async function initializeMain(): Promise<void> {
bindOwnerMetadataActions();
bindRotationActions();
bindStrongViewSecurityActions();
bindWalletExportActions();
bindShellActions();
clearSelectedWallet();
activateView("dashboard", "startup");