fix(email-renderer): validate basic media and spacer dimensions
Quality / quality (ubuntu-latest) (push) Failing after 0s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
Clintchiz committed 2026-09-05 15:05:27 +05:30
1 parent 5e30d6a7d0
commit c1f4f42b21
3 files changed
+46 -10

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/email-renderer",
"version": "0.1.21",
"version": "0.1.22",
"type": "module",
"description": "WRNexus block email renderers with HTML, text, AMP Email, and MJML interoperability.",
"exports": {
+8 -8
View File
@@ -49,13 +49,13 @@ export function ampLayoutCss(node: BlockNode): string {
return "";
}
const multilineText = (value: unknown) => html(value).replace(/\r\n?|\n/g, "<br />");
const imageDimension = (fallback: string): ValueSchema<string> => ({
const imageDimension = (fallback: string, minimum = 1): ValueSchema<string> => ({
kind: "string",
defaultValue: fallback,
parse(value = fallback, path = "value") {
return typeof value === "string" && /^\d{1,4}$/.test(value) && Number(value) > 0 && Number(value) <= 4000
return typeof value === "string" && /^\d{1,4}$/.test(value) && Number(value) >= minimum && Number(value) <= 4000
? { ok: true, value }
: { ok: false, errors: [{code:"email.image-dimension",message:"Image dimensions must be between 1 and 4000 pixels.",severity:"error",path}] };
: { ok: false, errors: [{code:"email.image-dimension",message:`Dimensions must be between ${minimum} and 4000 pixels.`,severity:"error",path}] };
},
});
const marketingImage = (node: BlockNode, target: "html" | "amp" | "mjml") => {
@@ -328,8 +328,8 @@ export const imageBlock = leaf(
object({
src: safeUrl({ schemes: ["https"] }),
alt: string({ max: 1_000, default: "" }),
width: string({ default: "600" }),
height: string({ default: "300" }),
width: imageDimension("600"),
height: imageDimension("300"),
decorative: boolean(false),
}) as never,
{
@@ -391,7 +391,7 @@ export const spacerBlock = leaf(
"content.spacer",
"Spacer",
"icon-[lucide--move-vertical]",
object({ height: string({ default: "24" }) }) as never,
object({ height: imageDimension("24", 0) }) as never,
{
emailHtml: ({ node }) =>
emailCell(
@@ -413,8 +413,8 @@ export const videoBlock = leaf(
href: safeUrl({ schemes: ["https"] }),
thumbnail: safeUrl({ schemes: ["https"] }),
alt: string({ max: 1_000, default: "Video thumbnail" }),
width: string({ default: "600" }),
height: string({ default: "338" }),
width: imageDimension("600"),
height: imageDimension("338"),
}) as never,
{
emailHtml: ({ node }) => emailCell(`<a href="${html(attrs(node).href)}" aria-label="${html(attrs(node).title)}"><img src="${html(attrs(node).thumbnail)}" alt="${html(attrs(node).alt)}" width="${html(attrs(node).width)}" height="${html(attrs(node).height)}" style="border:0;display:block;height:auto;width:100%;max-width:${html(attrs(node).width)}px;" /></a>`, node),
@@ -54,7 +54,7 @@ test("product and RSS images survive HTML, AMP and strict MJML exports", async (
const result = await renderEmail(document(type, props), { amp: true, mjml: true });
expect(result.ok).toBe(true);
if (!result.ok) continue;
expect(result.value.parts.html.content).toContain('width:100%;max-width:320px;');
expect(result.value.parts.html.content).toContain("width:100%;max-width:320px;");
for (const output of [
result.value.parts.html.content,
result.value.parts.amp?.content,
@@ -113,3 +113,39 @@ test("marketing image dimensions reject invalid or injected values", async () =>
expect(result.ok).toBe(false);
}
});
test("basic images, video thumbnails and spacers validate dimensions before rendering", async () => {
const cases: Array<{ type: string; props: Record<string, string> }> = [
{ type: "content.image", props: { src: "https://example.com/image.png", alt: "Image" } },
{
type: "content.video",
props: {
href: "https://example.com/video",
thumbnail: "https://example.com/image.png",
title: "Video",
alt: "Video",
},
},
{ type: "content.spacer", props: {} },
];
for (const block of cases) {
for (const height of ["-1", "4001", "100%", "24;position:fixed", '24" onload="bad', "1e3"]) {
const result = await renderEmail(
document(block.type, { ...block.props, height } as Record<string, string>),
);
expect(result.ok).toBe(false);
}
const valid = await renderEmail(
document(block.type, { ...block.props, height: "4000" } as Record<string, string>),
);
expect(valid.ok).toBe(true);
}
expect((await renderEmail(document("content.spacer", { height: "0" }))).ok).toBe(true);
expect(
(
await renderEmail(
document("content.image", { src: "https://example.com/image.png", height: "0" }),
)
).ok,
).toBe(false);
});