v0.2.6-pre.011

This commit is contained in:
2026-08-21 15:22:42 +02:00
parent 6dcd278459
commit 8ba554306a
14 changed files with 572 additions and 30 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-wallet-desk/frontend/ts/main.ts
// version: 11
// version: 12
import { Modal } from "bootstrap";
import DataTable from "datatables.net-bs5";
@@ -22,6 +22,7 @@ import type { WalletNoteAddRequestDto } from "./bindings/ksp_app_wallet_desk/wal
import type { WalletNoteDeleteRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_metadata/WalletNoteDeleteRequestDto.ts";
import type { WalletNoteUpdateRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_metadata/WalletNoteUpdateRequestDto.ts";
import type { WalletPasswordRotationRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_security/WalletPasswordRotationRequestDto.ts";
import type { WalletViewSecurityStatusDto } from "./bindings/ksp_app_wallet_desk/wallet_security/WalletViewSecurityStatusDto.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";
@@ -52,6 +53,8 @@ let activeViewEnabled = false;
let authorizedWalletProjection: WalletAuthorizedDto | null = null;
let configuredSecretCandidateCount = 0;
let deleteNoteModal: Modal | null = null;
let disableViewModal: Modal | null = null;
let recreateViewModal: Modal | null = null;
let pendingDeleteNoteId: string | null = null;
let stagedImportSource: WalletTransferInspectionDto | null = null;
@@ -184,7 +187,7 @@ function initializeWalletTable(): void {
zeroRecords: "Aucun wallet correspondant.",
},
});
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.010-credential-rotation" });
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.011-strong-view-administration" });
}
function renderWalletInventory(entries: WalletInventoryEntryDto[]): void {
@@ -223,6 +226,7 @@ function updateSessionActions(): void {
updateUnlockActions();
updateOwnerMetadataActions();
updateSecurityRotationActions();
updateStrongViewSecurityActions();
}
function updateOwnerMetadataActions(): void {
@@ -244,6 +248,17 @@ function updateSecurityRotationActions(): void {
});
}
function updateStrongViewSecurityActions(): void {
const ownerOpen = activeSessionState === "owner_open" && authorizedWalletProjection?.capability === "owner";
const disable = document.querySelector<HTMLButtonElement>("#disableWalletView");
if (disable) {
disable.disabled = !ownerOpen || !activeViewEnabled;
}
document.querySelectorAll<HTMLInputElement | HTMLButtonElement>("[data-view-security-recreate-control]").forEach(control => {
control.disabled = !ownerOpen;
});
}
function updateUnlockActions(): void {
const locked = activeSessionState === "locked";
const viewManual = document.querySelector<HTMLButtonElement>("#unlockViewManual");
@@ -342,6 +357,15 @@ function clearRotationSensitiveInputs(): void {
}
}
function clearStrongViewSensitiveInputs(): void {
for (const selector of ["#recreateViewPassword", "#recreateViewPasswordConfirm"]) {
const input = document.querySelector<HTMLInputElement>(selector);
if (input) {
input.value = "";
}
}
}
function clearBalanceProjection(): void {
setText("#currentWalletBalanceSol", "—");
setText("#detailsBalanceLamports", "—");
@@ -364,8 +388,10 @@ function clearAuthorizedProjection(): void {
clearBalanceProjection();
clearCreateFormSensitiveInputs();
clearRotationSensitiveInputs();
clearStrongViewSensitiveInputs();
clearUnlockSensitiveInputs();
setText("#rotationStatus", "Session OWNER requise.");
setText("#viewSecurityStatus", "Session OWNER requise pour administrer fortement VIEW.");
}
function clearSelectedWallet(): void {
@@ -403,6 +429,7 @@ function renderLockedWallet(wallet: LockedWalletDto): void {
setText("#currentWalletView", wallet.viewEnabled ? "enabled" : "disabled");
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.");
updateSessionActions();
frontendDebug("main", "Locked Wallet selection rendered", {
configuredSecretCandidateCount,
@@ -446,6 +473,14 @@ function renderAuthorizedWallet(wallet: WalletAuthorizedDto): void {
? "OWNER ouvert. Rotation OWNER et administration VIEW disponibles ; les secrets Config restent inchangés."
: "VIEW ouvert. Self-rotation VIEW disponible ; OWNER reste inaccessible.",
);
setText(
"#viewSecurityStatus",
wallet.capability === "owner"
? wallet.viewEnabled
? "OWNER ouvert : VIEW enabled. Strong disable ou strong recreate disponibles."
: "OWNER ouvert : VIEW disabled. Strong recreate disponible."
: "VIEW ouvert : ladministration forte VIEW est réservée à OWNER.",
);
updateSessionActions();
frontendDebug("main", "Authorized Wallet session rendered", {
capability: wallet.capability,
@@ -550,6 +585,7 @@ async function recoverOwnerStateConflict(walletId: string): Promise<void> {
renderLockedWallet(wallet);
setText("#ownerMetadataStatus", "Le fichier wallet a changé hors de cette session. Le handle OWNER stale a été purgé ; unlock OWNER requis avant toute nouvelle mutation.");
setText("#rotationStatus", "Conflit détat : rotation interrompue, wallet reverrouillé et réautorisation OWNER obligatoire.");
setText("#viewSecurityStatus", "Conflit détat : administration VIEW interrompue, handle OWNER purgé et réautorisation OWNER obligatoire.");
setText("#unlockStatus", "Conflit détat détecté : wallet réinspecté et reverrouillé. Réautoriser OWNER explicitement.");
await loadWalletInventory("list_wallets", false);
activateView("security", "user");
@@ -558,6 +594,7 @@ async function recoverOwnerStateConflict(walletId: string): Promise<void> {
clearSelectedWallet();
setText("#ownerMetadataStatus", "Conflit détat détecté et réinspection impossible. Resélectionner le wallet depuis linventaire.");
setText("#rotationStatus", "Conflit détat détecté ; la réinspection du wallet a échoué.");
setText("#viewSecurityStatus", "Conflit détat détecté ; la réinspection du wallet a échoué.");
frontendWarn("main", "Wallet state conflict recovery failed", { walletId });
}
}
@@ -1048,6 +1085,165 @@ async function rotateWalletPassword(capability: UnlockCapability): Promise<void>
}
}
function applyViewSecurityStatus(status: WalletViewSecurityStatusDto): boolean {
const projection = authorizedWalletProjection;
if (
!activeWalletId
|| status.walletId !== activeWalletId
|| status.capability !== "owner"
|| activeSessionState !== "privileged_operation"
|| projection?.capability !== "owner"
) {
frontendWarn("main", "Stale strong VIEW administration response ignored", { walletId: status.walletId });
return false;
}
activeSessionState = "owner_open";
activeViewEnabled = status.viewEnabled;
configuredSecretCandidateCount = status.configuredSecretCandidateCount;
authorizedWalletProjection = {
...projection,
configuredSecretCandidateCount: status.configuredSecretCandidateCount,
viewEnabled: status.viewEnabled,
};
setText("#currentWalletState", "OWNER open");
setText("#currentWalletView", status.viewEnabled ? "enabled" : "disabled");
setText("#currentWalletSecretCandidates", configuredSecretCandidateCount.toString());
setText("#unlockSecretCandidateCount", configuredSecretCandidateCount.toString());
updateSessionActions();
return true;
}
async function runStrongViewDisable(walletId: string): Promise<void> {
activeSessionState = "privileged_operation";
setText("#currentWalletState", "Strong disable VIEW…");
setText("#viewSecurityStatus", "Strong disable VIEW en cours…");
updateSessionActions();
frontendDebug("main", "Strong VIEW disable requested", { walletId });
try {
const status = await invokeKsp<WalletViewSecurityStatusDto>("main", "disable_wallet_view");
if (!applyViewSecurityStatus(status)) {
return;
}
setText("#viewSecurityStatus", "VIEW fortement désactivé. Lancien credential VIEW ne peut plus ouvrir létat courant.");
await loadWalletInventory("list_wallets", false);
frontendInfo("main", "Strong VIEW disable completed", { walletId });
} catch (caughtError) {
if (isWalletStateConflict(caughtError)) {
await recoverOwnerStateConflict(walletId);
return;
}
if (activeWalletId === walletId) {
activeSessionState = "owner_open";
setText("#currentWalletState", "OWNER open");
setText("#viewSecurityStatus", "Strong disable refusé ; la session OWNER reste ouverte.");
updateSessionActions();
}
frontendWarn("main", "Strong VIEW disable failed", { walletId });
}
}
async function runStrongViewRecreate(walletId: string, request: WalletPasswordRotationRequestDto): Promise<void> {
activeSessionState = "privileged_operation";
setText("#currentWalletState", "Strong recreate VIEW…");
setText("#viewSecurityStatus", "Strong recreate VIEW et Argon2 en cours…");
updateSessionActions();
frontendDebug("main", "Strong VIEW recreate requested", { walletId });
try {
const status = await invokeKsp<WalletViewSecurityStatusDto>("main", "recreate_wallet_view", { request });
if (!applyViewSecurityStatus(status)) {
return;
}
setText("#viewSecurityStatus", "VIEW fortement recréé avec une nouvelle autorité. Les secrets Config ne sont pas modifiés automatiquement.");
await loadWalletInventory("list_wallets", false);
frontendInfo("main", "Strong VIEW recreate completed", { walletId });
} catch (caughtError) {
if (isWalletStateConflict(caughtError)) {
await recoverOwnerStateConflict(walletId);
return;
}
if (activeWalletId === walletId) {
activeSessionState = "owner_open";
setText("#currentWalletState", "OWNER open");
setText("#viewSecurityStatus", "Strong recreate refusé ; la session OWNER reste ouverte.");
updateSessionActions();
}
frontendWarn("main", "Strong VIEW recreate failed", { walletId });
}
}
function bindStrongViewSecurityActions(): void {
const disableModalElement = document.querySelector<HTMLElement>("#disableWalletViewModal");
if (disableModalElement) {
disableViewModal = new Modal(disableModalElement);
}
const recreateModalElement = document.querySelector<HTMLElement>("#recreateWalletViewModal");
if (recreateModalElement) {
recreateViewModal = new Modal(recreateModalElement);
recreateModalElement.addEventListener("hidden.bs.modal", () => {
clearStrongViewSensitiveInputs();
});
}
document.querySelector<HTMLButtonElement>("#disableWalletView")?.addEventListener("click", () => {
if (!activeWalletId || activeSessionState !== "owner_open" || authorizedWalletProjection?.capability !== "owner" || !activeViewEnabled) {
setText("#viewSecurityStatus", "Session OWNER avec VIEW enabled requise.");
return;
}
frontendDebug("main", "Strong VIEW disable confirmation opened", { walletId: activeWalletId });
disableViewModal?.show();
});
document.querySelector<HTMLButtonElement>("#confirmDisableWalletView")?.addEventListener("click", () => {
const walletId = activeWalletId;
disableViewModal?.hide();
if (!walletId || activeSessionState !== "owner_open" || authorizedWalletProjection?.capability !== "owner" || !activeViewEnabled) {
return;
}
void runStrongViewDisable(walletId);
});
document.querySelector<HTMLButtonElement>("#recreateWalletView")?.addEventListener("click", () => {
if (!activeWalletId || activeSessionState !== "owner_open" || authorizedWalletProjection?.capability !== "owner") {
clearStrongViewSensitiveInputs();
setText("#viewSecurityStatus", "Session OWNER requise.");
return;
}
const password = document.querySelector<HTMLInputElement>("#recreateViewPassword");
const confirmation = document.querySelector<HTMLInputElement>("#recreateViewPasswordConfirm");
if (!password || !confirmation || password.value.length === 0) {
clearStrongViewSensitiveInputs();
setText("#viewSecurityStatus", "Nouveau VIEW password et confirmation requis.");
return;
}
if (password.value !== confirmation.value) {
clearStrongViewSensitiveInputs();
setText("#viewSecurityStatus", "La confirmation ne correspond pas au nouveau VIEW password.");
return;
}
frontendDebug("main", "Strong VIEW recreate confirmation opened", { walletId: activeWalletId });
recreateViewModal?.show();
});
document.querySelector<HTMLButtonElement>("#confirmRecreateWalletView")?.addEventListener("click", () => {
const walletId = activeWalletId;
const password = document.querySelector<HTMLInputElement>("#recreateViewPassword");
const confirmation = document.querySelector<HTMLInputElement>("#recreateViewPasswordConfirm");
recreateViewModal?.hide();
if (
!walletId
|| activeSessionState !== "owner_open"
|| authorizedWalletProjection?.capability !== "owner"
|| !password
|| !confirmation
|| password.value.length === 0
|| password.value !== confirmation.value
) {
clearStrongViewSensitiveInputs();
return;
}
const request: WalletPasswordRotationRequestDto = { password: password.value };
clearStrongViewSensitiveInputs();
void runStrongViewRecreate(walletId, request);
});
frontendTrace("main", "Strong VIEW security action handlers installed");
}
function bindRotationActions(): void {
document.querySelector<HTMLButtonElement>("#rotateOwnerPasswordSubmit")?.addEventListener("click", () => {
void rotateWalletPassword("owner");
@@ -1161,7 +1357,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 et getBalance prêts.");
setText("#shellStatus", "Config/Transport résolus ; import, VIEW/OWNER, metadata, rotations, strong VIEW et getBalance prêts.");
frontendTrace("main", "Wallet Desk runtime status rendered", {
compositeProfile: status.activeCompositeProfile,
fallbackLoggingActive: status.fallbackLoggingActive,
@@ -1214,6 +1410,7 @@ async function initializeMain(): Promise<void> {
bindBalanceActions();
bindOwnerMetadataActions();
bindRotationActions();
bindStrongViewSecurityActions();
bindShellActions();
clearSelectedWallet();
activateView("dashboard", "startup");