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

@@ -355,6 +355,31 @@
</div>
</div>
</div>
<div class="col-12">
<div class="card shadow-sm border-warning">
<div class="card-header fw-semibold">Administration forte VIEW — OWNER uniquement</div>
<div class="card-body">
<p class="small text-body-secondary">Ces opérations changent lautorité VIEW courante, pas seulement son password. Disable retire VIEW et rekey les metadata ; Recreate génère une nouvelle metadata key, un nouveau slot VIEW et un nouveau password. La keypair Solana, lOWNER, lalias et les notes restent inchangés.</p>
<div class="row g-3 align-items-end">
<div class="col-lg-4">
<button id="disableWalletView" class="btn btn-outline-danger w-100" type="button" disabled>Strong disable VIEW</button>
</div>
<div class="col-lg-4">
<label class="form-label" for="recreateViewPassword">Nouveau VIEW password</label>
<input id="recreateViewPassword" class="form-control" type="password" autocomplete="new-password" data-view-security-recreate-control disabled>
</div>
<div class="col-lg-4">
<label class="form-label" for="recreateViewPasswordConfirm">Confirmer VIEW password</label>
<div class="input-group">
<input id="recreateViewPasswordConfirm" class="form-control" type="password" autocomplete="new-password" data-view-security-recreate-control disabled>
<button id="recreateWalletView" class="btn btn-outline-warning" type="button" data-view-security-recreate-control disabled>Strong recreate</button>
</div>
</div>
</div>
<div id="viewSecurityStatus" class="alert alert-secondary mt-3 mb-0" role="status">Session OWNER requise pour administrer fortement VIEW.</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>
@@ -388,8 +413,38 @@
</div>
</div>
</div>
<div class="modal fade" id="disableWalletViewModal" tabindex="-1" aria-labelledby="disableWalletViewModalLabel" 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="disableWalletViewModalLabel">Strong disable VIEW ?</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">Le slot VIEW courant sera supprimé et les metadata seront rekeyées. Le password VIEW actuel ne pourra plus ouvrir létat courant. Les copies historiques du fichier restent hors de la garantie de révocation.</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button id="confirmDisableWalletView" type="button" class="btn btn-danger">Disable VIEW</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="recreateWalletViewModal" tabindex="-1" aria-labelledby="recreateWalletViewModalLabel" 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="recreateWalletViewModalLabel">Strong recreate VIEW ?</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">Une nouvelle metadata key et un nouveau slot VIEW seront créés. Tout credential VIEW courant sera remplacé. Le nouveau password nest jamais recopié dans cette confirmation.</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button id="confirmRecreateWalletView" type="button" class="btn btn-warning">Recreate VIEW</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 · credential rotation pre.010</span>
<span>KSP Wallet Desk · strong VIEW administration pre.011</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: 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");