Skip to content

fix: preview PDFs via blob URL instead of a remote iframe - #2

Open
shanzez wants to merge 1 commit into
Platform-Collective:developfrom
shanzez:fix/embedded-pdf-pdfjs
Open

fix: preview PDFs via blob URL instead of a remote iframe#2
shanzez wants to merge 1 commit into
Platform-Collective:developfrom
shanzez:fix/embedded-pdf-pdfjs

Conversation

@shanzez

@shanzez shanzez commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #1.

EmbeddedPDF iframes getFileUrl(...). That is blank when the file server is cross-origin (desktop loadFile origin, cloud dl.huly.app vs huly.app): the session cookie is not sent, and X-Frame-Options: SAMEORIGIN (or our nginx equivalent) refuses the frame.

This follows the existing files-preview workaround: download the file with Authorization: Bearer (datalake already accepts it; cors() default allows the preflight) and iframe a same-origin blob: URL. Tokens stay out of the URL. pdf.js is not required.

Same-origin self-host still works: the fetch is same-origin, so cookies go with it.

What changed

  • EmbeddedPDF.svelte — fetch the remote file (Bearer token when the caller has one), createObjectURL, iframe that. blob: / data: srcs are used as-is so HTML preview does not re-download. Aborts and revokes on src change / unmount.
  • PDF viewers — pass presentation.metadata.Token.
  • EmbeddedHTML / DOCX preview — same path, so converted HTML also authenticates when the file is on datalake.

Why not ?token= or pdf.js

  • Datalake already reads Authorization and cookies (extractToken). Putting the token in the URL is worse and unnecessary.
  • Cookie Set-Cookie on the datalake domain was never implemented; JS document.cookie cannot set it cross-origin.
  • pdf.js would work, but it is a large dependency for a problem the blob-URL workaround already solves.

Credit: Exaviz team.

Comment thread common/config/rush/common-versions.json Outdated

// 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"]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was not updated for a while yep.

@haiodo

haiodo commented Jul 10, 2026

Copy link
Copy Markdown
Member

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.

@shanzez

shanzez commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

btw, I am running this on prem right now.

ArtyomSavchenko pushed a commit that referenced this pull request Jul 21, 2026
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
ArtyomSavchenko added a commit that referenced this pull request Jul 24, 2026
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>
ArtyomSavchenko added a commit that referenced this pull request Jul 24, 2026
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>
@shanzez

shanzez commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

@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 ?token= query param buys nothing for a browser client. I am happy to drop it for that path.

The case it does not cover is the desktop app. The desktop front is loaded from disk — desktop/src/main/start.ts:282, await mainWindow.loadFile(containerPagePath) — so the renderer origin is local no matter how the server is hosted. Every request to the workspace is cross-origin from there, and a credentialed fetch is additionally blocked because the file server answers Access-Control-Allow-Origin: * with no Access-Control-Allow-Credentials. So cookie auth cannot be the only mechanism for desktop. That is the part of the token argument I would like to keep.

Proposal, if agreeable: derive the file URL's origin and only append the token when it differs from location.origin. Path-based browser deployments then use the cookie and never see a token in a URL; desktop still works. Small change on top of what is here, and I will push it.

Two corrections to my own PR description while I am here, because both were stated more strongly than what I actually verified:

  • The blank PDF I originally chased on our own on-prem instance turned out to be our own nginx sending X-Frame-Options: SAMEORIGIN on the file route. That is a same-origin path-based deployment, so it argues the iframe path is fragile to a proxy header rather than to an origin split. It supports the change, but not for the reason I gave.
  • I claimed WebKit does not render PDF-in-iframe at all. I have not re-tested that on current Safari and I am not going to defend it until I have. Treat the desktop origin and the header fragility as the case for this PR.

Still current as of today: the branch is mergeable/clean against develop, and packages/ui/src/components/EmbeddedPDF.svelte on develop still uses the native iframe, so nothing here has been overtaken.

@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.

@aonnikov

Copy link
Copy Markdown
Contributor

@shanzez I somehow overlooked this question.
pod-datalake is able to read token from both Authorization and Cookies headers. The main issue was with passing the token via cookies. As long as it was different origin (dl.huly.app vs huly.app), we would need to have cookies set somehow for datalake domain. One of ideas was to call an API method on pod-datalake API to let it set the cookie, but we never implemented this. Current implementation sets the token from the JS code, but it works only when pod-datalake is on the same origin.

For files preview there was a workaround implemented, instead of embedding PDF documents via URL we download them and embed via blob URL.

Iframing getFileUrl fails when the file server is cross-origin
(desktop loadFile, cloud dl.huly.app vs huly.app): cookies are not
sent, and X-Frame-Options: SAMEORIGIN blanks the frame.

Download the file with Authorization: Bearer (datalake already
accepts this; default cors() allows the preflight) and iframe a
blob: URL — the same workaround already used for HTML preview.
Same-origin self-host still works via cookies on the fetch.

Tokens stay out of the URL. pdf.js is not required.

Fixes Platform-Collective#1
@shanzez
shanzez force-pushed the fix/embedded-pdf-pdfjs branch from 2f6d7e4 to c1c28a8 Compare August 18, 2026 19:34
@shanzez shanzez changed the title fix: render PDFs with pdf.js instead of a native iframe fix: preview PDFs via blob URL instead of a remote iframe Aug 18, 2026
@shanzez

shanzez commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

@aonnikov thanks — that last sentence was the steer.

We dropped pdf.js and ?token=. EmbeddedPDF now does the existing files-preview workaround: fetch with Authorization: Bearer (datalake already reads it from that header; default cors() allows the preflight; we do not send credentials, so Access-Control-Allow-Origin: * is fine) and iframe a blob: URL. Same-origin self-host still sends cookies on that fetch. Desktop still works because the renderer can fetch with Authorization even though loadFile makes the page origin local.

Pushed over this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Desktop app: PDF/image preview breaks when the file server sends X-Frame-Options (native-iframe embed is cross-origin-fragile)

3 participants