');
expect(html).toContain('This is a sample DOCX file.');
});
- test('strips ` substring inside the binary.
+ *
+ * The CSP locks the page down to the pinned CDN host: scripts only
+ * from jsdelivr (with SRI), no outbound `fetch`/`XHR`, no eval, images
+ * only `self`/`data:`/`blob:` (docx-preview uses `URL.createObjectURL`
+ * for inline images), styles inline (`docx-preview` injects per-doc
+ * styles into `` at render time).
+ */
+function buildDocxCdnDocument(base64: string): string {
+ const csp = [
+ "default-src 'none'",
+ "script-src https://cdn.jsdelivr.net 'unsafe-inline'",
+ "style-src 'unsafe-inline'",
+ "img-src 'self' data: blob:",
+ 'font-src data:',
+ "connect-src 'none'",
+ "base-uri 'none'",
+ "form-action 'none'",
+ ].join('; ');
+ return `
+
+
+
+
+
+Preview
+
+
+
+
+
+
+Preview unavailable. Please download the file to view it.
+
+
+
+`;
+}
+
+async function wordDocToHtmlViaCdn(buffer: Buffer): Promise {
+ return buildDocxCdnDocument(buffer.toString('base64'));
+}
+
+async function wordDocToHtmlViaMammoth(buffer: Buffer): Promise {
const { convertToHtml } = await import('mammoth');
const result = await convertToHtml({ buffer }, { styleMap: DOCX_STYLE_MAP });
const sanitized = await sanitizeOfficeHtml(result.value);
return wrapAsDocument(`${sanitized}`, DOCX_EXTRA_CSS);
}
+/**
+ * Convert a `.docx` buffer to a sandboxed HTML document. Two render
+ * paths, chosen by file size:
+ *
+ * 1. **CDN-rendered (default for files ≤ 350 KB binary)**: embeds the
+ * binary as base64 and lets `docx-preview` render it inside the
+ * Sandpack iframe. High visual fidelity — preserves cell shading,
+ * run-level colors/fonts, headers/footers, columns, and images.
+ * 2. **Mammoth (fallback for larger files)**: server-side semantic
+ * HTML conversion. Lower fidelity (flat paragraphs, no shading)
+ * but produces compact output that fits the
+ * `MAX_TEXT_CACHE_BYTES` (512 KB) cap on `attachment.text` even
+ * for large documents.
+ *
+ * Both paths pre-flight through `assertSafeZipSize` so a zip-bomb DOCX
+ * is rejected before either renderer touches it — mammoth's internal
+ * extraction has no decompressed-size cap and would happily inflate a
+ * sub-1MB compressed bomb to 200+ MB of XML. See SEC review on PR
+ * #12934 for the original DoS finding.
+ */
+export async function wordDocToHtml(buffer: Buffer): Promise {
+ await assertSafeZipSize(buffer, { name: 'docx' });
+ if (buffer.length <= MAX_DOCX_CDN_BINARY_BYTES) {
+ return wordDocToHtmlViaCdn(buffer);
+ }
+ return wordDocToHtmlViaMammoth(buffer);
+}
+
+/**
+ * Test-only re-exports. Each path has distinct visual-fidelity and
+ * size characteristics that warrant direct testing rather than
+ * round-tripping through the size-based dispatcher with synthetically
+ * padded fixtures. Not part of the public API — callers in production
+ * code should always go through `wordDocToHtml`.
+ */
+export const _internal = {
+ wordDocToHtmlViaCdn,
+ wordDocToHtmlViaMammoth,
+ MAX_DOCX_CDN_BINARY_BYTES,
+ DOCX_PREVIEW_CDN,
+};
+
/* =============================================================================
* XLSX / XLS / ODS → HTML (multi-sheet with pure-CSS tab strip)
* ============================================================================= */