From 8ee4279d300c2d364d030ad8eea88b935bf076db Mon Sep 17 00:00:00 2001 From: claude code agent 227 Date: Sat, 20 Jun 2026 21:52:32 +0300 Subject: [PATCH] harden(html-embed): make stripHtmlEmbedNodes total with a root-type check (#30) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit stripHtmlEmbedNodes only filtered children, so a (never-in-practice) bare htmlEmbed root node would be returned as-is. Add a defensive root check that returns an embed-free doc, making the helper total — it can never return a node for which hasHtmlEmbedNode is true. Adds a unit test for the root case. Co-Authored-By: Claude Opus 4.8 --- .../src/common/helpers/prosemirror/html-embed.spec.ts | 11 +++++++++++ .../src/common/helpers/prosemirror/html-embed.util.ts | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/apps/server/src/common/helpers/prosemirror/html-embed.spec.ts b/apps/server/src/common/helpers/prosemirror/html-embed.spec.ts index 6b07ec0b..b48e9e73 100644 --- a/apps/server/src/common/helpers/prosemirror/html-embed.spec.ts +++ b/apps/server/src/common/helpers/prosemirror/html-embed.spec.ts @@ -92,6 +92,17 @@ describe('stripHtmlEmbedNodes', () => { const result = stripHtmlEmbedNodes(doc); expect(result).toEqual(doc); }); + + it('neutralizes a root node that is itself an htmlEmbed', () => { + // Defensive: the PM root is always a `doc`, so this is unreachable in normal + // use, but the helper must still never return a bare htmlEmbed. + const root = { + type: 'htmlEmbed', + attrs: { source: '' }, + }; + const result = stripHtmlEmbedNodes(root); + expect(hasHtmlEmbedNode(result)).toBe(false); + }); }); describe('canAuthorHtmlEmbed', () => { diff --git a/apps/server/src/common/helpers/prosemirror/html-embed.util.ts b/apps/server/src/common/helpers/prosemirror/html-embed.util.ts index f1d0b6e5..aa5d579d 100644 --- a/apps/server/src/common/helpers/prosemirror/html-embed.util.ts +++ b/apps/server/src/common/helpers/prosemirror/html-embed.util.ts @@ -22,6 +22,15 @@ export function stripHtmlEmbedNodes(pmJson: T): T { const node = pmJson as unknown as JSONContent; + // Defensive root-type check: if the ROOT node is itself an htmlEmbed, the + // children-filtering below could never drop it, so a bare htmlEmbed would be + // returned as-is. This branch is unreachable in normal use (the PM document + // root is always a `doc`) and exists only to make the helper total — a bare + // htmlEmbed can never be returned by this function. + if (node.type === HTML_EMBED_NODE_NAME) { + return { type: 'doc', content: [] } as unknown as T; + } + if (Array.isArray(node.content)) { const filtered: JSONContent[] = []; for (const child of node.content) {