diff --git a/app/src/components/notifications/NotificationBody.tsx b/app/src/components/notifications/NotificationBody.tsx new file mode 100644 index 0000000000..2b4b4dee88 --- /dev/null +++ b/app/src/components/notifications/NotificationBody.tsx @@ -0,0 +1,67 @@ +/** + * Shared `` rendering for notification surfaces + * (`NotificationCard` and the `Notifications` page). + * + * Notification bodies emitted by Rust error helpers can contain + * `` tags (e.g. morning-briefing + * failures pointing users at Discord / Settings). Without parsing, the raw + * tag leaks as literal text. This module mirrors the chat-side pill so the + * tag renders as a clickable pill instead. + * + * **Why this lives here and not in a global shared spot:** the chat-side + * `OpenhumanLinkPill` is a non-exported function inside `AgentMessageBubble.tsx` + * (`app/src/pages/conversations/`). Extracting from chat would change the chat + * render path — out of scope for this fix. Instead, we keep the grammar / parsing + * shared (reuses `parseBubbleSegments` from conversations) but reimplement the + * pill locally. Both notification surfaces share *this* file so the diff stays + * testable with one Vitest suite. + * + * Safety: this component renders **only** text + button elements. It never + * uses `dangerouslySetInnerHTML`, never sets an `href`, and the dispatched + * `OPENHUMAN_LINK_EVENT` is consumed by `OpenhumanLinkModal`, which hard- + * allowlists `path` values before routing. See `OpenhumanLinkModal.tsx` + * `ALLOWED_PATHS_SET`. + */ +import { parseBubbleSegments } from '../../pages/conversations/utils/format'; +import { OPENHUMAN_LINK_EVENT } from '../OpenhumanLinkModal'; + +function NotificationLinkPill({ path, label }: { path: string; label: string }) { + return ( + + ); +} + +export default function NotificationBody({ body }: { body: string }) { + const segments = parseBubbleSegments(body); + return ( + <> + {segments.map((seg, i) => + seg.kind === 'link' ? ( + + ) : ( + // React auto-escapes text content, so any other markup in the body + // (e.g. ``) renders as literal text. + {seg.text} + ) + )} + + ); +} diff --git a/app/src/components/notifications/NotificationCard.test.tsx b/app/src/components/notifications/NotificationCard.test.tsx index 2472276874..8abafa4167 100644 --- a/app/src/components/notifications/NotificationCard.test.tsx +++ b/app/src/components/notifications/NotificationCard.test.tsx @@ -1,50 +1,175 @@ -import { render, screen } from '@testing-library/react'; +/** + * Markup-leak guard for `` rendering in notification bodies. + * + * These tests are the airtight contract for issue #2279 (Bug A) — they assert: + * - well-formed tags become pills (no raw `` text leaks), + * - attacker-influenceable `path` values can't become a navigable + * `javascript:` link, and + * - non-link bodies and stray markup render as literal, auto-escaped text + * (no `'; + const { container } = renderCard(body); + + // React auto-escapes anything that's not an `` segment. + const bodyEl = screen.getByTestId('notification-card-body'); + expect(bodyEl.textContent).toBe(''); + + // Hard guarantee: no actual