aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-09 15:48:53 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-09 15:48:53 +0200
commit56776934c2ddfbad4884b252da6f5fb25864b8db (patch)
treeb34aa2a9b71a3129c937d9d0cecaa39505afbd6b /packages/meshbay-hub/src
parent78b309d18efdd227c0efa42464988eaaf1483c16 (diff)
downloadmeshbay-56776934c2ddfbad4884b252da6f5fb25864b8db.tar.gz
fix: the PDF preview needs object-src and frame-src, in both policies
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 `<object type="application/pdf">` 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XauykfBvRrpy6RYbF6F7Wu
Diffstat (limited to 'packages/meshbay-hub/src')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/webapp.py32
1 files changed, 28 insertions, 4 deletions
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
# `<script>`. `'wasm-unsafe-eval'` is required for the Argon2id WASM. The hub's
@@ -111,6 +115,26 @@ CSP = "; ".join([
"font-src 'self'",
"connect-src 'self' https: wss:",
"worker-src 'self'",
+ # `object-src` exists for one thing, and `frame-src`'s `blob:` for the same
+ # thing: previewing a PDF without writing it anywhere.
+ #
+ # `files-app.js` decrypts the file in the page, wraps it in a Blob and hands
+ # it to `<object type="application/pdf">`. Chromium renders that with its own
+ # viewer, which needs TWO permissions — the resource is loaded as plugin data
+ # (`object-src`) and then rendered in an internal frame (`frame-src`). This
+ # policy had neither: `object-src` was absent, so it fell back to
+ # `default-src 'none'`, and the fallback message ("this browser will not
+ # display the PDF inline") was shown on every platform from the day this CSP
+ # shipped. It read as a desktop-client limitation because the client has sent
+ # a CSP since its first launch and the hub had none before this file.
+ #
+ # `'self'` does not cover a same-origin `blob:` URL in either directive —
+ # measured, not assumed, in Chrome 152 against the deployed page — so the
+ # token is `blob:` and it is needed in both. Widening `object-src` from
+ # `'none'` 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 it is downloaded.
+ "object-src blob:",
# `'self'` is not decoration: the streamed-download path works by navigating
# a hidden iframe to `/_mbdl/<id>` so the service worker is asked for the
# response it is holding. Without it Chrome refuses the frame, the worker is
@@ -118,7 +142,7 @@ CSP = "; ".join([
# happen — on Firefox and Safari that is the *only* way to write a large
# file to disk, so the whole path was dead. Added when reCAPTCHA needed a
# frame, which is why nobody connected the two.
- f"frame-src 'self' {_RECAPTCHA_SRC}",
+ f"frame-src 'self' blob: {_RECAPTCHA_SRC}",
# `'self'`, not `'none'`, and the difference is one same-origin iframe.
#
# The threat frame-ancestors answers is clickjacking: a *foreign* page