From ac002b762a06c8eb0207c6c4a8c42793812b5a25 Mon Sep 17 00:00:00 2001 From: Josh McLeod Date: Wed, 25 Feb 2026 14:39:18 -0500 Subject: [PATCH 1/6] fix(pdf-viewer): bundle legacy pdfjs-dist build to resolve WASM issues Bundles the legacy build of pdfjs-dist which includes JS fallbacks for environments with strict WASM policies or lacking top-level await support. This resolves compatibility issues with consumers using older bundlers or strict CSPs. --- vite.config.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/vite.config.js b/vite.config.js index 05515ee..7d1eb16 100644 --- a/vite.config.js +++ b/vite.config.js @@ -15,7 +15,7 @@ export default defineConfig({ formats: ['es'] }, rollupOptions: { - external: [/^lit/, /^@lit/, 'pdfjs-dist'], + external: [/^lit/, /^@lit/], output: { preserveModules: true, preserveModulesRoot: 'src', @@ -30,6 +30,12 @@ export default defineConfig({ optimizeDeps: { exclude: ['pdfjs-dist'] }, + resolve: { + alias: [ + { find: /^pdfjs-dist\/build\/pdf\.worker\.mjs/, replacement: 'pdfjs-dist/legacy/build/pdf.worker.mjs' }, + { find: /^pdfjs-dist$/, replacement: 'pdfjs-dist/legacy/build/pdf.mjs' } + ] + }, server: { fs: { strict: false From 355d3c0decd6f57c4e1f1159e955b8becf8a4d40 Mon Sep 17 00:00:00 2001 From: Josh McLeod Date: Wed, 25 Feb 2026 16:46:53 -0500 Subject: [PATCH 2/6] fix(pdf-viewer): set wasmUrl to CDN default to resolve WASM path errors When pdfjs-dist runs its worker as a data URI, WASM files cannot be found via relative paths (import.meta.url resolves to the data URI). Providing wasmUrl to getDocument() gives the worker an absolute URL to fetch WASM from. Defaults to unpkg.com using the bundled pdfjs version so no consumer configuration is needed. Consumers can override with the wasm-url attribute if needed (e.g. for offline environments or self-hosted WASM). --- src/components/pdf-viewer/pdf-viewer.js | 6 +++++- test/components/pdf-viewer.test.js | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/pdf-viewer/pdf-viewer.js b/src/components/pdf-viewer/pdf-viewer.js index f29b119..6185c0f 100644 --- a/src/components/pdf-viewer/pdf-viewer.js +++ b/src/components/pdf-viewer/pdf-viewer.js @@ -2,6 +2,8 @@ import { html } from 'lit' import { ContextProvider } from '@lit/context' import * as pdfjsLib from 'pdfjs-dist' import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.mjs?url' + +const DEFAULT_WASM_URL = `https://unpkg.com/pdfjs-dist@${pdfjsLib.version}/wasm/` import styles from './pdf-viewer.styles.js' import { pdfContext } from './pdf-context.js' import { createThemeStyleSheet } from './theme-config.js' @@ -18,6 +20,7 @@ export default class PDFViewer extends RoleModelElement { static get properties() { return { src: { type: String }, + wasmUrl: { type: String, attribute: 'wasm-url' }, open: { type: Boolean, reflect: true }, initialPage: { type: Number, attribute: 'initial-page' }, themeHue: { type: Number, attribute: 'theme-hue' }, @@ -46,6 +49,7 @@ export default class PDFViewer extends RoleModelElement { constructor() { super() this.src = '' + this.wasmUrl = DEFAULT_WASM_URL this.open = false this.initialPage = 1 this.themeHue = 217 @@ -344,7 +348,7 @@ export default class PDFViewer extends RoleModelElement { this.pdfDoc = null this.loading = true try { - const loadingTask = pdfjsLib.getDocument(this.src) + const loadingTask = pdfjsLib.getDocument({ url: this.src, wasmUrl: this.wasmUrl }) this.pdfDoc = await loadingTask.promise this.totalPages = this.pdfDoc.numPages diff --git a/test/components/pdf-viewer.test.js b/test/components/pdf-viewer.test.js index d3e0056..efc1b8e 100644 --- a/test/components/pdf-viewer.test.js +++ b/test/components/pdf-viewer.test.js @@ -72,7 +72,7 @@ describe('PDFViewer Component', () => { await waitForCondition(() => element.pdfDoc !== null) - expect(pdfjsLib.getDocument).toHaveBeenCalledWith('/test.pdf') + expect(pdfjsLib.getDocument).toHaveBeenCalledWith(expect.objectContaining({ url: '/test.pdf' })) expect(element.pdfDoc).toBeDefined() expect(element.totalPages).toBe(5) }) From 22de273df2c43d901c3e3f07ec804515ea040196 Mon Sep 17 00:00:00 2001 From: Josh McLeod Date: Wed, 25 Feb 2026 17:06:32 -0500 Subject: [PATCH 3/6] fix(pdf-viewer): bundle WASM locally via LocalWasmFactory, remove CDN default Replace the DEFAULT_WASM_URL (unpkg.com CDN) with a LocalWasmFactory that uses ?url imports to bundle jbig2.wasm, openjpeg.wasm, and qcms_bg.wasm directly into the dist output. This eliminates the supply-chain risk of loading unverified WASM from an external CDN. - wasmUrl now defaults to null (bundled WASM used automatically) - loadPDF() uses WasmFactory: LocalWasmFactory by default; falls back to the URL-based approach only when wasm-url is explicitly set by a consumer - Document wasm-url in the README attributes table with a security note Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 1 + src/components/pdf-viewer/pdf-viewer.js | 29 ++++++++++++++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 25f634a..afc974d 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ This component aims to be that skin layer built upon PDF.js packaged in a lovely | `theme-hue` | `number` | `217` | Theme hue (`0-360`) | | `theme-saturation` | `number` | `89` | Theme saturation (`0-100`) | | `escape-closes-viewer` | `boolean` | `false` | Closes viewer on `Escape` when search is not open | +| `wasm-url` | `string` | Bundled | Base URL for WASM binaries. Only override with a trusted source. | #### Slot diff --git a/src/components/pdf-viewer/pdf-viewer.js b/src/components/pdf-viewer/pdf-viewer.js index 6185c0f..65c3e5c 100644 --- a/src/components/pdf-viewer/pdf-viewer.js +++ b/src/components/pdf-viewer/pdf-viewer.js @@ -2,8 +2,9 @@ import { html } from 'lit' import { ContextProvider } from '@lit/context' import * as pdfjsLib from 'pdfjs-dist' import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.mjs?url' - -const DEFAULT_WASM_URL = `https://unpkg.com/pdfjs-dist@${pdfjsLib.version}/wasm/` +import jbig2WasmUrl from 'pdfjs-dist/wasm/jbig2.wasm?url' +import openjpegWasmUrl from 'pdfjs-dist/wasm/openjpeg.wasm?url' +import qcmsWasmUrl from 'pdfjs-dist/wasm/qcms_bg.wasm?url' import styles from './pdf-viewer.styles.js' import { pdfContext } from './pdf-context.js' import { createThemeStyleSheet } from './theme-config.js' @@ -11,9 +12,24 @@ import { normalizeText } from './helpers/text-helper.js' import './toolbar/pdf-toolbar.js' import './sidebar/pdf-sidebar.js' import './canvas/pdf-canvas.js' - import RoleModelElement from '../../internal/rolemodel-element.js' +const BUNDLED_WASM_URLS = { + 'jbig2.wasm': jbig2WasmUrl, + 'openjpeg.wasm': openjpegWasmUrl, + 'qcms_bg.wasm': qcmsWasmUrl, +} + +class LocalWasmFactory { + constructor() {} + async fetch({ filename }) { + const url = BUNDLED_WASM_URLS[filename] + if (!url) throw new Error(`Unknown WASM file: ${filename}`) + const response = await fetch(url) + return new Uint8Array(await response.arrayBuffer()) + } +} + pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker export default class PDFViewer extends RoleModelElement { @@ -49,7 +65,7 @@ export default class PDFViewer extends RoleModelElement { constructor() { super() this.src = '' - this.wasmUrl = DEFAULT_WASM_URL + this.wasmUrl = null this.open = false this.initialPage = 1 this.themeHue = 217 @@ -348,7 +364,10 @@ export default class PDFViewer extends RoleModelElement { this.pdfDoc = null this.loading = true try { - const loadingTask = pdfjsLib.getDocument({ url: this.src, wasmUrl: this.wasmUrl }) + const loadingTask = pdfjsLib.getDocument({ + url: this.src, + ...(this.wasmUrl ? { wasmUrl: this.wasmUrl } : { WasmFactory: LocalWasmFactory }) + }) this.pdfDoc = await loadingTask.promise this.totalPages = this.pdfDoc.numPages From 0c164057d02aaaa1240ffc7c8494b79994832443 Mon Sep 17 00:00:00 2001 From: Josh McLeod Date: Wed, 25 Feb 2026 17:09:23 -0500 Subject: [PATCH 4/6] fix(pdf-viewer): remove wasm-url attribute to eliminate untrusted WASM loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the wasm-url attribute/property entirely. LocalWasmFactory is sufficient for all use cases — there is no need for a consumer-facing URL override that would allow arbitrary WASM to be loaded without integrity checks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 1 - src/components/pdf-viewer/pdf-viewer.js | 7 +------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/README.md b/README.md index afc974d..25f634a 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,6 @@ This component aims to be that skin layer built upon PDF.js packaged in a lovely | `theme-hue` | `number` | `217` | Theme hue (`0-360`) | | `theme-saturation` | `number` | `89` | Theme saturation (`0-100`) | | `escape-closes-viewer` | `boolean` | `false` | Closes viewer on `Escape` when search is not open | -| `wasm-url` | `string` | Bundled | Base URL for WASM binaries. Only override with a trusted source. | #### Slot diff --git a/src/components/pdf-viewer/pdf-viewer.js b/src/components/pdf-viewer/pdf-viewer.js index 65c3e5c..625870c 100644 --- a/src/components/pdf-viewer/pdf-viewer.js +++ b/src/components/pdf-viewer/pdf-viewer.js @@ -36,7 +36,6 @@ export default class PDFViewer extends RoleModelElement { static get properties() { return { src: { type: String }, - wasmUrl: { type: String, attribute: 'wasm-url' }, open: { type: Boolean, reflect: true }, initialPage: { type: Number, attribute: 'initial-page' }, themeHue: { type: Number, attribute: 'theme-hue' }, @@ -65,7 +64,6 @@ export default class PDFViewer extends RoleModelElement { constructor() { super() this.src = '' - this.wasmUrl = null this.open = false this.initialPage = 1 this.themeHue = 217 @@ -364,10 +362,7 @@ export default class PDFViewer extends RoleModelElement { this.pdfDoc = null this.loading = true try { - const loadingTask = pdfjsLib.getDocument({ - url: this.src, - ...(this.wasmUrl ? { wasmUrl: this.wasmUrl } : { WasmFactory: LocalWasmFactory }) - }) + const loadingTask = pdfjsLib.getDocument({ url: this.src, WasmFactory: LocalWasmFactory }) this.pdfDoc = await loadingTask.promise this.totalPages = this.pdfDoc.numPages From d3aa5b3b47eaf0d1ba84b924436e2bdfb8b0d42c Mon Sep 17 00:00:00 2001 From: Josh McLeod Date: Wed, 25 Feb 2026 17:10:50 -0500 Subject: [PATCH 5/6] fix(pdf-viewer): validate HTTP response status in LocalWasmFactory Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/components/pdf-viewer/pdf-viewer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/pdf-viewer/pdf-viewer.js b/src/components/pdf-viewer/pdf-viewer.js index 625870c..1c04168 100644 --- a/src/components/pdf-viewer/pdf-viewer.js +++ b/src/components/pdf-viewer/pdf-viewer.js @@ -26,6 +26,7 @@ class LocalWasmFactory { const url = BUNDLED_WASM_URLS[filename] if (!url) throw new Error(`Unknown WASM file: ${filename}`) const response = await fetch(url) + if (!response.ok) throw new Error(`Failed to fetch ${filename}: ${response.status} ${response.statusText}`) return new Uint8Array(await response.arrayBuffer()) } } From b5ebe3011c43d2560032a20f94d4adcfee518bfd Mon Sep 17 00:00:00 2001 From: Josh McLeod Date: Fri, 27 Feb 2026 10:33:43 -0500 Subject: [PATCH 6/6] Revert vite config change --- vite.config.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/vite.config.js b/vite.config.js index 7d1eb16..05515ee 100644 --- a/vite.config.js +++ b/vite.config.js @@ -15,7 +15,7 @@ export default defineConfig({ formats: ['es'] }, rollupOptions: { - external: [/^lit/, /^@lit/], + external: [/^lit/, /^@lit/, 'pdfjs-dist'], output: { preserveModules: true, preserveModulesRoot: 'src', @@ -30,12 +30,6 @@ export default defineConfig({ optimizeDeps: { exclude: ['pdfjs-dist'] }, - resolve: { - alias: [ - { find: /^pdfjs-dist\/build\/pdf\.worker\.mjs/, replacement: 'pdfjs-dist/legacy/build/pdf.worker.mjs' }, - { find: /^pdfjs-dist$/, replacement: 'pdfjs-dist/legacy/build/pdf.mjs' } - ] - }, server: { fs: { strict: false