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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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: "مُنقّى",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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é",
|
||||
}
|
||||
|
||||
@@ -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: "रिडैक्टेड",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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: "マスク済み",
|
||||
}
|
||||
|
||||
@@ -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: "마스킹됨",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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: "Замаскировано",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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: "ถูกซ่อนแล้ว",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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ờ",
|
||||
}
|
||||
|
||||
@@ -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: "已脫敏",
|
||||
}
|
||||
|
||||
@@ -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: "已脫敏",
|
||||
}
|
||||
|
||||
@@ -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: "已脫敏",
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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() {}')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user