From f162f42c6b44b82ef458bf6df281665336173449 Mon Sep 17 00:00:00 2001 From: Eyejoker Date: Tue, 28 Apr 2026 13:25:46 +0900 Subject: [PATCH] Extract dashboard redaction helpers Centralize dashboard secret redaction helpers and add focused regression tests. --- apps/dashboard/src/App.tsx | 10 ++-------- apps/dashboard/src/ParsedBody.tsx | 9 ++------- apps/dashboard/src/redaction.test.ts | 26 ++++++++++++++++++++++++++ apps/dashboard/src/redaction.ts | 17 +++++++++++++++++ 4 files changed, 47 insertions(+), 15 deletions(-) create mode 100644 apps/dashboard/src/redaction.test.ts create mode 100644 apps/dashboard/src/redaction.ts diff --git a/apps/dashboard/src/App.tsx b/apps/dashboard/src/App.tsx index 74e7bb3..c5e687f 100644 --- a/apps/dashboard/src/App.tsx +++ b/apps/dashboard/src/App.tsx @@ -61,6 +61,7 @@ import { isWatcherRoomMessage, } from './roomThread'; import { ParsedBody } from './ParsedBody'; +import { redactSecretsForPreview } from './redaction'; import './styles.css'; interface DashboardState { @@ -439,18 +440,11 @@ function queueLabel( return parts.join(' · '); } -const SECRET_ASSIGNMENT_RE = - /\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|API_KEY|AUTH|PRIVATE_KEY)[A-Z0-9_]*)\s*=\s*([^\s"'`]+)/gi; -const SECRET_VALUE_RE = - /\b(?:sk-[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9_]{12,}|xox[baprs]-[A-Za-z0-9-]{12,}|eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,})\b/g; - function safePreview( value: string | null | undefined, fallback: string, ): string { - const cleaned = (value ?? '') - .replace(SECRET_ASSIGNMENT_RE, '$1=') - .replace(SECRET_VALUE_RE, '') + const cleaned = redactSecretsForPreview(value ?? '') .replace(/<\/?internal[^>]*>/gi, '') .replace(/\s+/g, ' ') .trim(); diff --git a/apps/dashboard/src/ParsedBody.tsx b/apps/dashboard/src/ParsedBody.tsx index 580ac35..a6ef402 100644 --- a/apps/dashboard/src/ParsedBody.tsx +++ b/apps/dashboard/src/ParsedBody.tsx @@ -2,10 +2,7 @@ import { useMemo, type ReactNode } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; -const SECRET_ASSIGNMENT_RE = - /\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|API_KEY|AUTH|PRIVATE_KEY)[A-Z0-9_]*)\s*=\s*([^\s"'`]+)/gi; -const SECRET_VALUE_RE = - /\b(?:sk-[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9_]{12,}|xox[baprs]-[A-Za-z0-9-]{12,}|eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,})\b/g; +import { redactSecretsForMarkdown } from './redaction'; export function ParsedBody({ text, @@ -16,9 +13,7 @@ export function ParsedBody({ }): ReactNode { const cleaned = useMemo(() => { if (!text) return ''; - let out = text - .replace(SECRET_ASSIGNMENT_RE, (_m, key) => `${key}=***`) - .replace(SECRET_VALUE_RE, '***'); + let out = redactSecretsForMarkdown(text); if (truncate && out.length > truncate) { out = out.slice(0, truncate) + '…'; } diff --git a/apps/dashboard/src/redaction.test.ts b/apps/dashboard/src/redaction.test.ts new file mode 100644 index 0000000..eba3096 --- /dev/null +++ b/apps/dashboard/src/redaction.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; + +import { redactSecretsForMarkdown, redactSecretsForPreview } from './redaction'; + +describe('dashboard redaction', () => { + it('redacts named secret assignments for markdown output', () => { + expect(redactSecretsForMarkdown('OPENAI_API_KEY=sk-testvalue1234')).toBe( + 'OPENAI_API_KEY=***', + ); + }); + + it('redacts named secret assignments for compact previews', () => { + expect(redactSecretsForPreview('BOT_TOKEN=xoxb-testvalue1234')).toBe( + 'BOT_TOKEN=', + ); + }); + + it('redacts standalone secret values consistently', () => { + const raw = 'token sk-1234567890abcdef and ghp_1234567890abcdef'; + + expect(redactSecretsForMarkdown(raw)).toBe('token *** and ***'); + expect(redactSecretsForPreview(raw)).toBe( + 'token and ', + ); + }); +}); diff --git a/apps/dashboard/src/redaction.ts b/apps/dashboard/src/redaction.ts new file mode 100644 index 0000000..a48d673 --- /dev/null +++ b/apps/dashboard/src/redaction.ts @@ -0,0 +1,17 @@ +export const SECRET_ASSIGNMENT_RE = + /\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|API_KEY|AUTH|PRIVATE_KEY)[A-Z0-9_]*)\s*=\s*([^\s"'`]+)/gi; + +export const SECRET_VALUE_RE = + /\b(?:sk-[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9_]{12,}|xox[baprs]-[A-Za-z0-9-]{12,}|eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,})\b/g; + +export function redactSecretsForMarkdown(value: string): string { + return value + .replace(SECRET_ASSIGNMENT_RE, (_m, key) => `${key}=***`) + .replace(SECRET_VALUE_RE, '***'); +} + +export function redactSecretsForPreview(value: string): string { + return value + .replace(SECRET_ASSIGNMENT_RE, '$1=') + .replace(SECRET_VALUE_RE, ''); +}