From 56776934c2ddfbad4884b252da6f5fb25864b8db Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 9 Sep 2026 15:48:53 +0200 Subject: fix: the PDF preview needs object-src and frame-src, in both policies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A PDF preview showed the "this browser will not display the PDF inline" fallback everywhere — in the desktop client since its first launch, and in the browser since the hub started sending a CSP on 2026-09-01. It read as a missing native feature because before that commit the hub sent no policy at all, so Chrome had once worked and the application never had. Two directives govern one feature. `files-app.js` decrypts the file in the page and hands it to `` from a Blob; Chromium loads that as plugin data (`object-src`, absent and therefore falling back to `default-src 'none'`) and then renders it in an internal frame (`frame-src`). Opening either alone changes nothing visible — the second refusal produces the same fallback. `'self'` covers neither: a same-origin `blob:` URL is not matched by it in either directive, measured in Chrome 152 against the deployed page and in Electron 44 against the client's own policy. `plugins` stays at its default `false`: the built-in viewer is not behind that flag on Electron 44, verified by rendering one. Widening `object-src` from `'none'` to `blob:` admits only what page script minted itself, at a type this code sets — PDFium parsing bytes that came from a node, which is what any browser does with the same file once downloaded. Tests: each policy is pinned to carry `blob:` in both directives (each fails if either token is removed), and the two policies are now held identical directive by directive apart from the two deliberate differences — the comment claiming they were the same had already drifted and nothing checked it. The CSP source parser in test_desktop_shell.py read `//` comment lines as directives, which is the "parse directives, not text" mistake this file already records; it skips them now. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XauykfBvRrpy6RYbF6F7Wu --- packages/meshbay-hub/src/meshbay_hub/api/webapp.py | 32 +++++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) (limited to 'packages/meshbay-hub/src') diff --git a/packages/meshbay-hub/src/meshbay_hub/api/webapp.py b/packages/meshbay-hub/src/meshbay_hub/api/webapp.py index 96b93bb..0d9d1f8 100644 --- a/packages/meshbay-hub/src/meshbay_hub/api/webapp.py +++ b/packages/meshbay-hub/src/meshbay_hub/api/webapp.py @@ -93,9 +93,13 @@ _NO_STORE = {"Cache-Control": "no-store"} # Content-Security-Policy for the whole hub, applied by a middleware in app.py. # -# This is the *same* policy the desktop client's protocol handler already sends -# for these exact UI files (`meshbay-client/src/main.js`), plus the two reCAPTCHA -# hosts the sign-up widget loads its script, challenge iframe and images from. +# This is the desktop client's policy for these exact UI files +# (`meshbay-client/src/main.js`), which serves the same interface, and the two +# have to be changed together — a directive added here and not there breaks the +# app, and the reverse leaves the browser behind. They differ in two places, on +# purpose: `frame-ancestors` is `'self'` here and `'none'` there (nothing frames +# an `app://` page), and `frame-src` carries `'self'` here for the streamed +# download's hidden iframe, which the client has no service worker for. # `'unsafe-inline'` is style-only — htm/preact set inline `style=` attributes # everywhere; nothing inline executes, and the shell below carries no inline # `