v0.1.4-pre.013
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-app-config-desk/README.md -->
|
||||
<!-- version: 13 -->
|
||||
<!-- version: 14 -->
|
||||
|
||||
# `ksp-app-config-desk`
|
||||
|
||||
@@ -28,7 +28,7 @@ Le gabarit desktop actif fournit maintenant :
|
||||
- DataTables 3 + Select 4 pour l’inventaire interactif des documents Config ;
|
||||
- le panneau Documents générique alimenté par le registre `ksp-config-lib`, avec diagnostics backend et réparation validée ;
|
||||
- le panneau Profils générique pour `default_profile`, sélection explicite, vues global/profil/effective sûre et provenance ;
|
||||
- le panneau Environnement alimenté uniquement par `ConfigManagement`, avec rapport desired/effective sûr, source/shadowing et management atomique `.env` create/update/remove ;
|
||||
- le panneau Environnement alimenté uniquement par `ConfigManagement`, avec rapport desired/effective sûr, source/shadowing, management atomique `.env` et reveal Secret privilégié/transitoire ;
|
||||
- `tauri-plugin-tracing` côté Rust et `@fltsci/tauri-plugin-tracing` côté frontend ;
|
||||
- les ports dédiés `1430` pour Vite HTTP et `1431` pour le WebSocket de développement ;
|
||||
- la destination frontend externe commune à Tauri et Vite;
|
||||
@@ -55,6 +55,7 @@ frontend/
|
||||
│ ├── invoke.ts
|
||||
│ ├── main.ts
|
||||
│ ├── profiles.ts
|
||||
│ ├── secret_reveal.ts
|
||||
│ └── splash.ts
|
||||
├── main.html
|
||||
└── splash.html
|
||||
@@ -151,8 +152,10 @@ Le splash utilise `DOS_Amazigh.ttf` (`font-family: Dos Amazigh`) pour son titre,
|
||||
|
||||
La vue **Environnement / `.env`** expose la projection sûre de `ConfigManagement::environment_report()` puis une zone de management qui appelle exclusivement `ConfigManagement::set_dotenv_value()` et `ConfigManagement::remove_dotenv_value()`. Elle ne lit jamais directement le process environment ni le fichier `.env`. Pour chaque variable KSP/KSPB actuellement présente dans le process ou `.env`, le frontend reçoit uniquement : nom, namespace, sensibilité, valeur desired sûre, valeur effective sûre, source effective et indicateur de shadowing.
|
||||
|
||||
Les mutations acceptent un nom existant ou nouveau sous `KSP_*` / `KSPB_*`, présentent explicitement `source_changed`, `effective_changed`, `shadowed_by_process_environment` et `reload_required`, puis rechargent le rapport depuis Config. Une ligne du tableau ne précharge que le nom de variable : aucune ancienne valeur réelle n'est injectée dans le formulaire. Pour un namespace Secret, la nouvelle saisie est masquée ; la consultation d'une valeur réelle reste réservée au flux privilégié de `pre.013`.
|
||||
Les mutations acceptent un nom existant ou nouveau sous `KSP_*` / `KSPB_*`, présentent explicitement `source_changed`, `effective_changed`, `shadowed_by_process_environment` et `reload_required`, puis rechargent le rapport depuis Config. Une ligne du tableau ne précharge que le nom de variable : aucune ancienne valeur réelle n'est injectée dans le formulaire. Pour un namespace Secret, la nouvelle saisie est masquée. Les boutons **Révéler effective** et **Révéler .env** ouvrent un flux privilégié distinct : confirmation Bootstrap explicite, commande Tauri dédiée, appel exclusif aux méthodes `ConfigManagement::reveal_*`, puis affichage temporaire masqué par défaut. Le backend refuse ce flux pour les namespaces non-Secret.
|
||||
|
||||
La suppression demande une confirmation via un modal Bootstrap intégré au shell. Les dialogues navigateur natifs `window.alert` / `window.confirm` / `window.prompt` ne sont pas utilisés pour ce flux.
|
||||
|
||||
La valeur révélée n'est jamais stockée dans `AppState`, n'est jamais incluse dans un snapshot/diagnostic/log et est effacée du DOM à la fermeture du modal ou au changement de vue. Aucun `localStorage`/`sessionStorage` n'est utilisé.
|
||||
|
||||
Les valeurs `KSP_SECRET_*` / `KSPB_SECRET_*` restent redacted par `ksp-config-lib` dans le rapport. La priorité `process > .env > fallback` reste visible : une mutation `.env` shadowed peut changer le desired sans modifier l'effective. Les logs frontend/backend n'enregistrent jamais la valeur soumise, seulement le nom de variable, l'opération et les booléens du rapport de changement.
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<!-- file: crates/ksp-app-config-desk/TODO.md -->
|
||||
<!-- version: 11 -->
|
||||
<!-- version: 12 -->
|
||||
|
||||
# TODO `ksp-app-config-desk`
|
||||
|
||||
Le gabarit frontend, le bootstrap Config/Logging, `AppState`, le `LoggingGuard` durable, les premiers DTO TS-RS, le bridge frontend logging KSP, le lifecycle `splash -> main`, les panneaux Documents, Profils/provenance et Environnement avec management `.env` sont maintenant présents.
|
||||
Le gabarit frontend, le bootstrap Config/Logging, `AppState`, le `LoggingGuard` durable, les premiers DTO TS-RS, le bridge frontend logging KSP, le lifecycle `splash -> main`, les panneaux Documents, Profils/provenance et Environnement avec management `.env` et reveal Secret privilégié sont maintenant présents.
|
||||
|
||||
Les prochaines tranches doivent encore :
|
||||
|
||||
- développer les surfaces Secrets privilégiées puis Logging ;
|
||||
- développer les surfaces Logging ;
|
||||
- faire évoluer le runtime Logging vers des fichiers applicatifs uniques par lancement (`app-name.<start-timestamp>.<ext>`) ;
|
||||
- ramener le target applicatif de `debug` à `info` ou `warn` avant clôture de la release ;
|
||||
- démontrer le hot reload Logging et le panneau de test de routage ;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-app-config-desk/USAGE.md -->
|
||||
<!-- version: 13 -->
|
||||
<!-- version: 14 -->
|
||||
|
||||
# Utilisation de `ksp-app-config-desk`
|
||||
|
||||
@@ -192,7 +192,9 @@ shadowed_by_process_environment
|
||||
reload_required
|
||||
```
|
||||
|
||||
Cliquer une ligne du rapport remplit uniquement le nom de variable. L'ancienne valeur n'est jamais préchargée dans le formulaire. Une saisie `KSP_SECRET_*` / `KSPB_SECRET_*` est masquée ; le reveal de l'ancienne valeur réelle reste hors de cette tranche.
|
||||
Cliquer une ligne du rapport remplit uniquement le nom de variable. L'ancienne valeur n'est jamais préchargée dans le formulaire. Une saisie `KSP_SECRET_*` / `KSPB_SECRET_*` est masquée. Pour une variable Secret sélectionnée, **Révéler effective** et **Révéler .env** deviennent disponibles. Ils ouvrent un modal Bootstrap de confirmation avant toute commande Tauri privilégiée.
|
||||
|
||||
Le reveal utilise un DTO et une commande séparés des rapports ordinaires. Après confirmation, la valeur réelle est affichée dans un champ transitoire `password`, masqué par défaut ; **Afficher/Masquer** ne modifie que le contrôle local. Fermer le modal ou quitter la vue Environnement efface immédiatement la valeur du DOM. Le backend refuse le reveal des namespaces Public/Internal et ne journalise que le nom, la source demandée et la présence/absence d'une valeur.
|
||||
|
||||
Le bouton **Supprimer** ouvre un modal Bootstrap indiquant la variable ciblée. **Annuler**, la fermeture du modal ou `Esc` abandonnent l'opération ; **Supprimer** confirme ensuite l'appel à `ConfigManagement::remove_dotenv_value()`. Aucun `window.confirm()` natif n'est utilisé.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-app-config-desk/frontend/main.html -->
|
||||
<!-- version: 8 -->
|
||||
<!-- version: 9 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -265,6 +265,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center mb-3">
|
||||
<span class="small text-body-secondary me-1">Reveal Secret privilégié :</span>
|
||||
<button id="revealEnvironmentEffective" class="btn btn-outline-warning btn-sm" type="button" disabled>Révéler effective</button>
|
||||
<button id="revealEnvironmentDotenv" class="btn btn-outline-warning btn-sm" type="button" disabled>Révéler .env</button>
|
||||
<span class="form-text mb-0">Disponible uniquement pour KSP_SECRET_* / KSPB_SECRET_* et jamais préchargé automatiquement.</span>
|
||||
</div>
|
||||
|
||||
<div id="environmentMutationStatus" class="alert alert-secondary mb-3" role="status" aria-live="polite">
|
||||
Aucune mutation effectuée pendant ce lancement.
|
||||
</div>
|
||||
@@ -325,6 +332,39 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="secretRevealModal" class="modal fade" tabindex="-1" aria-labelledby="secretRevealModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="secretRevealModalLabel" class="modal-title fs-5">Reveal Secret privilégié</h2>
|
||||
<button class="btn-close" type="button" data-bs-dismiss="modal" aria-label="Fermer"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="secretRevealConfirmPanel">
|
||||
<p class="mb-2">Variable : <code id="secretRevealVariable" class="font-monospace"></code></p>
|
||||
<p class="mb-3">Source demandée : <code id="secretRevealSource"></code></p>
|
||||
<div class="alert alert-warning small mb-3" role="note">
|
||||
Cette action expose temporairement la valeur réelle dans la WebView. Elle n'est ni journalisée, ni ajoutée à AppState, ni persistée par le frontend.
|
||||
</div>
|
||||
</div>
|
||||
<div id="secretRevealResultPanel" hidden>
|
||||
<label class="form-label fw-semibold" for="secretRevealValue">Valeur réelle transitoire</label>
|
||||
<div class="input-group">
|
||||
<input id="secretRevealValue" class="form-control font-monospace" type="password" readonly autocomplete="off" spellcheck="false">
|
||||
<button id="secretRevealToggleVisibility" class="btn btn-outline-secondary" type="button">Afficher</button>
|
||||
</div>
|
||||
<div id="secretRevealValueAbsent" class="alert alert-secondary small mb-0" hidden>Aucune valeur n'existe dans cette source.</div>
|
||||
</div>
|
||||
<p id="secretRevealStatus" class="text-body-secondary small mt-3 mb-0" aria-live="polite"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline-secondary" type="button" data-bs-dismiss="modal">Fermer</button>
|
||||
<button id="secretRevealConfirmAction" class="btn btn-warning" type="button">Révéler</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="app-footer bg-dark text-light">
|
||||
<div class="container h-100 d-flex align-items-center justify-content-center">
|
||||
<small>© 2026 SASEDEV</small>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/frontend/ts/environment.ts
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Safe environment report and `.env` management panel backed exclusively by ConfigManagement.
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { ConfigEnvironmentChangeDto } from "./bindings/ksp_app_config_desk/
|
||||
import type { ConfigEnvironmentReportDto } from "./bindings/ksp_app_config_desk/environment/ConfigEnvironmentReportDto";
|
||||
import { frontendDebug, frontendTrace } from "./frontend_log";
|
||||
import { invokeKsp } from "./invoke";
|
||||
import { initializeSecretReveal, requestSecretReveal } from "./secret_reveal";
|
||||
|
||||
interface EnvironmentDataTable {
|
||||
destroy(): unknown;
|
||||
@@ -17,6 +18,7 @@ interface EnvironmentDataTable {
|
||||
|
||||
let environmentTable: EnvironmentDataTable | null = null;
|
||||
let currentReports: ConfigEnvironmentReportDto[] = [];
|
||||
let mutationControlsDisabled = false;
|
||||
|
||||
function setEnvironmentStatus(message: string, tone: "primary" | "success" | "warning" | "danger" = "primary"): void {
|
||||
const status = document.querySelector<HTMLElement>("#environmentStatus");
|
||||
@@ -163,9 +165,16 @@ function updateEditorSensitivity(): void {
|
||||
: sensitivity === "invalid"
|
||||
? "Saisis un nom KSP_* ou KSPB_* ; Config reste l'autorité de validation."
|
||||
: `${sensitivityLabel(sensitivity)} : la valeur saisie n'est jamais préchargée depuis .env.`;
|
||||
updateRevealButtons(sensitivity);
|
||||
frontendTrace("main", "Environment mutation editor sensitivity updated", { sensitivity });
|
||||
}
|
||||
|
||||
function updateRevealButtons(sensitivity = classifyEditorSensitivity(document.querySelector<HTMLInputElement>("#environmentVariableName")?.value.trim() ?? "")): void {
|
||||
const revealDisabled = mutationControlsDisabled || sensitivity !== "secret";
|
||||
document.querySelector<HTMLButtonElement>("#revealEnvironmentEffective")?.toggleAttribute("disabled", revealDisabled);
|
||||
document.querySelector<HTMLButtonElement>("#revealEnvironmentDotenv")?.toggleAttribute("disabled", revealDisabled);
|
||||
}
|
||||
|
||||
function selectVariableForMutation(variableName: string, source: "table" | "input"): void {
|
||||
const variable = document.querySelector<HTMLInputElement>("#environmentVariableName");
|
||||
const value = document.querySelector<HTMLInputElement>("#environmentVariableValue");
|
||||
@@ -218,8 +227,10 @@ function renderMutationResult(result: ConfigEnvironmentChangeDto): void {
|
||||
}
|
||||
|
||||
function setMutationControlsDisabled(disabled: boolean): void {
|
||||
mutationControlsDisabled = disabled;
|
||||
document.querySelector<HTMLButtonElement>("#setEnvironmentValue")?.toggleAttribute("disabled", disabled);
|
||||
document.querySelector<HTMLButtonElement>("#removeEnvironmentValue")?.toggleAttribute("disabled", disabled);
|
||||
updateRevealButtons();
|
||||
}
|
||||
|
||||
function confirmEnvironmentRemoval(variableName: string): Promise<boolean> {
|
||||
@@ -380,6 +391,14 @@ function bindEnvironmentActions(): void {
|
||||
frontendTrace("main", "Environment remove value button clicked");
|
||||
void removeEnvironmentValue();
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#revealEnvironmentEffective")?.addEventListener("click", () => {
|
||||
const variableName = document.querySelector<HTMLInputElement>("#environmentVariableName")?.value.trim() ?? "";
|
||||
requestSecretReveal(variableName, "effective");
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#revealEnvironmentDotenv")?.addEventListener("click", () => {
|
||||
const variableName = document.querySelector<HTMLInputElement>("#environmentVariableName")?.value.trim() ?? "";
|
||||
requestSecretReveal(variableName, "dotenv");
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#clearEnvironmentEditor")?.addEventListener("click", () => {
|
||||
const variable = document.querySelector<HTMLInputElement>("#environmentVariableName");
|
||||
const value = document.querySelector<HTMLInputElement>("#environmentVariableValue");
|
||||
@@ -396,6 +415,7 @@ function bindEnvironmentActions(): void {
|
||||
}
|
||||
|
||||
export function initializeEnvironmentPanel(): void {
|
||||
initializeSecretReveal();
|
||||
bindEnvironmentActions();
|
||||
updateEditorSensitivity();
|
||||
void refreshEnvironmentReport();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/frontend/ts/main.ts
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
import "bootstrap";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
@@ -11,6 +11,7 @@ import { initializeDocumentsPanel } from "./documents";
|
||||
import { initializeEnvironmentPanel } from "./environment";
|
||||
import { initializeProfilesPanel } from "./profiles";
|
||||
import { invokeKsp } from "./invoke";
|
||||
import { clearTransientSecretReveal } from "./secret_reveal";
|
||||
|
||||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||
installFrontendConsoleBridge("main");
|
||||
@@ -45,6 +46,9 @@ function isViewId(value: string): value is ViewId {
|
||||
}
|
||||
|
||||
function activateView(viewId: ViewId, source: "startup" | "user"): void {
|
||||
if (viewId !== "environment") {
|
||||
clearTransientSecretReveal();
|
||||
}
|
||||
if (source === "user") {
|
||||
frontendDebug("main", "Main navigation activated", { viewId });
|
||||
}
|
||||
|
||||
206
crates/ksp-app-config-desk/frontend/ts/secret_reveal.ts
Normal file
206
crates/ksp-app-config-desk/frontend/ts/secret_reveal.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
// file: crates/ksp-app-config-desk/frontend/ts/secret_reveal.ts
|
||||
// version: 1
|
||||
|
||||
import { Modal } from "bootstrap";
|
||||
import type { SecretRevealRequestDto } from "./bindings/ksp_app_config_desk/secrets/SecretRevealRequestDto";
|
||||
import type { SecretRevealResponseDto } from "./bindings/ksp_app_config_desk/secrets/SecretRevealResponseDto";
|
||||
import { frontendDebug, frontendTrace } from "./frontend_log";
|
||||
import { invokeKsp } from "./invoke";
|
||||
|
||||
export type SecretRevealSource = "effective" | "dotenv";
|
||||
|
||||
let pendingRequest: SecretRevealRequestDto | null = null;
|
||||
let revealInFlight = false;
|
||||
let revealGeneration = 0;
|
||||
|
||||
function sourceLabel(source: SecretRevealSource): string {
|
||||
return source === "effective" ? "effective (process prioritaire sur .env)" : ".env persisté";
|
||||
}
|
||||
|
||||
function modalElement(): HTMLElement | null {
|
||||
return document.querySelector<HTMLElement>("#secretRevealModal");
|
||||
}
|
||||
|
||||
function clearSecretValue(): void {
|
||||
const input = document.querySelector<HTMLInputElement>("#secretRevealValue");
|
||||
const absent = document.querySelector<HTMLElement>("#secretRevealValueAbsent");
|
||||
const result = document.querySelector<HTMLElement>("#secretRevealResultPanel");
|
||||
const confirmation = document.querySelector<HTMLElement>("#secretRevealConfirmPanel");
|
||||
const confirm = document.querySelector<HTMLButtonElement>("#secretRevealConfirmAction");
|
||||
const toggle = document.querySelector<HTMLButtonElement>("#secretRevealToggleVisibility");
|
||||
if (input) {
|
||||
input.value = "";
|
||||
input.type = "password";
|
||||
}
|
||||
if (absent) {
|
||||
absent.hidden = true;
|
||||
}
|
||||
if (result) {
|
||||
result.hidden = true;
|
||||
}
|
||||
if (confirmation) {
|
||||
confirmation.hidden = false;
|
||||
}
|
||||
if (confirm) {
|
||||
confirm.hidden = false;
|
||||
confirm.disabled = false;
|
||||
}
|
||||
if (toggle) {
|
||||
toggle.hidden = true;
|
||||
toggle.textContent = "Afficher";
|
||||
}
|
||||
}
|
||||
|
||||
function renderRevealResponse(response: SecretRevealResponseDto): void {
|
||||
const input = document.querySelector<HTMLInputElement>("#secretRevealValue");
|
||||
const absent = document.querySelector<HTMLElement>("#secretRevealValueAbsent");
|
||||
const result = document.querySelector<HTMLElement>("#secretRevealResultPanel");
|
||||
const confirmation = document.querySelector<HTMLElement>("#secretRevealConfirmPanel");
|
||||
const confirm = document.querySelector<HTMLButtonElement>("#secretRevealConfirmAction");
|
||||
const toggle = document.querySelector<HTMLButtonElement>("#secretRevealToggleVisibility");
|
||||
if (confirmation) {
|
||||
confirmation.hidden = true;
|
||||
}
|
||||
if (confirm) {
|
||||
confirm.hidden = true;
|
||||
}
|
||||
if (result) {
|
||||
result.hidden = false;
|
||||
}
|
||||
if (response.value === null) {
|
||||
if (input) {
|
||||
input.value = "";
|
||||
input.hidden = true;
|
||||
}
|
||||
if (absent) {
|
||||
absent.hidden = false;
|
||||
}
|
||||
if (toggle) {
|
||||
toggle.hidden = true;
|
||||
}
|
||||
} else {
|
||||
if (input) {
|
||||
input.hidden = false;
|
||||
input.type = "password";
|
||||
input.value = response.value;
|
||||
}
|
||||
if (absent) {
|
||||
absent.hidden = true;
|
||||
}
|
||||
if (toggle) {
|
||||
toggle.hidden = false;
|
||||
toggle.textContent = "Afficher";
|
||||
}
|
||||
}
|
||||
frontendDebug("main", "Privileged Secret reveal rendered", {
|
||||
variableName: response.variableName,
|
||||
source: response.source,
|
||||
valuePresent: response.value !== null,
|
||||
});
|
||||
}
|
||||
|
||||
async function executeReveal(): Promise<void> {
|
||||
if (!pendingRequest || revealInFlight) {
|
||||
return;
|
||||
}
|
||||
const request = pendingRequest;
|
||||
const requestGeneration = revealGeneration;
|
||||
const confirm = document.querySelector<HTMLButtonElement>("#secretRevealConfirmAction");
|
||||
const status = document.querySelector<HTMLElement>("#secretRevealStatus");
|
||||
revealInFlight = true;
|
||||
if (confirm) {
|
||||
confirm.disabled = true;
|
||||
}
|
||||
if (status) {
|
||||
status.textContent = "Reveal privilégié en cours via ksp-config-lib...";
|
||||
}
|
||||
frontendDebug("main", "Privileged Secret reveal requested", { variableName: request.variableName, source: request.source });
|
||||
try {
|
||||
const response = await invokeKsp<SecretRevealResponseDto>("main", "reveal_environment_value", { request });
|
||||
if (requestGeneration !== revealGeneration || !pendingRequest) {
|
||||
frontendDebug("main", "Privileged Secret reveal response discarded after modal state changed", {
|
||||
variableName: request.variableName,
|
||||
source: request.source,
|
||||
});
|
||||
return;
|
||||
}
|
||||
renderRevealResponse(response);
|
||||
if (status) {
|
||||
status.textContent = response.value === null ? "Aucune valeur n'existe dans la source demandée." : "Valeur révélée temporairement. Elle sera effacée à la fermeture.";
|
||||
}
|
||||
} catch {
|
||||
if (status) {
|
||||
status.textContent = "Le reveal a été refusé ou a échoué.";
|
||||
}
|
||||
if (confirm) {
|
||||
confirm.disabled = false;
|
||||
}
|
||||
} finally {
|
||||
if (requestGeneration === revealGeneration) {
|
||||
revealInFlight = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRevealVisibility(): void {
|
||||
const input = document.querySelector<HTMLInputElement>("#secretRevealValue");
|
||||
const toggle = document.querySelector<HTMLButtonElement>("#secretRevealToggleVisibility");
|
||||
if (!input || !toggle || input.hidden) {
|
||||
return;
|
||||
}
|
||||
const show = input.type === "password";
|
||||
input.type = show ? "text" : "password";
|
||||
toggle.textContent = show ? "Masquer" : "Afficher";
|
||||
frontendDebug("main", "Transient Secret reveal visibility changed", { visible: show });
|
||||
}
|
||||
|
||||
export function requestSecretReveal(variableName: string, source: SecretRevealSource): void {
|
||||
const modal = modalElement();
|
||||
const variable = document.querySelector<HTMLElement>("#secretRevealVariable");
|
||||
const sourceElement = document.querySelector<HTMLElement>("#secretRevealSource");
|
||||
const status = document.querySelector<HTMLElement>("#secretRevealStatus");
|
||||
if (!modal || !variable || !sourceElement || !status) {
|
||||
frontendDebug("main", "Privileged Secret reveal modal is unavailable", { variableName, source });
|
||||
return;
|
||||
}
|
||||
revealGeneration += 1;
|
||||
clearSecretValue();
|
||||
pendingRequest = { variableName, source };
|
||||
variable.textContent = variableName;
|
||||
sourceElement.textContent = sourceLabel(source);
|
||||
status.textContent = "Confirmation explicite requise avant tout accès à la valeur réelle.";
|
||||
frontendDebug("main", "Privileged Secret reveal confirmation opened", { variableName, source });
|
||||
Modal.getOrCreateInstance(modal).show();
|
||||
}
|
||||
|
||||
export function clearTransientSecretReveal(): void {
|
||||
const modal = modalElement();
|
||||
const variableName = pendingRequest?.variableName ?? null;
|
||||
revealGeneration += 1;
|
||||
clearSecretValue();
|
||||
pendingRequest = null;
|
||||
revealInFlight = false;
|
||||
if (modal) {
|
||||
Modal.getOrCreateInstance(modal).hide();
|
||||
}
|
||||
frontendTrace("main", "Transient Secret reveal state cleared", { variableName });
|
||||
}
|
||||
|
||||
export function initializeSecretReveal(): void {
|
||||
const modal = modalElement();
|
||||
document.querySelector<HTMLButtonElement>("#secretRevealConfirmAction")?.addEventListener("click", () => {
|
||||
void executeReveal();
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#secretRevealToggleVisibility")?.addEventListener("click", () => {
|
||||
toggleRevealVisibility();
|
||||
});
|
||||
modal?.addEventListener("hidden.bs.modal", () => {
|
||||
const variableName = pendingRequest?.variableName ?? null;
|
||||
revealGeneration += 1;
|
||||
clearSecretValue();
|
||||
pendingRequest = null;
|
||||
revealInFlight = false;
|
||||
frontendDebug("main", "Privileged Secret reveal closed and value cleared", { variableName });
|
||||
});
|
||||
frontendTrace("main", "Privileged Secret reveal handlers installed");
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/src/constants.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Application-owned tracing targets and domains.
|
||||
|
||||
@@ -23,3 +23,5 @@ pub(crate) const TRACING_DOMAIN_DOCUMENTS: &str = "config.documents";
|
||||
pub(crate) const TRACING_DOMAIN_PROFILES: &str = "config.profiles";
|
||||
/// Structured domain for safe Config environment reports.
|
||||
pub(crate) const TRACING_DOMAIN_ENVIRONMENT: &str = "config.environment";
|
||||
/// Structured domain for explicit privileged Secret reveal operations.
|
||||
pub(crate) const TRACING_DOMAIN_SECRETS: &str = "config.secrets";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/src/errors.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
//! Application-local error codes for the configuration desktop shell.
|
||||
|
||||
@@ -32,3 +32,8 @@ pub(crate) const ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED: ksp_core_lib::ErrorCo
|
||||
|
||||
/// A Documents command requested a registered file that is not a Config-kind document.
|
||||
pub(crate) const ERROR_CODE_DOCUMENT_KIND_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "document_kind_invalid");
|
||||
/// Privileged reveal was requested for a non-Secret KSP/KSPB namespace.
|
||||
pub(crate) const ERROR_CODE_SECRET_REVEAL_REQUIRES_SECRET: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("config_desk", "secret_reveal_requires_secret");
|
||||
/// Privileged reveal requested an unsupported source selector.
|
||||
pub(crate) const ERROR_CODE_SECRET_REVEAL_SOURCE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "secret_reveal_source_invalid");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/src/lib.rs
|
||||
// version: 9
|
||||
// version: 10
|
||||
|
||||
//! Tauri desktop application for managing and validating KSP configuration.
|
||||
|
||||
@@ -16,6 +16,7 @@ mod environment;
|
||||
mod errors;
|
||||
mod frontend_logging;
|
||||
mod profiles;
|
||||
mod secrets;
|
||||
mod splash;
|
||||
mod tauri;
|
||||
mod tw_main;
|
||||
@@ -32,6 +33,7 @@ pub(crate) use self::constants::TRACING_DOMAIN_DOCUMENTS;
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_ENVIRONMENT;
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_PROFILES;
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_SECRETS;
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
pub(crate) use self::constants::TRACING_TARGET_FRONTEND;
|
||||
@@ -53,6 +55,8 @@ pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
|
||||
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
|
||||
pub(crate) use self::errors::ERROR_CODE_PROFILE_CONTRACT_MISSING;
|
||||
pub(crate) use self::errors::ERROR_CODE_PROFILE_PROJECTION_FAILED;
|
||||
pub(crate) use self::errors::ERROR_CODE_SECRET_REVEAL_REQUIRES_SECRET;
|
||||
pub(crate) use self::errors::ERROR_CODE_SECRET_REVEAL_SOURCE_INVALID;
|
||||
pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID;
|
||||
pub(crate) use self::errors::ERROR_CODE_SPLASH_SETTING_INVALID;
|
||||
pub(crate) use self::errors::ERROR_CODE_TAURI_RUNTIME_FAILED;
|
||||
@@ -62,6 +66,8 @@ pub(crate) use self::frontend_logging::FrontendLogPayloadDto;
|
||||
pub(crate) use self::frontend_logging::emit_frontend_log_event;
|
||||
pub(crate) use self::profiles::ConfigProfileDetailDto;
|
||||
pub(crate) use self::profiles::ConfigProfileDocumentDto;
|
||||
pub(crate) use self::secrets::SecretRevealRequestDto;
|
||||
pub(crate) use self::secrets::SecretRevealResponseDto;
|
||||
pub(crate) use self::splash::SplashOrderDto;
|
||||
pub(crate) use self::splash::SplashSettings;
|
||||
pub(crate) use self::tw_main::show_and_focus as show_main_window;
|
||||
|
||||
105
crates/ksp-app-config-desk/src/secrets.rs
Normal file
105
crates/ksp-app-config-desk/src/secrets.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
// file: crates/ksp-app-config-desk/src/secrets.rs
|
||||
// version: 1
|
||||
|
||||
//! Privileged, explicitly requested Config Secret reveal boundary for Config Desk.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// Privileged reveal request kept separate from ordinary environment-report DTOs.
|
||||
#[derive(serde::Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/secrets/SecretRevealRequestDto.ts")]
|
||||
pub(crate) struct SecretRevealRequestDto {
|
||||
/// Secret KSP/KSPB variable to reveal.
|
||||
pub(crate) variable_name: String,
|
||||
/// Requested source: `effective` or `dotenv`.
|
||||
pub(crate) source: String,
|
||||
}
|
||||
|
||||
/// Privileged reveal response. This type deliberately does not derive `Clone` or `Debug`.
|
||||
#[derive(serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/secrets/SecretRevealResponseDto.ts")]
|
||||
pub(crate) struct SecretRevealResponseDto {
|
||||
/// Secret variable that was explicitly requested.
|
||||
pub(crate) variable_name: String,
|
||||
/// Source that was explicitly requested.
|
||||
pub(crate) source: String,
|
||||
/// Real value when the requested source contains one; never include this field in logs or ordinary application state.
|
||||
pub(crate) value: std::option::Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq)]
|
||||
enum SecretRevealSource {
|
||||
Effective,
|
||||
DotEnv,
|
||||
}
|
||||
|
||||
impl SecretRevealSource {
|
||||
fn parse(value: &str) -> ksp_core_lib::Result<Self> {
|
||||
return match value {
|
||||
"effective" => std::result::Result::Ok(Self::Effective),
|
||||
"dotenv" => std::result::Result::Ok(Self::DotEnv),
|
||||
_ => std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_SECRET_REVEAL_SOURCE_INVALID,
|
||||
"Secret reveal source must be effective or dotenv",
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
const fn label(self) -> &'static str {
|
||||
return match self {
|
||||
Self::Effective => "effective",
|
||||
Self::DotEnv => "dotenv",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Reveals one real Secret value only after the dedicated Tauri command has been explicitly invoked.
|
||||
pub(crate) fn reveal(state: &crate::AppState, request: SecretRevealRequestDto) -> ksp_core_lib::Result<SecretRevealResponseDto> {
|
||||
return reveal_from_management(state.config_management(), request);
|
||||
}
|
||||
|
||||
fn reveal_from_management(management: &ksp_config_lib::ConfigManagement, request: SecretRevealRequestDto) -> ksp_core_lib::Result<SecretRevealResponseDto> {
|
||||
let source = validate_request(request.variable_name.as_str(), request.source.as_str());
|
||||
let source = match source {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let value = match source {
|
||||
SecretRevealSource::Effective => management.reveal_effective_environment_value(request.variable_name.as_str()),
|
||||
SecretRevealSource::DotEnv => management.reveal_dotenv_value(request.variable_name.as_str()),
|
||||
};
|
||||
let value = match value {
|
||||
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_SECRETS,
|
||||
variable_name = request.variable_name.as_str(),
|
||||
source = source.label(),
|
||||
value_present = value.is_some(),
|
||||
"privileged Config Secret reveal completed"
|
||||
);
|
||||
return std::result::Result::Ok(SecretRevealResponseDto { variable_name: request.variable_name, source: source.label().to_owned(), value });
|
||||
}
|
||||
|
||||
fn validate_request(variable_name: &str, source: &str) -> ksp_core_lib::Result<SecretRevealSource> {
|
||||
let sensitivity = ksp_config_lib::ConfigSensitivity::from_variable_name(variable_name);
|
||||
let sensitivity = match sensitivity {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if sensitivity != ksp_config_lib::ConfigSensitivity::Secret {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_SECRET_REVEAL_REQUIRES_SECRET,
|
||||
"Privileged reveal is restricted to KSP/KSPB Secret namespaces",
|
||||
));
|
||||
}
|
||||
return SecretRevealSource::parse(source);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/secrets.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/src/tauri.rs
|
||||
// version: 10
|
||||
// version: 11
|
||||
|
||||
//! Tauri runtime assembly for the KSP configuration desktop application.
|
||||
|
||||
@@ -47,6 +47,7 @@ fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tau
|
||||
get_environment_report,
|
||||
set_environment_value,
|
||||
remove_environment_value,
|
||||
reveal_environment_value,
|
||||
emit_frontend_log,
|
||||
splash_frontend_ready
|
||||
]);
|
||||
@@ -157,6 +158,18 @@ fn remove_environment_value(
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn reveal_environment_value(
|
||||
request: crate::SecretRevealRequestDto,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::SecretRevealResponseDto, crate::CommandErrorDto> {
|
||||
let result = crate::secrets::reveal(&state, request);
|
||||
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);
|
||||
|
||||
32
crates/ksp-app-config-desk/unit_tests/secrets.rs
Normal file
32
crates/ksp-app-config-desk/unit_tests/secrets.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
// file: crates/ksp-app-config-desk/unit_tests/secrets.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn reveal_policy_accepts_only_secret_names_and_supported_sources() {
|
||||
let effective = super::validate_request("KSP_SECRET_TEST_VALUE", "effective");
|
||||
assert!(effective.is_ok());
|
||||
if let std::result::Result::Ok(source) = effective {
|
||||
assert_eq!(source.label(), "effective");
|
||||
}
|
||||
let dotenv = super::validate_request("KSPB_SECRET_TEST_VALUE", "dotenv");
|
||||
assert!(dotenv.is_ok());
|
||||
if let std::result::Result::Ok(source) = dotenv {
|
||||
assert_eq!(source.label(), "dotenv");
|
||||
}
|
||||
assert!(super::validate_request("KSP_PUBLIC_TEST_VALUE", "effective").is_err());
|
||||
assert!(super::validate_request("KSP_TEST_VALUE", "effective").is_err());
|
||||
assert!(super::validate_request("EXTERNAL_SECRET_VALUE", "effective").is_err());
|
||||
assert!(super::validate_request("KSP_SECRET_TEST_VALUE", "unknown").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reveal_response_type_does_not_require_debug_or_clone_contracts() {
|
||||
let response = super::SecretRevealResponseDto {
|
||||
variable_name: "KSP_SECRET_TEST_VALUE".to_owned(),
|
||||
source: "dotenv".to_owned(),
|
||||
value: std::option::Option::Some("secret-canary".to_owned()),
|
||||
};
|
||||
assert_eq!(response.variable_name, "KSP_SECRET_TEST_VALUE");
|
||||
assert_eq!(response.source, "dotenv");
|
||||
assert_eq!(response.value.as_deref(), std::option::Option::Some("secret-canary"));
|
||||
}
|
||||
Reference in New Issue
Block a user