fix: render PDFs with pdf.js instead of a native iframe - #2
Conversation
EmbeddedPDF.svelte used a native iframe pointed at the file URL, which the browser leaves blank when the file server sends X-Frame-Options: SAMEORIGIN (the desktop app runs its UI on a local origin and fetches files cross-origin) and which does not display PDFs at all in Safari/WebKit. Render with pdf.js (canvas) instead, which is immune to X-Frame-Options, CSP frame-ancestors, and per-browser native PDF viewer differences. - EmbeddedHTML.svelte no longer delegates to EmbeddedPDF; it owns its own native iframe plus css-injection implementation (used only for the DOCX-converted-to-HTML preview, which is same-origin blob content and unaffected by the PDF cross-origin issue). - EmbeddedPDF.svelte now loads documents via pdfjs-dist, with withCredentials true (cookie auth, no token in the URL), cMaps and standard fonts for CJK and non-embedded standard-font rendering, loading and error states, and full cleanup (cancel render tasks, destroy the document/loading task, terminate the worker) on unmount and on src change. The css prop, only ever needed by EmbeddedHTML, is removed from its public API. - The pdf.js worker follows the same resilience pattern already used for the recorder worker: primary construction via new Worker with a new URL built from import.meta.url, falling back to a fixed path emitted by an explicit webpack entry if that construction throws. desktop/webpack.config.js gains that entry plus CopyPlugin patterns for pdfjs-dist's cmaps and standard_fonts directories; the plain web build config gains the same cmaps/standard_fonts copying. - Added pdfjs-dist 4.10.38 to packages/ui. services/rekoni already vendors an older pdfjs-dist server-side for unrelated PDF text extraction, so two majors coexisting in the repo is expected. - Added a colocated unit test for the extractable render-scale helper. The worker construction stays inline in the component, matching the pre-existing recorder worker, since import.meta.url is ESM-only syntax that this package's ts-jest/CommonJS test transform cannot parse. Fixes Platform-Collective#1
Follow-up to the pdf.js EmbeddedPDF rewrite, resolving CI and review findings. - rush check: allow pdfjs-dist's two versions (rekoni's 2.12.313 and the new 4.10.38 in packages/ui) via allowedAlternativeVersions in common-versions.json. - Cross-origin auth: drop withCredentials (it maps to credentials:'include', which the file server rejects cross-origin — it responds with Access-Control-Allow-Origin: * and no Access-Control-Allow-Credentials). Instead append the client token as a ?token= query parameter (the server accepts it, and a plain non-credentialed request passes CORS). The token is read from presentation.metadata.Token by the two callers and passed via a new EmbeddedPDF `token` prop; appendToken() adds it with the right separator. - Asset paths: resolve cmaps/, standard_fonts/ and the worker fallback from the webpack public path (getAssetBase) instead of document.baseURI/route-relative paths, which 404 under the app's path-based pushState routing. - Text layer: render a pdf.js TextLayer over each page canvas so text is selectable, searchable (Ctrl+F) and screen-reader accessible. - Rescale vs refetch: split the reactive flow so the document is fetched/parsed only when src/token change; toggling `fit` re-paints existing pages at the new scale without refetching. - Pages render at their exact CSS size and scroll when wider than the viewport (with `align-items: safe center`) rather than being scaled down, which would distort the page and misalign the text layer. - Suppress the load-error console log for renders superseded by a newer generation (no console spam on rapid navigation). - Remove the leftover #view=FitH&navpanes=0 hash from EmbeddedHTML's iframe (it now loads HTML, not a PDF), and drop the now-unused css prop from presentation's PDFViewer.
- Disable the naming-convention rule for the `__webpack_public_path__` ambient declaration (webpack magic global, name is fixed by webpack). - Apply prettier line-wrap and prefer-const auto-fixes flagged by `rush fast-format`.
|
|
||
| // rekoni vendors an older pdfjs-dist server-side for PDF text extraction, | ||
| // while packages/ui uses a modern build for browser canvas rendering. | ||
| "pdfjs-dist": ["2.12.313", "4.10.38"] |
There was a problem hiding this comment.
It was not updated for a while yep.
|
Actually self hosted versions are path based and use same origin, it was a Huly global deployment used separate services. Also datalake in case of path based hosting support cookie based auth, so potentially token will not be required. @aonnikov may comment. |
|
btw, I am running this on prem right now. |
Backend blockers found during review of the office/video port: - rpc.ts (#1 broken access control, #2 modifiedBy spoofing): the six direct /api/v1/{create,addCollection,update,createMixin,updateMixin, remove} endpoints build their client via wrapPipeline(...,true), which runs as the system account and bypasses space/role/read-only checks. Gate them to system/service accounts (the only legitimate caller, e.g. the love service which connects with a system token). Regular clients must use the permission-checked /api/v1/tx path. This also stops non-system callers from spoofing modifiedBy. - guests.ts (#3 guest-token secret): sign/verify the guest platform token with the platform SECRET instead of the LiveKit API secret, so it validates against the transactor without conflating the two secrets. - transient.ts (#4 TransientMiddleware): clear the ttlChecker interval in a new close() override (timer leak); detect removes via tx._class (was tx.objectClass, never TxRemoveDoc, so removes re-added the object with a fresh TTL); delete expired entries from ttlObjectMap in checkTTL (map grew unbounded and re-broadcast every second). - FloorPreview.svelte (#9 committed debug scaffolding): remove the dev-only Debug/Clear-AI-Bot/Search-Accounts menu actions and their functions, which hardcoded personal emails and logged PII. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BZnVtCWH1hTH7EisaBdT7T
Backend blockers found during review of the office/video port: - rpc.ts (#1 broken access control, #2 modifiedBy spoofing): the six direct /api/v1/{create,addCollection,update,createMixin,updateMixin, remove} endpoints build their client via wrapPipeline(...,true), which runs as the system account and bypasses space/role/read-only checks. Gate them to system/service accounts (the only legitimate caller, e.g. the love service which connects with a system token). Regular clients must use the permission-checked /api/v1/tx path. This also stops non-system callers from spoofing modifiedBy. - guests.ts (#3 guest-token secret): sign/verify the guest platform token with the platform SECRET instead of the LiveKit API secret, so it validates against the transactor without conflating the two secrets. - transient.ts (#4 TransientMiddleware): clear the ttlChecker interval in a new close() override (timer leak); detect removes via tx._class (was tx.objectClass, never TxRemoveDoc, so removes re-added the object with a fresh TTL); delete expired entries from ttlObjectMap in checkTTL (map grew unbounded and re-broadcast every second). - FloorPreview.svelte (#9 committed debug scaffolding): remove the dev-only Debug/Clear-AI-Bot/Search-Accounts menu actions and their functions, which hardcoded personal emails and logged PII. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BZnVtCWH1hTH7EisaBdT7T Signed-off-by: Artem Savchenko <armisav@gmail.com>
Backend blockers found during review of the office/video port: - rpc.ts (#1 broken access control, #2 modifiedBy spoofing): the six direct /api/v1/{create,addCollection,update,createMixin,updateMixin, remove} endpoints build their client via wrapPipeline(...,true), which runs as the system account and bypasses space/role/read-only checks. Gate them to system/service accounts (the only legitimate caller, e.g. the love service which connects with a system token). Regular clients must use the permission-checked /api/v1/tx path. This also stops non-system callers from spoofing modifiedBy. - guests.ts (#3 guest-token secret): sign/verify the guest platform token with the platform SECRET instead of the LiveKit API secret, so it validates against the transactor without conflating the two secrets. - transient.ts (#4 TransientMiddleware): clear the ttlChecker interval in a new close() override (timer leak); detect removes via tx._class (was tx.objectClass, never TxRemoveDoc, so removes re-added the object with a fresh TTL); delete expired entries from ttlObjectMap in checkTTL (map grew unbounded and re-broadcast every second). - FloorPreview.svelte (#9 committed debug scaffolding): remove the dev-only Debug/Clear-AI-Bot/Search-Accounts menu actions and their functions, which hardcoded personal emails and logged PII. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BZnVtCWH1hTH7EisaBdT7T Signed-off-by: Artem Savchenko <armisav@gmail.com>
|
@haiodo you are right about the browser case, and I should have said so in July instead of the one-liner I left. Apologies for the long silence on my own PR. Path-based self-hosting is same-origin, and if datalake accepts the session cookie there, the The case it does not cover is the desktop app. The desktop front is loaded from disk — Proposal, if agreeable: derive the file URL's origin and only append the token when it differs from Two corrections to my own PR description while I am here, because both were stated more strongly than what I actually verified:
Still current as of today: the branch is @aonnikov you were tagged on 2026-07-10 and I do not think this reached you. The one question that decides the shape: does pod-datalake accept cookie auth for a path-based self-host today, or is that a "should work" rather than a "does work"? Tell me whether you want the origin-gated token or the token removed outright and I will turn it around this week. |
|
@shanzez I somehow overlooked this question. For files preview there was a workaround implemented, instead of embedding PDF documents via URL we download them and embed via blob URL. |
Summary
Fixes #1.
EmbeddedPDF.svelterendered PDFs with a native<iframe src={fileUrl}>. That is cross-origin-fragile: the browser refuses to render the framed content when the file server sendsX-Frame-Options: SAMEORIGIN(this breaks the desktop app, whose UI runs on a local origin and fetches files from a separate origin), and it renders blank in Safari/WebKit, which does not support PDF-in-iframe at all.This PR switches
EmbeddedPDFto render with pdf.js (fetch bytes into a<canvas>with a real text layer), which is immune toX-Frame-Options/ CSPframe-ancestorsand to per-browser native-PDF-viewer differences.What changed
EmbeddedHTML.svelte— previously delegated toEmbeddedPDF(passing ablob:URL of converted HTML + acssstring injected into the iframe<head>, which is how DOCX-to-HTML preview works). Decoupled first:EmbeddedHTMLnow owns its own small native-iframe implementation (same behavior). HTML content is same-originblob:, so an iframe is correct there; this freesEmbeddedPDFof thecssprop.EmbeddedPDF.svelte— rewritten to render viapdfjs-dist:Access-Control-Allow-Origin: *with noAccess-Control-Allow-Credentials, so a credentialed (withCredentials) fetch is CORS-blocked. Instead the client token (presentation.metadata.Token) is appended as?token=(server-supported), which a plain non-credentialed cross-origin request can carry. Newtokenprop, read by both callers, appended via the pure, unit-testedappendToken().getAssetBase()(webpack's__webpack_public_path__), notdocument.baseURI/route-relative paths — the app uses path-based pushState routing, under which those would 404 off the root. cmaps/standard_fonts are emitted at the bundle root viaCopyPlugin(desktop + web webpack configs).TextLayer+--scale-factor).fit-aware scaling, loading + error states (with a download fallback), and full cleanup (cancel render tasks, destroy the document/loading task, terminate thefromPortworker) on unmount /srcchange. Wide pages scroll rather than being scaled down (which would distort the page and misalign the text layer). Fetch/parse runs only onsrc/tokenchange;fittoggles repaint without refetching.new Worker(new URL(..., import.meta.url))) with a try/catch fallback plus an explicitpdfjs-workerwebpack entry indesktop/webpack.config.js.common-versions.json—pdfjs-distadded toallowedAlternativeVersions(services/rekonialready vendors2.12.313server-side; the UI uses4.10.38).rush checkpasses.Verification
rush check,rush validate,rush svelte-check,rush fast-format, andpackages/uitests pass; full desktop UI webpack build emits the worker chunk +cmaps/+standard_fonts/.cors()defaults +?token=auth +X-Frame-Options: SAMEORIGIN. Credentialed cross-origin fetch is blocked; token-in-URL fetch is 200; pdf.js renders to canvas with a working text layer; the native iframe is blank.Notes
services/rekoni's older server-sidepdfjs-distis unrelated (PDF text extraction), hence the allowlist entry rather than a forced single version.Credit: this fix was put together by the Exaviz team.