feat(changes): secret redaction layer for op previews

The changes-tab op previews replay read/write/edit payloads into the DOM
(diff rows, read rows, markdown reading mode, error text) — when the model
reads a credentials file, that plaintext must not land on screen or in
inspectable DOM. New display-only redaction layer, on by default with a
one-click pane toggle (persisted in localStorage):

- path layer: credential-shaped filenames (.env / *secret* / *credential* /
  *token* / *api-key* / private keys...) mask whole files line-wise
- content layer: ordinary files mask only secret-shaped spans — secret
  field assignments keep the field name (api_key: [REDACTED]), plus bare
  token prefixes (sk- / AKIA / ghp_ / xox / ya29.), Bearer headers, and
  PEM private-key block headers
- redacted payloads flow through the same viewOp shape into diff rows,
  read rows, markdown reading mode, and error text

Known edge (documented in README): an unquoted mermaid node label
containing a redacted secret breaks the diagram and falls back to source;
workaround is quoting the label. Session data is untouched — this is
display-only and not a boundary against same-context malicious plugins.

Locales: zh/en dictionaries + all 19 shipped third-language overrides
(key-set-equal gate green). 9 new unit tests; typecheck clean; targeted
suites 40/40 green. The fs-operations failure is pre-existing on the base
branch (environment-dependent, verified via stash) and unrelated.
This commit is contained in:
lhh010
2026-09-04 13:10:21 +08:00
parent 2eb9f66ce8
commit a2d3faf7b5
26 changed files with 349 additions and 4 deletions
+1 -1
View File
@@ -44,7 +44,7 @@
- **🌐 内嵌浏览器**:多开网页 tab,后退 / 前进 / 刷新;内容运行在沙箱 iframe;外链默认按协议分流——HTTP 在侧边栏打开、HTTPS 走系统浏览器(设置页可分别调整)
- **💻 真实终端**:xterm.js + node-pty 真实 shell,断线重连回放;可选为模型注入 `terminal_*` 工具
- **📂 模型侧边栏打开(可选)**:全局设置开启后注入 `sidebar_open` 工具——模型可主动在侧边栏打开文件 / 文件夹(树以该目录为根)/ HTTP(S) 网页
- **🌿 文件变动**:Git 视角(真 diff / 历史 / 暂存·提交·还原 / worktree·子仓库选择)与本轮文件视角(模型读 / 写 / 编辑实时追踪,按文件分组、按类型筛选)**双视角合一**;统一 diff 渲染(改蓝配对 + 行内字符级高亮 + 语法着色(含 mjs/cjs/mts/cts、CSS/SCSS/Less、HTML/XML/SVG/Vue、GraphQL、JSONC/JSON5)+ 上下文折叠),底部可拖拽预览面板,可一键展开为独立 diff tab(默认自由浮窗,可在设置改为面板下半 split);`.md` 操作(读 / 写 / 编辑)预览头部可切换**阅读模式**——经共享 MarkdownText 渲染 GFM 表格 / 任务列表 / 删除线 / 脚注 / 数学公式,本地图片自动改写为 `/sidebar/file` 媒体路由;含 ```mermaid 围栏时走编辑器同款懒加载 mermaid 渲染器(图可点击缩放 / 平移)
- **🌿 文件变动**:Git 视角(真 diff / 历史 / 暂存·提交·还原 / worktree·子仓库选择)与本轮文件视角(模型读 / 写 / 编辑实时追踪,按文件分组、按类型筛选)**双视角合一**;统一 diff 渲染(改蓝配对 + 行内字符级高亮 + 语法着色(含 mjs/cjs/mts/cts、CSS/SCSS/Less、HTML/XML/SVG/Vue、GraphQL、JSONC/JSON5)+ 上下文折叠),底部可拖拽预览面板,可一键展开为独立 diff tab(默认自由浮窗,可在设置改为面板下半 split);`.md` 操作(读 / 写 / 编辑)预览头部可切换**阅读模式**——经共享 MarkdownText 渲染 GFM 表格 / 任务列表 / 删除线 / 脚注 / 数学公式,本地图片自动改写为 `/sidebar/file` 媒体路由;含 ```mermaid 围栏时走编辑器同款懒加载 mermaid 渲染器(图可点击缩放 / 平移);**敏感内容脱敏**——凭据形态路径整文件遮罩、普通文件按内容形态遮值(api_key: / Bearer / sk- / AKIA / ghp_ / PEM 等,字段名保留),默认开启、预览面板一键开关(localStorage 记忆),仅影响显示、不改会话数据。已知边界:mermaid 无引号节点标签含被遮密钥时,图回退源码(规避:标签加引号)
- **🧩 后台任务页**:subagent 拓扑 + 后台任务(退出码 / 实时输出 / 强制终止)
- **💬 侧边对话(beta)**:Codex 风格的侧边线程——继承主会话完整上下文(含进行中的回合与工具调用)独立运行,不进入主会话;线程内可持续追问,一键「保存为新会话」提升为顶层会话
- **🪟 双工作台**:右侧栏 + 底部面板;拖 Tab 拆分 / 合并分栏(可跨面板),移动端自动合并全宽抽屉
+1 -1
View File
@@ -44,7 +44,7 @@
- **🌐 Embedded Browser**: multiple web tabs with back / forward / refresh; content runs in a sandboxed iframe; external links are routed by protocol by default — HTTP opens in the sidebar, HTTPS goes to the system browser (both adjustable in settings)
- **💻 Real Terminal**: xterm.js + node-pty real shell, reconnect with transcript replay; optionally injects `terminal_*` tools for the model
- **📂 Model-driven sidebar opens (opt-in)**: with the global setting on, the `sidebar_open` tool lets the model actively open files / folders (tree rooted there) / HTTP(S) pages in the sidebar
- **🌿 Changes**: one tab, two lenses — **Git** (real diff / history / stage·commit·revert / worktree & child-repo selection) and **This Session** (live tracking of every file the model reads / writes / edits, grouped by file with kind filters); a unified diff renderer (mod pairing + intra-line character highlights + syntax coloring incl. mjs/cjs/mts/cts, CSS/SCSS/Less, HTML/XML/SVG/Vue, GraphQL, JSONC/JSON5 + context folding), a draggable bottom preview pane, and one-click expansion into a dedicated diff tab; `.md` ops (read / write / edit) offer a **reading-mode** toggle in the preview header — the shared MarkdownText renders GFM tables / task lists / strikethrough / footnotes / math, with local images rewritten through the /sidebar/file media route; documents with ```mermaid fences render through the editor's lazy mermaid renderer (click-to-zoom / pan diagrams)
- **🌿 Changes**: one tab, two lenses — **Git** (real diff / history / stage·commit·revert / worktree & child-repo selection) and **This Session** (live tracking of every file the model reads / writes / edits, grouped by file with kind filters); a unified diff renderer (mod pairing + intra-line character highlights + syntax coloring incl. mjs/cjs/mts/cts, CSS/SCSS/Less, HTML/XML/SVG/Vue, GraphQL, JSONC/JSON5 + context folding), a draggable bottom preview pane, and one-click expansion into a dedicated diff tab; `.md` ops (read / write / edit) offer a **reading-mode** toggle in the preview header — the shared MarkdownText renders GFM tables / task lists / strikethrough / footnotes / math, with local images rewritten through the /sidebar/file media route; documents with ```mermaid fences render through the editor's lazy mermaid renderer (click-to-zoom / pan diagrams); **secret redaction** — credential-shaped paths mask whole files and ordinary files mask secret-shaped values (api_key: / Bearer / sk- / AKIA / ghp_ / PEM …, field names kept), on by default with a one-click preview-pane toggle (persisted in localStorage), display-only — session data untouched. Known edge: an unquoted mermaid label containing a redacted secret breaks the diagram (falls back to source); workaround: quote the label
- **🧩 Background Tasks**: agent topology + background tasks (exit codes / live output / force-kill)
- **💬 Side Chat (beta)**: Codex-style side threads — the child inherits the parent's FULL context (completed turns + the pending question + the in-progress turn's assistant output and tool activity, honestly frozen as "interrupted") and runs independently without entering the main conversation; threads support continuous follow-ups (auto-resumed after a DSH restart) and one-click "Save as new session" promotion to a top-level session
- **🪟 Dual Workbench**: right sidebar + bottom panel; drag tabs to split / merge panes (cross-panel), mobile auto-merges into a full-width drawer
+50 -2
View File
@@ -21,6 +21,7 @@ import { DiffFiles } from '../diff/DiffFiles.tsx'
import { langOfPath } from '../diff/highlight.ts'
import { buildDiffSegments, diffLines, diffStats, parseUnifiedDiff, unifiedSegments, type DiffRow } from '../diff/rows.ts'
import { parseReadContent, parseReadLines, type FileOp } from './ops.ts'
import { redactText } from '../redact.ts'
import { rewriteLocalImageUrls } from '../markdown-images.ts'
import { markdownTextProps } from '../markdown-labels.tsx'
import { splitMermaidBlocks } from '../mermaid-blocks.ts'
@@ -150,8 +151,40 @@ export function DiffPane({ target, scope, height, onHeightCommit, onClose, onExp
// ── Op target material (pure snapshots; the prior content came with the
// target so a running op shows what is already known). ────────────────
const op = target.kind === 'op' ? target.op : null
const prior = target.kind === 'op' ? target.prior : undefined
const opRaw = target.kind === 'op' ? target.op : null
const priorRaw = target.kind === 'op' ? target.prior : undefined
// Secret redaction: on by default, toggle persists per browser (localStorage).
// Every op payload consumer below (diff rows, read rows, markdown source,
// error text) renders from the REDACTED shape, so masked payloads are the
// only thing that can reach the DOM while the toggle is on. Display-only:
// session events and the fs layer keep their original bytes.
const [redactionOn, setRedactionOn] = useState((): boolean => {
try { return localStorage.getItem('dsh-better-sidebar.redaction') !== '0' } catch { return true }
})
const toggleRedaction = (): void => {
setRedactionOn((prev) => {
const next = !prev
try { localStorage.setItem('dsh-better-sidebar.redaction', next ? '1' : '0') } catch { /* storage unavailable */ }
return next
})
}
const { op, prior, redactionHit } = useMemo(() => {
const path = target.kind === 'op' ? target.path : ''
if (opRaw === null || !redactionOn) {
return { op: opRaw, prior: priorRaw, redactionHit: false }
}
const mask = (text: string): string => redactText(path, text).text
const hit = [opRaw.read, opRaw.content, opRaw.edit?.oldString, opRaw.edit?.newString, opRaw.errorText, priorRaw]
.some((text) => text !== undefined && redactText(path, text).hit)
const redacted: FileOp = {
...opRaw,
...(opRaw.read !== undefined ? { read: mask(opRaw.read) } : {}),
...(opRaw.content !== undefined ? { content: mask(opRaw.content) } : {}),
...(opRaw.edit !== undefined ? { edit: { oldString: mask(opRaw.edit.oldString), newString: mask(opRaw.edit.newString) } } : {}),
...(opRaw.errorText !== undefined ? { errorText: mask(opRaw.errorText) } : {}),
}
return { op: redacted, prior: priorRaw === undefined ? undefined : mask(priorRaw), redactionHit: true }
}, [opRaw, priorRaw, target, redactionOn]);
const opLang = useMemo(() => (target.kind === 'op' ? langOfPath(target.path) : undefined), [target])
const opRows = useMemo(() => (op === null ? [] : diffOf(op, prior)), [op, prior])
const opSegments = useMemo(() => buildDiffSegments(opRows), [opRows])
@@ -294,6 +327,21 @@ export function DiffPane({ target, scope, height, onHeightCommit, onClose, onExp
</button>
</>
)}
{redactionHit && redactionOn && (
<span className={css.redactBanner} role="status">{t('changesRedactBanner')}</span>
)}
{target.kind === 'op' && (
<button
type="button"
className={css.mdToggle}
data-on={redactionOn ? 'true' : undefined}
aria-pressed={redactionOn}
onClick={toggleRedaction}
title={redactionOn ? t('changesRedactOff') : t('changesRedactOn')}
>
{redactionOn ? t('changesRedactOnLabel') : t('changesRedactOffLabel')}
</button>
)}
{mdOp && (
<button
type="button"
+1
View File
@@ -342,6 +342,7 @@
/* ── Shared preview pane ───────────────────────────────────────────────── */
.redactBanner { flex: none; font: var(--dsw-font-xxxs-strong-11); padding: 2px 8px; border-radius: 7px; color: var(--dsw-alias-state-warning-primary, #b8860b); background: var(--dsw-alias-interactive-bg-hover); white-space: nowrap; }
.mdToggle { flex: none; border: 1px solid var(--dsw-alias-border-l1); border-radius: 7px; background: transparent; color: var(--dsw-alias-label-secondary); font: var(--dsw-font-xxxs-strong-11); padding: 2px 8px; cursor: pointer; }
.mdToggle:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
.mdToggle[data-on='true'] { border-color: var(--dsw-alias-state-business-primary); color: var(--dsw-alias-state-business-primary); }
+5
View File
@@ -424,4 +424,9 @@ export const ar: Record<string, string> = {
pluginDocsPanelDesc: '«وثائق عامة» في شريط DSH الجانبي: ملاحظات Markdown عامة، قابلة للقراءة من أي مساحة عمل — قائمة ملفات، مخطط تفصيلي، فتح في Chrome / VS Code، وأزرار نسخ؛ دليل الوثائق قابل للتكوين (الافتراضي ~/.dsh/docs)',
pluginEgoBrowserDesc: 'متصفح الوكيل لـ DeepSeek Harness: 32 أداة ego_* تقود متصفح Chromium حقيقي، مع تبويب «متصفح ego» أصلي في الشريط الجانبي يعرض مباشرة كل صفحة يزورها الوكيل — يمكنك النقر والسحب والكتابة لتولي التحكم. يسجّل التبويب تلقائيًا عند وجود better-sidebar، وإلا يظهر كفقاعة عائمة',
pluginBilingualReaderDesc: 'اقرأ ملفات PDF في الشريط الجانبي لـ DSH: عرض PDF أصلي، حدد نصًا لترجمته بواسطة النموذج اللغوي مع السياق، معزولًا تمامًا عن المحادثة الرئيسية',
changesRedactOn: "تنقيع مُفعّل: تظهر الأسرار كـ [REDACTED]؛ انقر للتعطيل (على مسؤوليتك)",
changesRedactOff: "تنقيع مُعطّل: سيُعرض المحتوى كما هو؛ انقر للتفعيل",
changesRedactOnLabel: "تنقيع",
changesRedactOffLabel: "بدون تنقيع",
changesRedactBanner: "مُنقّى",
}
+5
View File
@@ -409,4 +409,9 @@ export const de: Record<string, string> = {
pluginDocsPanelDesc: '„Globale Dokumente“ in der DSH-Seitenleiste: eigene Markdown-Notizen aus jedem Arbeitsbereich lesen – Dateiliste, Gliederung, Öffnen in Chrome / VS Code und Kopieren-Buttons; das Dokumentenverzeichnis ist konfigurierbar (Standard ~/.dsh/docs)',
pluginEgoBrowserDesc: 'Der Agent-Browser für DeepSeek Harness: 32 ego_*-Tools steuern ein echtes Chromium; ein nativer „ego-Browser“-Tab in der Seitenleiste zeigt live jede Seite, die der Agent besucht – Klicken, Ziehen und Tippen zum Übernehmen. Registriert den Tab automatisch, wenn better-sidebar vorhanden ist, sonst eine schwebende Blase',
pluginBilingualReaderDesc: 'PDFs im DSH-Seitenbereich lesen: native PDF-Anzeige, Text auswählen und mit dem LLM übersetzen — mit Kontext, vollständig vom Hauptgespräch isoliert',
changesRedactOn: "Schwärzung aktiv: Geheimnisse als [REDACTED]; Klicken zum Deaktivieren (auf eigenes Risiko)",
changesRedactOff: "Schwärzung aus: Inhalte werden unverändert angezeigt; Klicken zum Aktivieren",
changesRedactOnLabel: "Schwärzung",
changesRedactOffLabel: "Ohne Schwärzung",
changesRedactBanner: "Geschwärzt",
}
+5
View File
@@ -416,4 +416,9 @@ export const fr: Record<string, string> = {
pluginDocsPanelDesc: '« Documentation globale » dans la barre latérale DSH : notes Markdown globales, lisibles depuis n’importe quel espace de travail — sélection dans la liste, saut via le plan flottant, ouverture externe Chrome / VS Code, copie de code, répertoire configurable (défaut ~/.dsh/docs)',
pluginEgoBrowserDesc: 'Le navigateur d’agent pour DeepSeek Harness : 32 outils ego_* pilotent un vrai Chromium ; un onglet natif « ego browser » dans la barre latérale montre en direct chaque page visitée par l’agent — cliquez, glissez et tapez pour reprendre la main. Enregistre l’onglet automatiquement si better-sidebar est présent, sinon une bulle flottante',
pluginBilingualReaderDesc: 'Lire des PDF dans la barre latérale DSH : affichage PDF natif, sélectionnez du texte pour le traduire avec le LLM, avec contexte et totalement isolé de la conversation principale',
changesRedactOn: "Masquage actif : les secrets s'affichent en [REDACTED] ; cliquez pour désactiver (à vos risques)",
changesRedactOff: "Masquage désactivé : le contenu s'affiche tel quel ; cliquez pour activer",
changesRedactOnLabel: "Masquage",
changesRedactOffLabel: "Sans masquage",
changesRedactBanner: "Masqué",
}
+5
View File
@@ -423,4 +423,9 @@ export const hi: Record<string, string> = {
pluginDocsPanelDesc: 'DSH साइडबार में ग्लोबल डॉक्स: किसी भी वर्कस्पेस से अपने Markdown नोट्स पढ़ें — फ़ाइल सूची, रूपरेखा, Chrome / VS Code में खोलें, और कॉपी बटन; docs डायरेक्टरी कॉन्फ़िगर करने योग्य (डिफ़ॉल्ट ~/.dsh/docs)',
pluginEgoBrowserDesc: 'DeepSeek Harness के लिए एजेंट ब्राउज़र: 32 ego_* टूल असली Chromium चलाते हैं; साइडबार में नेटिव «ego browser» टैब एजेंट के हर विज़िट किए गए पेज को लाइव दिखाता है — आप क्लिक, ड्रैग और टाइप करके नियंत्रण ले सकते हैं। better-sidebar मौजूद होने पर टैब स्वतः रजिस्टर होता है, अन्यथा फ्लोटिंग बबल के रूप में दिखता है',
pluginBilingualReaderDesc: 'DSH साइडबार में PDF पढ़ें: मूल PDF प्रदर्शन, टेक्स्ट चुनकर LLM से अनुवाद करें, संदर्भ के साथ और मुख्य संवाद से पूरी तरह अलग',
changesRedactOn: "रिडैक्शन चालू: संवेदनशील मान [REDACTED] दिखेंगे; बंद करने के लिए क्लिक करें (अपने जोखिम पर)",
changesRedactOff: "रिडैक्शन बंद: सामग्री जैसी है वैसी दिखेगी; चालू करने के लिए क्लिक करें",
changesRedactOnLabel: "रिडैक्शन",
changesRedactOffLabel: "बिना रिडैक्शन",
changesRedactBanner: "रिडैक्टेड",
}
+5
View File
@@ -421,4 +421,9 @@ export const id: Record<string, string> = {
pluginDocsPanelDesc: 'Dokumen global di sidebar DSH: baca catatan Markdown Anda sendiri dari ruang kerja apa pun — daftar berkas, kerangka, buka di Chrome / VS Code, dan tombol salin; direktori docs dapat dikonfigurasi (default ~/.dsh/docs)',
pluginEgoBrowserDesc: 'Browser agen untuk DeepSeek Harness: 32 alat ego_* menggerakkan Chromium sungguhan; tab «ego browser» native di bilah samping menampilkan langsung setiap halaman yang dikunjungi agen — Anda bisa mengklik, menyeret, dan mengetik untuk mengambil alih. Tab terdaftar otomatis jika better-sidebar terpasang; jika tidak, jatuh ke gelembung mengambang',
pluginBilingualReaderDesc: 'Baca PDF di sidebar DSH: tampilan PDF asli, pilih teks untuk diterjemahkan dengan LLM, dengan konteks dan sepenuhnya terisolasi dari percakapan utama',
changesRedactOn: "Redaksi aktif: rahasia tampil sebagai [REDACTED]; klik untuk mematikan (risiko Anda sendiri)",
changesRedactOff: "Redaksi mati: konten tampil apa adanya; klik untuk mengaktifkan",
changesRedactOnLabel: "Redaksi",
changesRedactOffLabel: "Tanpa redaksi",
changesRedactBanner: "Teredaksi",
}
+5
View File
@@ -414,4 +414,9 @@ export const it: Record<string, string> = {
pluginDocsPanelDesc: 'Documenti globali nella barra laterale di DSH: legga le sue note Markdown da qualsiasi spazio di lavoro — una lista di file, una struttura, apertura in Chrome / VS Code e pulsanti di copia; la directory dei documenti è configurabile (predefinita ~/.dsh/docs)',
pluginEgoBrowserDesc: 'Il browser agente per DeepSeek Harness: 32 strumenti ego_* pilotano un vero Chromium; una scheda nativa «ego browser» nella barra laterale mostra in tempo reale ogni pagina visitata dall’agente — puoi cliccare, trascinare e digitare per prendere il controllo. Registra la scheda automaticamente se better-sidebar è presente, altrimenti una bolla flottante',
pluginBilingualReaderDesc: 'Leggi PDF nella barra laterale DSH: visualizzazione PDF nativa, seleziona il testo per tradurlo con l’LLM, con contesto e completamente isolato dalla conversazione principale',
changesRedactOn: "Mascheramento attivo: i segreti appaiono come [REDACTED]; clicca per disattivare (a tuo rischio)",
changesRedactOff: "Mascheramento disattivo: il contenuto appare invariato; clicca per attivare",
changesRedactOnLabel: "Mascheramento",
changesRedactOffLabel: "Senza mascheramento",
changesRedactBanner: "Mascherato",
}
+5
View File
@@ -423,4 +423,9 @@ export const ja: Record<string, string> = {
pluginDocsPanelDesc: 'DSH サイドバー内の「グローバルドキュメント」:任意のワークスペースから読めるグローバル Markdown ノート。リストで選んで閲覧、ホバーでアウトラインジャンプ、Chrome / VS Code で外部オープン、コードコピー。ディレクトリは設定可(デフォルト ~/.dsh/docs)',
pluginEgoBrowserDesc: 'DeepSeek Harness 用エージェントブラウザ:32 個の ego_* ツールが本物の Chromium を操作し、サイドバーのネイティブ「ego ブラウザ」Tab がエージェントの訪れる全ページをライブ表示。クリック・ドラッグ・入力で操作を引き継げます。better-sidebar があれば Tab を自動登録、なければフローティングバブルにフォールバック',
pluginBilingualReaderDesc: 'DSH サイドバーで論文 PDF を読む:ネイティブ PDF 表示、テキストを選択して LLM で翻訳。コンテキストを使い、メイン会話から完全に分離',
changesRedactOn: "マスキング有効:機密値は [REDACTED] で表示。クリックで無効化(自己責任)",
changesRedactOff: "マスキング無効:内容がそのまま表示されます。クリックで有効化",
changesRedactOnLabel: "マスキング中",
changesRedactOffLabel: "マスキング解除",
changesRedactBanner: "マスク済み",
}
+5
View File
@@ -415,4 +415,9 @@ export const ko: Record<string, string> = {
pluginDocsPanelDesc: 'DSH 사이드바의 "전역 문서": 전역 Markdown 메모로, 어떤 작업 공간에서든 언제든 읽을 수 있습니다. 목록을 클릭해 읽기, 마우스를 올리면 개요로 이동, Chrome / VS Code에서 외부 열기, 코드 복사. 디렉터리를 설정할 수 있습니다(기본 ~/.dsh/docs)',
pluginEgoBrowserDesc: 'DeepSeek Harness용 에이전트 브라우저: 32개의 ego_* 도구가 실제 Chromium을 구동하며, 사이드바의 네이티브 «ego 브라우저» 탭에서 에이전트가 방문하는 모든 페이지를 실시간으로 볼 수 있습니다 — 클릭, 드래그, 타이핑으로 직접 조작할 수 있습니다. better-sidebar가 있으면 탭을 자동 등록하고, 없으면 플로팅 버블로 대체합니다',
pluginBilingualReaderDesc: 'DSH 사이드바에서 PDF 읽기: 네이티브 PDF 표시, 텍스트를 선택해 LLM으로 번역, 컨텍스트 사용 및 메인 대화와 완전 격리',
changesRedactOn: "마스킹 켜짐: 민감 값이 [REDACTED]로 표시됩니다. 클릭하면 꺼집니다(책임 본인)",
changesRedactOff: "마스킹 꺼짐: 내용이 그대로 표시됩니다. 클릭하면 켜집니다",
changesRedactOnLabel: "마스킹 중",
changesRedactOffLabel: "마스킹 해제",
changesRedactBanner: "마스킹됨",
}
+5
View File
@@ -421,4 +421,9 @@ export const nl: Record<string, string> = {
pluginDocsPanelDesc: 'Globale docs in de DSH-zijbalk: lees uw eigen Markdown-notities vanuit elke werkruimte — een bestandslijst, een outline, openen in Chrome / VS Code, en kopieerknoppen; de docs-map is configureerbaar (standaard ~/.dsh/docs)',
pluginEgoBrowserDesc: 'De agentbrowser voor DeepSeek Harness: 32 ego_*-tools besturen een echt Chromium; een native «ego-browser»-tab in de zijbalk toont live elke pagina die de agent bezoekt — u kunt klikken, slepen en typen om over te nemen. Registreert de tab automatisch als better-sidebar aanwezig is, anders een zwevende bel',
pluginBilingualReaderDesc: 'Lees PDF’s in de DSH-zijbalk: native PDF-weergave, selecteer tekst om te vertalen met de LLM, met context en volledig geïsoleerd van het hoofdgesprek',
changesRedactOn: "Redactie aan: geheimen worden als [REDACTED] getoond; klik om uit te schakelen (op eigen risico)",
changesRedactOff: "Redactie uit: inhoud wordt ongewijzigd getoond; klik om in te schakelen",
changesRedactOnLabel: "Redactie",
changesRedactOffLabel: "Zonder redactie",
changesRedactBanner: "Geredigeerd",
}
+5
View File
@@ -425,4 +425,9 @@ export const pl: Record<string, string> = {
pluginDocsPanelDesc: 'Globalne dokumenty w panelu bocznym DSH: globalne notatki Markdown, czytelne z dowolnego obszaru roboczego — lista z kliknięciem, kontur z przejściami, otwieranie w Chrome / VS Code i przyciski kopiowania kodu; katalog konfigurowalny (domyślnie ~/.dsh/docs)',
pluginEgoBrowserDesc: 'Przeglądarka agenta dla DeepSeek Harness: 32 narzędzia ego_* sterują prawdziwym Chromium; natywna karta «ego browser» na pasku bocznym pokazuje na żywo każdą stronę odwiedzaną przez agenta — możesz klikać, przeciągać i pisać, aby przejąć kontrolę. Karta rejestruje się automatycznie, gdy better-sidebar jest zainstalowany; w przeciwnym razie pływający dymek',
pluginBilingualReaderDesc: 'Czytaj PDF-y w panelu bocznym DSH: natywny podgląd PDF, zaznacz tekst, aby przetłumaczyć go LLM-em, z kontekstem i całkowicie izolowany od głównej rozmowy',
changesRedactOn: "Redakcja włączona: tajne dane pokazywane jako [REDACTED]; kliknij, aby wyłączyć (na własne ryzyko)",
changesRedactOff: "Redakcja wyłączona: treść pokazywana bez zmian; kliknij, aby włączyć",
changesRedactOnLabel: "Redakcja",
changesRedactOffLabel: "Bez redakcji",
changesRedactBanner: "Zredagowano",
}
+5
View File
@@ -406,4 +406,9 @@ export const pt: Record<string, string> = {
pluginDocsPanelDesc: 'Documentos globais na barra lateral do DSH: leia suas próprias notas Markdown de qualquer workspace — uma lista de arquivos, um sumário, abrir no Chrome / VS Code e botões de copiar; o diretório de documentos é configurável (padrão ~/.dsh/docs)',
pluginEgoBrowserDesc: 'O navegador de agente para o DeepSeek Harness: 32 ferramentas ego_* controlam um Chromium real; uma aba nativa «ego browser» na barra lateral mostra ao vivo cada página visitada pelo agente — você pode clicar, arrastar e digitar para assumir. Registra a aba automaticamente quando o better-sidebar está presente; caso contrário, uma bolha flutuante',
pluginBilingualReaderDesc: 'Leia PDFs na barra lateral do DSH: visualização nativa de PDF, selecione o texto para traduzir com o LLM, com contexto e totalmente isolado da conversa principal',
changesRedactOn: "Máscara ativa: segredos exibidos como [REDACTED]; clique para desativar (por sua conta)",
changesRedactOff: "Máscara desativada: conteúdo exibido como está; clique para ativar",
changesRedactOnLabel: "Máscara",
changesRedactOffLabel: "Sem máscara",
changesRedactBanner: "Mascarado",
}
+5
View File
@@ -421,4 +421,9 @@ export const ru: Record<string, string> = {
pluginDocsPanelDesc: '«Глобальные документы» в боковой панели DSH: глобальные Markdown-заметки, доступные из любой рабочей области — список для чтения, навигация по плавающему оглавлению, открытие в Chrome / VS Code, копирование кода; каталог настраивается (по умолчанию ~/.dsh/docs)',
pluginEgoBrowserDesc: 'Браузер агента для DeepSeek Harness: 32 инструмента ego_* управляют настоящим Chromium; встроенная вкладка «ego browser» в боковой панели показывает в реальном времени каждую страницу, которую посещает агент — кликайте, перетаскивайте и вводите текст, чтобы перехватить управление. Вкладка регистрируется автоматически при наличии better-sidebar, иначе используется плавающий пузырёк',
pluginBilingualReaderDesc: 'Читайте PDF в боковой панели DSH: нативный просмотр PDF, выделите текст для перевода с помощью LLM, используя контекст и полностью изолированно от основного разговора',
changesRedactOn: "Маскировка включена: секреты показываются как [REDACTED]; нажмите, чтобы отключить (на свой риск)",
changesRedactOff: "Маскировка выключена: содержимое показывается как есть; нажмите, чтобы включить",
changesRedactOnLabel: "Маскировка",
changesRedactOffLabel: "Без маскировки",
changesRedactBanner: "Замаскировано",
}
+5
View File
@@ -406,4 +406,9 @@ export const sv: Record<string, string> = {
pluginDocsPanelDesc: 'Globala dokument i DSH-sidopanelen: läs era egna Markdown-anteckningar från valfri arbetsyta — en fillista, en översikt, öppna i Chrome / VS Code och kopieringsknappar; dokumentkatalogen är konfigurerbar (standard ~/.dsh/docs)',
pluginEgoBrowserDesc: 'Agentwebbläsaren för DeepSeek Harness: 32 ego_*-verktyg styr en riktig Chromium; en inbyggd «ego browser»-flik i sidopanelen visar varje sida agenten besöker i realtid — du kan klicka, dra och skriva för att ta över. Registrerar fliken automatiskt när better-sidebar finns, annars en flytande bubbla',
pluginBilingualReaderDesc: 'Läs PDF-filer i DSH-sidopanelen: inbyggd PDF-visning, markera text för att översätta med LLM, med kontext och helt isolerad från huvudkonversationen',
changesRedactOn: "Maskning på: hemligheter visas som [REDACTED]; klicka för att stänga av (på egen risk)",
changesRedactOff: "Maskning av: innehåll visas som det är; klicka för att aktivera",
changesRedactOnLabel: "Maskning",
changesRedactOffLabel: "Utan maskning",
changesRedactBanner: "Maskerat",
}
+5
View File
@@ -423,4 +423,9 @@ export const th: Record<string, string> = {
pluginDocsPanelDesc: 'เอกสารส่วนกลางในแถบด้านข้าง DSH: อ่านบันทึก Markdown ของคุณเองจากพื้นที่ทำงานใดๆ — รายการไฟล์, เค้าโครง, เปิดใน Chrome / VS Code และปุ่มคัดลอก; ไดเรกทอรีเอกสารสามารถกำหนดค่าได้ (ค่าเริ่มต้น ~/.dsh/docs)',
pluginEgoBrowserDesc: 'เบราว์เซอร์เอเจนต์สำหรับ DeepSeek Harness: เครื่องมือ ego_* 32 ตัวขับเคลื่อน Chromium จริง แท็บ «ego browser» เนทีฟในแถบด้านข้างแสดงทุกหน้าที่เอเจนต์เยี่ยมชมแบบสด — คลิก ลาก และพิมพ์เพื่อเข้าไปควบคุมแทนได้ ลงทะเบียนแท็บอัตโนมัติเมื่อมี better-sidebar มิฉะนั้นจะใช้บับเบิลลอย',
pluginBilingualReaderDesc: 'อ่าน PDF ในแถบด้านข้าง DSH: แสดง PDF ต้นฉบับ เลือกข้อความเพื่อแปลด้วย LLM พร้อมบริบทและแยกจากการสนทนาหลักโดยสิ้นเชิง',
changesRedactOn: "ซ่อนข้อมูลอยู่: ค่าลับแสดงเป็น [REDACTED]; คลิกเพื่อปิด (ยอมรับความเสี่ยงเอง)",
changesRedactOff: "ปิดการซ่อนข้อมูล: เนื้อหาจะแสดงตามจริง; คลิกเพื่อเปิด",
changesRedactOnLabel: "กำลังซ่อน",
changesRedactOffLabel: "ไม่ซ่อน",
changesRedactBanner: "ถูกซ่อนแล้ว",
}
+5
View File
@@ -423,4 +423,9 @@ export const tr: Record<string, string> = {
pluginDocsPanelDesc: 'DSH kenar çubuğunda «Genel belgeler»: herhangi bir çalışma alanından kendi Markdown notlarınızı okuyun — dosya listesi, ana hat, Chrome / VS Code’da açma ve kopyalama düğmeleri; belgeler dizini yapılandırılabilir (varsayılan ~/.dsh/docs)',
pluginEgoBrowserDesc: 'DeepSeek Harness için aracı tarayıcı: 32 ego_* aracı gerçek bir Chromium’u yönetir; kenar çubuğundaki yerel «ego tarayıcı» sekmesi aracının ziyaret ettiği her sayfayı canlı gösterir — tıklayarak, sürükleyerek ve yazarak kontrolü devralabilirsiniz. better-sidebar varsa sekme otomatik kaydedilir, yoksa yüzen baloncuğa düşer',
pluginBilingualReaderDesc: 'DSH kenar çubuğunda PDF okuyun: yerel PDF görüntüleme, LLM ile çevirmek için metin seçin, bağlamı kullanarak ve ana sohbetten tamamen izole',
changesRedactOn: "Maskeleme açık: gizli değerler [REDACTED] olarak görünür; kapatmak için tıklayın (kendiniz sorumlusunuz)",
changesRedactOff: "Maskeleme kapalı: içerik olduğu gibi görünür; açmak için tıklayın",
changesRedactOnLabel: "Maskeleme",
changesRedactOffLabel: "Maskeleme yok",
changesRedactBanner: "Maskelendi",
}
+5
View File
@@ -423,4 +423,9 @@ export const vi: Record<string, string> = {
pluginDocsPanelDesc: '«Tài liệu toàn cục» trong thanh bên DSH: ghi chú Markdown toàn cục, đọc từ bất kỳ workspace nào — danh sách chọn để đọc, outline hover để nhảy, mở ngoài trong Chrome / VS Code, sao chép code, thư mục cấu hình được (mặc định ~/.dsh/docs)',
pluginEgoBrowserDesc: 'Trình duyệt agent cho DeepSeek Harness: 32 công cụ ego_* điều khiển Chromium thật; tab «ego browser» gốc trong thanh bên hiển thị trực tiếp mọi trang agent truy cập — bạn có thể bấm, kéo và gõ để tiếp quản. Tự động đăng ký tab khi có better-sidebar, nếu không thì rơi về bong bóng nổi',
pluginBilingualReaderDesc: 'Đọc PDF trong thanh bên DSH: hiển thị PDF gốc, chọn văn bản để dịch bằng LLM, dùng ngữ cảnh và hoàn toàn tách biệt khỏi cuộc trò chuyện chính',
changesRedactOn: "Đang làm mờ: giá trị nhạy cảm hiển thị [REDACTED]; nhấp để tắt (tự chịu rủi ro)",
changesRedactOff: "Đã tắt làm mờ: nội dung hiển thị nguyên bản; nhấp để bật",
changesRedactOnLabel: "Đang làm mờ",
changesRedactOffLabel: "Không làm mờ",
changesRedactBanner: "Đã làm mờ",
}
+5
View File
@@ -438,4 +438,9 @@ export const zhHK: Record<string, string> = {
pluginDocsPanelDesc: 'DSH 側邊欄裡的「全域文件」:全域 Markdown 筆記,任何工作區隨時可讀——列表點選閱讀、懸浮大綱跳轉、Chrome / VS Code 外部開啟、程式碼複製,目錄可設定(預設 ~/.dsh/docs)',
pluginEgoBrowserDesc: '將 CitroLabs/ego-lite 接進 DeepSeek Harness 的 agent 瀏覽器:32 個 ego_* 工具驅動真實 Chromium,側邊欄原生「ego 瀏覽器」Tab 實時觀察 agent 逛的每個頁面,可直接點擊/拖拽/輸入接管;裝 better-sidebar 時自動註冊 Tab,沒裝則退回浮動浮窗',
pluginBilingualReaderDesc: '喺 DSH 側邊欄讀論文 PDF:原生 PDF 顯示,揀一段文字就用大模型劃詞翻譯,結合上下文、完全隔離主對話,只做閱讀輔助',
changesRedactOn: "已開啟脫敏:敏感內容以 [REDACTED] 顯示;點擊關閉(自擔風險)",
changesRedactOff: "脫敏已關閉:將原樣顯示文件內容;點擊開啟",
changesRedactOnLabel: "脫敏中",
changesRedactOffLabel: "脫敏關",
changesRedactBanner: "已脫敏",
}
+5
View File
@@ -438,4 +438,9 @@ export const zhMO: Record<string, string> = {
pluginDocsPanelDesc: 'DSH 側邊欄裡的「全域文件」:全域 Markdown 筆記,任何工作區隨時可讀——列表點選閱讀、懸浮大綱跳轉、Chrome / VS Code 外部開啟、程式碼複製,目錄可設定(預設 ~/.dsh/docs)',
pluginEgoBrowserDesc: '將 CitroLabs/ego-lite 接進 DeepSeek Harness 的 agent 瀏覽器:32 個 ego_* 工具驅動真實 Chromium,側邊欄原生「ego 瀏覽器」Tab 實時觀察 agent 逛的每個頁面,可直接點擊/拖拽/輸入接管;裝 better-sidebar 時自動註冊 Tab,沒裝則退回浮動浮窗',
pluginBilingualReaderDesc: '喺 DSH 側邊欄讀論文 PDF:原生 PDF 顯示,揀一段文字就用大模型劃詞翻譯,結合上下文、完全隔離主對話,只做閱讀輔助',
changesRedactOn: "已開啟脫敏:敏感內容以 [REDACTED] 顯示;點擊關閉(自擔風險)",
changesRedactOff: "脫敏已關閉:將原樣顯示文件內容;點擊開啟",
changesRedactOnLabel: "脫敏中",
changesRedactOffLabel: "脫敏關",
changesRedactBanner: "已脫敏",
}
+5
View File
@@ -438,4 +438,9 @@ export const zhTW: Record<string, string> = {
pluginDocsPanelDesc: 'DSH 側邊欄裡的「全域文件」:全域 Markdown 筆記,任何工作區隨時可讀——列表點選閱讀、懸浮大綱跳轉、Chrome / VS Code 外部開啟、程式碼複製,目錄可設定(預設 ~/.dsh/docs)',
pluginEgoBrowserDesc: '將 CitroLabs/ego-lite 接進 DeepSeek Harness 的 agent 瀏覽器:32 個 ego_* 工具驅動真實 Chromium,側邊欄原生「ego 瀏覽器」Tab 實時觀察 agent 逛的每個頁面,可直接點擊/拖拽/輸入接管;裝 better-sidebar 時自動註冊 Tab,沒裝則退回浮動浮窗',
pluginBilingualReaderDesc: '在 DSH 側邊欄讀論文 PDF:原生 PDF 顯示,選取一段文字即用大模型劃詞翻譯,結合上下文、完全隔離主對話,只作閱讀輔助',
changesRedactOn: "已開啟脫敏:敏感內容以 [REDACTED] 顯示;點擊關閉(自擔風險)",
changesRedactOff: "脫敏已關閉:將原樣顯示文件內容;點擊開啟",
changesRedactOnLabel: "脫敏中",
changesRedactOffLabel: "脫敏關",
changesRedactBanner: "已脫敏",
}
+10
View File
@@ -32,6 +32,11 @@ export const zh = {
changesEdit: '编辑',
changesRunning: '执行中',
changesError: '出错',
changesRedactOn: '已开启脱敏:敏感内容以 [REDACTED] 显示;点击关闭(自担风险)',
changesRedactOff: '脱敏已关闭:将原样显示文件内容;点击开启',
changesRedactOnLabel: '脱敏中',
changesRedactOffLabel: '脱敏关',
changesRedactBanner: '已脱敏',
changesFold: '{count} 行…点击展开',
changesContext: '上下文',
changesPriorUnknown: '变更前的内容不在窗口内,显示为全新增',
@@ -443,6 +448,11 @@ export const en: Record<keyof typeof zh, string> = {
changesEdit: 'Edit',
changesRunning: 'running',
changesError: 'error',
changesRedactOn: 'Redaction on: secrets render as [REDACTED]; click to disable (at your own risk)',
changesRedactOff: 'Redaction off: file content renders verbatim; click to enable',
changesRedactOnLabel: 'Redacting',
changesRedactOffLabel: 'Redaction off',
changesRedactBanner: 'Redacted',
changesFold: '{count} lines…click to expand',
changesContext: 'context',
changesPriorUnknown: 'Prior content is outside the loaded window; shown as all-added',
+132
View File
@@ -0,0 +1,132 @@
/**
* Secret redaction for rendered file content. The tracer replays read/write
* tool results into the DOM (diff, read view, markdown mode) — when the
* model reads a credentials file, that plaintext must not land on screen or
* in inspectable DOM. Two layers, both pure and unit-tested:
*
* 1. Path layer: files whose NAME marks them as secrets (.env, *secret*,
* *credential*, *token*, *api-key*, private keys, …) are masked whole —
* every non-empty line becomes [REDACTED].
* 2. Content layer: for ordinary files, secret-SHAPED lines and tokens are
* masked in place (api_key/token/password assignments, sk-/AKIA/ghp_/
* xox prefixes, Bearer headers, PEM private-key block headers).
*
* Redaction is display-only: the session log and the tool results keep their
* original bytes; this layer guarantees only that this plugin never renders
* the secret. It is NOT a security boundary against a same-context malicious
* plugin (which can read anything on the page or call host APIs itself) —
* it removes the tracer's own contribution to secret exposure.
*/
/** Placeholder substituted for every secret found. */
export const REDACTED = '[REDACTED]'
/** Result of redacting one text: the masked text + whether anything was hit. */
export interface RedactionOutcome {
readonly text: string
readonly hit: boolean
}
/** Lowercased filename substrings that mark a whole file as sensitive. */
const SENSITIVE_PATH_PATTERNS: readonly string[] = [
'.env', 'secret', 'credential', 'token', 'api-key', 'apikey', 'password',
'passwd', 'private_key', 'privatekey', 'id_rsa', 'id_ed25519',
'.pem', '.key', '.p12', '.pfx',
]
/**
* Whether a path's own name marks the file as sensitive (case-insensitive,
* matched against the full path so .env in any directory counts).
*/
export function isSensitivePath(path: string): boolean {
const lower = path.toLowerCase()
return SENSITIVE_PATH_PATTERNS.some((pattern) => lower.includes(pattern))
}
/**
* Assignment-shaped secret lines: "api_key: …", "TOKEN = …", "password": ….
* The key name survives; the value is masked so the user still sees WHICH
* field was secret.
*/
const ASSIGNMENT_RE = /^(\s*["']?[A-Za-z0-9_.-]*["']?\s*[:=]\s*)(\S.*)$/
/**
* Field names whose assigned value is a secret. Stored in NORMALIZED form
* (lower-case, separators stripped — see normalizeField) so api_key/API-KEY/
* "api-key" all match one entry.
*/
const SECRET_FIELDS = new Set([
'apikey', 'key', 'secret', 'secretkey',
'token', 'apitoken', 'accesstoken', 'refreshtoken', 'idtoken', 'auth',
'password', 'passwd', 'pass', 'pwd',
'clientsecret', 'privatekey',
'accesskey', 'accesskeyid', 'secretaccesskey',
'bearer', 'authorization',
])
/** Bearer-scheme token in free text (covers HTTP headers in logs/configs). */
const BEARER_RE = /(\bBearer\s+)[A-Za-z0-9\-_.+/]{16,}/gi
/** Well-known credential token prefixes (OpenAI/AWS/GitHub/Slack/Google). */
const TOKEN_RES: readonly RegExp[] = [
/\bsk-[A-Za-z0-9_-]{12,}/g,
/\bAKIA[0-9A-Z]{16}\b/g,
/\bghp_[A-Za-z0-9]{20,}\b/g,
/\bgho_[A-Za-z0-9]{20,}\b/g,
/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g,
/\bya29\.[A-Za-z0-9_-]{20,}\b/g,
]
/** Mask an entire non-empty line, keeping whitespace shape. */
function maskLine(line: string): string {
return line.trim().length === 0 ? line : REDACTED
}
/** Normalize a field name for SECRET_FIELDS lookup. */
function normalizeField(raw: string): string {
return raw.replace(/["'\s:=]/g, '').toLowerCase().replace(/[^a-z0-9]/g, '')
}
/** Redact one possibly-secret assignment line; undefined when not a secret field. */
function redactAssignment(line: string): string | undefined {
const match = ASSIGNMENT_RE.exec(line)
const prefix = match?.[1]
if (prefix === undefined || match?.[2] === undefined) return undefined
if (!SECRET_FIELDS.has(normalizeField(prefix))) return undefined
return prefix + REDACTED
}
/** Whether a line is the header of a PEM private-key block. */
function isPemPrivateKeyHeader(line: string): boolean {
return line.includes('-----BEGIN') && line.includes('PRIVATE KEY-----')
}
/**
* Redact secret-shaped content from an ordinary file's text. Only the secret
* itself is masked; surrounding code/config stays readable.
*/
export function redactContentText(text: string): string {
return text
.split('\n')
.map((line) => {
if (isPemPrivateKeyHeader(line)) return REDACTED
const assignment = redactAssignment(line)
if (assignment !== undefined) return assignment
let masked = line.replace(BEARER_RE, (_m, prefix) => prefix + REDACTED)
for (const re of TOKEN_RES) masked = masked.replace(re, () => REDACTED)
return masked
})
.join('\n')
}
/**
* Apply the full two-layer redaction for one file's text. Sensitive paths
* mask whole lines; ordinary files mask only secret-shaped spans.
*/
export function redactText(path: string, text: string): RedactionOutcome {
if (isSensitivePath(path)) {
return { text: text.split('\n').map(maskLine).join('\n'), hit: text.trim().length > 0 }
}
const masked = redactContentText(text)
return { text: masked, hit: masked !== text }
}
+59
View File
@@ -0,0 +1,59 @@
import { describe, expect, it } from 'vitest'
import { isSensitivePath, redactContentText, redactText, REDACTED } from '../src/client/redact.js'
describe('isSensitivePath', () => {
it('marks credential-shaped filenames', () => {
expect(isSensitivePath('C:/proj/.env')).toBe(true)
expect(isSensitivePath('config/secrets.yaml')).toBe(true)
expect(isSensitivePath('~/auth/credentials.json')).toBe(true)
expect(isSensitivePath('deploy/api-key.prod.yml')).toBe(true)
expect(isSensitivePath('/home/u/.ssh/id_rsa')).toBe(true)
expect(isSensitivePath('cert/server.pem')).toBe(true)
})
it('passes ordinary filenames', () => {
expect(isSensitivePath('src/index.ts')).toBe(false)
expect(isSensitivePath('docs/readme.md')).toBe(false)
expect(isSensitivePath('package.json')).toBe(false)
})
})
describe('redactContentText', () => {
it('masks yaml secret assignments keeping the key name', () => {
const out = redactContentText('name: demo\napi_key: sk-1234567890abcdef\nport: 8080')
expect(out).toContain('name: demo')
expect(out).toContain('api_key: ' + REDACTED)
expect(out).toContain('port: 8080')
expect(out).not.toContain('sk-1234567890abcdef')
})
it('masks quoted json fields and equals-style env lines', () => {
expect(redactContentText('"password": "hunter2"')).toBe('"password": ' + REDACTED)
expect(redactContentText('API_TOKEN=abcdef1234567890')).toBe('API_TOKEN=' + REDACTED)
})
it('masks bare credential tokens in free text', () => {
const out = redactContentText('see sk-abcdefghijklmnop123456 and AKIAIOSFODNN7EXAMPLE')
expect(out).not.toContain('sk-abcdefghijklmnop123456')
expect(out).not.toContain('AKIAIOSFODNN7EXAMPLE')
expect(out).toContain(REDACTED)
})
it('masks bearer headers and PEM key headers', () => {
expect(redactContentText('Authorization: Bearer abcdef1234567890abcd')).toContain(REDACTED)
expect(redactContentText('-----BEGIN RSA PRIVATE KEY-----')).toBe(REDACTED)
})
it('leaves ordinary code untouched', () => {
const code = 'const x = 42\nconsole.log("count", x)'
expect(redactContentText(code)).toBe(code)
})
})
describe('redactText', () => {
it('masks a whole sensitive file line-wise', () => {
const out = redactText('/app/.env', 'USER=me\nTOKEN=abc\n\n# comment')
expect(out.text).toBe(REDACTED + '\n' + REDACTED + '\n\n' + REDACTED)
expect(out.hit).toBe(true)
})
it('reports hit=false for clean ordinary content', () => {
const out = redactText('src/main.ts', 'export function main() {}')
expect(out.hit).toBe(false)
expect(out.text).toBe('export function main() {}')
})
})