feat: add sensitive data redaction to export (Phase C)

Server-side regex redaction masks IPs, emails, bearer/API tokens, and
UNC paths in exported session content. Redaction runs post-generation
and post-variable-resolution with fail-closed error handling. Frontend
gets a "Mask Sensitive Data" toggle in the export preview modal with
a summary of what was redacted. 24 unit tests passing, frontend build clean.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-14 00:11:20 -05:00
parent 1172c5394f
commit 303570ca2c
9 changed files with 427 additions and 45 deletions

View File

@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
import { Copy, Download, Check, RotateCcw } from 'lucide-react'
import { Modal } from '@/components/common/Modal'
import { cn } from '@/lib/utils'
import type { RedactionSummary } from '@/types'
interface ExportPreviewModalProps {
isOpen: boolean
@@ -12,6 +13,9 @@ interface ExportPreviewModalProps {
onDownload: (content: string) => void
includeSummary?: boolean
onToggleSummary?: (include: boolean) => void
redactionEnabled?: boolean
onToggleRedaction?: (enabled: boolean) => void
redactionSummary?: RedactionSummary | null
}
export function ExportPreviewModal({
@@ -23,6 +27,9 @@ export function ExportPreviewModal({
onDownload,
includeSummary = false,
onToggleSummary,
redactionEnabled = false,
onToggleRedaction,
redactionSummary,
}: ExportPreviewModalProps) {
const [copied, setCopied] = useState(false)
const [editedContent, setEditedContent] = useState(content)
@@ -71,17 +78,43 @@ export function ExportPreviewModal({
<span className="ml-2 text-xs text-yellow-400">(edited)</span>
)}
</p>
<div className="flex items-center gap-3">
{onToggleSummary && (
<label className="flex items-center gap-2 text-sm text-white/60 cursor-pointer">
<input
type="checkbox"
checked={includeSummary}
onChange={(e) => onToggleSummary(e.target.checked)}
className="h-4 w-4 rounded border-white/20 bg-black/50"
/>
Include Summary
</label>
<div className="flex flex-col items-end gap-1">
<div className="flex items-center gap-3">
{onToggleSummary && (
<label className="flex items-center gap-2 text-sm text-white/60 cursor-pointer">
<input
type="checkbox"
checked={includeSummary}
onChange={(e) => onToggleSummary(e.target.checked)}
className="h-4 w-4 rounded border-white/20 bg-black/50"
/>
Include Summary
</label>
)}
{onToggleRedaction && (
<label className="flex items-center gap-2 text-sm text-white/60 cursor-pointer">
<input
type="checkbox"
checked={redactionEnabled}
onChange={(e) => onToggleRedaction(e.target.checked)}
className="h-4 w-4 rounded border-white/20 bg-black/50"
/>
Mask Sensitive Data
</label>
)}
</div>
{redactionEnabled && redactionSummary && redactionSummary.total > 0 && (
<p className="text-xs text-blue-400">
Masked: {[
redactionSummary.ips > 0 && `${redactionSummary.ips} IP${redactionSummary.ips !== 1 ? 's' : ''}`,
redactionSummary.emails > 0 && `${redactionSummary.emails} email${redactionSummary.emails !== 1 ? 's' : ''}`,
redactionSummary.tokens > 0 && `${redactionSummary.tokens} token${redactionSummary.tokens !== 1 ? 's' : ''}`,
redactionSummary.unc_paths > 0 && `${redactionSummary.unc_paths} UNC path${redactionSummary.unc_paths !== 1 ? 's' : ''}`,
].filter(Boolean).join(', ')}
</p>
)}
{redactionEnabled && redactionSummary && redactionSummary.total === 0 && (
<p className="text-xs text-white/40">No sensitive data detected</p>
)}
{isModified && (
<button