From 42b91f765db6cf12e824828ef87a2f63453d249f Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Sat, 5 Sep 2026 13:24:26 +0530 Subject: [PATCH] fix(email-renderer): preserve fallbacks when AMP validation is unavailable --- packages/email-renderer/package.json | 2 +- packages/email-renderer/src/email.ts | 20 ++++++++++++++++++-- packages/email-renderer/test/email.test.ts | 11 +++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/email-renderer/package.json b/packages/email-renderer/package.json index 9f3f2712..a79b7160 100644 --- a/packages/email-renderer/package.json +++ b/packages/email-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/email-renderer", - "version": "0.1.18", + "version": "0.1.19", "type": "module", "description": "WRNexus block email renderers with HTML, text, AMP Email, and MJML interoperability.", "exports": { diff --git a/packages/email-renderer/src/email.ts b/packages/email-renderer/src/email.ts index a26a86b4..1efa121e 100644 --- a/packages/email-renderer/src/email.ts +++ b/packages/email-renderer/src/email.ts @@ -110,6 +110,9 @@ export async function renderEmail( variables?: Record; requireVariables?: boolean; countdownImageUrl?: string; + /** Trusted host injection; never sourced from the editable document. */ + ampValidator?: (content: string) => Promise; + ampValidationTimeoutMs?: number; } = {}, ): Promise> { const personalized = resolveDocumentVariables(document, emailKind.variables, options.variables, { @@ -164,10 +167,23 @@ export async function renderEmail( const amp = await renderTree(source, emailKind, registry, { ...base, target: "ampEmail", data: options.variables }); if (amp.ok) { const content = ampHead(source, amp.value.content, base.direction); - const validation = await validateAmpEmail(content); + let timer: ReturnType | undefined; + let validation: ValidationReport; + const requestedTimeout = options.ampValidationTimeoutMs ?? 2000; + const timeout = Number.isFinite(requestedTimeout) ? Math.min(10000, Math.max(10, requestedTimeout)) : 2000; + try { + validation = await Promise.race([ + Promise.resolve().then(() => (options.ampValidator ?? validateAmpEmail)(content)), + new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("AMP validation timed out.")), timeout); }), + ]); + } catch { + validation = { valid: false, diagnostics: [{code:"amp.validatorUnavailable",message:"AMP validation was unavailable or timed out. HTML and plain text remain available.",severity:"warning"}] }; + } finally { + if (timer !== undefined) clearTimeout(timer); + } if (validation.valid) bundle.parts.amp = { contentType: "text/x-amp-html; charset=utf-8", content, validation }; - else warnings.push(...validation.diagnostics); + else warnings.push(...validation.diagnostics.map(diagnostic => ({...diagnostic,severity:"warning" as const}))); } else warnings.push(...amp.errors.map((error) => ({ ...error, severity: "warning" as const }))); } diff --git a/packages/email-renderer/test/email.test.ts b/packages/email-renderer/test/email.test.ts index dcfe5790..0f756877 100644 --- a/packages/email-renderer/test/email.test.ts +++ b/packages/email-renderer/test/email.test.ts @@ -57,6 +57,17 @@ const document: BlockDocument = { }; describe("email renderer", () => { + test("optional AMP validation failures preserve HTML/text fallbacks", async () => { + for (const ampValidator of [async () => {throw new Error("network unavailable")}, () => new Promise(() => {})]) { + const result=await renderEmail(document,{amp:true,ampValidator,ampValidationTimeoutMs:10}); + expect(result.ok).toBe(true); + if(!result.ok)continue; + expect(result.value.parts.amp).toBeUndefined(); + expect(result.value.parts.html.content).toContain("Hello <world>"); + expect(result.value.parts.text.content).toContain("Hello "); + expect(result.value.warnings.some(item=>item.code==="amp.validatorUnavailable"&&item.severity==="warning")).toBe(true); + } + }); test("builder column rows export without nesting mj-column inside mj-column", async () => { for (const count of [1,2,3,4]) { const draft=structuredClone(document);