feat: secrets masqués — couverture universelle clés API/mots de passe + clic pour copier (#188)
CI / lint (push) Successful in 2m40s
CI / security (push) Successful in 1m33s
CI / test (push) Successful in 4m19s
CI / build (push) Successful in 1m31s
CI / e2e (push) Successful in 16m56s

This commit is contained in:
2026-10-08 13:22:27 -04:00
parent 634ba8a272
commit e01e837a2a
20 changed files with 520 additions and 65 deletions
+6 -2
View File
@@ -5302,8 +5302,12 @@ curl -X POST https://votre-serveur.com/webhook \
<ul>
<li>
<strong>Secret redactor</strong><span data-i18n="help.desc_9846fc07"> : Masque
JWT, clés API, tokens GitHub dans les
aperçus</span></li>
JWT, mots de passe et clés API (OpenAI,
Anthropic, GitHub, Google, AWS, Slack,
Stripe, GitLab, Hugging Face…) dans les
aperçus markdown ; cliquez sur un masque
pour copier la valeur dans le
presse-papiers</span></li>
<li>
<strong>Path traversal</strong> : Validation
des chemins contre les attaques
+24 -2
View File
@@ -7276,7 +7276,10 @@ export function renderFile(data) {
prettyBtn.classList.add("active");
} else {
mdDiv.innerHTML = data.html;
mdDiv.querySelectorAll("pre code").forEach((block) => safeHighlight(block));
mdDiv.querySelectorAll("pre code").forEach((block) => {
if (block.querySelector(".secret-mask")) return;
safeHighlight(block);
});
prettyBtn.classList.remove("active");
}
});
@@ -7327,8 +7330,10 @@ export function renderFile(data) {
renderBacklinksPanel(data.vault, data.path, area);
}
// Highlight code blocks
// Highlight code blocks — a block carrying a secret mask (#188) must be
// left alone: highlight.js rebuilds the markup and would drop the badge.
area.querySelectorAll("pre code").forEach((block) => {
if (block.querySelector(".secret-mask")) return;
safeHighlight(block);
});
@@ -7370,6 +7375,23 @@ export function renderFile(data) {
});
});
// Secret masks (#188): click copies the real value to the clipboard.
// The handler is delegated (and attached once) so it survives re-renders.
area.querySelectorAll(".secret-mask").forEach((mask) => {
mask.title = t("viewer.secret_copy_hint");
});
if (!area.dataset.secretMaskWired) {
area.dataset.secretMaskWired = "1";
area.addEventListener("click", (e) => {
const mask = e.target instanceof Element ? e.target.closest(".secret-mask") : null;
if (!mask || !area.contains(mask)) return;
const secret = mask.getAttribute("data-secret");
if (!secret) return;
const ok = copyToClipboard(secret);
showToast(ok ? t("viewer.secret_copied") : t("viewer.secret_copy_failed"), ok ? "success" : "error");
});
}
safeCreateIcons();
area.scrollTop = 0;
+4 -1
View File
@@ -1004,7 +1004,7 @@
"help.desc_8f0bffe3": "JWT tokens",
"help.desc_9186a3a3": "Explorez l'arborescence : Naviguez dans les dossiers",
"help.desc_93902d18": "Utilisez les tags : Filtrez par tags pour affiner",
"help.desc_9846fc07": "In the",
"help.desc_9846fc07": ": Masks JWTs, passwords and API keys (OpenAI, Anthropic, GitHub, Google, AWS, Slack, Stripe, GitLab, Hugging Face…) in markdown previews — click a mask to copy the value to the clipboard",
"help.desc_985d759b": "Token unique : 64 caractères hexadécimaux, impossible à deviner",
"help.desc_991307d6": " — Generate content from the selection:",
"help.desc_995a711f": ": Opens the CodeMirror editor",
@@ -2221,6 +2221,9 @@
"viewer.export_html": "Export as HTML",
"viewer.export_md_bundle": "Export as Markdown bundle (.zip)",
"viewer.export_start": "Exporting...",
"viewer.secret_copy_hint": "Click to copy the value",
"viewer.secret_copied": "Value copied to the clipboard",
"viewer.secret_copy_failed": "Copy failed",
"viewer.export_title": "Export document",
"viewer.forge_brand": "Forge",
"viewer.forge_title": "Forge (new editor)",
+4 -1
View File
@@ -1004,7 +1004,7 @@
"help.desc_8f0bffe3": ": Tokens JWT\n avec mots de passe hachés Argon2id",
"help.desc_9186a3a3": "Explorez l'arborescence :\n Naviguez dans les dossiers",
"help.desc_93902d18": "Utilisez les tags : Filtrez\n par tags pour affiner",
"help.desc_9846fc07": ": Masque\n JWT, clés API, tokens GitHub dans les\n aperçus",
"help.desc_9846fc07": ": Masque JWT, mots de passe et clés API (OpenAI, Anthropic, GitHub, Google, AWS, Slack, Stripe, GitLab, Hugging Face…) dans les aperçus markdown ; cliquez sur un masque pour copier la valeur dans le presse-papiers",
"help.desc_985d759b": "Token unique : 64\n caractères hexadécimaux, impossible à\n deviner",
"help.desc_991307d6": "— Génère du\n contenu à partir de la sélection :",
"help.desc_995a711f": ": Ouvre l'éditeur\n CodeMirror",
@@ -2221,6 +2221,9 @@
"viewer.export_html": "Exporter en HTML",
"viewer.export_md_bundle": "Exporter en bundle Markdown (.zip)",
"viewer.export_start": "Export en cours...",
"viewer.secret_copy_hint": "Cliquer pour copier la valeur",
"viewer.secret_copied": "Valeur copiée dans le presse-papiers",
"viewer.secret_copy_failed": "Échec de la copie",
"viewer.export_title": "Exporter le document",
"viewer.forge_brand": "Forge",
"viewer.forge_title": "Forge (nouvel éditeur)",
+12
View File
@@ -2324,6 +2324,18 @@ select {
cursor: default;
}
/* Secret masks (#188) — click copies the real value to the clipboard */
.secret-mask {
cursor: pointer;
padding: 0 3px;
border-radius: 3px;
border-bottom: 1px dashed var(--text-muted);
background: var(--bg-secondary);
}
.secret-mask:hover {
background: var(--bg-hover);
}
/* Image placeholders */
.image-not-found {
display: inline-block;