harden(html-embed): make stripHtmlEmbedNodes total with a root-type check (#30)

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 <noreply@anthropic.com>
This commit is contained in:
claude code agent 227
2026-06-20 21:52:32 +03:00
parent 6a052b88b4
commit 8ee4279d30
2 changed files with 20 additions and 0 deletions

View File

@@ -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: '<script>alert(1)</script>' },
};
const result = stripHtmlEmbedNodes(root);
expect(hasHtmlEmbedNode(result)).toBe(false);
});
});
describe('canAuthorHtmlEmbed', () => {

View File

@@ -22,6 +22,15 @@ export function stripHtmlEmbedNodes<T = JSONContent>(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) {